BCC小demo系列
实现效果:有文件打开时,输出打开文件的进程与该文件的文件名
在上一篇的hello world中,我们只是简单的在系统有文件打开操作时,打印了hello wold。实际上,通常当我们绑定了do_sys_open函数时,更加想知道执行该调用的进程时什么,被打开的文件是什么。
这个小功能主要的实现点在于:
- 如何读取内核函数的参数
- 如何通过参数获取文件名和调用该函数的进程名
获取内核函数的参数
两种方式可以获取内核函数的参数
- 将想要获取的函数参数作为kprobe绑定函数的入参
- 直接读取参数所在寄存器的值
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
int kprobe__do_sys_open(struct pt_regs *ctx, void *dummy, char* filename)
第二种方法更加直接。只需要知道参数保存在哪个寄存器,直接读取寄存器的值。
这里内核提供了参数寄存器的变量PT_REGS_PARM*。即使用PT_REGS_PARM2(ctx)就可以获取到函数的第二个参数值
// bpf_tracing.h
#define PT_REGS_PARM1(x) ((x)->di)
#define PT_REGS_PARM2(x) ((x)->si)
#define PT_REGS_PARM3(x) ((x)->dx)
#define PT_REGS_PARM4(x) ((x)->cx)
#define PT_REGS_PARM5(x) ((x)->r8)
#define PT_REGS_RET(x) ((x)->sp)
#define PT_REGS_FP(x) ((x)->bp)
#define PT_REGS_RC(x) ((x)->ax)
#define PT_REGS_SP(x) ((x)->sp)
#define PT_REGS_IP(x) ((x)->ip)
获取调用进程的pid
bpf_get_current_pid_tgid
Syntax: u64 bpf_get_current_pid_tgid(void)
Return: current->tgid << 32 | current->pid
Returns the process ID in the lower 32 bits (kernel’s view of the PID, which in user space is usually presented as the thread ID), and the thread group ID in the upper 32 bits (what user space often thinks of as the PID). By directly setting this to a u32, we discard the upper 32 bits.
u32 pid = bpf_get_current_pid_tgid() >> 32;
简单的实现
知道了如何获取函数参数以及调用进程后,只要在hello world代码的基础上,作一些小的改动,就可以简单地实现我们先要的功能。
通过BPF程序编译执行的流程都是一样的,只需要修改注入的c代码
package main
import (
"fmt"
bpf "github.com/iovisor/gobpf/bcc"
"github.com/iovisor/gobpf/pkg/tracepipe"
"os"
)
import "C"
const source string = `
#include <uapi/linux/ptrace.h>
int kprobe__do_sys_open(struct pt_regs *ctx, void *dummy, char* fname)
{
char buf[256];
bpf_probe_read(&buf, sizeof(buf), (void *) fname );
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_trace_printk("pid=%d, file= %s\n", pid, &buf);
return 0;
}
`
func main() {
m := bpf.NewModule(source, []string{})
defer m.Close()
kp, err := m.LoadKprobe("kprobe__do_sys_open")
if err != nil {
fmt.Printf("Failed to load kprobe count: %s\n", err)
os.Exit(1)
}
err = m.AttachKprobe("do_sys_open", kp, -1)
if err != nil {
fmt.Printf("Failed to attach kprobe to strlen: %s\n", err)
os.Exit(1)
}
// 逐行读取tracepipe中的数据,如果输出没有换行,就不会读取到数据
tp, err := tracepipe.New()
if err != nil {
fmt.Printf("Failed to attach kprobe to strlen: %s\n", err)
os.Exit(1)
}
defer tp.Close()
channel, errChannel := tp.Channel()
for {
select {
case event := <-channel:
fmt.Printf("%+v\n", event)
case err := <-errChannel:
fmt.Printf("%+v\n", err)
}
}
}
效果:
上面的代码只是简单的实现了我们想要的打印文件名和进程id的功能。实际使用时却发现了两个问题,算是踩坑了
1. 文件名较长时只能打印部分内容
这里调整了buf大小,换了几个参数都没有用。结果看了下bpf_trace_printk的源码实现。函数的输出长度限制为64个字节,超出就截断了,所以tracepipe获取到的内核输出只有64个字节
ps:测试使用版本为v5.4
2. bpf_trace_printk()一次最多只能接收3个输出参数,如果想打印更多的内容,就不能使用它
针对这两个问题,可以对输出方式进行改进,让咱们的案例更加可用。
我们将bpf_trace_printk 输出换成perf_event_output,将内核空间的参数先传值到用户空间,再进行打印。这样对于数据的处理和展示也更加的灵活
package main
import (
"bytes"
"encoding/binary"
"fmt"
bpf "github.com/iovisor/gobpf/bcc"
"os"
"os/signal"
)
import "C"
const source string = `
#include <uapi/linux/ptrace.h>
struct event_data_t {
u32 pid;
char fname[256]; // max of filename
};
BPF_PERF_OUTPUT(fnamearr);
int kprobe__do_sys_open(struct pt_regs *ctx, void *dummy, char* fname)
{
//char file_name[400];
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct event_data_t evt = {};
evt.pid = pid;
bpf_probe_read(&evt.fname, sizeof(evt.fname), (void *) fname );
fnamearr.perf_submit(ctx, &evt, sizeof(evt)); // 将内核参数传递到用户空间
return 0;
}
`
type Event struct {
Pid uint32
Str [256]byte
}
func main() {
m := bpf.NewModule(source, []string{})
defer m.Close()
kp, err := m.LoadKprobe("kprobe__do_sys_open")
if err != nil {
fmt.Printf("Failed to load kprobe count: %s\n", err)
os.Exit(1)
}
err = m.AttachKprobe("do_sys_open", kp, -1)
if err != nil {
fmt.Printf("Failed to attach kprobe to strlen: %s\n", err)
os.Exit(1)
}
table := bpf.NewTable(m.TableId("fnamearr"), m)
channel := make(chan []byte)
perfMap, err := bpf.InitPerfMap(table, channel, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to init perf map: %s\n", err)
os.Exit(1)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, os.Kill)
fmt.Printf("%10s\t%s\n", "PID", "filename")
go func() {
var event Event
for {
data := <-channel
err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &event)
if err != nil {
fmt.Printf("failed to decode received data: %s\n", err)
continue
}
// Convert C string (null-terminated) to Go string
comm := string(event.Str[:bytes.IndexByte(event.Str[:], 0)])
fmt.Printf("%10d\t%s\n", event.Pid, comm)
}
}()
perfMap.Start()
<-sig
perfMap.Stop()
}
输出效果:
想要更简单地实现该功能,还可以借助内核自带的trace工具
cd /sys/kernel/debug/tracing
echo 'p:open do_sys_open file=+0(%si):string' > kprobe_events
或者
echo 'p:open do_sys_open file=+0($arg2):string' > kprobe_events
echo 1 > events/kprobes/open/enable # 手动打开开关
cat error_log # 随便打开一个文件,测试用
cat trace. # 输出结果
【参考】
https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md
https://stackoverflow.com/questions/62441361/bpf-how-to-inspect-syscall-arguments
https://elixir.bootlin.com/linux/v5.4.182/source/kernel/trace/bpf_trace.c#L214
https://blog.csdn.net/sydyh43/article/details/122262587
https://elixir.bootlin.com/linux/latest/source/Documentation/trace/kprobetrace.rst