打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
C|编译器是如何实现函数调用的?

任何编程语言,函数都是很重要的一个概念。

算法要借助函数来实现。

面向过程编程的函数是其基本模块。

面向对象编程的函数(方法)是类或对象对其属性(数据的处理)。

函数式编程自然更是以函数为核心。

通常,程序的控制结构会确保一个入口、一个出口,支持函数的嵌套调用。如何确保代码的正确流动(包括正确返回原函数调用处)?编译器会维护一个栈的内存结构。

另外,关于堆栈平衡,可由调用函数负责,也可由被调函数负责。当有多个参数时,参数按什么顺序计算?这些都可由调用约定来进行规定,如:

void __stdcall add(int a,int b);

函数声明中的__stdcall就是关于调用约定的声明。其中标准C函数的默认调用约定是__stdcall,C++全局函数和静态成员函数的默认调用约定是__cdecl,类的成员函数的调用约定是__thiscall。剩下的还有__fastcall__naked等。

调用约定指明了函数调用中的参数传递方式和堆栈平衡方式。

调用约定 堆栈平衡方式

__stdcall 函数自己平衡

__cdecl 调用者负责平衡

__thiscall 调用者负责平衡

__fastcall 调用者负责平衡

__naked 编译器不负责平衡,由编写者自己负责

简单的一个函数调用语句,其实对于编译器来说,是一个比较复杂的过程。

以下是一个函数嵌套调用的实例:

#include <iostream>using namespace std;int combinations(int n, int k);int fact(int n);int main() { int n, k; cout << 'Enter the number of objects (n): '; cin >> n; cout << 'Enter the number to be chosen (k): '; cin >> k; cout << 'C(n, k) = ' << combinations(n, k) << endl; // 在这里设一断点 return 0;}int combinations(int n, int k) // C(n, k){ return fact(n) / (fact(k) * fact(n - k));}int fact(int n) // factorial of n{ int result = 1; for (int i = 1; i <= n; i++) result *= i; return result;}

整体流程如下:

编译后在上述备注处插入一断点(F9)→运行(F5),按提示输入:

Enter the number of objects (n): 6Enter the number to be chosen (k): 2

运行至断点处,调出反汇编调试窗口,跟踪fact(6)的内部流程:

此时的调用堆栈是:

1 参数压栈(传参时,可能存在隐式类型转换)

push eax,表示将eax的值压和栈、内存栈,具体位置由寄存器esp给出。

寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。ebp指向了本次函数调用开始时的栈顶指针,它也是本次函数调用时的“栈底”(这里的意思是,在一次函数调用中,ebp向下是函数的临时变量使用的空间)。在函数调用开始时,我们会使用mov ebp,esp,把当前的esp保存在ebp中。

寄存器esp(stack pointer)可称为“ 栈指针”。esp指向当前的栈顶,它是动态变化的,随着我们申请更多的临时变量,esp值不断减小(栈是向下生长的)。函数调用结束,我们使用mov esp,ebp,来还原之前保存的esp。

在函数调用过程中,ebp和esp之间的空间被称为本次函数调用的“栈帧”。函数调用结束后,处于栈帧之前的所有内容都是本次函数调用过程中分配的临时变量,都需要被“返还”。这样在概念上,给了函数调用一个更明显的分界。

2 call fact(6)

0040193C call @ILT+165(fact) (004010aa)00401941 add esp,4

call 相当于 push+jmp。

2.1 push 返回地址00401941

2.2 jmp (fact) (004010aa)

004010AA jmp fact (004019a0)

也就是,首先把call指令的下一条指令地址作为本次函数调用的返回地址压栈,然后使用jmp指令修改指令指针寄存器EIP,使cpu执行 fact函数的指令代码。

指令指针寄存器也叫程序计数器,是用于存放下一条指令所在单元的地址的地方。

当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

可以看到,程序计数器是一个cpu执行指令代码过程中的关键寄存器:它指向了当前计算机要执行的指令地址,CPU总是从程序计数器取出当前指令来执行。当指令执行后,程序计数器的值自动增加,指向下一条将要执行的指令。

在x86汇编中,执行程序计数器功能的寄存器被叫做EIP,也叫作指令指针寄存器

3 一些寄存器压栈,保存其状态信息

004019A0   push        ebp004019A1   mov         ebp,esp004019A3   sub         esp,48h004019A6   push        ebx004019A7   push        esi004019A8   push        edi

4 栈帧分配,并初始化

004019A9 lea edi,[ebp-48h]004019AC mov ecx,12h004019B1 mov eax,0CCCCCCCCh004019B6 rep stos dword ptr [edi]

rep指令的目的是重复其上面的指令,ECX的值是重复的次数。STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址。

5 局部变量压栈

004019B8   mov         dword ptr [ebp-4],126:      for (int i = 1; i <= n; i++)004019BF   mov         dword ptr [ebp-8],1

6 返回值(或地址)保存到寄存器eax

可以察看此时寄存器调试窗口:

返回值返回时,可能存在隐式数据类型转换。

7 一些寄存器值从栈上恢复

004019E8 pop edi004019E9 pop esi004019EA pop ebx004019EB mov esp,ebp004019ED pop ebp

8 ret

ret = pop + jmp

004019EE ret

00401941 add esp,4

表示取出当前栈顶值,作为返回地址,并将指令指针寄存器EIP修改为该值,实现函数返回。

7 堆栈平衡

00401941   add         esp,4

9 中间值存储到寄存器

10 表达式的最后结果保存在寄存器中

以下是combinations(int n, int k)整体的汇编代码(不包括函数调用时的进入):

18: int combinations(int n, int k) // C(n, k)19: {00401920 push ebp00401921 mov ebp,esp00401923 sub esp,40h00401926 push ebx00401927 push esi00401928 push edi00401929 lea edi,[ebp-40h]0040192C mov ecx,10h00401931 mov eax,0CCCCCCCCh00401936 rep stos dword ptr [edi]20: return fact(n) / (fact(k) * fact(n - k));00401938 mov eax,dword ptr [ebp+8]0040193B push eax0040193C call @ILT+165(fact) (004010aa)00401941 add esp,400401944 mov esi,eax00401946 mov ecx,dword ptr [ebp+0Ch]00401949 push ecx0040194A call @ILT+165(fact) (004010aa)0040194F add esp,400401952 mov edi,eax00401954 mov edx,dword ptr [ebp+8]00401957 sub edx,dword ptr [ebp+0Ch]0040195A push edx0040195B call @ILT+165(fact) (004010aa)00401960 add esp,400401963 imul edi,eax00401966 mov eax,esi00401968 cdq00401969 idiv eax,edi21: }0040196B pop edi0040196C pop esi0040196D pop ebx0040196E add esp,40h00401971 cmp ebp,esp00401973 call __chkesp (00422890)00401978 mov esp,ebp0040197A pop ebp0040197B ret

-End-

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
深度剖析函数四个部分(返回值,参数,函数名,函数体)
C语言函数调用栈(一)
[C/C++] 函数调用的栈分配
关于c语言在函数调用过程中栈布局动态变化的讨论
汇编学习 1 寄存器的作用 寻址方式 - DraculaW - JavaEye技术网站
x86、arm、mips架构函数调用实例分析
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服