29、常见问题与调试:Kernel Panic 分析、Tombstone 解读、Logcat 高级用法

做嵌入式Android系统开发,说白了就是跟各种崩溃打交道。你想想看,从底层内核到上层应用,任何一个环节出问题,轻则闪退,重则直接黑屏重启。我这些年踩过的坑,十个有八个都跟调试有关。今天咱们就把最核心的三个调试手段聊透:Kernel Panic、Tombstone 和 Logcat 高级用法。

核心观点:调试不是玄学,是方法论。掌握这三板斧,90%的疑难杂症都能找到根因。

嵌入式Android调试三板斧 Kernel Panic Tombstone Logcat高级用法 Oops信息 Call Trace 信号信息 寄存器状态 缓冲区过滤 日志级别 内核级崩溃 → 进程级崩溃 → 应用级日志 逐层深入,定位根因

一、Kernel Panic 分析:内核崩溃的现场还原

Kernel Panic 是嵌入式系统里最严重的错误。一旦发生,系统直接挂掉,连个招呼都不打。我刚开始做驱动开发时,遇到 Panic 就慌,后来发现其实它留下的信息非常丰富。

Panic 发生时,内核会打印出一堆信息,关键看这几个地方:

  • Oops 信息:告诉你哪个模块、哪个地址出错了。比如 "Unable to handle kernel NULL pointer dereference at virtual address 0x00000008"——这基本就是空指针解引用了。
  • Call Trace(调用栈):这是最值钱的部分。从下往上看,找到第一个属于你驱动或模块的函数,那就是问题所在。
  • CPU 寄存器状态:PC(程序计数器)指向哪条指令,LR(链接寄存器)保存了哪个返回地址,这些都能帮你定位。

我的习惯:每次拿到 Panic 日志,先搜 "PC is at" 或 "LR is at",然后对照 System.map 或 addr2line 工具转成源码行号。这比瞎猜快十倍。

举个例子,我曾经遇到一个 Wi-Fi 驱动导致的 Panic:

[   45.123456] Unable to handle kernel paging request at virtual address ffffff8000123456
[   45.123457] pgd = ffffffc0006b0000
[   45.123458] [ffffff8000123456] *pgd=0000000000000000
[   45.123459] Internal error: Oops: 96000004 [#1] PREEMPT SMP
[   45.123460] CPU: 1 PID: 1234 Comm: wpa_supplicant Tainted: G        W
[   45.123461] PC is at wlan_tx_packet+0x24/0x100 [wlan_driver]
[   45.123462] LR is at wlan_tx_worker+0x48/0x80 [wlan_driver]

看到 "PC is at wlan_tx_packet+0x24/0x100" 了吗?这表示崩溃发生在 wlan_tx_packet 函数偏移 0x24 的位置,函数总长度 0x100。用 addr2line 一转换,直接定位到源码第 87 行——一个未检查的 skb 指针。

注意:Kernel Panic 日志默认只保留在 RAM Console 里,重启后就没了。一定要在启动参数里加上 console=ram 或者通过 pstore/ramoops 保留现场。

二、Tombstone 解读:Native 进程的死亡档案

Tombstone 是 Android 为 Native 进程(C/C++ 写的进程)准备的"死亡档案"。每次 Native 进程崩溃,系统都会在 /data/tombstones/ 目录下生成一个文件,里面记录了崩溃时的完整上下文。

我个人觉得,Tombstone 比 Kernel Panic 更友好,因为它已经帮你解析好了。打开一个 Tombstone 文件,重点看这几块:

字段 含义 怎么看
Build fingerprint 系统版本信息 确认是不是你编译的版本
pid / tid 进程ID / 线程ID 确认是哪个进程挂了
signal 导致崩溃的信号 SIGSEGV(11) 是段错误,SIGABRT(6) 是主动 abort
abort message 主动打印的错误信息 很多库会在这里写具体原因
backtrace 调用栈 从下往上找,最上面的就是崩溃点
memory map 内存映射 看崩溃地址落在哪个库的哪个段

我曾经遇到一个 SurfaceFlinger 的 Tombstone,backtrace 指向了 BufferQueue::dequeueBuffer。一看 memory map,发现对应的 so 文件大小异常——原来是编译时优化等级开太高,导致某些变量被优化掉了。降级优化后问题解决。

避坑指南:我曾经在调试一个多媒体播放器崩溃时,连续看了 5 个 Tombstone 都没找到规律。后来发现每次崩溃的 backtrace 都不一样,但 signal 都是 SIGSEGV。最后用 AddressSanitizer 重新编译,才发现是堆内存越界写入。所以,如果 Tombstone 信息混乱,试试用 ASAN 或 HWASAN 重新编译。

三、Logcat 高级用法:不只是看日志

Logcat 是 Android 调试的瑞士军刀。但很多人只会 logcat -s TAG,其实它还有很多高级玩法。

3.1 缓冲区与日志级别

Logcat 有四个缓冲区:main(应用日志)、system(系统日志)、events(事件日志)、crash(崩溃日志)。默认只输出 main 和 system。如果你在调试底层问题,记得加上 -b all

# 查看所有缓冲区
adb logcat -b all

# 只看崩溃日志
adb logcat -b crash

# 只看事件日志(比如 ANR、启动完成等)
adb logcat -b events

日志级别从低到高:V(Verbose)、D(Debug)、I(Info)、W(Warning)、E(Error)、F(Fatal)。我习惯用 *:W 过滤掉低级别日志:

# 只显示 Warning 及以上级别
adb logcat *:W

# 只看某个 TAG 的 Error 级别
adb logcat -s MyTAG:E

3.2 时间戳与线程信息

调试时序相关问题时,时间戳是救命稻草:

# 显示精确到毫秒的时间戳
adb logcat -v time

# 显示线程ID(多线程调试必备)
adb logcat -v threadtime

# 组合使用
adb logcat -v threadtime -b all *:W

3.3 日志过滤与正则

当日志量特别大时,用 grep 配合正则效率最高:

# 过滤包含特定关键词的日志
adb logcat | grep -E "error|fatal|crash"

# 排除噪音(比如 SurfaceFlinger 的 VSYNC 日志)
adb logcat | grep -v "VSYNC"

# 使用 logcat 自带的过滤语法
adb logcat -s "MyTAG:*" "OtherTAG:W"

我的小技巧:在开发阶段,我会在关键函数入口和出口加 Log.d,并带上函数名和行号。比如 Log.d(TAG, "enter: " + methodName + "() line:" + lineNumber)。这样出问题时,看日志就能知道代码执行到哪一步了。

3.4 日志持久化与抓取

有些问题在连接 USB 时不会复现,断开后就出现。这时候需要把日志写到文件里:

# 后台持续抓取日志到文件
adb logcat -v threadtime -f /data/local/tmp/logcat.txt &

# 或者用 logcat 的 -r 参数做轮转
adb logcat -v threadtime -r 1024 -n 5 -f /sdcard/logcat.txt

-r 1024 表示每个文件 1024KB,-n 5 表示保留 5 个历史文件。这样就不会因为日志太大撑爆存储。

注意:生产环境不要开 Verbose 级别的日志,会严重影响性能。我见过一个产品因为 Log.v 太多,导致 UI 掉帧严重。上线前记得把调试日志关掉或降到 Info 级别。

四、实战:一次完整的调试流程

最后,我分享一个真实案例。某次系统在播放视频 10 分钟后必重启,现象非常稳定。

  1. 第一步:看 Logcat。用 adb logcat -b crash -v threadtime 抓到崩溃日志,发现是 mediaserver 进程挂了,signal 是 SIGSEGV。
  2. 第二步:看 Tombstone。去 /data/tombstones/ 找到对应的文件,backtrace 指向了 OMXCodec::read 函数。
  3. 第三步:加日志复现。在 read 函数的关键路径加 Log.d,重新编译烧录。再次复现后,日志显示在某个特定码流下,buffer 指针变成了 NULL。
  4. 第四步:查 Kernel 日志。用 dmesg 看内核日志,发现有个 "Out of memory" 的警告——原来是内存碎片导致大块分配失败。
  5. 第五步:根因定位。最终发现是视频解码器在长时间运行后,没有释放某些临时 buffer,导致内存碎片化。修掉内存泄漏后,问题解决。

你看,整个流程就是从上到下、从应用到内核,逐层排查。没有一步是多余的。

总结一下:Kernel Panic 看内核栈,Tombstone 看进程栈,Logcat 看应用日志。三者配合,没有查不出的崩溃。记住,调试不是靠运气,是靠方法。


公众号:蓝海资料掘金营,微信deep3321