一、案件启动:指挥官与收集员的协作
在 Android 系统的侦探事务所里,bugreport 是经验丰富的指挥官,它的任务是在系统出现问题时,快速收集所有线索。当用户输入 adb bugreport 命令时,就像侦探事务所接到了紧急案件。
cpp
运行
int main() {
// 启动dumpstate收集员服务
property_set("ctl.start", "dumpstate");
// 尝试20次联系收集员,每次间隔1秒
int s;
for (int i = 0; i < 20; i++) {
s = socket_local_client("dumpstate", ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_STREAM);
if (s >= 0) break;
sleep(1);
}
// 设置3分钟超时,没收到线索就结束
struct timeval tv = {3*60, 0};
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
// 持续接收收集员发来的线索并记录
while (1) {
char buffer[65536];
ssize_t bytes_read = read(s, buffer, sizeof(buffer));
if (bytes_read <= 0) break;
write(STDOUT_FILENO, buffer, bytes_read);
}
}
故事版解析:
- 指挥官
bugreport首先给调度中心(init 进程)发消息,让dumpstate收集员准备工作 - 指挥官用 "电话"(socket)联系收集员,打了 20 次才接通
- 指挥官设定了 3 分钟的通话超时,如果收集员没说话就挂断
- 收集员不断把线索通过电话传给指挥官,指挥官记录到笔记本上(标准输出)
二、收集员的准备工作:装备升级与权限管理
dumpstate 收集员接到任务后,开始做准备工作,确保自己不会被打断,并且有足够的权限收集线索。
cpp
运行
int main(int argc, char *argv[]) {
// 检查权限:如果不是root,就强制调用bugreport(老版本兼容)
if (getuid() != 0) {
return execl("/system/bin/bugreport", "/system/bin/bugreport", NULL);
}
// 提高优先级,防止被OOM杀手(系统清理程序)赶走
setpriority(PRIO_PROCESS, 0, -20);
FILE *oom_adj = fopen("/proc/self/oom_adj", "we");
if (oom_adj) { fputs("-17", oom_adj); fclose(oom_adj); }
// 解析命令参数,比如是否要添加时间、输出到文件等
while ((c = getopt(argc, argv, "dho:svqzpB")) != -1) {
// 处理不同参数...
}
// 打开振动器,作为开始收集的提醒(手机震动)
FILE *vibrator = fopen("/sys/class/timed_output/vibrator/enable", "we");
if (vibrator) vibrate(vibrator, 150);
// 收集前先记录系统启动命令行
FILE *cmdline = fopen("/proc/cmdline", "re");
if (cmdline) { fgets(cmdline_buf, sizeof(cmdline_buf), cmdline); fclose(cmdline); }
// 收集虚拟机和native进程的堆栈线索
dump_traces_path = dump_traces();
// 获取tombstone(崩溃日志)文件描述符
get_tombstone_fds(tombstone_data);
// 切换到非root用户,但保留必要权限
gid_t groups[] = { AID_LOG, AID_SDCARD_R, AID_SDCARD_RW, AID_MOUNT, AID_INET, AID_NET_BW_STATS };
setgroups(sizeof(groups)/sizeof(groups[0]), groups);
setgid(AID_SHELL); setuid(AID_SHELL);
// 最终开始核心收集工作
dumpstate();
}
故事版解析:
- 收集员检查身份:如果不是侦探所长(root),就叫真正的指挥官来
- 收集员穿上 "防驱逐背心":提高优先级,让系统别把自己赶走
- 戴上 "参数眼镜":根据不同参数(如 -d 加时间、-o 存文件)调整工作方式
- 按下 "震动警报":告诉手机开始工作了(手机震动一下)
- 先查看 "系统入场券"(/proc/cmdline),记录系统启动时的参数
- 拿出 "堆栈扫描仪"(dump_traces)和 "崩溃日志探测器"(tombstone_fds)
- 换上 "普通工作服"(非 root 用户),但保留查看日志等必要权限
- 最后拿出 "核心收集手册",开始正式收集线索
三、核心取证:收集员的现场调查
dumpstate() 是收集员的核心工作,它会遍历系统的各个角落,收集所有可能的线索。
cpp
运行
static void dumpstate() {
// 先记录系统基本信息
printf("========================================================\n");
printf("== dumpstate: %s\n", date);
printf("Build: %s\n", build);
printf("Build fingerprint: '%s'\n", fingerprint);
// 记录系统运行时长
run_command("UPTIME", 10, "uptime", NULL);
// 内存信息
dump_file("MEMORY INFO", "/proc/meminfo");
// CPU和进程信息
run_command("CPU INFO", 10, "top", "-n", "1", "-d", "1", "-m", "30", "-t", NULL);
run_command("PROCRANK", 20, "procrank", NULL);
// 内核日志
do_dmesg();
// 系统日志(logcat)
run_command("SYSTEM LOG", timeout/1000, "logcat", "-v", "threadtime", "-d", "*:v", NULL);
// 虚拟机堆栈线索(ANR等)
if (dump_traces_path) dump_file("VM TRACES JUST NOW", dump_traces_path);
// tombstone崩溃日志
for (size_t i = 0; i < NUM_TOMBSTONES; i++) {
if (tombstone_data[i].fd != -1) {
dump_file_from_fd("TOMBSTONE", tombstone_data[i].name, tombstone_data[i].fd);
}
}
// 网络信息
run_command("NETWORK INTERFACES", 10, "ip", "link", NULL);
// Binder通信信息(Android特有的进程通信)
dump_file("BINDER STATS", "/sys/kernel/debug/binder/stats");
// 最重要的dumpsys信息,收集所有系统服务状态
run_command("DUMPSYS", 60, "dumpsys", NULL);
// 应用相关信息
run_command("APP ACTIVITIES", 30, "dumpsys", "activity", "all", NULL);
}
故事版解析:
- 收集员先填写 "案件基本信息表":系统版本、编译时间等
- 查看 "系统运行时间表"(uptime),了解系统已经运行了多久
- 进入 "内存仓库"(/proc/meminfo),记录内存使用情况
- 到 "CPU 调度室"(top 命令),查看各个进程的 CPU 占用
- 前往 "内核日志档案馆"(do_dmesg),调取内核启动以来的所有日志
- 打开 "系统日志数据库"(logcat),导出系统运行日志
- 检查 "虚拟机犯罪现场"(dump_traces),收集 ANR 和崩溃堆栈
- 走访 "崩溃日志墓地"(tombstone),查看近期的程序崩溃记录
- 到 "网络通信部"(ip 命令),记录网络连接状态
- 调查 "Binder 通信中心"(binder stats),了解进程间通信情况
- 最重要的环节:突击 "系统服务总部"(dumpsys),收集所有系统服务的状态
- 最后检查 "应用活动现场"(dumpsys activity),查看哪些应用在运行
四、关键工具:收集员的取证装备
1. run_command:派遣手下执行任务
cpp
运行
int run_command(const char *title, int timeout_seconds, const char *command, ...) {
// fork子进程执行命令
pid_t pid = fork();
if (pid == 0) { // 子进程(手下)执行
prctl(PR_SET_PDEATHSIG, SIGKILL); // 确保自己能被杀死
struct sigaction sigact; memset(&sigact, 0, sizeof(sigact));
sigact.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sigact, NULL); // 忽略中断
// 组装命令参数
const char *args[1024] = {command};
va_list ap; va_start(ap, command);
for (size_t arg = 1; arg < 1024; ++arg) {
args[arg] = va_arg(ap, const char *);
if (args[arg] == NULL) break;
}
// 执行命令
execvp(command, (char**)args);
_exit(-1); // 执行失败就退出
} else { // 父进程(收集员)等待
int status;
bool ret = waitpid_with_timeout(pid, timeout_seconds, &status);
if (!ret) { // 超时就杀死子进程
kill(pid, SIGTERM);
if (!waitpid_with_timeout(pid, 5, NULL)) kill(pid, SIGKILL);
}
}
}
故事版解析:
- 收集员派手下(子进程)去执行具体任务(如 top、dumpsys)
- 给手下戴上 "自杀手环"(PR_SET_PDEATHSIG),任务完成或超时就自动结束
- 手下组装好任务清单(命令参数),然后出发执行(execvp)
- 收集员在原地等待,超时就发信号让手下回来,不听话就强行召回(SIGKILL)
2. dump_file:读取文件线索
cpp
运行
int dump_file(const char *title, const char *path) {
// 打开文件
int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) { // 打不开就记录错误
if (title) printf("------ %s (%s) ------\n", title, path);
printf("*** %s: %s\n", path, strerror(errno));
return -1;
}
// 读取文件内容并输出
return _dump_file_from_fd(title, path, fd);
}
static int _dump_file_from_fd(const char *title, const char *path, int fd) {
if (title) printf("------ %s (%s", title, path);
// 如果是/proc或/sys文件,显示修改时间
if (memcmp(path, "/proc/", 6) && memcmp(path, "/sys/", 5) && !fstat(fd, &st)) {
char stamp[80];
strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", localtime(&st.st_mtime));
printf(": %s", stamp);
}
printf(") ------\n");
// 读取文件内容,30秒没数据就超时
fd_set read_set; struct timeval tm = {30, 0};
while (1) {
FD_ZERO(&read_set); FD_SET(fd, &read_set);
int ret = select(fd + 1, &read_set, NULL, NULL, &tm);
if (ret <= 0) break; // 超时或错误
char buffer[65536];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read > 0) fwrite(buffer, bytes_read, 1, stdout);
}
close(fd);
}
故事版解析:
- 收集员来到文件柜(path)前,尝试打开抽屉(open 文件)
- 如果抽屉锁了(权限问题),就记录 "无法打开抽屉:原因..."
- 如果打开了,就查看抽屉标签(title),并标注最后一次整理时间(修改时间)
- 开始翻阅抽屉里的文件,每 30 秒没翻到新内容就停止
3. dump_traces:收集堆栈犯罪证据
cpp
运行
const char *dump_traces() {
// 先保护上次的ANR日志
property_get("dalvik.vm.stack-trace-file", traces_path, "");
strlcpy(anr_traces_path, traces_path, sizeof(anr_traces_path));
strlcat(anr_traces_path, ".anr", sizeof(anr_traces_path));
rename(traces_path, anr_traces_path);
// 创建新的日志文件
int fd = open(traces_path, O_CREAT | O_WRONLY | O_TRUNC, 0666);
// 遍历所有进程,收集堆栈
DIR *proc = opendir("/proc");
while ((d = readdir(proc))) {
int pid = atoi(d->d_name);
if (pid <= 0) continue;
// 读取进程信息
char path[PATH_MAX];
readlink("/proc/%d/exe", pid, data, sizeof(data)-1);
// 如果是Java进程(app_process),发送SIGQUIT信号让它dump堆栈
if (!strncmp(data, "/system/bin/app_process", ...)) {
if (!strncmp(data, "zygote", ...)) continue; // 跳过zygote
kill(pid, SIGQUIT); // 发送信号,让Java进程dump堆栈
// 等待5秒,接收dump结果
struct pollfd pfd = { ifd, POLLIN, 0 };
poll(&pfd, 1, 5000);
} else if (should_dump_native_traces(data)) {
// 本地进程用debuggerd dump堆栈,超时20秒
dump_backtrace_to_file_timeout(pid, fd, 20);
}
}
// 重命名日志文件,保存本次收集结果
strlcpy(dump_traces_path, traces_path, sizeof(dump_traces_path));
strlcat(dump_traces_path, ".bugreport", sizeof(dump_traces_path));
rename(traces_path, dump_traces_path);
// 恢复上次的ANR日志
rename(anr_traces_path, traces_path);
return dump_traces_path;
}
故事版解析:
-
收集员先把上次的 ANR 日志(老证据)打包藏好(.anr 后缀)
-
拿出新的笔记本(traces.txt)准备记录新证据
-
走进 "进程大厦"(/proc 目录),逐个房间(进程)检查:
- 看到 Java 程序的房间(app_process),就敲敲门(SIGQUIT 信号),让里面的程序把堆栈记录下来
- 看到本地程序的房间,就找 debuggerd 帮忙,花 20 秒时间收集堆栈
-
收集完后,把新证据笔记本重命名(.bugreport)保存
-
最后把老证据笔记本放回原处(恢复 traces.txt)
五、案件总结:bugreport 的证据清单
通过以上步骤,bugreport 最终会生成一份包含五大类证据的报告:
-
当前日志:内核日志、系统日志、事件日志、无线电日志
-
历史日志:上次内核崩溃日志、上次系统日志
-
虚拟机线索:当前堆栈、上次 ANR 堆栈、崩溃墓碑日志
-
系统服务状态:所有系统服务 dump、电池 / 内存 / 网络统计
-
应用现场:正在运行的 Activity、Service、Provider
每类证据都以 ------ 证据类别 ------ 开头,就像侦探在报告里分章节记录线索,方便后续分析。
六、侦探事务所的工作流程总结
-
指挥官
bugreport启动收集员dumpstate,建立通信 -
收集员做好准备工作,提高优先级,获取必要权限
-
收集员遍历系统各角落,使用不同工具收集各类证据
-
收集员将证据通过 socket 传给指挥官,指挥官记录到文件
-
最终生成包含系统全面信息的 bugreport,为分析 bug 提供依据
通过这个故事,我们可以把复杂的 bugreport 源码流程理解为侦探事务所的一次案件调查,每个代码模块都是侦探的不同工具和步骤,最终目的是收集足够的线索来解决系统问题。