計算機科學基礎知識(六)理解棧幀


一、前言

本文以一個簡單的例子來描述ARM linux下的stack frame。

本文也是對tigger網友問題的回復。

 

二、源代碼

#include <stdio.h>

static int static_interface_leaf( int x, int y )
{
    int tmp0 = 0x12;
    int tmp1 = 0x34;
    int tmp2 = 0x56;

    tmp0 = x;
    tmp1 = y;

    return (tmp0+tmp1+tmp2);
}

int public_interface_leaf( int x, int y )
{
    int tmp0 = 0x12;
    int tmp1 = 0x34;
    int tmp2 = 0x56;

    tmp0 = x;
    tmp1 = y;

    return (tmp0+tmp1+tmp2);
}

void public_interface( int x )
{
    int tmp0 = 0x12;
    int tmp1 = 0x34;

    tmp0 = x;
    public_interface_leaf( tmp0, tmp1 );
    static_interface_leaf( tmp0, tmp1 );
}

int main(int argc, char **argv)
{
    int tmp0 = 0x12;

    public_interface( tmp0 );

    return 0;
}

 

三、逐級stack frame分析

1、准備知識

根據AAPCS的描述,stack是full-descending並且需要滿足兩種約束:一種是通用約束,適用所有的場景,另外一種是針對public interface的約束。通用約束有3條:

(1)SP只能訪問stack base和stack limit之間的memory,即Stack-limit < SP <= stack-base

(2)SP必須對齊在4個字節上,即SP mod 4 = 0

(3)函數只能訪問自己能回溯的那些棧幀。例如f1調用f2,而f2函數又調用了f3,那么f3是可以訪問自己的stack以及f2和f1的stack,也就是說,函數可以訪問[SP, stack-base – 1]之間的內容

對public interface的約束多了一條,就是SP必須對齊在8個字節上,即SP mod 8 = 0

關於ARM的ABI,還有一份文檔,IHI0046B_ABI_Advisory_1,這份文件中講到,在調用所有的AAPCS兼容的函數的時候都要求SP是對齊在8個字節上。

2、起始點的用戶棧的情況

靜態鏈接文檔中,我們說過,函數的入口函數不是main函數而是_start函數,調用序列是_start()->__libc_start_main()->main()。main函數之前對於所有的程序都是一樣的,因此不需要每一個程序員都重復進行那些動作,因此留給程序員一個main函數的入口,開始自己相關邏輯的處理。內核在start函數(我在這里以及后面的文檔中省略了下划線)之前的stack frame並不是空的,內核會創建一些資料在stack上,具體如下:

具體怎么在用戶棧上建立上面的數據結構,有興趣的同學可以參考內核的create_elf_tables函數。此外,需要提醒的是這些數據內容雖然在棧上,但是不是stack frame的一部分,有點類似內核空間到用戶空間參數傳遞的味道。為何這么說呢?因為在start函數中有一條匯編指令:mov    fp, #0,該指令清除frame pointer,在debugger做棧的回溯的時候,當fp等於0的時候也就意味着到了最外層函數。

3、start函數的start frame

0000829c <_start>:
    829c:    e59fc024     ldr    ip, [pc, #36]    ; 82c8 <.text+0x2c>
    82a0:    e3a0b000     mov    fp, #0    ; 0x0--------最外層函數,清除frame pointer
    82a4:    e49d1004     ldr    r1, [sp], #4----------r1 = argc, sp=sp+4,sp指向了argv[]
    82a8:    e1a0200d     mov    r2, sp----------r2保存了stack end,也就是argv[]那個位置
    82ac:    e52d2004     str    r2, [sp, #-4]!--------將stack end壓入棧
    82b0:    e52d0004     str    r0, [sp, #-4]!--------將rtld_fini壓入棧
    82b4:    e59f0010     ldr    r0, [pc, #16]    ; 82cc <.text+0x30>
    82b8:    e59f3010     ldr    r3, [pc, #16]    ; 82d0 <.text+0x34>
    82bc:    e52dc004     str    ip, [sp, #-4]!--------將fini壓入棧
    82c0:    ebffffef     bl    8284 <.text-0x18>-------call __libc_start_main
    82c4:    ebffffeb     bl    8278 <.text-0x24>
    82c8:    0000848c     .word    0x0000848c
    82cc:    00008454     .word    0x00008454
    82d0:    00008490     .word    0x00008490

在調用__libc_start_main函數之前,stack frame的情況如下:

start_sf

大家可以對照上面的匯編和圖片,我這里只是描述基本知識點:

1、stack的確是full-descending的,SP指向了start函數的頂部,下一個函數必須先減SP,才能保存其棧上的數據。

2、內核到用戶空間當然是public interface,因此在進入start函數的時候SP當前是8字節對齊。而start函數的棧有3個變量共計12個字節,在調用__libc_start_main函數這個public interface的時候當然也要8字節對齊,按理說這里start函數有一個小小的4字節的空洞,但實際上,代碼是抹去了用戶棧的argc這個參數,因此start的棧的細節如下:

ks

雖然抹去了用戶棧的argc這個參數,不過沒有關系,反正它已經保存在了r1寄存器中了。

4、__libc_start_main函數的stack frame

__libc_start_main是libc定義的符號,我們動態鏈接的時候,這些代碼沒有進入我們測試的ELF文件。這里略過吧,畢竟查閱c庫代碼也是非常煩人的事情。

5、main函數的stack frame

00008454

:
    8454:    e92d4800     stmdb    sp!, {fp, lr}---將上一個函數的 fp和lr寄存器壓入stack, sp=sp-8
    8458:    e28db004     add    fp, sp, #4    ; ---上一個函數的sp+4就是本函數stack frame的開始
    845c:    e24dd010     sub    sp, sp, #16    ; 0x10
    8460:    e1a03000     mov    r3, r0
    8464:    e50b1014     str    r1, [fp, #-20]------保存argv
    8468:    e54b300d     str    r3, [fp, #-16]------保存argc
    846c:    e3a03012     mov    r3, #18    ; 0x12---tmp0 = 0x12,[fp, #-8]就是源代碼的tmp0
    8470:    e50b3008     str    r3, [fp, #-8]
    8474:    e51b0008     ldr    r0, [fp, #-8]-----傳遞tmp0參數
    8478:    ebffffe3     bl    840c
    847c:    e3a03000     mov    r3, #0    ; 0x0
    8480:    e1a00003     mov    r0, r3
    8484:    e24bd004     sub    sp, fp, #4    ; 0x4
    8488:    e8bd8800     ldmia    sp!, {fp, pc}


在調用public_interface之前,main函數的stack frame如下:

main_sf

對照代碼和圖片,我們有下面的解釋:

(1)第一條指令就是stmdb,這里db就是decrease before的意思,再次確認stack的確是full-descending的

(2)雖然只有一個臨時變量tmp0,但是編譯器還是傳遞了argc和argv這兩個參數,具體為何我也沒有考慮清楚,因此在分配main的stack frame的時候使用了sub    sp, sp, #16,分配4個int型數據,當然是為了對齊8字節。

(3)在一個函數的執行過程中,sp和fp之間就是該函數的stack frame。sp執行stack frame的頂部(低地址),fp執行頂部。

(4)由於main函數的fp加4就是__libc_start_main的sp,因此在main函數的stack上不需要保存其sp,只要保存fp就OK了。

6、public_interface的stack frame

0000840c :
    840c:    e92d4800     stmdb    sp!, {fp, lr}
    8410:    e28db004     add    fp, sp, #4    ; 0x4
    8414:    e24dd010     sub    sp, sp, #16    ; 0x10
    8418:    e50b0010     str    r0, [fp, #-16]---------中間變量,保存傳入的x參數
    841c:    e3a03012     mov    r3, #18    ; 0x12
    8420:    e50b300c     str    r3, [fp, #-12]---------tmp0 = 0x12
    8424:    e3a03034     mov    r3, #52    ; 0x34
    8428:    e50b3008     str    r3, [fp, #-8]----------tmp1 = 0x34
    842c:    e51b3010     ldr    r3, [fp, #-16]
    8430:    e50b300c     str    r3, [fp, #-12]---------tmp0 = x
    8434:    e51b000c     ldr    r0, [fp, #-12]
    8438:    e51b1008     ldr    r1, [fp, #-8]
    843c:    ebffffda     bl    83ac
    8440:    e51b000c     ldr    r0, [fp, #-12]
    8444:    e51b1008     ldr    r1, [fp, #-8]
    8448:    ebffffbf     bl    834c
    844c:    e24bd004     sub    sp, fp, #4    ; 0x4
    8450:    e8bd8800     ldmia    sp!, {fp, pc}

棧幀情況如下:

pli_sf

這里比較簡單,大家自行分析就OK了。

 

7、調用static函數

根據AAPCS的描述,只有public接口才需要SP 8字節對齊。不過測試程序表明所有的都是8字節對齊的,我的編譯器關於ABI的缺省設定是-mabi=aapcs-linux,猜想可能是所有的函數都被編譯成AAPCS-comforming fuction。具體大家可以自己寫代碼練習一下。

 

參考文獻

1、AAPCS。Procedure Call Standard for the ARM Architecture

2、IHI0046B_ABI_Advisory_1。ABI for the ARM Architecture Advisory Note – SP must be 8-byte aligned on entry to AAPCS-conforming functions


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM