上使用 FP Unwind 学习
文章中涉及的 Android 源码以 Android 10 源码为例;讨论的是 64 位 APK 的情况,32 位 FP Unwind 支持的不好(因为使用 FP Unwind ARM/Thumb 指令混合的方法栈不可靠,另外 AArch32 的 ABI 也没有规定 FP 的行为),不在本文讨论范围内
名词
文章中有些名词大家平时可能接触的少,这里先统一介绍下
名词 | 含义 |
---|---|
AArch32 | Arm 32-bit Architecture |
AArch64 | Arm 64-bit Architecture |
Unwind | 栈展开,就是栈回溯的意思 |
Dwarf | Dwarf 是一种编译器、调试器用于源码调试的文件格式;实际上 Android so 文件的很多 section 就是 Dwarf 格式的 |
Caller | 调用程序,也就是父函数 |
Callee | 被调用程序,也就是子函数 |
APCS64 | Procedure Call Standard for AArch64,AArch64 程序调用标准,它描述了调用程序与被调用程序间的协议 |
gnu_debugdata | 它是 so 中的一个名为.gnu_debugdata 的 section,包含一些调试信息,采用 LZMA 压缩 |
背景
我们在开发 Native 内存泄漏监控方案时,遇到一个问题,如何在内存分配时高效的获取堆栈?快手是一个短视频应用,有大量 Native 内存高频分配场景「每秒钟高达近万次内存分配」;我们调研了现有的一些获取堆栈的方式,比如使用 Android 系统的 libunwindstack 库 API、LLVM 提供的 llvm_libunwind 库、libunwind 等;它们都能准确的获取堆栈,但性能「大都是基于 Dwarf 的 unwind,有 IO 及解码 Dwarf 帧指令的开销」都不能满足我们的要求;为了高效的获取堆栈我们最终采用了 FP Unwind,FP Unwind 最大的优势是获取堆栈速度快「只需要简单的回溯下栈内存,速度是其他 unwind 方式的 10X ~ 100X」,但是也有一些缺点:依赖编译器编译参数-fno-omit-frame-pointer
等,庆幸的是 Android NDK 工具链编译器默认添加了-fno-omit-frame-pointer
。但是在接入 FP Unwind 后,我们又发现了另一问题:它在 Android 上 unwind 到虚拟机的帧时往往会中断,why? 本文将解开这个谜团!
在分析问题前,这里先简单介绍下 FP Unwind 的背景知识: AArch64 通用寄存器以及 AArch64 栈帧布局。
FP(Frame Pointer) Unwind
AArch64 通用寄存器及其在 APCS64 中的使用
Register | Special | Role in the procedure call standard |
---|---|---|
SP | The Stack Pointer | |
X30 | LR | The Link Register |
X29 | FP | The Frame Pointer |
X19...X28 | Callee-saved registers | |
X18 | The Platform Register, if needed; otherwise a temporary register. | |
X17 | The second intra-procedure-call temporary register (can be used by call veneers and PLT code); at other times may be used as a temporary register. | |
X16 | The first intra-procedure-call scratch register (can be used by call veneers and PLT code); at other times may be used as a temporary register. | |
X9...X15 | Temporary registers | |
X8 | Indirect result location register | |
X0...X7 | Parameter/result registers |
- 有两个寄存器需要我们特别关注
- X30,别名 LR 用于存储当前执行指令(PC)的下一条指令,用于子函数执行完成后恢复 PC(= LR) 继续执行
- X29,别名 FP,编译器使能
-fomit-frame-pointer
时 FP 的使用没有规定,本文主要讨论使能-fno-omit-frame-pointer
的情况,见下文
AArch64 栈帧布局与 Unwind
- 栈帧布局
- 需要编译器添加编译参数
-fno-omit-frame-pointer
,否则 FP 可能不会被保存在栈中;Android NDK clang 默认添加 - 当编译器添加编译参数
-fno-omit-frame-pointer
时,ARM Compiler armclang Reference Guide 有如下规定- FP 总是指向 FrameRecord 的最低地址
- FrameRecord 有两部分(FP + LR)组成
- FP 在 FrameRecord 的低地址
- LR 在 FrameRecord 的高地址
- 需要编译器添加编译参数
- 基于 FP 进行 Stack Unwind 的过程
- 获取当前的 FP 寄存器值,判断 FP 是否合法,合法继续;否则中断
- 通过 FP 值获取 Caller 栈中保存的 FP 及 LR,LR 保存起来用于计算 backtrace
- 回到第一步
FP Unwind 在 Android 中的“坑”
“坑”(FP Unwind 在 Android 上 unwind 到虚拟机的帧时往往中断)
-------------- backtrace -------------------------------
libkwaiplayer.so KP_EGL_create (kpsdl_misc.h:53)
libkwaiplayer.so SDL_VoutAndroid_CreateForANativeWindow (kpsdl_vout_android_nativewindow.c:298)
libkwaiplayer.so kpmp_android_create (kpplayer_android.c:39)
libkwaiplayer.so KpMediaPlayer_native_setup (kpplayer_jni_core.c:1186)
libkwaiplayer.so KpMediaPlayer_reset (kpplayer_jni_core.c:536)
libart.so
-------------- backtrace -------------------------------
- 在上面的 backtrace 中可以看到 Unwind 到 libart.so 时就终止了,jni 方法的 Caller 通常应该为 Android 虚拟机的跳板/Stub函数,也就是应该在 libart.so 中,但应该能够继续 Unwind 才对,为什么会中断?
- 这里猜测应该是虚拟机内部实现的跳板/Stub 函数帧布局不满足 FP Unwind,真的是这样吗?下面我们通过一个 NDK 的例子来探讨
一个简单的 NDK 的例子
......
bool __attribute__((oninline)) TestFunc(JNIEnv* env) {
std::string s = "Hellooooooooooooooo ";
return s.size() > random();
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_kwai_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
if (TestFunc(env)) {
hello.append(" TestFunc");
}
return env->NewStringUTF(hello.c_str());
}
- 这个例子很简单就是从 Java Native 方法中调用了
TestFunc
方法,我们把断点设在TestFunc
的第一行看下此时的 backtrace 如下图,也就是在虚拟机跳板之后应该还有很多解释执行(或者机器码执行)等栈才对
正常的 backtrace 如下:
TestFunc(_JNIEnv*) native-lib.cpp:151
::Java_com_kwai_myapplication_MainActivity_stringFromJNI(JNIEnv *, jobject) native-lib.cpp:160
art_quick_generic_jni_trampoline 0x00000074b4b50354 <---- Android 虚拟机的跳板函数
art_quick_invoke_stub 0x00000074b4b47338
art::ArtMethod::Invoke() 0x00000074b4b561a8
art::interpreter::ArtInterpreterToCompiledCodeBridge() 0x00000074b4cf24a0
bool art::interpreter::DoCall()
......
为什么 FP Unwind 会出现中断
-
先看下
TestFunc
开始执行时的寄存器信息(lldb) re r General Purpose Registers: x0 = 0x00000074b50f3980 x1 = 0x0000007440a00900 x2 = 0x000000000000000e x3 = 0x75002b2b43206d6f x4 = 0x0000007440a0090e x5 = 0x0000007fd58ffebf x6 = 0x7266206f6c6c6548 x7 = 0x2b2b43206d6f7266 ...... x28 = 0x0000007fd58fff00 fp = 0x0000007fd58ffe60 lr = 0x0000007440a097e4 libnative-lib.so`::Java_com_kwai_myapplication_MainActivity_stringFromJNI(JNIEnv *, jobject) + 56 at native-lib.cpp:160 sp = 0x0000007fd58ffe00
-
有了 FP 和 SP 寄存器之后,我们 dump 下栈内容
(lldb) x --force -s8 -fx -c512 0x0000007fd58ffe00 0x7fd58ffe00: 0x0000000000000000 0x00000074b5010800 0x7fd58ffe10: 0x0000007fd5900120 0x000000000000000e 0x7fd58ffe20: 0x0000007440a00900 0x0000007fd58ffeb0 0x7fd58ffe30: 0x000000753aae8020 0x00000074b50f3980 0x7fd58ffe40: 0x0000007fd58ffeb0 0xc1da7c077a586644 0x7fd58ffe50: 0x0000000000000007 0xc1da7c077a586644 0x7fd58ffe60: 0x0000007fd58ffed0(FP’) 0x0000007440a097e4(LR’) <--------- Java_com_kwai_myapplication_MainActivity_stringFromJNI 0x7fd58ffe70: 0x0000007fd5900020 0x00000074b4cb0988 0x7fd58ffe80: 0xc1da7c077a586644 0x0000007fd5900020 0x7fd58ffe90: 0x000000753aae8020 0x00000074b50b88a0 0x7fd58ffea0: 0x0000007fd58fff04 0x00000074b50f3980 0x7fd58ffeb0: 0x66206f6c6c65481c 0x002b2b43206d6f72 0x7fd58ffec0: 0x0000007fd5900154 0xc1da7c077a586644 0x7fd58ffed0: 0x0000007fd58fff00(FP”) 0x00000074b4b50354(LR”) <--------- art_quick_generic_jni_trampoline 0x7fd58ffee0: 0x00000074b50b8600 0x00000007d5900128 0x7fd58ffef0: 0x00000075354df1d0 0x0000007fd5904038 0x7fd58fff00: 0x130c9d1000000001(错误的 FP'") 0x0000000012c85440(错误的 LR'") <----- 这里会出现中断因为 FP'" 保存的已经不再是 Caller 栈的 FP 了 0x7fd58fff10: 0x0000000000000000 0x00494c020045474c 0x7fd58fff20: 0x52415242494c0d00 0x0000040000000001 0x7fd58fff30: 0x0000000000100000 0x4010040140100401 0x7fd58fff40: 0x0000001000040401 0x8020080280200802 0x7fd58fff50: 0x00000000130c9d10 0x0000000000000000 0x7fd58fff60: 0x00000074b5010800 0x0000007fd5900460 0x7fd58fff70: 0x000000744522150e 0x0000000000000040 0x7fd58fff80: 0x636d604b1fff3a76 0x0000000000000000 0x7fd58fff90: 0x00000074b5010800 0x0000007fd5900170 0x7fd58fffa0: 0x000000744522150e 0x0000000000000004 0x7fd58fffb0: 0x000000753aae8020 0x00000074b50108b0 0x7fd58fffc0: 0x0000000000000001 0x0000000000000002 0x7fd58fffd0: 0x0000007fd58ffff0(FP'") 0x00000074b4b47338(LR'") <--------- art_quick_invoke_stub ==> 如果我们把 FP" 中的 0x7fd58fff00 调整为 0x7fd58fffd0 就可继续 FP Unwind。 ==> 而 0x7fd58fffd0 - 0x7fd58fff00 = 208,后面我们会看到为什么调整 208 个字节就可以继续了 ......
-
二进制内容比较枯燥我们用图来表示下栈的情况
-
从上面的栈帧内容可以发现 0x7fd58ffed0 中保存的 FP 已经是错误的地址,从而导致 FP Unwind 中断;也就是
Java_com_kwai_myapplication_MainActivity_stringFromJNI
帧中保存的art_quick_generic_jni_trampoline
方法的 FP 是错误的「这里的错误指的是 FP 指向的地址并没有保存 Caller 的 FP 寄存器」,下面我们看下art_quick_generic_jni_trampoline
方法的栈帧布局究竟是什么样的?
art_quick_generic_jni_trampoline 是执行 JNI 方法时虚拟机需要执行的跳板函数,负责准备参数,解决 JNI 方法的地址等
代码路径:https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:art/runtime/arch/arm64/quick_entrypoints_arm64.S
// 宏定义,该宏的作用是保存 d0 ~ d7、x1 ~ x7、x20 ~ x30 寄存器的内容到栈中
.macro SETUP_SAVE_REFS_AND_ARGS_FRAME_INTERNAL
INCREASE_FRAME 224
// Ugly compile-time check, but we only have the preprocessor.
#if (FRAME_SIZE_SAVE_REFS_AND_ARGS != 224)
#error "FRAME_SIZE_SAVE_REFS_AND_ARGS(ARM64) size not as expected."
#endif
// Stack alignment filler [sp, #8].
// FP args.
stp d0, d1, [sp, #16]
stp d2, d3, [sp, #32]
stp d4, d5, [sp, #48]
stp d6, d7, [sp, #64]
// Core args.
SAVE_TWO_REGS x1, x2, 80
SAVE_TWO_REGS x3, x4, 96
SAVE_TWO_REGS x5, x6, 112
// x7, Callee-saves.
// Note: We could avoid saving X20 in the case of Baker read
// barriers, as it is overwritten by REFRESH_MARKING_REGISTER
// later; but it's not worth handling this special case.
SAVE_TWO_REGS x7, x20, 128
SAVE_TWO_REGS x21, x22, 144
SAVE_TWO_REGS x23, x24, 160
SAVE_TWO_REGS x25, x26, 176
SAVE_TWO_REGS x27, x28, 192
// x29(callee-save) and LR.
SAVE_TWO_REGS x29, xLR, 208 <-------- sp 距离 x29 208 个字节
.endm
// 该宏的作用是保存 x0 到栈中,并且设置 sp 到 Thread::Current()->managed_stack->tagged_top_quick_frame_ 成员中
.macro SETUP_SAVE_REFS_AND_ARGS_FRAME_WITH_METHOD_IN_X0
SETUP_SAVE_REFS_AND_ARGS_FRAME_INTERNAL
str x0, [sp, #0] // Store ArtMethod* to bottom of stack.
// Place sp in Thread::Current()->top_quick_frame.
mov xIP0, sp
str xIP0, [xSELF, # THREAD_TOP_QUICK_FRAME_OFFSET]
.endm
art_quick_generic_jni_trampoline 方法的实现是平台相关的汇编实现
ENTRY art_quick_generic_jni_trampoline
SETUP_SAVE_REFS_AND_ARGS_FRAME_WITH_METHOD_IN_X0 // 定义见上面的宏
// Save SP , so we can have static CFI info.
mov x28, sp
.cfi_def_cfa_register x28
// This looks the same, but is different: this will be updated to point to the bottom
// of the frame when the handle scope is inserted.
// 这里 xFP(x29) 寄存器设置为 sp
mov xFP, sp
// 我们看下此时栈帧布局
// * #-------------------#
// * | |
// * | caller method... |
// * #-------------------#
// * | Return X30/LR |
// * | X29/FP | callee save <----------- 如果 xFP 指向这里,FP Unwind 才能正确进行
// * | X28 | callee save
// * | X27 | callee save
// * | X26 | callee save
// * | X25 | callee save
// * | X24 | callee save
// * | X23 | callee save
// * | X22 | callee save
// * | X21 | callee save
// * | X20 | callee save
// * | X19 | callee save
// * | X7 | arg7
// * | X6 | arg6
// * | X5 | arg5
// * | X4 | arg4
// * | X3 | arg3
// * | X2 | arg2
// * | X1 | arg1
// * | D7 | float arg 8
// * | D6 | float arg 7
// * | D5 | float arg 6
// * | D4 | float arg 5
// * | D3 | float arg 4
// * | D2 | float arg 3
// * | D1 | float arg 2
// * | D0 | float arg 1
// * | Method* | <- X0 <--------------- xFP 指向这里,距离上面保存 x29 的地址 208 个字节
// * #-------------------#
......
blr xIP0 // native call. 调用Native方法或者先查找再调用;注意 JNI 方法中保存的 x29 也就是该方法中的 xFP
// 不管是直接调用 Native 方法或者调用查找跳板, Native 方法的保存的 FP 是相同的,因为跳板会 Clear 自己的栈
......
END art_quick_generic_jni_trampoline
art_quick_generic_jni_trampoline
的实现是平台相关的汇编代码,从汇编上看在调用 Native 方法时,它的 FP 确实没有指向保存 Caller FP 的位置,而是偏差了 208 字节,这导致 FP Unwind 的中断,至此我们就找到了 FP Unwind 在 Android 上发生中断的原因。
小结
- Android 虚拟机内部的跳板/Stub 函数的实现导致 FP Unwind 在 Android 运行时发生中断,这可能并不是 Android 的 BUG;编译器参数不同 FP 的意义可能不同,这也提醒我们在 Android 上使用 FP Unwind 时要小心,可能会有想不到的问题
思考及后续文章
一些思考
- FP Unwind 在 Android 上有“坑”我们还能用吗?
- 如果我们只关心 Native 层的栈,在性能要求高的场景下 FP Unwind 依然是最佳选择
- Android 系统进行 Stack Unwind 时,它是如何做的?
- 基于 Dwarf 的 Stack Unwind
- 基于 gnu_debugdata 的 Unwind
- Android 上 Java 和 Native Stack 是同一个栈,Android 系统 Unwind 时如何识别出 Java Frame 并计算出方法名行号?
- 解释执行时如何并识别出 Java Frame?
- 机器码执行时如何识别出 Java Frame ?
- 执行 JIT Code Cache 中的代码时又会怎样?
后续文章
- 对 Android 系统的 Stack Unwind 做一个全面的介绍;上面的问题都会从文章中找到答案
参考
- Procedure Call Standard for the Arm® 64-bit Architecture (AArch64)
- ARM Compiler armclang Reference Guide
- GDB to LLDB command map
- AOSP 源码
原文地址:https://www.cnblogs.com/liunx1109/p/14685608.html
- 深度学习(deep learning)发展史
- 遗传算法简述
- Spark详解03Job 物理执行图Job 物理执行图
- 干货|Kotlin入门第一课:从对比Java开始
- Spark详解04Shuffle 过程Shuffle 过程
- Spark详解02Job 逻辑执行图Job 逻辑执行图
- Spark详解01概览|Spark部署|执行原理概览Job 例子
- Spark详解05架构Architecture架构
- SQL Server常用命令(平时不用别忘了)
- Spark详解06容错机制Cache 和 Checkpoint Cache 和 Checkpoint
- SQL Server 学习笔记
- Collaborative Filtering(协同过滤)算法详解
- 【Hadoop】三句话告诉你 mapreduce 中MAP进程的数量怎么控制?
- Spark系列课程-00xxSpark RDD持久化
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- python第二十八课——编码小常识
- Linux系统——KVM虚拟机安装与管理
- python第二十九课——文件读写(读取读取中文字符)
- python第二十九课——文件读写(readline()和readlines()的使用)
- linux系统运维企业常见面试题集合(一)
- python第二十九课——文件读写(写数据的操作)
- python第二十九课——文件读写(复制文件)
- python第三十课--异常(异常处理定义格式和常见类型)
- python第三十课--异常(finally讲解)
- python第三十课--异常(else讲解)
- Linux系统——shell脚本编程基础介绍
- python第三十课--异常(raise关键字)
- python第三十课--异常(异常对象传递过程)
- python第三十课--异常(with as操作)
- linux系统运维企业常见面试题集合(二)