背景
目标分析
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, %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 包含的关键信息如下:
- 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 程序的代码段
实验
- 准备一个使用 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 调试
- GRUB 命令行引导 OS
- TBD
- 检查串口输出
- TBD
持久配置
- 添加自定义 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
}
- 将新的启动项设置为 GRUB 默认启动项:
cp 40_custom /etc/grub.d/40_custom
grub2-mkconfig -o /boot/grub2/grub.cfg
grub2-set-default "bit bootloader"
- 检查后重启
grub2-editenv list | grep saved_entry
reboot