当前位置: 首页 > news >正文

multiboot 规范实践分析

文章目录

  • 背景
    • 目标分析
    • 引导流程
  • 实现
    • OS 镜像实现
    • 镜像元数据
      • header & program header
      • section header
  • 实验

背景

目标分析

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, %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 包含的关键信息如下:
  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 程序的代码段
    在这里插入图片描述

实验

  • TODO
http://www.lryc.cn/news/623802.html

相关文章:

  • 交叉编译 手动安装 SQLite 库 移植ARM
  • Python数据分析案例82——基于机器学习的航空公司满意度分析
  • 攻防世界—unseping(反序列化)
  • pytorch线性回归
  • (一)React企业级后台(Axios/localstorage封装/动态侧边栏)
  • iSCSI服务配置全指南(含服务器与客户端)
  • JMeter(进阶篇)
  • LeetCode算法日记 - Day 13: 前缀和、二维前缀和
  • es下载、安装、部署以及集成和mysql数据同步
  • **守护进程(Daemon)** 是一种在后台运行的特殊进程
  • 为什么神经网络在长时间训练过程中会存在稠密特征图退化的问题
  • Linux中聚合链路与软件网桥配置指南
  • 深入了解linux系统—— 线程控制
  • AI 编程在老项目中的困境与改进方向
  • 【Linux | 网络】高级IO
  • 63.不同路径
  • 分治-归并-315.计算右侧小于当前元素的个数-力扣(LeetCode)
  • C++ vector的使用
  • C语言(12)——进阶函数
  • 北京JAVA基础面试30天打卡12
  • 语音转文字,如何提升内容创作效率?
  • 智能汽车领域研发,复用云原始开发范式?
  • WebSocket--精准推送方案(二):实时消息推送-若依项目示例
  • 在职老D渗透日记day19:sqli-labs靶场通关(第26a关)get布尔盲注 过滤or和and基础上又过滤了空格和注释符 ‘)闭合
  • 【架构师从入门到进阶】第五章:DNSCDN网关优化思路——第十一节:网关安全-对称与非对称加密
  • 告别“测试滞后”:AI实时测试工具在敏捷开发中的落地经验
  • 【165页PPT】锂电池行业SAP解决方案(附下载方式)
  • 自动驾驶中的传感器技术34——Lidar(9)
  • 定时器中断点灯
  • 记一次安装OpenStack(Stein)-nova报错问题解决