探究Linux下参数传递及查看和修改方法

   X86-64下有16个64位寄存器,其中%rdi、%rsi、%rdx,%rcx、%r8、%r9用作传递函数参数,分别对应第1个参数、第2个参数直到第6个参数,如下图所示(图片来自网络):
探究Linux下参数传递及查看和修改方法_第1张图片
  如果函数的参数个数超过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, line 26.
(gdb) b func
Breakpoint 2 at 0x4004ac : file m.c, line 17.
(gdb) b func2
Breakpoint 3 at 0x40047e : file m.c, line 8.
(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!

你可能感兴趣的:(探究Linux下参数传递及查看和修改方法)