本篇,我们对C语言函数的调用进行一个深入地研究。
1. main函数的调用过程
首先我们看一个代码。
#include <stdio.h>
#include <stdlib.h>
int add(int x, int y) {
return x + y;
}
int main() {
int a = 10;
int b = 10;
int ret = add(a, b);
printf("ret = %d\n", ret);
system("pause");
return 0;
}
接下来我们打开调试,观察一下它的调用堆栈。
这时我们可以看到,原来入口函数 main函数 也是被别的函数调用的。
一个函数名为 __tmainCRTStartup() 的函数对 main函数 进行了调用,调用如下。
从调用堆栈中我们还能发现, __tmainCRTStartup()函数 又被另一个叫做 exe!mainCRTStartup() 的调用的,调用如下。
至此,main函数的前世今生我们就算摸清楚了。
main函数在 __tmainCRTStartup 函数中调⽤的,⽽ __tmainCRTStartup 函数是
在 mainCRTStartup 被调⽤。
我们知道每⼀次函数调⽤都是⼀个过程。这个过程我们通常称之为: 函数的调⽤过程。这个过程要为函数开辟栈空间,⽤于本次函数的调⽤中临时变量的保存、现场保护。这块栈空间我们称之为函数栈帧。
接下来我们了解一下函数的栈帧。
2. 函数栈帧
栈帧的维护我们必须了解ebp和esp两个寄存器。在函数调⽤的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。
- ebp:指向栈底的指针
- esp:指向栈顶的指针
栈空间的使用是由高地址向低地址增长的,栈底固定不变,栈顶向低地址增长。
接着上面的代码,我们研究一下它的反汇编。
2.1 main函数栈帧初始化
int main() {
01101B10 push ebp // 进入main函数,先将ebp压栈
// 方便main函数调用完毕返回 __tmainCRTStartup()
01101B11 mov ebp,esp // 将esp赋值给ebp,产生新栈底
01101B13 sub esp,0E4h // C栈是向下生长的,esp减去0E4H,即开辟 0E4H 的栈空间
01101B19 push ebx
01101B1A push esi
01101B1B push edi
01101B1C lea edi,[ebp-0E4h] // 接下来的四句汇编,意思为初始化栈空间
01101B22 mov ecx,39h
01101B27 mov eax,0CCCCCCCCh // 初始化的值为0CCCCCCCCh,这也就是为什么内存访问越界时会显示 烫烫烫,
// 烫烫烫 ASCII码值对应的就就是0CCCCCCCCh
01101B2C rep stos dword ptr es:[edi]
int a = 10;
01101B2E mov dword ptr [a],0Ah // 创建局部变量a
int b = 10;
01101B35 mov dword ptr [b],0Ah // 创建局部变量b
接下来,进入到 add 函数的调用。
2.2 add函数调用
首先,先进行函数的传参。
int ret = add(a, b);
01101B3C mov eax,dword ptr [b] // 将[b]中存放的变量b存入寄存器eax
01101B3F push eax // eax压栈
01101B40 mov ecx,dword ptr [a] // 将[a]中存放的变量a存入寄存器ecx
01101B43 push ecx // ecx压栈
01101B44 call _add (011011F9h) // call指令跳转到 011011F9h
// 回来时,会跳到call指令下一条语句,即 01101B49
01101B49 add esp,8
01101B4C mov dword ptr [ret],eax
call跳转。
经过call来到了 011011F9,此地址处是一条 jmp 指令,我们又要进行跳转。
这里就是 add 函数的栈帧。
int add(int x, int y) {
01103CF0 push ebp // 压栈当前ebp
01103CF1 mov ebp,esp // 将esp赋值给ebp,产生新栈底
01103CF3 sub esp,0C0h // esp减去0C0H,即开辟 0C0H 的栈空间
01103CF9 push ebx
01103CFA push esi
01103CFB push edi
01103CFC lea edi,[ebp-0C0h] // 下面四句指令同 main函数
01103D02 mov ecx,30h
01103D07 mov eax,0CCCCCCCCh
01103D0C rep stos dword ptr es:[edi]
return x + y;
01103D0E mov eax,dword ptr [x]
01103D11 add eax,dword ptr [y] // 进行 x+y 的运算,结果保存在寄存器eax中
}
01103D14 pop edi
01103D15 pop esi
01103D16 pop ebx
01103D17 mov esp,ebp
01103D19 pop ebp
01103D1A ret // 跳转结束,跳转回 call 下一条指令
程序运行到这里,执行 ret 指令,结束跳转,执行流回到 call 下一条指令。
接下来的都是语句的具体操作,这里就不进行赘述了。