一、前言
Android系统类问题主要有stability、performance、power、security。tombstoned是android平台的一个守护进程,它注册成3个socket服务端,客户端封装在crash_dump和debuggerd_client。 crash_dump用于跟踪定位C++ crash, debuggerd_client用于在某些场景(发生ANR,watchdog,shell执行debuggerd -b)dump指定进程 的backtrace。
二、tombstone原理
1,ELF程序加载过程
在进入execve()系统调用之后,Linux内核就开始进行真正的装配工作。在内核中,execve()系统调用相应的入口是sys_execve()。sys_execve()进行一些参数的检查复制之后,调用do_execve()。do_execve()会首先查找被执行的文件,如果找到文件,则读取文件的前128个字节。文件的前128个字节保存着可执行文件的格式信息,特别是前四个字节(魔数)。这样可以根据不同的可执行文件信息,来调用不同的装载模块。当do_execve()读取了这128个字节的文件头部之后,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理。linux中所有被支持的可执行文件格式都有相应的装载处理过程。search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并且调用相应的过程。ELF可执行文件的装载处理过程叫做load_elf_binary()。load_elf_binary()被定义在fs/Binfmt_elf.c。它的主要步骤是:
1. 检查ELF可执行文件格式的有效性,比如魔数,程序头表中段(Segment)的数量
2. 寻找动态链接的“.interp”段,设置动态连接器路径(与动态链接有关)
3. 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
4. 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址。
5. 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_entry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态连接器。
当load_elf_binary()执行完毕,返回至do_execve()再返回到sys_execve(),上面的第5步中已经把系统调用的返回地址改成了被装载的ELF程序的入口地址了。所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器存放下一个机器指令的地址, 直接跳转到了ELF程序的入口地址了,于是新的程序开始执行,ELF可执行文件装载完成。
在 execve() 执行过程中,系统会清掉 fork() 复制的原程序的页目录和页表项,并释放对应页面。系统仅为新加载的程序代码重新设置进程数据结构中的信息,申请和映射了命令行参数和环境参数块所占的内存页面,以及设置了执行代码执行点。此时内核并不从执行文件所在块设备上加载程序的代码和数据。当该过程返回时即开始执行新的程序,但一开始执行肯定会引起缺页异常中断发生。因为代码和数据还未被从块设备上读入内存。此时缺页异常处理程序会根据引起异常的线性地址在主内存区为新程序申请内存页面(内存帧),并从块设备上读入引起异常的指定页面。同时还为该线性地址设置对应的页目录项和页表项。这种加载执行文件的方法称为需求加载(Load on demand)。
2,Android Linker
无 --dynamic-linker编译参数的ELF文件会加载为静态可执行程序,如init进程:
Android大部分可执行程序使用linker做动态连接器,编译时候加上参数 --dynamic-linker
除了init进程,recovery,adbd进程也没有配置--dynamic-linker链接器。他们在Android.mk里面声明LOCAL_FORCE_STATIC_EXECUTABLE := true或者在Android.bp声明static_executable: true。静态可执行程序不能加载libc等动态库,只能导入static_libs。以上都是针对ELF格式的程序。
当然,/system/bin/linker也是静态连接的。他自己不能配置为动态链接器。Linker的Android.bp里面配置static_executable: true。同时根据arch编译汇编代码 begin.S来配置程序入口。Arm64汇编指令 bl:跳转指令,但是在跳转之前,会将下一条指令保存到返回地址(链接寄存器)LR寄存器中。Br与bl类似,只是后面参数需要特定的寄存器。
3,Android Linker 拦截信号
内核在应用程序的栈上构建一个信号处理栈帧,当信号事件发生后,内核将信号置为pending状态.应用程序注册信号处理hanlder,在中断返回或者系统调用返回时,该进程留一个时间片查看pending的信号,然后通过中断返回或者系统调用返回到用户态,执行信号处理函数。执行信号处理函数之后,再次通过sigreturn系统调用返回到内核,在内核中再次返回到应用程序被中断打断的地方或者系统调用返回的地方接着运行。
debuggerd_signal_handler()函数最开始使用互斥锁 pthread_mutex_lock() 来保护线程,方式同一时间多个线程处理信号而导致冲突。接着,调用 log_signal_summary() 来输出一些log 信息信息,例如fault addr、signo、signame、pid、tid、线程名、主线程名等。
接着,调用clone() 函数创建伪线程,并在伪线程中调用 debuggerd_dispatch_pseudothread() 函数,原来的线程原地等待子线程的开始和结束。
如果应用程序没有注册对应的信号处理函数,那么信号发生后,内核按照内核默认的信号处理方式处理该信号。
debuggerd_dispatch_pseudothread() 线程中会 fork 一个子进程,并通过 execle() 系统调用去执行 crash_dump64 程序,父进程等待 crash_dump64 进程退出。
4,LINUX信号
当程序运行出现异常时候,CPU会发出指令异常信号。非可靠信号,由linker注册。Linker加载的动态可执行程序,即使被加载程序调用了注册函数函数,也不能正常收到,因为这些信号已经提前被被linker截断。
中断信号的产生有以下4个来源:1,外设(来自中断控制器); 2,IPI(处理器间中断); 3, CPU异常(比如前一条指令存在除零错误、缺页错误等); 4,中断指令。前两种中断都可以叫做硬件中断,都是异步的;后两种中断都可以叫做软件中断,都是同步的。在arm64架构加,将上述中断分类为异步异常(Physical interrupts和Virtual interrupts)和同步异常(同步异常与当前指令的执行直接相关)。其实大同小异。以上是CPU层面的,对于LINUX来说,以上描述的异常或者中断