Android中基于DWARF的stack unwind实现原理

一、简介

在软件开发中,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)的布局如下图所示:

2f73181a915392ea004ad5240e0002b4.png

以一个简单的 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 伪代码简单描述如下:

1fe2ce68b665b10f3ccc30554ae51d5c.png       

2.2、编译器优化对 fp unwind 的影响

由于现代编译器的优化技术越来越先进,有时候会使用寄存器重用等技术,甚至不保存/恢复fp,这样的函数就不再具有标准的栈帧结构,从而导致这种 fp 栈回溯方式失效。gcc/clang默认在O2+以上编译优化级别,会禁用 fp,当然如果是自己的代码,可强制使用 (-fno-omit-frame-pointer) 打开。比如,为使native so/bin文件达到最好的调试效果,可以修改 Android.bp,加入以下编译选项:      

592fcafc03aac79dbee0ed26874a7460.png

三、.eh_frame/.debug_frame解析             

3.1、ELF & DWARF

DWARF(Debugging With Attributed Record Formats-使用属性化记录格式进行调试)是一种用于调试信息的标准格式,通常与编译器一起使用。它提供了一种有效的方法来生成、存储和访问程序符号和调试信息,这些信息可以在程序崩溃或其他错误情况下帮助开发人员调试分析代码。       

ELF 格式  ( ELF Format Cheatsheet ):    

2255be8ca0cfbdf8c62a0aada315598d.png

在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) 结构,格式如下:

a8d1178e4764a15d3a202bb63bace240.png

图来自 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    

3b3194ef2d19d70a930af0457e93b5fd.png         

四、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 库):    

c1a1e648f20f52f3c39d5a15cf4366c3.png

4.2 Unwind 代码流程分析

631f4651530e63076ad2e8c27267763b.png        

五、参考

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

Android分区挂载原理介绍(上)

Android分区挂载原理介绍(下)

IO调度器详解

c2f81d488ca73b6db191e2d56f864df0.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OPPO内核工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值