函数调用及返回汇编浅析-C语言部分

举报
dosomething 发表于 2023/12/05 19:01:00 2023/12/05
【摘要】 C语言C语言的函数调用有如下种类1、无参无返回2、无参有返回3、有参无返回4、有参有返回这里取几个典型:1、无参无返回2、有基础类型参数无返回3、有基础类型参数有返回无参无返回取简单的helloword来分析下#include <stdio.h>void sayHello(void); //原型int main(int argc, const char * argv[]) { // ...

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汇编

主函数的汇编如下

image.png

sayHello函数生成的汇编

image (5).png

第三步、分析汇编

在进行汇编分析前,先对其中的一些关键字进行解释

寄存器(关于寄存器的知识,感兴趣的可以自行查询):

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函数置行过程分为如下几个阶段

第一、保存调用点的栈帧寄存器到栈内存,并重置堆栈指针环境

d.png

第二阶段、开辟空间,保存局部变量值

d (1).png

第三阶段、调用sayHello

第四阶段、将返回值置为0,存入寄存器

第五阶段、恢复rsp、rbp

d (2).png

sayHello执行过程

push    rbp    //同上

mov     rbp, rsp    //同上

lea     rdi, aHelloWorld ; "Hello, World!\n"    //加载有效地址,将字符串地址加载到寄存器中,其中rdi用于传入第一个参数

mov     al, 0    //清空返回值位0

call    _printf    //调用printf函数

pop     rbp    //恢复栈

retn    //返回

 

对sayHello执行的几个阶段进行图形解析

第一阶段、同上

d (3).png

第二阶段、准备参数调用printf

d (6).png

第三阶段、恢复

d (7).png

总结

通过对无参无返回的函数进行分析,我们可以得出如下几个结论

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函数

image (1).png

addCal函数

image (2).png

在进行汇编分析前,先介绍下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进行图形分析

第一阶段,保存调用点的栈帧寄存器到栈内存,并重置堆栈指针环境

d (8).png

第二阶段、开辟0x10大小的内存

d (9).png

第三阶段、执行加法

d (10).png

第四阶段、调用printf函数

d (11).png

第五阶段、恢复rsp/rbp

d (12).png

总结:

通过对上述过程分析,我们可以感受到明明很简单的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函数

image (3).png

addCal函数

image (4).png

下面来分析汇编的执行

汇编分析前我们需知,函数返回值通过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、栈内存是向下增长的,堆内存是向上增长的,内存申请达到一定的极限,还有存在两者碰头的风险,所以还是需要尽量节省内存,特别对我们这种内存消耗大户

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。