函数调用及返回汇编浅析-C语言部分之有基础类型参数无返回
有参无返回
下面取一个简单的加法计算函数,来看下有参调用时如何完成的
#include <stdio.h>
void addCal(int a, int b);
int main(int argc, const char * argv[]) {
// insert code here...
addCal(1, 11);
return 0;
}
void addCal(int a, int b)
{
int c;
c = a + b;
printf("a + b = %d\n", c);
}
通过编译链接转换位汇编
main函数
addCal函数
在进行汇编分析前,先介绍下x86-64函数调用时,参数传递的方式
汇编中传递参数需要通过寄存器+内存的方式来传递参数,参数量少于等于6个时,通过寄存器存放调用时使用的参数,超过部分通过内存来传递,篇幅所限,感兴趣可以自己探索
参数位置 | 寄存器名称 | |||
64位寄存器 | 32位寄存器 | 16位寄存器 | 8位寄存器 | |
第1个 | rdi | edi | di | dil |
第2个 | rsi | esi | si | sil |
第3个 | rdx | edx | dx | dl |
第4个 | rcx | ecx | cx | cl |
第5个 | r8 | r8d | r8w | r8b |
第6个 | r9 | r9d | r9w | r9b |
下面来分析汇编的执行
main函数,和前面相同的地方不再加颜色
push rbp //将调用方分配的栈帧地址存入栈内存,函数执行完后,需要恢复此寄存器,目的是可以返回调用方的内存地址,所保存的地址是rsp寄存器指向的地址
mov rbp, rsp //将堆栈指针寄存器存入rbp
sub rsp, 10h //rsp寄存器的值减0x10,也就是栈顶向下移动0x10大小,用于保存当前函数得栈数据
mov [rbp+var_4], 0 //前面可以看到:var_4 = dword ptr -4,此句话就是mov [rbp -4], 0,其中"[]",用于将寄存器的值转成内存地址(栈地址),也就是将 rbp-4的地址存入0
mov [rbp+var_8], edi //和前面一句一样:将edi里的值存入[rbp - 8]的内存地址,保存main函数的的第一个参数,argc,4个字节
mov [rbp+var_10], rsi //和前面一句一样:将rsi里的值存入[rbp - 0x10]的内存地址,保存main函数的第二个参数的地址,argv,8个字节
mov edi, 1 //将1传入edi寄存器,int类型长度位4字节,所以使用edi
mov esi, 0Bh //将0x0b也就是11传入esi
call _addCal //调用addCal
xor eax, eax //返回结果置零
add rsp, 10h //恢复堆栈指针寄存器,相当于回收局部变量得内存,但是并未将相应得内存置0,有被重新获取到得风险
pop rbp //恢复栈帧指针寄存器,恢复调用前的帧位置
retn //返回调用方
可以看到main函数通过edi和esi将参数传给addCall
addCall函数
push rbp //将调用方分配的栈帧地址存入栈内存,函数执行完后,需要恢复此寄存器,目的是可以返回调用方的内存地址,所保存的地址是rsp寄存器指向的地址
mov rbp, rsp //将堆栈指针寄存器存入rbp
sub rsp, 10h //rsp寄存器的值减0x10,也就是栈顶向下移动0x10大小,用于保存当前函数得栈数据
mov [rbp+var_4], edi //将第一个参数edi放置到rbp-4的位置
mov [rbp+var_8], esi //将第二个参数esi放置到rbp-8的位置
mov eax, [rbp+var_4] //将rbp-4内存的数据放到eax里面
add eax, [rbp+var_8] //执行加法,eax = eax的值+rbp-8位置的值;有点类似与a += b;
mov [rbp+var_C], eax //将eax的值存储到rbp-0xC的位置
mov esi, [rbp+var_C] //将rbp-0xC的值存储到esi寄存器中,就是上表中的函数调用的第二个参数
lea rdi, aABD ; "a + b = %d\n" //将字符串"a + b = %d\n"的地址存入rdi(指针时8个字节需使用64位寄存器)寄存器中,也就是第一个参数
mov al, 0 //返回值置0
call _printf //调用printf
add rsp, 10h //恢复rsp
pop rbp //恢复rbp
retn //返回
main函数执行和无参没差别,这里不再画图表示
对addCall进行图形分析
第一阶段,保存调用点的栈帧寄存器到栈内存,并重置堆栈指针环境
第二阶段、开辟0x10大小的内存
第三阶段、执行加法
第四阶段、调用printf函数
第五阶段、恢复rsp/rbp
总结:
通过对上述过程分析,我们可以感受到明明很简单的c=a+b,汇编做了太多工作,内存的申请,释放,寄存器和内存间的数据传递
直观感受存在的问题:
1、方法名可见,对别人拿到应用程序后,可以通过名称比如addCal,就知道是用于加法计算
2、字符串可见,如果是银行的代码,这样子就绝对存在问题了
如果需要完善上述过程,需要在汇编生成的时候做些手脚,比如构建语法树的时候,这也是高级版本的代码混淆,IR层混淆;
有基础类型参数有返回
下面对第一个函数进行改造,将加的结果返回给调用方,由调用方打印
#include <stdio.h>
int addCal(int a, int b);
int main(int argc, const char * argv[]) {
// insert code here...
int a = 1;
int b = 11;
int c = addCal(a, b);
printf("a + b = %d\n", c);
return 0;
}
int addCal(int a, int b)
{
int c;
c = a + b;
return c;
}
通过编译链接转换位汇编
main函数
addCal函数
下面来分析汇编的执行
汇编分析前我们需知,函数返回值通过eax寄存器来返回
main函数,和前面相同的地方不再加颜色
push rbp //将调用方分配的栈帧地址存入栈内存,函数执行完后,需要恢复此寄存器,目的是可以返回调用方的内存地址,所保存的地址是rsp寄存器指向的地址
mov rbp, rsp //将堆栈指针寄存器存入rbp
sub rsp, 20h //rsp寄存器的值减0x20,也就是栈顶向下移动0x20大小,用于保存局部变量
mov [rbp+var_4], 0 //前面可以看到:var_4 = dword ptr -4,此句话就是mov [rbp -4], 0,其中"[]",用于将寄存器的值转成内存地址(栈地址),也就 是将rbp-4的地址存入0
mov [rbp+var_8], edi //和前面一句一样:将edi里的值存入[rbp - 8]的内存地址,保存main函数的的第一个参数,argc,4个字节
mov [rbp+var_10], rsi //和前面一句一样:将rsi里的值存入[rbp - 0x10]的内存地址,保存main函数的第二个参数的地址,argv,8个字节
mov [rbp+var_14], 1 //将1传入内存rbp - 0x14的位置
mov [rbp+var_18], 0Bh //将0x0B放到rbp-0x18的位置
mov edi, [rbp+var_14] //将rbp-0x14位置存的值放到edi寄存器中,可对照传参寄存器表
mov esi, [rbp+var_18] //将rbp-0x18位置存的值放到esi寄存器中,可对照传参寄存器表
call _addCal //调用addCal
mov [rbp+var_1C], eax //eax代表的是addCall的返回值,将addCall的返回值发到rbp-0x1C的内存中
mov esi, [rbp+var_1C] //将rbp-0x1C的位置存的值发哦到esi寄存器中,可对照传参寄存器表
lea rdi, aABD ; "a + b = %d\n" //将字符串"a + b = %d\n"的地址存入rdi(指针时8个字节需使用64位寄存器)寄存器中,可对照传参寄存器表
mov al, 0 //返回值置0
call _printf //调用printf函数
xor eax, eax //返回结果置零
add rsp, 10h //恢复堆栈指针寄存器,相当于回收局部变量得内存,但是并未将相应得内存置0,有被重新获取到得风险
pop rbp //恢复栈帧指针寄存器,恢复调用前的帧位置
retn //返回调用方
此main函数开辟的栈内存块0x20比前两个0x10都要大,这是因为此main函数,有3个局部变量需要存储,a/b/c,所以局部变量的大小直接影响栈内存的占用,但是好处是,栈内存执行完就会回收,其他地方和前面的区别不大,这里不再做细致分析
addCal函数
push rbp //将调用方分配的栈帧地址存入栈内存,函数执行完后,需要恢复此寄存器,目的是可以返回调用方的内存地址,所保存的地址是rsp寄存器指向的地址
mov rbp, rsp //将堆栈指针寄存器存入rbp
mov [rbp+var_4], edi //将第一个参数edi放置到rbp-4的位置
mov [rbp+var_8], esi //将第二个参数esi放置到rbp-8的位置
mov eax, [rbp+var_4] //将rbp-4内存的数据放到eax里面
add eax, [rbp+var_8] //执行加法,eax = eax的值+rbp-8位置的值;有点类似与a += b;
mov [rbp+var_C], eax //将eax的值存储到rbp-0xC的位置
mov eax, [rbp+var_C] //将rbp-0xC的位置值防止到eax寄存器中
pop rbp //恢复rbp
retn //返回
通过上述代码我们可以看出,addCall并没有开辟自己的栈内存,而是使用了main函数开辟的栈内存,此种情形属于汇编优化,在不影响调用方的前提下,可以尽量节省栈内存的使用,同时计算的返回值通过eax传递给调用方
此过程和前面无返回值的相似,这里不再画图分析,相信各位大佬可以很容易理解
总结:
1、局部变量的大小直接影响栈内存的占用
2、非频繁调用的函数,使用栈内存比堆内存要好,栈内存使用后就自动释放了,堆内存需要自己管理,逻辑思维能力差的同学就有可能因忘记释放或多次释放引起野指针问题
3、栈内存是向下增长的,堆内存是向上增长的,内存申请达到一定的极限,还有存在两者碰头的风险,所以还是需要尽量节省内存,特别对我们这种内存消耗大户
- 点赞
- 收藏
- 关注作者
评论(0)