1.基本概念
我们先要明白一些基本概念,这样方便我们的研究过程,汇编指令不方便列出,所以给出一些基本概念方便理解。
首先:
(1)本次实验一个地址等于4个字节:地址-4就是下一个地址。
(2)栈帧:函数调用时开辟的空间
(3)函数调用:每次调用函数都需要开辟一个空间
(4)几种寄存器:
- 1.eax,ebx,ecx,edx:通用寄存器,保留临时数据,函数返回时确保返回值不会被销毁
- 2.ebp和esp这两个寄存器是用来存放地址的,用于维护栈帧
- * ebp 栈底指针
- * esp 栈顶指针
- 3.eip:指令寄存器,用于保存下一条指令的地址
调试相关汇编指令
这个指令调试时会用到,自己调试时可以做一下参考
push:压栈,给栈顶放一个元素,esp的地址值要减小用于同步。
pop:出栈,从栈顶出一个元素,esp的地址值要增大用于同步。
mov:数据转移指令,把数据赋给其他地址
add:加法指令,让esp往下走指向栈顶,用于函数出栈后减小栈帧
sub:减法指令,让esp指向栈顶,主要用于给函数分配栈帧
call:call指令的下一个地址,用于返回函数压入地址和转入目标函数
三个地址的概念
创建栈帧时涉及到三个地址:
(1)低地址:位于main的地址之上
(2)main函数的栈帧:main开辟的空间地址
(3)高地址:位于main地址之下
关系如图所示:
知道了他们的位置,我们接下来研究一下栈帧创建与销毁的过程
2.创建与销毁过程
第一步,发现_tmainRTStractup做准备工作
高地址的位置:
在用vs2013实验中,我们溯源main函数,发现main函数并不是一开始就调用的,而是在_tmainRTStractup中调用的,它存放在高地址中,如图可以看到它的位置。
第二步,发现系统会开辟main函数栈帧,让esp和ebp处于维护位置
1.通过esp减去main空间的大小使得esp位于main函数栈顶(栈顶位于地址值,栈底位于高地址)
esp=ebp-main
esp和ebp是两个指针,代表了函数栈帧的地址范围和空间大小,可以发现栈顶指针esp的地址比栈底指针ebp的地址值小。
2.系统给main函数所有内存地址的内容赋予初值,例如都是cc cc cc cc,这是也是我们输出乱码的原因,但是初始化还是有必要的,这样可以防止地址空间里的值不一样。
第三步,栈中压入一个函数
因为main函数开辟空间后,可能还要调用别的函数,所以我们要调用call指令(不占用空间),它的意思是在call指令后一个地址内存放释放空间后让ebp回来的地址与调用函数的作用,如图所示
call指令后,esp指向call指令后一个地址,为开辟函数栈帧做准备
接下来让ebp指向esp,esp往低地址跑,这样可以开辟函数栈帧了
这样函数栈帧就创建好了
注意一点:
- all指令是不占用空间的,但是它的下一个地址要用于存放两个寄存器指针返回时的地址
第四步,释放一个函数
先让esp指向ebp,意思就函数出栈了,实际上函数里的内存并没有清空,只是象征意义上的,我们知道每为一个入栈的函数分配好内存,会给对应地址的内存全部赋初值,所以我们出栈不需要清空了,不会影响结果。
让f函数出栈
接下来 让存放(ebp-main即bsp上一个地址)的内存出栈,同时把ebp-main的地址结果赋给ebp,这样ebp就回到了main栈帧的底部,同理,由于ebp-main出栈了,esp也会向下移动一个地址,这样esp和ebp就会回到压入该函数前的位置了
注意:
*call不占空间,所以esp往下移动一个地址时不用看call,这里把call忽略就行了
同理,当压栈多个函数时
只需要用到多个call就行了,每个call的下一个地址记载的是上一次ebp的地址,便于返回,每出一个栈ebp会根据call上面的地址回到栈底,esp回到上一个栈顶
函数的调用时内存原理
上一步我们知道了main函数里调用其他函数,会在main低地址处开辟了的概念,我们也想知道函数调用时,形参与实参分别存储在哪里?函数有返回值后返回值存储在哪里?之类的问题,请看图:
(1)实参就是main函数里的参数,他们会放在main的栈帧里
(2)调用的函数的栈帧,都是在main之上低地址里,所以形参会拷贝main栈帧里的实参,他们会放在调用函数栈帧对应的低地址里,这也就是我们常说的形参是实参的拷贝的概念
(3)我们知道函数调用结束后就没了,按理来说对应的空间会被清空,我们该如何解决这个问题呢?我们会把返回值保存在寄存器里,例如eax,而寄存器和内存不在一个空间,这样就可以让他保存下来了
第五步,得到规律和原理
(1)在函数调用的过程中,函数的参数是由右向左入栈的;然后是函数内部的 局部变量(注意static变量是不入栈的);在函数调用结束(函数运行结束)后,局部变量最先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行。
(2)每次出栈,main上函数栈区,所有的内存释放,而esp和ebp会回到原来的维护(初始)位置,而每次入栈,bsp会到esp的位置,而esp的位置会指向新栈顶
3.浅入研究与结论
函数创建的变量是放栈哪个地方的?他们是局部变量还是全局变量?
函数里创建的所有变量都是放在对应函数内存里的,main函数栈帧里放main函数里的变量,其他函数里放他们各自的变量,而且他们都是局部变量,因为每次函数出栈esp就会往下移动,所以这些变量等同于函数一调用完数据就会销毁。还有一点要注意全局变量在静态区里,这里只是简单做一讨论。 注意的是全局变量具有全局性,是存放在静态区的,这里堆里的是变量都是局部变量,存储一般有静态区,堆区,栈区。所以这里不可能出现全局变量的
函数的形参怎么传入,工作原理是什么?
如图,看代码
#include<stdio.h>
int f(int x,int,y)
{
int z;
z=x+y;
return z;
}
int main()
{
int a=8,b=10;
printf("%d",f(a,b));
return 0;
}
可以看到各自函数创建的变量在自己的栈帧里,但是我们的形参并没有在f函数里,而是从右往左的顺序压入栈,可见形参是实参的临时拷贝这句话没有任何问题,具体运算过程应该是y复制b的内容压入栈,x再复制a的内容压入栈,用寄存器eax保存x和y相加的值,再赋给z,这样z就等于a+b的结果了
问题:
函数出栈后结果会消失,那返回值z应该销毁才对,我们怎么得到返回值呢?
答:函数中的返回值z最后会保存在寄存器里,这样就不会随着函数栈帧的销毁而销毁了
esp和ebp起到是什么作用?
esp和ebp起到确定空间大小,象征意义上销毁栈的作用,是维护栈的关键
简单附上一些能看懂的源码仅限理解
#include <stdio.h>
int f(int x,int y)
{
int z;
z = x + y;
return z;
}
int main()
{
int a = 8, b = 10;
printf("%d", f(a, b));
return 0;
}
int f(int x,int y)
{
00241750 push ebp
00241751 mov ebp,esp
00241753 sub esp,0CCh
00241759 push ebx
0024175A push esi
0024175B push edi
0024175C lea edi,[ebp-0Ch]
0024175F mov ecx,3
00241764 mov eax,0CCCCCCCCh
00241769 rep stos dword ptr es:[edi]
0024176B mov ecx,offset _A89CBE4B_源@cpp (024C008h)
00241770 call @__CheckForDebuggerJustMyCode@4 (0241311h)
int z;
z = x + y;
00241775 mov eax,dword ptr [x]
00241778 add eax,dword ptr [y]
0024177B mov dword ptr [z],eax
return z;
0024177E mov eax,dword ptr [z]
}
00241781 pop edi
00241782 pop esi
00241783 pop ebx
00241784 add esp,0CCh
0024178A cmp ebp,esp
0024178C call __RTC_CheckEsp (0241230h)
00241791 mov esp,ebp
00241793 pop ebp
00241794 ret
--- 无源文件 -----------------------------------------------------------------------
00241795 jmp dword ptr [__imp____stdio_common_vfprintf (024B174h)]
0024179B int 3
0024179C int 3
0024179D int 3
0024179E int 3
0024179F int 3
Project2.exe!_JustMyCode_Default(void):
002417A0 push ebp
002417A1 mov ebp,esp
002417A3 pop ebp
002417A4 ret
002417A5 jmp dword ptr [__imp____acrt_iob_func (024B178h)]
002417AB int 3
002417AC int 3
002417AD int 3
002417AE int 3
002417AF int 3
--- F:\cpp code\learn_cppl\Project2\Project2\源.cpp -----------------------------
int main()
{
002417B0 push ebp
002417B1 mov ebp,esp
002417B3 sub esp,0C0h
002417B9 push ebx
002417BA push esi
002417BB push edi
002417BC lea edi,[ebp-18h]
002417BF mov ecx,6
002417C4 mov eax,0CCCCCCCCh
002417C9 rep stos dword ptr es:[edi]
002417CB mov ecx,offset _A89CBE4B_源@cpp (024C008h)
002417D0 call @__CheckForDebuggerJustMyCode@4 (0241311h)
int a = 8, b = 10;
002417D5 mov dword ptr [a],8
002417DC mov dword ptr [b],0Ah
printf("%d", f(a, b));
002417E3 mov eax,dword ptr [b]
002417E6 push eax
002417E7 mov ecx,dword ptr [a]
002417EA push ecx
002417EB call f (02413B6h)
002417F0 add esp,8
002417F3 push eax
002417F4 push offset string "%d" (0247BCCh)
002417F9 call _printf (02413A7h)
002417FE add esp,8
return 0;
00241801 xor eax,eax
}
00241803 pop edi
00241804 pop esi
00241805 pop ebx
00241806 add esp,0C0h
0024180C cmp ebp,esp
0024180E call __RTC_CheckEsp (0241230h)
00241813 mov esp,ebp
00241815 pop ebp
00241816 ret