NEX-5T干净HDMI输出:固件逆向、PMCA打包与踩坑速查
从固件分析到可用的 PMCA 应用
目录
- 问题的技术本质:两层显示架构
- 为什么固件里没有「HDMI Info. Display」
- Backup.bin 格式与 service mode 逆向
- PMCA 应用的打包要求
- CameraEx:反射调用序列
- 按键扫描码的提取
- 危险的 DisplayManager 接口
- APP 关键代码
- 工具链清单
- 踩坑速查表
一、问题的技术本质:两层显示架构
NEX-5T 的显示由两层合成后输出到 LCD 和 HDMI:
┌─────────────────────────────────────────┐
│ 视频层(硬件层) │
│ · 相机实时画面 │
│ · 按相机自己的分辨率渲染 │
│ · 不经过 Android 的 SurfaceFlinger │
├─────────────────────────────────────────┤
│ 图形层(UI 层) │
│ · 相机 OSD(快门/光圈/ISO/曝光挡位) │
│ · 各种拍摄图标 │
│ · 菜单 │
│ · PMCA 应用的界面 │
│ · 由 Android 合成 │
└─────────────────────────────────────────┘
↓ 合成
LCD / HDMI 输出
关键性质(实测确认):
- HDMI 是 LCD 的镜像 —— 不是独立通路。按 DISP 键,两边同步变化;「宽影像 → 全屏」同时改变两边。
- PMCA 应用运行时,相机的取景器渲染停止 —— 相机系统不再往屏幕上画自己的东西。
- 视频层的画质不受 Android 侧影响 —— 应用只能拿到画面,不能改变它的分辨率或处理方式。
推论:干净画面是怎么来的
相机 OSD 属于图形层,由相机系统在"拍摄界面"状态下绘制。
PMCA 应用占屏时,相机处于"应用界面"状态,取景器渲染停止 → OSD 根本不参与渲染。
所以 APP 只要: - 把相机的实时画面显示出来 - 上面什么都不画
HDMI 上就是干净的实时画面。
实测验证:SurfaceView 尺寸/格式只影响图形层
一度以为"把 SurfaceView 做大能让相机按更大尺寸渲染预览"。实测否定:
| 实验 | 结果 |
|---|---|
| SurfaceView 640×480 → 1920×1080 | 画质无变化,视野无变化 |
色深 RGB_565 → RGBA_8888(format=1 生效) |
因为不绘制内容,视觉无差别 |
surfaceFrame 确认缓冲区确实是 1920×1080 |
但视频层不受影响 |
结论:SurfaceView 的尺寸和格式属于图形层的事,视频层按相机自己的方式走。
二、为什么固件里没有「HDMI Info. Display」
这是整个项目前期投入最大的一段。目标:确认 NEX-5T 到底有没有这个设置(哪怕是被隐藏的)。
对照机型的选择
同期 CXD4132 平台的机型里,只有 SLT-A99 有「HDMI Info. Display」(说明书 p.173):
MENU → [HDMI Info. Display] → [Off]对比了几台 CXD4132 机型的 0x0107(TV/HDMI)子系统大小:
| 机型 | 属性数 |
|---|---|
| SLT-A77 | 2381 |
| SLT-A65 | 2381 |
| NEX-7 | 2389 |
| NEX-5T | 2828 |
| SLT-A99 | 2833 |
| DSC-RX100M2 | 3181(在那几个索引处结构不同,排除) |
三条独立证据
证据 1:索尼规格表 NEX-5T 的规格表里,「HDMI Info. Display」这一行标为不支持。
证据 2:设置数据库的结构对比
解析两台机器的设置库后发现:
- A99 的
0x0107子系统 = NEX-5T 的 + 末尾 5 个属性(0x01070b0c–0x01070b10) - 结构对齐(size + attr)率 100.0%
- NEX-5T 最大 ID
0x01070b0b;A99 最大 ID0x01070b10
结构对齐测试是必须先做的 —— 否则跨机型按索引比较会得到纯粹的错误结论。
证据 3:用实机查询
相机有 service mode,可以按 ID 直接读设置。喂给它 A99 独有的那个 ID:
readBackup(0x01070b0f) → Senser adjust control error 129
关键设计:同时跑了正对照和负对照。
| 类别 | ID | 结果 |
|---|---|---|
| 正对照(真实存在) | 0x01070148 (palNtscSelector) 等 |
全部成功 |
| 负对照(随机 ID) | 若干 | 全部 error 129 |
| 被测 | 0x01070b0f |
error 129 |
被测项与随机 ID 的反应完全一致 → 确认为"不存在的属性",而不是"存在但被锁"。
补充实验:改固件也没用
我们还构造了一个嫁接版 Backup.bin:
- 把 A99 那 5 个属性移植进 NEX-5T 的设置库
numProperties14507 → 14512,子系统0x01072828 → 2833- 原有 13494 个属性字节级完全一致(保证没有破坏别的东西)
产物:0110_backup_MODIFIED_HDMI.bin(672,655 字节)
但静态分析给出了致命结论:
两台机器的固件里,都没有任何代码引用 0x01070b0f —— 不是 u32、不是 u16 对、也不是大端形式。也就是说,这个设置即使被加进去,也没有代码去读它 —— 是死的。
而且刷入需要进入 updater 模式,会触发"恢复相机默认设置"(设置全丢),收益为零。
最终结论:不是隐藏,是这台机器的固件里就没有这套机制。
方法论教训(这一段值钱)
前期走了不少弯路,都是在密集二进制里做模式匹配导致的:
| 做法 | 结果 |
|---|---|
| 在固件里按 u16 搜设置 ID | 命中几千个巧合 |
直接扫 Backup.bin 找 ID 字节 |
产出 3 字节步进的假阳性 |
教训: 1. 永远先跑正对照和负对照,确认工具本身能分辨真假,再去扫真实目标。 2. 跨机型按索引比较前,必须先做结构对齐验证(size + attr)。
三、Backup.bin 格式与 service mode 逆向
Backup.bin 结构
相机的设置存在一个 Backup.bin 里,格式如下:
偏移 0x00 ─┬─ 256 字节文件头
│
├─ 子系统表:numSubsystems × 6 字节
│ u16 numProperties
│ u32 指向属性表的偏移
│
├─ 属性表:numProperties × 5 字节(revision 2)
│ u8 attr
│ u32 指向数据区的偏移
│
└─ 数据区
属性 ID 编码:ID = (subsystem << 16) | index
注意:只有当 index < subsystem.numProperties 时 ID 才有效。这是判断"设置是否存在"的结构依据。指针编码:ptr = (size << 24) | absoluteFileOffset
| size 值 | 含义 |
|---|---|
0 |
变长数据,在 offset 处有 u16 size + u16 maxSize |
0xff |
超长数据(u16 长度) |
| 其他 | 定长数据,长度即 size |
附加规则:若 attr & 0x74 非零,则在 offset + maxSize 处还有一份 resetData(恢复默认值用的数据)。
校验和:存放在 0x20 处,等于
sum(bytes[0x00 : 0x20]) + sum(bytes[0x24 : dataOffset + dataSize])
service mode
相机的 service mode 通过 MSC(海量存储器)模式 + serviceshell 进入,提供 XPD 服务:
| 服务 | 状态 |
|---|---|
readBackup / writeBackup |
✅ 可用 |
readFile / writeFile(FileControl) |
✅ 可用 |
readHasp |
❌ Senser product info error 128(ProductInfo 类,NEX-5T 不支持) |
setTerminalEnable |
❌ 同上 |
FileControl 可用这点很重要 —— 它意味着可以读写相机上的文件,是做逆向的关键通道。
辅助线索:字符串文件
string_*.uxc 文件的格式:
[u16 string_id][u16 marker][ASCII string \0]
marker 值:0x1e00(NEX-5T)/ 0x1e04(A99)。
但要注意:字符串文件是跨机型的超集 —— 某个字符串在文件里存在,不能证明对应功能在该机型上存在。
「宽影像」= Wide Image 就是这样定位到的(记录 id 0x037d,Setup 菜单区,紧邻 Disp. Basic Info)。
四、PMCA 应用的打包要求(为什么安装器拒绝我们的包)
症状
Starting task
Uploading 0% → 100%
Downloading 100%
Installing 0% ← 卡在这里
Communication error 100: Error completed
一个不提供任何信息的错误。
排除过程
逐项排除,每一步都要重走完整安装流程:
| 尝试 | 结果 |
|---|---|
| SHA-256 → SHA-1 签名 | ❌ 仍失败 |
补上图标和 app_name 资源 |
❌ |
加 CAMERA 权限 |
❌ |
targetSdkVersion 18 → 10 |
❌ |
| 确认 dex 版本 035(与可用 APP 一致) | ❌ |
| 包体积 6 KB → 2 MB | ❌ |
| SPK 加密容器往返测试 | ✅ 容器本身没问题 |
| 对齐(手写 zipalign) | ❌ |
关键对照:重装一个已知可用的第三方 APP(胶片坊 RecipeLab_CN_1.3.2.apk):
Uploading 0% → 100%
Downloading 67% → 100%
Installing 0% → 100% ← 走完了
Task completed successfully
证明:相机的安装通道正常,第三方签名的包也能装 —— 问题在我们的包本身。
根因
jarsigner 会重写整个 ZIP 文件。相机端的安装器对 ZIP 内部结构敏感,期望:
AndroidManifest.xml → 资源 → classes.dex → META-INF/(签名在最后)
而 jarsigner 签名时会重排整个压缩包,丢掉了这个结构(顺带也破坏了 zipalign 的对齐)。
正确工具链
aapt2 compile --dir res -o res.zip
aapt2 link -o base.apk -I android.jar --manifest AndroidManifest.xml res.zip
cp base.apk unsigned.apk
( cd dex-dir && aapt add -f unsigned.apk classes.dex ) # 用 aapt add,不是 zip
zipalign -f 4 unsigned.apk aligned.apk
apksigner sign \
--v1-signing-enabled true --v2-signing-enabled false --v3-signing-enabled false \
--min-sdk-version 10 --max-sdk-version 18 \
--out CleanHDMI.apk aligned.apk
换成这套之后,一次装上。
其他约束
| 项目 | 要求 |
|---|---|
minSdkVersion |
10 |
targetSdkVersion |
10 |
| dex 版本 | 035 |
| 签名 | v1(JAR signing),SHA-1 或 SHA-256 均可 |
| zipalign | 需要(但 v1 签名后仍可对齐,因为摘要算的是条目数据而非偏移) |
五、CameraEx:反射调用序列
第三方 PMCA 应用通过 com.sony.scalar.hardware.CameraEx 访问相机。不需要索尼 SDK,用反射即可(胶片坊就是这么做的)。
API 结构(从 sony.cameraex.odex 反汇编得到)
// 静态方法
CameraEx open(int id, CameraEx$OpenOptions options)
// 实例方法
Camera getNormalCamera() // ← 返回标准 android.hardware.Camera
void release()
// 以及一堆监听器注册方法(预览分析、放大倍率、快门速度等)
CameraEx$OpenOptions 的字段:
I inherit_setting setInheritSetting(boolean)
I preview setPreview(boolean) // 只是"要不要预览"
I target_media setTargetMedia(String) // 存储介质
注意:没有任何分辨率设置。 一度寄希望于此,实测否定。
调用序列
// onCreate
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // ★ 必须
holder.setFormat(PixelFormat.RGBA_8888);
// 打开相机
Class<?> c = Class.forName("com.sony.scalar.hardware.CameraEx");
Class<?> oo = Class.forName("com.sony.scalar.hardware.CameraEx$OpenOptions");
Method open = c.getMethod("open", int.class, oo);
Object cameraEx = open.invoke(null, 0, null); // id=0, options=null
Camera camera = (Camera) c.getMethod("getNormalCamera").invoke(cameraEx);
holder.addCallback(this);
// surfaceCreated
camera.setPreviewDisplay(holder);
camera.startPreview();
三个关键发现
① setType(SURFACE_TYPE_PUSH_BUFFERS) 是必需的
缺这一行,startPreview() 必然失败。这是 v2 → v3 的分水岭。
来源:反汇编胶片坊的 onCreate:
const/4 v0, 0x3
invoke-interface {p1, v0}, Landroid/view/SurfaceHolder;->setType(I)V
② 相机 HAL 是个薄壳
camera.getParameters().getPreviewSize() → null
camera.getParameters().getSupportedPreviewSizes() → null
camera.getParameters().getPreviewFormat() → 0 (NV21)
camera.getParameters().getFocusMode() → "dmf"(Sony 特有)
预览尺寸既不可查也不可设 —— 真正的预览由索尼自己的层驱动,标准 Camera API 只是个接口壳。
③ OpenOptions 传 null 完全可行
六、按键扫描码的提取
这是整个项目里最"取证"的一段。
背景:为什么 keyCode 不够用
相机的物理键(播放、菜单、快门等)由相机系统处理,映射成 Sony 自定义扫描码。
反汇编胶片坊的按键处理,看到:
public boolean onKeyDown(int keyCode, KeyEvent e) {
return runKey(e.getScanCode()); // ← 用扫描码,不是 keyCode
...
if (scan == 0xe8) { ... } // 确定键
if (scan == 0xcf) { ... } // 播放键
}
所以 APP 是收得到按键的,只是必须用 getScanCode() 去比对。
常量表的位置
相机的 Android 框架里有:
framework/com.sony.scalar.sysutil.ScalarInput.odex
反汇编后能看到 89 个按键常量,但——值全是空的:
.field public static final ISV_KEY_PLAY:I
因为 Java 的 static final int 常量的值存在 dex 的 static_values 编码数组里,而 baksmali 不打印它们。
读 static_values
DEX 的 class_def_item 第 8 个字段就是 static_values_off:
class_def_item (32 字节):
class_idx u32
access_flags u32
superclass_idx u32
interfaces_off u32
source_file_idx u32
annotations_off u32
class_data_off u32
static_values_off u32 ← 这里
static_values 是一个 encoded_array:uleb128 size + size 个 encoded_value。
每个 encoded_value 的格式:
第 1 字节 = (value_arg << 5) | value_type
紧随其后 value_arg + 1 个字节的数据(小端,有符号类型需符号扩展)
⚠️ 这里踩了个坑:我第一次写解析器时,把数据长度写成了按value_type固定的字节数(比如 int 就固定 4 字节)。 正确做法是value_arg + 1字节。 结果第一版输出每个值尾巴都带着0x24(=(1<<5)|0x04,正是"2 字节 int"的头部字节),一眼就能看出错位。
修正后的完整映射表:
| 按键 | 扫描码 | 常量名 | 说明 |
|---|---|---|---|
| 播放图像 | 0xcf |
ISV_KEY_PLAY |
会切回放界面 |
| 菜单 | 0x202 |
ISV_KEY_MENU |
|
| MOVIE | 0x220 |
ISV_KEY_MODE_MOVIE |
会切录制界面 |
| 开始/停止 | 0x203 |
ISV_KEY_STASTOP |
|
| 快门半按 | 0x204 |
ISV_KEY_S1_1 |
对焦 |
| 快门全按 | 0x206 |
ISV_KEY_S2 |
拍摄 |
| 确定 | 0xe8 |
ISV_KEY_ENTER |
|
| 上 / 下 / 左 / 右 | 0x67 / 0x6c / 0x69 / 0x6a |
ISV_KEY_UP/DOWN/LEFT/RIGHT |
|
| FN / AEL / AF-MF | 0x208 / 0x214 / 0x215 |
ISV_KEY_FN/AEL/AFMF |
|
| 红外遥控各键 | 0x228 – 0x24b |
ISV_KEY_IR_* |
|
| 拨轮 | 0x209 – 0x211 |
ISV_DIAL_* |
交叉验证
两套完全独立的数据源互相印证:
- 胶片坊(能正常工作的第三方 APP)里比较的是
0xe8和0xcf - 我们从固件
static_values里读出的ISV_KEY_ENTER=0xe8、ISV_KEY_PLAY=0xcf
完全一致。
七、危险的 DisplayManager 接口
相机的 Android 层还有一个显示管理接口:
com.sony.scalar.hardware.avio.DisplayManager
可安全使用的读取方法:
getActiveDevices() // 当前活动的显示设备
getDeviceInfo(String)
getDisplayedVideoRect()
⚠️ 危险方法:
controlGraphicsOutputAll(boolean onoff)
名字很有诱惑力("关掉图形输出"听起来正是我们要的),但实测结论是:
它关掉的是整个显示输出,不是叠加层。
实测表现(app-osd/ 就是这个实验):
调用 controlGraphicsOutputAll(false)
→ 画面全黑(连相机实时画面也没了)
→ 约 10 秒后看门狗检测到"无显示输出"
→ 相机强制重启
结论:不可用,且会让相机重启。 对应的 CleanOSD 应用建议从相机上删除。
八、APP 关键代码
三个必须做对的地方
① 预览初始化
holder = surface.getHolder();
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // ★ 缺这行必失败
holder.setFormat(PixelFormat.RGBA_8888); // 32 位色(默认 RGB_565 有色带)
② 按键处理 —— 只对会切换界面的键退出
private static final int SCAN_MENU = 0x202;
private static final int SCAN_PLAY = 0x0cf;
private static final int SCAN_MOVIE = 0x220;
private static final int SCAN_STASTOP = 0x203;
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
int scan = e.getScanCode(); // ★ 必须用扫描码
if (e.getAction() == KeyEvent.ACTION_DOWN) {
// 电源键:释放相机但放行事件(不能拦截,否则关不了机)
if (e.getKeyCode() == KeyEvent.KEYCODE_POWER) {
release(); finish();
return super.dispatchKeyEvent(e); // ★ 放行
}
// 会切换显示界面的键 → 主动退出避让
if (scan == SCAN_MENU || scan == SCAN_PLAY
|| scan == SCAN_MOVIE || scan == SCAN_STASTOP) {
release(); finish();
return true;
}
// 其余(快门、拨轮、AEL、方向键…)放行给相机
}
return super.dispatchKeyEvent(e);
}
③ 生命周期 —— 及时交还相机
@Override protected void onPause() { super.onPause(); release(); ... }
@Override protected void onStop() { super.onStop(); release(); ... }
@Override protected void onDestroy(){ super.onDestroy(); release(); }
release() 做三件事:stopPreview() → camera.release() → cameraEx.release()。
为什么这三处都不能省
| 位置 | 不做的后果 |
|---|---|
setType(PUSH_BUFFERS) |
startPreview() 失败,黑屏 |
| 按键处理(避让 + 放行) | 按播放键 → 死循环重启;一刀切 → 快门失效 |
| 生命周期释放 | 按关机键 → 硬断电,镜头不缩回 |
九、工具链清单
构建(全部免 root 装在 ~/dl-android)
| 组件 | 用途 |
|---|---|
| JDK 17(Adoptium) | javac / keytool |
android.jar(platform-23) |
编译用的 API 桩 |
aapt2(2.20) |
资源编译 + 链接 + 生成二进制 Manifest |
aapt(build-tools 内) |
aapt add 塞 classes.dex(关键) |
zipalign(build-tools) |
4 字节对齐 |
apksigner(build-tools) |
v1-only 签名(关键) |
r8.jar(D8) |
class → dex |
逆向调查(项目里自己写的)
| 工具 | 用途 |
|---|---|
dexdump.py |
极简 DEX 解析器 —— 遍历类/方法/字段,从 .odex 挖 API 签名 |
read-static-values.py |
读 dex 的 static_values,还原 static final 常量值(按键表来源) |
run-readbackup.py |
service mode 下按 ID 读设置(内置正/负对照) |
run-service-probe.py |
service mode 自动化驱动 |
fetch-models.sh |
多机型固件批量下载 + 解包流水线 |
build-modified-backup.py |
Backup.bin 结构改写(属性跨机型嫁接) |
build-apk.sh |
一键构建(含全部工具链步骤) |
make-icon.py |
图标生成器(超采样抗锯齿) |
固件解包路径(备忘)
| 容器 | 处理方式 |
|---|---|
.uxc(magic uxc\x07) |
直接解 |
IMT 线 .exe(FDAT / Packman) |
fwtool.py unpack -f <dat> -o <out> |
Mac .dmg(未压缩 Apple Partition Map) |
7z 提取 → 得 FirmwareData_*.dat → 再走 fwtool |
十、踩坑速查表
| # | 坑 | 症状 | 解法 |
|---|---|---|---|
| 1 | jarsigner 重写 ZIP |
安装失败 error 100 |
改用 apksigner(v1-only) |
| 2 | zip 追加 dex 破坏结构 |
同上 | 用 aapt add |
| 3 | 缺 setType(PUSH_BUFFERS) |
startPreview() 失败 |
加上(= 3) |
| 4 | 用 keyCode 比按键 |
按键拦截无效 | 用 getScanCode() |
| 5 | 一刀切"任意键退出" | 半按快门无法对焦 | 按扫描码区分,拍摄类放行 |
| 6 | 只在 onDestroy 释放相机 |
关机硬断电、镜头不缩回 | onPause/onStop 也释放 |
| 7 | 拦截电源键 | 关不了机 | 释放但放行事件 |
| 8 | 试图用 setPreviewSize |
返回 null,无从下手 | 接受:视频层分辨率由相机决定 |
| 9 | 试图放大 SurfaceView 提画质 | 画质/视野都不变 | 只影响图形层,无意义 |
| 10 | controlGraphicsOutputAll(false) |
全黑 → 10 秒后重启 | 不要用 |
| 11 | dex encoded_value 长度 |
常量值错位(尾巴带 0x24) |
长度 = value_arg + 1,不是固定值 |
| 12 | 在密集二进制里模式匹配 | 大量假阳性 | 先跑正/负对照,再扫目标 |
| 13 | 跨机型按索引比设置 | 错误结论 | 先做结构对齐(size+attr)验证 |
| 14 | 相机装完 APP 自动关机 | 每次都要重连 | 属正常行为;把「自动关机」设长 |
| 15 | 按键拦截后仍死循环 | 光"吞事件"挡不住相机系统 | 必须主动 release() 避让 |
故障恢复
相机卡死(按键全无响应,包括电源键)时:
1. 取出电池(唯一有效办法)
2. 等 10 秒
3. 装回电池,开机
4. 相机应正常落到拍摄界面(已验证有效)
附:最终产物
| 文件 | 说明 |
|---|---|
build-app/CleanHDMI.apk |
最终 APP(11.6 KB) |
app/src/com/nex5t/cleanhdmi/MainActivity.java |
源码(关键点均有注释) |
app/res/ |
图标(4 个密度)+ 字符串 |
build-apk.sh |
构建脚本 |
README.md |
方案文档 |
记录于 2026-10-14。所有结论均为实机验证,而非推测。
有AI相关的想法,但不知道怎么落地?
我是国冰,独立AI开发者。十多年技术一线——半导体显示、智慧城市到自动驾驶;现在一个人+AI,接AI落地项目,从梳理需求到系统落地,陪你走通。
看我能做什么