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

首页常识更新时间:2023-05-10 11:28:47

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

算法要借助函数来实现。

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

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

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

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

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

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): 6 Enter 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 ebp 004019A1 mov ebp,esp 004019A3 sub esp,48h 004019A6 push ebx 004019A7 push esi 004019A8 push edi

4 栈帧分配,并初始化

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

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

5 局部变量压栈

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

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

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

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

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

004019E8 pop edi 004019E9 pop esi 004019EA pop ebx 004019EB mov esp,ebp 004019ED pop ebp8 ret

ret = pop jmp

004019EE ret

00401941 add esp,4

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

7 堆栈平衡

00401941 add esp,49 中间值存储到寄存器

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

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

18: int combinations(int n, int k) // C(n, k) 19: { 00401920 push ebp 00401921 mov ebp,esp 00401923 sub esp,40h 00401926 push ebx 00401927 push esi 00401928 push edi 00401929 lea edi,[ebp-40h] 0040192C mov ecx,10h 00401931 mov eax,0CCCCCCCCh 00401936 rep stos dword ptr [edi] 20: return fact(n) / (fact(k) * fact(n - k)); 00401938 mov eax,dword ptr [ebp 8] 0040193B push eax 0040193C call @ILT 165(fact) (004010aa) 00401941 add esp,4 00401944 mov esi,eax 00401946 mov ecx,dword ptr [ebp 0Ch] 00401949 push ecx 0040194A call @ILT 165(fact) (004010aa) 0040194F add esp,4 00401952 mov edi,eax 00401954 mov edx,dword ptr [ebp 8] 00401957 sub edx,dword ptr [ebp 0Ch] 0040195A push edx 0040195B call @ILT 165(fact) (004010aa) 00401960 add esp,4 00401963 imul edi,eax 00401966 mov eax,esi 00401968 cdq 00401969 idiv eax,edi 21: } 0040196B pop edi 0040196C pop esi 0040196D pop ebx 0040196E add esp,40h 00401971 cmp ebp,esp 00401973 call __chkesp (00422890) 00401978 mov esp,ebp 0040197A pop ebp 0040197B ret

-End-

,
展开阅读全文
推荐内容
热门内容
热门文章

© 2007-2022 http://www.anhuiqq.cn,All Rights Reserved.