摘要:linux程序運行的狀態以及如何推導調用棧。
咱們先看一下指令格式(64bit),以及指令對於寄存機執行結果的影響node
類型一、STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>linux
將Xt1和Xt2存入Xn|SP對應的地址內存中,而後,將Xn|SP的地址變動爲Xn|SP + imm偏移量的新地址架構
類型二、STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!函數
將Xt1和Xt2存入Xn|SP的地址自加imm對應的地址內存中,而後,將Xn|SP的地址變動爲Xn|SP + imm的offset偏移量後的新地址post
類型三、STP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]ui
將Xt1和Xt2存入Xn|SP的地址自加imm對應的地址內存中this
手冊中有三種操做碼,咱們只討論程序中涉及的後兩種spa
Pseudocode以下:線程
Shared decode for all encodings integer n = UInt(Rn); integer t = UInt(Rt); integer t2 = UInt(Rt2); if L:opc<0> == '01' || opc == '11' then UNDEFINED; integer scale = 2 + UInt(opc<1>); integer datasize = 8 << scale; bits(64) offset = LSL(SignExtend(imm7, 64), scale); boolean tag_checked = wback || n != 31; Operation for all encodings bits(64) address; bits(datasize) data1; bits(datasize) data2; constant integer dbytes = datasize DIV 8; boolean rt_unknown = FALSE; if HaveMTEExt() then SetNotTagCheckedInstruction(!tag_checked); if wback && (t == n || t2 == n) && n != 31 then Constraint c = ConstrainUnpredictable(); assert c IN {Constraint_NONE, Constraint_UNKNOWN, Constraint_UNDEF, Constraint_NOP}; case c of when Constraint_NONE rt_unknown = FALSE; // value stored is pre-writeback when Constraint_UNKNOWN rt_unknown = TRUE; // value stored is UNKNOWN when Constraint_UNDEF UNDEFINED; when Constraint_NOP EndOfInstruction(); if n == 31 then CheckSPAlignment(); address = SP[]; else address = X[n]; if !postindex then address = address + offset; if rt_unknown && t == n then data1 = bits(datasize) UNKNOWN; else data1 = X[t]; if rt_unknown && t2 == n then data2 = bits(datasize) UNKNOWN; else data2 = X[t2]; Mem[address, dbytes, AccType_NORMAL] = data1; Mem[address+dbytes, dbytes, AccType_NORMAL] = data2; if wback then if postindex then address = address + offset; if n == 31 then SP[] = address; else X[n] = address;
紅色部分對應推棧的關鍵邏輯,其餘彙編指令含義可自行參考armv8手冊或者度娘。3d
熟悉了上面的部分,接下來咱們看一個實例:
C代碼以下:
相關的幾個函數反彙編以下(和推棧相關的通常只有入口兩條指令):
main\f3\f4\strlen
咱們經過gdb運行後,能夠看到strlen地方會觸發SEGFAULT,引起進程掛掉
上述經過代碼編譯後,沒有strip,所以elf文件是帶着符號的
查看運行狀態(info register):關注$2九、$30、SP、PC四個寄存器
一個核心的思想:CPU執行的是指令而不是C代碼,函數調用和返回實際是在線程棧上面的壓棧和彈棧的過程
接下來咱們來看上面的調用關係在當前這個任務棧是如何玩的:
函數調用在棧中的關係(call function壓棧,地址遞減;return彈棧,地址遞增):
如下是推棧的過程(劃重點)
再回頭來看以前的彙編:
main\f3\f4\strlen
從當前的sp開始,frame 0是strlen,這塊沒有開棧,所以上一級的調用函數仍然是x30,所以推導:frame1調用爲f3
函數f3的起始入口彙編:
(gdb) x/2i f3 0x400600 <f3>: stp x29, x30, [sp,#-48]! 0x400604 <f3+4>: mov x29, sp
能夠看到,f3函數開闢的棧空間爲48字節,所以,倒推frame2的棧頂爲當前的sp + 48字節:0xfffffffff2c0
(gdb) x/gx 0xfffffffff2c0+8 0xfffffffff2c8: 0x000000000040065c (gdb) x/i 0x000000000040065c 0x40065c <f4+36>: mov w0, #0x0 // #0 frame2的函數爲sp+8:0x000000000040065c -> <f4+36>
繼續從sp = 0xfffffffff2c0倒推frame1的函數
函數f4的起始入口彙編爲:
(gdb) x/2i f4 0x400638 <f4>: stp x29, x30, [sp,#-48]! 0x40063c <f4+4>: mov x29, sp
能夠看到,f4函數開闢的棧空間也是爲48字節,所以,倒推frame3的棧頂爲當前的0xfffffffff2c0 + 48字節:0xfffffffff2f0
frame2的函數爲0xfffffffff2c0 + 8:0x000000000040065c -> <f4+36> (gdb) x/gx 0xfffffffff2f0+8 0xfffffffff2f8: 0x0000000000400684 (gdb) x/i 0x0000000000400684 0x400684 <main+28>: mov w0, #0x0 // #0
所以frame3的函數爲main函數,main函數對應的棧頂爲0xfffffffff320
至此推導結束(有興趣的同窗能夠繼續推導,能夠看到libc如何拉起main的過程)
總結:
推棧的關鍵:
現場有以下的core:能夠看到,全部的符號找不到,加載了符號表依然很差使,解析不出來實際的調用棧
(gdb) bt #0 0x0000ffffaeb067bc in ?? () from /lib64/libc.so.6 #1 0x0000aaaad15cf000 in ?? () Backtrace stopped: previous frame inner to this frame (corrupt stack?)
先看info register,關注x2九、x30、sp、pc四個寄存器的值
推導任務棧:
先將sp內容導出:
下圖實際已先將結果標出,咱們下面來詳細描述如何推導
pc表明當前執行的函數指令,若是當前指令未開棧,通常狀況x30表明上一級的frame調用當前函數的下一條指令,查看彙編,能夠反解爲以下函數
(gdb) x/i 0xaaaacd3de4fc 0xaaaacd3de4fc <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+108>: mov x27, x0
找到棧頂函數後,查看該函數的棧操做:
(gdb) x/6i PGXCNodeConnStr 0xaaaacd3de490 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)>: sub sp, sp, #0xd0 0xaaaacd3de494 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+4>: stp x29, x30, [sp,#80] 0xaaaacd3de498 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+8>: add x29, sp, #0x50
能夠看到,上一級的frame存在了當前的sp + 0xd0 - 0x80也就是0xfffec4cebd40 + 0xd0 - 0x80 = 0xfffec4cebd90的地方,而棧底在0xfffec4cebd40+ 0xd0 = 0xfffec4cebe10的地方
所以就找到了下一級的frame對應的棧頂和上一級的LR返回指令,反解,能夠獲得函數build_node_conn_str
(gdb) x/i 0x0000aaaacd414e08 0xaaaacd414e08 <build_node_conn_str(Oid, DatabasePool*)+224>: mov x21, x0
繼續重複上述推導,能夠看到這個函數build_node_conn_str開了176字節的棧,
(gdb) x/4i build_node_conn_str 0xaaaacd414d28 <build_node_conn_str(Oid, DatabasePool*)>: stp x29, x30, [sp,#-176]! 0xaaaacd414d2c <build_node_conn_str(Oid, DatabasePool*)+4>: mov x29, sp
所以繼續用0xfffec4cebe10 + 176 = 0xfffec4cebec0
查看調用者0xfffec4cebe10+8爲reload_database_pools
繼續看reload_database_pools
(gdb) x/8i reload_database_pools 0xaaaacd4225e8 <reload_database_pools(PoolAgent*)>: sub sp, sp, #0x1c0 0xaaaacd4225ec <reload_database_pools(PoolAgent*)+4>: adrp x5, 0xaaaad15cf000 0xaaaacd4225f0 <reload_database_pools(PoolAgent*)+8>: adrp x3, 0xaaaacf0ed000 0xaaaacd4225f4 <reload_database_pools(PoolAgent*)+12>: adrp x4, 0xaaaaceeed000 <_ZN4llvm18ConvertUTF8toUTF16EPPKhS1_PPtS3_NS_15ConversionFlagsE> 0xaaaacd4225f8 <reload_database_pools(PoolAgent*)+16>: add x3, x3, #0x9e0 0xaaaacd4225fc <reload_database_pools(PoolAgent*)+20>: adrp x1, 0xaaaacf0ee000 <_ZZ25PoolManagerGetConnectionsP4ListS0_E8__func__+24> 0xaaaacd422600 <reload_database_pools(PoolAgent*)+24>: stp x29, x30, [sp,#-96]!
實際開棧0x220字節,所以這一層frame的棧底爲0xfffec4cebec0 + 0x220 = 0xfffec4cec0e0
所以獲得基本的調用關係的結構以下
以上基本能夠夠用來分析問題了,所以不須要再繼續推導
TIPS:arm架構下通常調用都會使用這種指令,
stp x29, x30, [sp,#immediate]! 有歎號或者無歎號
所以在每一層的frame都保存了上一層frame的棧頂地址和LR指令,經過準確找到底層的frame 0棧頂後,就能夠快速推導出全部的調用關係(紅色虛線圈出來的部分),函數的反解依賴符號表,只要原始的elf文件的symbol段沒有strip掉,是均可以找到對應的函數符號(經過readelf -S查看便可)
找到Frame後,每一層frame裏面的內容,結合彙編基本就能夠用來推導過程變量了。
本文分享自華爲雲社區《代碼 or 指令,淺析ARM架構下的函數的調用過程》,原文做者:K______。