栈是什么
(1)简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。
(2)栈的生长方向是从高地址到低地址的,这是因为在下文讲的栈帧中,栈就是向下生长的。pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。
何为栈帧
(1)栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。每次函数调用,都为函数开辟一块空间,成为栈帧。
(2)栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
(3)对x86体系的CPU而言,其中
寄存器ebp(base pointer )可称为“帧指针”或“基址指针。
寄存器esp(stack pointer)可称为“ 栈指针”。
要知道的是:
ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。
(4)一般来说,我们将 ebp 到 esp 之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道在哪里获取“被调用者”返回的值;2)“被调用者”需要知道传入的参数在哪里,3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,ebp, esp 等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。
注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念esp所指的栈帧顶部和系统栈的顶部是同一个位置。
代码剖析
#include <stdio.h>
int Sub(int x,int y)
{
int t = 0;
t = x-y;
return t;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Sub(a,b);
return 0;
}
剖析上面代码的运行过程:运行环境:VS2008
给出主函数的汇编代码:
int main()
{
003113F0 push ebp
003113F1 mov ebp,esp
003113F3 sub esp,0E4h
003113F9 push ebx
003113FA push esi
003113FB push edi
003113FC lea edi,[ebp-0E4h]
00311402 mov ecx,39h
00311407 mov eax,0CCCCCCCCh
0031140C rep stos dword ptr es:[edi]
int a = 10;
0031140E mov dword ptr [a],0Ah
int b = 20;
00311415 mov dword ptr [b],14h
int c = 0;
0031141C mov dword ptr [c],0
c = Sub(a,b);
00311423 mov eax,dword ptr [b]
00311426 push eax
00311427 mov ecx,dword ptr [a]
0031142A push ecx
0031142B call Sub (31108Ch)
00311430 add esp,8
00311433 mov dword ptr [c],eax
return 0;
00311436 xor eax,eax
}
00311438 pop edi
00311439 pop esi
0031143A pop ebx
0031143B add esp,0E4h
00311441 cmp ebp,esp
00311443 call @ILT+315(__RTC_CheckEsp) (311140h)
00311448 mov esp,ebp
0031144A pop ebp
0031144B ret
接下来分析这段汇编代码:
在这里我们要知道在VC++下,连接器对控制台程序设置的入口函数是 _mainCRTStartup,mainCRTStartup 再调用main 函数;
所以当我们操作时,首先会给mainCRTStartup()函数开辟一段空间,然后esp和ebp在他们所在的位置
(1)push ebp
push就是压栈,把ebp的地址压入栈中,
注意:每次压栈后,esp都指向最新的栈顶位置。
(2)mov ebp,esp
使ebp=esp,即ebp也指向栈顶位置。
(3)为main函数预开辟空间
003113F3 sub esp,0E4h
(4)3个push 以及初始化开辟的空间
003113F9 push ebx
003113FA push esi
003113FB push edi
003113FC lea edi,[ebp-0E4h]
00311402 mov ecx,39h
00311407 mov eax,0CCCCCCCCh
0031140C rep stos dword ptr es:[edi]
1)3个push 分别把ebx,esi,edi 3个寄存器压入栈中。
2)lea 就是把 [ebp-0E4h]的地址放在edi中,ebp-0E4h是3个push之前esp的位置。
3)2个move操作,ecx寄存器的值为39h,eax为初始化值0CCCCCCCCh 。
4)然后rep stos操作:实际上就是把初始化开辟的空间,初始值为eax寄存器内的值0CCCCCCCCh,从edi开始(edi保存的esp的位置),向高地址的部分进行字节拷贝,每一次拷贝4个字节。拷贝的内容就是eax的内容,拷贝次数为39h次。
注意:用0xccccccccch初始化,所以未初始化的字符串,经常看到“烫烫”
可以查看内存值的变化,来验证
(5)实参入栈
int a = 10;
0031140E mov dword ptr [a],0Ah
int b = 20;
00311415 mov dword ptr [b],14h
int c = 0;
0031141C mov dword ptr [c],0
(6)调用Sub函数准备,形参入栈
形参从右向左入栈的,看出形参是实参的一份拷贝
c = Sub(a,b);
00311423 mov eax,dword ptr [b]
00311426 push eax
00311427 mov ecx,dword ptr [a]
0031142A push ecx
ebp-8就是b的位置,ebp-4就是a的位置
(7)call指令
0031142B call Sub (31108Ch)
00311430 add esp,8
call指令就是把下一条指令add的地址31108Ch压入栈中
给出Sub函数的汇编代码:
int Sub(int x,int y)
{
003113A0 push ebp
003113A1 mov ebp,esp
003113A3 sub esp,0CCh
003113A9 push ebx
003113AA push esi
003113AB push edi
003113AC lea edi,[ebp-0CCh]
003113B2 mov ecx,33h
003113B7 mov eax,0CCCCCCCCh
003113BC rep stos dword ptr es:[edi]
int t = 0;
003113BE mov dword ptr [t],0
t = x-y;
003113C5 mov eax,dword ptr [x]
003113C8 sub eax,dword ptr [y]
003113CB mov dword ptr [t],eax
return t;
003113CE mov eax,dword ptr [t]
}
003113D1 pop edi
003113D2 pop esi
003113D3 pop ebx
003113D4 mov esp,ebp
003113D6 pop ebp
003113D7 ret
进入Sub函数:
步骤其实大致和main函数一样
(1)为Sub函数准备
003113A0 push ebp
此时ebp指向的main函数的栈底指针
003113A1 mov ebp,esp
003113A3 sub esp,0CCh
003113A9 push ebx
003113AA push esi
003113AB push edi
003113AC lea edi,[ebp-0CCh]
003113B2 mov ecx,33h
003113B7 mov eax,0CCCCCCCCh
003113BC rep stos dword ptr es:[edi]
(2)以上代码与main函数2,3,4步骤差不多,简单说就是让ebp指向esp指向的位置,为Sum函数分配栈帧,esp指向栈顶,将寄存器ebx、esi、edi依次压入栈顶,edi的值是ebp-0CCh,然后依次从edi的值开始初始化开辟的空间。
(3)指向Sub函数,计算差值
int t = 0;
003113BE mov dword ptr [t],0
t = x-y;
003113C5 mov eax,dword ptr [x]
003113C8 sub eax,dword ptr [y]
003113CB mov dword ptr [t],eax
return t;
003113CE mov eax,dword ptr [t]
在VS中可以通过变量名找到地址。
把t初始化为0,然后计算t=x-y,把ebp+8(x)的值(a) 存放在eax,然后把eax值与ebp+12(y)的值(b)相减放在eax中,然后把eax值保存在t中返回值 t,把ebp-4内的值(t)取出放在eax中。
(4)函数调用结束,释放栈帧
这里先介绍一个概念:
现场保护 当出现中断时,把CPU现在的状态,也就是中断的入口地址保存在寄存器中,随后转向执行其他任务,当任务完成,从寄存器中取出地址继续执行。保护现场其实就是保存中断前一时刻的状态不被破坏。保护现场通过利用一系列PUSH指令保护CPU现场,即将相关寄存器的内容入栈保护起来。所以要把ebp 入栈push。
003113D1 pop edi
003113D2 pop esi
003113D3 pop ebx
003113D4 mov esp,ebp
003113D6 pop ebp
接下来的指令就是返回,先进行3次出栈,把栈顶的指令分别给了edi,esi,ebx三个寄存器。然后把ebp给了esp,这时也就是让esp指向了ebp的位置,这是ebp和esp指向同一位置,这个位置就是你所保存的main函数的ebp,然后再pop ebp,这样ebp就维护到main函数的栈帧了。
003113D7 ret
在这,当ret指令执行之后,会pop一下,把这个地址pop以后,就从Sub函数返回了main函数,这也是最初为什么要保存这个地址的原因。这样call指令就完成了。此时指向main函数中call指令的下一条指令add。
(5)
00311430 add esp,8
00311433 mov dword ptr [c],eax
return 0;
00311436 xor eax,eax
}
00311438 pop edi
00311439 pop esi
0031143A pop ebx
0031143B add esp,0E4h
00311441 cmp ebp,esp
00311443 call @ILT+315(__RTC_CheckEsp) (311140h)
00311448 mov esp,ebp
0031144A pop ebp
0031144B ret
main函数中:
esp+8 :把形参a,b 释放
mov dword ptr [c],eax:把eax中值(返回值t)保存在ebp-12(c的位置)中
接下来,和对Sub函数的返回类似,对main函数返回,然后再销毁main函数,执行ret指令。
参考:
http://m.blog.csdn.net/wenqiang1208/article/details/74353303
https://segmentfault.com/a/1190000007977460
继续学习:
http://www.cnblogs.com/java20130723/archive/2013/07/24/3211358.html:
http://blog.csdn.net/jelly_9/article/details/53239718
http://blog.csdn.net/rxan1234/article/details/49862043
http://15129279495.blog.51cto.com/10845420/1735749