一、简介
在软件开发中,unwind stack(栈回溯 或 调用栈展开)是调试和异常处理中至关重要的一环,通过理解其实现原理,可以更好地理解程序的执行流程,更有效地进行调试和错误排查。
本文主要介绍 AArch64 架构下的两种最典型的栈回溯实现方式:
1.基于 frame pointer (FP) 的栈回溯
2.基于 ELF DWARF .eh_frame/.debug_frame 的 stack unwind
二、基于FP的栈回溯实现原理
2.1、AARCH64函数调用标准实现
fp(frame pointer)是一个指向当前函数栈帧的指针,在函数调用时用于定位局部变量和函数参数。在栈回溯中,FP 可以帮助我们在运行时轻松地遍历调用链。其实现原理如下:
实现原理:在函数的入口处,编译器会将 FP 设置为当前栈帧的基地址。通过 FP 和栈帧的布局,我们可以轻松地获取到上一个栈帧的信息,例如返回地址和参数等。
函数调用过程中FP(栈帧指针寄存器: x29)、LR(返回地址寄存器: x30)的布局如下图所示:
以一个简单的 C 函数汇编码为例:
ARM64 汇编 | 备注 | |
prologue | stp x29, x30, [sp, #-48]! | 函数栈大小(48 Bytes)编译期间已经确定,入口处通过调整sp预留出函数栈空间大小(sp=sp-48),将x29(fp), x30(lr)依次存入[sp-48]处 |
mov x29, sp | x29=sp, 当前sp已经指向函数栈的栈顶, 栈帧总大小为48B, 这里设置x29(fp) 指向当前函数栈栈顶 | |
str wzr, [sp, #40] str wzr, [sp, #44] ... ldr w1, [sp, #44] add x0, sp, #0x28 bl 0x1234 <_func> | 函数执行过程中, fp始终作为当前函数栈帧指针用来定位参数、局部变量位置 这里 sp 未变化,可以直接用 sp 代替 fp;有些时候可能会通过 fp 来间接寻址局部变量, 如:stur x8, [x29, #-8] | |
epilogue | ldp x29, x30, [sp], 48 | 函数返回时, 将栈顶的父函数的栈帧地址(x29), 函数返回地址(x30)出栈, 出栈后 sp=x29恢复为父函数状态 |
ret | 返回x30(lr)指向的返回地址 |
在上述汇编例子中,函数fp始终指向当前函数栈顶 (假设当前函数未调用alloca动态分配栈空间),fp(x29) 指针指向的位置依次存储的就是父(调用者)函数的fp和返回地址x30(lr),所以通过 fp/lr 展开调用栈的过程用 C 伪代码简单描述如下:
2.2、编译器优化对 fp unwind 的影响
由于现代编译器的优化技术越来越先进,有时候会使用寄存器重用等技术,甚至不保存/恢复fp,这样的函数就不再具有标准的栈帧结构,从而导致这种 fp 栈回溯方式失效。gcc/clang默认在O2+以上编译优化级别,会禁用 fp,当然如果是自己的代码,可强制使用 (-fno-omit-frame-pointer) 打开。比如,为使native so/bin文件达到最好的调试效果,可以修改 Android.bp,加入以下编译选项:
三、.eh_frame/.debug_frame解析
3.1、ELF & DWARF
DWARF(Debugging With Attributed Record Formats-使用属性化记录格式进行调试)是一种用于调试信息的标准格式,通常与编译器一起使用。它提供了一种有效的方法来生成、存储和访问程序符号和调试信息,这些信息可以在程序崩溃或其他错误情况下帮助开发人员调试分析代码。
ELF 格式 ( ELF Format Cheatsheet ):
在ELF文件中,DWARF信息存储在以下这些section中:
.debug_aranges 内存地址和编译之间的映射 .debug_frame 调用栈帧信息 .debug_info 包含DWARF调试信息项(DIE)的核心DWARF数据 .debug_line 程序行号信息 .debug_loc 位置说明 .debug_macinfo 宏描述 .debug_pubnames 全局对象和函数的查找表 .debug_pubtypes 全局类型的查找表 .debug_ranges DIE(Debug information entry)引用的地址范围 .debug_str .debug_info使用的字符串表 .debug_types 类型说明 |
这里重点关注.debug_frame section,其中存储了一系列描述栈帧布局和 unwind 信息的条目,每个条目包含了一些规则,用于指示如何从当前栈帧回溯到上一个栈帧,但该信息仍然不足以支持异常处理,因为它不支持指定原始语言,且该section在release发布时通常会被strip掉。
3.2 .eh_frame section
LSB (Linux Standard Base)标准中定义了一个.eh_frame section来解决上述问题。这个section和 .debug_frame 非常类似,但它编码更紧凑。.eh_frame section中存储着与函数入栈相关的关键数据,当函数执行入栈指令后,在该section中记录了入栈相关操作引起的寄存器变化(按特殊编码存储),根据这些编码数据,就能计算出当前函数栈基址、cpu的哪些寄存器入栈了,存储在栈中什么位置,从而可以很方便地从栈内存中还原寄存器。对于编译器而言,无论是否有 -g 选项,gcc/clang 默认都会生成 .eh_frame 和 .eh_frame_hdr section,除非显式指定 -fno-asynchronous-unwind-tables。
无论是 .debug_frame 还是 .eh_frame,这些section的存储结构都可通过 readelf 工具以更直观友好的形式解析展示,以便于理解和分析。
DWARF规范定义了 CFI (Call Frame Information) 信息,将其存储在 .debug_frame/.eh_frame section 中,CFI 包含了 CIE (Common Information Entry) 和 多个 FDE (Frame Description Entry) 结构,格式如下:
图来自 Exploiting the Hard-Working DWARF
关于CIE、FDE数据结构的详细说明:
https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html
以下以liblog.so 中的 create_android_logger 函数的 CFI 信息为例:
注:底下提到的 CFA (Canonical Frame Address 或称 Call Frame Address) 表示当前函数栈帧基地址。
通过下边 readelf 的解析结果,我们可以推导出上一个栈帧的 CFA及各寄存器信息,从而实现栈回溯。
$ readelf -Wwf liblog.so ( 解析 .eh_frame section ) | |
CIE | |
Version: 1 Augmentation: "zR" Code alignment factor: 1 Data alignment factor: -4 Return address column: 30 Augmentation data: 1b DW_CFA_def_cfa: r31 (sp) ofs 0 | Augmentation: 记录此CIE和CIE相关的FDE的增强特性,具体详见: <Augmentation String Format> Initial Instrctions: 此CIE的初始化指令 使用 r31(sp) 寄存器作为CFA,偏移量0, 即CFA = SP + 0 |
FDE指令 (函数 create_android_logger,地址范围 5020 - 5070 ) | |
00000050 000000000000002c 00000054 FDE cie=00000000 pc=0000000000005020..0000000000005070 此列下边为函数汇编码 | 00000050:这是FDE的起始地址(偏移量) 000000000000002c:FDE条目的长度 00000054:从节(section)的起始到CIE的偏移 FDE cie=00000000:指明该FDE引用的CIE的偏移量是00000000 pc=0000000000005020..0000000000005070: 表示这个FDE覆盖的程序计数器(PC)范围,从0x5020到0x5070,即对应的函数地址 |
5020 paciasp 5024 stp x29, x30, [sp, #-32]! | .DW_CFA_advance_loc: 4 to 5024:指示当前地址前进4个字节,到 0x5024,这意味着后续 FDE 指令描述自 0x5024 开始。 DW_CFA_GNU_window_save:这是一个GNU扩展操作,用于在某些架构上保存所有寄存器窗口。具体行为取决于目标架构。 |
5028 stp x20, x19, [sp, #16] 502c mov x29, sp | DW_CFA_advance_loc: 4 to 5028:指示当前地址再次前进4个字节,到 0x5028。 DW_CFA_def_cfa_offset: 32:表示当前函数栈基地址相对偏移是32,即 CFA=SP+32 |
5030 mov w19, w0 5034 mov w0, #0x1 5038 mov w1, #0x1044 503c mov w20, #0x1 5040 bl 0xe900 5044 cbz x0, 0x5060 5048 mov w8, #0x2 504c mov w9, #0x3 5050 str w20, [x0, #44] 5054 str w8, [x0, #92] 5058 strb w9, [x0, #96] 505c stp w19, w8, [x0] | DW_CFA_advance_loc: 8 to 5030:指示当前地址前进8个字节,到 0x5030。 DW_CFA_def_cfa: r29 (x29) ofs 32:定义新的CFA,使用寄存器 r29(x29) 和偏移量32, 即:CFA=x29+32 DW_CFA_offset: r19 (x19) at cfa-8:指示寄存器 r19 (x19) 保存在 CFA-8 处,依次类推其他几个寄存器 DW_CFA_offset: r20 (x20) at cfa-16,DW_CFA_offset: r30 (x30) at cfa-24,DW_CFA_offset: r29 (x29) at cfa-32 |
5060 ldp x20, x19, [sp, #16] 5064 ldp x29, x30, [sp], #32 | DW_CFA_advance_loc: 48 to 5060:指示当前地址前进48个字节,到0x5060。 DW_CFA_def_cfa: r31(sp) ofs 32:重新定义CFA: CFA = r31(sp) + 32。 |
5068 autiasp 506c ret | DW_CFA_advance_loc: 8 to 5068:指示当前地址前进8个字节,到 0x5068。 DW_CFA_def_cfa_offset: 0:改变CFA的偏移量为0,即 CFA= sp + 0 DW_CFA_advance_loc: 4 to 506c:指示当前地址前进4个字节,到0x506c 。 DW_CFA_GNU_window_save:GNU窗口保存操作 恢复以下这些寄存器 DW_CFA_restore: r19 (x19), DW_CFA_restore: r20 (x20), DW_CFA_restore: r30 (x30), DW_CFA_restore: r29 (x29) |
通过上表中的FDE指令,我们可以清楚地看到函数CFA(栈基址)、各寄存器在函数执行过程中的变化情况,结合当前栈内存信息,就可利用CFA逐级恢复各级栈帧中各寄存器的值。
readelf 还可以直观地展示函数指令执行过程中各寄存器的变化,以及如何从CFA中取出当前寄存器的值 [ u表示当前无变化, c为当前CFA, ra表示return address (x30) ]:
$ readelf -wF /system/lib64/liblog.so
四、simpleperf中基于dwarf的unwind
4.1 simpleperf 简介
simpleperf 是 Google 对传统 linux perf 工具基础上进行了简化和优化,以方便开发者在 Android 平台上进行性能分析诊断。工具本身可以抓取硬件PMU、kernel tracepoint事件、perf events 等数据,借助 simpleperf 提供的众多脚本,可以轻松将记录的 trace 数据转换成火焰图。
这里我们主要介绍 simpleperf 抓取 perf events 数据过程中,是如何解析事件所在执行线程调用栈的实现原理。当perf events 发生时,simpleperf客户端会读取出事件触发时当前线程寄存器及线程栈内存,结合目标线程(进程)的map表来展开调用栈,用到3个数据:
1.regs (当前线程所有寄存器信息)
2.进程map表 (/proc/PID/maps)
3.stack_memory (当前线程栈内存)
示例代码 ( unwind相关实现在 libunwindstack 库):
4.2 Unwind 代码流程分析
五、参考
1.https://student.cs.uwaterloo.ca/~cs452/docs/rpi4b/aapcs64.pdf
2.https://zhuanlan.zhihu.com/p/636099175
3.https://cs.dartmouth.edu/~sergey/battleaxe/hackito_2011_oakley_bratus.pdf
4.https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html
往
期
推
荐
长按关注内核工匠微信
Linux内核黑科技| 技术文章| 精选教程