計算機科學基礎知識(六)理解棧幀
一、前言
本文以一個簡單的例子來描述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的情況如下:
大家可以對照上面的匯編和圖片,我這裏只是描述基本知識點:
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的棧的細節如下:
雖然抹去了用戶棧的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如下:
對照代碼和圖片,我們有下面的解釋:
(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}
棧幀情況如下:
這裏比較簡單,大家自行分析就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
計算機科學基礎知識(六)理解棧幀