引言 |??函数栈帧实质是指在内存栈区为函数所开辟的一块空间,使函数得以运行,本章简要介绍函数栈帧的创建及销毁,涉及少量的汇编代码。? ? ? ? VS2013演示
1.寄存器
?寄存器是集成在中央处理器上的元件,可以临时存储数据(就像内存一样,但其访问速度较内存快),例如eax? ebx? ecx? edx? ebp? esp? ,其中ebp(栈底指针),esp(栈顶指针)存放的是地址,这两的存放的地址用于维护函数指针;
ebp(栈底指针)? esp(栈顶指针)
压栈(push):向栈顶放置一个元素; 出栈(pop):从栈空间拿走一个元素;
2.main()主函数
在实际程序运行中,main()函数也是被其他的函数调用,在写好实例代码后:
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 13;
int b = 12;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
?按下F10启动逐过程调试,调试->窗口->调用堆栈,
(这里看到main()函数被调用)调试至结束后(return 0;语句走完);
由于栈空间是从高地址向低地址的次序使用的,故mainCRTStartup()函数首先调用__tmainCRTStartup()函数,之后__tmainCRTStartup()函数调用main()函数,可以在源代码ctrexe.c中找到相关函数名及调用方式;
?3.main()函数及Add()函数运行是对应的汇编代码;
在shiift+F5退出后,再按F10进入调试,调试->窗口->反汇编,找到对应的汇编代码;
3.1. main()主函数的对应汇编代码:
--- d:\learningfile\class103\test\函数栈帧\函数栈帧\test.c ----
int main()
{
01011410 push ebp
01011411 mov ebp,esp
01011413 sub esp,0E4h
01011419 push ebx
0101141A push esi
0101141B push edi
0101141C lea edi,[ebp-0E4h]
01011422 mov ecx,39h
01011427 mov eax,0CCCCCCCCh
0101142C rep stos dword ptr es:[edi]
int a = 13;
0101142E mov dword ptr [ebp-8],0Dh
int b = 12;
01011435 mov dword ptr [ebp-14h],0Ch
int c = 0;
0101143C mov dword ptr [ebp-20h],0
c = Add(a, b);
01011443 mov eax,dword ptr [ebp-14h]
01011446 push eax
01011447 mov ecx,dword ptr [ebp-8]
0101144A push ecx
0101144B call 010110E1
01011450 add esp,8
01011453 mov dword ptr [ebp-20h],eax
printf("%d\n", c);
01011456 mov esi,esp
01011458 mov eax,dword ptr [ebp-20h]
0101145B push eax
0101145C push 1015858h
01011461 call dword ptr ds:[01019114h]
01011467 add esp,8
0101146A cmp esi,esp
0101146C call 0101113B
return 0;
01011471 xor eax,eax
}
01011473 pop edi
}
01011474 pop esi
01011475 pop ebx
01011476 add esp,0E4h
0101147C cmp ebp,esp
0101147E call 0101113B
01011483 mov esp,ebp
01011485 pop ebp
01011486 ret
?3.2. Add()函数对应的汇编代码:
--- d:\learningfile\class103\test\函数栈帧\函数栈帧\test.c -------
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
010113C0 push ebp
010113C1 mov ebp,esp
010113C3 sub esp,0CCh
010113C9 push ebx
010113CA push esi
010113CB push edi
010113CC lea edi,[ebpebp-0CCh]
010113D2 mov ecx,33h
010113D7 mov eax,0CCCCCCCCh
010113DC rep stos dword ptr es:[edi]
int z = 0;
010113DE mov dword ptr [ebp-8],0
z = x + y;
010113E5 mov eax,dword ptr [ebp+8]
010113E8 add eax,dword ptr [ebp+0Ch]
010113EB mov dword ptr [ebp-8],eax
return z;
010113EE mov eax,dword ptr [ebp-8]
}
010113F1 pop edi
}
010113F2 pop esi
010113F3 pop ebx
010113F4 mov esp,ebp
010113F6 pop ebp
010113F7 ret
对main()汇编代码的理解:在执行main函数前,__tmainCRTStartup()函数的栈帧已存在,其中epb指向该函数栈帧的底部(高地址处),esp指向该函数栈帧的顶部;
—————————————————— <<<----esp
| | | 低地址
| | |
| | |
| __tCRTStartuo() | |
| 的函数栈帧 | |
| | |
| | |
__________________ <<<------ebp | 高地址
?main()函数栈帧预开辟:
01011410 push ebp
01011411 mov ebp,esp
01011413 sub esp,0E4h
push指令(压栈)表示将ebp的值压入栈中,产生新的栈顶,同时esp存放地址会减小,保持其存放地址始终是栈顶,mov指令表示将esp的值赋值给ebp,时ebp指向的地址发生改变,sub指令表示减法,将esp减去0E4h(h表示该数字为十六进制)后更新esp,此时ebp与esp之间的空间即为main()函数的函数栈帧,如图:
—————————————————— <<<----esp
| | | 低地址
| | |
| main()函数 | |
| 的函数栈帧 | |
| | |
| | |
| | |
—————————————————— <<<------ebp |
| ebp | |
—————————————————— |
| | |
| | |
| | |
| __tCRTStartuo() | |
| 的函数栈帧 | |
| | |
| | |
__________________ | 高地址
01011419 push ebx
0101141A push esi
0101141B push edi
三个push指令表示将这三个值压入栈中,同时esp会发生相应减小:
—————————————————— <<<----esp | 低地址
| edi | |
—————————————————— |
| esi | |
—————————————————— |
| ebx | |
—————————————————— |
| | |
| | |
| main()函数 | |
| 的函数栈帧 | |
| | |
| | |
| | |
—————————————————— <<<------ebp | 高地址
0101141C lea edi,[ebp-0E4h]
01011422 mov ecx,39h
01011427 mov eax,0CCCCCCCCh
0101142C rep stos dword ptr es:[edi]
lea指令(load effective address加载有效地址)表示将ebp中存放的地址值减去0E4h后存放在edi中;mov指令表示分别将39h与0cccccccch放入ecx与eax中;rep stos指令表示将以edi存放的地址开始向下的ecx次个dword的字节(word表示两个字节,dword即double word,表示4字节)修改为eax的内容,由于0E4h = ecx * dword ,故main的函数栈帧被全部更改为cc;
int a = 13;
0101142E mov dword ptr [ebp-8],0Dh
mov指令表示将0Dh的值(对应的十进制是13)放入epb-8字节的地址处,此时int占据4个字节,使用完后离ebp所指向的真实地址还有4个空间,这样设计是为了防止越界访问造成的影响:
?
——————————————————
| edi |
—————————————————— | 低地址
| esi | |
—————————————————— |
| ebx | |
—————————————————— |
| | |
| | |
| main()函数 | |
| 的函数栈帧 | |
| | |
—————————————————— < epb -8 |
| a = 13 | |
—————————————————— < ebp -4 |
| 四个字节 | |
—————————————————— <<<------ebp | 高地址
这就是为什么在局部变量未赋值时为“烫烫”的原因,里面全部都是cc;接下来的赋值指令都是如此执行的,变量之间相差了8个字节 ;
c = Add(a, b);
01011443 mov eax,dword ptr [ebp-14h]
01011446 push eax
01011447 mov ecx,dword ptr [ebp-8]
0101144A push ecx
0101144B call 010110E1
01011450 add esp,8
01011453 mov dword ptr [ebp-20h],eax
mov指令表示将ebp-14h处4字节的空间中的值放入eax寄存器中,push指令表示将eax压入栈区,(这里即是创建形参,注意是从右向左传参,先将最右的值压栈,逐渐向右压栈)esp存放地址改变;在执行call指令前,按下F11(逐语句调试)会出现:
_Add:
00FC10E1 jmp 00FC13C0
call指令表示执行之后的地址010110E1处的指令,以及将call指令下一条指令的地址(01011450)压入栈中,jmp指令表示跳至00FC13C0地址处,即开始执行Add函数汇编代码,经过预开辟空间,赋值:
int Add(int x, int y)
{
010113C0 push ebp
010113C1 mov ebp,esp
010113C3 sub esp,0CCh
010113C9 push ebx
010113CA push esi
010113CB push edi
?对应图示:
—————————————————— <<<----esp(新) | 低地址
| edi | |
—————————————————— |
| esi | |
—————————————————— |
| ebx | |
—————————————————— <<<----esp(旧) |
| | |
| | |
| add()函数预开辟 | |
| 空间 | |
| | |
| | |
| | |
| | |
—————————————————— <<<------ebp | 更低的地址
| epb(存放的是 | |
| main的 栈底地址) | |
—————————————————— |
| 01011450 | |
—————————————————— <<<---ebp + 8 |
| ecx a = 13 | |
—————————————————— <<---ebp + 0Ch |
| eax b = 12 | |
—————————————————— | 低地址
| esi | |
—————————————————— |
| ebx | |
—————————————————— | ————————————————————————
| | |
| | |
| main()函数 | |
| 的函数栈帧 | | 均为main()函
| | | 数的函数栈帧
—————————————————— |
| a = 13 | |
—————————————————— |
| 四个字节 | |
—————————————————— | 高地址 ——————————————
下一部分:?
int z = 0;
010113DE mov dword ptr [ebp-8],0
z = x + y;
010113E5 mov eax,dword ptr [ebp+8]
010113E8 add eax,dword ptr [ebp+0Ch]
010113EB mov dword ptr [ebp-8],eax
return z;
010113EE mov eax,dword ptr [ebp-8]
}
?010113E5代码与010113E8代码通过ebp加整数方式找到ecx将里面的值赋给eax,在通过此方法将eax的值找到加给eax(更新eax的值),010113EB代码将eax的值放入[epb - 8]的四个字节的空间(即z的空间)中,010113EE代码将[epb - 8]的四个字节的空间的值重新放入eax寄存器,防止Add函数空间释放后无法找到所需要的值
return z;
00FC13EE mov eax,dword ptr [ebp-8]
}
00FC13F1 pop edi
00FC13F2 pop esi
00FC13F3 pop ebx
00FC13F4 mov esp,ebp
00FC13F6 pop ebp
00FC13F7 ret
?三次将寄存器弹出后,esp被mov指令指向ebp所指向空间,在pop指令将epb弹出后,esp指向更新,同时ebp重新指向main()函数栈底;ret是执行流程导向?01011450 指令,此时执行流程回到主函数(读完01011450 地址后,esp自动更新):
—————————————————— |
| 01011450 | |
—————————————————— <<<------esp |
| ecx a = 13 | |
—————————————————— |
| eax b = 12 | |
—————————————————— <<<----esp+8 | 低地址
| edi |
——————————————————
| esi | |
—————————————————— |
| ebx | |
—————————————————— | ————————————————————————
| | |
| | |
| main()函数 | |
| 的函数栈帧 | | 均为main()函
| | | 数的函数栈帧
—————————————————— |
| a = 13 | |
—————————————————— |
| 四个字节 | |
—————————————————— <<<---ebp | 高地址 ——————————————
01011450 add esp,8
01011453 mov dword ptr [ebp-20h],eax
add指令?表示esp重新指向esp+8地址,ecx与eax两个空间;mov指令表示将exa里的值放入[ebp-20h]所指向的4字节空间,即变量c的空间,函数变量传递完成。
结语? |? 局部变量的创建、函数传参及其顺序、形参与实参的关系,函数如何调用以及如何返回。
注:不同编译器实现略有差异,但基本原理相同
|