文章目录
- 前言
- 1.什么是栈区?
- 2.知识点
前言
在早期的学习中,我们已经了解到当函数传值调用参数的时候,用的是形参。
形参是实参的一份临时拷贝,对形参的改变不会影响实参里的值。
传值调用和传址调用????点我
今天让我们以汇编语言
来了解函数调用的参数压栈这一知识点
所用编译器:VS2019
不同编译器下的实现略有不同,请以实际为准
1.什么是栈区?
栈,是一种数据结构。
在学习C语言的过程中,我们一般只关注内存中的3个区域,分别是栈区、堆区和静态区。
其中堆区主要用于动态内存管理,在之前的博客中已经和大家介绍过。
详解动态内存管理????点我
而栈区就是编译器给函数运行分配的空间了。
和堆区空间需要手动分配不同,这一部分空间是编译器自动管理的,函数的栈帧会自动创建,自动销毁。
1.1栈区小知识点
- 栈区的使用是从高地址到低地址
- 栈区的使用遵循先进后出,后进先出
- 栈区的放置是从高地址往低地址放置:
push
压栈 - 删除是从低往高删除:
pop
出栈
2.知识点
//本次使用的代码
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
2.1 寄存器
常见寄存器有eax、ebx、ecx、edx,其中ebp和esp较为特殊
ebp、esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的
- eax/ebx/ecx/edx:通用寄存器,保留临时数据
- ebp:栈低指针
- esp:栈顶指针
- eip:指令寄存器,保存当前指令的下一条指令的地址
2.2 主函数调用
每一个函数调用,都要在栈区创建一个空间
我们知道main
函数是程序的入口
实际上,main
函数也是被其他函数调用的
-
mainCRTStartup
函数调用__tmainCRTStartup
-
__tmainCRTStartup
函数调用main
函数
编译器会先在内存高地址处开辟一部分空间给mainCRTStartup
和__tmainCRTStartup
函数,它们进行调用main函数的操作。
在VS2019中,按F10进行调试,出现黄色小箭头后,右键-转到反汇编
,即可打开调试中汇编语言的显示界面
3. 逐条解释
3.1 从main开始
先来看第一部分代码,逐条语句进行解释
push ebp//在栈顶开辟存放ebp这一寄存器对应值的空间
mov ebp,esp//将esp的值传入ebp中(即将ebp指针移动到原本esp指向的位置)
sub esp,0E4h//将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置)
push ebx//在栈顶放入ebx
push esi//在栈顶放入esi
push edi//在栈顶放入edi
- lea:load effecticve address 加载有效地址
- dword:double word – 4个字节
lea edi,[ebp-24h]//将ebp-24h的地址放入edi
mov ecx,9//将9放入ecx,对应十进制36
mov eax,0CCCCCCCCh//将0CCCCCCCCh放入eax
rep stos dword ptr es:[edi]//将edi往下ecx个地址的数据全部初始化为0CCCCCCCCh
按F10往下运行,过rep那一步后,可以看到36个字节的数据都被初始化为0CCCCCCCCh
继续往下运行,可以看到编译器初始化a、b变量的过程
- VS2019下是小端存储
int c =Add(a,b);
mov eax,dword ptr [b]//把b的内容放入eax
push eax //在栈顶放入eax
mov ecx,dword ptr [a]//把a的内容放入ecx
push ecx //在栈顶放入ecx
call _Add (01A10B4h) //在栈顶放入该地址(call指令下一条指令的地址)
最后这一步call
很关键,后续会用到
3.2 调用Add
按F11,进入Add函数
push ebp//将ebp上移
mov ebp,esp//将esp内容放入ebp(移动ebp)
sub esp,0CCh//esp+0CCh(为Add开辟空间)
push ebx//在栈顶放入ebx
push esi//在栈顶放入esi
push edi//在栈顶放入edi
lea edi,[ebp-0Ch]//ebp-0Ch的空间
mov ecx,3//3存入ecx
mov eax,0CCCCCCCCh//存入eax
rep stos dword ptr es:[edi]//esp往下0ch的空间初始化
mov ecx,offset _6A27082D_test@c (024C003h)
call @__CheckForDebuggerJustMyCode@4 (024131Bh)
继续往下,寄存器初始化了Z地址处的数据 为0
mov eax,dword ptr [x]//将x的值放入eax
add eax,dword ptr [y]//将y的值加道eax中,即x+y
mov dword ptr [z],eax//将eax的值放入变量z
return z;
mov eax,dword ptr [z]//将形参z的值放入eax
pop edi//出栈,删除为edi创建的栈区
pop esi//pop指令会将esi的值放入esi(等于没变)
pop ebx//每pop一次,esp就往高位移动一次
add esp,0CCh//为esp地址+0CCh,即退出Add程序的栈区空间
cmp ebp,esp//将esp的值与ebp进行比较
call __RTC_CheckEsp (0241244h)
mov esp,ebp//ebp的值赋给esp,此时esp和ebp相同
pop ebp//弹出ebp
这里执行弹出指令时
- 将ebp所指向的main函数的起始地址赋值给了ebp指针
- esp指针向高位移动一位
最后的结果如下图所示,esp和ebp重新开始维护main函数的栈区空间
3.3 回到main函数
ret
前面提到call _Add (01A10B4h)
这条指令非常重要,实际上,在执行ret
指令时,esp指针就指向了栈顶存放的call指令的下一条指令的地址,同时,这个地址也被pop掉了
回到调试界面,可以看到黄色小箭头的确指向了call指令的下一条
而这一条指令的意思,是往esp里加8,即向高位移动8个字节。
实际上这条指令就是在销毁我们的形参
mov dword ptr [c],eax//将eax中的值放入变量c
此时eax中存放的就是Add函数的返回值
这里我们可以得出一个结论:
自定义函数的返回值是通过寄存器这一中间“变量”,返回主函数中的。
- 先把返回值放入寄存器A
- 主函数从寄存器A中取出返回值,放入接受返回值的变量
继续往下,可以看到printf("%d\n", c);
语句后,编译器又一次将变量c的值放回了eax
实际上这里是printf函数的运行:
- 先把待打印变量放入eax
- 在栈顶压入eax
-
offset string "%d\n"
(猜测是数据类型检查) -
_printf
:执行printf函数
add esp,8//给esp+8个字节
我对这一部分产生了疑惑,重新调试发现
-
push eax
这一指令让esp往低地址处走了4个字节 -
offset string "%d\n"
这一指令也让esp往低走了4个字节
执行完这一指令后,esp回到了执行printf之前的地址处
3.4 结束程序
return 0;
xor eax,eax//xor指令是异或--在这里的作用不清楚
}
pop edi//出栈--esp对应移动
pop esi//
pop ebx//
add esp,0E4h//esp+0E4h(退出为main函数开辟的空间)
cmp ebp,esp//比较ebp和esp
call __RTC_CheckEsp (0241244h)
mov esp,ebp//将ebp的值复制给esp
//此时esp和ebp的值依旧相同
pop ebp//ebp出栈--esp和ebp分离
ret//main函数结束
4.本篇博客中的汇编语言总结
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器往低位走
pop:数据弹出至指定位置,同时esp栈顶寄存器往高位走
sub:减法
add:加法
call:函数调用。1.压入返回地址;2.转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:回复返回地址,压入eip,类似
pop eip
指令cmp(比较):执行从目的操作数中减去源操作数的隐含减法操作,并且不修改任何操作数
xor:在两个操作数的对应位之间进行(按位)逻辑异或(XOR)操作,并将结果存放在目标操作数中
部分汇编指令参考:C语言中文网
5.结语
完成这篇博客的时候,已经是周六的00:54????
从周五的19:20开始,不知不觉中写了这么久
函数调用堆栈这一部分的知识有些晦涩难懂,写下这篇博客也算是理清了一些思路吧。
加油!
顶不住了,睡觉去了
说来讽刺,几个舍友还在打某3亿鼠标的枪战梦想……