对于 fentry 程序而言,在 arm64 上做 backtrace 不像 x86 上那么简单;第一步找到可用的 frame pointer 就很费劲了。

毕竟,找到 frame pointer 之后,就可以通过 frame pointer 逐级回溯,找到调用者的 IP 和 FP。

TL;DR 代码细节请看 pwru PR: Introduce two enhancements for func IP。

bpf R10 寄存器

在 x86 上,bpf R10 寄存器就是 frame pointer,可以直接用来做 backtrace。但在 arm64 上,R10 寄存器是 R25 寄存器而不是 R29 寄存器。

因此,在 fentry 里,该如何找到 frame pointer 呢?

trampoline 和 fentry 的栈分布

直接分析 trampoline 和 fentry 的栈分布,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    /* Stack layout on arm64:
     * |  r9  |
     * |  fp  | FP of tracee's caller
     * |  lr  | IP of tracee
     * |  fp  | FP of tracee
     * +------+ FP of trampoline  <-------+
     * |  ..  | padding                   |
     * |  ..  | callee saved regs         |
     * | retv | retval of tracee          |
     * | regs | regs of tracee            |
     * | nreg | number of regs            |
     * |  ip  | IP of tracee if needed    | possible range of
     * | rctx | bpf_tramp_run_ctx         | detection
     * |  lr  | IP of trampoline          |
     * |  fp  | FP of trampoline  <--------- detect it
     * +------+ FP of current prog        |
     * | regs | callee saved regs         |
     * +------+ R10 of bpf prog   <-------+
     * |  ..  |
     * +------+ SP of current prog
     */

可以看到,bpf R10 寄存器跟 fentry 的 FP 之间隔着几个被调用者保存的寄存器。

因此,如果能够知道 fentry 里使用了几个被调用者保存的寄存器,就可以找到 FP。

但是,在 commit 5d4fa9ec5643 (“bpf, arm64: Avoid blindly saving/restoring all callee-saved registers”) 之前,fentry 需要保存的寄存器数量是确定的,是 5 或者 6 个(取决于 commit 66ff4d61dc12 (“bpf, arm64: Fix tailcall hierarchy”))。而在这个 commit 之后,fentry 里保存的寄存器数量由 fentry 程序本身决定,即 fentry 使用了多少被调用者保存的寄存器,就保存多少个。

有没有简单可行的办法来找到 FP 呢?

猜测 trampline 的 FP

如上图,trampoline 的 FP 保存于 trampoline 栈帧的底部,与 bpf prog 的 FP 相隔不远。

那么,假设 fentry FP 加上一段偏移后,就是 trampoline 的 FP,该如何判断该 FP 就是 trampoline 的 FP 呢?

一个简单一些的办法是,判断读取到的 fp 在 fentry 的 FP 到 fentry 的 FP 加上 256 字节之间;如果是,那么大概率就是 trampoline 的 FP。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static __always_inline u64
detect_tramp_fp(void) {
    static const int range_of_detection = 256;
    u64 fp, r10;

    r10 = get_tracing_fp(); /* R10 of current bpf prog */
    for (int i = 6; i >= 0; i--) {
        bpf_probe_read_kernel(&fp, sizeof(fp), (void *) (r10 + i * 16));
        if (r10 < fp && fp < r10 + range_of_detection)
            return fp;
    }

    return r10;
}

不是很准确,但能用。

如果要拿到 tracee IP 呢?

如上图,在拿到 trampoline 的 FP 之后,就可以直接读取 FP + 8 字节的位置,即可拿到 tracee 的 IP;该 IP 减去 12 字节,就是 tracee 的原始 IP。

详情可以参考 commit b2ad54e1533e (“bpf, arm64: Implement bpf_arch_text_poke() for arm64”)。

总结

在 arm64 上,通过 fentry 实现 backtrace 是比较困难的,但是通过一些技巧,还是可以实现的。

详情请参考 pwru PR: Introduce two enhancements for func IP。