这篇写什么?
一类很折磨人的问题:一开 ACR(Echo Reference / 内容识别参考通路)相关宏,audio 服务就挂——有时是接 Soundbar / 切 HDMI-IN,有时是开机播 YouTube。墓碑里常见:
Scudo ERROR: corrupted chunk header- 或 ASan 在
malloc/free时直接SIGABRT
表面像「MS12 和 DSPC 抢内存」,真正根因往往更朴素:某次 16bit→32bit 转换把字节数传大了一倍,堆被写烂,崩点落在别的 free/malloc 上。
一句话:
消融实验 → 只有 ENABLE_ACR_APP 必现
→ 墓碑在 close/free 或 MS12 malloc(堆已坏)
→ 假读仍崩,关掉 apply_volume_16to32 不崩
→ 应传 bytes/2,却传了 bytes → 越界踩堆

相关:ACR:LoopbackB 从 TDM-A 取 48 kHz Ref、ALSA period 与缓冲。
一、现象:两类复现,同一类挂死
场景 A:Zoom + 语音键 + HDMI-IN
双屏电视在 Zoom 会议中 多次连按遥控器语音键,再切到 HDMI-IN / dongle 播音乐,出现 audioserver 挂死。
场景 B:开了 ACR 就「开机播 YouTube 挂」
后来改成 ACR 分离声卡 / TDM-A 策略后,旧复现路径弱了,但只要打开 ACR,仍可能在 开机播 YouTube 等路径挂死——说明根因仍在 ACR 读通路,而不只是「和 MS12 同生命周期切换」。
HAL 侧常同时存在三块宏(名称因项目略有差异):
| 宏 | 大致职责 |
|---|---|
ENABLE_DSPC_AEC |
DSPC 回声消除一类 |
ENABLE_ACR_APP |
ACR / Echo Reference 采集与处理 |
ENABLE_DSPC_EQ |
DSPC EQ 等 |
要用 消融实验 先锁模块,再抠代码。
二、消融实验:锁死是 ACR
统一在「Zoom 连按语音键 → HDMI-IN 播音乐」场景下测:
| # | AEC | ACR | EQ | 墓碑 | 声音 | 备注 |
|---|---|---|---|---|---|---|
| 1 | off | off | off | 无 | 有 | 基线正常 |
| 2 | off | off | on | 无 | 有 | 音量键有「连加/连减」副作用,但未挂死 |
| 3 | off | on | on | 有 | 有 | 开始出墓碑 |
| 4 | on | off | on | 无 | 有 | AEC+EQ 未单独复现挂死 |
| 5 | off | on | off | 有/无 均见过 | 无 | 开 ACR 即异常 |
| 6 | on | on | on | 有 | 无 | 全开最差 |
结论很清楚:
只要打开
ENABLE_ACR_APP,就容易出问题;关 ACR 后同类操作可稳住。
三、初期误判:像「MS12 被 DSPC 踩内存」
接 Soundbar 时,上层 new device → AudioPolicy 开新 output → adev_open_output_stream_new → 创建 / 释放 MS12。挂死点曾落在:
get_dolby_ms12_cleanup附近- 或 MS12 内部再次
malloc时 ASan 直接炸
DSPC EQ / Talk 等可能各占几十 MB 量级,切源时又在释放 MS12,再加上 ACR 使用额外 FIFO / 临时缓冲——很容易怀疑是 堆布局冲突。
因此做过一版 分离路径 方案(另开声卡、TDM-A 读写、少跟系统音量搅在一起),意图躲开 MS12↔DSPC 同堆撕扯。这对「切换设备时立刻崩」有帮助,但 开 ACR 仍崩,说明还有更直接的写越界。
四、墓碑在说什么:崩点 ≠ 根因
分离策略之后,tombstone 常落在 关输出流 free 临时缓冲:
Scudo ERROR: corrupted chunk header at address 0x...
...
#n adev_close_output_stream → aml_audio_free(audioeffect_tmp_buffer)

调用链经 adev_close_output_stream_new → adev_close_output_stream:

真正 free 的位置:

更早一版 ASan 则在 MS12 malloc 时 abort——同一类「堆元数据已坏」:

要点:
| 观察 | 含义 |
|---|---|
崩在 free / malloc |
堆元数据早被踩坏 |
| 崩在 effect buffer / MS12 | 只是「第一个发现坏掉的人」 |
| 必须回溯 谁先越界写 | 不能只改 close 路径 |
五、往回追:谁踩了 audioeffect_tmp_buffer?
在 malloc / free 前后打印缓冲地址、大小,以及指针前方 16 字节(chunk header 附近):


墓碑里对应地址附近 整段被写成 0——典型「被人用 memset/越界写抹掉了分配器头」:

同进程里,地址较近、且持续读写的,是 ACR 读线程(reader → in_read → aml_alsa_input_read):

于是给 ACR in_read / pcm_read 打字节数与 buffer 地址日志:

起初看「ALSA 读本身」并不一定像冲掉邻居——还要继续隔离。
六、关键实验:假读 + 开关转换函数
在 in_read(AUDIO_SOURCE_ECHO_REFERENCE)里 先不读 ALSA:
memset(buffer, 0x55, bytes); // 填固定图案
usleep(32000); // 约 32ms:12288 字节 @ 32bit 立体声 48k

结果:
| 操作 | 是否仍挂死 |
|---|---|
假读 + 仍调用 apply_volume_16to32(..., bytes) |
仍会 |
屏蔽 apply_volume_16to32 |
不挂 |
说明:锅在 16→32 音量/位宽转换,不在 pcm_read 本身。
再打日志看转换前后「字节数」:
ACR: Read data -- bytes requested: 12288 ...
ACR: Data after volume adjustment -- bytes: 12288

数字没变,容易误以为「参数没错」——真正要看的是:apply_volume_16to32 把最后一个参数 当成「16bit 侧字节数」 还是 「32bit 侧字节数」。
七、根因:bytes 与 bytes/2 差了一倍
ACR 路径典型写法(示意):
// 从声卡读 16bit 数据到临时缓冲,长度是 32bit 请求的一半
ret = aml_alsa_input_read(stream, adev->acrTmpbuffer, bytes / 2);
// 错误:最后一个参数传了 bytes
apply_volume_16to32(1.0, adev->acrTmpbuffer, buffer, bytes);
// 正确:应按 16bit 输入字节数传
apply_volume_16to32(1.0, adev->acrTmpbuffer, buffer, bytes / 2);

函数内部大致是:
void apply_volume_16to32(float volume, int16_t *in_buf, int32_t *out_buf, int bytes)
{
int16_t *input16 = (int16_t *)in_buf;
int32_t *output32 = (int32_t *)out_buf;
for (i = 0; i < bytes / sizeof(int16_t); i++) {
// 每个 16bit 样本 → 一个 32bit 样本
output32[i] = ...;
}
}

用数字算一遍(bytes = 12288)
| 量 | 正确理解 | 错误传 bytes 时 |
|---|---|---|
上层 in_read 请求 |
要填满 12288 字节的 32bit 缓冲 | 同左 |
| 16bit 输入应有字节数 | 6144(bytes/2) |
仍只有约 6144 有效数据 |
| 函数认为要处理的 16bit 样本数 | 6144/2 = 3072 |
12288/2 = 6144(多一倍) |
| 写出的 32bit 字节数 | 3072 × 4 = 12288(刚好) |
6144 × 4 = 24576(越界约一倍) |
结果:读穿 acrTmpbuffer,写穿 buffer,隔壁堆块(effect tmp、分配器 header、甚至别处)被踩成 0 或乱值 → 稍后在 aml_audio_free / MS12 malloc 爆掉。
这也解释了为何:
- 消融只跟 ACR 绑定
- 假读仍崩(转换还在)
- 关转换就不崩
- 崩点飘忽(close / malloc / Scudo / ASan)——经典 堆损坏延后显现
八、修复与自检
修复(一行语义):
apply_volume_16to32(1.0, adev->acrTmpbuffer, buffer, bytes / 2);
保证:
16bit 输入字节数 == 传给 apply_volume_16to32 的 bytes 参数
32bit 输出字节数 == 上层 in_read 的 bytes(约为输入的 2 倍)
自检清单
-
aml_alsa_input_read(..., bytes/2)与apply_volume_16to32(..., bytes/2)对齐 - dump 16bit / 32bit raw 时,长度分别用
bytes/2与bytes - 开 ASan / 看 Scudo,回归:Zoom 语音键风暴、HDMI-IN、开机播流媒体
- 别被「崩在 MS12/free」带走——先搜最近的 位宽转换 / memcpy 长度
九、小结
| 阶段 | 结论 |
|---|---|
| 消融 | 问题绑定 ENABLE_ACR_APP |
| 表象 | MS12 cleanup / malloc、audioeffect_tmp_buffer free 挂死 |
| 本质 | ACR in_read 里 apply_volume_16to32 多传了一倍字节数 |
| 修复 | 第三个长度参数改为 bytes/2 |
堆损坏类 bug 的教学意义:tombstone 指向的 free,常常只是受害者;真正的凶手是更早的一次越界写。 用假读 + 开关嫌疑函数,比反复猜「模块互踩」更快。
相关阅读
评论
还没有评论,来做第一个吧
赞同、吐槽、提问都行,评论区开放