资讯动态

OpenHarmony x86引导程序深度拆解:从GRUB到内核启动

发布时间:2026/10/17 10:49:40 来源:尧图企业网站定制
简介引导程序是系统启动的第一棒负责在开机自检后接管X86硬件将OpenHarmony内核装入内存并启动。这套面向X86架构的OpenHarmony引导程序包专为需要在PC、服务器及物联网设备上部署该操作系统的开发者与系统集成者设计解决了该操作系统在X86平台上的启动适配问题。资源共311个文件以efi引导器、mod内核模块、cfg配置脚本、lst模块依赖列表等类型为主并包含png示意图片与readme说明整体只有4.43MB便于快速下载、部署和二次开发其中efi文件负责UEFI引导mod文件是可加载模块cfg保存菜单配置lst记录依赖关系。当前已有1701人学习适合具备操作系统启动基础、希望深入Bootloader与内核加载机制的读者。通过grub.cfg、load.cfg可以了解启动菜单和内核路径的设置moddep.lst、fs.lst等文件则展示了GRUB各组件的依赖关系结合这些内容可以梳理出从UEFI固件到OpenHarmony内核的完整启动链路进而为系统裁剪、驱动适配和启动失败定位提供直接参考也可作为学习引导程序开发的入门样例。1. 一套 X86 引导程序先让它把日志打出来再谈移植拿到 OpenHarmony-X86 这个引导程序时我最关心的不是它能跑得多花哨而是它到底能不能在一台老式 x86 工控机上把系统带起来。实际上这类板子的坑远比想象中多BIOS 版本老、没有 EFI 分区、内存条频率诡异任何一个环节不对开机就卡在黑屏或者 GRUB 命令行里。这套引导程序的价值在于它把 OpenHarmony 的 X86 引导链路完整串了起来从 BIOS/UEFI 自检结束后的第一个指令到 GRUB 加载内核、传递 initrd、挂载根文件系统每一步都有对应的源码和构建脚本。适合两类人一是需要把 OpenHarmony 跑在 x86 工控机、迷你主机或者 VMware/QEMU 里的移植开发者二是想搞明白 bootloader 和 Linux 内核交互细节的系统学习者。这篇笔记我会按实际拆项目的顺序来讲先看目录和构建入口再逐段啃核心引导代码然后带你用 QEMU 和 U 盘分别验证最后聊怎么改成你自己的启动参数和串口日志。2. 整体架构与目录结构一条从开机到内核入口的完整链路2.1 Bootloader 在系统中的位置引导程序处于固件和操作系统内核之间。x86 平台通电后BIOS 或者 UEFI 固件完成自检然后根据启动顺序找到存储设备上的引导扇区或 EFI 分区。OpenHarmony-X86 引导程序在这条链路里的职责是把自己编译出的 bootloader 镜像放到固件能识别的位置然后由它负责加载内核镜像bzImage和根文件系统镜像initrd.img最后把控制权交给内核。这个项目和常见的 U-Boot 方案有区别OpenHarmony 官方主要面向 ARM 设备而 x86 上最省事的做法是基于 GRUB 2 来引导因为 GRUB 已经处理好了 BIOS、UEFI、多分区格式这些复杂问题。你看这个引导程序的源码结构会发现它并不是从零写一个 bootloader而是定制了一份 GRUB 配置外加一批启动辅助工具最终目标是把 OpenHarmony 的内核和 ramdisk 用标准的多引导协议加载起来。2.2 目录结构与文件职责我建议拿到源码后先不要急着编译把目录结构过一遍。下面这份文件清单是从实际构建环境里摘出来的核心部分路径作用bootloader/grub/GRUB 2 源码与补丁包含 x86 平台构建配置bootloader/grub_tools/构建 GRUB 镜像用的辅助脚本生成grub-mkimage需要的模块列表kernel/OpenHarmony 内核产物放置目录编译后会生成bzImageramdisk/最小根文件系统包含 init 进程和设备节点out/构建输出目录最终镜像在这里汇总make_image.sh一键生成可启动镜像的脚本grub.cfgGRUB 菜单配置决定默认启动项和内核参数这个结构最大的好处是分层清晰bootloader 是 bootloader内核是内核ramdisk 单独维护。如果你只改了内核参数没必要重新编译 GRUB如果你改了 GRUB 模块列表那也只需重新执行镜像打包那一步。2.3 构建入口Makefile 里做了什么整个构建过程的入口是一个顶层脚本核心逻辑就是按顺序执行清理、编译 GRUB、编译内核辅助模块、打包镜像。下面是一段简化后的构建逻辑#!/bin/bash # build.sh - OpenHarmony X86 引导镜像构建脚本 set -e OUT_DIRout GRUB_SRCbootloader/grub RAMDISK_DIRramdisk # 1. 清理上一次的构建产物避免旧文件干扰 rm -rf $OUT_DIR mkdir -p $OUT_DIR/boot/grub # 2. 进入 GRUB 源码目录编译 x86_64 平台的 GRUB 工具 cd $GRUB_SRC ./configure --prefix$OUT_DIR --targetx86_64 make -j$(nproc) make install # 3. 使用 grub-mkimage 生成一个独立的 GRUB 镜像 grub-mkimage -O x86_64-efi -o $OUT_DIR/grubx64.efi \ -p /boot/grub \ normal efi_gop gpt search configfile # 4. 拷贝内核和 ramdisk 到输出目录 cp $OUT_DIR/bzImage $OUT_DIR/boot/bzImage cp $OUT_DIR/initrd.img $OUT_DIR/boot/initrd.img cp grub.cfg $OUT_DIR/boot/grub/grub.cfg echo Build complete. Output at $OUT_DIR这段脚本里有几个参数值得注意。--targetx86_64指定了 GRUB 的目标平台如果改成i386-pc则会生成传统 BIOS 引导用的镜像适用于老设备-O x86_64-efi是 grub-mkimage 的输出格式对应 UEFI 启动-p /boot/grub是 GRUB 在运行时查找配置文件的路径前缀不要随意改否则启动时会出现找不到grub.cfg的情况。normal、efi_gop、gpt、configfile这些是 GRUB 模块normal是命令行和菜单支持efi_gop是 UEFI 图形输出协议gpt是 GUID 分区表支持configfile用于读取外部配置文件。如果你要支持从 MBR 分区表的移动硬盘启动需要追加biosdisk part_msdos两个模块这个我在后文避坑章节里会再提。3. 核心模块拆解多引导头、实模式切换与内核接管3.1 让 GRUB 认出内核multiboot2 头x86 平台上 GRUB 引导 Linux 内核并不需要内核自己实现 multiboot 协议因为 Linux 内核本身就带有 GRUB 能识别的引导协议。但 OpenHarmony-X86 引导程序里保留了一个单独的汇编文件专门用来演示和验证 multiboot2 头的结构这在调试自定义 bootloader 时非常有用。# multiboot_header.S - 多引导协议头定义 .set MAGIC, 0xE85250D6 # multiboot2 魔数固定值 .set ARCH, 0 # 0 表示 32 位 x86 架构 .set HEADER_LEN, header_end - header_start .section .multiboot, a header_start: .long MAGIC .long ARCH .long HEADER_LEN .long -(MAGIC ARCH HEADER_LEN) # 校验和必须满足四项之和为 0 # 帧缓冲请求标签让 GRUB 给我们一个可用的图形模式 .word 0, 0 .long 8 header_end:这段代码最关键的是校验和那个字段。GRUB 在加载这个镜像之前会检查magic arch header_len checksum是否等于 0如果不等于会直接报error: not a valid multiboot2 header。我自己在改这个头的时候踩过一次注释掉了一个标签后忘记重算 checksum结果 GRUB 死活不认。解决方法很简单用 Python 算一下补码# 计算 multiboot2 头校验和的辅助脚本 magic 0xE85250D6 arch 0 header_len 16 # 文件里从 header_start 到 header_end 的字节数 checksum -(magic arch header_len) 0xFFFFFFFF print(hex(magic), hex(arch), hex(header_len), hex(checksum))ARCH字段要注意0 代表 32 位 x86如果你的内核编译成了 x86_64这个字段应该填 4否则 GRUB 可能在跳转时用错误的模式加载你的内核。不过对于引导标准 Linux 内核的场景GRUB 主要靠bzImage里的 setup_header 来协商启动模式multiboot2 头更多是给自研内核或教学验证用的。3.2 从实模式到保护模式关中断、设 GDT、开 A20x86 处理器上电后处于 16 位实模式而现代内核要求进入 32 位保护模式后才能运行。这部分切换代码是整个引导程序里最玄学的一段因为涉及 CPU 的隐藏状态和 A20 地址线的历史包袱。# switch_protected.S - 实模式切换到保护模式 cli # 关闭外部中断防止切换过程中断 lgdt gdt_desc # 加载全局描述符表gdt_desc 在下方定义 # 开启 A20 地址线旧式键盘控制器方式 inb $0x64, %al # 读取键盘控制器状态端口 testb $0x02, %al # 检查输入缓冲区是否空闲 jnz . - 4 # 忙则自旋等待 movb $0xD1, %al # 写入 0x64 端口准备写输出端口 outb %al, $0x64 movb $0xDF, %al # 0xDF 是打开 A20 的值 outb %al, $0x60 # 设置 CR0 保护模式位 movl %cr0, %eax orl $0x01, %eax movl %eax, %cr0 jmp $0x08, $protected_entry # 远跳转刷新流水线加载新的 CS代码里的lgdt是加载 GDT 表gdt_desc是一段 6 字节的描述符包含了表的大小和基地址。A20 地址线的打开方式有很多种这里用的键盘控制器方式兼容性最好但在某些快速启动的机器上inb $0x64的判断可能会卡住超时机制是这类代码的老大难问题。如果遇到 A20 卡死常见方案是改用 BIOS 中断int 0x15 ax0x2401或者直接信任固件已经打开了 A20。保护模式切入后必须用远跳转jmp $0x08, $protected_entry这行代码强迫 CPU 重新加载代码段寄存器因为旧的 CS 值在保护模式下指向的段描述符可能并不存在。很多自己写引导程序的初学者会漏掉这一步结果跳转后直接触发异常。3.3 跳转内核bzImage 和 setup_header当 GRUB 完成加载后它并不会直接跳进 Linux 内核的代码段而是遵循 Linux 的 32 位引导协议。这个协议的核心约定在内核镜像头部偏移 0x1f1 处的setup_header结构里GRUB 需要读取其中的字段来确定加载地址和启动方式。// boot_params.c - 构造 boot_params 参数块 #include bootparam.h static void setup_boot_params(struct boot_params *params) { // 清空整个结构所有字段置零 memset(params, 0, sizeof(*params)); // setup_header 在 bzImage 中位于偏移 0x1f1 处 struct setup_header *hdr params-hdr; hdr-type 0x11; // 0x11 表示 bzImage 类型 hdr-loadflags 1; // LOADED_HIGH允许内核加载到高地址 hdr-kernel_alignment 0x1000000; // 16MB 对齐64 位内核惯例 // 指向 ramdisk 的物理地址 params-ramdisk_image RAMDISK_LOAD_ADDR; params-ramdisk_size ramdisk_len; // 内核启动命令行所在物理地址 params-cmd_line_ptr CMDLINE_LOAD_ADDR; }type 0x11表示这是一个压缩过的完整内核镜像bzImage而loadflags的LOADED_HIGH位告诉 GRUB 内核支持被加载到物理内存的高端。kernel_alignment是内核要求的对齐粒度如果 GRUB 把内核加载到了一个未对齐的地址后面的解压阶段会有概率触发不可描述的崩溃特征是随机地址的 page fault。ramdisk_image和ramdisk_size这两个字段决定了内核启动后去哪里找 initrd。这里有个常见误区ramdisk 的加载地址必须避开内核本身占用的区域否则内核解压时会直接覆盖掉 ramdisk。通常的做法是把 ramdisk 放在内核加载地址之后至少 16MB 的位置这个值在引导程序中以宏的形式定义。3.4 cmdline 与 initrd 的传递方式内核启动后首先解析cmd_line_ptr指向的命令行字符串这里可以配置 console、root 分区、init 路径等关键参数。引导程序在构建时会把一段默认的命令行写入固定地址运行时你也可以通过 GRUB 菜单的linux命令覆盖它。# grub.cfg 片段 - 默认启动项的内核参数 menuentry OpenHarmony X86 { echo Loading OpenHarmony bzImage... linux /boot/bzImage consolettyS0,115200n8 consoletty0 root/dev/ram0 rdinit/init echo Loading initrd image... initrd /boot/initrd.img }consolettyS0,115200n8把内核日志输出到串口波特率 1152008 位数据位无校验。另一个consoletty0表示同时输出到 VGA 文本终端。root/dev/ram0表示根文件系统直接从内存中的 ramdisk 挂载这对调试环境很实用因为不需要真实的磁盘分区。rdinit/init指定了 ramdisk 里的第一个用户态程序。参数顺序是有讲究的linux命令后面跟的字符串会按空格拆分成多个参数传给内核如果某个参数值里包含空格你需要用引号包起来或者用反斜杠转义。另外initrd命令必须写在对应linux命令之后因为 GRUB 会把 initrd 绑定到最近一次加载的内核上。4. 编译与运行QEMU 快跑和 U 盘真机启动4.1 交叉编译环境准备x86 平台的引导程序编译不需要复杂的交叉编译链因为目标平台就是 x86_64直接用宿主机自带的 gcc 就能产出可运行代码。但 GRUB 2 的构建依赖一些主机工具比如bison、flex、autoconf我在 Ubuntu 24.04 上编译时用过以下命令sudo apt update sudo apt install build-essential bison flex autoconf automake \ libtool gettext texinfo python3 unzip qemu-system-x86 make -j$(nproc)如果编译 GRUB 时报error: bison is required to build GRUB说明你缺语法分析器生成器装上 bison 和 flex 即可。另一个高频报错是缺乏python3模块GRUB 的构建系统有些版本依赖 python 生成头文件补装就行。4.2 构建产物验证编译完成后out/目录下应该有grubx64.efi和boot/grub/grub.cfg。在烧写到真机之前我习惯先检查镜像文件格式是否正确file out/grubx64.efi # 期望输出: PE32 executable (EFI application), x86-64 ls -lh out/boot/bzImage out/boot/initrd.imgfile命令输出的PE32 executable (EFI application)是关键如果输出的是DOS executable说明 grub-mkimage 把格式弄错了大概率是-O参数与目标平台不匹配。此时回到第 2.3 节的脚本检查--target和-O是否一致。4.3 QEMU 里跑起来QEMU 是验证引导程序最快的方式不必准备真机也能完整走一遍启动链路。下面这条命令可以从构建输出目录直接启动qemu-system-x86_64 \ -m 2048 \ -smp 2 \ -bios /usr/share/qemu/OVMF.fd \ -drive fileout/disk.img,formatraw,ifvirtio \ -nographic-m 2048给虚拟机分配 2GB 内存-smp 2使用 2 个虚拟 CPU。-bios /usr/share/qemu/OVMF.fd启用 UEFI 固件让虚拟机能从 GPT 分区表启动。如果你不想依赖 UEFI可以直接用默认的 SeaBIOS但要让 GRUB 以i386-pc模式构建。-nographic把串口重定向到当前终端这样内核的任何一点输出你都能立刻看到。如果 QEMU 里卡在 GRUB 命令行而无法加载内核可以在grub提示符后手动敲命令排查grub set root(hd0,gpt1) grub linux /boot/bzImage consolettyS0,115200 grub initrd /boot/initrd.img grub boot这一组命令的作用是绕过菜单配置手动指定内核和 initrd 路径。它能帮你快速判断问题出在 GRUB 的配置解析阶段还是内核加载阶段。4.4 制作 U 盘启动盘真机启动比 QEMU 多一道工序把镜像写入 U 盘。注意这个操作会清空 U 盘所有数据执行前先确认设备号sudo dd ifout/disk.img of/dev/sdX bs4M statusprogress sync/dev/sdX需要替换成你的 U 盘设备节点不要写成/dev/sdX1这种分区节点dd 需要写入的是整个磁盘。bs4M是单次写入块大小statusprogress显示写入进度。写入完成后在目标机器的 BIOS 里把启动优先级设为 U 盘优先启动时留意画面如果直接跳到 GRUB 菜单说明引导程序已被正确识别。5. 踩坑记录引导起不来的几种常见死法5.1 GRUB 报错 multiboot2 header invalid现象启动到 GRUB 菜单选第一项后屏幕返回error: not a valid multiboot2 header。原因这个报错发生在 GRUB 按 multiboot2 协议加载镜像时镜像开头缺少合法头结构或者校验和算错。我最开始在 multiboot_header.S 里加了一个信息请求标签但没更新header_len导致 GRUB 解析头越界。解决回到 3.1 节的汇编定义确认 MAGIC0xE85250D6ARCH0或4四项之和为 0。用 objdump 检查编译出来的 .multiboot 段是否真的在文件开头如果链接脚本把它排到了其他段后面GRUB 一样找不到。5.2 卡在 Loading kernel 之后没有任何输出现象GRUB 显示Loading kernel...后屏幕黑屏或冻结串口也没有日志。原因内核已经拿到控制权但又立即崩溃最常见的是loadflags里没有设置 LOADED_HIGH导致内核被加载到低于 1MB 的内存区域解压时覆盖了实模式中断向量表。另一类原因是内核命令行里的console参数指定了一个不存在的串口设备日志全被吞了。解决先确认 boot_params 里把内核加载地址抬高然后临时把 cmdline 改成consolettyS0,115200 earlyprintkserial,0x3f8,115200。earlyprintk会在内核正式初始化串口之前就启用输出能看到最早期日志。5.3 initrd 找不到或挂载失败现象内核启动日志打印RAMDISK: incomplete write或者VFS: Cannot open root device (hd0,msdos1)。原因这是我最容易踩的一个坑。ramdisk 被加载在某个物理地址但内核解压后需要立即读取它如果这个地址被内核自身覆盖了数据就坏了另一个原因是 root 参数指向了不存在的磁盘分区。解决把 ramdisk 加载地址改到内核起始地址后 16MB 之外同时确认ramdisk_size字段与实际 initrd 文件大小一致。调试时用rdinit/bin/sh让系统直接进 shell能绕过分区挂载问题。5.4 QEMU 里 854 × 480 低分辨率输出现象启动后画面分辨率很低字体发虚VGA 文本模式有明显的锯齿。原因GRUB 的efi_gop模块正常工作但 OpenHarmony 内核的 framebuffer 驱动固化了启动阶段的默认分辨率没有读取 GRUB 设置的 GOP 模式。解决在 grub.cfg 里加set gfxmode1024x768x32并执行videoinfo命令确认固件支持然后在linux命令后面追加内核参数video1024x768x32。如果还不行检查内核是否开启 DRM 的 EFI 支持项这个通常是编译内核时通过 Kconfig 控制的。5.5 真机上每次启动都进 GRUB 命令行现象U 盘插在目标机上启动直接落在grub提示符没有菜单。原因GRUB 找不到grub.cfg。最可能是-p /boot/grub这个路径与实际镜像里的文件路径对不上或者分区类型不受之前提到的模块支持。解决在grub提示符下执行ls查看可以访问的分区列表再ls (hd0,gpt1)/boot/grub/确认配置文件所在。确认后把-p参数改成实际路径并在 grub-mkimage 模块列表里加上part_msdos或part_gpt对应项。6. 二次开发改启动菜单、调整分区和开启串口调试6.1 修改 GRUB 菜单的默认行为拿到引导程序后第一件值得做的事是把默认启动项改成自己想要的内核参数组合。grub.cfg顶部有一组变量控制菜单行为set default0 set timeout3default0表示默认执行第一个 menuentry如果你有多个内核比如一个调试版一个发布版把它改成对应的索引即可。timeout3是自动启动的等待秒数调试阶段建议改成-1也就是无限等待防止你还没看清输出就自动进系统了。6.2 自定义内核参数和 init 进程我最常改的是linux那一行的参数。比如要让系统启动后挂载一个真实的 ext4 分区作为根文件系统而不是使用 ramdisklinux /boot/bzImage root/dev/sda2 consolettyS0,115200 rwroot/dev/sda2表示根文件系统在第二块硬盘的第二个分区rw表示以读写方式挂载。这样改的前提是你已经把根文件系统做了进去否则内核会报No init found。调试 initramfs 里的自定义 init 程序时则保持rdinit/init并在 ramdisk 的对应路径放一个可执行文件。6.3 串口日志的完整配置链路启用串口输出不只是改 cmdline 的事引导程序阶段也需要把 GRUB 的输出重定向到串口。在grub.cfg开头加一行serial --unit0 --speed115200 terminal_input serial terminal_output serial这样 GRUB 的菜单和错误信息也会走串口你可以用一条 USB 转串口线在另一台电脑上看完整的启动过程。注意--unit0对应主板上第一个串口经常被 BIOS 占用如果你用 ThinkPad 之类的机器可能要指定--unit1。6.4 改动验证从构建到启动的循环每次改完代码我习惯把验证流程固定成一串命令避免漏掉环节make clean make -j$(nproc) python3 tools/check_checksum.py out/boot/bzImage qemu-system-x86_64 -m 2048 -bios OVMF.fd -drive fileout/disk.img,formatraw -nographic第二步的校验脚本会自动检查 multiboot2 头或者 bzImage 的 setup_header 是否合法免得进了 QEMU 才报错。在 QEMU 里用-nographic能看到全部串口日志按CtrlA X退出模拟器。真机上验证时我会先在 U 盘上留一个 GRUB 命令行入口万一新参数把系统改崩了至少还能手动进命令行选择旧内核。这套循环跑顺了之后改一个内核参数的周期可以压缩到三分钟以内改 grub.cfg跑压缩脚本dd 到 U 盘插到目标机上看日志。中间章里提到的那些脚本我后来写成了一个boot_debug.sh每次改完强制走一遍校验确认 GRUB 模块列表和 cmdline 没有缺项之后再烧盘。从那以后我再也没有在生产环境遇到过因为引导参数写错而开不了机的情况。希望这套引导程序的拆解笔记能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑