关键词:
栈区:就是一个内存地址空间,每调用一次函数就会在栈区为此函数分配一段空间(主要用于存储局部变量,
此段空间下面就直接定义为函数栈)
ebp :用于存放函数栈的栈顶地址
esp:用于存放此函数栈的栈底地址
注意:栈顶地址大于栈底地址,栈是从栈顶向栈底增长。即ebp-->esp;
下面我们分析如下代码例子,看看它的调用机制
#include<stdio.h>
int fun(int c)
{
int d=c+1;
return d;
}
int main()
{
int a=1;
int b=fun(a);
return 0;
}
int a=1;
汇编代码: mov dword ptr [ebp-4],1
很显然,ebp是用作的了基址寄存器,注意ebp的值是不会变的,它会固定指向函数栈的栈顶,在这里ebp此时的值是0x0013FF80
int b=fun(a);
汇编代码:
mov eax,dword ptr [ebp-4]
push eax
call @ILT+0(fun) (00401005)
add esp,4
mov dword ptr [ebp-8],eax
注意前面两句, [ebp-4]显然是a的地址,这两句是将参数a压入esp指向的栈中(即栈底)在这里esp此时的值是0x0013FF2C(
即编译器给main函数局部变量分配的栈空间为0x0013FF80-0x0013FF2C=54H的栈空间)
当执行完了第二条指令之时 esp的值减4H变为0x0013FF28,所以可以看出函数参数是被放在了main函数栈的下面,紧挨栈底。
然后系统自动继续将函数返回地址(即上面第四条语句的地址)压入栈中,此时esp的值0x0013FF24.
call指令调用fun函数,在fun函数开始执行自己代码之前会先执行如下代码(这在函数调用中是必须的)
push ebp
mov ebp,esp
sub esp,44h
push ebx
push esi
push edi
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
具体工作如下:
现将main函数栈顶指针ebp压入栈中(这很重要,在函数返回时有用),此时esp为0x0013FF20,然后将此值再存入ebp中,
即下一个fun函数栈的栈顶。fun函数栈底则变为0x0013FF20-44h即,编译器为fun函数局部变量分配了44h的栈空间,
此时esp指向了0x0013FEDC。然后再将ebx,esi,edi中的值压入栈中(在这个简单的例子中,这些寄存器都没用到)然后esp减去CH,
变为了0x0013FED0。后面四条指令与我们分析的主题无直接关系,就不说了,而且在此例中无用。
下面将注意力放在fun函数是如何取参数的:
int d=c+1;
mov eax,dword ptr [ebp+8]
add eax,1
mov dword ptr [ebp-4],eax
从第一条指令可以看出fun函数的参数是在[ebp+8]位置的,即在fun函数栈顶的上面,这种机制可以很好的区分函数参数与局部
变量,之所以加8是因为在ebp的上面还有main函数返回地址。然后才是函数参数。
现在可以将注意力集中在fun函数是如何返回的:
返回代码如下:
mov eax,dword ptr [ebp-4]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
第一条指令就是return d;将d的值保存在eax寄存器中。
后面三条POP分别对应前面三条push,注意三条pop之后,在fun函数栈底一端任务是完成了。
后面的mov指令将esp重新指向了0x0013FF20,此地址中的值正好是main函数栈的栈顶的地址。
然后执行 POP ebp将ebp重新指向main函数栈顶。
注意ret返回指令不是这么简单的,是由硬件自动执行了隐藏的指令,我的分析是这样的:
在执行POP ebp之后,esp则正好指向了原来保存的main函数返回地址。即相当于这条指令:pop eip;
这就是整个调用过程了。
但是不要忘了esp还没有恢复原值呢!这时候esp的上面还有上次传参时保留的参数值a。所以这也就解释了前面第一段汇编代码的
add esp,4指令。这时整个调用过程才彻底结束!