开篇:先别慌,从日志里找线索

「录像没有声音」这类问题,第一反应往往是恐慌式排查:是不是麦克风坏了?是不是权限没给?是不是某个配置项写错了?

但经验告诉我们:先别慌,把日志抓下来,让日志告诉你问题在哪。

这次的整个过程,其实就是顺着一行日志一路追下去:

HiRecorder: (Prepare(), 405): A333 camera recorder disables audio path
    for video-only raw NV12 recording experiment.

就这一行,把根因的性质、位置、甚至具体代码行号都标出来了。后面所有的定位、反汇编、改二进制,都是围绕它展开的。

几个可以先记下来的经验:

  1. 日志是最诚实的。 应用层看着一切正常,不代表系统层没问题 —— 真正的开关往往藏在系统引擎里,而它出错时通常会留下一句话。
  2. 先缩小范围,再深入。 从 tag(C02b2c/HiRecorder)和进程(media_service)就能锁定是哪个库、哪个进程,不必从最底层一层层扒。
  3. 别急着改代码。 先确认「问题出在哪一步、为什么出」,再动手 —— 这次如果凭感觉去改应用层,改再多也无济于事。
  4. 没有源码不等于没法修。 本机没有系统源码,但通过 ELF 解析 + 反汇编,照样定位到了具体分支,只改了 4 个字节就修好。

下面就是完整的排查与修复过程。


起因

某自研设备上使用相机录像时,录出的视频没有声音。

排查的第一站是应用层 —— CameraService 中 AVRecorder 的配置是完整的(AAC 编码、48kHz、双声道),麦克风权限声明和运行时授权也正常。应用这边没问题,转向系统媒体层。


发现关键线索

给真机抓取了 hilog,媒体服务进程(media_service)的日志里出现了这样一行:

HiRecorder: (Prepare(), 405): A333 camera recorder disables audio path for video-only raw NV12 recording experiment.

带 A333 前缀、又写着「video-only raw NV12 recording」—— 这一行,直接把根因的性质定死:这是系统媒体引擎里的厂商实验补丁,在录像准备阶段主动把音频通道关掉了。

而拉取录制文件后确认 mp4 里没有 soun 音轨,佐证了「只录了视频」。音频链路不是坏的,是被显式杀掉的。


第一步:确定问题在哪个 .so 里

运行日志里那行警告的 tag 是 C02b2c/HiRecorder,进程 pid 是 660,对应 media_service。所以在设备上:

hdc shell "cat /proc/660/maps"

拿到 media_service 加载的所有 .so 列表,筛出媒体相关的几个候选(libmedia_engine_histreamer.z.so、libmedia_service.z.so 等),逐个拉到电脑上:

hdc file recv /system/lib64/media/libmedia_engine_histreamer.z.so ./xxx.so

然后本机 grep -c "A333" —— 命中的是 libmedia_engine_histreamer.z.so(设备端 grep 对二进制会漏检,必须拉回本机查)。


第二步:解析 ELF,拿到“地图”

用 pyelftools 解析这个文件,得到三样东西:

  1. 符号表(.symtab 没被剥离,很幸运):找到 _ZN4OHOS5Media14HiRecorderImpl7PrepareEv,即 HiRecorderImpl::Prepare(),虚拟地址 0x66a10,大小 1408 字节 —— 这就是日志里打印 (Prepare(), 405) 的函数;
  2. .rodata 里的字符串地址:"A333 camera recorder disables audio path..." 位于 0x3ef0e;
  3. 段表(program headers):后面把虚拟地址换算成文件偏移要用。

第三步:用字符串地址反查引用它的代码

ARM64 每条指令固定 4 字节,不能把 64 位地址直接塞进一条指令,所以引用字符串都是两条指令配合:

adrp x4, #0x3e000     ; 取目标所在 4KB 页的起始地址
add  x4, x4, #0xf0e   ; 加上页内偏移 → x4 = 0x3ef0e(字符串地址)

我写了个扫描脚本(capstone 反汇编 + pyelftools 读字符串):线性扫 .text,遇到 adrp 就把「寄存器 → 页地址」记进字典,后面紧跟的 add 如果用了同一个寄存器,就算出最终地址去 .rodata 读字符串,含 "A333" 就报出来。结果在 Prepare() 内部 0x66b10 处找到了对 "disables audio path" 的引用 —— 警告日志的打印代码就在这个函数里。

顺便全库扫了一遍:A333 字符串共 7 处引用,其余 6 处在 SetVideoSource / PrepareVideoEncoder 等函数,全是 NV12 视频通路 —— 说明只有 Prepare() 这一处碰音频,动它不会影响视频。


第四步:反汇编 Prepare(),重建控制流

对 0x66a10 开始的 1408 字节做反汇编,关键片段(x19 保存的是 this 指针):

0x66af4: ldrb w8, [x19, #0x185]   ; 读成员变量:模式字节(A333 NV12 标志)
0x66af8: cmp  w8, #2
0x66afc: b.hs #0x66eb8            ; ≥2 → 非法值,报错返回
0x66b00: cbz  w8, #0x66d30        ; ★ ==0 → 直接跳到汇合点(正常路径)
                                   ;   ≠0 → 掉进下面的禁用音频块
0x66b04~0x66b30: 打印警告日志      ; w6=0x195=405,正好是日志行号(__LINE__)
0x66b34~0x66bd8: 释放 audioCaptureFilter_ (shared_ptr 减引用→析构)
0x66be0~0x66ca4: 释放第二个音频 filter
0x66d00~0x66d2c: 清理音频相关状态
0x66d30: ←———— 两条路径在这里汇合 ————
0x66d48: bl  PrepareAudioCapture
0x66d5c: bl  PrepareAudioDataSource
0x66d70: bl  PrepareVideoEncoder      ← A333 NV12 就是在这里面设置的
0x66dc0: bl  PrepareMetaData
0x66e10: bl  PrepareVideoCapture
0x66e70: bl  Pipeline::Prepare

(这些 bl 的目标一开始是匿名地址,我解析 .rela.plt 把 PLT 桩 → GOT 表 → 符号名逐一对应,才确认是上面这些函数。)

结论清晰了:A333 分支比正常路径多干的唯一事情,就是把音频 filter 拆掉;视频准备(PrepareVideoEncoder 里的 NV12 设置)在汇合点之后,两条路径共用。 所以修法就是:让 0x66b00 的条件跳转变成无条件跳转,禁用块变成永远走不到的死代码,其余一切照旧(模式字节本身没动,视频 NV12 通路不受影响)。


第五步:算指令编码(4 字节的由来)

原指令 88 11 00 34

小端序,实际指令字是 0x34001188。CBZ 的编码格式是:

sf 0010100 imm19(19位) Rt(5位)
  • Rt = 0b01000 = 8 → 被测寄存器是 w8
  • imm19 = (0x34001188 >> 5) & 0x7FFFF = 0x8C = 140
  • 跳转目标 = PC + imm19×4 = 0x66b00 + 140×4 = 0x66b00 + 0x230 = 0x66d30 ✓(和反汇编显示一致)

新指令

无条件跳转 B 的编码是 000101 imm26(26位):

  • 偏移 = 0x66d30 − 0x66b00 = 0x230;imm26 = 0x230 >> 2 = 0x8C(同样以 4 字节为单位)
  • 指令字 = 0x14000000 | 0x8C = 0x1400008C
  • 小端序写回文件就是 8c 00 00 14

注意新旧指令的 imm 巧合地都是 0x8C(因为目标一样,只是编码位宽不同)。


第六步:虚拟地址 → 文件偏移,写盘

虚拟地址不能直接当文件偏移用。查 PT_LOAD 段表,0x66b00 落在某个段的 vaddr 基址 + 页偏移 处,换算出文件偏移 0x65B00。写个小脚本:

  1. 断言原字节确实等于 88 11 00 34(防呆 —— 库里版本不对就中止);
  2. 替换成 8c 00 00 14,另存为新文件;
  3. 重新反汇编新文件验证:0x66b00: b #0x66d30 ✓

第七步:部署

hdc shell "mount -o remount,rw /"                   # 系统分区改读写
hdc file send 新.so /data/local/tmp/                 # 先推到临时目录
hdc shell "cat /data/local/tmp/新.so > /system/lib64/media/libmedia_engine_histreamer.z.so"
                                                        # cat 覆写保留原 inode 的权限/SELinux 标签
hdc shell "kill <media_service的pid>"                # safwk 自动拉起新进程加载新库

最后 md5 核对设备上的文件 = 本地补丁版,再看日志验证(不再出现禁用警告、AudioCaptureFilter 走完全链路)。


总结

「录制视频没有声音」从最初的恐慌排查,最终落在了一个系统级代码分支的选择问题上。最终解法:把走禁用分支的条件去掉,让系统走自己的正常音频准备流程。

技术要点写在代码注释里了:一句 ARM64 分支指令,让整个媒体引擎重新学会「听」。


复现与避坑提示

过程中踩的几个坑,供你复现时参考:

  1. 设备端 grep 二进制会漏检 —— 必须把 .so 拉回本地再做检索;
  2. capstone 遇到无法解码的字节会静默停止 —— 要按 4 字节步进重试;
  3. adrp 的目的寄存器在 operands[0]、立即数在 operands[1] —— 取反了寄存器名会是 None;
  4. 符号表没被剥离 —— 若缺失,ELF 解析就困难了,注意检查是否有人做了 strip;
  5. PLT 目标需交叉解析 —— 匿名 bl 地址通过 .rela.plt + GOT 对齐成符号名才看得清;
  6. 虚拟地址 → 文件偏移 —— 库版本变了后 vaddr 基址可能变,必须从段表重新算;
  7. 部署覆写 —— 用 cat > 而非 cp,能保留原文件的权限与 SELinux 标签,否则 remount rw 后可能被拒绝。

所有脚本都留在本地工作目录中(scan_refs.py 找字符串引用、plt.py 解析 PLT、disasm.py 反汇编、patch.py 打补丁),以后遇到类似「系统库改一个分支」的需求可以直接改着用。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐