在前两篇eBPF: Attach Go(Part0): Go 语言基础和特性和eBPF: Attach Go(Part1): Go ABI SPEC (silence.pink)中,我们已经对Go语言的基础类型和接口的实现、Go的ABI和在eBPF程序中访问获取Go函数的参数和返回值有所了解。而本文,将介绍如何编写eBPF程序去挂钩Go程序的函数,以及提到了eBPF uretprobe引起的程序崩溃问题。
前言
Go: go1.20.7 linux/amd64
OS: Ubuntu 22.04.2 LTS
Kernel: Linux 5.19.0-50-generic
我们在以下面的代码为例,看看eBPF程序是如何获取Go函数的参数和返回值的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// main.go
package main
import (
"fmt"
"time"
)
func main() {
var v1, v2, v3 int64 = 2, 3, 4
var str string = "cccccc"
var b []byte = []byte(str)
for {
go foo(v1, v2, v3, b)
time.Sleep(time.Second)
}
}
//go:noinline
func foo(a1, a2, a3 int64, b []byte) (int64, []byte) {
var b1, b2 int64 = 10, 20
var c []byte = b
if a1 > 6 {
return a1, c
}
fmt.Printf("foo return :%d\n", a1*a3+a2*a3+b1*b2)
return a1*a3 + a2*a3 + b1*b2, c
}
|
结合前面篇章中的知识,对上述代码我们有:
所以,在eBPF的kern代码中,我们可以很容易的如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// gouprobe.bpf.c
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// func foo(a1, a2, a3 int64, bbbb []byte) (int64, []byte)
SEC("uprobe/foo")
int probe_entry_foo(struct pt_regs *ctx) {
uint64_t a1 = (uint64_t)(ctx->ax);
uint64_t a2 = (uint64_t)(ctx->bx);
uint64_t a3 = (uint64_t)(ctx->cx);
uint64_t b_data = (char*)(ctx->di);
bpf_printk("foo entry, a1:%ld, a2:%ld, a3:%ld, b:%s",
a1, a2, a3, b_data);
}
SEC("uretprobe/foo")
int probe_exit_foo(struct pt_regs *ctx) {
uint64_t ret0 = (uint64_t)(ctx->ax);
uint64_t ret1 = (uint64_t)(ctx->bx);
bpf_printk("foo eixt, ~r0:%ld, ~r1:%s", ret0, ret1);
}
|
问题: eBPF uretprobe 引起的Go程序崩溃
然后,我们执行go build -gcflags=all="-N -l" main.go
编译得到Go的二进制文件main
,而eBPF文件编译得到gouprobe
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
./main
runtime: g 18: unexpected return pc for main.foo called from 0x7fffffffe000
stack: frame={sp:0xc00009aeb8, fp:0xc00009af70} stack=[0xc00009a000,0xc00009b000)
0x000000c00009adb8: 0x0000000000000000 0x0000000000000000
0x000000c00009adc8: 0x0000000000000000 0x0000000000000000
0x000000c00009add8: 0x0000000000000000 0x0000000000000000
0x000000c00009ade8: 0x0000000000000000 0x0000000000000000
0x000000c00009adf8: 0x0000000000000000 0x0000000000000000
0x000000c00009ae08: 0x0000000000000000 0x0000000000000000
0x000000c00009ae18: 0x000000c00009aea8 0x000000000049827e <fmt.Printf+0x000000000000009e>
0x000000c00009ae28: 0x00000000004c5ec8 0x000000c0000ae008
0x000000c00009ae38: 0x00000000004ba136 0x000000000000000f
0x000000c00009ae48: 0x000000c00009af08 0x0000000000000001
0x000000c00009ae58: 0x0000000000000001 0x0000000000000000
0x000000c00009ae68: 0x0000000000000000 0x0000000000000000
0x000000c00009ae78: 0x0000000000000000 0x0000000000000000
0x000000c00009ae88: 0x0000000000000000 0x0000000000000000
0x000000c00009ae98: 0x0000000000000000 0x0000000000000000
0x000000c00009aea8: 0x000000c00009af60 0x000000000049f516 <main.foo+0x00000000000001b6>
0x000000c00009aeb8: <0x00000000004ba136 0x000000000000000f
0x000000c00009aec8: 0x000000c00004a708 0x0000000000000001
0x000000c00009aed8: 0x0000000000000001 0x0000000000000000
0x000000c00009aee8: 0x0000000000000014 0x000000000000000a
0x000000c00009aef8: 0x0000000000532ee0 0x000000c00004a708
0x000000c00009af08: 0x00000000004a7220 0x0000000000532ee0
0x000000c00009af18: 0x0000000000000000 0x0000000000000000
0x000000c00009af28: 0x0000000000000000 0x000000c0000b4000
0x000000c00009af38: 0x0000000000000006 0x0000000000000008
0x000000c00009af48: 0x000000c00004a708 0x0000000000000001
0x000000c00009af58: 0x0000000000000001 0x000000c00004a7d0
0x000000c00009af68: !0x00007fffffffe000 >0x0000000000000002
0x000000c00009af78: 0x0000000000000003 0x0000000000000004
0x000000c00009af88: 0x000000c0000b4000 0x0000000000000006
0x000000c00009af98: 0x0000000000000008 0x0000000000000004
0x000000c00009afa8: 0x0000000000000003 0x0000000000000002
0x000000c00009afb8: 0x000000c0000b4000 0x0000000000000006
0x000000c00009afc8: 0x0000000000000008 0x0000000000000000
0x000000c00009afd8: 0x0000000000467141 <runtime.goexit+0x0000000000000001> 0x0000000000000000
0x000000c00009afe8: 0x0000000000000000 0x0000000000000000
0x000000c00009aff8: 0x0000000000000000
fatal error: unknown caller pc
runtime stack:
runtime.throw({0x4ba8ad?, 0x52cd20?})
/home/xxx/xxx/go1.20.7/src/runtime/panic.go:1047 +0x5d fp=0x7ffdb981bbc0 sp=0x7ffdb981bb90 pc=0x437abd
runtime.gentraceback(0xffffffffffffffff, 0xffffffffffffffff, 0x7ffdb981bf10?, 0xc0000824e0, 0x0, 0x0, 0x7fffffff, 0x4c1050, 0xc?, 0x0)
... ...
|
是的,由于使用了eBPF uretprobe而引起了被attach的Go程序的崩溃。其实,在bcc和ecapture的相关讨论和博客中,都提到了在对Go程序使用uprobe时,会导致目标进程崩溃,uretprobe似乎通过修改堆栈来放置返回探针,这与Go管理堆栈的方式冲突(Go中的堆栈可以在任何时候增长/缩小,它通过将整个堆栈复制到一个新的较大区域,调整堆栈中的指针以指向新区域等方式实现)。因此,如果我们正在进行uretprobe操作,并且堆栈在此期间发生增长(或缩小),它可能导致Go运行时发生错误。
值得注意的是,我尝试浮现的过程发现也并不是每次都会导致崩溃?在未使用goroutine
的情况下,倒是正常运行的。
替代方案
得益于前人的栽树,崩溃的问题已有成熟的解决方案:使用uprobe来替代uretprobe,原理就是在函数的RET
指令处插入uprobe。
如果用过libbpf的框架就很清楚在eBPF attach时有一个func_offset
的参数,如果再往libbpf更深处查看其实会发现,在内部libbpf其实会去读取ELF格式的二进制文件,然后根据symbol
得到函数符号在文件中的偏移offset,也就是说假设你知道函数符号相对于文件的偏移,那么你就可以不用告诉libbbpf函数符号symbol
名称,而是直接使用offset挂载(只是libbpf帮我们做了从函数符号到偏移值转换的这一步)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* @brief **bpf_program__attach_uprobe_opts()** is just like
* bpf_program__attach_uprobe() except with a options struct
* for various configurations.
*
* @param prog BPF program to attach
* @param pid Process ID to attach the uprobe to, 0 for self (own process),
* -1 for all processes
* @param binary_path Path to binary that contains the function symbol
* @param func_offset Offset within the binary of the function symbol
* @param opts Options for altering program attachment
* @return Reference to the newly created BPF link; or NULL is returned on error,
* error code is stored in errno
*/
LIBBPF_API struct bpf_link *
bpf_program__attach_uprobe_opts(const struct bpf_program *prog, pid_t pid,
const char *binary_path, size_t func_offset,
const struct bpf_uprobe_opts *opts);
|
我们可以使用readelf
和objdump
对Go二进制文件进行反汇编有:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
readelf -s main|grep main.foo
2253: 000000000049f360 630 FUNC GLOBAL DEFAULT 1 main.foo
objdump -D --start-address=0x049f360 --stop-address=0x049f5e6 main
objdump -D --start-address=0x049f360 --stop-address=0x049f5e6 main
main: 文件格式 elf64-x86-64
Disassembly of section .text:
000000000049f360 <main.foo>:
49f360: 4c 8d 64 24 d0 lea -0x30(%rsp),%r12
49f365: 4d 3b 66 10 cmp 0x10(%r14),%r12
49f369: 0f 86 21 02 00 00 jbe 49f590 <main.foo+0x230>
49f36f: 48 81 ec b0 00 00 00 sub $0xb0,%rsp
49f376: 48 89 ac 24 a8 00 00 mov %rbp,0xa8(%rsp)
49f37d: 00
49f37e: 48 8d ac 24 a8 00 00 lea 0xa8(%rsp),%rbp
49f385: 00
49f386: 48 89 84 24 b8 00 00 mov %rax,0xb8(%rsp)
49f38d: 00
49f38e: 48 89 9c 24 c0 00 00 mov %rbx,0xc0(%rsp)
... ...
|
查找ret
指令的位置:
1
2
3
|
objdump -D --start-address=0x049f360 --stop-address=0x049f5e6 main|grep ret
49f44a: c3 ret
49f58f: c3 ret
|
所以得到函数main.foo
中ret
的偏移位置为:
1
2
|
0x49f44 - 0x49f360 = 0xea
0x49f58f - 0x49f360 = 0x22f
|
示例
kern修改代码如下:
1
2
3
4
5
6
7
|
SEC("uprobe/foo_exit")
int probe_exit_foo(struct pt_regs *ctx) {
uint64_t ret0 = (uint64_t)(ctx->ax);
uint64_t ret1 = (uint64_t)(ctx->bx);
bpf_printk("foo eixt, ~r0:%ld, ~r1:%s", ret0, ret1);
}
|
user修改代码为如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
obj->links.probe_exit_foo =
bpf_program__attach_uprobe_opts(obj->progs.probe_exit_foo,
-1,
binary,
0xea,
&goabi_opts
);
obj->links.probe_exit_foo =
bpf_program__attach_uprobe_opts(obj->progs.probe_exit_foo,
-1,
binary,
0x22f,
&goabi_opts
);
|
查看日志打印:
参考链接
golang uretprobe的崩溃与模拟实现 – CFC4N的博客 (cnxct.com)
Go crash with uretprobe · Issue #1320 · iovisor/bcc