前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >汇编角度看函数堆栈调用

汇编角度看函数堆栈调用

作者头像
lexingsen
发布于 2022-02-24 07:36:48
发布于 2022-02-24 07:36:48
72100
代码可运行
举报
文章被收录于专栏:乐行僧的博客乐行僧的博客
运行总次数:0
代码可运行

下面以主函数调用求和函数分析函数堆栈调用

带着以下一个问题来探索: (1)形参的内存空间的开辟和清理是由调用方还是由被调用方执行的? (2)主函数调用函数结束后,主函数从哪里开始执行?从头开始还是从调用之后开始? (3)返回值是如何带出来的?

用于验证的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<srtio.h>

int sum(int a,int b)
{
    int res = 0;
    res = a+b;
    return res;
}

int main()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = sum(a,b);
    printf("ret = %d\n",ret);
    return 0;
}

实验环境:vc++ 6.0 和 Win10操作系统

注意:linux操作系统采用的汇编指令是AT&T,而Windows采用的是intel x86 最简易区分它们的规则是:intel x86从左向右读,而AT&T是从右往左读。 反汇编代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1:    #include<stdio.h>
2:
3:    int sum(int a,int b)
4:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,44h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
5:        int res = 0;
00401038   mov         dword ptr [ebp-4],0
6:        res = a+b;
0040103F   mov         eax,dword ptr [ebp+8]
00401042   add         eax,dword ptr [ebp+0Ch]
00401045   mov         dword ptr [ebp-4],eax
7:        return res;
00401048   mov         eax,dword ptr [ebp-4]
8:    }
0040104B   pop         edi
0040104C   pop         esi
0040104D   pop         ebx
0040104E   mov         esp,ebp
00401050   pop         ebp
00401051   ret 

int main()
12:   {
00401060   push        ebp
00401061   mov         ebp,esp
00401063   sub         esp,4Ch
00401066   push        ebx
00401067   push        esi
00401068   push        edi
00401069   lea         edi,[ebp-4Ch]
0040106C   mov         ecx,13h
00401071   mov         eax,0CCCCCCCCh
00401076   rep stos    dword ptr [edi]
13:       int a = 10;
00401078   mov         dword ptr [ebp-4],0Ah
14:       int b = 20;
0040107F   mov         dword ptr [ebp-8],14h
15:       int ret = 0;
00401086   mov         dword ptr [ebp-0Ch],0
16:       ret = sum(a,b);
0040108D   mov         eax,dword ptr [ebp-8]
00401090   push        eax
00401091   mov         ecx,dword ptr [ebp-4]
00401094   push        ecx
00401095   call        @ILT+0(_sum) (00401005)
0040109A   add         esp,8
0040109D   mov         dword ptr [ebp-0Ch],eax
17:
18:       printf("ret = %d\n",ret);
004010A0   mov         edx,dword ptr [ebp-0Ch]
004010A3   push        edx
004010A4   push        offset string "ret = %d\n" (0042201c)
004010A9   call        printf (004010e0)
004010AE   add         esp,8
19:
20:       return 0;
004010B1   xor         eax,eax
21:   }
004010B3   pop         edi
004010B4   pop         esi
004010B5   pop         ebx
004010B6   add         esp,4Ch
004010B9   cmp         ebp,esp
004010BB   call        __chkesp (00401160)
004010C0   mov         esp,ebp
004010C2   pop         ebp
004010C3   ret

可以看到在主函数和求和函数中首先出现的反汇编代码,我们以求函数举例,其实它们的功能是相同的,就是开辟栈帧

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00401060   push        ebp
00401061   mov         ebp,esp
00401063   sub         esp,4Ch
00401066   push        ebx
00401067   push        esi
00401068   push        edi
00401069   lea         edi,[ebp-4Ch]
0040106C   mov         ecx,13h
00401071   mov         eax,0CCCCCCCCh
00401076   rep stos    dword ptr [edi]

要看懂以上的汇编代码,首先我们必须具备的基础知识是几条简单的汇编指令寄存器的功能和作用以及通常用的几个寄存器。

1.常用的intelx86汇编指令。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[push  寄存器]  功能:将一个寄存器中的数据入栈。包含两个动作:将寄存器中的数据入栈,栈顶指针向上(低地址)偏移。
[pop 寄存器]  功能:出栈,以一个寄存器接受出栈的数据。包含两个动作:将栈中的数据保存在寄存器中,同时栈顶指针向下(高地址)偏移。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[add 寄存器,数据] 如:add ax,8    //相当于ax += 8;
[sub 寄存器,数据] 如:sub bx,4    //相当于bx -= 4;
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
常见的几种mov指令:
[mov 寄存器,寄存器] 如:move ax,8
[mov 寄存器,数据] 如:move ax,bx
[mov 寄存器,内存单元] 如:move ax,[0]
[mov 内存单元,寄存器] 如:move [0],ax
[mov 段寄存器,寄存器] 如:move ds,ax
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
call指令:call指令有两个动作
(1)将下一行指令地址压栈
(2)跳转
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[lea ax,[]]  功能:将有效地址放[]到指定寄存器中。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[rep stos ]
如:
lea     edi,[ebp-0C0h] 
mov     ecx,30h 
mov     eax,0CCCCCCCCh 
rep stos dword ptr [edi]
rep指令的目的是重复其上面的指令。ecx寄存器中的值是重复的次数。
stos指令的作用是将eax寄存器中的值拷贝到[edi]指向的地址。

2.常用的寄存器。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
eax:累加寄存器,它是许多加法乘法指令的缺省寄存器。(缺省即默认defalut)
ebx:基地址寄存器,在内存寻址是存放基地址。
ecx:计数器,是重复前缀指令res和loop指令的内定计数器。
edx:总是被用来存放整数产生的余数。
esp:专门用作堆栈指针,被形象的称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp就越来越小。在32位平台上,esp每次减少4个字节。
ebp:堆栈的栈底指针。
esi/edi:"分别叫做源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中,DS:ESI指向源串,而ES:EDI指向目标串。

具备上边常用的intelx86汇编指令以及常用寄存器的功能。开始分析函数栈帧开辟的过程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00401060   push        ebp
00401061   mov         ebp,esp
00401063   sub         esp,4Ch
00401066   push        ebx
00401067   push        esi
00401068   push        edi
00401069   lea         edi,[ebp-4Ch]
0040106C   mov         ecx,13h
00401071   mov         eax,0CCCCCCCCh
00401076   rep stos    dword ptr [edi]
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1.压栈,并将值保存于ebp寄存器中,即ebp指向该块内存区域。

2.使得esp和ebp指向同一块内存区域,虽然esp和ebp是寄存器,但由于其内保存的是地址,所以在此我们也可以形象的将esp和ebp看做指针,便于理解。

3.sub esp,4Ch,对照上边的汇编指令,这里做的操作是 -= ,即esp = esp-4ch,我们都知道,指针进行加减还是指针。所以栈顶指针向上(低地址)移动76个字节,为什么会移动76个字节。我们可以认为,编译认为主函数栈帧开辟76个字节大小完全足够使用。在这里还需要注意的一点是,虚拟地址空间中栈的生长方向是从高地址到低地址,所以我们看到的是esp-4ch。

4.由于接下来的三条汇编指令   入栈
00401066   push        ebx
00401067   push        esi
00401068   push        edi
与后边的出栈指令呼应,相当于没有入栈,在此不做赘述。
004010B3   pop         edi
004010B4   pop         esi
004010B5   pop         ebx

5.lea edi [ebp-4ch],[ebp-4ch]的地址存放在地址寄存器中。

6.mov ecx,13h
7.mov eax,CCCCCCCCh
8.rep stos dword ptr [edi]
以上三条指令构成循环拷贝指令,循环次数13.拷贝的内容,CCCCCCCCh,即汉字"烫烫"。也就是说开辟栈帧结束后,对其做初始化。这就是为什么当我们访问未初始化内存中的内容,看到的是如下图的情况。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
简单小程序验证一下:
#include<stdio.h>
int main()
{
    int ch;
    putchar(ch);
    return;
}

//执行程序会报错,但可以通过调试查看内存获取内容。

下面将开辟栈帧之后的图展示一下,以便理解:

下面分析栈帧开辟完成之后的汇编指令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
13:       int a = 10;
00401078   mov         dword ptr [ebp-4],0Ah
14:       int b = 20;
0040107F   mov         dword ptr [ebp-8],14h
15:       int ret = 0;
00401086   mov         dword ptr [ebp-0Ch],0
16:       ret = sum(a,b);
0040108D   mov         eax,dword ptr [ebp-8]
00401090   push        eax
00401091   mov         ecx,dword ptr [ebp-4]
00401094   push        ecx
00401095   call        @ILT+0(_sum) (00401005)
0040109A   add         esp,8
0040109D   mov         dword ptr [ebp-0Ch],eax
17:
18:       printf("ret = %d\n",ret);
004010A0   mov         edx,dword ptr [ebp-0Ch]
004010A3   push        edx
004010A4   push        offset string "ret = %d\n" (0042201c)
004010A9   call        printf (004010e0)
004010AE   add         esp,8

纵观上边列出的指令,可以看到。布局变量并没有表现出来,它是通过ebp栈底指针的偏移量来表示的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
13:       int a = 10;
00401078   mov         dword ptr [ebp-4],0Ah
14:       int b = 20;
0040107F   mov         dword ptr [ebp-8],14h
15:       int ret = 0;
00401086   mov         dword ptr [ebp-0Ch],0

1.mov dword ptr [ebp-4],0Ah,dword表示双字,即四字节。即将0Ah放入[ebp-4]指向的四字节内存块中。
2.mov dword ptr [ebp-8],14h,将14h放入[ebp-8]指向的四字节内存块中。
3. mov dword ptr [ebp-0Ch],0,将0放入[ebp-0Ch]指向的四字节内存块中。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
16:       ret = sum(a,b);
0040108D   mov         eax,dword ptr [ebp-8]
00401090   push        eax
00401091   mov         ecx,dword ptr [ebp-4]
00401094   push        ecx
1.[ebp-8]指向内存块中的值存入寄存器eax中,并进行压栈。同时栈顶指针向上偏移(低地址)2.[ebp-4]指向内存块中的值存入寄存器ecx中,并进行压栈。同时栈顶指针向上偏移(低地址)在这两行代码中,可以得到以下的结论:
(1)形参的内存是由调用方开辟的。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00401095   call        @ILT+0(_sum) (00401005)

这条指令是尤为重要的,前面已经讲过call指令有两个动作。
(1)将下一条指令的地址压栈。
(2)跳转

如何确定call指令是否执行了上述的动作,我们使用反汇编代码进行调试。

黄箭头表示此条指令还未执行,那么我们查看此时栈底指针esp的地址,查看内存中的内容。

查看0x0019fee0对应的内存块。

可见上述实参1020已经入栈,并且栈顶指针指向10(0A)所在的内存块。

下面执行call指令我们看看会发生什么? 首先栈顶指针向上偏移(低地址)。

重点:下一条指令的地址被压栈

由于intelx86体系的机器是小端模式,读取0x0019fedc内存块的内容,0040109A,正是call指令下一条指令的地址。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00401095   call        @ILT+0(_sum) (00401005)
0040109A   add         esp,8

跳转到被调用函数中,首先也是开辟栈帧并作初始化,在此不做赘述。但是值得注意的是栈帧开辟的时候进行push ebp的操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
5:        int res = 0;
00401038   mov         dword ptr [ebp-4],0
6:        res = a+b;
0040103F   mov         eax,dword ptr [ebp+8]
00401042   add         eax,dword ptr [ebp+0Ch]
00401045   mov         dword ptr [ebp-4],eax
7:        return res;
00401048   mov         eax,dword ptr [ebp-4]
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1.在求和函数中,将0存入[ebp-4]指向的内存块中。
2.[ebp+8]指向的内存块中的值放入eax寄存器中,而[ebp+8]指向的内存块对应的正是实参`10`3.[eb[ebp+0Ch]指向的内存块中的值放入eax寄存器中,而[ebp+0Ch]指向的内存中对应的正是实参`20`4.将eax寄存器中的值压栈,存放到[ebp-4]指向的内存块中,此时eax寄存器中的值为`30`
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
7:        return res;
00401048   mov         eax,dword ptr [ebp-4]


0040104E   mov         esp,ebp
00401050   pop         ebp
00401051   ret 

1.由mov    eax,dword ptr [ebp-4]可知,函数的返回值(小于等于四个字节)是由寄存器带回的。

2.0040104E   mov       esp,ebp使得被调用函数栈帧回退。此时栈帧空间的内容还存在。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
3.pop   ebp 两个动作,出栈,并将出栈的值赋给ebp。在这里,即ebp==0x100。栈顶指针向下回退四个字节(高地址)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//下面看主函数调用求和函数执行的指令
0040109A   add         esp,8
0040109D   mov         dword ptr [ebp-0Ch],eax

1.add   esp,8 相当于esp = esp + 8,是主函数压实参的栈帧回退。所以形参内存是由调用方清理的。
2.将eax寄存器中的值`30`放入[ebp-0Ch]指向的四字节内存块中。

到这里,函数堆栈调用的过程就完全展示出来了。现在回答最开始我们提出的几个题:

(1)形参的内存空间的开辟和清理是由调用方还是由被调用方执行的? (2)主函数调用函数结束后,主函数从哪里开始执行?从头开始还是从调用之后开始? (3)返回值是如何带出来的?

答: (1)形参的内存空间的开辟和清理是由调用方执行的。 (2)主函数调用函数后执行执行调用之后的代码,是因为调用方在进行调用的过程中,将下一行指令的地址压栈。所以调用完成之后是从调用之后开始,不会从头开始。 (3)返回值是由累加寄存器eax带出来的(当返回值的字节数小于等于四个自己时)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
微信热修复tinker初探
前言 Tinker简介 Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。 Tinker已知问题 1) Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件; 2) 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码 3) 在Android N上,补丁对应用启动时间有轻微的影响; 4) 不支持部分三
用户1665735
2018/06/20
2K1
Tinker-使用教程与原理分析(上)
前面我们讲解了AndFix的使用,这篇我们来讲解下微信的Tinker热修复,相比AndFix,Tinker的功能更加全面,更主要的是他支持gradle。他不仅做到了热修复更实现了“热更新”。既然他这么强大,下面我们就来了解他是如何使用的。
g小志
2018/09/11
1.9K0
Tinker-使用教程与原理分析(上)
Android热更新之微信Tinker集成(接入Bugly热更新)
最近公司项目中需要集成热更新功能,由于刚开始接入的时候踩了很多坑,所以现在记录一下集成的过程.
SoullessCoder
2019/08/07
2K0
Android热更新之微信Tinker集成(接入Bugly热更新)
【Android】热修复——Tinker(入门)
前言 不知你是否遇到这样的情况?千辛万苦上开发了一个版本,好不容易上线了,突然发现了一个严重bug需要进行紧急修复,怎么办?难道又要重新打包App、测试,发布新个版本?就为了修改一两行的代码? 莫慌,这种问题其实可以分分钟解决。如果你学会了这项黑科技——热修复。 在用户使用App的时候,不知不觉,这个Bug就被修复了。 莫慌 热修复:热修复(也称热补丁、热修复补丁,英语:hotfix)是一种包含信息的独立的累积更新包,通常表现为一个或多个文件。这被用来解决软件产品的问题(例如一个程序错误)。——维基
Gavin-ZYX
2018/05/18
3.4K3
Android热更新利器Tinker接入
基准包 例如有一个版本A,但是这时A是有Bug的,然后修复Bug后的生成的版本我们称为B。A和B之间的区别产生一个差分包(这里也称为补丁包),那么我们就可以说这个差分包是以A作为基准包相对B生成的。 基本步骤 1、注册Tinker账号并新建项目 2、配置gradle和代码 3、生成基准包 4、修复Bug 5、生成补丁包 6、发布补丁包 Tinker做了什么 1、1-2步是APP开发的基本步骤,完成1-3步,那么你的APP就集成了Tinker。 集成Tinker后,Tinker会根据各个版本的配置信息去自动加
用户1269200
2018/03/26
1.3K0
Android热更新利器Tinker接入
Android架构之路--热更新Tinker(上)
当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:
conanma
2021/09/04
1.9K0
微信热修复框架 Tinker 从使用到 patch 加载、生成、合成原理分析
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
张拭心 shixinzhang
2019/12/10
2.3K0
微信热修复框架 Tinker 从使用到 patch 加载、生成、合成原理分析
Bugly热更新SDK你需要知道的一些事
Bugly出热更新SDK了? 没错,Bugly也出热更新SDK啦,2016.11.25号,我们Bugly也上线了Android版的热更新SDK,大家都知道这一年来热更新被无数次提起,各大厂自主研发的热更新方案层出不穷,下面就列举一些大家比较熟悉的一些热更新方案: 微信开源:Tinker 大众点评:Nuwa 阿里巴巴:Dexposed 阿里巴巴:AndFix 美团:Robust 各个方案的优劣性笔者就不在这里做过多讨论了,总的一句话没有最好的,只有最适合自己的。 我们Bugly也是出于高可用性的考虑,Tink
巫山老妖
2018/07/20
1.6K0
【详细】Android热更新Bugly集成配置
上一篇文章说道tinker的热更新,可是少了点补丁包的管理,这一篇文章介绍的bugly就是增强版的,更加方便你集成tinker和包括了补丁包的后台管理。 为什么使用 Bugly 热更新?
ppjun
2018/09/05
1.1K1
【详细】Android热更新Bugly集成配置
Tinker源码分析(七):dex合成流程
前面讲到了 Tinker 安装补丁的流程,现在就详细地来看下 dex 合成的代码。代码入口就在 DexDiffPatchInternal.tryRecoverDexFiles 中。
俞其荣
2019/03/29
1.4K0
【Android 热修复】运行 Tinker 官方示例 ( 处理 TINKER_ID 问题 | 编译 debug 包 | 修改 Gradle 脚本 | 生成 patch 包 | 热修复 )
Tinker 官方代码示例 : https://github.com/Tencent/tinker/tree/dev/tinker-sample-android
韩曙亮
2023/03/29
7400
【Android 热修复】运行 Tinker 官方示例 ( 处理 TINKER_ID 问题 | 编译 debug 包 | 修改 Gradle 脚本 | 生成 patch 包 | 热修复 )
Tinker原理
Tinker采用的是下发差分包,然后在手机端合成全量的dex文件进行加载。而在build.gradle配置中的tinkerPatch
老马的编程之旅
2022/06/22
6700
Tinker原理
【云+社区年度征文】让移动开发更简单,集成异常上报、运营统计与应用升级
做移动开发最麻烦的就是收集用户在使用过程中的程序的异常崩溃日志,因为这个异常崩溃是无征兆的在毫无防备随时的出现,所以有时候真是丈二金刚(摸不着头脑);这个还是其次要命的是用户端程序的每次迭代和版本的分布又不容易推送和获取。
谭广健
2020/12/19
7500
Tinker源码分析(六):补丁合成流程
下发的补丁包其实并不能直接加载,因为补丁包只是差异包,需要和本地的 dex 、资源等进行合成后,得到全量的 dex 才能被完整地使用。这样也就避免了热修复中 dex 的 pre-verify 问题,也减少了补丁包的体积,方便用户下载。
俞其荣
2019/03/19
1.5K0
章鱼抓娃娃添加Bugly-Tinker热更新支持
Bugly热更新采用Tinker开源方案,官方文档如下: Bugly Android热更新使用指南 Bugly Android热更新详解
冰之角
2019/03/06
8720
Tinker源码分析(四):加载资源补丁流程
我们回到 TinkerLoader.tryLoadPatchFilesInternal 方法中来看。
俞其荣
2019/03/15
1.3K0
Android热更新方案Robust
美团•大众点评是中国最大的O2O交易平台,目前已拥有近6亿用户,合作各类商户达432万,订单峰值突破1150万单。美团App是平台主要的入口之一,O2O交易场景的复杂性决定了App稳定性要达到近乎苛刻的要求。用户到店消费买优惠券时死活下不了单,定外卖一个明显可用的红包怎么点也选不中,上了一个新活动用户一点就Crash……过去发生过的这些画面太美不敢想象。客户端相对Web版最大的短板就是有发版的概念,对线上事故很难有即时生效的解决方式,每次发版都如临深渊如履薄冰,毕竟就算再完善的开发测试流程也无法保证不会将B
美团技术团队
2018/03/12
1.5K0
Android热更新方案Robust
Android架构之路--热更新Tinker(下)
tinker官方文档推荐用walle或者packer-ng-plugin来辅助打渠道包。估计有不少同学用过,今天我想推荐另外一款多渠道打包的插件ApkMultiChannelPlugin,它作为Android Studio插件进行多渠道打包。 安装步骤:打开 Android Studio: 打开 Setting/Preferences -> Plugins -> Browse repositories 然后搜索 ApkMultiChannel 安装重启。 有不了解的同学,可以直接看它的文档。
conanma
2021/09/04
4950
有赞移动热修复平台建设
随着公司的快速发展,需求的快速增加,App迭代也越来越频繁,如果移动应用出现问题,不仅仅影响用户体验,还会影响公司口碑,甚至可能造成资损。需要快速修复线上问题,对比常规的开发流程而言,热修复更加灵活方便,优势很多:
有赞coder
2020/08/24
1.3K0
有赞移动热修复平台建设
Bugly 多渠道热更新解决方案
Gradle使用productFlavors打渠道包的痛 有很多同学可能会采用配置productFlavors来打渠道包,主要是它是原生支持,方便开发者输出不同定制版本的apk,举个例子: android { ... defaultConfig { minSdkVersion 8 versionCode 10 } productFlavors { flavor1 { packageName "com
腾讯Bugly
2018/03/23
1.6K0
推荐阅读
相关推荐
微信热修复tinker初探
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档