函数调用及返回汇编浅析-C语言部分
C语言
C语言的函数调用有如下种类
1、无参无返回
2、无参有返回
3、有参无返回
4、有参有返回
这里取几个典型:
1、无参无返回
2、有基础类型参数无返回
3、有基础类型参数有返回
无参无返回
取简单的helloword来分析下
#include <stdio.h>
void sayHello(void); //原型
int main(int argc, const char * argv[]) {
// insert code here...
sayHello();
return 0;
}
void sayHello() { //定义
printf("Hello, World!\n");
}
第一步、将源码编译链接成可执行程序
第二步、用IDA查看生成的汇编程序,这里生成的是x86-64汇编
主函数的汇编如下
sayHello函数生成的汇编
第三步、分析汇编
在进行汇编分析前,先对其中的一些关键字进行解释
寄存器(关于寄存器的知识,感兴趣的可以自行查询):
rbp:是栈帧指针寄存器,用于标识当前栈帧的起始位置,函数的返回地址,rbp和rsp配合使用,rsp和rbp得差值,就是当前函数申请得栈内存得大小
rsp:是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 rsp 的值即移动堆栈指针的位置来实现的。
edi:32位寄存器,用于传递第一个参数
rdi:64位寄存器,用于传递第一个参数
rsi:64位寄存器,用于传递第二个参数
eax:32位寄存器,累加寄存器,用于存储返回值
al:8位寄存器,累加寄存器,用于存储返回值
汇编执行:
push:入栈,将寄存器的值存入栈顶
mov:移动
call:调用
xor:异或,主要用于清空寄存器的值
sub:减
add:加
retn:返回
lea:load effective address, 加载有效地址,将地址加载到寄存器中
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个字节
call _sayHello //并未传输任何参数调用sayHello函数
xor eax, eax //返回结果置零
add rsp, 10h //恢复堆栈指针寄存器,相当于回收局部变量得内存,但是并未将相应得内存置0,有被重新获取到得风险
pop rbp //恢复栈帧指针寄存器,恢复调用前的帧位置
retn //返回调用方
通过对上述main函数的分析,我们可以将main函数置行过程分为如下几个阶段
第一、保存调用点的栈帧寄存器到栈内存,并重置堆栈指针环境
第二阶段、开辟空间,保存局部变量值
第三阶段、调用sayHello
第四阶段、将返回值置为0,存入寄存器
第五阶段、恢复rsp、rbp
sayHello执行过程
push rbp //同上
mov rbp, rsp //同上
lea rdi, aHelloWorld ; "Hello, World!\n" //加载有效地址,将字符串地址加载到寄存器中,其中rdi用于传入第一个参数
mov al, 0 //清空返回值位0
call _printf //调用printf函数
pop rbp //恢复栈
retn //返回
对sayHello执行的几个阶段进行图形解析
第一阶段、同上
第二阶段、准备参数调用printf
第三阶段、恢复
总结
通过对无参无返回的函数进行分析,我们可以得出如下几个结论
1、栈是自大向小的方向进行扩展的(堆是反过来的,这里没做介绍)
2、函数调用,迭代使用的寄存器是,rbp和rsp
3、函数调用后并不会清理使用过的内存,数据还在,通过一定的手段是可恢复的,所以华为C/C++规范里就明确表示敏感数据用后需清理,敏感数据不能放到string的局部变量里
有参无返回
下面取一个简单的加法计算函数,来看下有参调用时如何完成的
#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)