本文以android4.1为基础,分析debuggerd这个工具的使用方法和源码。
1.Debuggerd 简介
debuggerd是一个daemon进程,在系统启动时随着init进程启动。主要负责将进程运行时的信息dump到文件或者控制台中。
1.1 debuggerd的运行原理
- 创建一个名为 “Android:debuggerd”的socket,作为server端等待其他client端进程的连接
- 接收client端进程发送来的tid和action信息
- 将由tid指定的那个进程的运行信息,按照由action指定的动作dump到文件或者控制台中
可以作为debuggerd的client端的进程主要有几种:
这种程序由bionic的linker安装异常信号的处理函数,当程序产生异常信号时,进入信号处理函数,与debuggerd建立。
debuggerd可以在控制台中以命令debuggerd -b []启动 ,然后与debuggerd daemon建立连接。这样debuggerd可以在不中断进程执行的情况下dump由tid指定的进程的信息。
控制台中运行命令callstack/dumpstate,并指定必要的参数,命令中会调用dump_backtrace_to_file与debuggerd交互
1.2 debuggerd的使用方法
- 产生异常信号的C/C++程序与debuggerd建立连接后,debuggerd将进程信息dump到tombstone_XX文件中保存到/data/tombstone/文件夹下。可通过查看tombstone_XX分析异常进程的堆栈信息
- 在控制台中以命令debuggerd -b []启动。如果加上-b参数,则由tid指定的进程的信息将dump到控制台上,否则dump到tombstone文件中
- 控制台中运行命令callstack/dumpstate,进程信息会写入这两个命令指定的文件中
1.3 tombstone文件的分析方法
使用prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.6/bin下的工具
- arm-linux-androideabi-addr2line 将类似libxxx.so 0×00012345的调用栈16进制值翻译成文件名和函数名 arm-linux-androideabi-addr2line -e libxxx.so 0×00012345
- arm-linux-androideabi-nm 列出文件的符号信息 arm-linux-androideabi-nm -l -C -n -S libdvm.so > dvm.data
- arm-linux-androideabi-objdump 列出文件的详细信息 arm-eabi-objdump -C -d libc.so > libc.s
通过以上工具的分析 ,可以得到较完整的调用栈以及调用逻辑的汇编码
tombstone文件的分析的实用工具和参考文章:
- tombstones.py Python script to retrieve line info from an android tombstone using ndk-stack and addr2line. https://github.com/feichh/android-tombstones
- android-ndk-stacktrace-analyzer A simple tool for analyzing the stack trace generated from crashing native code in the android system. http://code.google.com/p/android-ndk-stacktrace-analyzer/
- How to read Android crash log and stack trace This article briefly explains the structure of the log, how to read it. http://bootloader.wikidot.com/linux:android:crashlog
1.4 Debuggerd 框架图
2.linker中debugger.c源码分析
bionic libc,是一种 C 标准函式库, 用于 Android 嵌入式系统上。bionic库中的链接器会对以下七种信号设置Handler(debugger_signal_handler):
- SIGILL(非法指令异常)
- SIGABRT(abort退出异常)
- SIGBUS(硬件访问异常)
- SIGFPE(浮点运算异常)
- SIGSEGV(内存访问异常)
- SIGSTKFLT(协处理器栈异常)
- SIGPIPE(管道异常)
链接到bionic库上的C/C++程序崩溃时,内核会发送相应的signal,进程收到异常信号后,会转入debugger_signal_handler函数中进行处理。
debugger_init函数(见文件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); sigaction(SIGSTKFLT, &act, NULL); sigaction(SIGPIPE, &act, NULL); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
)
;
sigaction
(
SIGSTKFLT
,
&act
,
NULL
)
;
sigaction
(
SIGPIPE
,
&act
,
NULL
)
;
}
|
debugger_init中act.sa_flags = SA_RESTART | SA_SIGINFO的涵义:
如果指定该参数,表示若信号中断了进程的某个系统调用,则系统自动启动该系统调用。如果不指定该参数,则被中断的系统调用返回失败,错误码为EINTR。这个标志位只要用于处理慢系统调用(可能会被阻塞的系统调用)。比如调用write系统调用写某个设备被阻塞,这时进程捕获某个信号且进入相应信号处理函数返回时,该系统调用可能要返回ENINTR错误。指定这个参数后,系统调用会重启,与RETRY_ON_EINTR宏配合使用则可以保证写操作的完成
如果指定该参数,表示信号附带的参数(siginfo_t结构体)可以被传递到信号处理函数中。
debugger_signal_handler函数处理流程
- debugger_signal_handler函数分析:调用logSignalSummary将signal信息写入文件
- 调用socket_abstract_client函数与debuggerd建立socket连接
- 如果连接建立成功,则设置结构体debugger_msg_t,并发送给debuggerd
msg.action = DEBUGGER_ACTION_CRASH;//告诉debuggerd采取何种行 msg.tid = tid;//线程号 RETRY_ON_EINTR(ret, write(s, &msg, sizeof(msg)));
|
msg
.
action
=
DEBUGGER_ACTION_CRASH
;
//告诉debuggerd采取何种行
msg
.
tid
=
tid
;
//线程号
RETRY_ON_EINTR
(
ret
,
write
(
s
,
&
msg
,
sizeof
(
msg
)
)
)
;
|
- 等待debuggerd的回复,阻塞在下面的调用中,收到回复后接着执行下面的流程
RETRY_ON_EINTR(ret, read(s, &tid, 1));
|
RETRY_ON_EINTR
(
ret
,
read
(
s
,
&
tid
,
1
)
)
;
|
- 重新设置信号处理函数为SIG_DFL,即采取默认的动作
- 重新发送信号,进程从当前信号处理函数返回后,会处理这个信号,进行默认的信号处理动作,即中断进程。
logSignalSummary函数分析:
- 获取异常信号的名字和thread名字,并格式化字符串
- 调用函数__libc_android_log_write函数,用相关信息填充struct iovec vec[3],此结构体数组的内容最终将写入log文件
- 函数__libc_android_log_write中,log_id为LOG_ID_MAIN,因此
log_channels[LOG_ID_MAIN] = { __write_to_log_init, -1, "/dev/"LOGGER_LOG_MAIN }
|
log_channels
[
LOG_ID_MAIN
]
=
{
__write_to_log_init
,
-
1
,
"/dev/"
LOGGER_LOG
_MAIN
}
|
log_channels[log_id].logger(log_id, vec)将调用__write_to_log_init
- 在函数__write_to_log_init中,log文件将写入”/dev/log/main”中,执行写log操作的函数是__write_to_log_kernel
3.Debuggerd 源码分析
debuggerd有两种启动方式:
- 在init进程中以deamon的方式启动,在init.rc中
service debuggerd /system/bin/debuggerd class main
|
service
debuggerd
/
system
/
bin
/
debuggerd
class
main
|
以这种方式启动的话,进入main函数后,将调用do_server函数,作为server端为其他进程提供dump进程信息的服务
- 直接运行system/bin/debuggerd可执行文件,需要指定参数,用法为
debuggerd -b [] //参数-b表示在控制台中输出backtrace
|
debuggerd
-
b
[
<
tid
>
]
//参数-b表示在控制台中输出backtrace
|
以这种方式启动的话,进入main函数后,将调用do_explicit_dump函数,与debuggerd daemon通信,将指定进程的信息dump到文件或控制台
Debuggerd框架图
do_server函数流程分析
(以daemon方式启动的时候将会进入这个函数)
1.为异常信号安装处理函数SIG_DFL,即忽略自身出现的错误问题 signal(SIGILL, SIG_DFL); signal(SIGABRT, SIG_DFL);
signal(SIGBUS, SIG_DFL); signal(SIGFPE, SIG_DFL); signal(SIGSEGV, SIG_DFL); signal(SIGPIPE, SIG_DFL); signal(SIGSTKFLT, SIG_DFL);
|
signal
(
SIGBUS
,
SIG_DFL
)
;
signal
(
SIGFPE
,
SIG_DFL
)
;
signal
(
SIGSEGV
,
SIG_DFL
)
;
signal
(
SIGPIPE
,
SIG_DFL
)
;
signal
(
SIGSTKFLT
,
SIG_DFL
)
;
|
2. 调用下面的代码,建立socket通信的server端
s = socket_local_server(DEBUGGER_SOCKET_NAME,ANDROID_SOCKET_NAMESPACE_ABSTRACT, SOCK_STREAM);
|
s
=
socket_local_server
(
DEBUGGER_SOCKET_NAME
,
ANDROID_SOCKET_NAMESPACE_ABSTRACT
,
SOCK_STREAM
)
;
|
3.进入无限循环中,等待连接请求,相关代码如下
for(;;) { struct sockaddr addr; socklen_t alen; int fd; alen = sizeof(addr); fd = accept(s, &addr, &alen); if(fd < 0) { continue; } fcntl(fd, F_SETFD, FD_CLOEXEC); handle_request(fd); }
1
2
3
4
5
6
7
8
9
10
11
12
|
for
(
;
;
)
{
struct
sockaddr
addr
;
socklen_t
alen
;
int
fd
;
alen
=
sizeof
(
addr
)
;
fd
=
accept
(
s
,
&
addr
,
&
alen
)
;
if
(
fd
<
0
)
{
continue
;
}
fcntl
(
fd
,
F_SETFD
,
FD_CLOEXEC
)
;
handle_request
(
fd
)
;
}
|
最终调用handle_request处理socket上的请求
handle_request函数流程分析
1.调用read_request处理socket上由client端进程发送来的数据,处理流程为
<1.1>首先调用函数getsockopt给结构体变量ucred cr赋值
getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &len);
|
getsockopt
(
fd
,
SOL_SOCKET
,
SO_PEERCRED
,
&
cr
,
&
len
)
;
|
ucred存储了socket的client端进程的pid uid gid
<1.2>从socket上读取debugger_msg_t结构体
<1.3> 给debugger_request_t* out_request 赋值
out_request->action = msg.action; out_request->tid = msg.tid; out_request->pid = cr.pid; out_request->uid = cr.uid; out_request->gid = cr.gid;
|
out_request
->
action
=
msg
.
action
;
out_request
->
tid
=
msg
.
tid
;
out_request
->
pid
=
cr
.
pid
;
out_request
->
uid
=
cr
.
uid
;
out_request
->
gid
=
cr
.
gid
;
|
<1.4>如果debugger_msg_t中设置的action为DEBUGGER_ACTION_CRASH,说明是crash的C/C++进程发来的请求,则判断传进来的tid是否有效
<1.5>如果debugger_msg_t中设置的action为
DEBUGGER_ACTION_DUMP_BACKTRACE或者 DEBUGGER_ACTION_DUMP_BACKTRACE_TO_LOG
|
DEBUGGER_ACTION_DUMP
_BACKTRACE或者
DEBUGGER_ACTION_DUMP_BACKTRACE_TO_LOG
|
说明是其他方式(debuggerd/callstack)发来的请求,则要求必须为root权限或者system权 限,然后再判断tid是否有效
2.从read_request返回后,调用ptrace函数attach由tid指定的进程,此时debuggerd将变为被attache进程的父进程,然后ptrace函数会向子进程发送SIGSTOP信号将子进程停下来。此时,父进程有机会检查子进程核心image和寄存器的值
3.调用下面的语句给client端子进程回复消息,使clinet端的进程能从read调用中返回
TEMP_FAILURE_RETRY(write(fd, "\0", 1)
|
TEMP_FAILURE_RETRY
(
write
(
fd
,
"\0"
,
1
)
|
4.在for循环中等待子进程停止
int signal = wait_for_signal(request.tid, &total_sleep_time_usec);
|
int
signal
=
wait_for_signal
(
request
.
tid
,
&
total_sleep_time_usec
)
;
|
子进程收到SIGSTOP信号时,根据不同的action进行动作
if (request.action == DEBUGGER_ACTION_DUMP_TOMBSTONE) { tombstone_path = engrave_tombstone(request.pid, request.tid, signal, true, true, &detach_failed, &total_sleep_time_usec); } else if (request.action == DEBUGGER_ACTION_DUMP_BACKTRACE) { dump_backtrace(fd, request.pid, request.tid, &detach_failed, &total_sleep_time_usec); } else if (request.action == DEBUGGER_ACTION_DUMP_BACKTRACE_TO_LOG) { dump_backtrace_for_thread(fd, request.pid, request.tid, &detach_failed, &total_sleep_time_usec);
|
if
(
request
.
action
==
DEBUGGER_ACTION_DUMP_TOMBSTONE
)
{
tombstone_path
=
engrave_tombstone
(
request
.
pid
,
request
.
tid
,
signal
,
true
,
true
,
&
detach_failed
,
&
total_sleep_time_usec
)
;
}
else
if
(
request
.
action
==
DEBUGGER_ACTION_DUMP_BACKTRACE
)
{
dump_backtrace
(
fd
,
request
.
pid
,
request
.
tid
,
&
detach_failed
,
&
total_sleep_time_usec
)
;
}
else
if
(
request
.
action
==
DEBUGGER_ACTION_DUMP_BACKTRACE_TO_LOG
)
{
dump_backtrace_for_thread
(
fd
,
request
.
pid
,
request
.
tid
,
&
detach_failed
,
&
total_sleep_time_usec
)
;
|
子进程收到七种异常信号时,调用函数engrave_tombstone进行处理 Notes:收到SIGSTOP说明与debuggerd通信的不是crash的进程,根据action不同将进程信息写入tombstone文件,或者写入socket文件并由相应的客户端进程会读取socket文件进行处理。收到七种异常信号说明是crash的进程,调用engrave_tombstone直接将dump的信息写到tombstone文件中
5.调用ptrace(PTRACE_DETACH, request.tid, 0, 0)解除对子进程的追踪
6.调用kill(request.pid, SIGCONT)恢复被停止的子进程,并让其自然终止
关于attach_gdb的说明
1.如果运行了类似以下指令:
adb shell setprop debug.db.uid 10000
|
adb
shell
setprop
debug
.
db
.
uid
10000
|
则所有uid<10000的进程crash的时候attach_gdb为true,crash的进程将停止
2.调用ptrace(PTRACE_DETACH, request.tid, 0, 0) 解除对子进程的追踪后,开始等待gdb的连接
adb forward tcp:5039 tcp:5039 adb shell gdbserver :5039 --attach pid &
|
adb
forward
tcp
:
5039
tcp
:
5039
adb
shell
gdbserver
:
5039
--
attach
pid
&
|
3.用户按下HOME或者VOLUME DOWN按键,可以使进程继续进行,自然crash 4.attach_gdb为false时,只会解除对子进程的追踪
engrave_tombstone函数流程分析
对于crash的C/C++进程,主要通过这个函数dump进程信息
1.创建”/data/tombstones”文件夹并修改权限
2.调用函数find_and_open_tombstone,tombstone_XX文件最多10个,超过则覆盖最早的
3.调用dump_crash将所有信息dump到tombstone文件
4.如果isSystemServerCrash(crash的进程是gaiad)或isSystemFinalizerTimeout为true,则运行logcat命令将log缓冲区(/dev/log下的system,events ,radio,main文件)中的内容输出到tombstone文件中
dump_crash函数流程分析
1.dump_build_info(log)
dump “Build fingerprint”
2.dump_fault_addr(log, tid, signal) 调用ptrace(PTRACE_GETSIGINFO, tid, 0, &si)dump siginfo信息
3.dump_thread(context, log, tid, true, total_sleep_time_usec)
dump进程的上下文信息
4.dump_maps(log, pid)
dump /proc/pid/maps 中的信息
5.dump_smaps(log, pid)
dump /proc/pid/smaps 中的信息
6.dump_status(log, pid)
dump /proc/pid/status中的信息
7.dump_sibling_thread_report(context, log, pid, tid, total_sleep_time_usec)
do_explicit_dump函数流程分析
(在命令行中加参数启动的时候将进入这个函数)
判断dump_backtrace为true,调用dump_backtrace_to_file
(1) 调用socket_local_client与debuggerd deamon建立连接
(2) 给debugger_msg_t变量赋值,并发送给debuggerd
debugger_msg_t msg; msg.tid = tid; msg.action = DEBUGGER_ACTION_DUMP_BACKTRACE;
|
debugger_msg_t
msg
;
msg
.
tid
=
tid
;
msg
.
action
=
DEBUGGER_ACTION_DUMP_BACKTRACE
;
|
(3)等待debuggerd 回复,并读取dump的信息,写入控制台中
while ((n = TEMP_FAILURE_RETRY(read(s, buffer, sizeof(buffer)))) > 0) { if (TEMP_FAILURE_RETRY(write(fd, buffer, n)) != n) { result = -1; break; } }
|
while
(
(
n
=
TEMP_FAILURE_RETRY
(
read
(
s
,
buffer
,
sizeof
(
buffer
)
)
)
)
>
0
)
{
if
(
TEMP_FAILURE_RETRY
(
write
(
fd
,
buffer
,
n
)
)
!=
n
)
{
result
=
-
1
;
break
;
}
}
|
判断dump_backtrace为false,调用dump_tombstone
(1) 调用socket_local_client与debuggerd deamon建立连接
(2) 给debugger_msg_t变量赋值,并发送给debuggerd
debugger_msg_t msg; msg.tid = tid; msg.action = DEBUGGER_ACTION_DUMP_TOMBSTONE;
|
debugger_msg_t
msg
;
msg
.
tid
=
tid
;
msg
.
action
=
DEBUGGER_ACTION_DUMP_TOMBSTONE
;
|
(3)等待debuggerd 回复,调用
TEMP_FAILURE_RETRY(read(s, pathbuf, pathlen - 1))
|
TEMP_FAILURE_RETRY
(
read
(
s
,
pathbuf
,
pathlen
-
1
)
)
|
这里和
handle_request的write(fd, tombstone_path, strlen(tombstone_path))
|
handle
_request的
write
(
fd
,
tombstone_path
,
strlen
(
tombstone_path
)
)
|
相互呼应 read读到的是tombstone_path,赋值给pathbuf后由do_explicit_dump调用
fprintf(stderr, "Tombstone written to: %s\n", tombstone_path)
|
fprintf
(
stderr
,
"Tombstone written to: %s\n"
,
tombstone_path
)
|
输出