OpenHarmony:编译后的 SO 如何进入镜像并刷入开发板
本文以当前源码树中的标准系统构建链为主线,追踪一个
ohos_shared_library从链接产物、安装元数据、分区目录、文件系统镜像,到 Flashd 写入开发板分区的全过程。核心结论是:SO 并不是被链接器直接写进 image,而是先由构建模板生成“源文件到分区路径”的安装清单,再由打包阶段复制到分区目录树,最后由镜像工具把目录树序列化为 ext4/f2fs/cpio 镜像。烧录时,主机通过 HDC 传输镜像数据,开发板上的 Flashd 将数据流写入对应块设备。
目录
- 1. 问题边界与全链路结论
- 2. 第一阶段:SO 的编译产物与安装属性
- 3. 第二阶段:安装元数据如何描述 SO 的去向
- 4. 第三阶段:将 SO 安装到分区目录树
- 5. 第四阶段:分区目录树如何变成 image
- 6. 第五阶段:image 如何刷入开发板
- 7. 以 system SO 为例还原一次完整执行
- 8. 设计评价、常见陷阱与排查方法
- 9. 总结
术语表
| 缩写 | 英文全称 | 本文含义 |
|---|---|---|
| SO | Shared Object | ELF 动态共享库,通常以 .so 结尾 |
| GN | Generate Ninja | 生成 Ninja 构建图的元构建系统 |
| HDC | HarmonyOS Device Connector | 主机与 OpenHarmony 设备之间的调试和数据传输通道 |
| DAC | Discretionary Access Control | 制作文件系统镜像时写入 UID、GID 与权限的访问控制规则 |
| SELinux | Security-Enhanced Linux | 制作镜像时写入文件安全上下文的强制访问控制机制 |
| F2FS | Flash-Friendly File System | 面向闪存设备的文件系统,常用于 userdata 分区 |
| eMMC | embedded MultiMediaCard | 开发板常见的非易失性块存储介质 |
| OTA | Over-the-Air | 通过升级包更新系统的机制,与本文的原始分区镜像刷写不同 |
注:术语按在本文中出现的先后顺序排列。
1. 问题边界与全链路结论
1.1 两条容易混淆的流水线
OpenHarmony 构建中存在两条相邻但职责不同的流水线:
| 流水线 | 输入 | 输出 | 核心职责 |
|---|---|---|---|
| 编译流水线 | C/C++ 源码、头文件、依赖库 | ELF .so |
编译、链接、符号处理 |
| 制品流水线 | .so、可执行文件、配置、HAP、资源 |
分区目录与 .img |
选择安装内容、布置路径、写权限、制作文件系统 |
链接完成只意味着 libxxx.so 已经存在于 out/<product>/... 的编译输出目录。它能否进入镜像、进入哪个镜像、位于镜像中的哪个目录,由后续安装元数据控制。
1.2 从 SO 到开发板的完整链路
BUILD.gn 中的 ohos_shared_library
-> Ninja/Clang 链接生成 libxxx.so
-> cxx.gni 同时创建 xxx_info 元数据目标
-> gen_module_info.py 生成 *_module_info.json
source = 编译输出中的 libxxx.so
dest = system/lib64/libxxx.so 等目标路径
-> 产品部件清单聚合可安装模块
-> modules_install.py 复制到 packages/<platform>/<partition>/...
-> build_image.py 选择分区配置
-> mkimages.py 选择 ext4/f2fs/cpio 制作器
-> mke2fs 创建空文件系统
-> e2fsdroid 把目录树、DAC 和 SELinux 标签写入 system.img
-> 主机 HDC 将 system.img 分块发送到 Flashd
-> FlashCommander -> Partition -> FlashdWriterRaw -> DataWriter
-> 写入 /dev/block/.../by-name/system
-> 重启后内核按板级 fstab/启动脚本挂载该分区,动态链接器加载其中的 SO
这条链路的关键分界点有三个:
*_module_info.json:从“编译目标”切换到“安装目标”;packages/<platform>/<partition>/:从“零散模块”切换到“分区目录树”;<partition>.img:从“可查看的目录树”切换到“可写入块设备的文件系统映像”。
2. 第一阶段:SO 的编译产物与安装属性
2.1 ohos_shared_library 不只创建链接目标
build/templates/cxx/cxx.gni:833-869 展示了共享库模板的双重职责:一方面创建 shared_library,另一方面调用 generate_module_info 生成安装描述。
# 位置:build/templates/cxx/cxx.gni:833-869
# 作用:为共享库声明安装元数据。
generate_module_info(_module_info_target) {
module_name = ohos_module_name
module_type = "lib"
module_install_images = [ "system" ]
if (defined(invoker.install_images)) {
module_install_images = invoker.install_images
}
install_enable = true
if (defined(invoker.install_enable)) {
install_enable = invoker.install_enable
}
}
随后,build/templates/cxx/cxx.gni:998-1025 才展开真正的 shared_library("${target_name}")。因此一个共享库目标实际产生两类构建节点:
| 节点 | 产物 | 用途 |
|---|---|---|
shared_library |
libxxx.so |
运行时代码本体 |
generate_module_info |
xxx_module_info.json |
告诉打包器源文件和目标安装路径 |
默认情况下,共享库的 install_enable 为 true,默认安装镜像为 system。如果模块显式设置 install_enable = false,链接仍可成功,但安装阶段会跳过它。
2.2 架构决定默认的 lib 或 lib64 目录
build/templates/metadata/module_info.gni:79-90 将模板中的逻辑类型 lib 转换为架构相关的安装类型:
arm64、x86_64、loongarch64、riscv64->lib64;arm、x86等 32 位目标 ->lib。
这不是简单的文件名变化。后续目标目录计算会把 module_type 直接作为路径组成部分,所以 64 位共享库默认进入:
system/lib64/libxxx.so
32 位共享库默认进入:
system/lib/libxxx.so
如果设置 relative_install_dir = "module/ability",64 位目标则变为:
system/lib64/module/ability/libxxx.so
2.3 install_images 决定进入哪个分区
build/ohos_var.gni:61-89 定义了标准分区目录名,包括 system、vendor、ramdisk、updater、sys_prod、chip_prod、eng_system 和 eng_chipset。
共享库常见配置如下:
# 示例:业务模块的 BUILD.gn
ohos_shared_library("example") {
sources = [ "example.cpp" ]
install_enable = true
install_images = [ "system" ]
relative_install_dir = "example"
part_name = "example_component"
subsystem_name = "example_subsystem"
}
安装位置由三个属性共同决定:
| 属性 | 决定内容 | 默认行为 |
|---|---|---|
install_images |
进入 system、vendor 等哪个分区 | 共享库默认为 system |
module_install_dir |
从分区根开始指定绝对的内部目录 | 优先级最高 |
relative_install_dir |
在默认类型目录 lib/lib64 下追加子目录 |
次于 module_install_dir |
同一个模块可以配置多个 install_images,此时元数据会产生多个 dest,安装阶段会把同一产物复制进多个分区目录。这样虽然可行,但容易造成重复占用,通常只用于明确的跨启动环境需求。
3. 第二阶段:安装元数据如何描述 SO 的去向
3.1 module info 是编译与镜像之间的契约
build/templates/metadata/module_info.gni:99-172 将模块名称、源码输出目录、分区列表和安装目录作为参数传给 gen_module_info.py。真正写入 JSON 的字段位于 build/templates/metadata/gen_module_info.py:148-185:
# 位置:build/templates/metadata/gen_module_info.py:148-160
# 作用:形成打包阶段读取的模块安装记录。
data = {
'type': module_type,
'label': module_label,
'label_name': module_name,
'source': source,
'dest': install_dests,
'collect': collect,
'install_enable': install_enable
}
对一个 64 位 system 共享库,概念上的 JSON 类似:
{
"type": "lib64",
"label_name": "example",
"source": "out/rk3568/.../libexample.so",
"dest": [
"system/lib64/example/libexample.so"
],
"install_enable": true
}
这里的关键不是 JSON 格式本身,而是它把两个此前分离的世界连接起来:
source指向编译系统产生的真实文件;dest描述它在最终分区根目录中的位置。
3.2 目标路径的计算规则
build/templates/metadata/gen_module_info.py:49-106 实现目标路径计算:
若 module_install_dir 非空:
<分区基目录>/<module_install_dir>/<文件名>
否则若 relative_install_dir 非空:
<分区基目录>/<lib或lib64>/<relative_install_dir>/<文件名>
否则:
<分区基目录>/<lib或lib64>/<文件名>
其中 install_images 被逐个遍历:system 选 system_base_dir,vendor 选 vendor_base_dir,其余分区同理。最后在路径末尾拼接 libxxx.so。
需要注意,module_install_dir 与 relative_install_dir 不是叠加关系。源码中的 if/elif 表明前者存在时后者不会生效。
3.3 为什么这里只生成 JSON,而不立即复制
模板阶段不直接把 SO 复制到 system 目录,主要有四个原因:
| 原因 | 机制价值 |
|---|---|
| 产品裁剪 | 同一源码模块可被多个产品引用,最终由产品部件清单决定是否安装 |
| 增量构建 | 元数据和链接产物分别成为 Ninja 节点,变更哪一侧就重跑哪一侧 |
| 统一处理 | SO、可执行文件、配置、HAP 都能转成统一的 source/dest 模型 |
| 可审计性 | 打包前可以检查安装清单、重复路径、缺失文件和白名单规则 |
因此,module info 更像“安装意图”,不是安装结果。
4. 第三阶段:将 SO 安装到分区目录树
4.1 make_packages 聚合产品所选部件
build/ohos/packages/BUILD.gn:36-57 中的 make_packages 为每个平台依赖 ${platform}_install_modules。${platform}_parts_list 会先根据产品选择的部件生成 system_install_parts.json,然后安装动作读取各部件下的 module info。
这意味着“源码仓里存在某个共享库目标”并不足以让它进入镜像。还需要同时满足:
- 目标被构建图引用;
- 所属部件被当前产品选择;
- module info 中
install_enable为真; source文件真实存在;- 目标
dest没有发生不可接受的冲突。
4.2 modules_install.py 执行真实复制
build/ohos/packages/BUILD.gn:280-359 把 modules_install.py 注册为安装动作,并指定:
--platform-installed-path为当前平台的 packages 目录;- 输出
system_module_info.json、system_modules_list.txt和system.zip; - 输入为产品部件安装信息与各模块元数据。
复制核心位于 build/ohos/packages/modules_install.py:78-145:
# 位置:build/ohos/packages/modules_install.py:78-145
# 作用:筛选 install_enable 模块,并将 source 复制到 staging 目录的 dest。
for value in modules_info_dict.values():
module_info = read_json_file(value)
install = module_info.get('install_enable')
if not install:
continue
output_result.append(module_info)
for module_info in all_target_result:
source = module_info.get('source')
dests = module_info.get('dest')
for dest in dests:
dest_dir = os.path.join(platform_installed_path,
os.path.dirname(dest))
os.makedirs(dest_dir, exist_ok=True)
shutil.copy2(source, os.path.join(platform_installed_path, dest))
在复制前,脚本会清理旧的 system、vendor、updater 等 staging 目录,见 build/ohos/packages/modules_install.py:267-331。这避免上一次构建残留的文件悄悄进入新镜像。
4.3 staging 目录不是镜像,而是镜像内容的展开形态
build/ohos/build_var.gni:14-16 定义:
product_output_dir = "$root_build_dir/packages"
随后每个平台使用 ${product_output_dir}/<platform>。在常见标准产品中,最终可观察到类似结构:
out/<product>/packages/<platform>/
├── system/
│ ├── bin/
│ ├── etc/
│ ├── lib/
│ └── lib64/
├── vendor/
├── sys_prod/
├── chip_prod/
├── data/
└── images/
不同源码版本或产品配置可能把 <platform> 命名为 phone,也可能使用其他目标平台名;应以 out/<product>/build_configs/platforms_list.gni 和实际构建输出为准。
此时 system/lib64/libxxx.so 已经位于 staging 目录,但还只是普通宿主机文件。该目录允许开发者在制镜像前直接检查:
- 文件是否存在;
- ELF 架构是否正确;
- 文件权限和软链接关系;
- 是否被错误安装到 system/vendor;
- 是否有两个模块覆盖同一目标路径。
5. 第四阶段:分区目录树如何变成 image
5.1 GN 为每个分区创建镜像目标
build/ohos/images/BUILD.gn:20-47 的 make_images 聚合 system、vendor、userdata、sys_prod、chip_prod、updater_ramdisk、eng_system 和 eng_chipset 等镜像目标。
每个分区的具体 action 位于 build/ohos/images/BUILD.gn:258-338:
# 位置:build/ohos/images/BUILD.gn:258-338
# 作用:把分区 staging 目录交给 build_image.py。
action_with_pydeps("${_platform}_${_image_name}_image") {
script = "//build/ohos/images/build_image.py"
deps = [ "//build/ohos/packages:${_platform}_install_modules" ]
image_input_path = "$current_platform_dir/${_image_name}"
output_image_file = "$current_platform_dir/images/${_image_name}.img"
args = [
"--image-name", _image_name,
"--input-path", rebase_path(image_input_path, root_build_dir),
"--image-config-file", rebase_path(image_config_file, root_build_dir),
"--device-image-config-file", rebase_path(device_image_config_file, root_build_dir),
"--output-image", rebase_path(output_image_file, root_build_dir)
]
}
依赖 ${platform}_install_modules 非常重要:它保证镜像动作开始之前,SO 和其他模块已经复制进分区目录树。
源码兼容性说明:当前
BUILD.gn传入参数名为--output-image,而本文所读build_image.py的参数声明是--output-image-path。这表明当前检出的源码树存在脚本与 GN 参数名的版本不一致。正式构建前应检查本地生成 Ninja 命令或修正两端参数名,否则 action 会在参数解析阶段失败。本文对镜像链路的分析基于两端表达的共同语义,不掩盖这一实际不一致。
5.2 build_image.py 选择配置并准备目录
build/ohos/images/build_image.py:127-149 完成三件事:
- 对 system、ramdisk、updater 等分区补充必要目录和软链接;
- 在通用配置与设备专用配置之间选择实际配置;
- 调用
mkimages.mk_images()。
设备配置的优先级高于通用配置:
# 位置:build/ohos/images/build_image.py:139-149
config_file = args.image_config_file
if os.path.exists(args.device_image_config_file):
config_file = args.device_image_config_file
mk_image_args = [
args.input_path,
config_file,
args.output_image_path,
image_type
]
mkimages.mk_images(mk_image_args)
因此,分区大小、文件系统类型或设备特有参数,可能来自:
out/<product>/packages/imagesconf/<partition>_image_conf.txt
而不是 build/ohos/images/mkimage/ 下的默认配置。调试镜像大小时,应先确认究竟使用了哪一份配置。
5.3 ext4 镜像的真正生成过程
build/ohos/images/mkimage/mkimages.py:52-73 通过配置文本判断文件系统类型:
| 配置关键字 | 制作脚本 | 常见用途 |
|---|---|---|
ext4 |
mkextimage.py |
system、vendor 等只读系统分区 |
f2fs |
mkf2fsimage.py |
userdata 等闪存友好分区 |
cpio |
mkcpioimage.py |
ramdisk |
默认 system_image_conf.txt 的内容见 build/ohos/images/mkimage/system_image_conf.txt:1-5:
/
2147483648
--fs_type=ext4
--dac_config ../../build/ohos/images/mkimage/dac.txt
--file_context obj/base/security/selinux_adapter/file_contexts.bin
它描述挂载点 /、文件系统容量、ext4 类型、DAC 配置和 SELinux file contexts。
build/ohos/images/mkimage/mkextimage.py 将 ext4 制作拆成两步:
第一步:mke2fs
创建指定容量、块大小、label 和 mount point 的空 ext4 文件系统
第二步:e2fsdroid
将 staging 目录中的文件复制进 ext4
同时应用 DAC UID/GID/权限和 SELinux file_context
对应源码为:
mkextimage.py:59-92:构造并运行mke2fs;mkextimage.py:95-123:构造并运行e2fsdroid -f <src> -a <mount> <image>;mkextimage.py:126-136:顺序执行两步,失败时删除不完整镜像。
如果启用 sparse image,mkimages.py:84-89 会在原始镜像生成后调用 img2simg 转为 Android sparse 格式。Sparse 只改变传输和存储表达,不改变展开后的文件系统内容。
5.4 system 镜像为什么还要合并 root 目录
system 镜像不是简单对 packages/.../system 执行打包。build/ohos/images/mkimage/mkimages.py:38-49 的 build_rootdir() 会:
- 复制同级
root目录到临时目录; - 删除临时目录内原有
system; - 把当前 system staging 目录复制为临时根下的
system。
build_image.py:36-53 提前创建 root 目录结构和软链接,例如:
/bin -> /system/bin
/init -> /system/bin/init
/etc -> /system/etc
/lib -> /system/lib
/lib64 -> /system/lib64 # 64 位目标
/chipset -> /vendor
这说明当前 system image 表达的是一个带根目录骨架的文件系统布局。SO 的实体仍位于 /system/lib64,但根下的 /lib64 可以通过软链接访问它。
6. 第五阶段:image 如何刷入开发板
6.1 刷机链路不属于 GN 镜像生成
到 system.img 生成完成为止,GN/Ninja 的职责已经结束。镜像生成系统只保证:
- 文件系统格式正确;
- 分区内容完整;
- 权限、SELinux 标签和软链接已写入;
- 输出文件可供后续烧录工具使用。
“把哪个 image 写到哪一个物理分区”由设备刷机协议、分区表和开发板启动模式决定,而不是由 build_image.py 决定。
源码树中可以验证的一条标准路径是升级子系统提供的 Flashd。厂商 Bootloader 下载模式则属于板级工具链,通常使用 Rockchip、HiSilicon 等芯片厂商的 USB 下载协议。
6.2 Flashd 模式与 HDC 传输通道
base/update/updater/services/flashd/Readme.md:12-23 明确说明:Flashd 是升级子系统的刷机模式,客户端与服务端通过 HDC 传输数据。正常系统中执行:
hdc_std shell reboot updater
设备会进入 updater/Flashd 环境。
服务端入口位于 base/update/updater/services/flashd/daemon/flashd_main.cpp:29-65。它默认启用 USB,设置 updater.flashd.configfs=1,然后初始化 HDC daemon。
RK3568 updater USB 配置 device/board/hihope/rk3568/updater/config/init.rk3568.usb.cfg:44-63 进一步说明:
- 监听
updater.flashd.configfs=1; - 停止普通
hdcd; - 将
sys.usb.config切换为flashd; - 把 FunctionFS HDC 功能挂到 USB gadget;
- 绑定 USB Device Controller。
也就是说,Flashd 并非绕过 HDC,而是让 updater 环境中的 HDC daemon 切换到具有刷写命令的任务实现。
6.3 镜像数据从主机到分区的调用链
Flashd 的刷写命令字符串在 base/update/updater/services/flashd/common/flashd_define.h:30-47 中定义为 flash,协议阶段包括 CHECK、BEGIN、DATA、FINISH 和 PROGRESS。
设备端完整调用链为:
主机 Flashd 客户端发起 flash <partition> <image>(具体 CLI 语法以配套 HDC 版本为准)
-> HDC 将文件信息和镜像数据分块传输
-> DaemonUpdater::CheckCommand()
解析 functionName、options、fileSize
-> CommanderFactory::CreateCommander("flash")
-> FlashCommander::DoCommand(options, fileSize)
保存分区名与总大小
-> DaemonUpdater::DataCommand()
逐块取出 payload
-> FlashCommander::DoCommand(payload, payloadSize)
-> FlashCommander::DoFlash()
-> Partition::DoFlash()
-> FlashdWriterRaw::Write()
-> DataWriter::Write()
-> 目标块设备
对应源码证据:
daemon_updater.cpp:118-165:解析命令并将数据块交给 commander;commander_factory.cpp:25-44:将字符串flash映射到FlashCommander;flash_commander.cpp:30-53:解析分区名并准备接收;flash_commander.cpp:56-77:累计写入大小并上报进度;flash_commander.cpp:97-116:初始化 Partition 并写入本批数据;partition.cpp:30-41:调用 writer 写数据;image_writer.cpp:55-68:调用DataWriter::Write()。
Flashd 采用流式写入,而不是先把完整 system.img 保存到开发板文件系统再复制。这样可以减少 updater 环境对临时存储空间的要求。
6.4 分区名如何映射为真实块设备
对于不使用扩展分区表接口的路径,base/update/updater/services/flashd/image_writer/image_writer.cpp:35-52 调用:
DataWriter::CreateDataWriter(
WRITE_RAW,
GetBlockDeviceByMountPoint(partition));
分区擦除路径则使用 Utils::GetPartitionRealPath()。base/update/updater/utils/utils.cpp:875-878 将分区节点前缀与分区名拼接,再通过 realpath() 解析真实设备。
在 RK3568 的 device/board/hihope/rk3568/cfg/fstab.rk3568:3-9 中,可以看到该产品源码中的实际映射:
| 分区 | 块设备路径 | 挂载点 | 文件系统 |
|---|---|---|---|
| system | /dev/block/platform/fe310000.sdhci/by-name/system |
/usr |
ext4 |
| vendor | /dev/block/platform/fe310000.sdhci/by-name/vendor |
/vendor |
ext4 |
| sys-prod | /dev/block/platform/fe310000.sdhci/by-name/sys-prod |
/sys_prod |
ext4 |
| chip-prod | /dev/block/platform/fe310000.sdhci/by-name/chip-prod |
/chip_prod |
ext4 |
| userdata | /dev/block/platform/fe310000.sdhci/by-name/userdata |
/data |
f2fs |
因此,主机命令中的逻辑分区名最终会落到 by-name/<partition> 指向的 eMMC 分区节点。完成刷写并重启后,内核和 init 根据板级 fstab 与启动脚本组织最终根文件系统,系统才会使用新镜像中的 SO。特别要注意:这份 RK3568 fstab 把 system 块设备挂到 /usr,而镜像内部又包含 system/ 目录及根级软链接;不能仅凭镜像文件名推断运行时挂载点,必须结合该产品完整启动流程判断最终可见路径。
6.5 Bootloader 工具烧录与 Flashd 的边界
开发板首次烧录或系统无法启动时,通常无法先进入 Flashd。这时要进入芯片 BootROM/Loader 模式,使用板厂或芯片厂商工具写入 loader、boot、system、vendor 等分区。对于 RK3568,常见工具是 Windows 侧的 RKDevTool 或 Linux 侧的 Rockchip upgrade_tool/同类下载工具;具体按钮、短接点、loader 文件和分区配置取决于开发板 BSP。
两种路径的差异如下:
| 维度 | Flashd/HDC | Bootloader 厂商工具 |
|---|---|---|
| 前置条件 | updater 可启动、设备通常需处于允许刷写状态 | BootROM/Loader 可枚举即可 |
| 协议终点 | OpenHarmony updater 中的 Flashd 服务 | 芯片 BootROM 或厂商 Loader |
| 典型用途 | 开发阶段单分区更新、整包升级 | 首次烧录、救砖、刷 loader/boot/分区表 |
| 源码可见性 | OpenHarmony 源码中可完整追踪 | 多数实现位于厂商闭源或 BSP 工具中 |
| 写入目标 | 由 Flashd 解析分区并创建 DataWriter | 由厂商工具和 parameter/分区表决定 |
因此,“image 如何刷入板子”没有唯一工具答案,但底层本质一致:根据分区表找到目标块区域,将镜像字节流写入该区域;重启后由 Bootloader、内核和 init 按启动与挂载配置消费这些分区。
7. 以 system SO 为例还原一次完整执行
7.1 BUILD.gn 配置
假设有一个 64 位共享库:
# 示例:foundation/example/BUILD.gn
import("//build/ohos.gni")
ohos_shared_library("demo_runtime") {
sources = [ "demo_runtime.cpp" ]
install_images = [ "system" ]
relative_install_dir = "demo"
part_name = "demo_component"
subsystem_name = "demo_subsystem"
}
模板推导结果为:
模块逻辑名:demo_runtime
链接文件名:libdemo_runtime.so
模块类型:lib64
目标分区:system
镜像内路径:/system/lib64/demo/libdemo_runtime.so
7.2 中间产物的形态变化
| 阶段 | 产物示意 | 本质 |
|---|---|---|
| 链接 | out/<product>/.../libdemo_runtime.so |
ELF 文件 |
| 元数据 | out/<product>/obj/.../demo_runtime_module_info.json |
source/dest 安装契约 |
| 安装 | out/<product>/packages/<platform>/system/lib64/demo/libdemo_runtime.so |
分区 staging 文件 |
| 制镜像 | out/<product>/packages/<platform>/images/system.img |
ext4 文件系统映像 |
| 烧录 | by-name/system 对应 eMMC 区域 |
物理分区内容 |
| 启动后 | 由板级挂载布局映射出的运行时路径 | 目标机文件系统中的 SO;常见逻辑位置为 /system/lib64/...,但应以产品启动配置为准 |
路径中的 <platform>、目标挂载点和输出目录会受产品配置影响。判断真实路径时,优先读取生成的 module info、platforms_list.gni、镜像 action 和设备 fstab,而不是只依赖文档中的固定示例。
7.3 增量验证方法
构建后可以按以下顺序验证链路,每一步都比直接刷整机更容易定位问题:
# 1. 找到模块元数据
find out/<product> -name '*demo_runtime*module_info.json'
# 2. 检查 source、dest 和 install_enable
python3 -m json.tool <module_info.json>
# 3. 检查 staging 目录中的 SO
find out/<product>/packages -path '*system/lib64/demo/libdemo_runtime.so'
# 4. 检查 ELF 架构和动态依赖
file <staging-so>
readelf -h <staging-so>
readelf -d <staging-so>
# 5. 只重建 system 镜像,目标名以本地 GN 图为准
ninja -C out/<product> system_image
# 6. 查看实际镜像生成命令,确认配置和输入目录
ninja -C out/<product> -t commands | grep 'system.img'
刷写前,还应核对镜像大小小于目标分区容量,并确认 raw/sparse 格式是否被所用工具支持。
8. 设计评价、常见陷阱与排查方法
8.1 为什么采用“元数据加 staging”而非直接制镜像
| 设计点 | 优点 | 代价 | 缓解方式 |
|---|---|---|---|
| 模块生成独立安装元数据 | 编译目标与产品安装策略解耦 | 中间 JSON 多,调用链较长 | 从 module info 的 source/dest 开始排查 |
| 先构造分区 staging | 镜像前可直接检查内容,支持多文件系统后端 | 占用额外磁盘空间 | 增量构建;只保留所需产品输出 |
| 通用配置加设备覆盖 | 公共逻辑可复用,设备可改容量和标签 | 容易看错实际生效配置 | 查看 Ninja 命令与 packages/imagesconf |
| Flashd 流式写块设备 | 不需在设备侧保存完整镜像 | 中途断电会留下不完整分区 | 在 updater 环境刷写,完成后校验并重启 |
这个设计的核心取舍是:增加中间层,以换取产品裁剪、可检查性和多设备复用。
8.2 常见陷阱
-
SO 编译成功但不在镜像中
先检查install_enable、所属部件是否进入产品,以及 module info 是否被${platform}_install_modules聚合。不要直接怀疑镜像工具。 -
把
output_dir当成安装目录
output_dir影响编译产物落点;module_install_dir和relative_install_dir才影响镜像内路径。 -
把 system/vendor 当成链接属性
install_images是制品安装属性。它不改变.so的 ELF 内容,只改变dest。 -
修改了默认 image conf 但没有生效
如果packages/imagesconf/<name>_image_conf.txt存在,设备配置会覆盖默认配置。 -
只复制新 SO 到 staging,却没有重建 image
开发板读取的是已刷入分区,不读取宿主机 staging。修改 staging 后必须重新生成并刷写对应 image,或在调试设备上通过可写挂载单独推送文件。 -
镜像格式与烧录工具不匹配
部分工具接受 raw image,部分接受 sparse image。sparse_image会触发img2simg,刷写前应确认接收端能力。 -
刷错分区名
构建侧名称可能使用sys_prod,块设备侧使用sys-prod。以设备 fstab、分区表和by-name节点为准,不能机械复制文件名。 -
设备锁定导致 Flashd 拒绝操作
daemon_updater.cpp:41-52在命令分发前调用IsDeviceLocked(),锁定设备会返回operation is not allowed。 -
源码版本内部接口不一致
本文检查到build/ohos/images/BUILD.gn使用--output-image,而build_image.py声明--output-image-path。应以真实构建错误和生成命令为准修复版本错配,不能假定仓库任意时刻都完全自洽。
8.3 FAQ
Q1:只编译一个 .so,为什么不会自动更新 system.img?
因为单模块链接目标只保证 ELF 更新。只有构建 packages/install_modules 与 system_image 目标时,才会重新复制 staging 并制作镜像。
Q2:怎样最快判断 SO 最终会进入哪里?
查看该目标生成的 *_module_info.json。其中 source 是编译产物,dest 是最终分区内路径,这是最直接的证据。
Q3:为什么 64 位 SO 在 lib64,有些 NAPI 库却在更深目录?
架构先决定 lib64,relative_install_dir 再追加 module/... 等业务子目录。
Q4:镜像中有 SO,启动后为什么仍然加载旧版本?
依次检查:是否刷到了正确分区、刷写是否成功、设备是否从另一槽位或另一分区启动、挂载点是否对应预期分区、进程是否仍持有旧映射。必要时重启并在设备端核对文件哈希。
Q5:Flashd 能否刷 loader 和分区表?
Flashd 更适合 updater 可启动后的系统分区操作。首次烧录、loader、分区表或救砖通常使用芯片厂商 BootROM/Loader 工具,具体能力取决于 BSP。
Q6:为何不直接把 SO 打进 boot 镜像?
普通运行时 SO 属于 system/vendor 文件系统内容。boot 镜像主要承载内核和启动 ramdisk;只有 updater 或早期启动明确需要的库,才会通过 install_images = [ "ramdisk" ] 或 updater 进入对应环境。
9. 总结
OpenHarmony 把编译后的 SO 打包并刷入开发板,可以压缩为五个阶段:
| 阶段 | 核心文件/模块 | 关键产物 |
|---|---|---|
| 编译 | build/templates/cxx/cxx.gni |
libxxx.so |
| 描述安装 | module_info.gni、gen_module_info.py |
*_module_info.json |
| 构造分区 | build/ohos/packages/modules_install.py |
packages/<platform>/system/lib64/... |
| 制作镜像 | build_image.py、mkimages.py、mkextimage.py |
system.img 等 |
| 写入设备 | HDC、Flashd、Partition、DataWriter | eMMC/UFS 对应分区内容 |
最重要的理解不是记住某条固定输出路径,而是掌握下面这条连续转换链:
编译文件 -> 安装元数据 -> 分区目录树 -> 文件系统镜像 -> 块设备分区
排查问题时也应严格按这个顺序前进:先看 SO,再看 module info,再看 staging,再看 image 生成命令,最后才看刷写协议与设备分区。这样才能准确区分“没有编进去”“没有安装进去”“没有重新制镜像”和“没有刷到正确分区”这四类完全不同的问题。

浙公网安备 33010602011771号