在前面文章的例子中,函数参数都是整数类型,对于string数据类型,bpftrace的追踪方式也是一样的吗?
例子
golang程序中定义了一个函数,实现两个字符串的拼接。其参数数据类型为string。代码如下:
# cat string.gopackage mainimport ( "fmt")//go:noinlinefunc join(s1, s2 string) string { return s1 + s2}func main() { s1 := "world" ss := join("hello,", s1) fmt.Println(ss)}# ./stringhello,world
使用bpftrace追踪join函数,代码如下:
#!/usr/bin/bpftraceuprobe:./string:main.join{ printf("arg1:%s\n", str(sarg0)); printf("arg2:%s\n", str(sarg1));}
执行bpftrace代码后,可以看到,输出结果为:
# bpftrace string.btAttaching 1 probe...arg1:hello,objectpopcntselectstringstructsweep sysmontimersuint16uinarg2:
从输出结果来看,显然bpftrace追踪代码的实现是不对的。因此,对golang程序进一步分析。
分析
使用gdb调试golang程序,查看join函数的参数传递。在ss := join("hello,", s1)处设置断点,运行后,单步跟踪至函数调用处,查看对应汇编代码。
(gdb) b 13(gdb) r(gdb) si 6(gdb) disassembleDump of assembler code for function main.main: 0x0000000000498e80 <+0>: mov %fs:0xfffffffffffffff8,%rcx 0x0000000000498e89 <+9>: cmp 0x10(%rcx),%rsp 0x0000000000498e8d <+13>: jbe 0x498f47 <main.main+199> 0x0000000000498e93 <+19>: sub $0x58,%rsp 0x0000000000498e97 <+23>: mov %rbp,0x50(%rsp) 0x0000000000498e9c <+28>: lea 0x50(%rsp),%rbp 0x0000000000498ea1 <+33>: lea 0x2309d(%rip),%rax # 0x4bbf45 0x0000000000498ea8 <+40>: mov %rax,(%rsp) 0x0000000000498eac <+44>: movq $0x6,0x8(%rsp) 0x0000000000498eb5 <+53>: lea 0x22f35(%rip),%rax # 0x4bbdf1 0x0000000000498ebc <+60>: mov %rax,0x10(%rsp) 0x0000000000498ec1 <+65>: movq $0x5,0x18(%rsp)=> 0x0000000000498eca <+74>: callq 0x498e00 <main.join> 0x0000000000498ecf <+79>: mov 0x20(%rsp),%rax 0x0000000000498ed4 <+84>: mov 0x28(%rsp),%rcx 0x0000000000498ed9 <+89>: mov %rax,(%rsp) 0x0000000000498edd <+93>: mov %rcx,0x8(%rsp) 0x0000000000498ee2 <+98>: callq 0x40a120 <runtime.convTstring> 0x0000000000498ee7 <+103>: mov 0x10(%rsp),%rax 0x0000000000498eec <+108>: xorps %xmm0,%xmm0 0x0000000000498eef <+111>: movups %xmm0,0x40(%rsp) 0x0000000000498ef4 <+116>: lea 0xaa05(%rip),%rcx # 0x4a3900
结合如下join函数的汇编代码,
0x0000000000498ea8 <+40>: mov %rax,(%rsp) 0x0000000000498eac <+44>: movq $0x6,0x8(%rsp) 0x0000000000498eb5 <+53>: lea 0x22f35(%rip),%rax # 0x4bbdf1 0x0000000000498ebc <+60>: mov %rax,0x10(%rsp) 0x0000000000498ec1 <+65>: movq $0x5,0x18(%rsp)
可以得出:
- join函数的参数是通过栈进行传递的。因此,可以通过sargN变量访问到函数的参数。(argN和sargN是有区别的。)
- 在main函数中,一共向join函数传递了4个参数,在栈中的位置分别为:$rsp、$rsp+0x8、$rsp+0x10和$rsp+0x18。
为了进一步验证,查看栈中保存的内容:
(gdb) p ($rsp)$4 = (void *) 0xc000064f28(gdb) x/10c *0xc000064f280x4bbf45: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 44 ',' 111 'o' 98 'b'0x4bbf4d: 106 'j' 101 'e'(gdb) p *($rsp+0x10)Attempt to dereference a generic pointer.(gdb) p ($rsp+0x10)$3 = (void *) 0xc000064f38(gdb) x/10c *0xc000064f380x4bbdf1: 119 'w' 111 'o' 114 'r' 108 'l' 100 'd' 119 'w' 114 'r' 105 'i'0x4bbdf9: 116 't' 101 'e
同时,$rsp+0x8为字符串"hello,"的长度6,$rsp+0x18为字符串"world"的长度5。
验证
通过前面的分析,将bpftrace代码修改为:
#!/usr/bin/bpftraceuprobe:./string:main.join{ printf("arg1[%d]:%s\n", sarg1, str(sarg0, sarg1)); printf("arg2[%d]:%s\n", sarg3, str(sarg2, sarg3));}
运行之后,可以看到结果:
# bpftrace string.btAttaching 1 probe...arg1[6]:hello,arg2[5]:world
指针参数
上文中,函数join的两个参数均为string类型,现将其定义为string指针,对应函数调用的汇编代码也发生了变化,如下所示:
//go:noinlinefunc join(s1, s2 *string) string { return *s1 + *s2}ss1 := join(&s1, &s1)对应的汇编代码:498fd0: 48 89 04 24 mov %rax,(%rsp)498fd4: 48 89 44 24 08 mov %rax,0x8(%rsp)498fd9: e8 a2 fe ff ff callq 498e80 <main.join>
可以看到,传递给join函数的参数只有两个,即s1的地址。因此,在uprobe中sargN存储的内容为字符串的地址。此时,需要将该地址转换为string结构体才能可视化输出。
查阅golang中string结构的存储结构,在bpftrace程序中定义一个GoString的结构体:
struct GoString { char* str; int len;};
uprobe的代码可以写成:
uprobe:./string:main.join{ $p1 = (struct GoString*) sarg0; printf("arg1[%d]:%s\n", $p1->len, str($p1->str, $p1->len)); $p2 = (struct GoString*) sarg1; printf("arg2[%d]:%s\n", $p2->len, str($p2->str, $p2->len));}
通过这种方式,有效地解决了地址参数的可视化输出的问题。同样的原理,如果参数为结构体或其他数据类型,也可以通过同样的方式进行解决。
总结
使用bpftrace分析应用程序的输入参数,重点在于,需要弄清楚:
- 参数的传递方式:是栈进行传递?还是寄存器进行传递?这决定了在uprobe中,使用sargN还是argN来获取参数。
- 参数的存储结构:如果参数是一个地址或结构体,需要将该地址强转到对应的数据结构,才能正常解析。
发表回复
评论列表(0条)