ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OpenHarmony X86引导程序:固件到内核交接与启动排查

OpenHarmony X86引导程序:固件到内核交接与启动排查

简介:面向X86架构计算机的OpenHarmony启动引导程序,适合需要在该平台部署、调试OpenHarmony系统的开发者与系统集成人员参考使用,重点解决引导加载、内核启动衔接及硬件适配等问题。压缩包共311个文件、约4.43MB,包含265个mod模块、3个efi引导文件,以及lst模块清单、pf2字体、cfg配置、png界面素材和readme说明等,覆盖GRUB引导所需的模块、配置与显示资源。已有1710人学习下载,说明该方向具备一定关注度。读者可借助其中的引导模块、配置清单和说明文件,理解X86平台下引导程序的组成与加载流程,排查启动配置、模块依赖和显示资源等问题,并据此开展裁剪、定制或集成验证,适合具备一定系统底层基础的开发者按需查阅。

1. 引导程序不是加载器:OpenHarmony X86 引导程序真正在翻译什么

反直觉的结论先摆在这儿:把 OpenHarmony 搬到 x86 机器上,卡住大多数人的不是图形栈、不是 HDF 驱动,而是上电后的头 300 毫秒。这 300 毫秒里没有虚拟内存、没有文件系统、没有 libc,CPU 刚从实模式或固件环境里走出来,手里只有一堆寄存器和几块约定好地址的内存。所谓 OpenHarmony X86 引导程序,干的就是在这 300 毫秒里,把固件那种“从某扇区读若干字节然后跳过去”的粗糙语言,翻译成内核那种“给我 e820 内存表、framebuffer 描述、initrd 物理地址和 ACPI 根表指针”的精确契约。

问题在于,这两端谁都不负责中间那一段。固件交权时只保证“控制权到了你手上”,内核接手时只保证“少一个字段我就早期 panic”,中间这块无人区就是引导程序的全部地盘。它踩空的表现还特别有迷惑性:黑屏、卡在开机 logo、init 报找不到分区、界面出来了但花屏,看上去全是驱动或用户态的问题,实际根因都埋在交接现场那几百字节的数据结构里。

2. 固件到内核:一次控制权交接里到底传了什么

2.1 两条固件路径:UEFI 与 Legacy 的分岔口

x86 上第一道分岔不在内核,在固件。老机器走 Legacy,新机器走 UEFI,两者对“引导程序”这个东西的形态要求完全不同,这也是做 x86 移植时最容易被低估的工作量来源。

Legacy 路径下,固件自检结束后读启动盘的第一个扇区(512 字节,末尾两字节必须是0x55AA),把这段机器码搬到物理地址0x7C00处,然后以实模式跳过去。此时 CPU 寻址上限是 1MB,A20 地址线还没打开,你能用的内存只有 640KB 出头。引导程序必须在这么点空间里完成“打开 A20 → 建立 GDT → 进保护模式 → 自己写磁盘读例程”这一整套动作,稍有不慎就是三重故障直接重启。这条路径不是历史包袱,而是很多工控机、老主板唯一能走的路。

UEFI 路径下,引导程序是一个标准的 PE/COFF 可执行文件(64 位机器上就是BOOTX64.EFI),由固件从 FAT32 的 ESP 分区里读出来加载,此时 CPU 已经在长模式,你拿到的是EFI_SYSTEM_TABLE和一堆BootServices:可以开文件、可以分配内存、可以枚举 PCI、可以查 GOP 拿 framebuffer。代价是你得在跳转到内核之前调用ExitBootServices,那一刻所有固件服务全部失效,之后的任何一次“我以为还能用”都会变成静默崩溃。

维度Legacy 路径UEFI 路径
可执行文件形态512 字节扇区 + 链式加载PE/COFF,FAT32 分区上的.EFI
起始 CPU 模式实模式(16 位)长模式(64 位)
可用内存约 640KB,需自行打开 A20几乎全部,通过 BootServices 分配
图形信息需自己调 VBE 中断拿模式信息用 GOP 协议直接拿 framebuffer 描述
交权前必做关中断、建页表、进长模式调ExitBootServices,之后固件服务全失效

常见的做法是两条路径的产物都出,共用同一份内核与 ramdisk,只在最前面套一个不同的“壳”。这样做的好处是核心逻辑(填交接结构、读文件、跳转)只写一份,坏处是壳里那次模式切换的代码必须单独测,因为它在两条路径上的入口条件完全不同。

2.2 交给内核的不是文件,是一组内存契约

很多人对引导程序的想象停在“把 kernel 读进内存然后跳过去”。真正决定成败的是跳过去之前那一刻,内存里摆着的那几张表。以 64 位交接为例,内核入口第一件事就是校验魔数,然后按固定偏移去读这些字段:

/* 引导器在跳转前必须填好的交接块,少填一项内核就可能在早期静默挂掉 */ struct boot_handoff { uint32_t magic; /* 交接协议魔数,内核靠它判断"这是不是一个合法的启动" */ uint32_t mmap_count; /* 物理内存条目数,不是总大小,是分段描述 */ uint64_t mmap_addr; /* 指向 e820 数组,每项 20 字节:基址、长度、类型 */ uint64_t initrd_addr; /* ramdisk 物理地址,必须按页对齐,否则解包会越界 */ uint32_t initrd_size; /* ramdisk 字节数,gzip 压缩后的长度,不是解压后的 */ uint64_t fb_addr; /* 线性 framebuffer 基址,0 表示这台机器没有可用图形输出 */ uint32_t fb_width; /* 像素宽,用户态第一次拿到的分辨率就是它 */ uint32_t fb_height; /* 像素高 */ uint32_t fb_pitch; /* 每行字节数,注意是字节不是像素,混了就是花屏 */ uint32_t fb_bpp; /* 每像素位数,常见 32,但通道顺序还要另看 */ uint64_t acpi_rsdp; /* ACPI 根表指针,电源管理与 PCI 枚举全靠它 */ char *cmdline; /* 内核命令行,控制台与 init 路径都从这里来 */ };

mmap_addr指向的 e820 表是最容易被糊弄过去的一项。它不是一个“总共有多少内存”的数字,而是一串(base, length, type)的三元组,描述哪段是可用的type=1、哪段是固件保留、哪段是 ACPI 可回收。引导器如果把这张表简化成“从 0 到某个上限都是内存”,内核就会把固件占用的区域当空闲页分配出去,然后在你最想不到的地方——比如枚举 PCI 的时候——读到一堆垃圾数据。血泪经验是:宁可原样透传固件给的表,一条都别自己改。

另一种常见的交接方式是 Multiboot2 规范,它用“标签”而不是固定结构来传递信息,扩展性更好,代价是引导器得在镜像里声明自己要什么。

.section .multiboot_header, "a" .align 8 mb2_start: .long 0xE85250D6 /* 规范魔数,引导器靠它认出这段头 */ .long 0 /* 架构:0 表示 32 位保护模式入口 */ .long mb2_end - mb2_start /* 头部总长度 */ .long -(0xE85250D6 + 0 + (mb2_end - mb2_start)) /* 校验和:四项相加必须为 0 */ /* 信息请求标签:相当于点菜,不点的引导器就不填 */ .short 1 /* 标签类型 1 = 信息请求 */ .short 0 .long 16 /* 标签长度 = 8 字节头 + 2 个 4 字节请求项 */ .long 4 /* 请求 basic meminfo:低端/高端内存各多少 */ .long 6 /* 请求 mmap:完整物理内存地图,比上面那个重要得多 */ /* 结束标签,缺了它整个头都不合法 */ .short 0 .short 0 .long 8 mb2_end:

这段头的作用是把“我需要什么”提前告诉引导器。校验和那行的算法是四项相加等于零,看起来像玄学,但它保证引导器不会把一段普通数据误认成 Multiboot 头。真正值钱的是那两个请求项:只点basic meminfo的话,内核拿到的只是“低端多少、高端多少”两个数字,等于没地图;点了mmap才能拿到带类型的分段描述。我一般会把它理解成一份接口约定——你不需要内核认识你,你只需要让内核认识它自己期望的那几个标签。

还有一个隐性契约是内存放置。内核镜像、ramdisk、交接结构三者不能重叠,而且 ramdisk 必须放在内核不会覆盖的位置。常见做法是把内核放在 1MiB 之后,ramdisk 紧跟着按页对齐放,交接结构放在低端一块固件声明为“保留”但实际可写的小区域,或者干脆自己分配后把地址写进去。一旦重叠,表现是内核解压到一半自杀式重启,日志什么都没有。

3. 镜像怎么组织:ESP、ramdisk 与分区账本

3.1 三段式布局:把“启动”和“运行”分开

镜像布局这件事,决定的是“改一行配置要不要重新烧盘”。做 x86 移植时我见过最省事的方案是把所有东西塞进一个 ramdisk,也见过最折腾的方案是分三个区各管各的,两者没有绝对优劣,取舍点在于迭代速度和内存占用。

分区格式典型大小内容谁在读
ESPFAT3264MiB引导器.EFI、内核镜像、ramdisk、引导配置固件 + 引导器
systemext41GiB 起用户态根文件系统(库、服务、应用包)内核挂载后由 init 使用
dataext4按需可写数据、日志、应用沙箱用户态服务

把内核和 ramdisk 放在 ESP 上而不是单独的/boot分区,好处是引导器只用实现一个 FAT32 读文件的能力就够了——FAT32 是固件本身就必须支持的格式,实现成本最低;坏处是 FAT32 单文件上限 4GiB,内核加 ramdisk 一旦超过就得拆,另外 FAT32 没有权限位,别指望靠它做任何安全检查。

这里有个容易被忽略的细节:initramfs 的格式。内核认的不是“一个压缩包”,而是“cpio 的 newc 格式再套一层 gzip”。这两步顺序反了,或者 cpio 用了别的新旧格式,内核会直接报“解压失败”然后 panic,而这条错误信息往往淹没在一屏日志里。

#!/usr/bin/env bash set -euo pipefail IMG=oh-x86.img ESP_OFF=$((1 * 1024 * 1024)) # 分区起始偏移,1MiB 对齐是 UEFI 的硬性要求 ESP_LEN=$((64 * 1024 * 1024)) # 1. 造空盘并按 GPT 分区。GPT 的备份头在盘尾,所以镜像要留出整盘空间 dd if=/dev/zero of=${IMG} bs=1M count=$((1 + 64 + 1024)) status=none parted -s ${IMG} mklabel gpt parted -s ${IMG} mkpart ESP fat32 1MiB $((1 + 64))MiB parted -s ${IMG} mkpart system ext4 $((1 + 64))MiB 100% # 2. 格式化 ESP。簇大小给 512 字节,小文件多的时候不浪费空间 mkfs.vfat -F 32 -n OHESP -S 512 ${IMG} --offset $((ESP_OFF / 512)) # 3. 先打 ramdisk:newc 格式的 cpio,再 gzip。顺序不能反 (cd rootfs && find . -print0 | cpio --null -o -H newc --quiet | gzip -9) > build/ramdisk.img # 4. 往 ESP 里塞文件。image@@offset 语法直接按偏移写,不需要 root 挂载 mmd -i ${IMG}@@${ESP_OFF} ::/EFI ::/EFI/BOOT ::/boot mcopy -i ${IMG}@@${ESP_OFF} build/BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI mcopy -i ${IMG}@@${ESP_OFF} build/kernel.bin ::/boot/kernel.bin mcopy -i ${IMG}@@${ESP_OFF} build/ramdisk.img ::/boot/ramdisk.img mcopy -i ${IMG}@@${ESP_OFF} build/loader.cfg ::/boot/loader.cfg

几个参数值得单独说。--offset接收的是扇区数不是字节数,所以ESP_OFF / 512这个除法必须整除,一旦分区起始地址不是 512 的整数倍,mkfs.vfat会写出一个“看起来能挂载、实际每次读都错位”的文件系统——这种盘的症状是引导器偶尔能起来偶尔不能,属于最常见的玄学故障之一。-H newc指定 cpio 的新格式(newc),老格式里 inode 和时间戳字段宽度不同,内核解析器只认 newc。gzip -9只是压得更狠,用别的 gzip 兼容实现也行,但别用 xz 或 zstd,除非内核配置里明确开了对应解压器。

find . -print0 | cpio --null这一对是给路径里有空格或特殊字符的情况兜底的。根文件系统里带空格的文件不多,但带中文名或者带[的资源文件并不罕见,不加--null会在打包阶段静默丢文件,然后用户态某个服务启动时报“找不到配置”,排查方向直接跑偏到配置解析上去。

3.2 引导配置文件:把可变的东西挪出二进制

引导器把内核路径、ramdisk 路径、命令行写死在代码里,第一次能跑通,第二次就后悔。配置文件的价值不在于“可配置”,而在于出事时你能改一行就验证一个猜想,不用重新编译引导器、重新打包、重新刷盘。

# loader.cfg:引导器在跳转前读取,每一项都直接落进内核的交接结构 timeout 3 default oh-x86 entry oh-x86 kernel /boot/kernel.bin ramdisk /boot/ramdisk.img # console 决定早期日志往哪走:先串口再 framebuffer,两条腿都留着 cmdline console=ttyS0,115200 earlycon=uart,mmio32,0x3f8 \ console=tty0 init=/init root=/dev/ram0 rw \ ohos.boot.revision=standard loglevel=7 end

console=出现两次是有意的:内核会把日志同时往两个控制台写,串口那条在图形还没起来的时候救过很多次场,framebuffer 那条在没法接串口的整机上才有用。earlycon比console更早生效,它不依赖完整驱动模型,只靠一个固定的 MMIO 地址往串口写字符,所以能在“内核已经跑起来但驱动还没初始化”的那段空窗期给出输出——那正是最容易黑屏的阶段。init=/init告诉内核第一个用户态进程是谁,这个路径写错的表现是内核打印Kernel panic: No init found,非常直白,反而是最好查的一类。root=/dev/ram0表示根文件系统就是 ramdisk,后面那个rw让 ramdisk 可写,调试期有用,量产后一般改成只读加 overlay。

4. 内核到用户态:init 怎么把一堆服务拉起来

4.1 init.cfg 与服务图:并行启动不是“一起启动”

内核挂载完 ramdisk、执行/init之后,控制权就交给了用户态。这一步之后,OpenHarmony 的启动逻辑基本由配置文件驱动,理解它的关键是搞清“并行”到底并的是什么。

{ "services": [ { "name": "ueventd", "path": ["/system/bin/ueventd"], "start-mode": "boot", "critical": true }, { "name": "hilogd", "path": ["/system/bin/hilogd"], "start-mode": "boot", "critical": true }, { "name": "samgr", "path": ["/system/bin/sa_main", "-s", "/system/profile/samgr.json"], "start-mode": "boot", "critical": true, "depend-on": ["ueventd", "hilogd"] }, { "name": "foundation", "path": ["/system/bin/foundation"], "start-mode": "boot", "depend-on": ["samgr"] } ] }

这里的critical: true是个强约束:这几个服务里任何一个起不来,init 会认为“用户态已经不可救”,触发一次用户态重启而不是继续往下走。ueventd排在最前是因为它负责根据内核通过 netlink 发来的设备事件创建/dev下的节点——没有它,后面所有要打开设备的服务都会拿到ENOENT,而错误信息往往只写“打开设备失败”,完全不提设备节点根本不存在。

depend-on构成一张有向图,init 的调度器解析这张图之后,无依赖的节点并行拉起。很多人把“并行启动”理解成“所有服务同时 fork”,这是误解:并行的意义是让那些彼此不依赖的服务在 I/O 等待期互相重叠,而依赖边保证samgr一定在foundation之前完成注册。系统服务管理器本身是一个注册中心,foundation启动时要向它注册自己的能力,注册失败会一直重试直到超时,超时之后的表现就是“桌面起来了但点任何东西都没反应”——看着像 UI 卡死,实际是服务注册没完成。

另外还有一个容易被忽略的机制是启动阶段(boot event)。init 会按阶段推进:early-boot阶段只做最基础的文件系统和设备节点准备,boot阶段拉起核心服务,late-boot阶段才允许拉起应用进程。应用进程由一个专门的孵化器进程 fork 出来,每个应用跑在独立沙箱里,沙箱的挂载命名空间在这一步才建立。所以“应用起不来”和“服务起不来”是两个完全不同的排查方向,先看它属于哪个阶段。

4.2 显示为什么先黑一下:framebuffer 的两次交接

从引导到出图,屏幕上那块 framebuffer 其实被交接了两次,中间那一下黑屏是设计使然,不是 bug。

第一次是引导器交给内核。引导器通过固件的图形协议拿到一块线性显存的基址、宽高、每行字节数和像素格式,填进交接结构。内核在极早期用这块内存做最简单的文本或位图输出,这就是你看到的启动日志。

第二次是内核交给用户态。显示服务启动后会重新枚举 PCI 总线,找到显卡设备,加载对应的显示驱动,创建自己的 surface 和合成器,然后把画面送到显存。这一次交接意味着旧的那块“内核随手用的 framebuffer”被释放或者不再被写入,中间就出现了短暂黑屏。

/* 用户态探测当前 framebuffer 参数,用来判断交接是否成功 */ #include <fcntl.h> #include <sys/ioctl.h> #include <linux/fb.h> int probe_fb(const char *node) { struct fb_var_screeninfo var; int fd = open(node, O_RDWR); if (fd < 0) return -1; /* 节点都不存在,多半是 ueventd 没起来 */ if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0) { close(fd); return -2; /* 节点在但驱动没绑定上 */ } /* 这里必须同时看 bpp 和通道偏移,只看 bpp 会把 BGRA 当成 RGBA */ if (var.bits_per_pixel != 32 || var.red.offset != 16 || var.blue.offset != 0) { close(fd); return -3; /* 格式和渲染管线预期不一致,会花屏 */ } close(fd); return 0; }

这段探测代码的价值在于把“黑屏”拆成三种不同的失败:节点不存在、驱动没绑定、格式不匹配。返回-1说明问题在更早的设备节点创建阶段,往ueventd方向查;返回-2说明设备在但驱动没认,往 PCI 枚举和驱动匹配表查;返回-3才是真正的显示问题,而且花屏和黑屏的根因常常是同一个——通道偏移搞反了,红蓝互换,某些纯色背景的界面上看起来就像“渲染失败”。

5. 排查:x86 引导失败时,日志断在哪一层

引导阶段的日志是分段出现的,判断故障位置最有效的方法不是看最后一行报了什么,而是看“最后一行之后本该出现什么”。下面几条是按发生频率排的。

5.1 现象:上电后完全无输出,串口也安静

原因几乎总是控制权没交出去:要么固件根本没找到引导器(ESP 分区不是 FAT32、文件名大小写不对、GPT 分区表损坏),要么引导器在跳转前自己崩了(页表没建好、栈指针指向未映射区域、ExitBootServices之后还去调固件服务)。

解决顺序是从后往前剥:先在引导器入口处往固定 I/O 端口0x3f8直接写一个字符,确认“我确实被加载了”;再在填完交接结构后写第二个字符,确认“我填完了”;最后在跳转前写第三个。三个字符出来几个,就知道断在哪一段,比任何日志框架都可靠。

5.2 现象:内核打印几行后停住,最后一句是解压相关的错误

原因是 ramdisk 格式不对:cpio 用了老格式、gzip 流里混了多个成员、或者打包时把build/这一层目录也打进去了导致路径变成./build/init而不是./init。最后一种最阴,因为 cpio 本身完全合法,内核也解压成功,只是解出来的第一个进程路径对不上,报的是No init found,很容易被误判成init=/init写错。

解决办法是先本地验一遍再上盘:把 ramdisk 单独解开,确认根目录下直接就是init、system、vendor这些目录,而不是套了一层。命令行里的init=路径也要和打包时的顶层目录严格一致。

5.3 现象:内核起来了,但报 mount 失败,随后 panic

原因是 system 分区的挂载参数和实际不符:分区没格式化、root=写的是 ramdisk 但 fstab 里又去挂一个不存在的设备、或者文件系统类型写成了 ext4 实际做的是别的。还有一种少见但真实的情况是分区表用 GPT 而固件以 Legacy 方式启动,读到的分区信息和内核解析出来的对不上。

解决方式是先确认挂载源:进引导器配置里把root=临时改成 ramdisk,让系统起来,然后在 shell 里手工执行挂载,把真实错误码打出来。mount的返回码比日志有用得多,ENODEV是设备不存在,EINVAL是文件系统类型不认,两者排查方向完全不同。

5.4 现象:服务都在跑,日志显示启动完成,但屏幕始终黑

原因是显示链路上的两次交接只成功了一次。内核早期用引导器给的 framebuffer 打得岀日志,说明第一次交接成功;用户态显示服务接管失败,说明第二次没成——常见于显卡的 PCI 设备被枚举到但驱动匹配表里没有对应 ID,或者驱动匹配上了但拿到的 BAR 地址被固件映射成了不可缓存区域,写进去的数据读不回来。

排查方法是在用户态跑一遍前面那段probe_fb,再用lspci之类的工具确认设备 ID 和驱动绑定状态。如果设备在、驱动没绑,问题在匹配表;如果绑了但写显存无效,问题在内存映射属性,得回头看交接时有没有把这段物理区间正确标记成设备内存。

5.5 现象:系统能起来,但偶发重启或时间跳变

原因是时钟源或内存映射的细节问题。x86 上可选时钟源有多个,固件给的 ACPI 表如果没透传好,内核可能选到一个精度很差甚至不可靠的源,表现是服务超时判断错乱、日志时间戳倒流。另一种是 e820 表被简化过,内核把某段固件保留区当成了普通内存,跑一段时间后被别的东西覆写,触发随机崩溃。

解决方式是启动后立刻检查内核选中的时钟源和完整的内存地图,和固件给出的原始表逐条比对。这一步看起来繁琐,但它能把“偶发”变成“必现”,而偶发问题最耗时间的地方从来不是修,是复现。

6. 设计取舍:为什么不写一个 UEFI 应用把一切搞定

最直接的方案其实很有诱惑力:既然 UEFI 环境下有完整的文件系统、内存分配、PCI 枚举能力,为什么还要分出引导器和内核两层?把内核直接编成一个.EFI应用,让固件加载它,省掉中间那层交接结构,不是更简单?

短期看确实简单,长期看会撞上三堵墙。第一堵是生命周期:UEFI 应用活在固件服务还活着的时候,一旦你想用内核自己的内存管理、自己的中断处理、自己的多核调度,就必须在某个点切断和固件的联系。这个切断点放在哪里,本质上就是引导器和内核的分界线——把它前移到“整个系统都是 UEFI 应用”,等于放弃了内核那套已经验证过的内存管理和驱动模型,去重新实现一遍。第二堵是可移植性:Legacy 机器上根本没有 UEFI 环境,走这条路等于直接放弃一半存量设备。第三堵是可调试性:引导器只有几百 KB,出问题可以逐字节打点;一旦把它和内核揉成一个几 MB 的固件应用,故障面立刻扩散到无法定位。

所以现实中的取舍是这样的:

方案迭代速度覆盖面调试难度适用场景
一体式 UEFI 应用快仅 UEFI极高单一机型验证
引导器 + 标准内核 + ramdisk中UEFI 与 Legacy低通用移植、长期维护
引导器 + 分区根文件系统慢两者皆可中需要持久化、量产出货

第二行和第三行的差别在“根文件系统放哪里”。全塞进 ramdisk 的优点是启动快、镜像自包含、不怕分区表出错;缺点是内存占用固定,ramdisk 解压后有多大就吃掉多少内存,而且改一个配置文件要重新打包整个 ramdisk。分区方案省内存、可增量更新,但每次启动都要真实挂载一次文件系统,挂载失败就是起不来。我一般会这么理解:调试期用 ramdisk,量产后切分区,中间靠引导配置里的root=一行切换,两条路都保留着。

还有一个常被忽略的取舍是“要不要兼容 Multiboot2”。自己定义交接结构更自由,字段想加就加,但代价是所有引导器都得自己写,别人想在你的镜像上做二次开发就得先读你的结构体定义。走 Multiboot2 的话,通用引导器可以直接用,代价是字段受规范约束,而且规范里的标签是按需填充的,你不点就没有。多数项目的选择是两者都留:内部用自己的紧凑结构,对外暴露一个 Multiboot2 兼容入口。

真正让我改变做法的是一个很具体的教训。早期我为了省事,把内核镜像的加载地址写死在引导器里,觉得反正这台机器的内存布局就这样。结果换了一台内存更大的机器,固件把可用内存的起始位置上移了,内核正好被加载到一段被固件标记为保留的区域上,跑起来一切正常,直到某个服务开始大量分配内存,覆写了那段区域,系统在启动完成后的第 40 秒准时重启。那次之后,我每次改引导相关的东西,都会强制走一遍“打印完整 e820 表 → 逐条标注用途 → 确认内核和 ramdisk 落在 type=1 区间内”的流程,哪怕看起来完全没必要。引导这一层没有后悔药,它所有的错误都会延迟到别的地方爆发,而那时候你已经不在现场了。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表