[GDB] 觀察 x64 環境下函式參數的傳遞方式

[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 個參數呢?

這我目前還不知道…. (其實這才是最重點的部分,平常函式參數也沒那麼多個啊~~ =_=)

 

參考資料:

GDB: x command

Oracle x86 Assembly Language Reference Manual

 

(本頁面已被瀏覽過 708 次)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料