[读书笔记]C语言函数调用过程
*** 本文是《老码识途》第一章的读书笔记 ***
函数调用
例子代码如下所示:
int Add(int x, int y) { int sum; sum = x + y; return sum; } void main() { int z; z = Add(1, 2); printf("z=%d\n", z); }
下面分析一下 Add函数的调用过程。
首先断点在z = Add(1, 2);处, 反汇编如下所示:
int z; z = Add(1, 2); 002C141E 6A 02 push 2 002C1420 6A 01 push 1 002C1422 E8 60 FC FF FF call 002C1087 002C1427 83 C4 08 add esp,8 002C142A 89 45 F8 mov dword ptr [ebp-8],eax
首先压入参数1和2:
002C141E 6A 02 push 2 002C1420 6A 01 push 1
通过观察ESP可以看到参数从右到左依次入栈,ESP往低内存方向移动8字节:
ESP=0025FCCC ... 0x0025FCAA 00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 3d 5a b2 0f *** 01 00 00 00 02 00 00 00 *** 0x0025FCCC 00 00 00 00
然后执行:
002C1422 E8 60 FC FF FF call 002C1087
call指令执行时,首先压入call指令的返回地址,即add esp,8这一句的地址002C1427:
0x0025FCAA 00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 *** 27 14 2c 00 *** 01 00 00 00 02 00 00 00
然后跳转到02C1087。02C1087处为jmp语句,跳转到Add函数入口地址002C13C0:
int Add(int x, int y) { 002C13C0 55 push ebp 002C13C1 8B EC mov ebp,esp 002C13C3 81 EC CC 00 00 00 sub esp,0CCh 002C13C9 53 push ebx 002C13CA 56 push esi 002C13CB 57 push edi 002C13CC 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h] 002C13D2 B9 33 00 00 00 mov ecx,33h 002C13D7 B8 CC CC CC CC mov eax,0CCCCCCCCh 002C13DC F3 AB rep stos dword ptr es:[edi] int sum; sum = x + y; 002C13DE 8B 45 08 mov eax,dword ptr [ebp+8] 002C13E1 03 45 0C add eax,dword ptr [ebp+0Ch] 002C13E4 89 45 F8 mov dword ptr [ebp-8],eax return sum; 002C13E7 8B 45 F8 mov eax,dword ptr [ebp-8] } 002C13EA 5F pop edi 002C13EB 5E pop esi 002C13EC 5B pop ebx 002C13ED 8B E5 mov esp,ebp 002C13EF 5D pop ebp 002C13F0 C3 ret
获取参数
目前为止,栈上的情况如下图所示,从上往下内存地址从高到低:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ ESP | return address | +------> +----------------+
此时参数可由ESP + 4,ESP + 8获得。但是由于程序执行时ESP会变化,为了方便定位栈上的数据,引入EBP(Extended Base Pointer,扩展基址指针寄存器),保存进入函数时ESP的值。
由于函数可以嵌套调用,所以在进入函数时必须将EBP的旧值保存起来,以防覆盖EBP导致函数返回后无法恢复EBP。这里通过将EBP压入栈来保存旧值。如:
002C13C0 55 push ebp 002C13C1 8B EC mov ebp,esp ... 002C13EF 5D pop ebp
所以在函数开头有如下代码:
int Add(int x, int y) { 002C13C0 55 push ebp 002C13C1 8B EC mov ebp,esp
此时栈上的内存布局如下图所示:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ | return address | +----------------+ ESP | ebp | +------> +----------------+
取出1,2参数的代码如下所示:
int sum; sum = x + y; 002C13DE 8B 45 08 mov eax,dword ptr [ebp+8] 002C13E1 03 45 0C add eax,dword ptr [ebp+0Ch]
其中ebp+8取出参数1,ebp+0Ch取出参数2(0C为十进制的12),然后计算结果放在EAX中。
初始化堆栈和分配局部变量
接下来有如下代码段,将esp下移0CC,然后push ebx,esi,edi这三个寄存器:
002C13C3 81 EC CC 00 00 00 sub esp,0CCh // 1 002C13C9 53 push ebx 002C13CA 56 push esi 002C13CB 57 push edi
其中语句1是为了给局部变量分配足够大的栈空间,然后再保存三个寄存器的值。局部变量利用ebp定位,存于ebp和OCCh之间。
ebx,esi,edi的作用如下所示来源:
寄存器%ebx、%esi和%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。
然后运行:
002C13CC 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h] 002C13D2 B9 33 00 00 00 mov ecx,33h 002C13D7 B8 CC CC CC CC mov eax,0CCCCCCCCh 002C13DC F3 AB rep stos dword ptr es:[edi]
首先,通过lea指令,将ebp+FFFFFF34h地址(栈帧的底部)写入edi。然后设置ecx和eax,最后运行rep stos语句。
rep stos dword ptr es:[edi]语句的意思是:将栈上从ebp+FFFFFF34h开始的位置向高地址方向的内存赋值eax(0xCCCCCCCC),次数重复ecx(0x33, 51)次。每运行一次edi的值会增加。注意0xCCCCCCCC代表着未被初始化(int3中断)。这样做的原因是防止分配好的局部变量空间中的代码被意外执行。
局部变量
示例代码中,x+y的结果保存在局部变量sum中,由如下代码可知,sum分配在栈上。
int sum; sum = x + y; 002C13DE 8B 45 08 mov eax,dword ptr [ebp+8] 002C13E1 03 45 0C add eax,dword ptr [ebp+0Ch] 002C13E4 89 45 F8 mov dword ptr [ebp-8],eax <<==== 结果保存在局部变量sum中
目前栈的分配情况如下所示:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ | return address | +----------------+ | ebp | +----------------+ | ? | +----------------+ ESP | sum | +------> +----------------+
“?”处的4字节是编译器为了防止溢出攻击而设置的。
返回值
函数返回处的代码如下:
002C13EA 5F pop edi 002C13EB 5E pop esi 002C13EC 5B pop ebx 002C13ED 8B E5 mov esp,ebp 002C13EF 5D pop ebp 002C13F0 C3 ret
函数返回时需要考虑两件事情:恢复栈和保存返回值。
恢复栈
首先,通过pop栈恢复edi,esi和ebx的值,然后将esp恢复到ebp处,然后pop ebp,将ebp恢复旧值。此时esp指向return address:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ ESP | return address | +------>+----------------+ | ebp | +----------------+ | ? | +----------------+ | sum | +----------------+
接下来运行ret指令。ret指令会将栈顶保存的地址压入指令寄存器EIP,相当于pop eip。运行后EIP和ESP都会有变化。
然后程序跳转到return address处,如下所示:
int z; z = Add(1, 2); 002C141E 6A 02 push 2 002C1420 6A 01 push 1 002C1422 E8 60 FC FF FF call 002C1087 002C1427 83 C4 08 add esp,8 // ret跳转到此处 002C142A 89 45 F8 mov dword ptr [ebp-8],eax
其中add esp,8语句的目的是将1,2参数出栈,将栈恢复到函数调用之前的状态。接下来便可以从eax中取出返回值:
002C142A 89 45 F8 mov dword ptr [ebp-8],eax
由于ebp已被恢复,故其中ebp-8即为临时变量z的地址