大概两个月前,我在 Linux ARM 设备上排查一起 Chromium 的偶发崩溃。现象不算复杂:硬件解码播放视频时,用户把页面拖到一半突然关闭标签页,大约每二三十次操作就有一次闪退,崩溃线程不在主线程,而在一个名为 hw_decoder 的专用线程上。第一次抓到的栈回溯指向 PC 值为 0,指针完全无效,我一度怀疑是 Rockchip 平台的驱动或用户态封装又出了什么幺蛾子。查了两天后才明白,这场崩溃跟硬件一毛钱关系没有,问题出在异步回调把对象的生命周期算错了一张多米诺骨牌。这次修复本身改动不到十行,但排查过程把 Chromium 异步编程里几个很容易被忽视的规则全部趟了一遍。这篇博文就把完整链路写下来,给同样在 Chromium 或类似 C++ 异步架构里做开发的同行做个参考。
1. 崩溃现场:一个每二三十次必现的闪退
1.1 复现路径与崩溃特征
先说环境:设备是 RK3588 的 Linux ARM 板子,Chromium 启用 V4L2 硬件视频解码,播放的是 H.264 1080p 视频。复现路径非常明确:
- 打开一个视频播放页面,等待首帧渲染完成;
- 快速拖动进度条两三次,让解码器连续进入 decode 流程;
- 在最后一次 seek 后 500ms 内直接关闭标签页;
- 大约每二三十次操作,浏览器进程就会闪退。
崩溃特征也很有意思。崩溃线程不是浏览器主线程,而是硬件解码线程 hw_decoder。抓到的栈回溯长这样:
text复制Thread 8 (hw_decoder_thread):
#0 0x0000007f9c12ab34 in media::RockchipVideoDecoder::DecodeOnHardwareThread()
#1 0x0000007f9c12cd58 in base::internal::Invoker<base::OnceCallback<void ()>>
#2 0x0000007f9c12e9ef in base::TaskAnnotator::RunTask()
#3 0x0000007f9c12aabc in base::Thread::Run()
#4 0x0000007f9c12bf90 in base::MessagePump::Run()
#5 0x0000007f9c12c1a7 in base::RunLoop::Run()
注意 #0 这一帧,函数还在,但 PC 指向的是一个完全无效的地址,说明当前代码执行的上下文里 this 指针已经指向了被释放的内存。这种崩溃在 release 版本里往往表现为“莫名其妙的空指针”“野指针”“内存踩踏”,如果没有 ASAN 或者 minidump 辅助,很容易被误判成驱动问题。
1.2 第一轮排查:假线索反而最像真的
遇到这种硬件相关崩溃,第一反应自然是怀疑驱动。我们当时的排查顺序是这样的:
- 关闭硬件解码,改用 FFmpeg 软解,同样的操作反复跑了上百次,一次都没崩。这几乎坐实了“硬件解码路径有问题”的判断;
- 查看内核日志,
dmesg里有几条来自媒体驱动模块的报错,但都是解码错误码,没有 Oops,没有内存访问冲突记录; - 怀疑是 V4L2 buffer 在 close 时与解码线程竞争,于是往驱动的
close路径里加了一堆打印和延迟,没有改变崩溃频率; - 怀疑是用户态 buffer 池在解码线程上被并发回收,把
VideoFramePool相关的锁全部查了一遍,也没找到明显的竞态条件。
这一轮下来基本可以确定:问题大概率不在内核态,而在 Chromium 自己的异步调用链里。真正有价值的线索是崩溃线程固定是 hw_decoder,而触发时机固定在“请求还在解码队列里时对象被销毁”。这已经把方向指向了异步生命周期管理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 根因定位:回调触发时对象已经析构
2.1 用 ASAN 抓出分配的出生地与释放现场
Release 版栈回溯只能告诉你在哪崩的,很难告诉你对象是谁在什么时候释放的。这时候必须上 ASAN 构建。ASAN 的好处是能在崩溃第一时间同时打印出“这块内存的分配栈”和“释放栈”,等于把遗属关系一次性交代清楚。
我们
