C/C++子函数参数传递,堆栈帧、堆栈参数详解_1
来源:互联网 发布:js实现格式化html代码 编辑:程序博客网 时间:2024/04/30 02:19
因为参数传递和汇编语言有很大联系,之后会出现较多x86汇编代码。
该文会先讲一下x86的堆栈参数传递过程,然后再分析C/C++子函数是怎样通过堆栈传递参数的。
注:汇编语言的过程和C/C++的子函数是一回事。
寄存器参数,存储器参数和堆栈参数都可以用于x86汇编乃至其他汇编语言传递参数的方式。但C/C++在编译时,编译器会对子函数使用堆栈参数传递方式。
三种参数传递方式对比:
1、寄存器参数
... mov eal,4 call Proc_using_eal ...
2、存储器参数
.data temp DB ?.code ... mov temp,4 call Proc_using_temp ...
3、堆栈参数
... push 4 call Proc_using_stack ...
1、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 retadd_num endp
之前笔者给出的堆栈是CPU执行call指令前的结果,接下来从开始执行call指令一步一步分析堆栈的变化情况。
(1)call add_num
执行call add_num时,ESP减4后将add_num过程的返回地址压入堆栈,即当前指令指针EIP的值(该值为主程序中call指令的下一条指令(不是push ebp)的地址)
(2)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要方便很多。
(3)pop ebp
此时ebp弹出,ebp恢复调用前的值
(4)ret
最后弹出返回地址,程序返回到主程序中,并执行下一条指令
以上为整个堆栈参数传递过程。
这里有几个需要注意的点:
(1)、堆栈帧到底是什么
堆栈帧(stack frame)(或活动记录(activation record))是一块堆栈保留区域,用于存放被传递的实际参数、子程序的返回值、局部变量以及被保存的寄存器。
实际上堆栈帧就相当于子函数的缓存,当子函数使用的堆栈个数最大时,其所拥有的所有部分构成了这个函数的堆栈帧。
以add_num过程为例,其堆栈帧如下图灰色部分所示。
(2)、堆栈帧为什么叫做堆栈帧
“堆栈”很好理解,而“帧”的概念在上面那个例子中的确很难搞通。不久后笔者会分析递归函数中的堆栈帧增消的现象,那个时候“帧”这个概念体现得淋漓尽致。
(3)、输入参数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 8add_num endpC调用规范 ... 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函数就是一个例子
2、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 flatINCLUDELIB MSVCRTDINCLUDELIB OLDNAMESPUBLIC _add_numPUBLIC _mainEXTRN __RTC_CheckEsp:PROCEXTRN __RTC_InitBase:PROCEXTRN __RTC_Shutdown:PROC; COMDAT rtc$TMZrtc$TMZ SEGMENT__RTC_Shutdown.rtc$TMZ DD FLAT:__RTC_Shutdownrtc$TMZ ENDS; COMDAT rtc$IMZrtc$IMZ SEGMENT__RTC_InitBase.rtc$IMZ DD FLAT:__RTC_InitBasertc$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 ENDSEND
首先看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 retadd_num endp
两者基本一致。但是编译器给出结果多出一个mov esp,ebp。这句命令在这里有没有都没有关系,因为这个函数没有局部变量。但是如果有局部变量的话,是一定要加上的。可以自己写一个带有局部变量的函数,自己想一想。下一篇博文会讲述带有局部变量的情况。
- C/C++子函数参数传递,堆栈帧、堆栈参数详解_1
- 参数传递和堆栈平衡
- 参数传递和堆栈平衡
- 参数传递与堆栈调用
- 从堆栈到参数传递
- C 堆栈详解
- C/C++堆栈详解
- Perl子函数参数传递
- [c/c++][转]堆栈详解
- C/C++参数传递详解
- 汇编中的参数传递和堆栈平衡
- 参数传递对堆栈的影响
- 汇编中的参数传递和堆栈平衡
- 可变参数, 它依赖于堆栈----小话c语言(23)
- 【C++】堆栈
- C 堆栈
- 堆栈详解(c语言)
- C语言之堆栈详解
- Linux系统安装时分区的方案
- PAT (Advanced Level) Practise 1011 World Cup Betting
- [LeetCode]Kth Largest Element in an Array
- 数组
- Boost库的简介与安装
- C/C++子函数参数传递,堆栈帧、堆栈参数详解_1
- 《UNIX网络编程卷1》第一例及问题
- 洛谷 P1339 [USACO09OCT]热浪Heat Wave(用Dij)
- 1005. 继续(3n+1)猜想 (25)
- CF
- 记录当前项目lua内存泄露问题
- Socket网络编程(多客户端、信息共享、文件传输)
- 读取文件内容到list (commons-io)
- okhttp+“图灵机器人API” 实现智能聊天