Golang BCC 开发学习笔记--打印进程与文件名

BCC小demo系列
实现效果:有文件打开时,输出打开文件的进程与该文件的文件名

在上一篇的hello world中,我们只是简单的在系统有文件打开操作时,打印了hello wold。实际上,通常当我们绑定了do_sys_open函数时,更加想知道执行该调用的进程时什么,被打开的文件是什么。

这个小功能主要的实现点在于:

  1. 如何读取内核函数的参数
  2. 如何通过参数获取文件名和调用该函数的进程名

功能拆解

获取内核函数的参数

两种方式可以获取内核函数的参数

  1. 将想要获取的函数参数作为kprobe绑定函数的入参
  2. 直接读取参数所在寄存器的值

先看第一种,以上面代码中hook的do_sys_open函数为例,它的函数声明为
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)

可见我们想要获取到的文件名为第二个参数filename
所以在定义kprobe__do_sys_open 时,将filename设置为ctx后的第二个参数,需要注意的是,第一个dfd参数虽然没有用到,但是不能省略。
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

bcc提供了帮助函数 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.

该函数返回一个64位的uint值,高32位为线程组id(即我们需要的pid),低32为线程id。
所以我们要对返回值右移32位取到tgid(即用户态的pid)。反之如果需要取到内核层的pid,左移32位
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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值