[GDB] 觀察 x64 環境下函式參數的傳遞方式
最近在用 gdb 看一些 crash dump,蠻多時候 gdb 都沒能列出函式參數的內容,
在 x86 環境下,大部分的函式參數是用 stack 的方式在傳遞,
但在 x64 環境下就不一樣了,有好幾個參數都會使用暫存器 (register) 來傳遞,
這也導致了不小的麻煩….
決定還是從一個基本的 C 程式反組譯,來看看它究竟是如何運作的~
下面是一個簡單的小程式,首先在 main() 裡面呼叫 foo() 並給了 8 個參數,
在 foo() 第一次被呼叫時又會再呼叫 foo() 一次,但參數值都加 1,
之後就故意去位址 0 的地方寫一個字元 ‘A’ 來造成 segmentation fault,
產生 crash dump:
#include <stdio.h> #include <inttypes.h> void foo(char a, short b, int c, long d, long long e, char* f, int64_t g, uint64_t h) { if (a == 0x11) { foo(a+1, b+1, c+1, d+1, e+1, f, g+1, h+1); } f = NULL; *f = 'A'; } int main() { foo(0x11, 0x2222, 0x33333333, 0x44444444, 0x55555555, (char*)"66666666", 0x7777777777777777, 0x8888888888888888); return 0; }
程式執行後,如預期的產生了 crash dump,
用 gdb 來觀察一下 main 函式:
(gdb) disas main Dump of assembler code for function main: 0x000000000040063c <+0>: push %rbp 0x000000000040063d <+1>: mov %rsp,%rbp 0x0000000000400640 <+4>: sub $0x10,%rsp 0x0000000000400644 <+8>: movabs $0x8888888888888888,%rax 0x000000000040064e <+18>: mov %rax,0x8(%rsp) 0x0000000000400653 <+23>: movabs $0x7777777777777777,%rax 0x000000000040065d <+33>: mov %rax,(%rsp) 0x0000000000400661 <+37>: mov $0x400720,%r9d 0x0000000000400667 <+43>: mov $0x55555555,%r8d 0x000000000040066d <+49>: mov $0x44444444,%ecx 0x0000000000400672 <+54>: mov $0x33333333,%edx 0x0000000000400677 <+59>: mov $0x2222,%esi 0x000000000040067c <+64>: mov $0x11,%edi 0x0000000000400681 <+69>: callq 0x4005b0 <_Z3foocsilxPclm> 0x0000000000400686 <+74>: mov $0x0,%eax 0x000000000040068b <+79>: leaveq 0x000000000040068c <+80>: retq End of assembler dump. (gdb) x/s 0x400720 0x400720: "66666666"
根據 System V AMD64 ABI calling convention 這邊的敘述,
在 x64 環境下呼叫函式時,概念上,整數或指標參數是由左至右,
會依序放至暫存器 RDI, RSI, RDX, RCX, R8, and R9,剩下的參數才會放到 stack 裡面去~
(但在實作上,是先將最右邊的多餘參數放到 stack,以由右至左的方式一個個處理參數的傳遞)
而由 disas main 反組譯的結果來看,也的確符合這樣的規則:
– 第 1 個參數 (char) 0x11 被放在 edi 暫存器
– 第 2 個參數 (short) 0x2222 被放在 esi 暫存器
– 第 3 個參數 (int) 0x33333333 被放在 edx 暫存器
– 第 4 個參數 (long) 0x44444444 被放在 ecx 暫存器
– 第 5 個參數 (long long) 0x55555555 被放在 r8d 暫存器
– 第 6 個參數 (char*) “66666666” 這個字串是在 0x400720,被放在 r9d 暫存器
– 第 7 個參數 (int64_t) 0x7777777777777777 先被放在 rax 暫存器,接著被轉移到 stack 的 rsp 的位置
– 第 8 個參數 (uint64_t) 0x8888888888888888 先被放在 rax 暫存器,接著被轉移到 stack 的 rsp+8 的位置
這上面的例子只用到了整數和指標型態的參數,如果是 float/double 型態的話,
參數值會由如 xmm0~7 等暫存器傳遞,就更加複雜了~
現在假設我們拿到了這個當掉後的 crash dump,先用 bt 看看狀況:
(gdb) bt #0 0x0000000000400637 in foo(char, short, int, long, long long, char*, long, unsigned long) () #1 0x000000000040062b in foo(char, short, int, long, long long, char*, long, unsigned long) () #2 0x0000000000400686 in main ()
這邊的 back trace 是符合預期的,main 先呼叫 foo(),foo() 會再呼叫 foo() 一次,
接著第二次的 foo() 就會去存取位址 0 的地方導致 segmentation fault~
但我們想要知道的是參數的內容…
先用 info frame 看一下 frame 0 的資訊,可以看到 Arglist 那邊沒有東西
(但事實上有兩個參數是用 stack 傳的,所以應該要有?)
(gdb) info frame Stack level 0, frame at 0x7fff5809a830: rip = 0x400637 in foo(char, short, int, long, long long, char*, long, unsigned long); saved rip 0x40062b called by frame at 0x7fff5809a880 Arglist at 0x7fff5809a820, args: Locals at 0x7fff5809a820, Previous frame's sp is 0x7fff5809a830 Saved registers: rbp at 0x7fff5809a820, rip at 0x7fff5809a828
用 info reg 看目前暫存器的值的話,
可以看到 rdi, rsi, rdx, rcx, r8, r9 有儲存第二次 foo() 呼叫的參數值,
所以如果是 back trace 的最後一個函式 (frame 0) 的話,其參數可以從暫存器的內容得到:
(gdb) info reg rcx 0x44444445 1145324613 rdx 0x33333334 858993460 rsi 0x2223 8739 rdi 0x12 18 rbp 0x7fff5809a820 0x7fff5809a820 rsp 0x7fff5809a7e0 0x7fff5809a7e0 r8 0x55555556 1431655766 r9 0x400720 4196128 rip 0x400637 0x400637 <foo(char, short, int, long, long long, char*, long, unsigned long)+135>
但如果暫存器的值在 crash 之前有被改變的話,這一招應該就無效了…
另一個可以看到第 7 個參數和之後參數的方法是查 stack,
我們剛剛從 info frame 那邊得知 saved register 裡的 rbp 是 0x7fff5809a820,
因此可以由此來推算:
– rbp:上一個 frame 的 rbp 位址
– rbp+0x8: return address
– rbp+0x10: 第 7 個參數的值
– rbp+0x18: 第 8 個參數的值
用 x/4a 來看一下 rbp 指到的記憶體的 4 個 8-byte 的內容,
的確依次是上次的 rbp, return address (就是 foo),
0x7777777777777777+1, 0x8888888888888888+1:
(gdb) x/4a 0x7fff5809a820 0x7fff5809a820: 0x7fff5809a870 0x40062b <_Z3foocsilxPclm+123> 0x7fff5809a830: 0x7777777777777778 0x8888888888888889
可以用同樣的方法,來查第一次 foo() 呼叫時,第 7 個和之後的參數的內容~
先用 frame 1 切到第一次 foo() 呼叫的 frame,
用 info frame 查到當時的 rbp 是 0x7fff5809a870,
再用 x/4a 列出 previous rbp, return address, 第 7 參數和第 8 參數,
的確就是 0x7777777777777777 和 0x8888888888888888:
(gdb) frame 1 #1 0x000000000040062b in foo(char, short, int, long, long long, char*, long, unsigned long) () (gdb) info frame Stack level 1, frame at 0x7fff5809a880: rip = 0x40062b in foo(char, short, int, long, long long, char*, long, unsigned long); saved rip 0x400686 called by frame at 0x7fff5809a8a0, caller of frame at 0x7fff5809a830 Arglist at 0x7fff5809a870, args: Locals at 0x7fff5809a870, Previous frame's sp is 0x7fff5809a880 Saved registers: rbp at 0x7fff5809a870, rip at 0x7fff5809a878 (gdb) x/4a 0x7fff5809a870 0x7fff5809a870: 0x7fff5809a890 0x400686 <main+74> 0x7fff5809a880: 0x7777777777777777 0x8888888888888888
不過要怎麼取得第 1 個到第 6 個參數呢?
這我目前還不知道…. (其實這才是最重點的部分,平常函式參數也沒那麼多個啊~~ =_=)
參考資料:
Oracle x86 Assembly Language Reference Manual