如何实现c语言编译器,C|编译器是如何实现函数调用的?

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

算法要借助函数来实现。

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

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

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

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

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

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

整体流程如下:

204983014_1_20201020080930447

编译后在上述备注处插入一断点(F9)→运行(F5),按提示输入:Enter the number of objects (n): 6Enter the number to be chosen (k): 2

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

此时的调用堆栈是:

204983014_2_20201020080930509

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

204983014_3_202010200809319

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

204983014_4_20201020080931337

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

204983014_5_20201020080931494

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

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

204983014_6_20201020080931587

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

204983014_7_20201020080931962

以下是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-

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值