i386和arm32架構下函數棧幀分析

棧幀的實現實際跟系統本身的架構密切相關,這裏主要對i386和arm32兩種架構下的棧幀展開分析。

下面關於棧幀的分析都將基於這個例子進行:

int func(int a,int b)
{
    int sum;
    sum = a + b;
    return sum;
}
int main(void)
{
    int sum;
    sum = func(1,2);
    return 0;
}

1. i386中的棧幀
下面是i386架構系統上跟棧幀相關的一組寄存器:
        4個數據寄存器(eax、ebx、ecx、edx)       這4個是通用數據寄存器,主要用於存放變量值,其中eax還用於傳遞函數返回值
        ebp                                     基址指針寄存器,用於存放一個指針,指向當前棧幀的棧底
        esp                                     棧指針寄存器,用於存放一個指針,指向當前棧幀的棧頂
        eip                                     指令指針寄存器,用於存放一個指針,指向下一條將要執行的指令,該寄存器不能直接操作,只能通過call、ret指令間接操作

        
i386架構系統上當前函數對應的典型棧幀佈局:
        ^           ^               |                                                                                                                  |
        |            |                | 函數實參n                                                                                               |
        |        偏移爲正     | ......                                                                                                           |
        |            |           +8 | 函數實參1                                                                                               |
                                 +4 | 返回地址,實際就是指向當前函數返回後下一條將要執行的指令 |
     地址增大    ebp --> | 保存的前一個棧幀的ebp                                                                      |
                                  -4 | 函數局部變量1                                                                                       |
        |           |                 | ......                                                                                                            |
        |        偏移爲負     | ......                                                                                                            |
        |           |    esp --> | 函數局部變量n                                                                                        |
        |           v                |                                                                                                                   |

當函數被調用時,新的棧幀被壓入棧中,當函數返回時,相應的棧幀從棧中彈出。
ebp中存放的就是棧基址,指向當前函數的棧底,顯然在當前函數生命週期中該位置是固定的。
訪問當前函數的實參和局部變量的方法就是以ebp爲基址,再加上一個偏移,由上圖可知,實參的偏移爲正,局部變量的偏移爲負。
esp中存放的就是棧指針,指向當前函數的棧頂,顯然該位置在當前函數生命週期中會發生變化。       
上面範例在i386中的彙編實現如下:

        func:
            pushl    %ebp
            movl     %esp, %ebp
            subl     $16, %esp
            movl     8(%ebp), %edx
            movl     12(%ebp), %eax
            addl     %edx, %eax
            movl     %eax, -4(%ebp)
            movl     -4(%ebp), %eax
            leave
            ret
        main:
            pushl    %ebp
            movl     %esp, %ebp
            subl     $16, %esp
            pushl    $2
            pushl    $1
            call     func
            addl     $8, %esp
            movl     %eax, -4(%ebp)
            movl     $0, %eax
            leave
            ret

基於上面的彙編實現可知i386中一個函數棧幀的建立和銷燬過程:
    建立過程:
        [1]. 首先將新函數的實參從右向左依次壓棧
        [2]. 接着調用call指令將eip中的地址壓棧作爲新函數棧幀銷燬後的返回地址,並將新函數的入口地址賦給eip,從而跳轉到新函數入口
        [3]. 最後將ebp壓棧,並將esp賦給ebp,這一步確立了新函數的棧底和棧頂位置,也意味着新函數的棧幀框架正式建立
    銷燬過程:
        [1]. 如果函數有返回值,首先將返回值賦給eax
        [2]. 接着調用leave指令將ebp賦給esp,並從新的棧頂彈出一個值賦給ebp,顯然這就是棧幀建立過程中步驟[3]的反過程
        [3]. 最後調用ret指令從棧頂彈出一個值賦給eip,顯然這就是棧幀建立過程中步驟[2]的反過程。
             至此,一個函數棧幀的銷燬過程實際上已經完成


2. arm32中的棧幀
相對於i386架構系統上相對統一的棧幀結構,arm32架構cpu上的棧幀實現就顯得非常多變,這裏將分析的是較典型的一種。
下面是arm32架構系統上跟棧幀相關的一組寄存器:
            r0~r3      這些寄存器既用於傳遞函數的入參,又用於傳遞函數的返回值(通常只是r0),在當前函數運行期間還可用於存放普通變量
            fp          棧幀指針寄存器,相當於i386中的ebp
            sp          棧指針寄存器,相當於i386中的esp
            pc          程序計數器,存放了下一條將要執行指令的位置,相當於i386中的eip
            lr          連接寄存器,用於存放當前函數的返回地址,i386中返回地址被存放在棧中

上面範例在arm32中的彙編實現如下:
 

        func:
            str fp, [sp, #-4]!
            add fp, sp, #0
            sub    sp, sp, #20
            str    r0, [fp, #-16]
            str    r1, [fp, #-20]
            ldr    r2, [fp, #-16]
            ldr    r3, [fp, #-20]
            add    r3, r2, r3
            str    r3, [fp, #-8]
            ldr    r3, [fp, #-8]
            mov    r0, r3
            sub    sp, fp, #0
            ldr    fp, [sp], #4
            bx    lr
        main:
            stmfd    sp!, {fp, lr}
            add    fp, sp, #4
            sub    sp, sp, #8
            mov    r1, #2
            mov    r0, #1
            bl    func
            str    r0, [fp, #-8]
            mov    r3, #0
            mov    r0, r3
            sub    sp, fp, #4
            ldmfd    sp!, {fp, lr}
            bx    lr

基於上面的彙編實現可知arm32中一個函數棧幀的建立和銷燬過程:
    建立過程:
        [1]. 首先將新函數的實參從左向右依次賦給r0~r3,如果函數存在超過4個的情況,則多餘的參入從右向左依次壓棧
        [2]. 接着調用bl指令將pc中的地址賦給lr作爲新函數棧幀銷燬後的返回地址,並將新函數的入口地址賦給pc,從而跳轉到新函數入口
        [3]. 最後將fp壓棧,並將sp賦給fp,這一步確立了新函數的棧底和棧頂位置,也意味着新函數的棧幀框架正式建立
    銷燬過程:
        [1]. 如果函數有返回值,首先將返回值賦給r0
        [2]. 接着將fp賦給sp,並從新的棧頂彈出一個值賦給fp,顯然這就是棧幀建立過程中步驟[3]的反過程
        [3]. 最後調用bx指令,跳轉到lr指向的返回地址,顯然這就是棧幀建立過程中步驟[2]的反過程
             至此,一個函數棧幀的銷燬過程實際上已經完成

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章