分享

C|从汇编的角度理解函数调用与参数传递

 山峰云绕 2022-01-07

https://m.toutiao.com/is/89vUoUv/?=C++ 


主调函数(caller)调用被调函数(callee),编译器要考虑两者的相互独立和相互联系。

一方面通过传递参数(值或址)和函数返回(值或址)来在两段代码之间建立联系。传址还可以形成副作用。

(C++的引用参数也是一种传址,只是编译器做了自动取址和解引用取值的动作。)

另一方面主调函数和被调函数都有各自的函数栈帧(function frame),但两者的地址空间是透明的(主调函数可以通过被调函数栈帧上的地址来间接访问被调函数栈帧上的空间)。

主调函数调用被调函数时,对于参数,不管是传值还是传址,都会有一个压栈操作,但压值和压址的操作稍有不同(后续会从汇编的角度分析),参数压栈后,被调函数体对参数的操作就是对压栈空间的引用,当然,对值的引用和址的引用在解析成汇编后也会不同,后者会增加一个解引用(从地址取值)的动作。

demo:

#include <stdio.h>int __cdecl callee(int *a, int &b, int d) // __cdecl是函数调用约定,是略写时的默认调用约定{ int t = *a; *a = b*d; b = t*d; return *a*b;}void caller(){ int a=3,b=4,d=2; int c = callee(&a,b,d); // 函数调用时会传址或传址,参数会通过压栈而形成副本机制 printf('%d\n',c);}int main() // main由操作系统调用而被执行,其它函数要由另外的函数去调用才会被执行, // main函数通常充当被调函数的作用 // main函数内定义的变量也是局部变量{ caller(); getchar(); return 0;}

demo中的主调函数是caller,被调函数是callee。

1 调用约定

调用约定主要定义主调函数和被调函数对于堆栈平衡的分工,参数压栈顺序规定等。

2 主调函数caller栈帧空间的建立

9:    void caller()10:   {00401090   push        ebp// ebp压栈,届时局部变量会压在ebp之上(低地址方向,栈往低地址方向增长)00401091   mov         ebp,esp00401093   sub         esp,50h// 局部变量使用的空间,编译器会计算局部变量的需求(适当增加)而不同00401096   push        ebx// 为保持寄存器状态而额外使用的栈空间(50h以外)00401097   push        esi00401098   push        edi00401099   lea         edi,[ebp-50h]0040109C   mov         ecx,14h004010A1   mov         eax,0CCCCCCCCh// debug模式时,会将50h的空间全部置0ch004010A6   rep stos    dword ptr [edi]

3 主调函数caller局部变量压栈

11: int a=3,b=4,d=2;004010A8 mov dword ptr [ebp-4],3// 局部变量地址以ebp为基准,向低地址方向增长004010AF mov dword ptr [ebp-8],4004010B6 mov dword ptr [ebp-0Ch],2

4 函数调用(实参压栈)和返回

12:       int c = callee(&a,b,d); // 函数调用时会传址或传址,参数会通过压栈而形成副本机制004010BD   mov         eax,dword ptr [ebp-0Ch] // d赋值给eax寄存器,注意这里是mov,值赋值(或值传递)004010C0   push        eax // d压栈004010C1   lea         ecx,[ebp-8] // b的地址赋值给ecx,注意这里是lea,址赋值(或址传递)004010C4   push        ecx // &b压栈004010C5   lea         edx,[ebp-4] // a的地址赋值给edx004010C8   push        edx // &a压栈004010C9   call        @ILT+20(callee) (00401019) // 这里要进入函数调用004010CE   add         esp,0Ch  // 函数调用完成后返回到这里,按__cdecl约定,由主调函数平衡参数所占空间004010D1   mov         dword ptr [ebp-10h],eax // 返回值存放在寄存器eax中,返回给主调函数的c

注意上述引用传址和指针传址使用了相同的汇编代码。

对于一个寄存器可以存下的返回值,通常通过eax返回,对于浮点数,一般通过浮点栈的寄存器返回,对于复合类型,会在主调函数的局部空间规划出一块空间用来存放返回值,这块空间的首地址会在压完参数后压在栈帧上。

call 函数名

push 返回地址(EIP) + jmp 函数地址

(EIP指向下一条指令)

(1) 将程序当前执行的位置IP的下一个地址压入堆栈中;

(2) 转移到调用的子程序。

5 caller call caller

@ILT+5(?callee@@YAHPAHAAHH@Z):0040100A jmp callee (00401030)

编译器会将返回地址004010CE压栈,此时的栈帧空间是:

6 被调函数栈帧空间建立

2:    int __cdecl callee(int *a, int &b, int d) // __cdecl是函数调用约定,是略写时的默认调用约定3:    {00401030   push        ebp00401031   mov         ebp,esp00401033   sub         esp,44h00401036   push        ebx00401037   push        esi00401038   push        edi00401039   lea         edi,[ebp-44h]0040103C   mov         ecx,11h00401041   mov         eax,0CCCCCCCCh00401046   rep stos    dword ptr [edi]

7 callee函数体对实参的引用

4: int t = *a;00401048 mov eax,dword ptr [ebp+8]0040104B mov ecx,dword ptr [eax] // 对a的解引用并赋值0040104D mov dword ptr [ebp-4],ecx5: *a = b*d;00401050 mov edx,dword ptr [ebp+0Ch]00401053 mov eax,dword ptr [edx] // 对b的解引用并赋值00401055 imul eax,dword ptr [ebp+10h] // 对b的值的直接引用00401059 mov ecx,dword ptr [ebp+8]0040105C mov dword ptr [ecx],eax6: b = t*d;0040105E mov edx,dword ptr [ebp-4]00401061 imul edx,dword ptr [ebp+10h]00401065 mov eax,dword ptr [ebp+0Ch]00401068 mov dword ptr [eax],edx7: return *a*b;0040106A mov ecx,dword ptr [ebp+8]0040106D mov edx,dword ptr [ebp+0Ch]00401070 mov eax,dword ptr [ecx]00401072 imul eax,dword ptr [edx]8: }

注意以上汇编对值的直接引用,对引用传递和指针传递的变量先是引用地址,然后通过地址来解引用。

8 被调函数负责的自己部分的堆栈平衡

00401075   pop         edi00401076   pop         esi00401077   pop         ebx00401078   mov         esp,ebp0040107A   pop         ebp // 相当于C语言中的ebp = *esp; esp += 40040107B   ret  // 相当于 pop EIP

-End-

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多