OpenHarmony:编译后的 SO 如何进入镜像并刷入开发板

本文以当前源码树中的标准系统构建链为主线,追踪一个 ohos_shared_library 从链接产物、安装元数据、分区目录、文件系统镜像,到 Flashd 写入开发板分区的全过程。核心结论是:SO 并不是被链接器直接写进 image,而是先由构建模板生成“源文件到分区路径”的安装清单,再由打包阶段复制到分区目录树,最后由镜像工具把目录树序列化为 ext4/f2fs/cpio 镜像。烧录时,主机通过 HDC 传输镜像数据,开发板上的 Flashd 将数据流写入对应块设备。

目录


术语表

缩写 英文全称 本文含义
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

这条链路的关键分界点有三个:

  1. *_module_info.json:从“编译目标”切换到“安装目标”;
  2. packages/<platform>/<partition>/:从“零散模块”切换到“分区目录树”;
  3. <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_enabletrue,默认安装镜像为 system。如果模块显式设置 install_enable = false,链接仍可成功,但安装阶段会跳过它。

2.2 架构决定默认的 lib 或 lib64 目录

build/templates/metadata/module_info.gni:79-90 将模板中的逻辑类型 lib 转换为架构相关的安装类型:

  • arm64x86_64loongarch64riscv64 -> lib64
  • armx86 等 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 定义了标准分区目录名,包括 systemvendorramdiskupdatersys_prodchip_prodeng_systemeng_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 被逐个遍历:systemsystem_base_dirvendorvendor_base_dir,其余分区同理。最后在路径末尾拼接 libxxx.so

需要注意,module_install_dirrelative_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。

这意味着“源码仓里存在某个共享库目标”并不足以让它进入镜像。还需要同时满足:

  1. 目标被构建图引用;
  2. 所属部件被当前产品选择;
  3. module info 中 install_enable 为真;
  4. source 文件真实存在;
  5. 目标 dest 没有发生不可接受的冲突。

4.2 modules_install.py 执行真实复制

build/ohos/packages/BUILD.gn:280-359modules_install.py 注册为安装动作,并指定:

  • --platform-installed-path 为当前平台的 packages 目录;
  • 输出 system_module_info.jsonsystem_modules_list.txtsystem.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-47make_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 完成三件事:

  1. 对 system、ramdisk、updater 等分区补充必要目录和软链接;
  2. 在通用配置与设备专用配置之间选择实际配置;
  3. 调用 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-49build_rootdir() 会:

  1. 复制同级 root 目录到临时目录;
  2. 删除临时目录内原有 system
  3. 把当前 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 进一步说明:

  1. 监听 updater.flashd.configfs=1
  2. 停止普通 hdcd
  3. sys.usb.config 切换为 flashd
  4. 把 FunctionFS HDC 功能挂到 USB gadget;
  5. 绑定 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 常见陷阱

  1. SO 编译成功但不在镜像中
    先检查 install_enable、所属部件是否进入产品,以及 module info 是否被 ${platform}_install_modules 聚合。不要直接怀疑镜像工具。

  2. output_dir 当成安装目录
    output_dir 影响编译产物落点;module_install_dirrelative_install_dir 才影响镜像内路径。

  3. 把 system/vendor 当成链接属性
    install_images 是制品安装属性。它不改变 .so 的 ELF 内容,只改变 dest

  4. 修改了默认 image conf 但没有生效
    如果 packages/imagesconf/<name>_image_conf.txt 存在,设备配置会覆盖默认配置。

  5. 只复制新 SO 到 staging,却没有重建 image
    开发板读取的是已刷入分区,不读取宿主机 staging。修改 staging 后必须重新生成并刷写对应 image,或在调试设备上通过可写挂载单独推送文件。

  6. 镜像格式与烧录工具不匹配
    部分工具接受 raw image,部分接受 sparse image。sparse_image 会触发 img2simg,刷写前应确认接收端能力。

  7. 刷错分区名
    构建侧名称可能使用 sys_prod,块设备侧使用 sys-prod。以设备 fstab、分区表和 by-name 节点为准,不能机械复制文件名。

  8. 设备锁定导致 Flashd 拒绝操作
    daemon_updater.cpp:41-52 在命令分发前调用 IsDeviceLocked(),锁定设备会返回 operation is not allowed

  9. 源码版本内部接口不一致
    本文检查到 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 库却在更深目录?
架构先决定 lib64relative_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.gnigen_module_info.py *_module_info.json
构造分区 build/ohos/packages/modules_install.py packages/<platform>/system/lib64/...
制作镜像 build_image.pymkimages.pymkextimage.py system.img
写入设备 HDC、Flashd、Partition、DataWriter eMMC/UFS 对应分区内容

最重要的理解不是记住某条固定输出路径,而是掌握下面这条连续转换链:

编译文件 -> 安装元数据 -> 分区目录树 -> 文件系统镜像 -> 块设备分区

排查问题时也应严格按这个顺序前进:先看 SO,再看 module info,再看 staging,再看 image 生成命令,最后才看刷写协议与设备分区。这样才能准确区分“没有编进去”“没有安装进去”“没有重新制镜像”和“没有刷到正确分区”这四类完全不同的问题。

posted @ 2026-07-15 17:55  getmoon  阅读(12)  评论(0)    收藏  举报