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内核黑科技| 技术文章| 精选教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/812319.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ的介绍

为什么使用 MQ&#xff1f; 流量削峰和缓冲 如果订单系统最多能处理一万次订单&#xff0c;这个处理能力在足够应付正常时段的下单&#xff0c;但是在高峰期&#xff0c;可能会有两万次下单操作&#xff0c;订单系统只能处理一万次下单操作&#xff0c;剩下的一万次被阻塞。我们…

.NET JWT入坑

前言 JWT (JSON Web Token) 是一种安全传输信息的开放标准&#xff0c;由Header、Payload和Signature三部分组成。它主要用于身份验证、信息交换和授权。JWT可验证用户身份&#xff0c;确保访问权限&#xff0c;实现单点登录&#xff0c;并在客户端和服务器之间安全地交换信息…

SQLite 在Android安装与定制方案(十七)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLite超详细的编译时选项&#xff08;十六&#xff09; 下一篇&#xff1a;SQLite Android 绑定&#xff08;十八&#xff09; 安装 有三种方法可以将 SQLite Android 绑定添加到应用程序&#xff1a; 1、通过…

C++的stack和queue类(三):适配所有容器的反向迭代器

目录 前言 list的反向迭代器 list.h文件 ReverseIterator.h文件 test.cpp文件 前言 迭代器按性质分类&#xff1a; 单向&#xff1a;forward_list双向&#xff1a;list随机&#xff1a;vector / deque 迭代器按功能分类&#xff1a; 正向反向const list的反向迭代器…

深入理解Apache ZooKeeper与Kafka的协同工作原理

目录 引言 一、ZooKeeper基础概念 &#xff08;一&#xff09;ZooKeeper简介 &#xff08;二&#xff09;ZooKeeper数据结构 &#xff08;三&#xff09;ZooKeeper特点 &#xff08;四&#xff09;应用场景 二、ZooKeeper工作模式 &#xff08;一&#xff09;工作机制 …

请求分发场景下的鉴权问题

说明&#xff1a;记录一次对请求分发&#xff0c;无法登录系统的问题。 场景 如下&#xff0c;在此结构下&#xff0c;如何判断该用户是已登录的用户&#xff1b; 常规操作&#xff0c;用户登录后给用户发Token&#xff0c;同时将发放的Token存入到Redis中。要求用户后续请求…

架构设计-订单系统之订单系统的架构进化

1、单数据库架构 产品初期&#xff0c;技术团队的核心目标是&#xff1a;“快速实现产品需求&#xff0c;尽早对外提供服务”。 彼时的专车服务都连同一个 SQLServer 数据库&#xff0c;服务层已经按照业务领域做了一定程度的拆分。 这种架构非常简单&#xff0c;团队可以分开…

单片机方案 发声毛绒小黄鸭

随着科技的不断进步&#xff0c;智能早教已经成为了新时代儿童教育的趋势。智能早教玩具&#xff0c;一款集互动陪伴、启蒙教育、情感培养于一身的高科技产品。它不仅能陪伴孩子成长&#xff0c;还能在游戏中启迪智慧&#xff0c;是家长和孩子的理想选择。 酷得电子方案开发特…

股票价格预测 | Python使用GRU预测股票价格

文章目录 效果一览文章概述代码设计效果一览 文章概述 Python使用GRU预测股票价格 代码设计 import pandas as pd import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from numpy

Python 正则表达式模块使用

目录 1、匹配单个字符 2、匹配多个字符 3、匹配开头结尾 4、匹配分组 说明&#xff1a;在Python中需要通过正则表达式对字符串进行匹配的时候&#xff0c;可以使用re模块 表达式&#xff1a;re.match(正则表达式&#xff0c; 要匹配的字符串) 有返回值说明匹配成功&#x…

13-pyspark的共享变量用法总结

目录 前言广播变量广播变量的作用 广播变量的使用方式 累加器累加器的作用累加器的优缺点累加器的使用方式 PySpark实战笔记系列第四篇 10-用PySpark建立第一个Spark RDD(PySpark实战笔记系列第一篇)11-pyspark的RDD的变换与动作算子总结(PySpark实战笔记系列第二篇))12-pysp…

Springboot+Vue项目-基于Java+MySQL的课程作业管理系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

秀米、135、蚂蚁编辑器如何为推文添加附件

秀米、135、蚂蚁编辑器作为第三方的公众号图文排版工具&#xff0c;给从事运营和编辑工作的同学提供了更多的排版选择。不同于公众号自家的编辑器&#xff0c;这些第三方编辑器脱离了微信的直接支持&#xff0c;在很多排版操作上&#xff0c;还是有很多操作不一样的地方。 公众…

AndroidAutomotive模块介绍(一)整体介绍

前言 Android Automotive 是一个基本 Android 平台&#xff0c;可运行 IVI 系统中预安装的 Android 应用以及可选的第二方和第三方 Android 应用。 本系列文档将会系统的介绍 Android Automotive 的功能、架构、逻辑等。模块逻辑将从 应用api接口、系统服务、底层服务&#x…

软件设计师:11-结构化开发与UML

结构化开发&#xff08;3-4分&#xff09; 一、模块化 二、耦合&#xff08;背&#xff09; 三、内聚&#xff08;背&#xff09; 四、设计原则&#xff08;背&#xff09; 五、系统文档 六、数据流图 数据流的起点或终点必须有一个是加工 判断依据&#xff1a; 1、…

【MATLAB源码-第46期】基于matlab的OFDM系统多径数目对比,有无CP(循环前缀)对比,有无信道均衡对比。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 OFDM&#xff08;正交频分复用&#xff09;是一种频域上的多载波调制技术&#xff0c;经常用于高速数据通信中。以下是关于多径数目、有无CP&#xff08;循环前缀&#xff09;以及有无信道均衡在OFDM系统中对误码率的影响&am…

小程序如何通过把动态数据值传入到css文件中控制样式

场景&#xff1a;动态改变一个模块的高度 一、常用解决方法&#xff1a;行内样式绑值&#xff0c;或者动态class来传递 <viewclass"box":style"height: ${boxHeight}px">我是一个动态高度的box,我的高度是{{boxHeight}}px </view>二、高度传…

第07-1章 计算机网络相关概念

7.1 本章目标 了解网络协议的概念了解网络体系结构熟悉ISO/OSI参考模型以及每一层的功能掌握TCP/IP模型各层的主要协议及其功能熟练掌握IP地址、子网规划等相关内容 7.2 网络协议的概念 7.2.1 概念介绍 &#xff08;1&#xff09;网络协议&#xff1a;计算机网络和分布系统中…

循序渐进丨MogDB 数据库带级联从库的集群切换后如何保持原有架构?

生产数据库运行过程中可能会涉及到升级或者打补丁&#xff0c;导致各节点的角色有计划的发生改变。如果集群内角色发生改变&#xff0c;是否还能保持原有架构继续对外提供服务呢&#xff1f;我们来做一下测试。 采用22模式模拟同城两机房部署4节点 MogDB 数据库集群&#xff0c…

gitlab、jenkins安装及使用文档二

安装 jenkins IP地址操作系统服务版本192.168.75.137Rocky9.2jenkins 2.450-1.1 jdk 11.0.22 git 2.39.3192.168.75.138Rocky9.2gitlab-ce 16.10.0 结合上文 jenkins安装 前期准备&#xff1a; yum install -y epel-release yum -y install net-tools vim lrzsz wget…