文章目录


前言

在早期的学习中,我们已经了解到当函数传值调用参数的时候,用的是形参。

形参是实参的一份临时拷贝,对形参的改变不会影响实参里的值。


传值调用和传址调用????​​点我​


今天让我们以​​汇编语言​​来了解​函数调用的参数压栈​这一知识点

所用编译器:VS2019


不同编译器下的实现略有不同,请以实际为准



1.什么是栈区?

栈,是一种数据结构。

在学习C语言的过程中,我们一般只关注内存中的3个区域,分别是栈区、堆区和静态区。

其中​堆区主要用于动态内存管理​,在之前的博客中已经和大家介绍过。


详解动态内存管理????​​点我​


【C语言】函数调用的参数压栈(详解)_c语言

而栈区就是编译器给函数运行分配的空间了。

和堆区空间需要手动分配不同,这一部分空间是编译器自动管理的,函数的栈帧会自动创建,自动销毁。

1.1栈区小知识点

  • 栈区的使用是从高地址到低地址
  • 栈区的使用遵循先进后出,后进先出
  • 栈区的放置是从高地址往低地址放置:​​push​​压栈
  • 删除是从低往高删除:​​pop​​出栈

2.知识点

//本次使用的代码
#include <stdio.h>
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函数的操作。

【C语言】函数调用的参数压栈(详解)_main函数_02

在VS2019中,按F10进行调试,出现黄色小箭头后,​​右键-到反汇编​​,即可打开调试中汇编语言的显示界面

【C语言】函数调用的参数压栈(详解)_函数调用_03

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

【C语言】函数调用的参数压栈(详解)_c语言_04

  • 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​

【C语言】函数调用的参数压栈(详解)_函数调用_05

【C语言】函数调用的参数压栈(详解)_寄存器_06

继续往下运行,可以看到编译器初始化a、b变量的过程

【C语言】函数调用的参数压栈(详解)_c语言_07

【C语言】函数调用的参数压栈(详解)_开发语言_08

  • VS2019下是小端存储

【C语言】函数调用的参数压栈(详解)_寄存器_09


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​​很关键,后续会用到

【C语言】函数调用的参数压栈(详解)_函数调用_10

3.2 调用Add

按F11,进入Add函数

【C语言】函数调用的参数压栈(详解)_main函数_11

【C语言】函数调用的参数压栈(详解)_寄存器_12

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)

【C语言】函数调用的参数压栈(详解)_寄存器_13

【C语言】函数调用的参数压栈(详解)_c语言_14

继续往下,寄存器初始化了Z地址处的数据 为0

【C语言】函数调用的参数压栈(详解)_main函数_15

mov    eax,dword ptr [x]//将x的值放入eax
add eax,dword ptr [y]//将y的值加道eax中,即x+y
mov dword ptr [z],eax//将eax的值放入变量z

【C语言】函数调用的参数压栈(详解)_寄存器_16

return z;
mov eax,dword ptr [z]//将形参z的值放入eax

【C语言】函数调用的参数压栈(详解)_开发语言_17

pop     edi//出栈,删除为edi创建的栈区
pop esi//pop指令会将esi的值放入esi(等于没变)
pop ebx//每pop一次,esp就往高位移动一次

add esp,0CCh//为esp地址+0CCh,即退出Add程序的栈区空间
cmp ebp,esp//将esp的值与ebp进行比较

【C语言】函数调用的参数压栈(详解)_寄存器_18

call        __RTC_CheckEsp (0241244h)  
mov esp,ebp//ebp的值赋给esp,此时esp和ebp相同

【C语言】函数调用的参数压栈(详解)_寄存器_19

pop         ebp//弹出ebp

这里执行弹出指令时

  • 将ebp所指向的main函数的起始地址赋值给了ebp指针
  • esp指针向高位移动一位

最后的结果如下图所示,esp和ebp重新开始维护main函数的栈区空间

【C语言】函数调用的参数压栈(详解)_开发语言_20

3.3 回到main函数

ret

前面提到​​call _Add (01A10B4h)​​​这条指令非常重要,实际上,在执行​​ret​​指令时,esp指针就指向了栈顶存放的​call指令的下一条指令的地址​,同时,这个地址也被pop掉了

【C语言】函数调用的参数压栈(详解)_main函数_21

回到调试界面,可以看到黄色小箭头的确指向了call指令的下一条

【C语言】函数调用的参数压栈(详解)_函数调用_22

而这一条指令的意思,是往esp里加8,即向高位移动8个字节。

实际上这条指令就是在​销毁我们的形参

【C语言】函数调用的参数压栈(详解)_寄存器_23

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函数

【C语言】函数调用的参数压栈(详解)_main函数_24

add  esp,8//给esp+8个字节

我对这一部分产生了疑惑,重新调试发现

  • ​push eax​​这一指令让esp往低地址处走了4个字节
  • ​offset string "%d\n"​​这一指令也让esp往低走了4个字节

【C语言】函数调用的参数压栈(详解)_c语言_25

执行完这一指令后,esp回到了执行printf之前的地址

【C语言】函数调用的参数压栈(详解)_main函数_26

3.4 结束程序

return 0;
xor eax,eax//xor指令是异或--在这里的作用不清楚
}
pop edi//出栈--esp对应移动
pop esi//
pop ebx//

【C语言】函数调用的参数压栈(详解)_寄存器_27

add    esp,0E4h//esp+0E4h(退出为main函数开辟的空间) 
cmp ebp,esp//比较ebp和esp

【C语言】函数调用的参数压栈(详解)_main函数_28

【C语言】函数调用的参数压栈(详解)_开发语言_29

call   __RTC_CheckEsp (0241244h)  
mov esp,ebp//将ebp的值复制给esp
//此时esp和ebp的值依旧相同

【C语言】函数调用的参数压栈(详解)_寄存器_30

pop    ebp//ebp出栈--esp和ebp分离
ret//main函数结束

【C语言】函数调用的参数压栈(详解)_函数调用_31


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亿鼠标的枪战梦想……