在学期的过程中,我们可能有很多困惑?
比如:
局部变量是怎么创建的?
为什么局部变量的值是随机值?
函数是怎么传参的?传参的顺序是怎样的?
形参和实参是什么关系?
函数调用是怎么做的?
函数调用结束后是怎么返回的?
当看完这篇文章,学习了函数栈帧的创建与销毁后,上面的问题,自然水落石出,对于接下来的学习,会有很大的帮助。
前言
接下来我要使用的是VS2013的环境,不使用太高级的编译器,是因为越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是有差异的,具体细节取决于编译器的实现。
铺垫
为了能看懂接下来的内容,我们先来了解以下几个知识。
1.栈区的使用习惯
我们知道堆区和栈区是相对着使用空间的,堆区从低地址到高地址,而栈区从高地址到低地址。
每个函数的调用,都需要在栈区中开辟一个空间,而这个空间的开辟,是要从高地址向低地址执行。也就是说调用函数时,会从栈区的高地址处开辟空间。
栈帧
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
---------------------------------说白了就是用来记录的。
寄存器
寄存器是CPU内部用来存放数据的一些小型存储区域,用来占时存放参与运算的数据和运算结果。
常见的寄存器有:eax,ebx,ecx,edx,ebp,esp。
其中 ebp 与esp 这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。ebp通常指向栈低,存储栈低的地址,又称栈低指针。esp 通常指向栈顶,存储栈顶地址,又称栈顶指针。
汇编指令
其他指令请点击https://blog.csdn.net/sinat_27382047/article/details/72810788
接下来要用到的汇编指令
先不用看,等用到了再看
1.add:加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数;
2.sub:减法指令,格式同 add;
3.call:调用函数,一般函数的参数放在寄存器中;
4.ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中;
5.push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;
6.pop:与push相反,esp每次加4(字节),一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中;
一般不会把sub、add这样的算术指令,以及call、ret这样的跳转指令归入堆栈相关指令中。但是实际上在函数参数传递过程中,sub和add最常用来操作堆栈;call和ret对堆栈也有影响。
7.mov:数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的一份。
8.lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。然而lea也同样可以实现mov的操作,例如: lea edi,[ebx-0ch]
方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把(ebx-0ch)放入到了edi中,但是mov指令是不支持第二个操作数是一个寄存器减去一个数值的。
有了上面的知识,接下来我通过一段代码来讲解,栈帧的创建与销毁
#include<stdio.h>
int Add(int a, int b)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
}
因为每次调用函数的时候都要在栈区开辟空间
首先调用main函数 ,ebp esp 存放着一个指向main函数这块空间的地址,
所以 在调用main函数时 ebp esp 这时候用来维护 main函数在栈帧中的这块空间如下图
在调用那个函数,ebp esp 就去维护那个空间,例如接下来就要调用add函数,这时候就ebp esp 就要去维护add函数空间了
所以我们就了解了 原来ebp esp这两个寄存器是用来维护函数栈帧的。
通常 我们把这个ebp叫做栈低指针 esp叫做栈顶指针
int main()
{
009219F0 push ebp
009219F1 mov ebp,esp
009219F3 sub esp,0E4h
009219F9 push ebx
009219FA push esi
009219FB push edi
009219FC lea edi,[ebp-0E4h]
00921A02 mov ecx,39h
int main()
{
00921A07 mov eax,0CCCCCCCCh
00921A0C rep stos dword ptr es:[edi]
int a = 10;
00921A0E mov dword ptr [a],0Ah
int b = 20;
00921A15 mov dword ptr [b],14h
int c = 0;
00921A1C mov dword ptr [c],0
c = Add(a, b);
00921A23 mov eax,dword ptr [b]
00921A26 push eax
00921A27 mov ecx,dword ptr [a]
00921A2A push ecx
00921A2B call _Add (09211DBh)
00921A30 add esp,8
00921A33 mov dword ptr [c],eax
printf("%d\n", c);
00921A36 mov esi,esp
00921A38 mov eax,dword ptr [c]
00921A3B push eax
00921A3C push 925858h
00921A41 call dword ptr ds:[929114h]
00921A47 add esp,8
00921A4A cmp esi,esp
00921A4C call __RTC_CheckEsp (0921136h)
}
00921A51 xor eax,eax
00921A53 pop edi
00921A54 pop esi
00921A55 pop ebx
}
00921A56 add esp,0E4h
00921A5C cmp ebp,esp
00921A5E call __RTC_CheckEsp (0921136h)
00921A63 mov esp,ebp
00921A65 pop ebp
00921A66 ret
从这张图可以看出,main函数是被调用的,那么是被谁调用?
在VS2013 main函数也是被别调用的 是
_tmainCRTStartup 这个然后又被
mainCRTStartup调用
大概就是怎么一个逻辑,
接来进行详细的讲解
main函数是被别调用的,那么调用它的函数的栈帧就已经创建好了
如图
接下里执行汇编语言第一句
002119F0 push ebp
002119F1 mov ebp,esp
002119F3 sub esp,0E4h
002119F9 push ebx
002119FA push esi
002119FB push edi
002119FC lea edi,[ebp+FFFFFF1Ch]
00211A02 mov ecx,39h
00211A07 mov eax,0CCCCCCCCh
00211A0C rep stos dword ptr es:[edi]
002119F0 push ebp
这句话是什么意思呢?
push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;
esp的地址发生了变化,压上去的ebp 里存放的就是_tmain的ebp的值0x012ffe7c
002119F1 mov ebp,esp
把esp 的值放到ebp中
所以这时候ebp就指向
ebp esp是寄存器 ,里面的值发生改变,指向的目标也会改变
mov以后 esp ebp的值相同了
002119F3 sub esp,0E4h
// 继续下一条
esp 减去 0E4h 后就变成
这意味着esp指向的位置要向上指了
终于为main函数开辟空间了
接下来三个push
至于edi esi edx 里面放的是什么 不用管它
它们一会 会通过 POP 弹出去 ’
002119FC lea edi,[ebp-0E4h]
00211A02 mov ecx,39h
00211A07 mov eax,0CCCCCCCCh
00211A0C rep stos dword ptr es:[edi]
上面的意思是 从ebp-0E4h一下的39h(57)次的dword(4个字节)空间都要变成的eax(0cccccccch)的内容 0E4h=57个字节 =228个bit
0x00CFFAE4就是 esp
push 就是压栈 给栈顶上放一个元素
pop 就是出栈 从栈顶删除一个元素
main函数已经开辟了
也进行了初始化了
接下来执行main语句块
局部变量的初始化就是这样创建的
调用Add函数
当我们创建好了a b c变量后,接下来就要调用Add函数了
函数调用前先进行传参
00211A23 mov eax,dword ptr [ebp-14h]
00211A26 push eax
00211A27 mov ecx,dword ptr [ebp-8]
00211A2A push ecx
把b的值放到 寄存器eax 里 ,然后 把eax的值压栈
把a的值放到 ecx 里 , 把ecx 压栈
00211A2B call 002111DB
00211A30 add esp,8
00211A33 mov dword ptr [ebp-20h],eax
call指令调用函数 ,先压栈存放下一条指令的地址 00211A30
为什么要存放这个地址 ,因为调用完Add函数后返回的时候计算机可以通过这个地址,找到下一条执行的语句,继续往下执行
现在才正式进入Add函数里
int Add(int a, int b)
{
002113C0 push ebp
002113C1 mov ebp,esp
002113C3 sub esp,0CCh
002113C9 push ebx
002113CA push esi
002113CB push edi
002113CC lea edi,[ebp+FFFFFF34h]
002113D2 mov ecx,33h
002113D7 mov eax,0CCCCCCCCh
002113DC rep stos dword ptr es:[edi]
int z = 0;
002113DE mov dword ptr [ebp-8],0
z = a + b;
002113E5 mov eax,dword ptr [ebp+8]
002113E8 add eax,dword ptr [ebp+0Ch]
002113EB mov dword ptr [ebp-8],eax
return z;
002113EE mov eax,dword ptr [ebp-8]
}
下面的就是为Add函数创建栈帧
002113C0 push ebp
002113C1 mov ebp,esp
002113C3 sub esp,0CCh
002113C9 push ebx
002113CA push esi
002113CB push edi
002113CC lea edi,[ebp+FFFFFF34h]
002113D2 mov ecx,33h
002113D7 mov eax,0CCCCCCCCh
002113DC rep stos dword ptr es:[edi]
在这之前我们应该知道,我们电脑为main函数开辟空间的范围
如图黄色就是
int z = 0;
002113DE mov dword ptr [ebp-8],0
z = a + b;
002113E5 mov eax,dword ptr [ebp+8]
002113E8 add eax,dword ptr [ebp+0Ch]
002113EB mov dword ptr [ebp-8],eax
return z;
002113EE mov eax,dword ptr [ebp-8]
}
在调用add之前,先传参,为add创建空间,执行a+b时找回,之前传参时压上去的值,压上去的值就等于a b ,所以说,形参只是实参的一份临时拷贝而已。
返回的值是如何带回去的
通过eax
如何回收空间的
002113F1 pop edi
002113F2 pop esi
002113F3 pop ebx
002113F4 mov esp,ebp
002113F6 pop ebp
002113F7 ret
执行完pop三条后
然后把ebp的值赋给esp
接下来关键来了
002113F6 pop ebp
先让esp +4 ,把弹出元素 ,把元素放到ebp中,也就是把ebp-main 放到ebp中,这时候ebp瞬间指向 维护main函数了
形参是如何销毁的
00211A30 add esp,8
Add函数空间回收后,ret ,弹出,执行弹出元素的执行地址,
最后就把eax的值赋值给了c;
============================================
上述描述的比较通俗,只是讲解了每条汇编指令具体是怎么做的,
如果上述描述不清楚,可以使用VS2013,自己跟着走一遍
下面是问题的解答