常见的Native Crash类型
类型 | 子类 | 原因 |
---|---|---|
SIGSEGV | SEGV_MAPERR | 地址不在 /proc/self/maps 映射中 |
SEGV_ACCERR | 没有访问权限 | |
SEGV_MTESERR | MTE特有类型 | |
SIGABRT | 程序主动退出,常见调用函数abort(),raise()等 | |
SIGILL | ILL_ILLOPC | 非法操作码(opcode) |
ILL_ILLOPN | 非法操作数(operand) | |
ILL_ILLADR | 非法寻址 | |
ILL_ILLTRP | 非法trap,如_builtintrap()主动崩溃 | |
ILL_PRVOPC | 非法特权操作码(privileged opcode) | |
ILL_PRVREG | 非法特权寄存器(privileged register) | |
ILL_COPROC | 协处理器错误 | |
ILL_BADSTK | 内部堆栈错误 | |
SIGBUS | BUS_ADRALN | 访问地址未对齐 |
BUS_ADRERR | 访问不存在的物理地址,常见访问的文件截断 | |
BUS_OBJERR | 特定对象的硬件错误 | |
SIGFPE | FPE_INTDIV | 整数除以0 |
FPE_INTOVF | 整数溢出 | |
FPE_FLTDIV | 浮点数除以0 | |
FPE_FLTOVF | 浮点数上溢(overflow) | |
FPE_FLTUND | 浮点数下溢(underflow) | |
FPE_FLTRES | 浮点数结果不精确 | |
FPE_FLTINV | 无效的浮点运算 | |
FPE_FLTSUB | 越界 |
Android日志
当程序发生了 Native Crash 错误,Android 的日志会输出到 log crash buffer 上,因此我们通过adb logcat -b crash 抓取到相应的错误报告,而日志本身能提供的信息是有限的,仅仅是错误堆栈,与当前线程的寄存器信息。
1 | yaml复制代码--------- beginning of crash |
当只有日志堆栈不能进行更详细的分析时,我们还需要程序的部分内存信息以及寄存器信息,而Android 的错误机制会相应的会生成一份 tombstone 文件保存到 /data/tombstones/tombstone_xx ,对于没有 Root 权限的机器则可以通过 adb bugreport 抓取出 tombstone 文件。
Tombstone
tombstone 文件保存的信息有错误程序的体系结构,通俗的说 arm、arm64 等,发生时间点,程序名,错误类型,错误程序的进程号、线程号,错误现场寄存器,堆栈和部分寄存器地址附近的内存信息,程序内存映射表 /proc/self/maps ,FD 信息以及发生错误时该程序输出的日志。
1 | yaml复制代码ABI: 'arm64' 【 arm64的程序 】 |
错误类型
1 | ini复制代码signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7c02d886f0 |
寄存器信息
1 | 复制代码x0 79748c5e568e2ddc x1 0000007ca13c3618 x2 0000000000000000 x3 0000007ca1291000 |
堆栈信息
1 | makefile复制代码backtrace: |
1 | ruby复制代码而线程栈位于 mmap segmemt上,我们可以在 /proc/self/maps 上找到该线程栈的地址空间范围。 |
内存信息
tombstone 会记录当前有效地址的寄存器附近内存信息,大小为0x100,这个可以修改文件
system/core/debuggerd/libdebuggerd/utility.cpp 中的宏定义 MEMORY_BYTES_TO_DUMP 参数
像这种一条堆栈的情况下,栈的内存信息配合下边的映射表可以帮助到我们对栈进行恢复。
1 | erlang复制代码memory near x1 (/system/lib64/libstagefright.so): |
内存映射表
1 | bash复制代码memory map (1146 entries): |
FD信息
1 | javascript复制代码open files: |
Coredump
前面 tombstone 文件内容,可以了解到它的信息很有限,当我们需要更多的内存信息时,它无法满足我们,这时 coredump 就尤为重要了,它可以按我们配置抓取相应的内存信息,关于 core 的介绍,见:
man7.org/linux/man-p…
AOSP方法
1 | bash复制代码# build/envsetup.sh |
常用方法
给 system_server 配置 coredump 参数,由于目标进程的 coredump 生成的目录受 selinux 权限限制,因此这种方法配置抓 coredump 的方法要注意目标进程对哪些目录文件有读写的 selinux 权限,再配置相应的目录。
1 | bash复制代码adb wait-for-device |
注意:确定问题与selinux权限无关,可以通过adb shell setenforce 0关闭selinux权限
给 com.android.settings 配置抓取 coredump 的参数,由于前面的配置中 /data/cores 目录恢复 selinux 权限后如下:
drwxrwxrwx 2 root root u:object_r:system_data_file:s0 3452 2022-07-04 15:08 cores
我们知道 app 一定有权限对自身的 /data/data/$PACKAGE/ 目录下的文件具有读写权限,于是可以配置成如下参数:
1 | kotlin复制代码adb wait-for-device |
1 | shell复制代码当我们在机器上验证对这个app进行kill -11模拟时 |
参数说明
coredump_filter 进程默认值是0x23,只抓取:私有匿名/共享匿名/私有大尺寸页,需要抓全部内存信息则 adb shell ‘echo 0x27 > /proc/$PID/coredump_filter’ 即可。
节点 | 参数 |
---|---|
/proc/$PID/coredump_filter | bit0: 私有匿名 |
bit1: 共享匿名 | |
bit2: 有底层文件的私有映射 | |
bit3: 有底层文件共享映射 | |
bit4: ELF头 | |
bit5: 私有大尺寸页 | |
bit6: 共享大尺寸页 |
core_pattern 控制生成 core 的文件名,以及输出的 core 的位置。例如:
adb shell ‘echo /data/cores/core.%p > /proc/sys/kernel/core_pattern’
节点 | 参数 |
---|---|
/proc/sys/kernel/core_pattern | %p: 添加pid |
%u: 添加当前uid | |
%g: 添加当前gid | |
%s: 添加导致产生core的信号 | |
%t: 添加core文件生成时的unix时间 | |
%h: 添加主机名 | |
%e: 添加命令名 | |
%E: 可执行文件的路径名,用斜杠(’/’)替换为感叹号(’!’)。 |
当程序调用了 seteuid()/setegid() 改变进程的有效用户或组,则在默认情况下系统不会为这些进程生成 core,此时你可能需要调整 suid_dumpable 参数进入调试模式或安全模式下进行。
节点 | 参数 |
---|---|
/proc/sys/fs/suid_dumpable | 0:默认模式 |
1:调试模式 | |
2:安全模式 |
文件格式
core 文件也是ELF文件的一种,因此它的主体格式组成部分与 ELF 文件相同,以案例讲解的 core 文件为例,它主要组成部分为 /proc/self/maps 下的VMA以及各个线程寄存器。其中寄存器信息存放在PT_NOTE,各VMA存放在 PT_LOAD,当被过滤掉的 VMA,它只有 Program Header 描述,没有对应的 segment,也就是对应的段的 p_filesz = 0x0。
离线调试
注:MTK 平台的 MINIDUMP 也是 coredump 的一种,它所保存的内存信息有限,core 的分析可以使用 GDB、lldb 等调试工具,如何使用这些调试工具,这里就不一一介绍。
1 | bash复制代码$ ~/work/debug/gdb_arm64/gdb-12.1/output/bin/aarch64-linux-gdb |
命令 | 用途 |
---|---|
(gdb) info sharedlibrary | 显示所有共享库的地址范围 |
(gdb) info registers | 显示当前线程的当前帧寄存器信息 |
(gdb) info locals | 显示当前帧的局部变量 |
(gdb) info thread | 显示有哪些线程 |
(gdb) thread 2 | 切换到2号线程上 |
(gdb) bt | 显示当前线程的堆栈 |
(gdb) thread apply all [command]例如打印所有线程堆栈(gdb) thread apply all bt | 让所有线程做同样的命令 |
(gdb) frame | 显示当前帧信息 |
(gdb) frame 3 | 切换到#3帧 |
(gdb) print 或 (gdb) p | 打印变量 |
(gdb) ptype ‘android::AHandler’ | 查看某个class或struct的数据结构 |
(gdb) ptype /o ‘android::AHandler’ | 查看数据类型占多少字节 |
(gdb) set print pretty on | 格式化输出 |
(gdb) set log on | 保存gdb输出的结果 |
(gdb) x /gx 0x7c02d886f0 | 读取地址0x7c02d886f0内存内容,其中输出格式如下:o(octal), x(hex), d(decimal),u(unsigned decimal), t(binary),f(float), a(address), i(instruction),c(char), s(string),z(hex, zero padded on the left). |
(gdb) disassemble 0x0000007c95de6708或(gdb) disassemble ‘android::AMessage::setTarget’ | 显示函数汇编信息 |
内存检测机制
ASAN
在 Android 11 之后的 AOSP master 中,弃用了 arm64 上的平台开发 ASAN,改为使用 HWASAN,AddressSanitizer (ASAN) 是一种基于编译器的快速检测工具,用于检测内存错误。
类型 | 意义 |
---|---|
Stack and heap buffer overflow/underflow | 堆栈和堆缓冲区上溢/下溢 |
Heap use after free | 使用已释放的内存 |
Stack use outside scope | 超出栈范围 |
Double free/wild free | 多次释放内存/错误释放 |
HWASAN
HWASan 仅适用于 Android 10 及更高版本,且只能用于 AArch64 硬件,具备 ASAN 同样的检测能力,在 Linux-4.14 版本以上支持 tagged-pointers才能使用。
1 | makefile复制代码编译Android版本时带入环境变量如下: |
MTE
最新 Android S 上引入的 ARM Memory Tagging Extension (MTE),MTE 的原理和 HWASan 类似,最大的区别在于 HWASan 需要重新编译,在所有内存访问前插桩相应的检测函数来实现,而 MTE 则在读写指令内部完成检测,完全由硬件支持。
更多内容转至掘金大佬-芦半山:
野指针的危害
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称 Wild pointer (野指针),如果这个野指针所指向的内存被分配给其它指针,而这个野指针仍在使用,程序将难以预测。
1 | arduino复制代码#include <stdio.h> |
以上程序会怎么输出,由于 B 与 A 的数据结构大小一致,运行在同一个线程,极大可能会分配刚释放的指针地址,因此这个程序最后指针 b 与指针 a_bak 是同一个。
1 | ini复制代码# ./data/Tester64 |
像以上这个结果,那程序如下改写,那么可怕的事情就会出现,下面这个程序会报什么错。
1 | arduino复制代码#include <stdio.h> |
上面这个程序在27行处,bad 会将 B 的虚函数表破坏,导致28和29行在虚函数表上寻找 foo 函数与析构函数地址时发段错误。
1 | yaml复制代码Timestamp: 2022-07-06 14:47:50.925654058+0800 |
程序会出错还好,如果程序不会出错,仍继续运行,那么这个程序将变得很可怕,因为你不知道程序将会怎么运行,像下面这个程序,刻意改写控制程序往其它方向运行。
1 | arduino复制代码#include <stdio.h> |
1 | ini复制代码# ./data/Tester64 |
数组越界的危害
比起前面的野指针,数组越界大多数情况都能被 HWASAN 等内存检测发现,当然前面的野指针情况也是能被检测发现的,数组越界往往会出现某个对象的内存前半部分被污染,而后半部分数据正常的情况。
1 | arduino复制代码#include <stdio.h> |
因为 b 的数据大小与对象 a 的数据大小相同,因此在这个程序刚启动分配指针地址时,基本会连在一块,因此 b[2] 越界行为将对象 a 的内容破坏,往往程序运行了很久,内存碎片化增加,就不知道 b[2] 越界操作会破坏哪个对象的内存。
1 | makefile复制代码# ./data/Tester64 |
机器码翻译
本文案例中的 tombstone 文件 PC 跑飞,没有落在 text 段地址上,这里换一份 tombstone 方便说明,我们可以通过编译的方法生成对应的 ELF 文件,即可用 objdump 得到对应的汇编。
1 | bash复制代码signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x40 |
编写汇编文件 code.S,内容如下:
1 | 复制代码.inst 0xf90023f7 |
1 | yaml复制代码编译:aarch64-linux-android-as code.S -o code.o |
汇编翻译
有些时候我们可能需要手动修改 ELF 文件的机器码达到一些调试的目的,这时候我们就需要知道汇编对应的机器码,去查手册也比较麻烦,这时我们直接编写汇编文件即可。
编写汇编文件 code_asm.S,内容如下:
1 | python复制代码str x30, [sp, #-16]! |
1 | python复制代码编译:aarch64-linux-android-as code_asm.S -o code_asm.o |
案例讲解
错误日志
1 | yaml复制代码Timestamp: 2022-06-07 01:53:32.033409857+0800 |
分析
直接原因
1 | less复制代码0000007c'02c90000-0000007c'02d8bfff rw- 0 fc000 [anon:stack_and_tls:11981] |
栈回溯
从 tombstone 中的 x29 和 SP 寄存器附近的内存中进行栈回溯,由于大小受限,往往无法回溯到栈底,无法完全证明程序就是经过这几个函数。
1 | arduino复制代码memory near x29 ([anon:stack_and_tls:11981]): |
最后这份 tombstone 只能通过 FP 推导出这两个函数,是否正确待取证,这也体现了 tombstone 的局限性,此时我们希望能完整的推导,并取证栈是完整性,需要使用到 coredump 文件进行。
FP | LR | 函数名 | |
---|---|---|---|
0x0000007c02d886f0 | 0x0000007c02d88750 | 0x0000007ca133f8e0 | android::MediaCodec::queueInputBuffer |
0x0000007c02d88750 | 0x0000007c02d88830 | 0x0000007ca796ee7c | android::NuPlayer::Decoder::onInputBufferFetched |
1 | scss复制代码MTK 的 aee 会将保存一份 minidump ,将 minidump 装载到gdb上回溯。 |
当前FP | Caller FP | Caller LR | Caller 函数名 |
---|---|---|---|
0x7c02d886f0 | 0x0000007c02d88750 | 0x0000007ca133f8e0 | android::MediaCodec::queueInputBuffer |
0x7c02d88750 | 0x0000007c02d88830 | 0x0000007ca796ee7c | android::NuPlayer::Decoder::onInputBufferFetched |
0x7c02d88830 | 0x0000007c02d888a0 | 0x0000007ca796c984 | android::NuPlayer::Decoder::doRequestBuffers |
0x7c02d888a0 | 0x0000007c02d88920 | 0x0000007ca7963d48 | android::NuPlayer::DecoderBase::onRequestInputBuffers |
0x7c02d88920 | 0x0000007c02d88980 | 0x0000007ca79717bc | android::NuPlayer::Decoder::handleAnInputBuffer |
0x7c02d88980 | 0x0000007c02d88a20 | 0x0000007ca7969328 | android::NuPlayer::Decoder::onMessageReceived |
0x7c02d88a20 | 0x0000007c02d88a70 | 0x0000007c95de2874 | android::AHandler::deliverMessage |
0x7c02d88a70 | 0x0000007c02d88ad0 | 0x0000007c95de8608 | android::AMessage::deliver |
0x7c02d88ad0 | 0x0000007c02d88b30 | 0x0000007c95de3bbc | android::ALooper::loop |
0x7c02d88b30 | 0x0000007c02d88ba0 | 0x0000007ca105907c | android::Thread::_threadLoop |
0x7c02d88ba0 | 0x0000007c02d88c10 | 0x0000007ca105886c | thread_data_t::trampoline |
0x7c02d88c10 | 0x0000007c02d88c50 | 0x0000007caaf3be74 | __pthread_start |
0x7c02d88c50 | 0x0000007c02d88c80 | 0x0000007caaedb830 | __start_thread |
这个线程栈内存能被回溯到 __start_thread 函数,基本可以确定这个栈是可信的,并且最后的 FP 地址存放了 Caller FP 与 Caller LR,并且从汇编的指令流程上是吻合的,可以确定最后的 FP 应该是某个函数的栈顶,记这个函数为 A 函数。
A 函数是谁?
1 | makefile复制代码(gdb) x /12gx 0x7c02d886d0 |
1 | ruby复制代码(gdb) disassemble 0x0000007ca133f8e0-0x20,+0x30 |
A 函数会是 AMessage 的构造方法吗?
1 | less复制代码(gdb) disassemble 0x7ca13bb390 |
这个函数的栈大小为0x30,并且 FP=SP,如果A函数是 AMessage 的构造函数,那么在该函数未退栈时 FP=SP 不成立。
继续往前看栈上内存信息:
1 | makefile复制代码0x7c02d88600: 0xb400007c13c24bd0 0x6f2ab3b40fa2f8ef |
1 | makefile复制代码0x7c02d886d0: 0xb400007c13c246d0 0x0000000001909705 /SP |
1 | ruby复制代码(gdb) disassemble 0x0000007c95de6754-0x20,+0x40 |
A函数会是android::AMessage::setTarget?
1 | ruby复制代码(gdb) disassemble 0x0000007c95de6708,+0x30 |
从汇编看函数栈正是0x60与A函数的栈相等。
回去再看看 AMessage::AMessage 的汇编,可以发现它是退栈后跳转到 android::AMessage::setTarget。
1 | less复制代码0x0000007c95de6700 <+88>: ldp x29, x30, [sp], #48 |
1 | 复制代码x0 79748c5e568e2ddc x1 0000007ca13c3618 x2 0000000000000000 x3 0000007ca1291000 |
再回看 setTarget 汇编刚好 x0=0x79748c5e568e2ddc,
1 | ruby复制代码0x0000007c95de6740 <android::AMessage::setTarget+56>: mov x0, #0x2ddc // #11740 |
x16 = 0x0000007c95dfdb70与x17= 0x0000007c9844f118 也满足tombstone信息。
1 | sql复制代码(gdb) disassemble 0x7c95dfb370 |
1 | less复制代码(gdb) disassemble 0x0000007c9844f118 |
计算汇编走向
根据 tombstone 最后的栈地址 SP=0x0000007c02d886d0 说明函数 __cfi_slowpath(uint64_t, void ) 已经退栈。
1 | makefile复制代码0x7c02d88620: 0x0000007c02d89000 0x6f2ab3b40fa2f8ef |
其中恢复其调用栈如下:
1 | arduino复制代码backtrace: |
__cfi_slowpath函数有两处退栈相关。
1 | ini复制代码第一部分: |
1 | less复制代码第二部分: |
如果走的是第一部分退栈,那么 x16 和 x17 将与 tombstone 不符合,因此可以确定走的是第二部分。
而这里会跳转至 x3,可以看下当前 tombstone 里 x3=0x0000007ca1291000 是否为一个有效的函数地址。
1 | less复制代码(gdb) disassemble 0x0000007ca1291000,+0x40 |
这个函数非常的大,但有规律,很显然先将 x8与x0 作比较,并且数值与 tombstone 中
x0=0x79748c5e568e2ddc 类似,那么我们可以搜索部分关键字找到相应的 case。
1 | less复制代码0x0000007ca12920b4 <+4276>: mov x8, #0x2ddc // #11740 |
1 | csharp复制代码0x0000007ca129576c <+18284>: adrp x8, 0x7ca13c2000 <_ZTVN7android17BufferChannelBaseE+24> |
tombstone 中 x9=0x0000007ca126fed7
1 | csharp复制代码0x0000007ca1295788 <+18312>: adrp x9, 0x7ca126f000 <_ZL5__txt+844> |
tombstone 中x8=0x0000000000000080
1 | less复制代码0x0000007ca12963a4 <+21412>: ldrb w8, [x9, x8] |
1 | ini复制代码0x0000007ca1296644 <+22084>: ldr x30, [sp], #16 |
最后在此处退栈回到 android::AMessage::setTarget ,然后 PC=LR=0x0000007c02d886f0 发生段错误。
结论
不难发现一个细节 0x7c02d886c0 地址上存储的内容是有问题的。
1 | makefile复制代码__cfi_slowpath(uint64_t, void*) |
1 | ini复制代码0x0000007c9844f118 < __cfi_slowpath +0>: stp x29, x30, [sp, #-16]! |
错误模拟
1 | yaml复制代码# ps -ef | grep "medias" |
Coredump寄存器恢复栈
通过readelf读取MINIDUMP文件,保存寄存器上下文在ELF文件的第一个节中。
1 | python复制代码Program Headers: |
用hexedit工具打开MINIDUMP文件找到寄存器地址。
1 | erlang复制代码0000FB20 00 00 00 00 00 00 00 00 05 00 00 00 88 01 00 00 ................ |
上图绿色为LR和PC,将它们修改为正确的返回地址0x0000007c95de6754
1 | erlang复制代码0000FC90 7C 00 00 00 F0 86 D8 02 7C 00 00 00 54 67 DE 95 |.......|...Tg.. |
重新将恢复后的MINIDUMP装载到gdb上即可恢复callstack。
1 | arduino复制代码(gdb) bt |
微信公众号链接:mp.weixin.qq.com/s/YDhqP_ZkS…
本文转载自: 掘金