Go 1.18.1下eBPF获取并修改函数参数值失败问题排查
Go程序Uprobe捕获并修改函数参数失败分析与解决
问题背景
环境:Go 1.18.1
编写了两个程序tracee.go和tracer.go,需求是通过eBPF的uprobe捕获simpleFunction的参数值,并将其修改为520。
tracee.go代码
package main import ( "fmt" ) //go:noinline func simpleFunction(val int) { fmt.Println("item value:", val) } func main() { simpleFunction(100) }
tracer.go原代码
package main import ( "bytes" "encoding/binary" "flag" "fmt" "github.com/iovisor/gobpf/bcc" "log" "os" "os/signal" ) var eBPF_Text = ` #include <uapi/linux/ptrace.h> #include <linux/string.h> BPF_PERF_OUTPUT(events); inline int function_was_called(struct pt_regs *ctx) { void* stackAddr = (void*)ctx->sp; long parameter_value; bpf_probe_read(¶meter_value, sizeof(parameter_value), stackAddr+8); long tmp = 520; bpf_probe_write_user(stackAddr+8, &tmp, sizeof(tmp)); events.perf_submit(ctx, ¶meter_value, sizeof(parameter_value)); return 0; } ` var tracePrg string var traceFun string func init() { flag.StringVar(&tracePrg, "binary", "", "") flag.StringVar(&traceFun, "func", "", "The function to probe") } func main() { flag.Parse() bpfModule := bcc.NewModule(eBPF_Text, []string{}) uprobeFd, err := bpfModule.LoadUprobe("function_was_called") if err != nil { panic(err) } if err = bpfModule.AttachUprobe(tracePrg, traceFun, uprobeFd, -1); err != nil { panic(err) } table := bcc.NewTable(bpfModule.TableId("events"), bpfModule) outputChannel := make(chan []byte) lostChan := make(chan uint64) perfMap, err := bcc.InitPerfMap(table, outputChannel, lostChan) if err != nil { log.Fatal(err) } sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) go func() { for { value := <-outputChannel var item int64 if err = binary.Read(bytes.NewBuffer(value), binary.LittleEndian, &item); err != nil { panic(err) } fmt.Println(item) } }() perfMap.Start() <-sigCh perfMap.Stop() }
操作步骤:
- 编译两个程序:
go build tracee.go、go build tracer.go - 启动tracer:
./tracer --binary ./tracee --func main.simpleFunction - 运行tracee:
./tracee
现象:tracee输出item value: 100,tracer捕获到的值为824633729440,参数修改未生效。
错误分析
- 参数位置判断错误:x86_64架构下,Go遵循System V调用约定,函数的第一个参数存储在
rdi寄存器中,而非栈上的sp+8位置。直接读取栈地址得到的是未初始化的垃圾内存值,导致捕获结果无效。 - 修改时机与位置错误:uprobe附加在函数入口(偏移量-1),此时函数还未将寄存器中的参数写入栈空间。后续函数执行时会把
rdi寄存器的值覆盖到栈上,你通过bpf_probe_write_user修改的栈内容会被覆盖,无法影响函数的参数使用。
修复方案
修改tracer.go中的eBPF代码,直接操作寄存器来获取和修改参数:
修改后的eBPF代码片段
#include <uapi/linux/ptrace.h> #include <linux/string.h> BPF_PERF_OUTPUT(events); inline int function_was_called(struct pt_regs *ctx) { // x86_64架构下,第一个参数存放在rdi寄存器 long parameter_value = ctx->rdi; // 修改rdi寄存器的值为520,函数后续执行会使用该值 ctx->rdi = 520; events.perf_submit(ctx, ¶meter_value, sizeof(parameter_value)); return 0; }
验证结果
- 重新编译tracer.go并启动,运行tracee后:
- tracee会输出
item value: 520,参数修改生效 - tracer会捕获到原始参数值
100
- tracee会输出
架构兼容说明
如果程序运行在ARM64架构下,函数第一个参数存储在x0寄存器中,需要将代码中的ctx->rdi替换为ctx->regs[0](或对应架构的寄存器字段)。
内容的提问来源于stack exchange,提问作者wuqinqiang
相关产品推荐
相关产品推荐

