RISC-V Linux 内核启动镜像头(Boot Image Header)完全解析:64 字节格式、字段语义与引导器实现
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
导读
本文以 Linux 内核仓库中 Documentation/arch/riscv/boot-image-header.rst 为核心,系统讲解 RISC-V Linux 解压内核镜像(decompressed kernel image)开头的 64 字节引导头(boot image header):包括每个字段的布局与字节序、text_offset/image_size/flags/version的取值规则、以及该头如何同时服务于传统引导器与 EFI stub。读完本文,你将掌握引导器(bootloader)、kexec、UEFI 固件解析 RISC-V Linux Image 所需的一切字段细节,并能对照 arch/riscv/include/asm/image.h 与 arch/riscv/kernel/head.S 在源码层面验证每一项规范。
一、为什么要有一个 64 字节的引导头
RISC-V 规范要求内核镜像必须携带一个固定格式、固定偏移的引导头,原因是引导链上的所有参与者——固件、U-Boot、OpenSBI、kexec、UEFI——都需要在不解析内核内部符号、不依赖链接器脚本的情况下,仅凭镜像开头的少量字节即可完成三件事:
- 判断该镜像是否是一个合法的 RISC-V Linux 内核(通过 magic 校验);
- 确定应当把镜像加载到内存的哪个偏移处(
text_offset); - 确定需要为镜像预留多少内存(
image_size)。
该头格式与 PE/COFF 兼容,并大量借鉴 ARM64 内核的引导头设计。文档明确指出,未来 ARM64 与 RISC-V 的引导头有望合并为一种通用格式,避免镜像头格式的泛滥(The intention is for this header format to be shared between multiple architectures,见 arch/riscv/include/asm/image.h 的注释)。
二、64 字节头的完整布局
文档给出了解压后的 Linux 内核镜像开头的 64 字节结构,字段按偏移排列如下:
| 偏移(Offset) | 字段 | 类型/大小 | 含义 |
|---|---|---|---|
| 0x00 | code0 | u32 | 可执行代码(第一条指令) |
| 0x04 | code1 | u32 | 可执行代码 |
| 0x08 | text_offset | u64,小端 | 镜像加载偏移 |
| 0x10 | image_size | u64,小端 | 内核有效镜像大小 |
| 0x18 | flags | u64,小端 | 内核标志位 |
| 0x20 | version | u32 | 本头的版本号 |
| 0x24 | res1 | u32 = 0 | 保留 |
| 0x28 | res2 | u64 = 0 | 保留 |
| 0x30 | magic | u64 =0x5643534952 | 魔数,小端,即 ASCII "RISCV" |
| 0x38 | magic2 | u32 =0x05435352 | 魔数 2,小端,即 "RSC\x05" |
| 0x3c | res3 | u32 | 保留,用于 PE COFF 偏移 |
上述布局在 C 语言侧由 arch/riscv/include/asm/image.h 中的struct riscv_image_header一一对应,字段顺序、大小与文档完全一致:
struct riscv_image_header { u32 code0; u32 code1; u64 text_offset; u64 image_size; u64 flags; u32 version; u32 res1; u64 res2; u64 magic; u32 magic2; u32 res3; };在汇编侧,arch/riscv/kernel/head.S 中的_start入口通过.dword/.word/.ascii指令按完全相同顺序填充这些字段,并明确警告:"Do not modify it without modifying the structure and all bootloaders that expects this header format!!"(不要在不修改结构体以及所有依赖该头格式的引导器的前提下改动它)。
2.1code0/code1:可执行代码区
code0与code1并非单纯的数据,而是真正的可执行指令。在非 EFI 配置下,_start的第一条指令是j _start_kernel,随后是 4 字节保留字,整体形成 8 字节对齐的可执行区域;当启用CONFIG_EFI时,code0会被替换为一条解码后恰好是 ASCII"MZ"的压缩指令(c.li s4, -13),这正是 UEFI 识别 PE/COFF 镜像的标志。
三、text_offset:镜像应当加载到哪里
text_offset表示内核镜像相对内存起始位置的加载偏移,小端存储。内核本身在 arch/riscv/kernel/head.S 中按配置写入实际值:
CONFIG_RISCV_M_MODE(M 模式运行)下为0 MB(/* Image load offset (0MB) from start of RAM for M-mode */);- RV64(
__riscv_xlen == 64)下为2 MB(0x200000); - RV32 下为4 MB(
0x400000)。
引导器必须将镜像加载到 RAM 起始地址加上text_offset的位置。需要特别指出的是,kexec 加载器把该值同时当作镜像的内存对齐要求来使用:在 arch/riscv/kernel/kexec_image.c 中,kbuf.buf_align = le64_to_cpu(h->text_offset),即以text_offset作为kexec_add_buffer()的缓冲对齐粒度,确保新内核被放到满足偏移约定的地址。
四、image_size:引导器的强制校验项
image_size表示内核的有效镜像大小(Effective Image size),由 arch/riscv/kernel/head.S 以_end - _start计算得出——即从入口符号到镜像结束符号之间的字节数。
文档强调:image_size对引导器是必填项,缺失将导致启动失败("Image size is mandatory for boot loader to load kernel image. Booting will fail otherwise.")。这一约定在 kexec 代码中得到了严格执行:image_load()首先检查h->image_size,若为 0 直接返回-EINVAL(arch/riscv/kernel/kexec_image.c),随后以le64_to_cpu(h->image_size)作为memsz申请目标内存(同文件 L72)。
五、flags:字节序标志
flags字段当前只定义了一位(Bit 0):内核字节序。1表示大端(BE),0表示小端(LE)。
在 arch/riscv/include/asm/image.h 中定义:
#define RISCV_IMAGE_FLAG_BE_SHIFT 0 #define RISCV_IMAGE_FLAG_BE_MASK 0x1 #define RISCV_IMAGE_FLAG_LE 0 #define RISCV_IMAGE_FLAG_BE 1 #define __HEAD_FLAGS (__HEAD_FLAG(BE))注意一个实现细节:目前当配置为CONFIG_CPU_BIG_ENDIAN时,编译会直接报错#error conversion of header fields to LE not yet implemented——也就是说,当前仓库中头字段到小端的转换尚未实现,因此实际编译出的内核头字段一律以小端写入,__HEAD_FLAG_BE恒为RISCV_IMAGE_FLAG_LE。
kexec 加载时同样会校验字节序匹配:image_load()读取h->flags得到be_image,与内核自身是否CONFIG_CPU_BIG_ENDIAN比较,不一致则拒绝加载(arch/riscv/kernel/kexec_image.c)。
六、version:版本兼容机制
version是一个 u32 字段,高低 16 位分别表示主、次版本:
| 位域 | 含义 |
|---|---|
| Bits 0:15 | 次版本号(Minor) |
| Bits 16:31 | 主版本号(Major) |
这种"主版本在高位、次版本在低位"的编码方式是为了保证新旧版本头部之间的兼容性("preserves compatibility across newer and older version of the header")。当前版本定义为0.2,对应源码宏(arch/riscv/include/asm/image.h):
#define RISCV_HEADER_VERSION_MAJOR 0 #define RISCV_HEADER_VERSION_MINOR 2 #define RISCV_HEADER_VERSION (RISCV_HEADER_VERSION_MAJOR << 16 | \ RISCV_HEADER_VERSION_MINOR)该值在 arch/riscv/kernel/head.S 通过.word RISCV_HEADER_VERSION写入头部。
版本 0.2 带来的行为变化
kexec 的image_probe()(arch/riscv/kernel/kexec_image.c)正是依据版本决定校验策略:
if (h->version >= RISCV_HEADER_VERSION && memcmp(&h->magic2, RISCV_IMAGE_MAGIC2, sizeof(h->magic2))) return -EINVAL;即:当头部版本 ≥ 0.2 时,改用magic2字段进行魔数校验;版本低于 0.2 的旧镜像则继续依赖magic字段。源码注释明确引用本文档作为该行为的依据:"According to Documentation/arch/riscv/boot-image-header.rst, use 'magic2' field to check when version >= 0.2."
七、两个魔数:magic与magic2
头部包含两个魔数,这是历史上 ARM64 头格式借鉴过程中的一个"事故"留下的双轨设计:
magic(0x30 偏移,u64):小端存储后为 ASCII"RISCV"。文档指出该字段自版本 0.2 起已废弃(deprecated),未来版本可能移除。它本应与 ARM64 头的magic字段对上,但遗憾的是并没有对上。magic2(0x38 偏移,u32):小端存储后为 ASCII"RSC\x05",即0x05435352。它取代magic与 ARM64 头对齐,是版本 0.2 及以后的实际校验依据。
对应源码宏(arch/riscv/include/asm/image.h):
#define RISCV_IMAGE_MAGIC "RISCV\0\0\0" #define RISCV_IMAGE_MAGIC2 "RSC\x05"写入位置见 arch/riscv/kernel/head.S:magic用.ascii写入 8 字节,随后.balign 4对齐后再用.ascii写入 4 字节的magic2,保证magic2恰好落在偏移 0x38 处。读者在手动解析镜像头时,应优先校验magic2,并注意区分两个字段的字节序与小端数值。
八、res3与 EFI stub:头如何兼作 PE/COFF
res3(0x3c 偏移,u32)当前保留,其用途是存放PE COFF 头偏移。RISC-V 的 EFI stub 复用了这个 64 字节头:UEFI 规范要求内核镜像开头必须是 PE/COFF 头才能作为 EFI 应用被加载,于是:
code0被替换为"MZ"魔数——在 arch/riscv/kernel/head.S 中,CONFIG_EFI开启时第一条指令是c.li s4,-13,该指令编码解码后恰为 ASCII"MZ"(源码注释原文:"This instruction decodes to 'MZ' ASCII required by UEFI.");res3(偏移 0x3c)指向 PE/COFF 头其余部分的起始位置:pe_head_start - _start(arch/riscv/kernel/head.S);- 完整的 PE 头内容由 arch/riscv/kernel/efi-header.S 中的
__EFI_PE_HEADER宏生成,其中按CONFIG_64BIT分别使用IMAGE_FILE_MACHINE_RISCV64/IMAGE_FILE_MACHINE_RISCV32机器类型、PE32+/PE32 可选头格式,并声明.text/.data两个节区。
也就是说,同一份镜像在非 EFI 引导器眼里是"跳转指令 + 引导头",在 UEFI 固件眼里则是一个合法的 PE/COFF 应用镜像。这是文档所述"该头格式与 PE/COFF 兼容"的具体落地。
九、实战:如何读取并校验一个 RISC-V Image 头
结合以上字段,以下给出引导器或工具代码读取头的推荐流程(字段取值与校验逻辑均有文档与源码依据):
- 读入前 64 字节,按
struct riscv_image_header(arch/riscv/include/asm/image.h)的布局解析; - 校验版本:读取
version(小端),主版本 =version >> 16,次版本 =version & 0xffff;当前内核为 0.2; - 校验魔数:若
version >= 0x00020000(0.2),比较magic2与0x05435352;否则比较magic与0x5643534952(kexec 的image_probe()即此逻辑,arch/riscv/kernel/kexec_image.c); - 确认镜像大小:
image_size必须非零,否则拒绝引导(文档强制要求); - 计算加载地址:
load_addr = RAM_START + text_offset,并将镜像按text_offset对齐放置(kexec 将其用作buf_align); - 按
flagsBit 0 判断字节序:1为大端、0为小端,与运行环境字节序不符则拒绝加载; - 可选支持 EFI:检测偏移 0 处是否为大端序可读的
0x5a4d(即"MZ"),若是,则按res3指向的偏移继续解析完整 PE/COFF 头(arch/riscv/kernel/efi-header.S)。
十、相关文档与源码索引
- 官方规范文档:Documentation/arch/riscv/boot-image-header.rst
- RISC-V 启动流程总览:Documentation/arch/riscv/boot.rst
- 头结构体定义与全部宏:arch/riscv/include/asm/image.h
- 头在入口汇编中的实际布局与
text_offset取值:arch/riscv/kernel/head.S - EFI PE/COFF 头生成宏:arch/riscv/kernel/efi-header.S
- kexec 对引导头的完整消费逻辑(probe/load):arch/riscv/kernel/kexec_image.c
总结
RISC-V Linux 的 64 字节引导头是整个引导链的"公共契约":text_offset决定加载位置,image_size决定内存占用,flags声明字节序,version提供向前兼容的演进通道,双魔数magic/magic2完成镜像合法性校验,而res3与"MZ"指令则让同一份镜像在 UEFI 环境下无缝变身 PE/COFF 应用。从本文引用的源码可以看到,无论是内核自身(head.S 写入)、kexec 加载器(kexec_image.c 消费)还是 EFI stub(efi-header.S 扩展),都严格遵循 Documentation/arch/riscv/boot-image-header.rst 定义的这一格式——理解它,是正确实现 RISC-V 引导器与启动调试的基础。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考