zoukankan      html  css  js  c++  java
  • C/C++子函数参数传递,堆栈帧、堆栈参数详解

    本文转载自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调用规范是在过程内部修改espret 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_testCompile_testCompile_testmain.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_testcompile_testcompile_testmain.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_testcompile_testcompile_testmain.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_numx86汇编子过程作比较:

    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。这句命令在这里有没有都没有关系,因为这个函数没有局部变量。但是如果有局部变量的话,是一定要加上的。可以自己写一个带有局部变量的函数,自己想一想。下一篇博文会讲述带有局部变量的情况。

  • 相关阅读:
    java怎样将一组对象传入Oracle存储过程
    android webview内容压线问题解决方法
    BS和CS对比
    【OpenCV-Python】Python Extension Packages for Windows
    hdu4462 Scaring the Birds
    tomcat安全配置之证书密码加密存储
    UVA 10714 Ants 蚂蚁 贪心+模拟 水题
    一个不喜欢读书的Javaer的读书单
    二叉树可视化--Graphviz
    [置顶] mmog游戏开发之业务篇
  • 原文地址:https://www.cnblogs.com/yungyu16/p/13142064.html
Copyright © 2011-2022 走看看