跳过正文

Linux 函数调用栈展开

·
Elf Dwarf Debug
目录
elf-debug - 这篇文章属于一个选集。
§ 4: 本文

介绍 Linux 函数调用栈生成和展开(stack unwinding)机制。

函数调用栈展开(stack unwinding)指的是获得函数调用栈的过程,当前有几种实现方式:

  1. FP:frame pointer;
  2. DWARF CFI:Call Frame Information,如 .eh_frame Section 信息;
  3. ORC: 4.14 及以后版本内核专用,简化版的 DWARF CFI;
  4. LBR: 新的 Intel CPU 支持;

FP
#

frame pointer(FP) 是通过一个特定的 CPU 寄存器(rbp)来保存栈指针,由编译器在函数调用和退出时添加额外的指令来保存、恢复该寄存器中保存的 frame pointer。

https://img.opsnull.com/blog/20250202121248714-stack-frame.png

https://img.opsnull.com/blog/20250202121344707-calling.png

基本原理:函数调用时,gcc 填充的指令会将函数参数 args、函数自动变量、函数返回地址、当前栈指针(rsp 寄存器中保存)push 当前 frame stack,并将 frame stack 地址存入 rpb 寄存器。当前 frame 的函数返回地址位于 rbp+8 内存 中,通过查找二进制符号表(.symtab) 即可获得该地址的函数名称。这些细节是体系结构相关的 ABI 如 X86_64 ABI,来标准化定义的。

测试程序:

// https://medium.com/coccoc-engineering-blog/things-you-should-know-to-begin-playing-with-linux-tracing-tools-part-i-x-225aae1aaf13
#include <stdio.h>
#include <unistd.h>void func_d() {
                int msec=1;
                printf("%s","Hello world from D\n");
                usleep(10000*msec);
}
void func_c() {
                printf("%s","Hello from C\n");
                func_d();
}
void func_b() {
                printf("%s","Hello from B\n");
        func_c();
}
void func_a() {
                printf("%s","Hello from A\n");
                func_b();
}

int main() {
        func_a();
}

编译,确认 gcc 在函数开始插入了保存 FP 的指令:

# 编译(不开启优化,默认添加 FP)
root@lima-ebpf-dev:~# gcc test.c -o test

root@lima-ebpf-dev:~# objdump -S test |grep func_c
000000000000119e <func_c>:
    11de:       e8 bb ff ff ff          call   119e <func_c>

root@lima-ebpf-dev:~# objdump -S  test |grep -A5 func_c
000000000000119e <func_c>:
    119e:       f3 0f 1e fa             endbr64
    11a2:       55                      push   %rbp
    11a3:       48 89 e5                mov    %rsp,%rbp # 将函数返回地址 push 到当前栈

    11a6:       48 8d 05 6a 0e 00 00    lea    0xe6a(%rip),%rax        # 2017 <_IO_stdin_used+0x17>
    11ad:       48 89 c7                mov    %rax,%rdi
--
    11de:       e8 bb ff ff ff          call   119e <func_c>
    11e3:       90                      nop
    11e4:       5d                      pop    %rbp
    11e5:       c3                      ret

00000000000011e6 <func_a>:
root@lima-ebpf-dev:~#

打印函数栈:

  1. 使用 bpftrace 的 uprobe + ustack

    root@lima-ebpf-dev:~# bpftrace -e 'uprobe:./hello:func_d {printf("%s",ustack)}' -c ./hello
    Attaching 1 probe...
    Hello from A
    Hello from B
    Hello from C
    Hello world from D
    
            func_d+0
            func_b+33
            func_a+33
            main+18
            __libc_start_call_main+128
    
  2. 使用 perf probe + record + script

    # 保存 FP
    root@lima-ebpf-dev:~# gcc test.c -g -o test
    # 添加 uprobe 函数
    root@lima-ebpf-dev:~# perf probe -x ./hello func_d
    # -g 表示记录函数调用栈,默认使用 FP
    root@lima-ebpf-dev:~# perf record -e probe_hello:func_d -aR -g ./hello
    # 显示函数调用栈
    root@lima-ebpf-dev:~# perf script
    hello 14239 [002]  5516.587824: probe_hello:func_d: (561112f12169)
                561112f12169 func_d+0x0 (/root/hello)
                561112f121e3 func_b+0x21 (/root/hello)
                561112f12207 func_a+0x21 (/root/hello)
                561112f1221c main+0x12 (/root/hello)
                7fe3e67e1d90 __libc_start_call_main+0x80 (/usr/lib/x86_64-linux-gnu/libc.so.6)
    

GCC 优化和 FP 关闭
#

FP 是用户空间程序通用的 stack unwinding 机制,但是由于性能和开销问题,从 GCC 4.6 开始,各大发行版 (Centos7/Ubuntu 20.04)发布的软件包都默认关闭了 FP,转而使用 DWARF CFI 的 .eh_frame 来做 stack unwinding。

Golang 从 1.7 开始对 amd64 提供 FP 的支持。(internal-abi.md)

从 GCC 4.6 开始,只要开启了优化就会关闭 FP,所以如果要开启 FP,则需要:

  1. 不开启优化,不使用任何 -O 选项或指定 -O0;
  2. 或者明确指定编译参数:-fno-omit-frame-pointer--enable-frame-pointer;

参考:

  1. Writing a Linux Debugger Part 8: Stack unwinding
  2. https://cs.wellesley.edu/~cs240/s16/slides/x86-procedures.pdf
  3. https://inst.eecs.berkeley.edu/~cs161/sp15/discussions/dis06-assembly.pdf

DWARF CFI
#

FP 并不是运行程序所必须的, 一般只在调试和性能分析时才需要。而编译器了解所有函数栈的大小和分配情况, 所以在编译时可以将函数栈信息写入 ELF 的 .debug_XX Sections 中,后续调试器通过读取这些内容来判断函数调用关系,这就出现了 DWARF CFI 规范.

DWARF CFI(DWARF 3 Spec) 是一种在 EFL 文件的 .debug_frame Section 中保存 stack unwinding 信息的规范:

  • 通过编译时添加 -g 选项来生成;
  • .debug_frame 不需要加载到内存,可以位于二进制外的单独 debuginfo 文件中;
  • 使用 readelf -w 来查看 .debug_frame 的内容;

在 CFI 的基础上,又提出了 exception handler framework(.eh_frame) 规范,用来解决 C++ 等语言的异常处理时的调用栈展开问题。.eh_frame.debug_frame 的子集,也称为 unwind table,遵从 DWARF CFI Extensions 规范。

gcc 默认会生成 .eh_frame, 它也不会被 strip,会被加载到内存。

使用 gcc 编译参数 -fno-exceptions-fno-unwind-tables 参数来关闭生成 .eh_frame

使用 readelf -Wwf 来查看 .eh_frame 内容。

Kernel ORC
#

Linus 反对内核自身使用 DWARF 来调用栈展开,所以 DWARF CFI 的 .debug_frame 只在用户空间程序使用。

老版本内核坚守 FP机制,从 v4.14 开始(CentOS 8 开始),内核开始使用 ORC 机制来作为 DWARF 的简化实现。

内核编译选项:

  1. CONFIG_FRAME_POINTER:开启 FP,支持的版本: 2.6.9–2.6.39, 3.0–3.19, 4.0–4.20, 5.0–5.19, 6.0–6.4, 6.5-rc+HEAD
  2. CONFIG_UNWINDER_ORC: 开启 ORC,支持的版本:X86_64, 4.15–4.20, 5.0–5.19, 6.0–6.4, 6.5-rc+HEAD
# 17E 的情况:关闭了 FRAME_POINTER, 使用 ORC
# grep -E  'UNWINDER_ORC|FRAME_POINTER' /boot/config-4.19.91-007.ali4000.alios7.x86_64
CONFIG_SCHED_OMIT_FRAME_POINTER=y
CONFIG_UNWINDER_ORC=y
# CONFIG_UNWINDER_FRAME_POINTER is not set

支持情况
#

对于 DWARF 的解析,主要有两个库:

  1. BFD (libbfd):被 GNU binutils 使用,如 objdump、ld、as 等。
  2. libdwarflibelf: 主要是 Solaris 和 FreeBSD 系统使用;

对于 stack unwinding 的支持,主要有两个库,它们都支持 FP 和 DWARF .eh_frame、.debug_frame

  1. libundwind: 使用 DWARF debuginfo;
  2. libdw: elfutils 项目提供,性能比 libunwind 好一些。

perf/bpftrace 等工具使用的是 libdw,perf tools: Add libdw DWARF unwind support

# perf 依赖 libdw,由 elfutils-libs 包提供
# ldd /usr/bin/perf  |grep -E 'dw|unw'
        libdw.so.1 => /lib64/libdw.so.1 (0x00007f5a5cf5f000)
# rpm -qf /lib64/libdw.so.1
elfutils-libs-0.176-4.1.alios7.x86_64

gdb、perf 等均通过 libundwind 和 libdw 来屏蔽了实现差异,所以同时支持 FP 和 DWARF CFI。

但是 bpf、bpftrace 等不支持使用 DWARF 做栈展开,所以在关闭 FP 后,uprobe、usdt 等不可用,不能正确打印调用栈。

参考
#

elf-debug - 这篇文章属于一个选集。
§ 4: 本文

相关文章

objdump
·
Elf Debug Tool
objdump 是 ELF 文件查看和反汇编工具。
readelf
·
Elf Debug Tool
readelf 是显示 ELF 二进制文件(可执行程序或动态库等)中各 Section 内容的重要工具。 显示符号表 Sections,如 .dnysym 和 .symtab 中的符号名称和地址; 显示 DWARF 格式的 Sections,如各种 .debug_xx,.eh_frame 等; 显示查找 debuginfo 文件所需的 .gnu_debuglink 和 .note.gnu.build-id ;
ELF 符号表和 DWARF 调试符号表
·
Elf Debuginfo Dwarf
介绍 Linux ELF 二进制文件的符号表和调试符号表(.debug_XX)生成、管理机制。
向 ELF 二进制添加元数据信息
·
Elf
介绍向 ELF 文件中添加自定义数据的方法。