天天看点

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

算法要借助函数来实现。

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

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

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

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

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

void __stdcall add(int a,int b);

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

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

调用约定 堆栈平衡方式

__stdcall 函数自己平衡

__cdecl 调用者负责平衡

__thiscall 调用者负责平衡

__fastcall 调用者负责平衡

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

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

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

#include 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;}
           

整体流程如下:

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

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

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

此时的调用堆栈是:

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

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 中间值存储到寄存器

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡9 中间值存储到寄存器10 表达式的最后结果保存在寄存器中

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

c++ 编译器_C|编译器是如何实现函数调用的?8 ret7 堆栈平衡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-

继续阅读