如何通过libbpf访问用户态Nginx函数的参数?
问题:使用libbpf插桩Nginx用户态函数时,如何正确访问并打印函数参数?
我尝试使用libbpf对用户态Nginx函数进行插桩,已成功挂载uprobe并能从探针中打印pid、tid等信息,但在解析函数参数数据时遇到了极大问题。我可以用bpftrace实现该功能,但无法用libbpf完成。
现有BPF代码(nginx.bpf.c)
#include "ngx_http.h" #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_core_read.h> char LICENSE[] SEC("license") = "Dual BSD/GPL"; SEC("uprobe//usr/sbin/nginx:ngx_http_finalize_request") int handle_ngx_http_finalize_request(struct ngx_http_request_s* r, ngx_int_t rc) { u_char *s_ptr; u_char str[128]; int err; err = bpf_probe_read_user(&s_ptr, sizeof(s_ptr), &r->request_line.data); if (!s_ptr || err < 0) { bpf_printk("Error %d\n", err); return -2; } bpf_probe_read_user_str(str, sizeof(str), &s_ptr); bpf_printk("String: %s\n", str); return 0; }
错误现象
- 使用
bpf_probe_read_user时返回错误码-14 - 使用
bpf_core_read时,BPF验证器拒绝代码,错误日志如下:
❯ sudo ./nginx libbpf: loading object 'nginx_bpf' from buffer libbpf: elf: section(3) uprobe//usr/sbin/nginx:ngx_http_finalize_request, size 280, link 0, flags 6, type=1 libbpf: sec 'uprobe//usr/sbin/nginx:ngx_http_finalize_request': found program 'handle_ngx_http_finalize_request' at insn offset 0 (0 bytes), code size 35 insns (280 bytes) libbpf: elf: section(4) .reluprobe//usr/sbin/nginx:ngx_http_finalize_request, size 32, link 12, flags 40, type=9 libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1 libbpf: license of nginx_bpf is Dual BSD/GPL libbpf: elf: section(6) .rodata, size 22, link 0, flags 2, type=1 libbpf: elf: section(7) .BTF, size 12720, link 0, flags 0, type=1 libbpf: elf: section(9) .BTF.ext, size 252, link 0, flags 0, type=1 libbpf: elf: section(12) .symtab, size 240, link 1, flags 0, type=2 libbpf: looking for externs among 10 symbols... libbpf: collected 0 externs total libbpf: map 'nginx_bp.rodata' (global data): at sec_idx 6, offset 0, flags 80. libbpf: map 0 is "nginx_bp.rodata" libbpf: sec '.reluprobe//usr/sbin/nginx:ngx_http_finalize_request': collecting relocation for section(3) 'uprobe//usr/sbin/nginx:ngx_http_finalize_request' libbpf: sec '.reluprobe//usr/sbin/nginx:ngx_http_finalize_request': relo #0: insn #13 against '.rodata' libbpf: prog 'handle_ngx_http_finalize_request': found data map 0 (nginx_bp.rodata, sec 6, off 0) for insn 13 libbpf: sec '.reluprobe//usr/sbin/nginx:ngx_http_finalize_request': relo #1: insn #28 against '.rodata' libbpf: prog 'handle_ngx_http_finalize_request': found data map 0 (nginx_bp.rodata, sec 6, off 0) for insn 28 libbpf: loading kernel BTF '/sys/kernel/btf/vmlinux': 0 libbpf: map 'nginx_bp.rodata': created successfully, fd=4 libbpf: sec 'uprobe//usr/sbin/nginx:ngx_http_finalize_request': found 1 CO-RE relocations libbpf: prog 'handle_ngx_http_finalize_request': relo #0: <byte_off> [2] typedef ngx_http_request_t.request_line.data (0:21:1 @ offset 992) libbpf: prog 'handle_ngx_http_finalize_request': relo #0: no matching targets found libbpf: prog 'handle_ngx_http_finalize_request': relo #0: substituting insn #1 w/ invalid insn libbpf: prog 'handle_ngx_http_finalize_request': BPF program load failed: Invalid argument libbpf: prog 'handle_ngx_http_finalize_request': -- BEGIN PROG LOAD LOG -- R1 type=ctx expected=fp ; int handle_ngx_http_finalize_request(ngx_http_request_t* r, ngx_int_t rc) 0: (bf) r3 = r1 1: <invalid CO-RE relocation> failed to resolve CO-RE relocation <byte_off> [2] typedef ngx_http_request_t.request_line.data (0:21:1 @ offset 992) processed 2 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0 -- END PROG LOAD LOG -- libbpf: prog 'handle_ngx_http_finalize_request': failed to load: -22 libbpf: failed to load object 'nginx_bpf' libbpf: failed to load BPF skeleton 'nginx_bpf': -22 Failed to open and load BPF skeleton
可正常工作的bpftrace代码(nginx.bt)
uprobe:/usr/sbin/nginx:ngx_http_finalize_request { $req = (struct ngx_http_request_s*)arg0; printf("Request Line: %s\n", str($req->request_line.data)); }
用户态加载代码
#include <stdio.h> #include <unistd.h> #include <sys/resource.h> #include <bpf/libbpf.h> #include "nginx.skel.h" static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } int main(int argc, char **argv) { struct nginx_bpf *skel; int err; libbpf_set_strict_mode(LIBBPF_STRICT_ALL); libbpf_set_print(libbpf_print_fn); skel = nginx_bpf__open_and_load(); if (!skel) { fprintf(stderr, "Failed to open and load BPF skeleton\n"); return 1; } err = nginx_bpf__attach(skel); if (err) { fprintf(stderr, "Failed to auto-attach BPF skeleton: %d\n", err); goto cleanup; } printf("Successfully started!\n"); for (;;) { sleep(1); } cleanup: nginx_bpf__destroy(skel); return -err; }
解决方案
错误原因分析
bpf_probe_read_user返回-14(EFAULT):直接取&r->request_line.data属于在内核态非法解引用用户态指针,必须通过bpf_probe_read_user系列函数安全读取用户态内存。- CO-RE重定位失败:CO-RE用于适配内核结构体版本差异,Nginx是用户态程序,内核无其BTF信息,因此libbpf无法完成CO-RE重定位。
修正后的BPF代码
#include "ngx_http.h" #include <linux/bpf.h> #include <bpf/bpf_helpers.h> char LICENSE[] SEC("license") = "Dual BSD/GPL"; // 手动定义ngx_str_t结构体(若头文件未正确包含时使用) struct ngx_str_t { u_char *data; size_t len; }; SEC("uprobe//usr/sbin/nginx:ngx_http_finalize_request") int handle_ngx_http_finalize_request(struct ngx_http_request_s* r, ngx_int_t rc) { struct ngx_str_t req_line; u_char str[128]; int err; size_t copy_len; // 第一阶段:读取request_line结构体(用户态内存) err = bpf_probe_read_user(&req_line, sizeof(req_line), &r->request_line); if (err < 0) { bpf_printk("Failed to read request_line: %d\n", err); return -1; } // 校验数据有效性 if (!req_line.data || req_line.len == 0) { bpf_printk("Invalid request line data\n"); return -1; } // 计算安全读取长度,避免缓冲区溢出 copy_len = req_line.len < (sizeof(str) - 1) ? req_line.len : (sizeof(str) - 1); // 第二阶段:读取request_line.data指向的字符串内容 err = bpf_probe_read_user(str, copy_len, req_line.data); if (err < 0) { bpf_printk("Failed to read request string: %d\n", err); return -1; } // 手动添加字符串终止符 str[copy_len] = '\0'; bpf_printk("Request Line: %s\n", str); return 0; }
关键修改说明
- 分阶段读取用户态内存:先读取
ngx_http_request_s中的request_line结构体,再读取结构体中data指针指向的字符串,避免直接解引用用户态指针。 - 缓冲区溢出防护:根据
ngx_str_t的len字段控制读取长度,防止超出BPF程序中数组的大小。 - 移除CO-RE依赖:由于用户态程序无内核BTF支持,直接使用
bpf_probe_read_user完成内存读取,无需CO-RE重定位。
编译运行注意事项
- 确保编译时正确包含Nginx头文件,或手动定义所需结构体(如
ngx_str_t)。 - 保持常规libbpf编译流程,无需启用CO-RE相关编译选项。
- 运行用户态加载程序时需使用
sudo获取足够权限。
内容的提问来源于stack exchange,提问作者nela
相关产品推荐
相关产品推荐

