内核返回错误debug神器 -- retsnoop

文摘   2024-10-07 11:54   陕西  

在libbpf/bpftool的github issue列表上经常可以看到maintainer推荐retsnoop工具去定位eBPF工具执行失败的原因,例如:

retsnoop本身也是出自Andrii之手,libbpf 70%的代码贡献都来自该大佬,如此光环下足见该工具多么硬核。

使用 retsnoop 的一个最典型的场景是执行eBPF工具或者其他应用app时遇到来自内核的报错,但不清楚是哪部分内核代码出错了,当然我们也可以用bpftrace等工具挨个函数打点去追踪内核代码,但这样做效率显然很低。而retsnoop的做法是我们可以通过通配符定义的方式,对相关函数做全量trace,只要遇到函数返回错误,就会把这部分代码的栈信息显示出来,从而方便问题的快速定位。

快速入门

这部分内容直接取至retsnoop项目的使用文档,工具有很多控制参数,这里只选几个常用的介绍,更详细的介绍可以直接参考项目文档。

retsnoop 支持三种不同且互补的模式:

stack trace mode

默认的堆栈跟踪模式简洁地指向满足用户条件(例如,从系统调用返回的错误)的最深函数调用堆栈。它显示了一系列函数调用、堆栈每一层的相应源代码位置,并发出延迟和返回结果:

$ sudo ./retsnoop -e '*sys_bpf' -a ':kernel/bpf/*.c'
Receiving data...
20:19:36.372607 -> 20:19:36.372682 TID/PID 8346/8346 (simfail/simfail):

                    entry_SYSCALL_64_after_hwframe+0x63  (arch/x86/entry/entry_64.S:120:0)
                    do_syscall_64+0x35                   (arch/x86/entry/common.c:80:7)
                    . do_syscall_x64                     (arch/x86/entry/common.c:50:12)
    73us [-ENOMEM]  __x64_sys_bpf+0x1a                   (kernel/bpf/syscall.c:5067:1)
    70us [-ENOMEM]  __sys_bpf+0x38b                      (kernel/bpf/syscall.c:4947:9)
                    . map_create                         (kernel/bpf/syscall.c:1106:8)
                    . find_and_alloc_map                 (kernel/bpf/syscall.c:132:5)
!   50us [-ENOMEM]  array_map_alloc
!*   2us [NULL]     bpf_map_alloc_percpu
^C
Detaching... DONE in 251 ms.

func trace mode

函数调用跟踪模式( -T )还提供了给定函数集的控制流的详细跟踪,允许更全面地理解内核行为:

FUNCTION CALL TRACE                               RESULT                 DURATION
-----------------------------------------------   --------------------  ---------
→ bpf_prog_load
    → bpf_prog_alloc
        ↔ bpf_prog_alloc_no_stats                 [0xffffc9000031e000]    5.539us
    ← bpf_prog_alloc                              [0xffffc9000031e000]   10.265us
    [...]
    → bpf_prog_kallsyms_add
        ↔ bpf_ksym_add                            [void]                  2.046us
    ← bpf_prog_kallsyms_add                       [void]                  6.104us
← bpf_prog_load                                   [5]                   374.697us

lbr mode

lbr 模式(最后分支记录)允许用户“回顾”并更深入地了解各个函数的内部结构,跟踪“不可见”的内联函数,并将问题一直精确到各个 C 语句。当跟踪内核的不熟悉部分而不知道要查找什么时,此模式特别有用。它支持迭代发现过程,而无需太多了解在哪里查找以及哪些功能是相关的。但此模式依赖intel cpu硬件特性,同时也只有在5.16+内核上才能使用。

$ sudo ./retsnoop -e '*sys_bpf' -a 'array_map_alloc_check' --lbr=any
Receiving data...
20:29:17.844718 -> 20:29:17.844749 TID/PID 2385333/2385333 (simfail/simfail):
...
[#22] ftrace_trampoline+0x14c                                    ->  array_map_alloc_check+0x5   (kernel/bpf/arraymap.c:53:20)
[#21] array_map_alloc_check+0x13  (kernel/bpf/arraymap.c:54:18)  ->  array_map_alloc_check+0x75  (kernel/bpf/arraymap.c:54:18)
      . bpf_map_attr_numa_node    (include/linux/bpf.h:1735:19)      . bpf_map_attr_numa_node    (include/linux/bpf.h:1735:19)
[#20] array_map_alloc_check+0x7a  (kernel/bpf/arraymap.c:54:18)  ->  array_map_alloc_check+0x18  (kernel/bpf/arraymap.c:57:5)
      . bpf_map_attr_numa_node    (include/linux/bpf.h:1735:19)
[#19] array_map_alloc_check+0x1d  (kernel/bpf/arraymap.c:57:5)   ->  array_map_alloc_check+0x6f  (kernel/bpf/arraymap.c:62:10)
[#18] array_map_alloc_check+0x74  (kernel/bpf/arraymap.c:79:1)   ->  __kretprobe_trampoline+0x0
...

另外也支持进程号过滤:

  -p, --pid=PID              Only trace given PID. Can be specified multiple
                             times
  -P, --no-pid=PID           Skip tracing given PID. Can be specified multiple
                             times

如果只关注内核函数执行时长,可以通过-L参数:

  -L, --longer=MS            Only emit stacks that took at least a given amount
                             of milliseconds

源码解析

resnoop用了大量的高版本eBPF特性,仅hook内核函数的方式就支持fentry、kprobe、kprobe_multi三种。除此之外,也用到了ringbuffer、全局变量、lbr特性等,如果想完整使用retsnoop的功能,建议使用5.16+内核。下面简单介绍下各*.bpf.c文件的作用。

相关文件

  • calib_feat.bpf.c

探测当前系统是否满足eBPF的高版本特性,例如是否支持ringbuffer、kprobe_mult、lbr采栈i等,择机使用当前系统上所能支持的eBPF特性。

  • mass_attacher.c

所有hook函数的入口,handle_func_entry/handle_func_exit 作为通过入口hook所有函数

  • retsnoop.bpf.c

整个retsnoop核心功能实现在此文件中,其实现的大致思路如下图所示。

整体实现思路

  1. 用户传入通配符匹配参数,通过通配符匹配如 “sys_bpf”,将所有跟sys_bpf相关的目标函数都会做hook处理;
  2. 每个目标函数开始执行和执行返回都会做hook处理,如下图步骤1,2,在函数开始执行时记录时间、seq_id(唯一标识)等信息,在函数返回时根据其返回结果判断函数是否正常(是否为0、是否非NULL等),如果不正常就记录其栈信息,如下图步骤3
  3. 最后当记录的栈深度为0时,既如下图func1返回时,上报记录的追踪信息,如下图步骤4

使用案例

下面介绍几个之前借助retsnoop工具解决的问题。

1.percpu map value值32K大小限制

工具开发中,percpu map是我们经常用到的map类型,但其value 值有32K的限制,一旦超过该大小,当我们跑工具时会有右图的报错,单看“Cannot allocate memory”提示内存不足,但查整机还有很多free内存。直到使用retsnoop才看到是申请percpu 内存时返回了NULL,根因就是内核中PCPU_MIN_UNIT_SIZE (32K)的限制。

2.aarch64 原子指令未支持,造成load失败

在5.15的aarch64内核上执行eBPF工具,报了“failed to load:-524”错误,以往当verifier失败时会有相关的内核日志,而此问题却只有一个错误码,难以定位错误原因。因此使用retsnoop抓取bpf load相关的函数,可以看到bpf_prog_load时内核报了”ENOTSUPPORT”错误,凭借此函数的错误提示定位到是代码中使用了sync_fetch_and_and原子子令,而aarch64下5.15的内核对该eBPF指令还未支持。

这么好用的工具只能在高版本使用确实心有不甘,所以感兴趣的朋友也可以尝试将其移植到低版本(仅支持kprobe和perf buffer的内核(4.19等))中。


参考

https://github.com/anakryiko/retsnoop


Linux内核之旅
Linux内核之旅
 最新文章