当前位置:   article > 正文

dropbox 、tombstones、debuggred_debuggerd和tombstoned

debuggerd和tombstoned

一、 Android tombstone

Android Native 程序本质上就是一个 Linux 程序,因此当它在执行时发生严重错误,也会导致程序 crash,然后产生一个记录 crash 的现场信息的文件。tombstone记录native进程crash 奔溃时, 系统的数据信息。

TOMBSTONE 是 Android 用来记录 native 进程崩溃的 core dump 日志, 系统服务在启动完成后会增加一个 Observer 来侦测 tombstone 日志文件的变化, 每当生成新的 tombstone 文件, 就会增加一条 SYSTEM_TOMBSTONE 记录到 DropBoxManager 中.

What a Terrible Failure简称WTF,WTF是Android系统记录错误的一种方式,报告一个永远不应该发生的情况,有些只是打印Error stack trace和存异常信息文件到dropbox,有些会crash app。

当 NDK 程序在发生 Crash 时,它会在路径 /data/tombstones/ 下产生导致程序 Crash 的文件 tombstone_xx。并且 Google 还在 NDK 包中为我们提供了一系列的调试工具,例如 addr2line、objdump、ndk-stack。

1.1 信号机制

信号机制是 Linux 进程间通信的一种重要方式,Linux 信号一方面用于正常的进程间通信和同步,如任务控制(SIGINT, SIGTSTP,SIGKILL, SIGCONT,……);另一方面,它还负责监控系统异常及中断。 当应用程序运行异常时, Linux 内核将产生错误信号并通知当前进程。 当前进程在接收到该错误信号后,可以有三种不同的处理方式。

  • 1、忽略该信号。(crash, logcat 打印堆栈)

  • 2、捕捉该信号并执行对应的信号处理函数(signal handler)。

  • 3、执行该信号的缺省操作(如 SIGSEGV, 其缺省操作是终止进程)。

当 Linux 应用程序在执行时发生严重错误,一般会导致程序 crash。其中,Linux 专门提供了一类 crash 信号,在程序接收到此类信号时,缺省操作是将 crash 的现场信息记录到 core 文件,然后终止进程。

Android在此机制上,实现了一个更实用的功能:拦截这些信号dump进程信息以供调试

在一个新进程启动时,android的实现是在其中插debugger_init方法以实现拦截系统异常的几个singal:SIGILL,SIGABRT, SIGBUS, SIGFPE, SIGSEGV和SIGPIPE,代码位于:

bionic/linker/debugger.c

void debugger_init()

{

struct sigaction act;

memset(&act, 0, sizeof(act));

act.sa_sigaction = debugger_signal_handler;

act.sa_flags = SA_RESTART | SA_SIGINFO;

sigemptyset(&act.sa_mask);

sigaction(SIGILL, &act, NULL);

sigaction(SIGABRT, &act, NULL);

sigaction(SIGBUS, &act, NULL);

sigaction(SIGFPE, &act, NULL);

sigaction(SIGSEGV, &act, NULL);

#ifdefined(SIGSTKFLT)

sigaction(SIGSTKFLT, &act, NULL);

#endif

sigaction(SIGPIPE, &act, NULL);

}

Debugger_init的调用时机,是在应用程序入口地址__start后,__linker_init中调用的。这部分属于bionic实现的一部分,则对所有android的程序有效(android和传统的linux下基于glibc的不同,glibc的interpreter是/lib/ld-linux-xx.so.2,android的interpreter是/system/bin/linker)。

1: 每一个进程启动,进行信号的注册。

2:然后捕获系统的异常信号,进行异常处理,dump进程异常信息。

1.2 分析tombstones

因此,分析出现 Crash 的原因和代码位置最重要的就是分析这个 tombstone 文件。data/tombstones。addr2line, ndk-stack 所以,在使用这两种工具之前,你首先得把 Android NDK 工具包在你的电脑上安装好。

alias addr2line='NDKHOME/toolchains/x86−4.6/prebuilt/linux−x86/bin/i686−linux−android−addr2line其中NDKHOME表示你NDK的安装路径。虽然在Linux中同样有addr2line命令了,但是它与NDK中提供的addr2line指令还是略有差别的,所以我们可以使用alias来将shell中默认的addr2line指令链接的NDK的上述路径中,命令如下所示:aliasaddr2line=′NDK_HOME/toolchains/x86-4.6/prebuilt/linux-x86/bin/i686-linux-android-addr2line' 这样之后,我们就可以在 Linux 中的 shell 环境中来直接使用 addr2line 命令了。 addr2line 命令的各个参数的含义。

当Native进程发生了异常,比如NULL指针

操作系统会去异常向量表的地址去处理异常,然后发送信号

在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文件

二、debuggerd

信号机制是 Linux 进程间通信的一种重要方式,Linux 信号一方面用于正常的进程间通信和同步,如任务控制(SIGINT, SIGTSTP,SIGKILL, SIGCONT,……);另一方面,它还负责监控系统异常及中断。 当应用程序运行异常时, Linux 内核将产生错误信号并通知当前进程。 当前进程在接收到该错误信号后,可以有三种不同的处理方式。

  • 1、忽略该信号。(crash, logcat 打印堆栈)

  • 2、捕捉该信号并执行对应的信号处理函数(signal handler)。

  • 3、执行该信号的缺省操作(如 SIGSEGV, 其缺省操作是终止进程)。

当 Linux 应用程序在执行时发生严重错误,一般会导致程序 crash。其中,Linux 专门提供了一类 crash 信号,在程序接收到此类信号时,缺省操作是将 crash 的现场信息记录到 core 文件,然后终止进程。

Android在此机制上,实现了一个更实用的功能:拦截这些信号dump进程信息以供调试

在一个新进程启动时,android的实现是在其中插debugger_init方法以实现拦截系统异常的几个singal:SIGILL,SIGABRT, SIGBUS, SIGFPE, SIGSEGV和SIGPIPE,代码位于:

bionic/linker/debugger.c

void debugger_init()

{

struct sigaction act;

memset(&act, 0, sizeof(act));

act.sa_sigaction = debugger_signal_handler;

act.sa_flags = SA_RESTART | SA_SIGINFO;

sigemptyset(&act.sa_mask);

sigaction(SIGILL, &act, NULL);

sigaction(SIGABRT, &act, NULL);

sigaction(SIGBUS, &act, NULL);

sigaction(SIGFPE, &act, NULL);

sigaction(SIGSEGV, &act, NULL);

#ifdefined(SIGSTKFLT)

sigaction(SIGSTKFLT, &act, NULL);

#endif

sigaction(SIGPIPE, &act, NULL);

}

Debugger_init的调用时机,是在应用程序入口地址__start后,__linker_init中调用的。这部分属于bionic实现的一部分,则对所有android的程序有效(android和传统的linux下基于glibc的不同,glibc的interpreter是/lib/ld-linux-xx.so.2,android的interpreter是/system/bin/linker)。

1: 每一个进程启动,进行信号的注册。

2:然后捕获系统的异常信号,进行异常处理,dump进程异常信息

socket的客户端,通过向名为android:debuggerd的socket,发送一个消息,参数是tid:也就是出错的线程ID。 这里,异常进程挂起,等待socket的服务端:也就是debuggerd,处理这个事件。

debuggerd这个daemon是具体处理进程退出时,代码位于:system/core/debuggerd/debuggerd.c,看其main函数,这里即是android:debuggerd的服务端:

s =socket_local_server(DEBUGGER_SOCKET_NAME,

ANDROID_SOCKET_NAMESPACE_ABSTRACT, SOCK_STREAM);

if(s < 0) return1;

fcntl(s, F_SETFD,FD_CLOEXEC);

LOG("debuggerd:" __DATE__ " " __TIME__ "\n");

for(;;) {

struct sockaddraddr;

socklen_talen;

int fd;

alen =sizeof(addr);

XLOG("waitingfor connection\n");

fd = accept(s,&addr, &alen);

if(fd < 0){

XLOG("accept failed: %s\n", strerror(errno));

continue;

}

fcntl(fd,F_SETFD, FD_CLOEXEC);

handle_request(fd);

}

return 0;

}

当一个进程由于发生异常时,通过前一部分的介绍的debugger_signal_handler,会通过socket向debuggerd进程发送消息,这里,socket将accept到消息,通过handle_request(fd);来处理这个异常。在handle_request中,首先通过read_request(fd,&request),获取到socket通信的另外一端的信息pid,uid和gid。然后从socket中,读到debugger_signal_handler送过来的tid,自此debuggerd即可知道需要被调试进程的信息了。

读取客户端送过的tid,tid是标明那个线程ID执行中遇到错误了,debuggerd就专门针对该线程dump出其寄存器、backtrace和栈信息以供调试。ptrace(PTRACE_ATTACH,request.tid, 0,0)这里,debuggerd就挂上ptrace了,attach到出问题的线程,这样debuggerd就可以控制tid线程了。ptrace的实现,attach上之后,debuggerd进程就是被调试进程的父进程了,PTRACE_ATTACH会向被调试进程发送SIGSTOP。由于之前,在目标进程的signal处理函数中,是堵在socket的read中(这样做是等待被debuggerd响应到),TEMP_FAILURE_RETRY(write(fd,"\0", 1)) != 1)这里写一下,则read可以读到数据,等待结束,之后如果使用ptrace(PTRACE_CONT,request.tid, 0, 0)的话,被调线程可以继续执行。

Debuggerd除了会在进程异常时产生tombstone外,还可以协助我们debug这个进程。使用方法是:

在串口中,设置需要debug的应用程序的uid后,如果这个程序出现异常,即可挂上gdb调试。

Androiduid的规则,所有zygote启动的app,都是从10000开始,比如ps时,看到一个app叫app_23,则可以设置:setpropdebug.db.uid 10023,即可debug此进程。

对于一些库,可能没有符号信息,这样在tombsotne中打印的trace,很难查看具体出错的函数,可以通过在该库模块的编译选项中,注释掉,重编编译,即可得到带符号信息的库.

Attach 实现的根本原理就是使用了 Linux 下是文件 Socket 通信(详情可以自行百度或 Google)。有人也许会问,为什么要采用文件 socket 而不采用网络 socket?我个人认为也许一方面是为了效率(避免了网络协议的解析、数据包的封装和解封装等),另一方面是为了减少对系统资源的占用(如网络端口占用)。采用文件 socket 通信,就好比两个进程通过事先约定好的协议,对同一个文件进行读写操作,以达到信息的交互和共享,控制。

1.1attach 机制

Attach机制实际就是JDK提供的一种JVM进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。

通过jstack查看线程dump可以看到这两个线程:Signal Dispatcher线程和Attach Listener线程

每个JVM都会有Signal Dispatcher线程,用于处理信号。Attach Listener线程用于JVM进程间的通信,但是它不一定会启动,启动它有两种方式:

1)启动的时候通过jvm参数指定启动Attach Listener线程

java -XX:+StartAttachListener mainClass

2)attach目标JVM成功后,目标JVM启动Attach Listener线程

如果不在启动JVM的时候启动Attach Listener线程,那只能依靠Signal Dispatcher线程来启动了。

就是目标 JVM 在启动的时候,在 JVM 内部启动了一个监听线程,这个线程的名字叫“Signal Dispatcher”,该线程的作用是,监听并处理 OS 的信号。至于什么是 OS 的信号(可以自行百度或 Google),简单理解就是,Linux系统允许进程与进程之间通过过信号的方式进行通信,如触发某个操作(操作由接受到信号的进程自定义)。如平常我们用的最多的就是 kill -9 ${pid}来杀死某个进程,kill进程通过向${pid}的进程发送一个编号为“9”号的信号,来通知系统强制结束${pid}的生命周期。内核转发信号。监听线程监听信号,收到信号进行处理。

Attach Listener线程启动后,就会创建一个监听套接字,并创建了一个文件/tmp/.java_pid<pid>这个就是LinuxVirtualMachine构造函数中一直尝试获取的socketFile。随着这个socketFile创建,也就意味着客户端那边的attach成功了。

之后客户端和目标JVM进程就通过这个socketFile进行通信。客户端可以通过这个socketFile发送相关命令。Attach Listener线程做的事情就是监听这个socketFile,发现有请求就解析,然后根据命令执行不同的方法,最后将结果返回。

**Unix domain socket又叫IPC(inter-process communication进程间通信) socket,用于实现同一主机上的进程间通信。**socket原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX domain socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX domain socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。

三、dropbox

Android DropBox 是 Android 用来持续化存储系统数据的一个管理类,主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat。

系统服务dropbox以文件形式记录了系统各种异常信息,例如app crash、native crash、anr、kernel panic等等。

/frameworks/base/core/java/android/os/DropBoxManager.java

/frameworks/base/services/core/java/com/android/server/DropBoxManagerService.java

dropbox日志路径:/data/system/dropbox

1、DropBoxManager dropbox = (DropBoxManager)getSystemService(Context.DROPBOX_SERVICE) 例如,dropbox.addText可实现把需要记录的数据丢给dropbox进行存储。

2、监听广播android.intent.action.DROPBOX_ENTRY_ADDED,可知系统发生了异常 注意权限:android.permission.READ_LOGS --- 平台签名或priv-app

 

java层:handleApplicationCrashInner("crash", r, processName, crashInfo); native层: mAm.handleApplicationCrashInner("native_crash", mApp, mApp.processName, ci);

DEFAULT_AGE_SECONDS = 3 * 86400:文件最长可存活时长为3天

DEFAULT_MAX_FILES = 1000:最大dropbox文件个数为1000

DEFAULT_QUOTA_KB = 5 * 1024:分配dropbox空间的最大值5M

DEFAULT_QUOTA_PERCENT = 10:是指dropbox目录最多可占用空间比例10%

DEFAULT_RESERVE_PERCENT = 10:是指dropbox不可使用的存储空间比例10%

QUOTA_RESCAN_MILLIS = 5000:重新扫描retrim时长为5s

上面这些都是默认值,完全可以通过设置content://settings/global数据库对应项来修改。

使用dropbox的优点

a.利用 DropBoxManager 来记录需要相对持久化存储的错误日志信息

优点:自动抓错, 避免人为因素而产生的错误遗漏

b.错误自动上报

可以利用dropbox每生成新的记录, Dropbox 就会发送广播:

DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED

app可以接收监听改广播获取指定的数据文件内容,内容发送到指定的服务器或邮箱完成错误自动上报。

利用前提:app要具有系统权限 因为:DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED是保护性广播,且读取系统日志也需要android.Manifest.permission.READ_LOGS权限。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/684790
推荐阅读
相关标签
  

闽ICP备14008679号