问题
在 的golang代码中,函数add的上一行,增加了一条注释语句: //go:noinline 。在bpftrace追踪时,是否可以去掉?有什么作用?
现象
为了说明该问题,设计一个例子。
golang代码中,有两个求和函数。其中,add1加上 //go:noinline ,另一个add2不加。代码如下:
# cat inline.go
package main
import "fmt"
//go:noinline
func add1(a, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func main() {
a := 1
r1 := add1(a, 2)
fmt.Println(r1)
r2 := add2(a, 3)
fmt.Println(r2)
}
# ./inline
3
4
bpftrace程序分别对函数add1和add2的输入参数、返回值进行追踪,代码如下:
# cat inline.bt
#!/usr/bin/bpftrace
uprobe:./inline:main.add1
{
printf("add1 arg1:%d\n", sarg0);
printf("add1 arg2:%d\n", sarg1);
}
ur:./inline:main.add1
{
printf("add1 retval:%d\n", retval);
}
uprobe:./inline:main.add2
{
printf("add2 arg1:%d\n", sarg0);
printf("add2 arg2:%d\n", sarg1);
}
ur:./inline:main.add2
{
printf("add2 retval:%d\n", retval);
}
# bpftrace ./inline.bt
Attaching 2 probes...
add1 arg1:1
add1 arg2:2
add1 retval:3
执行程序后,可以看到bpftrace程序能够正常追踪到函数add1,但是无法追踪到函数add2。
分析
通过上文中的示例代码,可以看到,没有加 //go:noinline 的函数无法被bpftrace程序追踪到。通过查阅golang相关文档,可以知道, //go:noinline 表示该函数在编译时,不会被内联。
使用 objump -S 生成golang程序的汇编代码如下:
a := 1
r1 := add1(a, 2)
498e41: 48 c7 04 24 01 00 00 movq $0x1,(%rsp)
498e48: 00
498e49: 48 c7 44 24 08 02 00 movq $0x2,0x8(%rsp)
498e50: 00 00
498e52: e8 a9 ff ff ff callq 498e00 <main.add1>
498e57: 48 8b 44 24 10 mov 0x10(%rsp),%rax
fmt.Println(r1)
-----省略无关代码-----
498ead: 48 c7 44 24 20 01 00 movq $0x1,0x20(%rsp)
498eb4: 00 00
498eb6: e8 a5 9a ff ff callq 492960 <fmt.Fprintln>
r2 := add2(a, 3)
fmt.Println(r2)
498ebb: 48 c7 04 24 04 00 00 movq $0x4,(%rsp)
498ec2: 00
498ec3: e8 d8 11 f7 ff callq 40a0a0 < runtime .convT64>
498ec8: 48 8b 44 24 08 mov 0x8(%rsp),%rax
498ecd: 0f 57 c0 xorps %xmm0,%xmm0
498ed0: 0f 11 44 24 40 movups %xmm0,0x40(%rsp)
498ed5: 48 8d 0d a4 a3 00 00 lea 0xa3a4(%rip),%rcx # 4a3280 <type.*+0xa280>
通过汇编代码,我们可以看到,主函数中,地址 0x498e52 处 callq 498e00 <main.add1> 调用了add1函数,地址 0x498ebb 处 movq $0x4,(%rsp) 直接计算求值。
因此,golang编译器在编译代码时,会对代码进行分析,并按照内联规则,将某些函数生成内联代码。一旦函数被内联,bpftrace将无法追踪到对应函数。也就是,上文中函数 add2 无法被追踪到。
解决方法
针对golang程序中编译器内联的问题,可以通过禁止内联的方式来解决。禁止内联的方式有:
- 全局禁用:设定编译参数为 go build -gcflags=”-l” ,其缺点是,所有函数都不会进行内联编译,对程序的运行性能可能会有一定的影响。
- 函数禁用:在函数定义的前一行,加上 //go:noinline ,golang编译器,对该函数不会进行内联。
在实践中,可以通过 go build -gcflags=”-m -m” 来查看,哪些函数会在编译时执行内联,如:
# go build -gcflags="-m -m" inline.go
# command-line-arguments
./inline.go:6:6: cannot inline add1: marked go:noinline
./inline.go:10:6: can inline add2 with cost 4 as: func(int, int) int { return a + b }
./inline.go:14:6: cannot inline main: function too complex: cost 234 exceeds budget 80
./inline.go:17:13: inlining call to fmt.Println func(...interface {}) (int, error) { var fmt..autotmp_3 int; fmt..autotmp_3 = <N>; var fmt..autotmp_4 error; fmt..autotmp_4 = <N>; fmt..autotmp_3, fmt..autotmp_4 = fmt.Fprintln(io.Writer(os.Stdout), fmt.a...); return fmt..autotmp_3, fmt..autotmp_4 }
./inline.go:18:12: inlining call to add2 func(int, int) int { return a + b }
./inline.go:19:13: inlining call to fmt.Println func(...interface {}) (int, error) { var fmt..autotmp_3 int; fmt..autotmp_3 = <N>; var fmt..autotmp_4 error; fmt..autotmp_4 = <N>; fmt..autotmp_3, fmt..autotmp_4 = fmt.Fprintln(io.Writer(os.Stdout), fmt.a...); return fmt..autotmp_3, fmt..autotmp_4 }
从输出中,可以看到:
- 函数add1被标记为不进行内联: cannot inline add1: marked go:noinline 。
- 函数add2内联代价小,进行内联: can inline add2 with cost 4 as: func(int, int) int { return a + b } 。
- main函数太大,内联代价超过80,不进行内联: cannot inline main: function too complex: cost 234 exceeds budget 80 。
关于golang编译器进行内联的场景,可以参考golang源码:。
总结
由于golang编译器内联优化,bpftrace可能无法正常追踪golang程序。在编写bpftrace脚本时,可以先使用 nm 命令查看一下可执行程序,是否存在需要追踪的函数的符号信息。如果没有则bpftrace将不能对其进行追踪。
前面的示例中,都是对 int 类型的参数进行追踪,那对于 string 类型的参数,是否也可以用同样的方式进行追踪?将在下一篇中进行讨论。