编译完OpenHarmony的某一天,我看着out目录里静静躺着的几个镜像文件,突然产生了一个念头:这几个GB的产物里到底装了什么?平时构建脚本跑得昏天黑地,一长串BUILD.gn、config文件填进去,最后出来的东西实际长什么样?于是花了一个周末的时间,把镜像从头到尾翻了个底朝天,从分区划分、文件系统格式,到内部目录、配置和服务进程,算是把这些镜像摸了一遍。这篇文章就是一份面向OpenHarmony镜像内容的拆解笔记,重点记录解剖镜像常用的工具、内部目录的职能划分、镜像内容与构建配置的对应关系,以及我在实际操作中踩过的几个坑。适合刚接触这套开源系统、想弄明白产物从哪里来、从哪里下手的开发者参考。
1. 编译落盘之后,产物里都有哪些镜像文件
1.1 镜像文件的常见构成与用途
先看编译完成后out目录下到底有什么。不同版本、不同产品配置下路径会有差异,但一般来说,编译完一套OpenHarmony标准系统,会在构建输出目录里看到这几个主要镜像:
| 镜像文件 | 主要用途 | 常见文件系统 |
|---|---|---|
| system.img | 系统主体镜像,包含系统服务、应用框架、内置应用、系统配置文件 | ext4 或 erofs |
| vendor.img | 厂商定制分区,存放硬件相关配置、厂商特有服务和驱动库 | ext4 或 erofs |
| userdata.img | 用户数据分区,首次启动时格式化,通常不预置关键数据 | f2fs 或 ext4 |
| updater.img | 升级镜像,用于设备OTA升级流程 | ext4 |
| boot.img | 内核启动镜像,包含内核和内存盘(或引导参数) | 内核格式 |
我只把system.img解开过,vendor.img的结构和它类似,只是内容更偏硬件。如果是研究系统启动流程,system.img优先;如果是排查外设、驱动、传感器这类问题,就要去vendor.img里找。
1.2 镜像的“真身”:稀疏镜像与文件系统格式
在解剖镜像之前,有个概念必须先弄清楚:某些镜像文件本身并不是直接可挂载的文件系统块设备,而是稀疏镜像(sparse image)。稀疏镜像的作用是省空间——文件系统里大片空白区域在镜像里不连续存储,用描述信息代替,这样镜像文件体积会比实际文件系统容量小很多。用file命令看一眼就能分辨:
$ file system.img system.img: Android sparse image, version 1.0, Total of 262144 4096-byte output blocks in 64 input chunks输出里出现“Android sparse image”字样,就说明这是稀疏格式。有些版本的工具链会直接产出非稀疏的raw镜像,但出于体积考虑,大部分官方产物都是稀疏的。所以,解包前第一步往往是先判断:能不能直接mount,还是需要先转成raw格式。
判断方法很简单,除了file命令,还可以用十六进制查看工具看一眼文件头。如果前几个字节出现了3a ff 26 ed这一串,就是sparse格式的魔数;如果是正常的ext4文件系统,magic number会是0xEF53(小端写入时显示为53 ef)。
1.3 镜像体积哪里来的:一次直观的估量
我习惯在解包前先用ls -lh看一眼镜像体积,再用du和df盘一下编译产物,大致推测系统大小分布。以我编译的一套标准系统为例,system.img在2GB左右,vendor.img在500MB左右,userdata.img虽然文件本身很小,但实际分区容量可能很大。这个比例能给你一个先验认知:系统大部分体积来自动态库、内置应用、字体和配置文件。后面解包后如果看到bin目录、lib目录特别大,就不会意外了。
搞清楚镜像的基本形态之后,接下来就是真正动手拆开它。
2. 动手解剖镜像:从挂载到逐个目录翻看
2.1 解开稀疏镜像,先把它变成可挂载的raw格式
拿到稀疏镜像后,第一步是转成raw。这个操作要用的工具叫simg2img,在Ubuntu等Linux发行版里一般包含在android-tools-fsutils包里,也可以直接搜索安装:
sudo apt install android-tools-fsutils转换命令非常简单:
simg2img system.img system_raw.img转换完成后再用file确认,如果输出变成了Linux rev 1.0 ext4 filesystem data这类信息,就说明已经是raw格式,可以继续挂载。顺带一提,反向工具img2simg可以把raw镜像重新打成稀疏镜像,后面如果要改文件再打包,会用到它。
有一点要注意:并不是所有OpenHarmony镜像都是sparse格式。有的版本或产品配置会直接生成system_ext4.img这类raw镜像,这时候就不需要simg2img这一步。我建议一律先用file命令确认格式,不要在“以为是raw”的情况下强行挂载,否则mount命令会报错或者行为异常,我在一开始就吃过这个亏。
2.2 挂载system镜像:两种路径都可以
转成raw之后,挂载就有两种方式:
一种是传统mount方式,需要root权限,适合在主机上快速翻看:
mkdir -p ~/ohos_mnt/system sudo mount -o loop system_raw.img ~/ohos_mnt/system对ext4来说,这样挂载基本就能直接ls了。但如果镜像文件系统是erofs,就要确认内核是否支持,实测下来Linux 5.4以上通常可以直接挂载,但更早的内核就未必了。
另一种是相对“温柔”的方式,用只读挂载,避免不小心改到镜像里的文件。我在调试过程中一般执行sudo mount -o loop,ro system_raw.img,加一个ro,防止操作失误污染镜像。不管用哪种方式,查看完务必umount,不然文件锁和缓存可能影响后续操作。
2.3 解不出镜像时,用binwalk来辅助判断
如果遇到不太常见的文件系统(比如某些产品用f2fs做userdata),主机上未必有现成的工具支持挂载。这时候我会先用binwalk扫一遍文件头,看能识别出什么:
binwalk system.imgbinwalk输出里如果识别到ext4、erofs等相关信息,就说明文件系统类型基本明确。如果没有识别结果,可以直接用od -A x -t x1z -N 64 system.img看前64字节的魔数,然后对照已知文件系统magic判断。这个方法虽然笨,但最可靠。毕竟不同OpenHarmony版本的工具链行为不完全一致,不能拿一套经验套所有镜像。
挂载成功后,真正的“内容探秘”才算开始。
3. 镜像内部目录与文件盘点:整个系统是这样的骨架
3.1 系统目录的“职能分工”
把system.img挂载后,用ls -lR或者tree会看到一长串目录。第一感觉是“这结构怎么有点眼熟又有点陌生”。OpenHarmony的系统目录确实吸收了不少成熟操作系统的设计思路,但落地细节完全不同。按我的观察,几个主要目录的职责可以总结成一张表:
| 目录 | 职责 | 典型内容 |
|---|---|---|
| /system/etc | 系统级配置 | 启动参数、服务配置、权限策略、网络配置 |
| /system/bin | 用户态可执行文件 | 命令行工具、Init进程入口 |
| /system/lib | 动态库与模块 | 系统服务依赖的.so、驱动框架模块 |
| /system/lib64 | 64位动态库 | 与lib对应,64位版本 |
| /system/app | 内置应用目录 | 存放.hap或解包后的应用资源 |
| /system/hap | 部分版本的应用存放路径 | 各路系统应用的hap安装包 |
| /system/usr | 资源与共享文件 | 字体、音频、图标等 |
| /vendor/etc | 厂商配置 | init配置文件、驱动配置文件、硬件相关信息 |
| /vendor/lib、/vendor/bin | 厂商库与二进制 | 传感器、音视频等硬件相关组件 |
我第一次看到/system/app里面全是.hap文件时挺意外的,原来系统里大部分“应用”都是以hap形式随镜像预置的,并没有安装到userdata。这也解释了为什么system镜像会这么大:一个内置应用往往就包含了若干动态库和资源文件。
3.2 配置文件里藏着的系统启动链路
翻到/system/etc/init/目录,会看到若干.cfg文件,比如init.cfg、以及各种子系统相关的cfg。这些就是系统启动过程中的核心配置。OpenHarmony的init进程启动时会读取这类配置文件,然后按序启动一系列服务。我在里面看到过很多服务名,基本能对应到代码仓库里的各个子系统模块,比如分布式软总线、应用管理、窗口管理等等。
如果你想快速理解一个系统启动时做了什么,最直接的办法就是读init目录下的cfg文件。它们一般会写明服务名称、可执行文件路径、启动参数、重启策略等内容。举个例子,某个服务如果配置了restart策略,那它崩溃后init会自动拉起;如果不想它在调试时反复重启,临时把这个服务的配置注释掉再打包镜像是一种常用手段。当然,这样改出来的镜像只能用于开发调试,不能当作正式产物。
cfg文件的语法并不复杂,看起来像JSON和脚本的混合体,我建议初次接触时先用一个简单的搜索命令把所有cfg文件列表搜出来,逐一打开看注释,比对着代码猜要快得多。
3.3 核心系统服务:一个大致的进程视图
系统服务方面,解包镜像后可以在/bin下找到很多可执行文件,同时也能从cfg文件里看到它们的启动入口。以标准系统为例,比较有代表性的几个服务包括:
- appspawn:应用进程孵化服务,负责创建新的应用进程,属于比较底层的进程管理服务。
- foundation:基础服务框架进程,承载大量系统能力和分布式软总线相关能力。
- samgr:系统能力管理服务,其他子系统通过它注册和查询系统能力。
看到这些名字,你基本能拼出一张“系统服务图”:init进程拉起samgr完成能力注册,再由foundation承载上层系统能力,应用进程由appspawn孵化。虽然实际链路更复杂,但镜像里这些文件给了你一条很好的线索去对照源码追踪。我在排查一个启动失败问题时,就是靠查镜像里有没有对应服务的可执行文件,快速判断出某个子系统是否真的编进了产物。
3.4 动态库和三方组件:镜像体积的“大头”
如果统计一下镜像里各类文件的体积占比,动态库一定排在前列。/system/lib、/system/lib64以及/vendor/lib下面躺着大量.so文件。这里有个很实际的经验:如果某个功能在运行时提示找不到符号或者加载失败,先到对应镜像的lib目录里看一眼有没有对应的库文件,再判断是编译没打进去,还是运行时加载路径不对。很多启动崩溃问题,根源就是二进制存在但依赖的库没进镜像。
从镜像内容反推构建产物,往往能快速定位一个问题是出在代码逻辑还是出在打包环节。
4. 镜像里的文件从哪来:构建配置反推
4.1 组件化打包与构建文件的关系
OpenHarmony的构建系统是高度组件化的,最终镜像里的每一样东西,基本都有对应的构建来源——要么来自某个子系统的构建目标,要么来自产品配置文件的定制项。一个典型的关系链是:
子系统代码→BUILD.gn中定义的group/component→系统镜像打包阶段→镜像里的具体文件路径
在源码仓库里,每个子系统下都有BUILD.gn,其中有ohos_component或者group之类的声明,用来定义这个组件编译后输出哪些产物、安装到哪个目录。镜像里的bin目录、lib目录下能看到的名字,绝大多数都能在对应子系统的BUILD.gn里找到出处。
如果你想知道某个镜像里的文件是谁打进去的,最快的办法是在源码根目录搜索文件名:
grep -r "my_service" --include="*.gn" --include="*.gni" .这样能定位到构建文件,再往上追溯到组件的依赖关系。这个“镜像文件溯源”方法,在排查“为什么我明明把代码编进去了,镜像里却没有”这类问题时非常有用。
4.2 产品配置如何决定镜像内容差异
同一个版本源码,编译不同产品配置,得到的镜像内容可能差很多。核心原因是产品定义文件决定了要包含哪些子系统、哪些组件。如果打开产品配置文件(一般在productdefine/products目录下),可以看到类似subsystems、deps信息的配置。产品配置里包含的组件越多,镜像体积越大,镜像里的服务和文件也越多。
我遇到过一个典型的场景:同一套代码,编译A产品时/vendor/etc目录里多了某个传感器的配置文件,编译B产品时就没有。查了半天发现A产品配置里支持了对应的传感器组件,B产品没加。镜像内容差异并不神秘,大部分情况都能在productdefine和vendor配置里找到答案。
4.3 镜像修改的三个边界认知
在动手改镜像之前,心里最好先有个边界,避免改了无效或者改出更严重的问题:
第一,编译期修改才是可持续的。直接修改镜像文件并重新打包,在当时可能有效,但下次编译产物会覆盖手工改动,改动不会保留。真正想把文件持久地加进镜像,还是要从构建配置入手。
第二,文件读取权限和上下文标签。OpenHarmony镜像里的文件带着权限位、owner和SELinux相关标签。手工改镜像时如果不小心破坏了文件的context信息,可能导致设备启动后某服务无法访问这个文件,表现成“权限拒绝”但有的时候不是真的文件权限问题。
第三,镜像签名和校验。正式设备会校验镜像签名,手工改动镜像后通常无法过校验。但在开发调试阶段用的是userdebug/eng版本,校验比较宽松,手工改镜像仍能启动,这也是国内不少开发者能愉快地“改镜像做实验”的背景。一旦要刷回量产设备,这种做法就行不通了,这点一定要分清楚。
理解了镜像内容怎么来,接下来一个很自然的实操需求就是:我想往镜像里加东西——这可能是调试一个库、放一个脚本、或者替换一个配置文件。
5. 实战:往镜像里加入自定义内容
5.1 通过构建配置持久化添加文件
先说正规做法:通过修改产品配置或对应子系统的构建文件,把自定义文件复制到镜像输出目录。以放置一个自定义配置文件到/system/etc为例,比较常见的方式是在所在子系统的BUILD.gn里写一个copy或install条目。下面这个例子是在某个子系统组件里,把一个本地产物复制到系统etc目录下:
# 在BUILD.gn中新增或修改 ohos_copy("copy_my_config") { sources = [ "//vendor/某厂商/产品目录/configs/my_config.cfg" ] dest = "//out/xxx/packages/phone/system/etc/my_config.cfg" install_images = [ "system" ] }然后把这target挂到对应组件的依赖里,重新编译,镜像重新打包后,这个文件就会出现在system/etc下。这样做的好处是后续重新编译不会丢,改成什么内容都能跟随构建产出。
如果只是想往vendor分区放文件,把install_images改成vendor即可。产品配置文件里也可以按分区调整目标路径,细节随着版本不同略有差异,但核心思路没变。
5.2 直接改镜像的快速实验方案
有时候不适合重新跑完整编译(耗时太长),只是想临时验证一个文件的效果,我通常走“解包-修改-重新打包”的路子。步骤可以压缩成下面这套流程:
- 用
fsck.ext4 -fn system_raw.img检查镜像的一致性; - 挂载只读副本到临时目录,拷贝需要修改的文件到挂载点;
- umount后生成新镜像;
- 如果需要稀疏格式再用
img2simg打回去。
但这里有个大坑:直接改挂载点里的文件,如果没有处理好,文件类型、属主、权限会不对,特别是动态链接库,修改后如果owner变成了root而原始是其他uid,可能导致进程启动时访问受限。我一般会养成习惯:在挂载前把原始镜像的所有者、文件权限导出保存,修改后逐个比对,把差异修正。
如果只是想快速替换单个配置文件,还有个更简单办法:直接用debugfs在原始raw镜像上操作,不挂载也能写文件。debugfs对ext4特别友好,比如执行下面的命令可以把宿主机上的文件拷入镜像指定路径:
debugfs -w -R "write my_config.cfg /system/etc/my_config.cfg" system_raw.img注意目录路径要写对,写完后用debugfs -R "ls /system/etc" system_raw.img验证。这个方式对不熟悉的文件系统类型不适用,比如erofs就要用专门工具或者重新挂载。
5.3 验证修改是否生效
验证方式也有讲究,不能光怀一腔信任。如果设备能通过hdc连上,重启后执行:
hdc shell ls -l /system/etc/my_config.cfg hdc shell cat /system/etc/my_config.cfg确认文件存在且内容正确。如果需要校验文件是否真的参与系统行为,可以看对应服务的日志。在开发调试阶段,我经常在修改一个配置后,直接kill掉对应进程,等init自动拉起,再用日志确认它到底读取了哪个文件。这种方式比反复重启整机快得多,也更不容易误判。
6. 翻镜像时踩过的坑与排查小技巧
6.1 “挂载成功却看不到文件”?多半是overlay或者文件系统机制
我第一次挂载system镜像时,明明mount成功了,ls却只能看到部分文件,甚至有的文件路径就是找不到。排查后发现,OpenHarmony在运行时还会挂overlay文件系统,镜像里的内容在启动后经过overlay叠加和挂载过滤,实际看到的目录结构并不完全等于镜像里的原始布局。而我只在运行中的设备上执行ls,自然看不到某些镜像里确实存在的文件。
所以要有意识地区分“镜像里的视图”和“运行时的视图”。排查问题时,如果设备上找不到某个文件,先回到镜像里确认它是否存在,再考虑是不是启动流程或overlay把它隐藏了。
6.2 erofs镜像的解包注意点
erofs文件系统在较新版本中用得越来越多。它解包方式和ext4不一样,不能用简单的mount解决所有问题,至少我遇到两种情况:
一种是用fsck.erofs或dump.erofs直接读取目录结构。dump.erofs -l system_erofs.img可以列出目录树,虽然没有完整挂载那么直观,但应付查看结构、确认文件是否存在的需求足够。
另一种是让内核直接挂载erofs。前提是内核开启EROFS支持,且镜像不是稀疏格式。如果遇到wrong fs type的报错,先检查一下自己电脑内核版本,别再硬挂。我自己的习惯是优先用dump.erofs,不依赖内核特性,少踩兼容性的坑。
6.3 userdata镜像为什么几乎解不出有用的东西
第一次翻镜像时,我把userdata.img也解了一遍,结果翻完就后悔了。userdata分区通常是空壳,首次启动时系统会在设备上对它格式化,里面不会有任何预置的系统文件。如果在userdata.img里找不到东西,别怀疑自己“拆错了”,这是完全正常的情况。
要调试用户数据分区相关问题,最好是设备启动后直接通过hdc进入shell查看/data目录,而不是解包镜像。
6.4 镜像解包前的“先确认快照”习惯
在解包修改镜像之前,我建议先把原始镜像的SHA256值记下来。理由是:你修改后的镜像一旦刷到设备上出了问题,至少能迅速判断问题是不是自己改动导致的。如果原始镜像能正常启动,修改后启动失败,优先回头看改动内容——很多问题其实就出在改文件后忘了处理权限和属主,而不是代码本身。
sha256sum system.img > sha256_before.txt刷机后如果发现问题,再hash一次当前镜像,对比就知道刷进去的到底是不是自己改过的版本。这个习惯在反复实验时特别有用,能省掉很多自我怀疑的时间。
6.5 把镜像当“线索库”使用
最后分享一个我觉得很有价值的思路:镜像不只是拿来刷机的,它还是一个巨大的“线索库”。遇到一些诡异问题时,我会先把镜像解包,在镜像里搜索关键词,查找相关配置、库和服务。比如有一次想确认某个功能是否默认开启,直接在/system/etc下grep相关配置项,很快找到了开关位置。比起直接去翻代码、跑编译,先看镜像里的实际内容往往更快也更精确。
对于OpenHarmony这种体量的系统,镜像里的内容其实是源码和构建配置的“快照”,它忠实地记录了那次编译选进了哪些代码、配置了哪些文件、打好了哪些依赖。读懂镜像,等于掌握了一种从产物反向理解系统构建与运行逻辑的能力。这套思路对于任何需要接触二进制、镜像、构建产物的开发场景都通用。