写这篇总结的缘由仅仅出于巧合,五一前帮一位同学看51的程序,在查看汇编代码的时候(事实上我当时的汇编知识基本都还给了老师),无意中问起我“某个局部变量的声明怎么没有对应的汇编语句”,我没有答出来。当时也只是把它当做一种常识给记了下来,平时不论还是在DSP、16位的单片上还是PC平台上编写c程序,由于程序不是很复杂且芯片资源通常足够,因此很少会考虑内存分配、堆栈方面的内容。这几天在看《Linux c 一站式编程》这本书关于变量内容的时候突然想起这个问题,顿时觉得很有必要把深究一下。
因为网络账号还没充钱的缘故,ubuntu系统还不能及时升级,因此在VC平台下做了以下相关试验。代码如下:
int main(void)
{
int d;
int f = d+1;
d = 1;
d = hello(5,10);
return 0;
}
对应的汇编代码如下:
16: int main(void)
17: {
00410960 push ebp
00410961 mov ebp,esp
00410963 sub esp,48h
00410966 push ebx
00410967 push esi
00410968 push edi
00410969 lea edi,[ebp-48h]
0041096C mov ecx,12h
00410971 mov eax,0CCCCCCCCh
00410976 rep stos dword ptr [edi]
18:
19:
20: int d;
21: int f = d+1;
00410978 mov eax,dword ptr [ebp-4]
0041097B add eax,1
0041097E mov dword ptr [ebp-8],eax
22: d = 1;
00410981 mov dword ptr [ebp-4],1
23: //printf('Hello world!\n');
24: //printf("a = %f,b=%f,c=%f",a,b,c);
25: //getch();
26: d = hello(5,10);
00410988 push 0Ah
0041098A push 5
0041098C call @ILT+5(_hello) (0040100a)
00410991 add esp,8
00410994 mov dword ptr [ebp-4],eax
27: return 0;
00410997 xor eax,eax
28: }
从以上汇编代码20、21两行中可以看到“int d”语句确实没有对应的函数声明。但是在用d给f赋值的时候,d却对应了dword ptr [ebp-4],这一点刚开始的时候很是疑惑,后来在看过几篇相关文章后得知,局部变量的声明和释放是由编译器调整栈指针(Stack Pointer)位置来完成的[1],编译器首先根据变量的类型和数量计算存储局部变量所需的空间,然后调整ESP的值来为局部变量分配空间。“int d;” 在编译的时候应该是将变量d 与地址 dword prt[ebp-4]对应了起来,既d的内存地址为ebp-4到ebp-7;同理,f值往上调整,对应ebp-8到ebp-11。
然而,了解了以上内容还远不够理解函数压栈出栈的原理,于是对main函数下面对应的17行做了分析,考虑linux平台下的情况[],主要针对ESP和EBP两个栈寄存器作分析解释。以下是通过单步调试的方式得到的结果:
图1 未单步前
未运行单步执行前,程序和寄存器值如图1所示。ESP = 0012FF84 ,EBP = 0012FFC0;这是旧的(在函数调用时就是主调函数的)栈对应的空间。
图2 执行 “00410960 push ebp”
执行的“push ebp”后,程序和寄存器值如图2所示。ESP = 0012FF80,EBP = 0012FFC0;因为是要在原来栈的基础上开辟新的栈空间,因此,在旧栈(不知道该如何称呼,暂且这么认为)的栈顶上移了四个字节作为新栈的栈底。因此ESP由原来的0012FF84变为0012FF80 。
图3 执行 “00410961 mov ebp,esp”
“mov ebp,esp”是为了将新的栈底指针赋值为ebp,如图3所示。ESP = 0012FF80,EBP = 0012FF80 。
图4 执行 “00410963 sub esp,48h”
“ sub esp,48h”则是为了给局部变量在栈中分配一定的存储空间,如图4所示。ESP由原来的0012FF80减小到0012FF38,这里需要注意的是0012FF80到0012FF7C为旧栈中的栈顶对应的四个字节,因此局部变量的分布实际是从地址0012FF7C到地址0012FF34共72个字节空间。
图5 执行 “push ebx ;push esi;push edi”
接下来,连续执行三个push,将相应的寄存器值压栈。
时间限制,下面转写了参考中的文献以备忘。
“
1.在一个栈中,依据函数调用关系,发起调用的函数(caller)的栈帧在下面(高地址方向),被调用的函数的栈帧在上面。
2.每发生一次函数调用,便产生一个新的栈帧,当一个函数返回时,这个函数所对应的栈帧被清除(eliminated)
”[1]