Tombstone原理分析

本文主要围绕三个问题对tombstone进行分析和介绍,debuggerd是如何监控进程并生成tombstone的?tombstone文件中的信息都是什么,是怎么获取的?tombstone文件应该怎么分析?



一、Tombstone简介

当一个native程序开始执行时,系统会注册一些连接到debuggerd的signal handlers。针对进程出现的不同的异常状态,Linux kernel会发送相应的signal给异常进程,debuggerd捕获这些signal,做出相应处理的同时(一般来说是退出异常进程),在/data/tombstones目录下生成一个tombstone。Tombstone文件以tombstone_XX形式命名,该文件个数上限可以进行设置,当超过上限时则每次覆盖时间最老的文件。Tombstone记录了崩溃的进程的基本信息,堆栈调用信息,内存信息等等。

二、Tombstone生成过程

为了更好地分析tombstone触发的过程,我们可以先了解一下Android中一个进程是如何跑起来的。

一个APP一般是存储在UFS,EMMC之类的存储设备上,当用户去点击图标的时候,操作系统会把这个APP从Flash里面加载到主存当中,通过类似fork()的命令去创建对应的进程。进程创建完毕之后通过类似exec的命令去加载APP的内容,然后由/system/bin/linker程序加载APP所用到的一些共享库。最终跳转到APP程序的入口处执行。

Debuggerd守护进程的初始化同样是在linker中进行的,下面我们首先分析linker部分中是如何去初始化debuggerd的。

1.Debuggerd初始化

Debuggerd是在linker中进行初始化的,我们从linker的起始位置开始进行分析,在/bionic/linker/arch/arm64/begin.S中可以看到,这里调用了__linker_init函数,对linker进行初始化,__linker_init应该要返回可执行程序的开始地址,之后跳转过去执行。

在__linker_init()方法中调用了__linker_init_post_relocation(),__linker_init_post_relocation()初始化linker的全局变量,之后调用linker_main()函数获取可执行程序的开始地址,然后跳转到开始地址继续执行。

可以看到linker_main()函数中对系统环境进行了确认,对系统属性进行了初始化设置,之后调用linker_debuggerd_init()初始化debuggerd。

linker_debuggerd_init()定义了一个callbacks结构体,之后把这个callbacks作为参数传递给debuggerd_init(),继续进行debuggerd的初始化。

在debuggerd_init()中首先把callbacks赋给g_callbacks,之后调用mmap为线程的栈分配空间,然后调用mprotect函数,设置stack对应的内存区的保护属性为可读可写,之后把栈的起始点定义在页末尾并对齐。最后调用debuggerd_register_handlers()去注册一些异常信号,而异常信号的处理函数是debuggerd_signal_handler()。

在debuggerd_register_handlers()函数中注册异常信号,可以通过这个函数看到都有哪些异常信号会被debuggerd捕获。



2.Debuggerd处理异常

当native进程出现了问题,则通过linux内核判断会发生信号,最终信号在被debuggerd捕获之后由debuggerd_signal_handler()函数处理。debuggerd_signal_handler()使用互斥锁pthread_mutex_lock()来保护线程,防止同一时间多个线程处理信号产生冲突。首先调用log_signal_summary()来输出一些信息,调用log_signal_summary()的目的是防止后面动作出错,最终无法定位到是哪个进程出现了错误,所以先在这里打印一些关键信息。可以在logcat里找到对应的信息。在这一部分中打印如下信息:信号的num、信号code、Fault addr出错时的地址、tid:对应的线程id和pid:对应的进程id。

之后debuggerd_signal_handler()调用debuggerd_dispatch_pseudothread(),通过clone系统调用clone出一个伪线程,去处理dispatch信号,原来的线程原地等待子线程的开始和结束。

在伪线程中通过fork去创建子线程,新创建的子线程中通过execle系统调用去执行crash_dump64程序,父进程等待crash_dump64进程退出。

在crash_dump64进程中,再fork出一个新进程,父进程通过fork_exit_read去等待子进程,子进程继续执行crash_dump的任务。

在crash_dump中,通过/proc/PID/cmdline获取进程的名字,通过/proc/PID/fd/获取此进程打开了多少文件,每个文件都有一个文件描述符。

循环遍历这个进程中的所有线程,对进程中的每一个线程进行ptrace操作,对目标线程读取其crashinfo。Crashinfo读取完毕后detach当前的线程。

之后在crash_dump()中调用tombstoned_connect()通过socket连接到tombstone进程。根据signal的si_val的值不同做出不同的判断,为0时dump tombstone,为1时只dump backtrace。

最终tombstone通过engrave_tombstone()函数生成,engrave_tombstone函数的第二个参数,unwinder,是输出 backtrace等信息的关键函数。

通过unwinder的初始化函数我们可以看到,unwinder初始化过程中获取了当前进程的内存和memory map。这些信息会在后面帮助debuggerd生成tombstone文件。

对tombstone的生成过程做一个小结,当Native进程发生了异常,操作系统会去异常向量表的地址去处理异常,然后发送信号。在debuggred_init()注册的信号处理函数就会捕获信号并处理,创建伪线程去启动crash_dump进程,crash_dump则会获取当前进程中各个线程的crash信息。tombstoned进程是开机就启动的,开机时注册好了socket等待连接。当crash_dump()去连接tombstoned进程的时候,根据传递的dump_type类型会返回一个/data/tombstones/下文件描述符。

crash_dump进程后续通过engrave_tombstone函数将所有的线程的详细信息写入到tombstone文件中,至此就在/data/tombstones下生成了此次对应的tombstone_XX文件。



三、Tombstone文件实例分析

上文中我们提到,tombstone通过engrave_tombstone()函数生成,在这一节中,我们将结合实例和代码来介绍tombstone文件中都包含哪些内容,这些内容都是如何获取和输出出来的。实例中的tombstone文件由Google提供的development.apk生成。

engrave_tombstone()中首先调用的是dump_header_info()函数打印tombstone头信息。

可以从tombstone实例中看到dump_header_info()函数输出的是一些和编译,CPU架构相关的信息。

调用dump_time_stamp()打印native crash发生的时间。

Tombstone实例如下,这里打印的时间是本机时间,因为实验机没有联网设置时间所以显示时间是2020-01-01 12:23:27+0800。

然后engrave_tombstone()要打印的是线程相关信息,首先调用unwinder的GetProcessMemory()方法,获取进程对应的内存信息,之后engrave_tombstone()把unwinder和获取到的内存信息作为参数传递给dump_thread()打印线程相关的信息。

dump_thread()中调用的第一个函数是dump_thread_info(),它的职责是打印出错的线程所属的进程pid,线程tid,线程名,进程名和出错线程对应的apk的uid。Tombstone实例如下:

 

然后dump_thread()调用dump_signal_info()函数打印引发这次tombstone的信号信息,dump_thread()在调用这个函数时把thread_info和进程对应的内存传递给了dump_signal_info()。在dump_signal_info函数中首先调用signal_has_si_addr()对信号的signo进行判断,可以在下面的函数中看到,只有在信号不是manually sent并且是某些特定信号的情况下,才会有对应的si_addr。

Tombstone实例如下,可以看到虽然SIGSEGV信号在上面列出的信号列表中,但是因为si_code是SI_USER,所以还是没有对应的信号地址。

之后调用dump_probable_cause(),通过分析signal_info打印可能的原因信息,主要依靠分析signal number和fault address来得出可能的结论。如果没有分析出可能的原因就不会打印出任何信息,在本文使用的这个tombstone的例子中就没有任何信息输出。

之后调用dump_registers()输出出错时寄存器的值,thread_info里面记录了错误发生时的寄存器信息,dump_registers将他们按顺序输出到tombstone文件里。Tombstone实例如下:

调用log_backtrace()打印backtrace信息,先在unwinder中调用unwind()方法解析内存中的信息,然后log_backtrace函数中调用了unwinder的FormatFrame()函数把获取的堆栈信息输出到tombstone文件中。调用unwinder的unwind()方法会导致保存的寄存器的值发生变化,所以在调用这个方法之前先对寄存器的值做一个备份。

unwind()函数中,首先根据pc寄存器的值找到函数unwind的段内存地址

然后根据unwind段中信息找到指令相关的编码数据,elf表等

接着根据入栈地址,分析函数上一级的栈底保存的sp和lr寄存器的值;最后更新寄存器信息。

在tombstone实例中,依次输出pc寄存器的值,对应的文件名,对应的函数名和offset。最近的frame中的pc寄存器的值可以直接从thread_info当中获取,后面的pc寄存器的值在unwind的过程中更新;后面的文件名可以根据memory map和pc寄存器中的地址得出;后面的function name和function offset是通过memory map和pc寄存器中的地址找到dex文件,解析dex文件可以得到对应的函数名和offset。

接下来调用dump_memroy_and_code()函数,这个函数只在主线程中调用,它循环遍历每一个寄存器,打印寄存器附近的memory信息。dump_memory_and_code()中主要是获取寄存器名和值,还有map_info的name属性编辑到label中,真正进行dump的部分在dump_memory()函数里。dump_memory()函数中从寄存器记录的地址addr的位置读取了256个byte的内存到data中,并输出到Tombstone文件,每16个byte一行,输出了16行,输出的第一段是对应的起始地址,第二第三段是内存的值,每一个byte用两个16进制数表示,第四段是对应的ascii码。

随后调用dump_all_maps()打印map信息,dump_threads()向dump_all_maps()传入unwinder和信号的对应地址,和dump_memroy_and_code()一样,dump_all_maps()只在主线程中被调用。Map信息在unwinder初始化的时候获取,map信息记录了进程对应的内存映射,包括开始地址,长度,访问权限,文件描述符,offset等信息。

从Tombstone实例中可以看到输出的信息依次为内存的起始和结束位置,对内存的操作权限,内存的offset,内存的长度,map的名字和buildID

至此dump_threads()函数运行完成,回到engrave_tombstone()函数中。随后engrave_tombstone()函数调用dump_logs()打印systemlog和mainlog的信息。Tombstone实例如下:



四、Tombstone文件分析方法

在tombstone中我们可以看到进程的pid和线程的tid,如果pid和tid相等,那么可以判断crash发生在这个进程的主线程中,后面的name属性表明了crash进程的名称和它在文件系统中的位置。在tombstone中我们还可以看到程序是因为什么信号导致了Crash以及出现错误的地址。根据这些信息可以初步判断crash的类型。下面列出一些信号的常见发送方和触发条件。

除了上面介绍的信息之外,我们主要要分析的是backtrace信息,我们可以通过addr2line工具去分析backtrace,根据backtrace提供的内存地址和符号库文件去找到代码出错的具体位置。我们可以通过addr2line工具找到是哪个文件下的哪个函数的哪一行发生了crash,在backtrace中越靠上的越接近最后被调用的函数。值得一提的是只有带symbol的so文件才能被定位到。本文中使用的实例中的一些库是在apex文件夹下的,这样的so文件里面是没有symbol的,所以即便找到了对应的so文件,也无法找到对应的代码段。

在可执行程序中都包含有调试信息(编译的时候需要加-g选项),addr2line根据程序源程序的行号和编译后的机器代码之间的对应关系Line Number Table去找找对应的行号。

另一个可以帮助我们解读tombstone文件的是工具是objdump,它可以反汇编指定so文件来得到对应的源代码和汇编代码,objdump工具和addr2line在同一个路径下。

长按关注

内核工匠微信

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

<think>嗯,用户问的是关于system/bin/tombstoned报错的问题。首先,我需要确定tombstoned是什么。我记得在Android系统中,tombstoned是一个处理native崩溃的服务,比如C/C++层面的崩溃。它会生成tombstone文件,记录崩溃信息。 接下来,用户可能遇到了相关的错误,比如应用崩溃,日志中出现这个报错。我需要解释tombstoned的作用,然后分析可能的原因。常见的原因包括native代码问题、内存错误、兼容性问题或者系统故障。 然后,需要给出排查步骤。用户可能需要检查日志,查看tombstone文件,分析堆栈跟踪,检查内存使用,更新应用或系统,联系开发者等。还要提醒他们如果对技术不熟悉,可能需要寻求专业帮助。 另外,要确保回答结构清晰,分点说明,用中文,并且注意LaTeX格式的正确使用,不过这个问题可能不太需要数学公式,所以可能用不上。但如果有变量或路径,可能需要用行内格式,比如`/data/tombstones`这样的路径,不过根据系统指令,可能不需要特别处理,保持自然的中文解释即可。 还要注意用户可能不太了解底层系统服务,需要用易懂的语言解释,避免过多技术术语,同时提供实际的解决步骤。确保建议的安全操作,比如备份数据,避免root设备除非必要。可能还需要提到如果系统被修改过,比如刷机,可能会影响稳定性,这也是一个常见原因。 最后,总结一下处理流程,让用户有明确的步骤可以遵循,从检查日志到联系开发者,逐步排查问题。同时提醒用户如果问题持续,可能需要专业支持,尤其是硬件相关的问题。</think>关于 `system/bin/tombstoned` 报错的解释与排查思路: --- ### 1. **`tombstoned` 是什么?** - **功能**:`tombstoned` 是 Android 系统中的一个 **后台服务**,专门用于记录和管理 **Native 层崩溃信息**(如 C/C++ 代码的崩溃)。当应用发生 Native 崩溃时,它会生成名为 `tombstone_XX` 的日志文件(存储路径:`/data/tombstones`),包含崩溃时的堆栈、寄存器状态等关键信息。 - **重要性**:这些日志是开发者调试 Native 代码(如 JNI、NDK 开发)的核心依据。 --- ### 2. **报错的常见原因** - **Native 代码缺陷**:应用或系统服务的 C/C++ 代码存在内存越界、空指针等问题。 - **内存不足**:物理内存耗尽导致 OOM(Out-of-Memory)。 - **硬件驱动问题**:内核模块或硬件驱动存在兼容性错误。 - **系统文件损坏**:系统分区文件被破坏(如 root 后误删关键文件)。 - **第三方模块冲突**:Magisk 模块、Xposed 框架等修改系统行为的工具引发异常。 --- ### 3. **如何排查?** #### 步骤 1:获取崩溃日志 - 通过 `adb` 命令导出 tombstone 文件: ```bash adb pull /data/tombstones/ ``` - 检查日志中的 **崩溃地址、堆栈调用、寄存器值**,定位到具体代码模块(如 `.so` 动态库)。 #### 步骤 2:分析日志关键点 - **信号类型**:如 `SIGSEGV`(段错误)表示内存非法访问。 - **崩溃线程**:主线程还是子线程? - **堆栈回溯**:查看崩溃时的函数调用链,找到崩溃代码位置。 #### 步骤 3:针对性解决 - **若为第三方应用**:联系开发者并提供 tombstone 文件。 - **若为系统问题**: - 尝试更新系统补丁。 - 恢复出厂设置(注意备份数据)。 - 如果是定制 ROM 或 root 后的设备,建议刷回官方系统。 --- ### 4. **用户操作建议** - **普通用户**:若频繁崩溃,优先卸载最近安装的应用或更新系统。 - **开发者**:结合 `addr2line` 或 `ndk-stack` 工具将崩溃地址转换为代码行号。 - **高级用户**:避免随意修改系统分区,谨慎使用 root 权限。 --- ### 5. **典型错误示例** ``` Build fingerprint: 'Xiaomi/...' Signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) Cause: null pointer dereference x0 0000000000000000 // 空指针! x1 0000007f8d4d3e00 backtrace: #00 pc 0000000000012345 /data/app/.../lib/arm64/libexample.so ``` --- **总结**:`tombstoned` 报错本质是 Native 层崩溃的记录机制,需结合日志分析根本原因。普通用户可通过系统更新或恢复操作缓解问题,开发者需依赖日志深入调试代码。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

OPPO内核工匠

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

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

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

打赏作者

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

抵扣说明:

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

余额充值