我是靠谱客的博主 火星上过客,最近开发中收集的这篇文章主要介绍Golang BCC 开发学习笔记--打印进程与文件名功能拆解简单的实现输出改进扩展实现,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

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

文章目录

  • 功能拆解
    • 获取内核函数的参数
    • 获取调用进程的pid
  • 简单的实现
  • 输出改进
  • 扩展实现

在上一篇的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= %sn", 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: %sn", err)
		os.Exit(1)
	}

	err = m.AttachKprobe("do_sys_open", kp, -1)
	if err != nil {
		fmt.Printf("Failed to attach kprobe to strlen: %sn", err)
		os.Exit(1)
	}

    // 逐行读取tracepipe中的数据,如果输出没有换行,就不会读取到数据
	tp, err := tracepipe.New()
	if err != nil {
		fmt.Printf("Failed to attach kprobe to strlen: %sn", err)
		os.Exit(1)
	}

	defer tp.Close()
	channel, errChannel := tp.Channel()
	for {
		select {
		case event := <-channel:
			fmt.Printf("%+vn", event)
		case err := <-errChannel:
			fmt.Printf("%+vn", 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: %sn", err)
		os.Exit(1)
	}

	err = m.AttachKprobe("do_sys_open", kp, -1)
	if err != nil {
		fmt.Printf("Failed to attach kprobe to strlen: %sn", 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: %sn", err)
		os.Exit(1)
	}
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt, os.Kill)

	fmt.Printf("%10st%sn", "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: %sn", err)
				continue
			}
			// Convert C string (null-terminated) to Go string
			comm := string(event.Str[:bytes.IndexByte(event.Str[:], 0)])
			fmt.Printf("%10dt%sn", 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

最后

以上就是火星上过客为你收集整理的Golang BCC 开发学习笔记--打印进程与文件名功能拆解简单的实现输出改进扩展实现的全部内容,希望文章能够帮你解决Golang BCC 开发学习笔记--打印进程与文件名功能拆解简单的实现输出改进扩展实现所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(47)

评论列表共有 0 条评论

立即
投稿
返回
顶部