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

1、前言html

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

本文也是對tigger網友問題的回覆。程序員

 

2、源代碼數據結構

#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; .net

    return (tmp0+tmp1+tmp2);
} debug

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

    tmp0 = x;
    tmp1 = y; htm

    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;
}

 

3、逐級stack frame分析

一、準備知識

根據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個字節上。

二、起始點的用戶棧的狀況

靜態連接文檔中,咱們說過,函數的入口函數不是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的時候也就意味着到了最外層函數。

三、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

你們能夠對照上面的彙編和圖片,我這裏只是描述基本知識點:

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

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

ks

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

四、__libc_start_main函數的stack frame

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

五、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了。

六、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了。

 

七、調用static函數

根據AAPCS的描述,只有public接口才須要SP 8字節對齊。不過測試程序代表全部的都是8字節對齊的,個人編譯器關於ABI的缺省設定是-mabi=aapcs-linux,猜測多是全部的函數都被編譯成AAPCS-comforming fuction。具體你們能夠本身寫代碼練習一下。

 

參考文獻

一、AAPCS。Procedure Call Standard for the ARM Architecture

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

相關文章
相關標籤/搜索