简介:这是面向x86架构的OpenHarmony引导程序资源包,用于在标准PC硬件上启动OpenHarmony系统,解决开源操作系统在通用计算平台上的引导与适配问题,适合嵌入式开发者、系统移植工程师及对开源系统底层机制感兴趣的进阶学习者,作为理解引导加载原理的重要参考。压缩包共包含311个文件,大小仅为4.43MB,结构紧凑;其中主体为265个mod模块文件,用于扩展引导程序对各文件系统与设备的支持能力,3个efi启动固件承担UEFI引导链的初始加载,8个lst与2个cfg配置类文件管理模块依赖关系和启动参数,另有23个png图示辅助直观理解引导过程,并附有readme与sh脚本便于查阅使用。已有1701人学习或下载过该资源。通过对照源码结构、grub配置与各模块文件,读者可以深入了解引导加载的工作原理、UEFI启动流程以及OpenHarmony的模块化设计,为后续裁剪定制引导程序、适配特定硬件提供具体参考。
1. 从「点不亮」到「一条命令进系统」:OpenHarmony X86 引导程序的价值
拿到一块 X86 开发板,第一件事不是编译系统,而是把引导程序折腾明白——这是 OpenHarmony 移植到 X86 平台上最容易被低估的环节。很多人以为内核编译出来就万事大吉,结果 U 盘插上去,屏幕黑在那,串口一点输出都没有,连报错的机会都不给。这个 OpenHarmony-X86 引导程序项目,解决的问题正是「内核如何被加载、根文件系统如何被挂载、启动参数如何传递」这条完整链路。它不是一个 flappy bird 式的示例,而是一份能直接对着源码研究、能改、能跑的引导实现,适合正在往 X86 或 QEMU 上移植 OpenHarmony 的开发者,也适合那些想搞懂系统启动底层机制的入门者。我会按「链路全貌 → 核心模块 → 构建部署 → 避坑 → 改造」的顺序,把它拆开讲清楚,每个关键环节都落到可以动手试的层面。
2. 引导链路全貌:从 UEFI 固件到根文件系统挂载
2.1 引导分几个阶段
OpenHarmony 在 X86 平台上的启动链路大体分成四个阶段。第一阶段是 UEFI 固件初始化,这个阶段由主板固件完成,跟我们的代码没关系,但它的配置直接决定后续能不能找到引导程序。第二阶段是引导加载器,一般用 GRUB 或者一个精简的 UEFI 应用,负责读取内核镜像和 ramdisk。第三阶段是内核初始化,OpenHarmony 的 kernel 起来之后会解析 cmdline、初始化驱动、挂载临时根文件系统。第四阶段是根文件系统切换,从 initrd 切到真正的系统分区。
这个项目把重点放在第二和第四阶段,引导加载器怎么找内核、initrd 怎么打包、根文件系统切换时的参数怎么传,这些才是移植时最常出问题的点。常见做法是用 GRUB 做引导,但 es 项目并没有直接抄 GRUB 的默认配置,而是针对 OpenHarmony 的镜像布局写了一套适配逻辑,包括分区名的映射和 cmdline 的动态拼接。
grub.cfg 核心配置(简化版): menuentry 'OpenHarmony X86' { insmod part_gpt insmod ext2 search --no-floppy --set=root --label ohos_root linux /vmlinux console=ttyS0,115200 root=LABEL=ohos_root rw initrd /ramdisk.img }这段配置的关键在search --label ohos_root这一行,它不是靠磁盘序号去猜设备,而是通过卷标定位根分区。这么做的好处是当机器上有多个硬盘或者 U 盘插在不同口时,引导程序依然能准确找到 OpenHarmony 的分区。root=LABEL=ohos_root是内核参数,告诉内核根文件系统在哪个设备上,rw表示以读写方式挂载,这样首次启动时 init 脚本才能正常写日志。串口参数console=ttyS0,115200在调试阶段特别有用,没有显示器也能完整看到启动日志。
2.2 分区表与镜像布局:引导程序依赖的磁盘结构
这个引导程序对磁盘分区有明确的约定,不是随便分个区把文件丢进去就能跑。整个系统镜像需要三个分区:第一个是 EFI 系统分区,格式为 FAT32,存放引导程序本身;第二个是根文件系统分区,建议 ext4,卷标固定为ohos_root;第三个是可选的 vendor 分区,用来存放设备相关的库和配置文件。
分区布局的合理性直接决定了后续 OTA 升级和日志采集的方便程度。vendor 分区单独拎出来,是因为 OpenHarmony 的 HDF 驱动框架允许不同厂商的驱动以动态库形式存在,如果把 vendor 塞进 root 分区,每次更新驱动都得重新刷整个系统。引导程序在挂载根文件系统之前,会先通过 GPT 表读取分区信息,然后根据卷标找到对应的分区设备节点。
2.3 这套引导方案的目录结构
. ├── bootloader/ │ ├── grub.cfg.in # 引导配置模板 │ ├── script/ │ │ ├── merge_cmdline.sh # 合并默认参数和用户参数 │ │ └── sign_boot.sh # 引导镜像签名 │ ├── uefi_app/ # 可选 UEFI 应用,替代 GRUB │ │ ├── main.c │ │ ├── load_kernel.c │ │ └── build.sh │ └── README.md ├── ramdisk/ │ ├── init.sh # 第一个用户态进程 │ ├── etc/ │ │ └── fstab # 根文件系统挂载配置 │ └── build_ramdisk.sh # 打包 initrd └── tools/ ├── mkimage.sh # 制作整盘镜像 └── flash.sh # 烧录脚本grub.cfg.in是一个模板文件,不是直接可用的配置,编译时会通过脚本生成最终版。这么设计是考虑到不同的开发板可能需要不同的默认 cmdline,模板机制允许在编译时注入。uefi_app目录下的代码是一个精简的 UEFI 引导应用,如果你不想依赖 GRUB,可以直接用这个方案,它通过 UEFI 运行时服务直接加载内核。
3. 核心模块拆解:启动参数拼接、内核加载与根文件系统切换
3.1 UEFI 入口阶段:设备初始化与内核镜像读取
在 UEFI 方案下,引导程序的入口不是 main 函数,而是 UEFI 规范定义的 entry point。这个项目的main.c里处理了三个阶段的工作:先初始化串口输出和存储设备,然后定位内核镜像分区,最后通过 LoadImage 和 StartImage 两个 UEFI 运行时服务把内核拉起来。
// uefi_app/main.c 关键逻辑 EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) { EFI_LOADED_IMAGE_PROTOCOL *loaded = NULL; EFI_HANDLE kernel_handle = NULL; EFI_DEVICE_PATH *kernel_path = NULL; // 1. 先拿当前镜像的加载信息,确定自己从哪个设备启动 gBS->HandleProtocol(image, &gEfiLoadedImageProtocolGuid, (VOID **)&loaded); // 2. 从 GPT 分区表中查找卷标为 ohos_root 的分区 EFI_GUID root_part_guid = find_partition_by_label(L"ohos_root"); if (root_part_guid == NULL) { output_serial(L"root partition not found, fallback to default\n"); return EFI_NOT_FOUND; } // 3. 构造内核镜像的设备路径并加载 kernel_path = construct_partition_path(root_part_guid, L"\\vmlinux"); gBS->LoadImage(FALSE, image, kernel_path, NULL, 0, &kernel_handle); // 4. 把内核镜像的启动参数通过 UEFI LoadOptions 传进去 CHAR16 *options = L"root=LABEL=ohos_root console=ttyS0,115200 rw"; gBS->StartImage(kernel_handle, NULL, NULL); return EFI_SUCCESS; }这段代码的注释已经标出了四个步骤的职责。HandleProtocol是 UEFI 里获取协议接口的通用方法,这里用来拿当前镜像的加载信息,目的是确定启动设备。find_partition_by_label是项目自己实现的 GPT 解析函数,它遍历分区表里的分区名,比对卷标字符串。如果找不到根分区,代码不会直接死循环,而是打印一段错误信息然后返回,方便你在串口上定位问题。
LoadImage和StartImage是 UEFI 规范里的标准服务,前者把内核镜像从磁盘读到内存,后者跳转到内核入口。值得注意的是LoadOptions这个参数,UEFI 规范允许在加载镜像时附加一段字符串作为参数传递,这个项目的做法是把完整的 cmdline 塞进去,省去了内核从磁盘重新读取配置文件的步骤,也减少了一层依赖。
3.2 启动参数拼接:cmdline 是怎么组装出来的
OpenHarmony 的启动参数比普通 Linux 发行版要啰嗦不少,除了常规的root和console,还需要传入ohos.softbus、ohos.boot这类系统服务标记,以及内存大小、显示分辨率的配置。如果把这些硬编码在 grub.cfg 里,换一块开发板就得改配置文件,很容易出现「参数漏传导致服务起不来」的诡异问题。
这个项目用了一个合并脚本,把不同来源的参数按优先级合并成最终 cmdline。
#!/bin/bash # script/merge_cmdline.sh # 用法: ./merge_cmdline.sh [平台名] PLATFORM=${1:-qemu_x86_64} BASE_CMDLINE="root=LABEL=ohos_root rw console=ttyS0,115200" # 平台相关的默认参数由 config 目录下的文件提供 PLATFORM_FILE="bootloader/config/${PLATFORM}.cmdline" EXTRA_ARGS="" if [ -f "$PLATFORM_FILE" ]; then EXTRA_ARGS=$(cat "$PLATFORM_FILE") fi # 用户可以通过环境变量追加参数,调试网络或日志时常用 if [ -n "$USER_CMDLINE" ]; then EXTRA_ARGS="${EXTRA_ARGS} ${USER_CMDLINE}" fi FINAL_CMDLINE="${BASE_CMDLINE} ${EXTRA_ARGS}" echo "${FINAL_CMDLINE}"这个脚本的逻辑并不复杂,但它的设计思路是「分层管理参数」。基础参数是任何平台都需要的,平台参数放在独立文件里,用户参数通过环境变量传入。实际使用中调试网络栈时,只需要执行USER_CMDLINE="ohos.softbus.ble=false" ./merge_cmdline.sh,就能在不改动工程文件的情况下临时关闭某个服务。
最终生成的 cmdline 会被写入 grub.cfg 或者传递给 UEFI 的 LoadOptions。这个分层设计在团队协作时特别有用——负责驱动的同事改平台参数文件,负责测试的同事用环境变量追加自己的调试参数,不会互相覆盖。
3.3 根文件系统切换:initrd 加载与 pivot_root 流程
引导程序把内核加载起来只是第一步,接下来内核会解压 initrd,把控制权交给 init 进程。OpenHarmony 的 init 进程在这个项目里是一个 shell 脚本,不是编译出来的二进制,这降低了改动门槛——只要能拿到镜像里的 ramdisk,任何修改都能直接看到效果。
#!/bin/sh # ramdisk/init.sh 精简版 # 在 ramdisk 阶段执行,负责真正的根文件系统挂载 mount -t proc proc /proc mount -t sysfs sysfs /sys # 等待 root 分区设备节点出现(USB 存储设备可能需要几秒钟枚举) ROOT_DEVICE="/dev/disk/by-label/ohos_root" for i in 1 2 3 4 5; do if [ -e "$ROOT_DEVICE" ]; then break fi sleep 1 done # 挂载真正的根文件系统 mount -t ext4 "$ROOT_DEVICE" /newroot # 切换根目录,这比 chroot 更彻底,会让 ramdisk 从内存中被释放 exec switch_root /newroot /bin/init这段脚本最容易忽略的是前面的sleep循环。在 X86 平台上,如果根文件系统放在 USB 移动硬盘或者某些 SATA 控制器后面,设备节点不是立刻出现的,内核需要时间枚举设备。没有这个等待逻辑,mount会报No such file or directory,而这个报错在串口上看起来和分区表损坏一模一样,特别容易误判。
switch_root和chroot的差别值得多说一句。chroot只是切换进程的根目录视角,但原来的 ramdisk 仍然占用内存;switch_root会把当前根目录的挂载点移走,让 initrd 占据的临时内存整个被释放。OpenHarmony 对内存占用敏感,尤其是低配 X86 设备,用switch_root能省出几十 MB 内存给系统运行。
3.4 fstab 与 vendor 分区的挂载时机
ramdisk 阶段的 init 脚本还负责读取fstab,这里面定义了 vendor 分区和 data 分区的挂载规则。
# ramdisk/etc/fstab LABEL=ohos_root / ext4 rw,noatime 0 1 LABEL=ohos_vendor /vendor ext4 ro,nodev 0 2 LABEL=ohos_data /data ext4 rw,noatime 0 2vendor 分区以只读方式挂载,这是厂家驱动库的安全性考虑。data 分区要挂载在 root 和 vendor 之后,因为 OpenHarmony 的应用沙箱和属性系统依赖 data 分区存储状态。如果你在实际操作中遇到「驱动库加载失败但文件确实存在于系统中」的情况,优先检查 vendor 分区是否被正确挂载——很多移植新手没有在 fstab 里配置 vendor,结果是驱动库的路径解析到了 root 分区上,导致符号链接断裂。
4. 构建与部署:从源码到可引导镜像的完整流程
4.1 环境准备:一套能编译的 Linux 主机
这个引导程序的构建环境要求不复杂,一个 Linux 发行版配上常规的编译工具链就能跑。项目没有依赖 OpenHarmony 全量源码,只有内核镜像和 ramdisk 打包脚本需要对应的工具。建议开发机内存不低于 8GB,因为mkimage.sh在制作镜像时需要把多个分区文件拼装在一起,内存不足会导致临时文件频繁交换到磁盘,整个打包过程慢得让人怀疑是不是死锁了。
依赖工具主要是mtools(操作 FAT 镜像文件)、e2fsprogs(格式化 ext4)、dosfstools,以及gcc和binutils。如果你是第一次编译,先跑一遍sudo apt install mtools dosfstools e2fsprogs把这几个装齐,后面每一步都用得上。
4.2 编译与打包:完整命令序列
整个构建流程不是一行命令搞定,它分三步。第一步编译 UEFI 引导应用,第二步创建 ramdisk 镜像,第三步把内核、ramdisk 和引导程序拼装成最终的磁盘镜像。
# 第一步:编译 UEFI 引导应用 cd bootloader/uefi_app make clean && make # 产出 build/ bootx64.efi # 第二步:打包 ramdisk cd ../../ramdisk ./build_ramdisk.sh # 产出 build/ramdisk.img # 第三步:制作可引导镜像 cd ../tools ./mkimage.sh -p qemu_x86_64 -o ohos_x86.img # 产出 ohos_x86.img三个脚本各自有日志输出,建议重点看mkimage.sh的分区打印信息。它在拼接镜像时会依次创建 EFI 分区、root 分区和 vendor 分区,并且打印每个分区的起始扇区和大小。这些信息在后续用dd验证镜像时非常有用,如果分区大小和预期不符,能在这一步就发现,不用等到启动阶段黑屏了再排查。
4.3 首次启动验证:串口日志里应该看到什么
把镜像写进 U 盘后,启动时观察串口输出。一次成功的引导,串口日志会呈现明显的三段特征:第一段是固件启动信息,通常以主板厂商的 logo 文本结束;第二段是 GRUB 或者 UEFI 引导应用的输出,能看到内核镜像找到的确认信息;第三段是内核日志,以Booting Linux on physical CPU或者类似字样开头,随后能看到 root 分区的挂载记录。
串口日志关键行(成功情况): [ 0.000000] Linux version 5.10.109 ... [ 1.236145] VFS: Mounted root (ext4 filesystem) on device 254:0. [ 1.238012] Freeing unused kernel memory: 4096K [ 1.240234] Run /bin/init as init process如果在第一段之后没有第二个阶段的输出,说明引导加载器没有执行,排查方向是 UEFI 启动顺序和 ESP 分区内文件路径。如果第二段正常但第三段卡在挂载根文件系统,查看内核版本与 ext4 模块是否编译进了内核,而不是以模块形式存在——ramdisk 阶段没有加载模块的能力,除非你显式在 init 脚本里探测并加载内核模块。
5. 避坑记录:这 5 个问题让引导程序翻过车
5.1 串口无输出但系统已经起来了
现象:显示器上能看到系统桌面,但串口终端一直黑屏,敲回车没反应。这不是系统崩溃,是引导参数里没加console=ttyS0,或者波特率不匹配。
原因:OpenHarmony 的 kernel 默认输出设备是显卡,串口需要显式指定才能打开。开发板常见的串口芯片是 16550 兼容的,但也有一些需要用console=ttyUSB0映射到 USB 转串口芯片。
解决:检查 grub.cfg 里的console=参数,确认波特率与串口终端设置一致。另外注意console=可以多次出现,内核会同时向所有指定的设备输出日志,调试阶段建议同时保留console=tty0和console=ttyS0,这样本地有显示能用,远端也能看串口。
5.2 分区卷标正确但找不到根分区
现象:提示root=LABEL=ohos_root无法解析,内核进入了 emergency mode。
原因:GPT 分区表里的卷标和实际格式化时的卷标不一致。常被忽略的是mkfs.ext4和tune2fs会保留旧卷标,如果你在同一个分区上重新格式化但没有显式指定-L参数,旧卷标会残留。
解决:重新格式化时显式指定sudo mkfs.ext4 -L ohos_root /dev/sdX2,然后用blkid /dev/sdX2确认卷标与引导参数一致。如果分区表里存在重复卷标,内核会选择一个——通常选第一个找到的,这种隐蔽问题最浪费排查时间。
5.3 initrd 打包过大,内核内存不够加载
现象:引导阶段直接卡死,串口日志只显示内核镜像加载完成,之后没有输出。
原因:OpenHarmony 的 ramdisk 如果塞入了过多调试工具和驱动,镜像体积可能超过 50MB。X86 平台上如果固件分配的初始内存域偏小,内核解压 ramdisk 时会因为内存不足直接放弃。
解决:控制 ramdisk 体积,只保留 init 脚本、常用 shell 和必要的设备节点。调试用工具放系统侧,不要往 ramdisk 里堆。检查内核日志中的内存布局信息,确认early_mem参数有没有限制可用内存。
5.4 根文件系统切换卡在 switch_root 阶段
现象:日志显示mount成功,但switch_root执行后进程直接退出,屏幕上只有 kernel panic。
原因:switch_root要求新根目录必须是一个挂载点,且当前进程的工作目录在切换前必须位于新根目录上。如果 init 脚本在挂载 root 之后执行了cd到某个临时目录,或者mount时没有把新根目录作为挂载点,就会触发这个报错。
解决:确认mount命令挂载的目录是空目录,且 init 脚本在调用switch_root前没有改变工作目录。我一般会在挂载后加一行cd /newroot作为兜底。
5.5 USB 设备枚举慢导致挂载超时
现象:把系统装在移动硬盘上启动,大约一半概率会卡在Waiting for device /dev/disk/by-label/ohos_root,重启后又能启动成功。
原因:USB 存储设备的内核枚举时间不稳定,取决于控制器和设备的握手速度。如果 init 脚本没有等待循环,直接挂载会失败;等待时间设得太短,慢速设备来不及枚举。
解决:把等待循环次数适当调大,并把rootwait加进 cmdline。这个是内核内置参数,会让内核一直等待根设备出现而不是只等固定时间。
6. 二次开发:把引导程序改造成你自己的启动框架
6.1 加入启动菜单:从硬编码到动态选择
这个项目默认只加载一个内核镜像,但实际开发中会同时存在两个版本——一个稳定版一个开发版,甚至可能需要启动宿主机 Linux 来对比行为差异。常见做法是在 GRUB 配置里增加多个 menuentry,配合GRUB_DEFAULT环境变量实现版本选择。
# 在 grub.cfg 里增加调试入口 menuentry 'OpenHarmony Debug (kernel 5.10.109)' { insmod part_gpt search --label ohos_root --set=root linux /vmlinux-debug root=LABEL=ohos_root rw console=ttyS0,115200 initrd /ramdisk-debug.img }改造思路是把mkimage.sh脚本里的复制动作改成根据环境变量选择镜像文件,这样你只需要维护vmlinux和vmlinux-debug两套内核,引导层不变。实际调试中,我的习惯是给调试版的内核增加nokaslr参数,防止地址随机化干扰崩溃日志的可读性。
6.2 支持 ramdisk 与 system-as-root 两种模式切换
OpenHarmony 的 X86 移植后期,系统会从 ramdisk 模式切换到 system-as-root 模式。前者的 root 文件系统被打包进 ramdisk,后者的 root 分区直接挂载,ramdisk 只承载 init 脚本。这个项目默认实现的是后者,因为在 X86 平台下直接挂载 root 分区更符合发行版习惯,也方便后续 OTA。
如果你想保留 ramdisk 模式,需要修改mkimage.sh里 root 分区的镜像打包逻辑,把整个 rootfs 目录树压缩进 ramdisk.img。此时 grub.cfg 里的root=参数要改成root=/dev/ram0,同时 init 脚本里要把挂载 root 分区的逻辑换成从 ramdisk 解压。两种模式各有优劣,ramdisk 模式更灵活但每次启动都要解压,system-as-root 模式启动更快但分区损坏时恢复麻烦。
6.3 验证方法建议
改完引导程序,最有效的验证方式是准备一台 QEMU 虚拟机和一个真实 U 盘,两边跑同一套镜像。QEMU 的-serial stdio参数可以把串口输出直接打到当前终端,配合截图对比,排查效率比纯物理机快不少。每次改动后,至少完整执行一遍「编译 → 打包 → 在 QEMU 里启动 → 登录 shell → 重启」的流程,确认改动没有引入回归问题。
这个项目我前后花了两个周末才真正吃透。最初以为只是个 grub.cfg 模板,后来才发现 UEFI 应用和 ramdisk 脚本加起来才是完整的引导链路。从那以后,我每次拿到一块新开发板,都会先按这个框架把引导层跑通,再碰内核配置——强迫自己走一遍「先看引导,再谈系统」的流程,确实少走了不少弯路。希望这份拆解能帮你少踩几个暗坑,顺利把 OpenHarmony 跑在 X86 上。
本文还有配套的精品资源,点击获取