维修站的特殊诊断仪:debuggerd
在 Android 系统的维修站里,摆放着两台重要的诊断仪器:
-
给 Java 机器(应用)用的 "信号听诊器"(kill -3)
-
给 Native 机器(二进制程序)用的 "系统 CT 机"(debuggerd)
今天我们要讲的是 CT 机的工作原理。当工程师发现一台 Native 机器(比如 mediaserver)运转异常时,会使用命令:
plaintext
ini
adb shell debuggerd -b 17529 // 给pid=17529的机器做CT
第一章:CT 机的工作流程
1.1 诊断请求的发出
维修站的前台(Client 进程)接到工程师的请求后,向 CT 机(debuggerd 服务端)发送了一张诊断单:"DEBUGGER_ACTION_DUMP_BACKTRACE"(请求获取调用栈)。
cpp
运行
scss
// 前台发送诊断请求
send_request(DEBUGGER_ACTION_DUMP_BACKTRACE, pid);
1.2 CT 机的响应机制
CT 机收到请求后,像医院的 CT 设备一样,先启动一个临时检查室(fork 子进程),由专业技师(worker_process)负责具体检查:
cpp
运行
ini
// debuggerd的响应流程
if (action == DEBUGGER_ACTION_DUMP_BACKTRACE) {
pid_t child_pid = fork();
if (child_pid == 0) {
worker_process(pid, tid, action); // 子进程处理诊断
}
}
1.3 技师的诊断程序
技师进入临时检查室后,拿出诊断手册(dump_backtrace 函数),开始按步骤检查:
cpp
运行
c
// 诊断手册的核心流程
void dump_backtrace(int fd, BacktraceMap* map, pid_t pid, pid_t tid, ...) {
dump_process_header(&log, pid); // 记录病人基本信息
dump_thread(&log, map, pid, tid); // 检查主线程
for (每个子线程) {
dump_thread(&log, map, pid, sibling); // 检查所有子线程
}
dump_process_footer(&log, pid); // 完成诊断记录
}
第二章:给机器填写病历(进程头部信息)
技师首先来到档案室(/proc 文件系统),调取机器的基本档案:
cpp
运行
c
// 调取进程基本信息
static void dump_process_header(log_t* log, pid_t pid) {
// 从/proc/[pid]/cmdline获取机器型号(进程名)
fopen("/proc/17529/cmdline", "r") -> "mediaserver"
// 记录诊断时间
time_t t = time(NULL); -> "2016-11-12 22:22:22"
// 记录机器类型(ABI)
_LOG(log, "ABI: 'arm'\n");
}
最终生成的病历头像是这样的:
plaintext
javascript
----- pid 17529 at 2016-11-12 22:22:22 -----
Cmd line: /system/bin/mediaserver // 机器型号
ABI: 'arm' // 硬件架构
第三章:检查每个零件(线程)的工作记录
3.1 主线程的工作检查
技师开始检查机器的核心零件(主线程),从零件档案柜(/proc/[tid]/comm)取出零件名称:
cpp
运行
c
// 检查线程基本信息
static void dump_thread(log_t* log, ...) {
// 从/proc/17529/comm获取零件名称
fopen("/proc/17529/comm", "r") -> "mediaserver"
_LOG(log, ""mediaserver" sysTid=17529\n"); // 记录零件名称和编号
// 调用Backtrace::Create获取该零件的工作记录(调用栈)
std::unique_ptr<Backtrace> backtrace = Backtrace::Create(pid, tid, map);
backtrace->Unwind(0); // 展开工作记录
dump_backtrace_to_log(backtrace, log, " "); // 输出记录
}
3.2 解析零件的工作轨迹(调用栈)
技师使用专业工具(FormatFrameData)解析每个零件的工作轨迹,就像查看机床的每一步操作记录:
cpp
运行
rust
// 解析工作轨迹的每一步
std::string Backtrace::FormatFrameData(const backtrace_frame_data_t* frame) {
// 工作步骤编号
frame->num -> #00
// 操作指针位置
frame->pc -> 00042dac
// 工具库名称
frame->map.name -> "/system/lib/libc.so"
// 具体操作(函数+偏移)
frame->func_name + frame->func_offset -> "ioctl+8"
// 组合成完整记录
return "#00 pc 00042dac /system/lib/libc.so (__ioctl+8)";
}
主线程的工作记录显示它正在等待 IO 操作:
plaintext
shell
"mediaserver" sysTid=17529
#00 pc 00042dac /system/lib/libc.so (__ioctl+8)
#01 pc 000498ad /system/lib/libc.so (ioctl+14)
#02 pc 0001ea5b /system/lib/libbinder.so (_ZN7android14IPCThreadState14talkWithDriverEb+174)
...
3.3 Binder 零件的工作记录
另一个重要零件(Binder 线程)的记录显示它在等待跨机器通信:
plaintext
shell
"Binder_1" sysTid=17931
#00 pc 00042dac /system/lib/libc.so (__ioctl+8)
#01 pc 000498ad /system/lib/libc.so (ioctl+14)
#02 pc 0001ea5b /system/lib/libbinder.so (...talkWithDriver+174)
...
第四章:CT 报告的生成与解读
4.1 报告收尾
所有零件检查完毕后,技师在报告末尾盖章确认:
cpp
运行
c
// 报告收尾
static void dump_process_footer(log_t* log, pid_t pid) {
_LOG(log, "----- end %d -----\n", pid);
}
4.2 诊断报告的价值
这份 CT 报告(调用栈)对维修非常关键:
- 通过 pc 指针和函数名,能定位到具体哪行代码出现阻塞
- 查看各线程的调用栈,可以发现是否存在死锁(如多个线程互相等待资源)
- 分析 libbinder.so 中的调用栈,能判断是否卡在跨进程通信(IPC)环节
- 对比正常机器的 CT 报告,可以发现异常的函数调用路径
技术内幕:CT 机的工作原理
debuggerd 之所以能生成调用栈,核心在于解析 /proc/[pid]/maps 文件,这个文件记录了进程的内存映射信息:
plaintext
bash
00040000-0004c000 r-xp 00000000 103:04 134387 /system/lib/libc.so
...
00010000-00020000 r-xp 00000000 103:04 134357 /system/lib/libbinder.so
通过这些映射关系,debuggerd 可以将内存中的 PC 地址(如 00042dac)转换为具体的函数名(如__ioctl+8),就像根据地图把 GPS 坐标转换为具体街道名称。
总结:从故事到技术
通过这个 CT 诊断的故事,我们理解了 Native 进程 Trace 的核心流程:
-
debuggerd 作为系统级诊断工具,专门处理 Native 进程的调用栈获取
-
通过解析 /proc 文件系统的信息(maps/cmdline/comm),构建完整的调用栈
-
调用栈中的每一行都对应一次函数调用,是定位 Native 层问题的关键线索
当遇到 Native 进程导致的 ANR 或崩溃时,这份 "CT 报告" 能帮助工程师快速定位到问题代码,就像医生根据 CT 片找出病灶一样,是 Android 系统调试中不可或缺的工具。