multiboot 规范实践分析
文章目录
- 背景
- 目标分析
- 引导流程
- 实现
- OS 镜像实现
- 镜像元数据
- header & program header
- section header
- 实验
背景
目标分析
GRUB基础 — Multiboot规范 中介绍了 GRUB 遵循的 multiboot 规范如何定义 bootloader 与 OS 之间的交互接口,从而解耦 OS 与 bootloader,两者既可以独立演进,又可以相互配合实现 OS 的引导。本篇文章基于以上原理,以引导一个现成的、具有 OS 雏形的镜像为案例,介绍如何实现兼容 multiboot 规范的 OS 镜像。
引导流程
- 我们选择 BIOS + GRUB + OS 的方式实现对兼容 multiboot 规范的 OS 的引导,流程示意图如下,如图所示,引导分为三个阶段:
- BIOS 固件在实模式下执行初始化程序,完成后加载 GRUB 到物理内存并跳转到约定物理内存地址 0x7c00,将控制权交给 GRUB。
- GRUB 切换到保护模式下,执行 CPU 和 内存的初始化,并搜集 OS 可能要求必要的硬件信息,之后寻找目标引导镜像中的魔数并根据 multiboot 规范进行加载、启动信息设置以及 machine 状态设置。完成后按照 multiboot 规范跳转到指定地址,对于 ELF 格式镜像文件,即跳转到 ELF header 中的 e_entry 字段记录的地址,将控制权交给 ELF 程序。
- ELF 程序完成对硬件的全面接管,变成 OS 的角色继续运行。
实现
OS 镜像实现
- start.S
start.S 是 OS 中与 GRUB 按照 multiboot 规范交互的组件,也是运行 OS 代码的起点:
/** Copyright wgchnln. All rights reserved.* function:hypervisor boot* log:6.16.2019 first create this file** MISRA C requires that all unsigned constants should have the suffix 'U'* (e.g. 0xffU), but the assembler may not accept such C-style constants. For* example, binutils 2.26 fails to compile assembly in that case. To work this* around, all unsigned constants must be explicitly spells out in assembly* with a comment tracking the original expression from which the magic* number is calculated. As an example:** /* 0x00000668 =* * (CR4_DE | CR4_PAE | CR4_MCE | CR4_OSFXSR | CR4_OSXMMEXCPT) *\/* movl $0x00000668, %eax** Make sure that these numbers are updated accordingly if the definition of* the macros involved are changed.*//* MULTIBOOT HEADER *//* 定义 multiboot 镜像头的前两个字段: magic 和 flags */
/* magic: 0x1badb002,multiboot v1 镜像 */
#define MULTIBOOT_HEADER_MAGIC 0x1badb002/* flags: 0b10,OS 请求 GRUB 把可用内存区域放到 boot information 的 mem_* 字段* 如果原始的 E820 内存映射表可用,将其内容存放到 mmap_* 字段*/
#define MULTIBOOT_HEADER_FLAGS 0x00000002 /*flags bit 1 : enable mem_*, mmap_**//* * 使用伪汇编指令定义一个 multiboot_header section,汇编器将定义一个名为的 section* "a" 表示 section 需要加载到内存,没有 "x",表 section 非代码,没有 "w",表 section 只读 * 这个 section 作用是存放 multiboot 镜像的头部*/.section multiboot_header, "a"
/* 使用伪汇编指令要求汇编器在计算当前代码地址时按照 4 字节对齐 */.align 4
/* 使用伪汇编指令要求汇编器在当前 section 写入长度为 4 字节,值为 0x1badb002 的数据 *//* header magic */.long MULTIBOOT_HEADER_MAGIC
/* 要求汇编器在当前 section 写入长度为 4 字节,值为 0x00000002 的数据 *//* header flags - flags bit 6 : enable mmap_* */.long MULTIBOOT_HEADER_FLAGS
/* 按照 multiboot 规范要求,计算 checksum,计算原理是 magic + flags + checksum = 0 *//* header checksum = -(magic + flags) */.long -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
/* 要求汇编器定义一个 entry section,代码,需要加载到内存*/.section entry, "ax"
/* 要求汇编器计算当前代码地址时 8 字节对齐 */.align 8
/* 要求汇编器生成 32-bit 模式下的机器码 */.code32
/* * 声明标号 cpu_primary_start_32 全局可见,汇编器会将该标号设置为全局符号,可以被其它 .o 文件引用 * 该标号会被链接器设置为 ELF 程序的 entry(参考下面的链接脚本分析),GRUB 加载 OS 镜像完成后会直接跳转到这里* 自该标号之后的指令都属于 OS 程序,即 OS 程序自此正式运行*/.global cpu_primary_start_32
cpu_primary_start_32:
/* * GRUB 按照 multiboot 规范放置的 magic 和启动参数* eax 存放 magic,值为 0x2badb002,表示 OS 自身是被一个 multiboot 兼容的 bootloader 启动的,这里就是 GRUB* ebx 存放指向启动信息的内存指针*//* save the MULTBOOT magic number & MBI */movl %eax, (boot_regs)movl %ebx, (boot_regs+4)/* Disable interrupts */cli/* Clear direction flag */cld
/* 检测当前 CPU 是否处于 64-bit 的 long 模式 *//* detect whether it is in long mode** 0xc0000080 = MSR_IA32_EFER*/movl $0xc0000080, %ecxrdmsr/* 0x400 = MSR_IA32_EFER_LMA_BIT */test $0x400, %eax
/* * 如果已经处于 long 模式,跳过模式切换,跳转到标号 cpu_primary_start_64* 否则初始化页表并完成其它模式切换必要的工作,为进入 long 模式做准备*/ /* jump to 64bit entry if it is already in long mode */jne cpu_primary_start_64/* Disable paging */mov %cr0, %ebx/* 0x7fffffff = ~CR0_PG */andl $0x7fffffff, %ebxmov %ebx, %cr0/* Set DE, PAE, MCE and OS support bits in CR4* 0x00000668 =* (CR4_DE | CR4_PAE | CR4_MCE | CR4_OSFXSR | CR4_OSXMMEXCPT) */movl $0x00000668, %eaxmov %eax, %cr4/* Set CR3 to PML4 table address */movl $cpu_boot32_page_tables_start, %edimov %edi, %cr3/* Set LME bit in EFER *//* 0xc0000080 = MSR_IA32_EFER */movl $0xc0000080, %ecxrdmsr/* 0x00000100 = MSR_IA32_EFER_LME_BIT */orl $0x00000100, %eaxwrmsr/* Enable paging, protection, numeric error and co-processormonitoring in CR0 to enter long mode */mov %cr0, %ebx/* 0x80000023 = (CR0_PG | CR0_PE | CR0_MP | CR0_NE) */orl $0x80000023, %ebxmov %ebx, %cr0/* Load temportary GDT pointer value */mov $gdt64_desc, %ebxlgdt (%ebx)/* 0x10 = HOST_GDT_RING0_DATA_SEL*/movl $0x10,%eaxmov %eax,%ss /* Was 32bit POC Stack*/mov %eax,%ds /* Was 32bit POC Data*/mov %eax,%es /* Was 32bit POC Data*/mov %eax,%fs /* Was 32bit POC Data*/mov %eax,%gs /* Was 32bit POC CLS*//* Perform a long jump based to start executing in 64-bit mode *//* 0x0008 = HOST_GDT_RING0_CODE_SEL */ljmp $0x0008, $primary_start_long_mode/* 要求汇编器生成 64-bit 模式下的机器码 */.code64.org 0x200.global cpu_primary_start_64
cpu_primary_start_64:
/* 保存 GRUB 传递的参数 *//* save the MULTBOOT magic number & MBI */lea boot_regs(%rip), %raxmovl %edi, (%rax)movl %esi, 4(%rax)
/* 开始在 64-bit 模式下执行指令 */
primary_start_long_mode:/* Initialize temporary stack pointer */lea ld_bss_end(%rip), %rsp/*0x1000 = PAGE_SIZE*/add $0x1000,%rsp/* 16 = CPU_STACK_ALIGN */and $(~(16 - 1)),%rsp
/* 检查 CPU 是否处于 long 模式,直到检查通过 *//* detect whether it is in long mode* 0xc0000080 = MSR_IA32_EFER*/movl $0xc0000080, %ecxrdmsr/* 0x400 = MSR_IA32_EFER_LMA_BIT */test $0x400, %eax/* jump to 64bit entry if it is already in long mode */
/* 跳转到 long 模式标号 */ jne is_long_modeloop:jmp loop
/* 执行 C 语言 main 函数 */
is_long_mode:call mainjmp loop
......
- link_ram.ld.S
link_ram.ld.S 链接脚本组织 ELF 可执行文件 section,因为 multiboot header 中没有显示指明 OS 镜像的加载方式,因此 GRUB 会按照 multiboot 规范将 OS 镜像默认为 ELF 镜像,所以链接脚本生成的 ELF 可执行文件镜像就是 OS 镜像,对 ELF 可执行文件的组织就是对 OS 镜像的组织:
/* 定义 OS 内存区域起始地址为 1M,占用的长度为 32M */
#define LOAD_PHYSICAL_ADDR 0x100000
#define LOAD_PHYSICAL_LEN 0x2000000/* 告诉链接器,生成 ELF 文件时,将该标号的值作为 e_entry 字段的内容,完成 ELF 程序的入口地址设置 */
ENTRY(cpu_primary_start_32)/* 定义 CPU 能够使用的物理内存区域* lowram: 物理内存的起始 1M 区间,为 BIOS 和 GRUB 程序加载的区间* ram: 物理内存的 1M 到 32 M 区间,为 OS 可以使用的区间*/
MEMORY
{/* Low 1MB of memory for secondary processor start-up */lowram : ORIGIN = 0, LENGTH = 0x00010000/* 32 MBytes of RAM for HV */ram : ORIGIN = LOAD_PHYSICAL_ADDR, LENGTH = LOAD_PHYSICAL_LEN
}/* 告诉链接器,生成的 ELF 文件的每个 section 在物理内存的布局 */
SECTIONS
{
/* 告诉链接器,ram 内存区域的第一个 section 为 .boot section* 所有 .o 文件的 multiboot_header section 将放到这个 section* 这里 start.S 的对象文件 start.o 才有这个 section,其它对象文件中没有 * */.boot :{KEEP(*(multiboot_header)) ;} > ram
/* 告诉链接器,将 .entry section 紧挨着 .boot section 存放 */.entry :{KEEP(*(entry)) ;} > ram
/* 告诉链接器,将 .text section 紧挨着 .entry section 存放 */.text :{*(.text .text*) ;*(.gnu.linkonce.t*)*(.note.gnu.build-id)*(.retpoline_thunk)} > ram
/* * 告诉链接器,将下一个 section 的起始地址按照 32 M 对齐* 因此 32 M 以上的内存区域存放是的除上述三个 section 以外* 其它所有 section 存放的区域* */. = ALIGN(0x200000);.rodata :{*(.rodata*) ;} > ram.rela :{*(.rela*)*(.dyn*)} > ram......
}
镜像元数据
- 上面分析的 start.S 与其它 C 语言文件一起编译成对象文件之后,链接器通过链接脚本 link_ram.ld.S 生成 ELF 格式的可执行程序。
header & program header
- 通过命令
xxd -u -a -g 1 bootloader.elf
查看 ELF 镜像的开始一段区间的二进制数据,遵循 64-bit ELF 规范:
- 使用 readelf 工具
readelf -h bootloader.elf
读取的 ELF header 信息作为对比,两者相符,header 包含的关键信息如下:
- ELF 镜像预期的入口地址 0x100010,汇编器基于该地址计算所有标号的值,从而让 GRUB 跳转到此处后执行的代码中,标号可以与程序运行的线性地址相等
- ELF header 长度为 64 字节,范围即从文件头开始的 64 字节
- ELF program header 在镜像起始的 64 字节处,program header 表中共有 2 个 条目,每个条目长度为 56 字节
- ELF section header 在镜像起始的 62432 处,section header 表中共有 19 个条目,每个条目长度为 64 字节
- 使用 readelf 工具
readelf -l bootloader.elf
读取 ELF program header 信息作为对比,两者相符,program header 包含的关键信息如下:
- program 类型为 ELF 格式的可执行文件
- program header 表中包含 2 个 program header,内容从文件 64 字节偏移处开始
- 第 1 个 program 的内容在文件内 4k 偏移处,即 .boot section 的内容,它预期加载到内存的线性地址是 1M 处,预期加载到内存的物理地址也是 1M 处,这里线性地址等于物理地址
section header
- 通过命令
xxd -u -a -g 1 -s 62432 bootloader.elf
查看 ELF 镜像0xF3E0 开始处的数据,展示的 section header 的部分 entry 如下:
- 使用 readelf 工具
readelf -S bootloader.elf
读取 ELF program header 信息作为对比,两者相符,分析 section header 描述的 关键 section:
- 第 1 个 section 保留,被初始化为 0
- 第 2 个 section 为 .boot section,存放 multiboot 的 header,在 ELF 文件的 4K 偏移处,预期被加载到 1M 处的内存线性地址,长度为 12 字节
- 第 3 个 section 为 .entry section,存放 start.S 中的代码段,在 ELF 文件的 4K + 16 byte 偏移处,预期被加载到 1M + 16 byte 处的内存线性地址,长度为 565 字节
- 第 4 个 section 为 .text section,存放其它 C 程序的代码段
实验
- TODO