multiboot v1 规范实践

发布于:2025-08-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

背景

目标分析

GRUB基础 — Multiboot规范 中介绍了 GRUB 遵循的 multiboot 规范如何定义 bootloader 与 OS 之间的交互接口,从而解耦 OS 与 bootloader,两者既可以独立演进,又可以相互配合实现 OS 的引导。本篇文章基于以上原理,以引导一个现成的、具有 OS 雏形的镜像为案例,介绍如何实现兼容 multiboot 规范的 OS 镜像。

引导流程

  • 我们选择 BIOS + GRUB + OS 的方式实现对兼容 multiboot 规范的 OS 的引导,流程示意图如下,如图所示,引导分为三个阶段:
  1. BIOS 固件在实模式下执行初始化程序,完成后加载 GRUB 到物理内存并跳转到约定物理内存地址 0x7c00,将控制权交给 GRUB。
  2. GRUB 切换到保护模式下,执行 CPU 和 内存的初始化,并搜集 OS 可能要求必要的硬件信息,之后寻找目标引导镜像中的魔数并根据 multiboot 规范进行加载、启动信息设置以及 machine 状态设置。完成后按照 multiboot 规范跳转到指定地址,对于 ELF 格式镜像文件,即跳转到 ELF header 中的 e_entry 字段记录的地址,将控制权交给 ELF 程序。
  3. 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, %ecx
    rdmsr
    /* 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, %ebx
    mov     %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, %eax
    mov     %eax, %cr4

    /* Set CR3 to PML4 table address */
    movl    $cpu_boot32_page_tables_start, %edi
    mov     %edi, %cr3

    /* Set LME bit in EFER */
    /* 0xc0000080 = MSR_IA32_EFER */
    movl    $0xc0000080, %ecx
    rdmsr
    /* 0x00000100 = MSR_IA32_EFER_LME_BIT */
    orl     $0x00000100, %eax
    wrmsr

    /* Enable paging, protection, numeric error and co-processor
       monitoring in CR0 to enter long mode */
    mov     %cr0, %ebx
    /* 0x80000023 = (CR0_PG | CR0_PE | CR0_MP | CR0_NE) */
    orl     $0x80000023, %ebx
    mov     %ebx, %cr0

    /* Load temportary GDT pointer value */
    mov     $gdt64_desc, %ebx
    lgdt    (%ebx)

    /* 0x10 = HOST_GDT_RING0_DATA_SEL*/
    movl    $0x10,%eax
    mov     %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), %rax
    movl    %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, %ecx
    rdmsr
    /* 0x400 = MSR_IA32_EFER_LMA_BIT */
    test     $0x400, %eax
    /* jump to 64bit entry if it is already in long mode */
/* 跳转到 long 模式标号 */    
    jne      is_long_mode

loop:
    jmp loop
/* 执行 C 语言 main 函数 */
is_long_mode:
    call main
    jmp 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 包含的关键信息如下:
  1. ELF 镜像预期的入口地址 0x100010,汇编器基于该地址计算所有标号的值,从而让 GRUB 跳转到此处后执行的代码中,标号可以与程序运行的线性地址相等
  2. ELF header 长度为 64 字节,范围即从文件头开始的 64 字节
  3. ELF program header 在镜像起始的 64 字节处,program header 表中共有 2 个 条目,每个条目长度为 56 字节
  4. ELF section header 在镜像起始的 62432 处,section header 表中共有 19 个条目,每个条目长度为 64 字节
    在这里插入图片描述
  • 使用 readelf 工具 readelf -l bootloader.elf 读取 ELF program header 信息作为对比,两者相符,program header 包含的关键信息如下:
  1. program 类型为 ELF 格式的可执行文件
  2. program header 表中包含 2 个 program header,内容从文件 64 字节偏移处开始
  3. 第 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. 第 1 个 section 保留,被初始化为 0
  2. 第 2 个 section 为 .boot section,存放 multiboot 的 header,在 ELF 文件的 4K 偏移处,预期被加载到 1M 处的内存线性地址,长度为 12 字节
  3. 第 3 个 section 为 .entry section,存放 start.S 中的代码段,在 ELF 文件的 4K + 16 byte 偏移处,预期被加载到 1M + 16 byte 处的内存线性地址,长度为 565 字节
  4. 第 4 个 section 为 .text section,存放其它 C 程序的代码段
    在这里插入图片描述

实验

  • 准备一个使用 GRUB 引导 OS 的 QEMU/KVM 虚机,将生成的 OS 镜像放置到 GRUB 可以识别的文件中,通过 multiboot 工具引导该 OS。

镜像准备

  • 生成 ELF 镜像工具并拷贝至 /boot 目录
cd /path/to/hedgehog
make
cp bootloader.elf /boot
  • /boot 目录为磁盘的第一个分区
  • TBD

设备配置

  • 为虚机配置 serial 设备,方便查看启动信息和调试:
    <serial type='pty'>
      <target type='isa-serial' port='0'>
        <model name='isa-serial'/>
      </target>
    </serial>

GRUB 调试

  1. GRUB 命令行引导 OS
  • TBD
  1. 检查串口输出
  • TBD

持久配置

  1. 添加自定义 GRUB 启动项 bit bootloader 的文件 40_custom
#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.

menuentry 'bit bootloader' --class openeuler --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-e23c76ae-b06d-4a6e-ad42-46b8eedfd7d3' {
	insmod gzio
	insmod part_gpt
	insmod ext2
	set root='hd0,msdos1'

	echo 'Loading bit bootloader ...'
	multiboot --quirk-modules-after-kernel /bootloader.elf
}
  1. 将新的启动项设置为 GRUB 默认启动项:
cp 40_custom /etc/grub.d/40_custom
grub2-mkconfig -o /boot/grub2/grub.cfg
grub2-set-default "bit bootloader"
  1. 检查后重启
grub2-editenv list | grep saved_entry
reboot

网站公告

今日签到

点亮在社区的每一天
去签到