bpftrace 无侵入遍历golang链表

bpftrace 基于 bcc 进行开发的工具,语法简洁、功能强大。用其分析Linux 环境下的程序会很方便。本文构造了一个入参为链表头节点的函数使用场景,通过使用bpftrace无侵入遍历链表成员的方式,介绍bpftrace attach uprobe 的使用。更多使用说明见:bpftrace官网使用文档

# 执行结果

下面直接给出执行结果。可以看到,通过bpftrace脚本输出的结果与代码中实际遍历的结果相同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sudo ./handle.bt  // 先启动监听
Attaching 1 probe...  // 启动后停止在这里
=== enter main.handle.  // 目标程序执行后输出
name: Alice
age : 10
name: Bob
age : 11
name: Claire
age : 12
=== total node: 3

./demo  // 再执行目标程序
cur name: Alice, cur aget: 10
cur name: Bob, cur aget: 11
cur name: Claire, cur aget: 12

# 示例说明

系统环境如下:

1
2
3
Linux 4.18.0-193.6.3.el8_2.v1.2.x86_64
bpftrace v0.14.0-72-g6761-dirty
go version go1.16.15 linux/amd64

示例环境目录:

1
2
3
4
5
.
├── demo
├── go.mod
├── handle.bt
└── main.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
29
30
31
32
33
34
35
36
37
38
39
// main.go
package main

import (
    "context"
    "fmt"
)

type Student struct {
    Name string
    Age  int64
    // Comment [600]Byte 这样会使得这个问题变得很麻烦,hhh
    Next *Student
}

// 添加如下配置以防止函数被编译优化掉
//go:noinline
func handle(ctx context.Context, student *Student) {
    for cur := student; cur != nil; cur = cur.Next {
        fmt.Printf("cur name: %s, cur aget: %d
", cur.Name, cur.Age)
    }
    return
}

func main() {
    first := &Student{
        Name: "Alice",
        Age:  10,
        Next: &Student{
            Name: "Bob",
            Age:  11,
            Next: &Student{
                Name: "Claire",
                Age:  13,
                Next: nil,
            }}}
    handle(context.Background(), first)
}
 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
// handle.bt
#!/bin/bpftrace

// 当目标结构体较小时(使得整体栈开销 < 512Byte),可以直接构造使用
struct student{
    u64 name_ptr;
    u64 name_length;
    long  age;
    struct student *next;
};

uprobe:./demo:"main.handle"
{
    printf("=== enter main.handle.
");

    $cur = (struct student *)sarg2;
    if ($cur == 0){
        printf("input param is nil.
");
        return;
    }
    $node_count = 1;
    unroll(10){  // 这里定义的最大节点数量为10
        printf("name: %s
", str($cur->name_ptr, $cur->name_length));
        printf("age : %d
", $cur->age);
        $cur = $cur->next;
        if ($cur == 0){
            printf("=== total node: %d
", $node_count);
            return;
        }
        $node_count += 1;
    }

    printf("==== meet max
");
    return;
}

在编译完成main.go后,通过sudo bpftrace -l "uprobe:./demo:*" > uprobe.info的方式,可以获取demo中所有可以attachuprobe信息。这里说明下取student*指针值时,为什么取sarg3bpftrace中对内存传参的参数支持是sarg0, sarg1, sarg2...,且每个参数实际只对应8Byte大小的空间。对于func handle(ctx context.Context, student *Student)函数来说,由于context.Context实际占用2*8Byte的空间(见golang常见类型字节数),因此需要使用sarg2来取student的值,而非直觉上的sarg1
整个过程比较简单、明了。只要拥有root权限,基本上可以对系统内的任何进程进行详细的分析。

Hello, World!
使用 Hugo 构建
主题 StackJimmy 设计