结构化程序的一个最基本的单元就是“函数”或者叫“过程”。在汇编这一层自然也相应的有支持这些概念的指令操作,如栈操作和栈帧的概念。
我们知道,一个由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack)— 由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等 。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表 。
3、全局区(静态区)(static)—存放全局变量、静态数据、常量。程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
5、程序代码区—存放函数体(类成员函数和全局函数)的二进制代码。
其中比较特殊的是堆区和栈区,二者中间有一块很大的内存,堆区申请内存是往高地址处向上生长,而栈地址则是往下增长的。
所以我们今天要研究的就是栈这块的空间。
我们知道,每一次函数的调用的过程,电脑都要为函数开辟栈空间,用于本次函数中临时变量的保存和现场保护。这块栈的空间就被我们称之为函数栈帧。
上面说过 ,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。
而栈帧的维护,我们就必须了解以下几个寄存器:
ebp(基址寄存器)———指向当前的栈帧的底部(高地址)
esp(栈顶寄存器)————指向当前的栈帧的顶部(低地址)
eip(指令指针寄存器)——存储着cpu要读取指令的地址,没有它,cpu就无法执行
eax,ebx,ecx,edx(数据寄存器)————主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器的时间
esi,edi(变址寄存器)———存放存储单元在段内的偏移量
结合此图可以更好地理解栈帧结构
下面,举个栗子:
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int mul(int x, int y)
{
int z = 0;
z = x*y;
return z;
}
int main()
{
int a = 4;
int b = 5;
int ret = mul(a, b);
printf("ret=%d", ret);
system("pause");
return 0;
}
--- c:\users\lcl\documents\visual studio 2013\projects\栈帧\栈帧\mul.c -------------
int main()
{
00FB13D0 push ebp //压栈
00FB13D1 mov ebp,esp //把栈底指针挪到新的位置
00FB13D3 sub esp,0E4 //扩展新的栈帧,是栈底和栈顶分离,即给esp减去一个16进制数0E4h,产生新的esp
00FB13D9 push //依次入栈
00FB13DA push esi
00FB13DB push edi
00FB13DC lea edi,[ebp-0E4h] //lea指令:将有效地址传到指定的寄存器 这里即把栈的最低地址load到edi寄存器
00FB13E2 mov ecx,39h //4*39H=0E4H
00FB13E7 mov eax,0CCCCCCCCh //把栈帧预开辟的空间全部初始化为0*CCCCCCCC,未初始化则为常见的“烫烫烫烫”
00FB13EC rep stos dword ptr es:[edi]
int a = 4;
00FB13EE mov dword ptr [a],4 // 局部变量a的创建,dword 通过指针赋值,ptr就是指针,相当于把4赋值给a
int b = 5;
00FB13F5 mov dword ptr [b],5 //同上
int ret = mul(a, b);
00FB13FC mov eax,dword ptr [b] //参数b压栈 注意:先把b的值mov到eax寄存器中
00FB13FF push eax //然后b先行入栈
00FB1400 mov ecx,dword ptr [a] //同上:a接着入栈
00FB1403 push ecx
00FB1404 call _mul (0FB1127h) //CALL:a1.将下一条指令所在的地址00FB1409入栈;a2.并将子程序的起始地址送入PC (即CPU的下一 条指令就转去执行子程序)
即跳转到_mul: 00FB1127 jmp mul (0FB1480h) // jmp指令是预示着即将跳转到mul 函数的起始位置00FB1480h
00FB1409 add esp,8 //栈顶向上移两个变量,8个字节
00FB140C mov dword ptr [ret],eax //eax寄存器在mul函数中保存了z的值,现在赋给ret
printf("ret=%d", ret);
00FB140F mov esi,esp //esp指向esi
00FB1411 mov eax,dword ptr [ret] //ptr[ret]=20赋值给eax
00FB1414 push eax //eax入栈
00FB1415 push 0FB5858h //不知道为啥
00FB141A call dword ptr ds:[0FB9118h] //同上 然后直接跳转到PRINT函数开头 59E7CE70 push ebp
printf("ret=%d", ret);
00FB1420 add esp,8 //同上 栈顶向上移
00FB1423 cmp esi,esp //cmp指令 :比较两个操作数的大小,第一个操作数减去第二个操作数 ,不保存结果,只影响标志寄存器(SF,ZF,CF,OF)
EXP:mov ax ,8 换行 mov bx, 3 换行 cmp ax ,bx
执行后 ax=8,ZF=0 ,PF=1,SF=0,CF=0,OF=0
其逻辑含义:ZF=1则AX=BX; CF=1则AX<BX; CF=0则AX>=BX
00FB1425 call __RTC_CheckEsp (0FB1140h)
system("pause"); //system函数
00FB142A mov esi,esp
00FB142C push 0FB5860h
00FB1431 call dword ptr ds:[0FB9110h]
00FB1437 add esp,4
00FB143A cmp esi,esp
00FB143C call __RTC_CheckEsp (0FB1140h)
return 0; //return函数
00FB1441 xor eax,eax
}
00FB1443 pop edi
00FB1444 pop esi
00FB1445 pop ebx
00FB1446 add esp,0E4h
00FB144C cmp ebp,esp
00FB144E call __RTC_CheckEsp (0FB1140h)
00FB1453 mov esp,ebp
00FB1455 pop ebp
00FB1456 ret //ending
--- c:\users\lcl\documents\visual studio 2013\projects\栈帧\栈帧\mul.c -------------
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h >
#include <Windows.h>
int mul(int x, int y) //今天的主题——函数调用栈帧
{
00FB1480 push ebp //main函数的栈底压入栈中,注意:它的上一条指令是00FB1409
00FB1481 mov ebp,esp //把调用mul()函数前的栈顶esp作为mul()栈帧的栈底ebp
00FB1483 sub esp,0CCh //esp往下移开辟空间,新的栈帧已经形成
00FB1489 push ebx //三者依次入栈
00FB148A push esi
00FB148B push edi
00FB148C lea edi,[ebp-0CCh] //将有效地址传给edi寄存器,即取偏移地址
00FB1492 mov ecx,33h //跟前面一样4*33h=它的偏移地址0CCh
00FB1497 mov eax,0CCCCCCCCh //初始化
00FB149C rep stos dword ptr es:[edi] //rep指令:重复上面的指令,ecx的值是重复次数;stos指令:将eax的值拷贝esp中(即edi指向的地址)
int z = 0;
00FB149E mov dword ptr [z],0 //创建变量z
z = x*y;
00FB14A5 mov eax,dword ptr [x] //把指针ptr[x]的值赋给eax
00FB14A8 imul eax,dword ptr [y] //imul函数:将乘数与被乘数均作为有符号数;即eax*=ptr[y]
mul函数:将乘数与被乘数均作为无符号数
00FB14AC mov dword ptr [z],eax //把eax的值赋给ptr[z]
return z;
00FB14AF mov eax,dword ptr [z] //将结果保存到eax寄存器,通过寄存器带回函数的返回值
}
00FB14B2 pop edi //三者依次出栈
00FB14B3 pop esi
00FB14B4 pop ebx
00FB14B5 mov esp,ebp //把ebp的内容给esp,即esp箭头指向ebp,ebp,esp同时在ebp处
00FB14B7 pop ebp //看第一条指令 ebp指向的是main函数的栈底,然后ebp出栈 (局部变量已经释放)
00FB14B8 ret //将函数的返回值地址返回到eip当中,此时将跳转到上面CALL指令的下一条指令
--- 无源文件 -----------------------------------------------------------------------
*******************************************************************************/
int __cdecl printf (
const char *format,
...
)
/*
* stdout 'PRINT', 'F'ormatted //输出函数print
*/
{
59E7CE70 push ebp //知识盲区。
59E7CE71 mov ebp,esp
59E7CE73 push 0FFFFFFFEh
59E7CE75 push 59F95998h
59E7CE7A push 59F3FEF0h
59E7CE7F mov eax,dword ptr fs:[00000000h]
59E7CE85 push eax
59E7CE86 add esp,0FFFFFFE4h
59E7CE89 push ebx
59E7CE8A push esi
59E7CE8B push edi
59E7CE8C mov eax,dword ptr ds:[59FA8100h]
59E7CE91 xor dword ptr [ebp-8],eax
59E7CE94 xor eax,ebp
59E7CE96 push eax
59E7CE97 lea eax,[ebp-10h]
59E7CE9A mov dword ptr fs:[00000000h],eax
va_list arglist;
int buffing;
int retval = 0;
59E7CEA0 mov dword ptr [retval],0