【C语言】函数调用的参数压栈(详解)
[TOC]
前言
在早期的学习中,我们已经了解到当函数传值调用参数的时候,用的是形参。
形参是实参的一份临时拷贝,对形参的改变不会影响实参里的值。
传值调用和传址调用👉点我
今天让我们以汇编语言
来了解函数调用的参数压栈这一知识点
所用编译器:VS2019
不同编译器的实现可能略有不同,以实际为准
1.什么是栈区?
栈,是一种数据结构。
在学习C语言的过程中,我们一般只关注内存中的3个区域,分别是栈区、堆区和静态区。
其中堆区主要用于动态内存管理,在之前的博客中已经和大家介绍过。
详解动态内存管理👉点我
而栈区就是编译器给函数运行分配的空间了。
和堆区空间需要手动分配不同,这一部分空间是编译器自动管理的,函数的栈帧会自动创建,自动销毁。
1.1栈区小知识点
- 栈区的使用是从高地址到低地址
- 栈区的使用遵循先进后出,后进先出
- 栈区的放置是从高地址往低地址放置:
push
压栈 - 删除是从低往高删除:
pop
出栈
2.知识点
1 | //本次使用的代码 |
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开始
先来看第一部分代码,逐条语句进行解释
1 | push ebp//在栈顶开辟存放ebp这一寄存器对应值的空间 |
- lea:load effecticve address 加载有效地址
- dword:double word – 4个字节
1 | lea edi,[ebp-24h]//将ebp-24h的地址放入edi |
按F10往下运行,过rep那一步后,可以看到36个字节的数据都被初始化为0CCCCCCCCh
继续往下运行,可以看到编译器初始化a、b变量的过程
- VS2019下是小端存储
1 | int c =Add(a,b); |
最后这一步call
很关键,后续会用到
3.2 调用Add
按F11,进入Add函数
1 | push ebp//将ebp上移 |
1 | lea edi,[ebp-0Ch]//ebp-0Ch的空间 |
继续往下,寄存器初始化了Z地址处的数据 为0
1 | mov eax,dword ptr [x]//将x的值放入eax |
1 | return z; |
1 | pop edi//出栈,删除为edi创建的栈区 |
1 | call __RTC_CheckEsp (0241244h) |
1 | pop ebp//弹出ebp |
这里执行弹出指令时
- 将ebp所指向的main函数的起始地址赋值给了ebp指针
- esp指针向高位移动一位
最后的结果如下图所示,esp和ebp重新开始维护main函数的栈区空间
3.3 回到main函数
1 | ret |
前面提到call _Add (01A10B4h)
这条指令非常重要,实际上,在执行ret
指令时,esp指针就指向了栈顶存放的call指令的下一条指令的地址,同时,这个地址也被pop掉了
回到调试界面,可以看到黄色小箭头的确指向了call指令的下一条
而这一条指令的意思,是往esp里加8,即向高位移动8个字节。
实际上这条指令就是在销毁我们的形参
1 | 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函数
1 | add esp,8//给esp+8个字节 |
我对这一部分产生了疑惑,重新调试发现
push eax
这一指令让esp往低地址处走了4个字节offset string "%d\n"
这一指令也让esp往低走了4个字节
执行完这一指令后,esp回到了执行printf之前的地址处
3.4 结束程序
1 | return 0; |
1 | add esp,0E4h//esp+0E4h(退出为main函数开辟的空间) |
1 | call __RTC_CheckEsp (0241244h) |
1 | pop ebp//ebp出栈--esp和ebp分离 |
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亿鼠标的枪战梦想……