以前学汇编,很清楚函数是怎么调用的,但是久不用之,又忘了~~不知其他人有没有这种经历,写c/c++程序时如果了解许多编译器底层细节,是很爽的;否则,有时会很沮丧。虽然是比较简单的内容,让我们也来回忆一下...
我们知道,函数调用最通常的传递参数的方式莫过于使用堆栈;函数的局部变量也是在栈上创建。具体怎么做呢?
假如我们有这么一个小小的程序:
void Test(int a){
int b;
}
int main(int argc, char* argv[])
{
int a;
Test(a);
return 0;
}
在VC6.0的调试版中反编译结果如下(加了些注释):
6: void Test(int a){;函数Tes
00401020 push ebp ;保存主调函数栈帧指针
00401021 mov ebp,esp ;激活当前栈帧指针
00401023 sub esp,44h ;为Test留出44h的“私人空间”:44h=4*17=4*11h(联系下面)
00401026 push ebx ;保存寄存器值
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h ;11h=17为循环次数
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi] ;循环,对“私人空间”填充(int)3
7: int b;
8: }
.......
10: int main(int argc, char* argv[])
11: {
00401050 push ebp;
00401051 mov ebp,esp ;激活当前栈帧指针
00401053 sub esp,44h ;为main函数留出44h的“私人空间”
00401056 push ebx
00401057 push esi
00401058 push edi
00401059 lea edi,[ebp-44h]
0040105C mov ecx,11h
00401061 mov eax,0CCCCCCCCh
00401066 rep stos dword ptr [edi]
12: int a;//变量定义没有对应的汇编代码,编译器会以EBP-4来表示(int)a
13: Test(a);
00401068 mov eax,dword ptr [ebp-4] ;dword ptr [ebp-4]也即main函数中int a的值
0040106B push eax ;int a 作为参数压栈
0040106C call @ILT+0(Test) (00401005) ;调用Test函数
00401071 add esp,4 ;因为在call之前压入了一个参数int a,占四个字节,Test函数退出后esp要回退到未压入参数前的位置。
14: return 0;
00401074 xor eax,eax
15: }
00401076 pop edi
00401077 pop esi
00401078 pop ebx
00401079 add esp,44h
0040107C cmp ebp,esp
0040107E call __chkesp (004010a0)
00401083 mov esp,ebp
00401085 pop ebp
00401086 ret
在堆栈中,每个函数都有一个相关的栈桢(stack frame)来保存它所有的局部对象和表达式计算过程中用到的临时对象。通常编译器使用EBP寄存器来指示当前活动的栈桢。编译器在编译时将所有局部对象解析成相对于栈桢指针(EBP)的固定偏移,函数则通过栈桢指针来间接访问局部对象。
编译器编译一个函数时,会在它的开头添加一些代码来为其创建并初始化栈桢,这些代码被称为序言(prologue);同样,它也会在函数的结尾处放上代码来清除栈桢,这些代码叫做尾声(epilogue)。
一般情况下,序言是这样的:
Push EBP ; 把原来的栈桢指针保存到栈上
Mov EBP, ESP ; 激活新的栈桢
Sub ESP, 10 ; 减去一个数字,让ESP指向栈桢的末尾
第一条指令把原来的栈桢指针EBP保存到栈上;第二条指令通过让EBP指向主调函数的EBP的保存位置来激活被调函数的栈桢;第三条指令把ESP减去了一个数字,这样ESP就指向了当前栈桢的末尾,而这个数字是函数要用到的所有局部对象和临时对象的大小。编译时,编译器知道函数的所有局部对象的类型和“体积”,所以,它能很容易的计算出栈桢的大小。
尾声所做的正好和序言相反,它必须把当前栈桢从栈上清除掉:
Mov ESP, EBP
Pop EBP ; 激活主调函数的栈桢
Ret ; 返回主调函数
它让ESP指向主调函数的栈桢指针的保存位置(也就是被调函数的栈桢指针指向的位置),弹出EBP从而激活主调函数的栈桢,然后返回主调函数。
一旦CPU遇到返回指令,它就要做以下两件事:把返回地址从栈中弹出,然后跳转到那个地址去。返回地址是主调函数执行call指令调用被调函数时自动压栈的。Call指令执行时,会先把紧随在它后面的那条指令的地址(被调函数的返回地址)压入栈中,然后跳转到被调函数的开始位置。主调函数把被调函数的参数也压进了堆栈,所以参数也是栈桢的一部分。函数返回后,主调函数需要移除这些参数,它通过把所有参数的总体积加到ESP上来达到目的。如果有一个函数调用链,foo()->bar()->widget();则其堆栈示意图如下: