探究Linux下參數傳遞及查看和修改方法

   X86-64下有16個64位寄存器,其中%rdi、%rsi、%rdx,%rcx、%r8、%r9用作傳遞函數參數,分別對應第1個參數、第2個參數直到第6個參數,如下圖所示(圖片來自網絡):

  如果函數的參數個數超過6個,則超過的參數直接使用棧來傳遞。在被調用函數執行前,會先將寄存器中的參數壓入堆棧,之後的訪問會通過棧寄存器加上偏移位置來訪問。下面我們結合程序及其反彙編的結果來看一看。C語言程序如下所示:
#include <stdio.h>
#include <stdlib.h>

static int func2(int i,int j)
{
int k;

k = i + j;
return k;
}

static int func(int fd,const char *ptr, int arg3, int arg4,int arg5,
int arg6, int arg7, int arg8)
{
int ret;

ret = arg7 + arg8;

func2(fd, arg3);

return ret;
}

int main(void)
{
func(12, "Hello,World!",3, 4, 5, 6, 7,8);

return 0;
}
  將上述程序保存爲m.c,使用gcc加上-g選項編譯,然後使用gdb來進行調試,我們在main調用func的位置及func()和func2()函數三處加上斷點,如下所示:
(gdb) b m.c:26
Breakpoint 1 at 0x4004d4: file m.c, line26.
(gdb) b func
Breakpoint 2 at 0x4004ac: file m.c, line17.
(gdb) b func2
Breakpoint 3 at 0x40047e: file m.c, line8.
(gdb)
  然後我們在第一個斷點處停下,反彙編當前的main函數,查看參數傳遞方式,如下所示:
(gdb) disassemble /m main
Dump of assembler code for function main:
25 {
0x00000000004004cc <+0>: push%rbp
0x00000000004004cd <+1>: mov%rsp,%rbp
0x00000000004004d0 <+4>: sub $0x10,%rsp

26 func(12,"Hello,World!", 3,4, 5, 6, 7, 8);
=> 0x00000000004004d4<+8>: movl $0x8,0x8(%rsp)
0x00000000004004dc <+16>: movl $0x7,(%rsp)
0x00000000004004e3 <+23>: mov $0x6,%r9d
0x00000000004004e9 <+29>: mov $0x5,%r8d
0x00000000004004ef <+35>: mov $0x4,%ecx
0x00000000004004f4 <+40>: mov $0x3,%edx
0x00000000004004f9 <+45>: mov $0x400608,%esi
0x00000000004004fe <+50>: mov $0xc,%edi
0x0000000000400503 <+55>: callq 0x40048f<func>

27
28 return 0;
0x0000000000400508 <+60>: mov $0x0,%eax

29 }
0x000000000040050d <+65>: leaveq
0x000000000040050e <+66>: retq

End of assembler dump.
(gdb)
  在func(12, "Hello,World!", 3, 4, 5, 6, 7, 8);這行下面我們可以看到在使用callq指令調用func()函數之前,會使用mov指令將前6個參數的值分別保存在edi、esi、edx、ecx、r8d、r9d這個6個寄存器中,而將第7個和第8個參數存儲在棧上。這個結果和我們前面說的一致。
  在進入第二個斷點之前,我們先來看看當前的寄存器信息,如下所示:
(gdb) i registers
rax 0x351658ff60 228008197984
rbx 0x0 0
rcx 0x0 0
rdx 0x7fffffffe4e8 140737488348392
rsi 0x7fffffffe4d8 140737488348376
rdi 0x1 1
rbp 0x7fffffffe3f0 0x7fffffffe3f0
rsp 0x7fffffffe3e0 0x7fffffffe3e0
r8 0x351658e300 228008190720
r9 0x3515a0e9d0 227996133840
r10 0x7fffffffe240 140737488347712
r11 0x351621ebe0 228004588512
r12 0x400390 4195216
r13 0x7fffffffe4d0 140737488348368
r14 0x0 0
r15 0x0 0
rip 0x4004d4 0x4004d4 <main+8>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
  我們看到,在第一個斷點處,也就是main函數中調用func()的位置,此時調用函數func()所需要的參數還沒有存儲到寄存器中,此時是在sub $0x10,%rsp彙編指令之後的位置。
  接下來我們進入第二個斷點,即設置在func()函數的斷點,查看此時的寄存器信息,如下所示:
(gdb) i registers
rax 0x351658ff60 228008197984
rbx 0x0 0
rcx 0x4 4
rdx 0x3 3
rsi 0x400608 4195848
rdi 0xc 12
rbp 0x7fffffffe3d0 0x7fffffffe3d0
rsp 0x7fffffffe3a0 0x7fffffffe3a0
r8 0x5 5
r9 0x6 6
r10 0x7fffffffe240 140737488347712
r11 0x351621ebe0 228004588512
r12 0x400390 4195216
r13 0x7fffffffe4d0 140737488348368
r14 0x0 0
r15 0x0 0
rip 0x4004ac 0x4004ac <func+29>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
  此時在rsi、rdi等寄存器中已經可以看到我們傳遞的參數值,我們此時的位置是在callq 0x40048f <func>之後的位置。
反彙編func()函數,如下所示:
(gdb) disassemble /m func
Dump of assembler code for function func:
14 {
0x000000000040048f <+0>: push%rbp
0x0000000000400490 <+1>: mov%rsp,%rbp
0x0000000000400493 <+4>: sub $0x30,%rsp
0x0000000000400497 <+8>: mov%edi,-0x14(%rbp)
0x000000000040049a <+11>: mov%rsi,-0x20(%rbp)
0x000000000040049e <+15>: mov%edx,-0x24(%rbp)
0x00000000004004a1 <+18>: mov%ecx,-0x28(%rbp)
0x00000000004004a4 <+21>: mov%r8d,-0x2c(%rbp)
0x00000000004004a8 <+25>: mov%r9d,-0x30(%rbp)

15 int ret;
16
17 ret = arg7+ arg8;
=> 0x00000000004004ac<+29>: mov 0x18(%rbp),%eax
0x00000000004004af <+32>: mov 0x10(%rbp),%edx
0x00000000004004b2 <+35>: lea (%rdx,%rax,1),%eax
0x00000000004004b5 <+38>: mov%eax,-0x4(%rbp)

18
19 func2(fd, arg3);
0x00000000004004b8 <+41>: mov-0x24(%rbp),%edx
0x00000000004004bb <+44>: mov-0x14(%rbp),%eax
0x00000000004004be <+47>: mov%edx,%esi
0x00000000004004c0 <+49>: mov%eax,%edi
0x00000000004004c2 <+51>: callq 0x400474<func2>

20
21 return ret;
0x00000000004004c7 <+56>: mov-0x4(%rbp),%eax

22 }
0x00000000004004ca <+59>: leaveq
0x00000000004004cb <+60>: retq
  從上面的彙編代碼可以看到,在做具體的操作之前,會將寄存器中的參數值壓入到棧上,並且在此後的操作中,都是隻會去操作棧上的值,而不是直接修改寄存器中的值。
現在有一個問題,我們在第二個斷點處,是在將寄存器壓入棧之前還是之後?如果此時打印fd,是從棧上取值還是寄存器中取值?這個問題也很好判斷,直接使用p命令打印fd即可,具體過程如下所示:
(gdb) p fd
$9 = 12
(gdb) p $rdi
$10 = 12
(gdb) set $rdi=15
(gdb) p fd
$11 = 12
(gdb) set *(int *)($rbp-0x14)=15
(gdb) p fd
$12 = 15
(gdb)
  開始的時候修改的是rdi寄存器,但是打印fd時仍然是12,直接修改壓棧的位置爲15,再次打印fd,此時的值爲15.所以在打印fd時,相應的值是從棧上讀取的。我們此時的斷點的位置也是在將參數壓棧之後的位置。fd對應的棧位置可以算出來,不過這裏是根據前面的反彙編結果。
接下來嘗試利用棧上的信息打印出我們的第二個參數,也就是"Hello,World!"字符串。我們知道C語言中字符串其實就是一段以空字符結尾的內存,通常使用其首地址來訪問該字符串。我們這裏的字符串的地址通過esi寄存器保存在棧上,位置就是$rbp-0x20,但是這個位置存儲的是一個指針的地址,如果將其理解爲指針,就是存儲指針的指針,也就是二級指針,所以的打印的時候應該是這樣:
(gdb) p *(char **)($rbp-0x20)
$6 = 0x400608"Hello,World!"
(gdb) p (char *)($rbp-0x20)
$7 = 0x7fffffffe3b0"\b\006@"
(gdb)
  其實前面修改變量的方法顯得很羅嗦也很繁瑣,但是平時如果要對正在運行的程序進行變量的修改,使用gdb則很麻煩,也不夠靈活,即使使用gdb腳本。做這種事情,當然是由強大的SystemTap來做,要方便的多。下面還以修改fd爲例,來說明如何使用SystemTap腳本來修改。
腳本如下:
probe process("a.out").statement("[email protected]+1") {
printf("func1: %s\n", $$vars);
$fd = 15;
}

probe process("a.out").statement("func2") {
printf("edi = %d\n", register("edi"));
}
  上面的a.out就是前面的C程序(保存到m.c文件)編譯後生成的。
  這個腳本的執行和輸出如下:
[root@CentOS_190 ~]# stap -gu -c ./a.out mod_reg.stp
func1: fd=0xc ptr=0x400608 arg3=0x3 arg4=0x4 arg5=0x5 arg6=0x6 arg7=0x7 arg8=0x8 ret=0x0
edi = 15
[root@CentOS_190 ~]#
  我們可以看到在第一個probe點process("a.out").statement([email protected]+1)將fd設置爲15,在第二個probe點通過edi寄存器看到函數func2()的第一個參數i的值爲15,而不是12.
細心的同學可能發現在第一個probe點加上了相對函數的偏移,在第二個probe點中使用寄存器來查看參數的信息,而不是使用$i變量。我們通過下面的腳本來說明這個問題,   腳本如下:
probe process("a.out").statement("func") {
printf("func1: %s\n", $$vars);
printf("func1: edi = %d\n", register("edi"));
}

probe process("a.out").statement("func2") {
printf("func2: %s\n", $$vars);
printf("func2: edi = %d\n", register("edi"));
}
  其輸出結果如下:
[root@CentOS_190 ~]# stap -gu -c ./a.out test.stp
func1: fd=0x0 ptr=0x7fffa0a4bff8 arg3=0x0 arg4=0x40036b arg5=0x0 arg6=0x0 arg7=0x7 arg8=0x8 ret=0x0
func1: edi =12
func2: i=0x0 j=0x1 k=0x35
func2: edi =12
[root@CentOS_190 ~]#
  如果沒有加上函數的偏移,在probe點觸發時,fd、ptr等參數的值還沒有初始化,也就是寄存器中的值還沒有壓入棧中,所以此時直接使用fd等變量獲取的值是未定義的,此時即使修改了fd等變量,在將寄存器壓棧的時候也會被覆蓋爲原來的值。結合我們前面用gdb看到的彙編代碼,這個地方理解起來就容易多了。
  我們知道在SystemTap腳本中可以嵌入C代碼,在嵌入的C代碼中也可以使用內聯彙編,但是在操作寄存器的時候要注意,在probe點觸發時,程序運行時的寄存器會被保存到棧上,所以在probe的處理中修改寄存器時,修改的只是當前的寄存器,在probe點的處理完成後會恢復程序運行時的寄存器,具體細節參見內核文檔kprobes.txt。
前面的腳本中不僅實現了修改變量的功能,在程序debug信息少的情況下,也可以選擇一些信息動態輸出,避免了修改程序及重新編譯的重複操作。
  再次向大家強烈推薦SystemTap!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章