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; .netreturn (tmp0+tmp1+tmp2);
} debugint public_interface_leaf( int x, int y )
{
int tmp0 = 0x12;
int tmp1 = 0x34;
int tmp2 = 0x56; ormtmp0 = x;
tmp1 = y; htmreturn (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的狀況以下:
你們能夠對照上面的彙編和圖片,我這裏只是描述基本知識點:
一、stack的確是full-descending的,SP指向了start函數的頂部,下一個函數必須先減SP,才能保存其棧上的數據。
二、內核到用戶空間固然是public interface,所以在進入start函數的時候SP當前是8字節對齊。而start函數的棧有3個變量共計12個字節,在調用__libc_start_main函數這個public interface的時候固然也要8字節對齊,按理說這裏start函數有一個小小的4字節的空洞,但實際上,代碼是抹去了用戶棧的argc這個參數,所以start的棧的細節以下:
雖然抹去了用戶棧的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以下:
對照代碼和圖片,咱們有下面的解釋:
(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}
棧幀狀況以下:
這裏比較簡單,你們自行分析就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