浅谈函数栈帧
先说说函数栈帧的概念,函数栈帧又叫函数运行时堆栈,栈帧也叫过程活动记录,是编译器用来实现函数调用的一种数据结构。这个该概念说起来比较抽象,简单的说就是函数在被调用时的一块空间,这个空间由esp寄存器和ebp寄存器共同维护。首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
main函数的调用
程序的入口必须是main吗?每当我们在点击一个(或者在命令行打开)一个C程序的时候,程序立马就能运行起来,从刚开始学习C语言的时候我们都直到main函数是程序的入口。至于为什么这么说完全是因为ANSIC就是这么规定的,一个程序的执行并不一定需要main函数,但是一定需要一个入口,做过单片机的同学应该深有体会。起来从CPU角度来看,将要执行的指令地址放在程序计数器里,程序需要执行必然需要一个入口地址。通用的可执行文件格式总会指定一个入口地址,这样操作系统才可以调度这样一个程序执行指令。所谓的main函数,就是执行时把这个程序装入任务调度器中,调度器执行调度的入口函数而已,而main函数只是个程序员和调度器之间的约定。
mainCRTStartup函数:扯得有点远了,在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup再调用main函数,mainCRTStartup所做的初始化准备工作,例如获取命令行参数、获取环境变量值,是通过调用相应的Windows系统调用来实现的。
1#include<stdio.h>
2
3int Add(int x, int y){
4 int z = x + y;
5 return z;
6}
7
8int main(void)
9{
10 int a = 10;
11 int b = 20;
12 int c = Add(a, b);
13 printf("c = %d\n",c);
14 return 0;
15}
下面是其反汇编代码:
1int Add(int x, int y){
2009613D0 push ebp
3009613D1 mov ebp,esp
4009613D3 sub esp,0CCh
5009613D9 push ebx
6009613DA push esi
7009613DB push edi
8009613DC lea edi,[ebp-0CCh]
9009613E2 mov ecx,33h
10009613E7 mov eax,0CCCCCCCCh
11009613EC rep stos dword ptr es:[edi]
12 int z = x + y;
13009613EE mov eax,dword ptr [x]
14009613F1 add eax,dword ptr [y]
15009613F4 mov dword ptr [z],eax
16 return z;
17009613F7 mov eax,dword ptr [z]
18}
19009613FA pop edi
20009613FB pop esi
21009613FC pop ebx
22009613FD mov esp,ebp
23009613FF pop ebp
2400961400 ret
25------------------- main.c ------------------------
26
27int main(void){
2800961410 push ebp
2900961411 mov ebp,esp
3000961413 sub esp,0E4h
3100961419 push ebx
320096141A push esi
330096141B push edi
340096141C lea edi,[ebp-0E4h]
3500961422 mov ecx,39h
3600961427 mov eax,0CCCCCCCCh
370096142C rep stos dword ptr es:[edi]
38 int a = 10;
390096142E mov dword ptr [a],0Ah
40 int b = 20;
4100961435 mov dword ptr [b],14h
42 int c = Add(a, b);
430096143C mov eax,dword ptr [b]
440096143F push eax
4500961440 mov ecx,dword ptr [a]
4600961443 push ecx
4700961444 call _Add (09610E6h)
4800961449 add esp,8
490096144C mov dword ptr [c],eax
50 printf("c = %d\n",c);
510096144F mov esi,esp
52 printf("c = %d\n",c);
5300961451 mov eax,dword ptr [c]
5400961454 push eax
5500961455 push 965858h
560096145A call dword ptr ds:[969118h]
5700961460 add esp,8
5800961463 cmp esi,esp
5900961465 call __RTC_CheckEsp (0961140h)
60 system("pause");
610096146A mov esi,esp
620096146C push 965864h
6300961471 call dword ptr ds:[969110h]
6400961477 add esp,4
650096147A cmp esi,esp
660096147C call __RTC_CheckEsp (0961140h)
67 return 0;
6800961481 xor eax,eax
69}
7000961483 pop edi
7100961484 pop esi
7200961485 pop ebx
7300961486 add esp,0E4h
740096148C cmp ebp,esp
750096148E call __RTC_CheckEsp (0961140h)
7600961493 mov esp,ebp
7700961495 pop ebp
7800961496 ret
由此也可以看出编程语言的进化历程,要是现在还是由我们来写这些汇编代码,想想都难受…
汇编指令简述
数据传送指令
这部分指令包括通用数据传送指令MOV、条件传送指令CMOVcc、堆栈操作指令PUSH/PUSHA/PUSHAD/POP/POPA/POPAD、交换指令XCHG/XLAT/BSWAP、地址或段描述符选择子传送指令LEA/LDS/LES/LFS/LGS/LSS等。注意,CMOVcc不是一条具体的指令,而是一个指令簇,包括大量的指令,用于根据EFLAGS寄存器的某些位状态来决定是否执行指定的传送操作。
在本例中,可以将MOV指令理解为赋值语句,例如 mov ebp,esp就是将esp中的值赋给ebp;push指令理解为压栈,例如 push ebx 就是将ebx寄存器压栈(入栈),同样的道理,pop ebx 就是将ebx寄存器弹栈(出栈),例如 lea edi,[ebp-0E4h] 就是取源操作数地址的偏移量,并把它传送到目的操作数所在的单元
整数和逻辑运算指令
这部分指令用于执行算术和逻辑运算,包括加法指令ADD/ADC、减法指令SUB/SBB、加一指令INC、减一指令DEC、比较操作指令CMP、乘法指令MUL/IMUL、除法指令DIV/IDIV、符号扩展指令CBW/CWDE/CDQE、十进制调整指令DAA/DAS/AAA/AAS、逻辑运算指令NOT/AND/OR/XOR/TEST等。
在本例中用到了 add和sub 指令,例如add esp,8,就是给esp寄存器中的值加上8,执行加法运算;还有一个是cmp指令,cmp是比较指令,cmp的功能是相当于减法指令,只是不保存结果.cmp指令执行后,将对标志寄存器产生影响.其他相关指令通过识别这些被影响的标志寄存器来得知比较结果.
串操作指令
这部分指令用于对数据串进行操作,包括串传送指令MOVS、串比较指令CMPS、串扫描指令SCANS、串加载指令LODS、串保存指令STOS,这些指令可以有选择地使用REP/REPE/REPZ/REPNE和REPNZ的前缀以连续操作。
本例中使用到了rep stos等等 lea edi,[ebp-0C0h] mov ecx,30h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] rep指令的目的是重复其上面的指令.ECX的值是重复的次数. STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址. LEA叫做loadEffectiveAddress,加载有效的地址
其他指令在本例中未出现,故在此不再赘述
函数栈帧解析
看完上述的汇编指令简述,再次看到汇编语言应该就不那么陌生了,接下来正式进入函数栈帧的分析环节main的函数栈帧
由前面我们已经知道,是mainCRTStartup函数调用的main函数,main函数终究也是一个函数,是函数调用就肯定需要空间的,由esp寄存器和ebp寄存器来维护这块空间,这块空间就叫函数栈帧
100961410 push ebp
200961411 mov ebp,esp
300961413 sub esp,0E4h
400961419 push ebx
50096141A push esi
60096141B push edi
70096141C lea edi,[ebp-0E4h]
800961422 mov ecx,39h
900961427 mov eax,0CCCCCCCCh
100096142C rep stos dword ptr es:[edi]
首先将ebp寄存器压栈,然后将ebp的位置移向了esp的位置,esp有向上移动了04E个位置,然后将ebx、esi、edi寄存器压栈,然后让edi寄存器加载红线所示的地址,从edi的位置开始copy,重复拷贝ecx次数据,一次拷贝的大小是双字节(4个字节),拷贝到eax里面!
0CCCCCCCC在被解析的时候就是乱码,对应的ACCSI码也就是”烫”,所以我们在使用未初始化的变量的时候又时会打印出”烫烫烫…”这也就不足为奇了
变量的创建以及函数调用时参数拷贝:
1int a = 10;
20096142E mov dword ptr [a],0Ah
3int b = 20;
400961435 mov dword ptr [b],14h
5int c = Add(a, b);
60096143C mov eax,dword ptr [b]
70096143F push eax
800961440 mov ecx,dword ptr [a]
900961443 push ecx
可以看出,首先创建了a和b变量并做了相应的初始化操作,然后将b的值放入到eax寄存器中,然后将eax寄存器压栈,同样把a变量放入ecx寄存器中,然后将ecx压栈:
也可以得出结论:
- C语言中所有的参数传递都是值传递,形式参数只是实参的一份临时拷贝
- 后面的参数先被压入栈中,即参数传递的顺序是右向左的
Add函数的函数的函数栈帧
call指令先将下一条指令语句的给地址存起来了,方便在函数执行完毕之后执行下一条语句,这一点至关重要,一个函数执行完毕一定要跳到下一条语句的地方继续执行才可以。Add函数栈帧的创建于main函数栈帧的创建与初始化操作是一样的,同样的道理Add函数也是采用同样的方式初始化
1009613D0 push ebp
2009613D1 mov ebp,esp
3009613D3 sub esp,0CCh
4009613D9 push ebx
5009613DA push esi
6009613DB push edi
7009613DC lea edi,[ebp-0CCh]
8009613E2 mov ecx,33h
9009613E7 mov eax,0CCCCCCCCh
10009613EC rep stos dword ptr es:[edi]
1//接下来就是创建Z变量并且执行加法操作将结果返回
200DE13EE mov eax,dword ptr [x]
300DE13F1 add eax,dword ptr [y]
400DE13F4 mov dword ptr [z],eax
500DE13F7 mov eax,dword ptr [z]
600DE13FA pop edi
700DE13FB pop esi
800DE13FC pop ebx
900DE13FD mov esp,ebp
1000DE13FF pop ebp
1100DE1400 ret
首先将x的值放入eax寄存器中,然后执行加法运算将结果放入eax寄存器中,然后把eax寄存器中的值放入变量z,然后将z的值存入eax寄存器作为返回,接着将edi、esi、ebx三个寄存器分别弹栈,又将ebp移向了esp的位置,此时Add函数的栈帧销毁,代表整个Add函数的结束!当pop ebp的时候,将ebp弹栈,此时下面正好就是Call指令下一条指令的地址,这样就能接着Add的函数的结束继续执行下面的代码了!
然后把eax寄存器中的值放入变量c,这样就拿到了Add函数的返回值,所以说返回值是寄存器带回来的!
VC6.0之坑
1#include <stdio.h>
2
3void fun(){
4 int tmp = 10;
5 int *p = (int*)(* (&tmp+1));
6 * (p-1) = 20;
7}
8
9int main(void){
10 int a = 0;
11 fun();
12 printf("a = %d\n",a);
13 return 0;
14}
在VC6.0的编译器下打印结果是20(VC6.0是多么可怕从这里也就可以看出了…),只要了解函数栈帧的话本题就不难解释,tmp的地址加一再解引用刚好就是main函数的ebp的地址,然后p-1得到的就是main函数中a的地址,这样的话打印出20也就不足为奇了!
函数栈帧的理解还是很重要的,其中涉及到函数栈帧的初始化,参数传递,返回值传递等等问题,学习函数栈帧就是明白函数的整个调用过程,还是非常重要的一部分!