今天写一个两数相加函数,由此来简单探讨下函数栈帧的创建与销毁。
栈帧
首先来说明一下什么是栈?
在数据结构中,栈是一个线性表,限定在表尾进行插入和删除的操作,
他按照后进先出的原则来存储和释放数据。
最先进入栈的数据压入栈底,称为压栈或进栈,
最后进来的数据为栈顶,需要读取数据时从栈顶读取,称为弹栈或出栈。
栈数据结构如同手枪弹夹,最先放入弹夹的子弹,最后才被射出去。
在计算机系统中,栈可以称为栈内存,是一个动态内存区域,
存储函数内部的局部变量和所调用函数的参数值。
栈用于维护函数调用的上下文。
而栈帧是什么呢?
我们将由下面的代码来说明:
int Add(int x, int y)
{
int z=0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c=0;
c = Add(a, b);
return 0;
}
首先,在程序跑起来时,会有一个入口函数mainCRTStartup,然后该函数会调用main函数;因此,首先会在栈上为函数开辟mainCRTStartup开辟一段空间。
1.在系统中程序执行时,栈都是从高地址往低地址增长的;
2.一个函数的栈帧用ebp和esp这两个寄存器来划定范围,ebp指向当前栈帧
的底部,esp始终指向当前栈帧
的顶部;
下面我们直接进入main函数查看反汇编代码:
进入main函数的栈帧
int main()
{
//重新调整esp和ebp的指向,对齐当前的栈帧的栈顶与栈底。
push ebp
mov ebp,esp
sub esp,0E4h
//
push ebx
push esi
push edi
//
lea edi,[ebp-0E4h]
mov ecx,39h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
接下来我们逐行分析:
push ebp
push
为压栈操作,将ebp的值压入栈顶,此时esp也会实时追随指向栈顶,如图:
mov ebp,esp
mov
为移动指令,将esp移入ebp中,即让ebp指向esp的位置,
sub esp,0E4h
sub
为减指令,让esp减去E4(16进制数)个字节数,让esp指向了esp-0E4h的位置处。此时由ebp和esp所维护的内存空间即是main函数的栈帧大小:
push ebx
push esi
push edi
压入三个寄存器
ebx
是"基地址"(base)寄存器, 在内存寻址时存放基地址。
esi/edi分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
lea edi,[ebp-0E4h]
lea(load effective address):加载有效地址,将ebp-0E4h的地址存放到edi处
mov ecx,39h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
eax
是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
ecx
是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
该三行汇编代码的意思是:将39h(=E4/4)给ecx,将0cccccccch给eax;
从edi开始的往下的ecx行的所有内存填充eax:
接后续反汇编代码
int a = 10;
mov dword ptr [ebp-8],0Ah
int b = 20;
mov dword ptr [ebp-14h],14h
int ret = 0;
mov dword ptr [ebp-20h],0
ret = Add(a,b);
mov eax,dword ptr [ebp-14h]
push eax
mov ecx,dword ptr [ebp-8]
push ecx
call 010F10E6
add esp,8
mov dword ptr [ebp-20h],eax
return 0;
xor eax,eax
}
继续逐过程分析:
int a = 10;
mov dword ptr [ebp-8],0Ah
在[ebp-8]指针所指的4字节内存空间中放入a的值:0Ah(十进制=10);
int b = 20;
mov dword ptr [ebp-14h],14h
int ret = 0;
mov dword ptr [ebp-20h],0
与上代码同理
查看内存
这样,a,b,c的值都存入了main的栈帧中。
下面进入add函数
c=Add(a, b);
mov eax,dword ptr [ebp-14h]
push eax
mov ecx,dword ptr [ebp-8]
push ecx
在进入函数前,先将存在[ebp-14h](b的值) 内存空间中的值(b的值)交给eax,然后压栈, [ebp-8]内存空间中的值(a的值)交给ecx,随后压栈。这个步骤便是函数的传参
注意,函数的传参是从右往左的,先拷贝b的值压栈,再拷贝a的值压栈
。
继续往下走:
call 006910B4
call
为调用指令,他将调用处于006910B4处的指令。同时call指令将下一行指令的地址进行压栈
。
注意此时在反汇编界面右击打开“显示地址
”
注意call下一行指令的地址为006917F7
,这一地址作为值进行压栈,esp自动跟随指向栈顶。
这样在函数调用结束后就能自动进入call之后的指令了!
调用Add函数
int Add(int x, int y)
{
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0Ch]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
int z=0;
mov dword ptr [ebp-8],0
z = x + y;
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
mov dword ptr [ebp-8],eax
return z;
mov eax,dword ptr [ebp-8]
}
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
逐步分析:
1)
push ebp
和main函数开始时的汇编代码一致:
首先push(压栈)ebp,此时的ebp的值为之前指向main函数栈底的地址
,此刻esp依旧自动指向栈顶;
esp中存放着main栈帧的ebp的值
2)
mov ebp,esp
将esp的值给ebp,时ebp也指向同一位置:
3)
sub esp,0CCh
push ebx
push esi
push edi
esp减去0cch个字节数,再接连压入ebx,esi,edi
4)
lea edi,[ebp-0Ch]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
同样再Add的栈帧中放入0CCCCCCCCh
,共放入33h行:
5)
int z=0;
mov dword ptr [ebp-8],0
z = x + y;
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
将0存入ebp-8的位置中,即z的内存空间的位置,再将ebp+8的值(实参a的拷贝)给eax,然后将eax与ebp+0Ch的值(实参b的拷贝)相加:
6)
mov dword ptr [ebp-8],eax
将得到eax的值放到z的内存空间中。
7)
return z;
mov eax,dword ptr [ebp-8]
返回z的值,将z位置中的值放入eax中。
8)
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
将edi,esi,ebx相继弹栈,再把ebp的值给esp,使esp指向Add栈帧的栈底位置,实现add的回收。此时完成了Add函数的调用
。
pop ebp
表示将栈顶弹出的值放到ebp中,栈顶此时放的是main函数的栈底指针,于是ebp可顺利找回main函数栈底的位置去了。
最后的ret
会使指令跳到栈顶esp所指的地址中去,也就是之前留下的call指令的下一行指令的地址
。
返回main函数
call 006910B4
add esp,8
mov dword ptr [ebp-20h],eax
return 0;
这时来到了call指令的下一行:
add esp,8
为esp加8个字节,意味着实参a,b的拷贝相继销毁:
mov dword ptr [ebp-20h],eax
将之前函数的返回值eax放入main栈帧中的c开辟的空间中。
最后main函数调用结束。