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, 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("func@m.c+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(func@m.c+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!
.png)
 
The only bad thing about this is that is not in English :(
回复删除