如果你对局部变量的创建、函数的传参、函数的调用和返回等C语言指令的实现的感到不求甚解,或者你想了解他们的具体逻辑,那么弄明白函数栈桢的创建和销毁绝对是你解惑的不二选择。
首先想要观察函数栈桢的创建和销毁我们必须要有具体相关的代码,在运行这些代码时使用编译器观察代码运行时具体的函数创建和销毁过程:
这里我们给出一个较为简单的Add(相加)函数,且能不省略步骤我们就不省略,便于我们的运行和观察。
了解函数栈桢的创建和销毁,我们需要先了解以下概念:
① 寄存器:诸如eax,ebx,edx,ebp,esp,其中esp ebp两寄存器中存放的是地址,他们用来维护 函数栈桢,调用某个函数时,esp、ebp两寄存器就维护这个函数的栈桢空间,值 得一提的还有在进行压栈时,esp的指向会随着增加,它会保值永远包含整个栈 桢空间
② 每一个函数的调用,都需要在栈区上创建一个空间,事实上main函数并不是第一个函数, 同样也是被调用的一个函数,在不同的编译器下调用它的函数或许并不相同,在VS 2013 中调用main函数的函数名为__tmainCRTStartup ,也即是说:我们一开始观察到的就是 main函数栈桢的创建
③ 栈桢的创建和销毁在栈区上进行,且先使用高地址后使用低地址
刚才我们提到main函数被__tmainCRTStartup 函数调用,我们知道了esp、ebp两寄存器会维护正被调的函数,这涉及到这两个寄存器的动态移动过程:
当__tmainCRTStratup 函数调用main函数之前,esp、ebp分别维护着这个栈桢空间的栈顶和栈底,所以他们也被称作栈顶指针和栈底指针。接下来我们具体分析开始调用main函数时栈桢的变化细节:
这些数据除了代码本身外被分作三类,从左到右分别是地址、指令操作、操作对象
我们从上到下依次分析:
①:push ebp push即为压栈操作,将ebp从栈底压到栈顶
②:mov ebp esp 该操作为把esp的值交给ebp,使ebp来到esp的位置:
③: sub esp,0E4h sub即为减,该操作为将esp的地址减去0E4h,使esp上移
此时就已经完成了对main函数栈桢的开辟。
④: push ebx
push esi
push edi push压栈,将三个寄存器压到栈顶
⑤:lea (load effective address) edi,【ebp-0E4h】 该操作为加载有效地址
⑥:move ecx,39h
mov eax,0CCC CCCCCh
rep stos dword ptr 【edi】 该操作为将从edi开始,向下39h位置都赋值为eax中的 值,也就是0CCCCCCCCh
那么截止到这里,main函数的调用准备工作才算完成,接下来才会正式开始对main函数内的代码进行运行。
dword ptr 【ebp-8】,0Ah
dword ptr 【ebp-14h】,14h
dword ptr 【ebp-20h】,0
变量的创建,分别在main函数的栈桢空间中由变量定义的先后,从高地址到低地址分别创 建存放变量。从这里我们也可以看出为什么局部变量如果不初始化会是一个随机值,因为用来存放局部变量的栈桢空间是main函数的栈桢空间,而在我们创建main函数的栈桢空间时这些栈桢就被我们赋值为CCCCCCCC。
接下里开始在main函数中调用Add函数:
在调用之前,编译器会根据参数的具体情况先从右到左创建形式参数并分别将他们压栈。
mov eax,dword ptr 【ebp-14h】
push eax
mov ecx,dword ptr 【ebp-8】
push ecx 分别将变量b和a的值赋给寄存器eax和ecx,再分别将两个寄存器压栈
现在开始调用Add函数
call表示调用函数,值得我们特别注意的是 add,他将call指令地址的相邻低地址压栈,这一操作 方便调用函数结束后返回main函数继续执行main函数,再将main函数的ebp压栈
接下来我们就正式进入Add函数之中
标蓝部分是不是很眼熟呢,没错,这和刚进入main函数时情况大致相同,都是为调用函数开辟栈桢空间,这里就不再赘述,直接以图表示:
接下来正式进入调用函数Add中,
mov dword ptr 【ebp-8】,0 创建变量c,初始化
mov eax,dword ptr 【ebp+8】
add eax,dword ptr 【ebp+0Ch】
mov dword ptr 【ebp,eax】 找到形式变量a,b的位置,将他们相加赋值给eax寄存器
从这里我们也可以看出函数传参的方式:在尚未进入调用函数时编译器就会根据你将要传的参从右到左依次依次将参数的值赋给寄存器,再将寄存器压栈作为形式参数,以方便在调用函数中找到这些形式参数,由此我们也可以知道为什么在调用函数中正常改变形参的值,对实参没有影响。为什么会说形参是实参的一份临时拷贝。
接下来是返回值,在函数调用结束时,我们知道函数内部创建的局部变量会被销毁,函数的栈桢空间也会释放返还给计算机系统,如果我们返回变量也就行不通,所以我们选择将变量的值赋值给寄存器,返回寄存器的内容,以此保证返回值的准确。
现在函数调用即将结束,为被调用函数所创建的栈桢空间也会释放:
pop edi
pop esi
pop ebx pop为出栈操作,将三个寄存器弹出被调函数的栈桢空间
mov esp,ebp 这和开创栈桢空间时恰恰相反使esp来到ebp的位置
pop ebp 将main函数的ebp弹出,这会使ebp回到main函数原来的位置,也即是说,调用Add函数彻底结束,现在ebp和esp又重新回到了此时被调用函数main原来的位置,维护main函数
ret 返回调用指令的下一条相邻地址,之后继续执行main函数
综上,我们基本了解了函数栈桢的创建和销毁的过程,希望可以帮到你!