eBPF程序在内核跟踪场景中经常需要读取被跟踪函数的参数。以网络协议栈为例,无论是tcp_v4_connect建立连接时的目的地址,还是ip_rcv接收包时指向skb的指针,这些参数对于诊断网络问题、统计流量特征都至关重要。传统方法依赖pt_regs结构,开发者需要根据CPU架构和函数调用约定,从寄存器数组中按偏移取出参数,例如x86_64下第1个参数在rdi寄存器,第2个在rsi。这种写法不仅代码晦涩,而且一旦内核函数签名变化或者换到ARM64等不同架构,参数位置映射就需要重新调整。bpf_get_func_arg正是为了解决这一痛点而引入的helper,它把参数序号抽象出来,让内核帮你完成寄存器到函数参数的语义转换。

在此之前,内核还提供了bpf_get_func_ip、bpf_get_retval等辅助函数,但它们不直接解决参数读取问题。bpf_get_func_arg首次出现在支持BPF trampoline的内核版本中,随后被推广到kprobe和kretprobe上下文。它的设计目标很简单:给一个参数索引n,返回该参数的值到用户提供的u64变量中。对于32位参数会自动零扩展,对于指针类型则直接存入地址值。这极大简化了代码,尤其适合快速迭代的观测工具开发。
理解bpf_get_func_arg的工作机制与适用条件
bpf_get_func_arg的函数原型定义在bpf_helper_defs.h中,签名如下:long bpf_get_func_arg(void *ctx, u32 n, u64 *value)。第一个参数ctx是程序上下文,对于kprobe类型就是struct pt_regs *,对于fentry/fexit类型则是struct bpf_insn的trampoline上下文。第二个参数n表示想要获取的函数参数序号,从0开始计数。第三个参数value是输出指针,helper会把对应参数的值写入这个地址。返回值0代表成功,负数代表错误码,例如n超出参数个数、当前上下文不支持该操作等。
需要注意的是,并非所有程序类型都能调用这个helper。从内核实现看,它依赖于底层架构提供的内省能力,即内核能否自动推导出函数签名并建立参数索引映射。对于kprobe/kretprobe,这一能力由CONFIG_HAVE_FUNCTION_ARG_ACCESS_API控制,大多数现代架构如x86_64、arm64、riscv都已支持。对于fentry/fexit程序,则依赖BPF trampoline机制,而BPF trampoline要求内核开启CONFIG_BPF_JIT且函数不能是内联或特殊优化的。如果你的内核版本较老或者目标函数被编译器优化掉了,调用bpf_get_func_arg可能返回-EINVAL或-EOPNOTSUPP。所以在编写BPF程序前,最好先确认运行内核的配置,可以通过/proc/config.gz查看CONFIG_HAVE_FUNCTION_ARG_ACCESS_API是否存在。
另外,bpf_get_func_arg只能获取整数、指针等标量参数。如果参数是结构体按值传递,比如struct sockaddr_in被展开进多个寄存器,那么按照C语言ABI,它可能占用多个参数槽位。此时helper并不会自动重建结构体,你只能看到第一个槽位的数值,后续数据需要自己拼接或者干脆改用bpf_probe_read_kernel读取指针指向的内存。这点与PT_REGS_PARM宏的行为一致,都只处理单个寄存器宽度的数据。
实战:用bpf_get_func_arg分析tcp_v4_connect与ip_rcv
网络内核函数中,tcp_v4_connect是一个很好的分析对象。它的定义在net/ipv4/tcp_ipv4.c中,原型为int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)。第0个参数sk是套接字指针,第1个参数uaddr是用户空间传入的地址结构指针,第2个参数addr_len是地址长度。如果我们想记录每个新建TCP连接的目标IP和端口,只需要获取第1个参数uaddr,然后使用bpf_probe_read_user读取它指向的struct sockaddr_in或sockaddr_in6。下面给出一个完整的kprobe BPF程序片段:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
SEC("kprobe/tcp_v4_connect")
int trace_tcp_v4_connect(struct pt_regs *ctx)
{
u64 uaddr_ptr = 0;
long ret = bpf_get_func_arg(ctx, 1, &uaddr_ptr);
if (ret != 0) {
bpf_printk("get arg1 failed: %ld\\n", ret);
return 0;
}
struct sockaddr_in sin4;
bpf_probe_read_user(&sin4, sizeof(sin4), (void *)uaddr_ptr);
u32 daddr = sin4.sin_addr.s_addr;
u16 dport = bpf_ntohs(sin4.sin_port);
bpf_printk("tcp connect to %pI4:%u\\n", &daddr, dport);
return 0;
}
char _license[] SEC("license") = "GPL";
代码中bpf_get_func_arg(ctx, 1, &uaddr_ptr)成功取得第二个参数后,uaddr_ptr存储的是用户空间虚拟地址。由于此时处于进程上下文,可以直接用bpf_probe_read_user读取。如果换成在软中断上下文触发的函数(比如ip_rcv),指针通常指向内核内存,应改用bpf_probe_read_kernel。另外注意打印IP地址使用了%pI4格式说明符,这是内核printk支持的IPv4打印格式,bpf_printk同样可用。
再来看ip_rcv函数,原型为int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)。第0个参数是skb,第1个参数是接收该包的网络设备。如果我们想统计每块网卡接收IP包的数量,可以获取第1个参数dev指针,然后读取dev->name字段。内核中struct net_device的name成员是字符数组,使用bpf_probe_read_kernel_str安全读取。示例代码如下:
SEC("kprobe/ip_rcv")
int trace_ip_rcv(struct pt_regs *ctx)
{
u64 dev_ptr = 0;
long ret = bpf_get_func_arg(ctx, 1, &dev_ptr);
if (ret != 0)
return 0;
char dev_name[IFNAMSIZ] = {};
struct net_device *dev = (struct net_device *)dev_ptr;
bpf_probe_read_kernel_str(dev_name, IFNAMSIZ, dev->name);
bpf_printk("ip_rcv on dev %s\\n", dev_name);
return 0;
}
注意在读取dev->name时,由于dev_ptr来自内核地址,必须使用bpf_probe_read_kernel_str而不能直接解引用。否则验证器会报错,因为直接指针解引用在内核探针上下文中是被禁止的。通过bpf_get_func_arg拿到指针值,再配合bpf_probe_read_kernel系列函数,能够安全地追踪嵌套结构体。
与PT_REGS_PARM的对比及参数读取最佳实践
传统BPF程序通常使用PT_REGS_PARM1、PT_REGS_PARM2等宏来获取参数。这些宏本质上是对pt_regs结构中寄存器名称的封装,例如在x86_64上PT_REGS_PARM1(ctx)展开为((ctx)->rdi)。它的优点是零额外开销,直接访问寄存器;缺点是严重依赖架构和函数调用约定,且当内核开启了CONFIG_FUNCTION_TRACER、使用ftrace来拦截函数入口时,参数寄存器可能已经被ftrace修改或保存到其他位置,导致PT_REGS_PARM读取到错误数据。bpf_get_func_arg则通过内核提供的参数访问API来获取,它会正确处理ftrace重写后的寄存器布局,因此更适合与kprobe共存。
从性能角度看,bpf_get_func_arg比直接寄存器访问多了一次helper调用开销。但对于大多数网络观测场景,这点纳秒级额外耗时可以忽略。从可移植性角度,bpf_get_func_arg让同一份BPF源码无需修改即可运行在x86_64、arm64、s390x等不同架构上,而PT_REGS_PARM需要为每个架构维护不同的寄存器映射文件,维护成本较高。当然,如果你追求极致性能且程序只运行在单一架构上,也可以选择PT_REGS_PARM,然后通过条件编译处理ftrace相关寄存器偏移。
使用bpf_get_func_arg时,有几个最佳实践值得注意。首先,永远检查返回值,因为参数索引错误或上下文不支持时会返回负值,忽略它会得到未初始化的变量值,导致后续逻辑不可预测。其次,对于指针类型参数,不要假设其指向的内存一定有效,要使用bpf_probe_read_kernel或bpf_probe_read_user进行安全读取。第三,当需要获取多个参数时,尽量减少helper调用次数,例如把多个bpf_get_func_arg调用合并到一个循环内,避免反复进入内核helper。最后,结合bpf_get_func_arg_ret(如果有的话)或直接读取函数返回值,可以构建完整的调用输入输出画像。
总结下来,bpf_get_func_arg为内核函数参数追踪提供了标准化的接口,尤其在网络协议栈这类参数丰富的场景中,它能显著提高开发效率并降低平台差异带来的维护成本。无论是排查连接异常还是统计流量特征,掌握这个helper都能让你的eBPF程序更加健壮和易读。
bpf_get_func_argeBPF网络内核函数修改时间:2026-10-01 05:34:56