日志与调试技巧:QNX slog2与syslog、Android logcat与dmesg、JTAG调试与Trace32、常见驱动崩溃问题定位
做驱动开发,说白了就是跟硬件和内核打交道。你写的代码跑在底层,一旦出问题,没有图形界面给你弹错误框,也没有断点让你慢慢看。这时候,日志和调试工具就是你的眼睛和手。
我个人习惯把调试手段分成三个层次:日志输出、内核转储、硬件调试器。这三个层次一个比一个深入,一个比一个麻烦。但真正的高手,往往在第一个层次就能解决80%的问题。
核心观点:日志打得好,调试少一半。别等到崩溃了才想着加打印,设计阶段就要规划好日志策略。
一、QNX 下的日志体系:slog2 与 syslog
QNX 是个实时操作系统,它的日志系统跟 Linux 不太一样。我刚开始接触 QNX 时,第一反应是去找 dmesg,结果发现根本没有。嗯,这里要注意,QNX 有自己的两套日志机制。
1.1 syslog:轻量级系统日志
syslog 是 POSIX 标准接口,QNX 也支持。它适合记录一些简单的文本信息,比如驱动初始化成功、某个配置加载失败等。
#include <syslog.h>
void driver_init(void) {
openlog("my_gpio_driver", LOG_PID | LOG_CONS, LOG_DRIVER);
syslog(LOG_INFO, "GPIO driver initializing...");
if (hw_register_failed) {
syslog(LOG_ERR, "Failed to register hardware: %s", strerror(errno));
closelog();
return -1;
}
syslog(LOG_DEBUG, "GPIO base address: 0x%08x", base_addr);
closelog();
}
我在项目中遇到过一个问题:用 syslog 打印太多调试信息,结果导致系统响应变慢。为什么?因为 syslog 默认是同步写入的,每次调用都会触发 I/O 操作。所以我的建议是:生产环境只保留 ERROR 和 WARNING 级别,DEBUG 级别只在开发阶段开启。
1.2 slog2:高性能二进制日志
slog2 是 QNX 特有的日志系统,专门为嵌入式实时场景设计。它用二进制格式存储,写入速度极快,而且支持按缓冲区大小循环覆盖。
#include <sys/slog2.h>
int main(void) {
slog2_buffer_t buffer;
slog2_buffer_config_t config;
config.buffer_name = "my_driver";
config.num_buffers = 2;
config.buffer_size = 8192; // 每个缓冲区8KB
config.verbosity_level = SLOG2_DEBUG1;
if (slog2_register(&config, &buffer) != 0) {
fprintf(stderr, "Failed to register slog2 buffer\n");
return -1;
}
slog2c(&buffer, 0, SLOG2_DEBUG1, "Driver loaded at address 0x%08x", addr);
slog2c(&buffer, 0, SLOG2_ERROR, "DMA transfer failed: %d", err_code);
return 0;
}
我的经验:在 QNX 上调试中断处理函数时,千万别用 syslog。中断上下文里调用它可能导致死锁。用 slog2 就安全得多,它不会阻塞。
查看 slog2 日志用 slog2info 命令:
# 实时查看所有日志
slog2info -w
# 只查看某个buffer的日志
slog2info -b my_driver
# 按级别过滤
slog2info -v DEBUG1
二、Android Automotive 的日志:logcat 与 dmesg
Android Automotive 本质上还是 Linux 内核 + Android 框架。所以它的日志体系也分两层:内核层用 dmesg,用户层用 logcat。
2.1 logcat:应用与HAL层的日志
驱动开发中,HAL(硬件抽象层)的日志通常通过 logcat 输出。这里有个坑:很多人直接用 printf,但在 Android 上,printf 不会显示在 logcat 里。
#include <log/log.h>
void hal_sensor_read(void) {
ALOGI("Sensor HAL: starting read operation");
if (ioctl(fd, SENSOR_IOCTL_READ, &data) < 0) {
ALOGE("Sensor HAL: ioctl failed: %s", strerror(errno));
return -1;
}
ALOGV("Sensor HAL: read %d bytes", data.size);
}
查看 logcat 的常用技巧:
# 只显示某个tag的日志
logcat -s SensorHAL
# 显示详细的时间和进程信息
logcat -v threadtime
# 清空旧日志,只看新的
logcat -c && logcat
注意:在 Android Automotive 上,有些系统服务会吃掉 stderr 输出。我曾经花了一整天找 bug,最后发现是日志被重定向到了 /dev/null。所以,请务必使用 ALOG* 宏,不要用 fprintf(stderr, ...)。
2.2 dmesg:内核驱动的日志
内核驱动用 printk 输出日志,通过 dmesg 查看。这里有个级别控制的问题:
// 内核驱动中
printk(KERN_ERR "i2c: transfer failed, addr=0x%02x\n", addr);
printk(KERN_DEBUG "i2c: register value = 0x%08x\n", reg_val);
// 用户空间查看
dmesg -w // 实时查看
dmesg -l err // 只看错误级别
dmesg | grep i2c // 过滤关键字
我建议内核驱动中至少打三类日志:
- 初始化阶段:打印硬件版本、寄存器基地址、中断号
- 错误路径:所有返回 -EIO、-ETIMEDOUT 的地方都要打日志
- 关键状态变化:比如电源状态切换、DMA 完成中断
三、JTAG 调试与 Trace32
当日志解决不了问题时,就该上硬件调试器了。JTAG 调试,说白了就是通过调试接口直接控制 CPU 的执行。
我常用的场景是:
- 系统启动阶段,日志系统还没初始化
- 中断处理函数中,不能加打印
- 内存越界导致系统随机崩溃
3.1 Trace32 的基本操作
Trace32 是 Lauterbach 公司的调试工具,在汽车电子领域用得非常多。它的界面虽然古老,但功能极其强大。
// Trace32 常用命令示例
// 设置断点
Break.Set 0x80001234
// 查看寄存器
Register.view
Register.Set R0 0x1000
// 查看内存
Data.dump 0xA0000000 /Quad
// 单步执行
Step.Over
Step.Into
// 查看函数调用栈
Frame.view
我的经验:Trace32 最强大的功能是 ETM(嵌入式跟踪宏单元)。它可以记录 CPU 执行的每一条指令,然后回放给你看。我曾经用它定位过一个极其隐蔽的竞态条件——两个线程在 10 微秒的时间窗口内同时访问了同一个寄存器。
3.2 JTAG 调试的常见技巧
用 JTAG 调试驱动时,我一般按这个流程走:
- 先确认 CPU 是否活着:查看 PC 寄存器和堆栈指针
- 检查中断控制器:看中断是否被屏蔽,pending 位是否置起
- 查看关键寄存器:比如 DMA 状态寄存器、FIFO 水位线
- 分析堆栈回溯:看崩溃时 CPU 在哪个函数里
这里有个坑:有些 SoC 的 JTAG 接口在低功耗模式下会被关闭。我遇到过好几次,系统进入休眠后 JTAG 就断开了,怎么都连不上。后来发现需要在 PMIC 里单独给调试接口供电。
四、常见驱动崩溃问题定位
驱动崩溃,说白了就是内核态出了异常。常见的就那么几种,我列个表:
| 崩溃类型 | 典型症状 | 定位方法 |
|---|---|---|
| 空指针解引用 | 访问地址 0x0 或接近 0x0 | 检查指针是否初始化,加 NULL 判断 |
| 内存越界 | 随机崩溃,堆栈被破坏 | 开启 KASAN 或 slub debug |
| 死锁 | 系统卡死,CPU 占用 100% | 查看 /proc/lock_stat,用 Trace32 看等待队列 |
| 中断风暴 | 系统响应极慢,dmesg 刷屏 | 检查中断触发方式,加中断次数统计 |
| DMA 超时 | 数据传输卡住,看门狗复位 | 检查 DMA 描述符链,确认中断是否到达 |
4.1 空指针解引用
这是最常见的崩溃原因。我见过最离谱的一次,是某个驱动在 probe 函数里先调用了 devm_kzalloc,然后紧接着就用了这个指针——但中间有个条件分支提前 return 了,导致指针没分配成功。
// 错误写法
struct my_dev *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
// 这里应该检查 dev 是否为 NULL
dev->reg_base = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 如果 dev 是 NULL,这里就崩溃了
// 正确写法
struct my_dev *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) {
dev_err(&pdev->dev, "Failed to allocate memory\n");
return -ENOMEM;
}
dev->reg_base = platform_get_resource(pdev, IORESOURCE_MEM, 0);
4.2 内存越界
内存越界是最难定位的问题之一。因为它可能在一个地方写坏了内存,却在另一个完全不相关的地方崩溃。
我曾经遇到过一个 case:一个 SPI 驱动在发送数据时,缓冲区大小计算错了,多写了 4 个字节。这 4 个字节恰好覆盖了相邻结构体的一个函数指针。结果系统在调用这个函数指针时跳到了一个非法地址,直接 panic。
定位这种问题,我的建议是:
- 开启内核的
CONFIG_KASAN或CONFIG_SLUB_DEBUG - 用
kmemleak检查内存泄漏 - 在怀疑的缓冲区前后加
canary(哨兵值),崩溃时检查是否被改写
4.3 死锁
死锁的典型症状是系统卡死,但 CPU 还在跑。用 top 看,某个进程的 CPU 占用率是 100%。
定位死锁的步骤:
- 用
cat /proc/lock_stat查看锁的统计信息 - 用
echo t > /proc/sysrq-trigger打印所有线程的堆栈 - 看堆栈里哪些线程在等锁,哪些线程持有锁
警告:在 QNX 上,死锁可能导致整个系统重启,因为 QNX 的进程管理器会检测到某个进程长时间不响应。我曾经在调试一个音频驱动时,不小心在中断处理函数里调用了 pthread_mutex_lock,结果系统每隔 3 秒就重启一次。嗯,这个教训很深刻。
五、知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作调试时的路线图:
调试驱动,说白了就是跟不确定性做斗争。日志是你的第一道防线,JTAG 是你的最后手段。但不管用哪种方法,思路清晰比工具强大更重要。
我个人习惯在写驱动代码之前,先想清楚三个问题:
- 如果这里出错了,我能不能从日志里看出来?
- 如果系统崩溃了,我能不能从堆栈里找到原因?
- 如果硬件挂了,我有没有办法通过调试器确认状态?
想清楚这三个问题,你的驱动就已经成功了一半。