C/C++子函数参数传递,堆栈帧、堆栈参数详解
导语
因为参数传递和汇编语言有很大联系,之后会出现较多x86
汇编代码。
该文会先讲一下x86
的堆栈参数传递过程,然后再分析C/C++
子函数是怎样通过堆栈传递参数的。
注:汇编语言的过程和C/C++的子函数是一回事。
寄存器参数,存储器参数和堆栈参数都可以用于x86
汇编乃至其他汇编语言传递参数的方式。但C/C++
在编译时,编译器会对子函数使用堆栈参数传递方式。
三种参数传递方式对比:
寄存器参数
...
mov eal,4
call Proc_using_eal
...
存储器参数
.data
temp DB ?
.code
...
mov temp,4
call Proc_using_temp
...
堆栈参数
...
push 4
call Proc_using_stack
...
x86堆栈参数传递过程
考虑一个过程add_num
,该过程有两个输入参数,一个输出参数。其功能是将两个输入参数求和并将其结果输出。下面这个例子中使用堆栈将3, 4两个参数输入到add_num
中。
push 3
push 4
call add_num
执行call指令前,堆栈如下:
其中ESP
为x86CPU使用的堆栈指针,每进行一次入栈操作,ESP
要减4(32位CPU
)(图上堆栈向上地址减小,向下地址增加)
明显的是,add_num
只需要把堆栈中相应的变量取出来使用就可以了。堆栈参数传递的确也是这么做,但是却要稍稍费事一点。
首先给出add_num
过程的程序
add_num proc
push ebp
mov ebp,esp
mov eax,[ebp+8]
add eax,[ebp+12]
pop ebp
ret
add_num endp
之前笔者给出的堆栈是CPU
执行call
指令前的结果,接下来从开始执行call
指令一步一步分析堆栈的变化情况。
call add_num
执行call add_num
时,ESP
减4后将add_num
过程的返回地址压入堆栈,即当前指令指针EIP
的值(该值为主程序中call
指令的下一条指令(不是push ebp
)的地址)
push ebp
mov ebp,esp
mov eax,[ebp+8]
add eax,[ebp+12]
此时已经进入add_num
过程内部。
这一步是为了将esp的值赋予ebp
。而将ebp
压入堆栈是为了保护ebp
,在add_num
过程结束后还要恢复ebp
的值。
此时esp指向堆栈中的ebp
,而将esp赋予ebp
后,ebp
便指向了堆栈中自己被保护的值。此时ebp
的主要作用是为参数读取提供绝对地址。比如参数4比ebp
所在地址高8Byte
(堆栈一个单元是4Byte
),则过程中要使用参数4时,使用基址-偏移量寻址即可,即[ebp+8]
。
当然这里也可以使用esp
达到相同的效果,但是这个例子没有局部变量。若子过程中有局部变量(局部变量也存放在堆栈里),采用ebp
要方便很多。
pop ebp
此时ebp
弹出,ebp
恢复调用前的值
ret
最后弹出返回地址,程序返回到主程序中,并执行下一条指令
以上为整个堆栈参数传递过程。
需要注意的点:
堆栈帧到底是什么?
堆栈帧(stack frame
)(或活动记录(activation record
))是一块堆栈保留区域,用于存放被传递的实际参数、子程序的返回值、局部变量以及被保存的寄存器。
实际上堆栈帧就相当于子函数的缓存,当子函数使用的堆栈个数最大时,其所拥有的所有部分构成了这个函数的堆栈帧。
以add_num
过程为例,其堆栈帧如下图灰色部分所示。
堆栈帧为什么叫做堆栈帧
“堆栈”很好理解,而“帧”的概念在上面那个例子中的确很难搞通。不久后笔者会分析递归函数中的堆栈帧增消的现象,那个时候“帧”这个概念体现得淋漓尽致。
输入参数3和4留在堆栈里没有释放是可以的吗
上面的例子并没有释放参数4和3,只是为了演示,实际上一定会有相应的代码去释放它。子函数的堆栈帧是包含其输入堆栈变量的,当退出子函数时,其所有的堆栈帧必须被完全释放,否则堆栈就会变得混乱。
释放参数涉及两种子函数调用标准,一种是STDCALL
标准,一种是C标准。两种在参数的堆栈传递细节几乎完全相同,不同的是释放参数的方式。
根据两个标准重新改写add_num
过程:
STDCALL
调用规范
add_num proc
push ebp
mov ebp,esp
mov eax,[ebp+8]
add eax,[ebp+12]
pop ebp
ret 8
add_num endp
C调用规范
...
push 3
push 4
call add_num
add esp,8
两种方式的核心思想就是修改esp,使esp指向堆栈参数3和4所在位置的前一个堆栈。但是STDCALL
调用规范是在过程内部修改esp
(ret 8
为将堆栈中返回地址弹出到EIP
后,再将ESP
加8);C调用规范是在子过程外部,在主调过程修改esp
。
另引用这两种方式的优缺点:
STDCALL不仅减少了子程序调用产生的代码量(减少了一条指令),还保证了调用程序永远不会忘记清除堆栈。另一方面,C调用规范允许子程序声明不同数量的参数,主调程序可以决定传递多少个参数。C语言的printf函数就是一个例子
C语言参数传递分析
我们仍考虑一个子函数有两个输入参数,一个输出参数,实现两个参数相加并输出。
程序如下:
int add_num(int x, int y)
{
return(x+y);
}
int main()
{
int sum;
sum = add_num(3,4);
return(0);
}
编译后输出的汇编代码如下:
; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.21005.1
TITLE D:\MyDocuments\《汇编语言-基于x86处理器》资料\Compile_test\Compile_test\Compile_test\main.c
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB MSVCRTD
INCLUDELIB OLDNAMES
PUBLIC _add_num
PUBLIC _main
EXTRN __RTC_CheckEsp:PROC
EXTRN __RTC_InitBase:PROC
EXTRN __RTC_Shutdown:PROC
; COMDAT rtc$TMZ
rtc$TMZ SEGMENT
__RTC_Shutdown.rtc$TMZ DD FLAT:__RTC_Shutdown
rtc$TMZ ENDS
; COMDAT rtc$IMZ
rtc$IMZ SEGMENT
__RTC_InitBase.rtc$IMZ DD FLAT:__RTC_InitBase
rtc$IMZ ENDS
; Function compile flags: /Odtp /RTCsu /ZI
; File d:\mydocuments\《汇编语言-基于x86处理器》资料\compile_test\compile_test\compile_test\main.c
; COMDAT _main
_TEXT SEGMENT
_sum$ = -8 ; size = 4
_main PROC ; COMDAT
; 7 : {
push ebp
mov ebp, esp
sub esp, 204 ; 000000ccH
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-204]
mov ecx, 51 ; 00000033H
mov eax, -858993460 ; ccccccccH
rep stosd
; 8 : int sum;
; 9 : sum = add_num(3, 4);
push 4
push 3
call _add_num
add esp, 8
mov DWORD PTR _sum$[ebp], eax
; 10 : return(0);
xor eax, eax
; 11 : }
pop edi
pop esi
pop ebx
add esp, 204 ; 000000ccH
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; Function compile flags: /Odtp /RTCsu /ZI
; File d:\mydocuments\《汇编语言-基于x86处理器》资料\compile_test\compile_test\compile_test\main.c
; COMDAT _add_num
_TEXT SEGMENT
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_add_num PROC ; COMDAT
; 2 : {
push ebp
mov ebp, esp
sub esp, 192 ; 000000c0H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-192]
mov ecx, 48 ; 00000030H
mov eax, -858993460 ; ccccccccH
rep stosd
; 3 : return(x + y);
mov eax, DWORD PTR _x$[ebp]
add eax, DWORD PTR _y$[ebp]
; 4 : }
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_add_num ENDP
_TEXT ENDS
END
首先看call _add_num
指令
; 8 : int sum;
; 9 : sum = add_num(3, 4);
push 4
push 3
call _add_num
add esp, 8
很明显使用了C调用规范,在调用完成后从堆栈中删除堆栈参数。
再看add_num
子程序对应的汇编代码:
_add_num PROC ; COMDAT
; 2 : {
push ebp
mov ebp, esp
sub esp, 192 ; 000000c0H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-192]
mov ecx, 48 ; 00000030H
mov eax, -858993460 ; ccccccccH
rep stosd
; 3 : return(x + y);
mov eax, DWORD PTR _x$[ebp]
add eax, DWORD PTR _y$[ebp]
; 4 : }
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_add_num ENDP
其中有两个地方之前没有介绍,
一是:
push ebx
push esi
push edi
...
pop edi
pop esi
pop ebx
这部分代码是为了保护寄存器
二是:
sub esp, 192 ; 000000c0H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-192]
mov ecx, 48 ; 00000030H
mov eax, -858993460 ; ccccccccH
rep stosd
除去push命令,剩下的部分是为了初始化堆栈,将栈顶后192Byte
的空间写入ccccccccH
(个人认为这一步可以不需要,只是用来增加程序稳定性的)
将这两部分删掉后,即可得到:
_add_num PROC ; COMDAT
; 2 : {
push ebp
mov ebp, esp
; 3 : return(x + y);
mov eax, DWORD PTR _x$[ebp]
add eax, DWORD PTR _y$[ebp]
; 4 : }
mov esp, ebp
pop ebp
ret 0
_add_num ENDP
和之前的add_num
的x86
汇编子过程作比较:
add_num proc
push ebp
mov ebp,esp
mov eax,[ebp+8]
add eax,[ebp+12]
pop ebp
ret
add_num endp
两者基本一致。但是编译器给出结果多出一个mov esp,ebp
。这句命令在这里有没有都没有关系,因为这个函数没有局部变量。但是如果有局部变量的话,是一定要加上的。可以自己写一个带有局部变量的函数,自己想一想。下一篇博文会讲述带有局部变量的情况。
C/C++子函数参数传递,堆栈帧、堆栈参数详解的更多相关文章
- socket函数的使用方法(参数详解)
socket函数的使用方法如下: int socket(int domain, int type, int protocol); 在参数表中,domain指定使用何种的地址类型,比较常用的有: PF_ ...
- [概念] js的函数节流和throttle和debounce详解
js的函数节流和throttle和debounce详解:同样是实现了一个功能,可能有的效率高,有的效率低,这种现象在高耗能的执行过程中区分就比较明显.本章节一个比较常用的提高性能的方式,通常叫做&qu ...
- Go语言Slice作为函数参数详解
Go语言Slice作为函数参数详解 前言 首先要明确Go语言中实质只有值传递,引用传递和指针传递是相对于参数类型来说. 个人认为上诉的结论不对,把引用类型看做对指针的封装,一般封装为结构体,结构体是值 ...
- PHP date函数参数详解
PHP date函数参数详解 作者: 字体:[增加 减小] 类型:转载 time()在PHP中是得到一个数字,这个数字表示从1970-01-01到现在共走了多少秒,很奇怪吧 不过这样方便计 ...
- Wordpress菜单函数wp_nav_menu各参数详解及示例
Wordpress菜单函数wp_nav_menu各参数详解及示例 注册菜单 首先要注册菜单,将以下函数添加至function.php函数里 register_nav_menus(array( ...
- str_replace函数的使用规则和案例详解
str_replace函数的使用规则和案例详解 str_replace函数的简单调用: <?php $str = '苹果很好吃.'; //请将变量$str中的苹果替换成香蕉 $strg = st ...
- Python函数参数详解
Python函数参数详解 形参与实参 什么是形参 在定义函数阶段定义的参数称之为形式参数,简称形参,相当于变量名. 什么是实参 在调用函数阶段传入的值称为实际参数,简称实参.相当于"变量值& ...
- C++ 子函数参数传递过程
编译环境:Visual Studio 2015 参数传递与汇编语言有很大关系.子函数传递参数主要方式有三种(这三种参数传递方式都可用用于x86汇编语言甚至其它汇编语言): 寄存器方式传递参数 存储器方 ...
- 栈帧%ebp,%esp详解
首先应该明白,栈是从高地址向低地址延伸的.每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息.寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部( ...
随机推荐
- MySQL按照(windows)及常用命令
MySQL语法规则 关键字与函数名称全部大写 数据库名称.表名称.字段名称全部小写 SQL 语句必须以分号结尾 MySQL安装 MySQL配置: 在cmd中输入 mysql,提示['mysql' 不是 ...
- JTS Geometry
JTS Geometry关系判断和分析 JTS Geometry关系判断和分析 1.关系判断 1.1实例 2.关系分析 2.1实例 JTS(Geometry) JTS Geometry关系判断和分析 ...
- telnet | ping
ping通常是用来检查网络是否通畅或者网络连接速度的命令. ping www.baidu.com 而telnet是用来探测指定ip是否开放指定端口的. telnet xxx 443 查看443开放没 ...
- Centos6.5添加163软件yum源
将yum源设置为163yum,可以提升软件包安装和更新的速度,同时避免一些常见软件版本无法找到.具体设置方法如下: 1,进入yum源配置目录cd /etc/yum.repos.d 2,备份系统自带的y ...
- 自定义tree
function YpTreeMenu(ypTreeMenu,treeObj) { this.ypTreeMenu=ypTreeMenu; this.treeObj=treeObj; this.tre ...
- Linux常用习惯和技巧
1.如果有些命令在执行时不断地在屏幕上输出信息,影响到后续命令的输入,则可以在执行命令时在末尾添加上一个&符号,这样命令将进入系统后台来执行.
- A - A Gifts Fixing
t组询问,每次给出数列长度n 以及两个长度为n的数列{ai}和{bi}. 有三种操作:ai−1, bi−1以及ai,bi同时− 1 -1−1. 问最少多少步以后可以让两个数列变成常数数列. ...
- CF 1326 D. Prefix-Suffix Palindrome
D. Prefix-Suffix Palindrome 题意 给一个字符串 s,求一个字符串 t,t 由 s 的某个前缀以及某个后缀拼接而成,且 t 是回文串,长度不能超过 s.输出最长的 t 分析 ...
- Codeforces Round #678 (Div. 2)【ABCD】
比赛链接:https://codeforces.com/contest/1436 A. Reorder 题解 模拟一下这个二重循环发现每个位置数最终都只加了一次. 代码 #include <bi ...
- 【uva 247】Calling Circles(图论--Floyd 传递闭包+并查集 连通分量)
题意:有N个人互相打了M次电话,请找出所有电话圈(Eg.a→b,b→c,c→d,d→a 就算一个电话圈)并输出.(N≤25,L≤25,注意输出格式) 解法:由于N比较小所有n^2或n^3的复杂度都没有 ...