在前面文章的例子中,函数参数都是整数类型,对于 string 数据类型,bpftrace的追踪方式也是一样的吗?
例子
golang程序中定义了一个函数,实现两个 字符串 的拼接。其参数数据类型为string。代码如下:
# cat string.go
package main
import (
"fmt"
)
//go:noinline
func join(s1, s2 string) string {
return s1 + s2
}
func main() {
s1 := "world"
ss := join("hello,", s1)
fmt.Println(ss)
}
# ./string
hello,world
使用bpftrace追踪 join 函数,代码如下:
#!/usr/bin/bpftrace
uprobe:./string:main.join
{
printf ("arg1:%s\n", str(sarg0));
printf("arg2:%s\n", str(sarg1));
}
执行bpftrace代码后,可以看到,输出结果为:
# bpftrace string.bt
Attaching 1 probe...
arg1:hello,objectpopcntselectstring struct sweep sysmontimersuint16uin
arg2:
从输出结果来看,显然bpftrace追踪代码的实现是不对的。因此,对golang程序进一步分析。
分析
使用 gdb 调试golang程序,查看 join 函数的参数传递。在 ss := join(“hello,”, s1) 处设置断点,运行后,单步跟踪至函数调用处,查看对应汇编代码。
(gdb) b 13
(gdb) r
(gdb) si 6
(gdb) disassemble
Dump 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 *0xc000064f28
0x4bbf45: 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 *0xc000064f38
0x4bbdf1: 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/bpftrace
uprobe:./string:main.join
{
printf("arg1[%d]:%s\n", sarg1, str(sarg0, sarg1));
printf("arg2[%d]:%s\n", sarg3, str(sarg2, sarg3));
}
运行之后,可以看到结果:
# bpftrace string.bt
Attaching 1 probe...
arg1[6]:hello,
arg2[5]:world
指针参数
上文中,函数 join 的两个参数均为string类型,现将其定义为string指针,对应函数调用的汇编代码也发生了变化,如下所示:
//go:noinline
func 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 来获取参数。
- 参数的存储结构:如果参数是一个地址或结构体,需要将该地址强转到对应的数据结构,才能正常解析。