65.9K
CodeProject 正在变化。 阅读更多。
Home

OX 引导加载程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2013年9月5日

GPL3

11分钟阅读

viewsIcon

23739

downloadIcon

385

OX 内核具有自己的自定义引导加载器,旨在引导 32 位保护模式内核。

引言

OX 内核具有自己的自定义引导加载器,旨在引导 32 位保护模式内核。加载器分为两个阶段实现,阶段 1 在 s1.s 中,阶段 2 在 s2.s 中。第一阶段是一个传统加载器,其代码组织在地址 0x7C00,即 PC BIOS 加载阶段 1 加载器的地址。阶段 1 必然由 512 字节的 16 位汇编代码组成,该代码使用 BIOS int 13 中断来加载阶段 2。阶段 2 是一个更大的 16 位汇编程序,由 16348 字节组成。因此,阶段 1 和阶段 2 的组合是 16 * 1024 + 512 = 16896 或 0x4200。内核紧随其后,是一个 32 位可执行文件,最大大小为 512 KB。vmox.img 包含 s1 阶段 1 加载器,紧随其后是阶段 2 加载器,再紧随其后是 32 位 ox 内核。然后,该镜像用空字节(值 0x0)填充到 1474560 字节,这是经典 1.44 MB 软盘的大小。这是为了能够通过 Virtual Box 虚拟机或 Bochs PC 模拟器使用软盘引导来引导内核。创建软盘镜像的程序随 ox 内核分发版提供,名为 mkboot。其源代码位于 boot/mkboot.c 中。还有一个用于提取二进制文件区域的工具,名为 get_data.c,其源代码位于 boot/elf/get_data.c 中。get_data 程序可用于检索 vmox.img 文件的部分。例如

./get_data vmox.img 0x0 0x200 s1
./get_data vmox.img 0x200 0x4000 s2
./get_data vmox.img 0x4200 0x2746d vmox.boot

上述命令从 vmox.img 引导盘中提取二进制文件。第一个命令将阶段 1 引导加载器的 512 字节检索到名为 s1 的文件中。第二个命令将阶段 2 引导加载器的 16348 字节检索到名为 s2 的文件中。第三个命令将 32 位内核镜像检索到名为 vmox.boot 的文件中。因此,get_data 程序的参数是镜像文件名,后跟文件中要检索镜像的十六进制偏移量,后跟十六进制长度,最后是要放置二进制镜像的输出文件。

get_data 工具还可以用于从 ELF 文件中提取 ELF 段。给定一个 32 位静态链接的 ELF 镜像(如 vmox 内核),可以首先使用以下命令查看各种段的详细信息

readelf -e vmox

例如,程序头可以是

  Type              Offset       VirtAddr       PhysAddr  FileSiz    MemSiz  Flg   Align 
  LOAD           0x001000 0x00100000 0x00100000 0x1f8c8 0x1f8c8   R E  0x1000 
  LOAD           0x021000 0x00120000 0x00120000 0x00e70 0x2d9c0  RW 0x1000 
  LOAD           0x0220d4 0x080480d4 0x080480d4 0x00024 0x00024  R     0x1000 
  NOTE           0x0220d4 0x080480d4 0x080480d4 0x00024 0x00024  R     0x4 
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4

请注意,该镜像有三个可加载段,这些段是由加载器加载到内存中的。PhysAddr 指示段在镜像文件中的起始位置,FileSiz 指示其在镜像文件中的大小。MemSiz 是段在内存中的大小。如果 FileSiz 小于 MemSiz,则加载器必须将差值清零。要使用 get_data 提取第一个段,请使用以下命令

./get_data  vmox 0x00100000 0x1f8c8 vmox.section1

阶段 1 加载器用于加载阶段 2 加载器,然后跳转到阶段 2 加载器在内存中的起始地址。阶段 2 从软盘驱动器加载整个区域,从扇区 1 开始,而不是扇区 0,因为扇区 0 是阶段 1 加载器。从阶段 2 加载器可以看出,内核的起始基地址是

%define _K_BASE 0x14000

这是阶段 2 加载器加载到内存段 0x1000 (地址 0x10000) 之后的地址,因此 32 位内核镜像的物理地址是 0x14000,因为阶段 2 占用了 0x4000 (16348 字节)。然后内核从内存段 0x1000 运行到 0x9000。因此,阶段 2 由三个主要部分组成

  1. 保护模式的初始化代码,重新启用实模式,启用 a20 线
  2. 从软盘加载阶段 2 和内核镜像
  3. 将 32 位 ELF 镜像转换为平面二进制文件,然后跳转到内核启动。

初始化代码源自 John Fine 的引导加载器 [1],并将处理器初始化为保护模式并启用 a20 线。a20 线允许处理器访问超过 1 兆字节的内存。实现此功能的代码位于第二阶段引导加载器文件 s2.s

cli 
enable_a20 
enable_pmode 
enable_rmode 
sti

请注意,在这个加载器中,使用了 NASM 汇编宏来使代码更具可读性和模块化。您可以通过在源代码中搜索它们来查看宏的详细信息。额外的 GDT 设置逻辑源自 Gareth Owen [2] 为 GazOS 开发的引导加载器。来自 [1] 和 [2] 的软件许可证分别为公共领域和 GPLv2。因此,OX 引导加载器也以 GPLv2 开放源代码的形式提供。GDT 逻辑设置了三个段描述符,一个用于 NULL 段,另外两个用于代码和数据。这是 enable_pmode 逻辑所必需的。

第一阶段和第二阶段加载器都需要从软盘驱动器读取以检索二进制文件并执行它们的逻辑。s1 加载器加载 s2 加载器,并且 s1 和 s2 加载器都是平面 16 位二进制文件。这意味着执行代码是将代码从软盘驱动器复制到 RAM 中的正确位置并跳转到它。然而,s2 加载器能够加载 32 位平面二进制内核或 32 位 ELF 静态链接内核。鉴于 ELF 允许加载器初始化内核的 .bss 段和未初始化内存,ELF 当然是首选格式。从软盘读取并加载到 RAM 的代码如下所示在 s1 中

%define _S2_LEN     0x21    ; (32 + 1) * 512 == 0x4000 
%define _S2_BASE    0x600   ; stage 2 base address 
%define _S2_LOAD_SEG    0x60    ; segment to place s2 loader

%define _S2_SIGN_OFF    0x3FFE  ; signature offset 

    mov bx,_S2_LOAD_SEG 
    mov es,bx 
    mov ax,1 
    mov cx,[load_len] ; load_len == _S2_LEN == 33, load load_len – 1 or 32 sectors
    mov di,1 

load_s2:
       call sector_read
       inc ax
       mov bx,es
       add bx,32
       mov es,bx

       cmp  ax,cx
       jne  load_s2

     call turn_off_floppy

     mov ax,[_S2_BASE + _S2_SIGN_OFF]
     cmp ax,0xAA55
     jne .load_err

     mov si,BOOT_MSG            ; display loader msg
     call bprint
     exec_bin_kernel _S2_BASE

请注意,load_s2 代码需要一个名为 sector_read 的宏,该宏使用 BIOS int 0x13 来读取软盘驱动器。该代码按顺序从驱动器读取数据并将其复制到 RAM 中。然后,宏 exec_bin_kernel _S2_BASE 跳转到内存中的该地址,立即执行阶段 2 加载器。

第二阶段加载器具有以下逻辑,使用更高级的宏从段 0x1000 加载到段 0x9000,以加载内核

%define _K_LEN                 0x9000         ; 512kb == 0x90000 / 0x10
%define _K_BASE      0x14000        ; address where kernel loaded
%define _K_LOAD_SEG       0x1000         ; segment where kernel loaded

%define _K_END_SEG          _K_LOAD_SEG + _K_LEN

%define _S1_BASE     0x7C00         ; stage 1 base address

%define _S2_CURR_SECT     0x1     ; chs == lba 0x21 == 33
%define _S2_CURR_HEAD    0x0
%define _S2_CURR_TRACK  0x0
%define _S2_NR_SECT         0x12   ; 18 sectors per track

read_tracks _S2_CURR_SECT, _S2_CURR_HEAD, _S2_CURR_TRACK,_K_LOAD_SEG,_K_END_SEG,_S2_NR_SECT,boot_drive

其中宏 read_tracks 完成了繁重的工作。请注意,我们从地址 0x10000(段 0x1000)开始加载 32 位内核,并继续到地址 0x90000(段 0x9000)。因此,我们有 0x90000 – 0x10000 = 0x80000 字节可用于内核,减去内核实际从地址 0x14000 开始的事实,因此有 8 * 16 ^ 4 – (4*16 ^ 3) 字节或 507904 字节可用于 32 位内核。作为优化,无需通过从磁盘的第一个扇区读取来开始加载 32 位内核,我们可以从软盘驱动器上的偏移量 0x4200 开始读取,因为此加载器实际上加载了两次阶段 2 加载器。但是,对于当前的 OX 内核来说,这是可以的,因为内核大约是 177773 字节,剥离后大约是 140032 字节。内核目前包含文件系统和内存分配器以及基本的进程处理(例如,fork、exec、调度)。

与加载阶段 2 加载器不同,加载 32 位内核需要加载器在跳转到内核之前解析并重新定位 RAM 中的内核。实际上,在 16 位非真实模式下,内核镜像必须从 32 位 ELF 转换为 32 位平面二进制文件,并正确清零 .bss 段。在此过程中,内核段被重新定位到内核将运行的物理偏移量。这些位置很重要,因为内核内部的内存分配器需要知道哪些物理内存位置属于内核。将内核转换为平面二进制 ELF 形式的代码在以下宏调用中完成

mov edx,_K_BASE 
exec_elf_kernel edx,nr_sections,kernel_start

在此调用之后,内核已重新定位到其在内存中的起始位置,并且其启动例程 _start 的地址存储在 kernel_start 变量中。要在命令行查看 kernel_start 的值,请运行

nm vmox | grep _start
或使用 readelf vmox -e,然后在输出中查找 "Entry point address:"。这些值以十六进制给出。在 s2.s 源代码中,您可以使用以下命令打印地址
mov eax, [kernel_start]
print_reg eax

它也将打印入口点。为了使加载工作,这些必须匹配,因为这是 ELF 中 e_entry 的值,即 C 语言的 _start 例程。

请注意,在 C 语言中,起始函数不是 main,而是一个名为 _start 的函数,在内核的情况下,它包含初始化内核的逻辑。在正常的 C 程序中,它包含用于启动进程以在原生操作系统上运行的初始化代码。exec_elf_kernel 宏使用 ELF 头文件来查找可加载段并将它们移动到内存中。如果段的文件大小小于内存大小,则将差值清零。由于这发生在所谓的非真实模式下,因此该过程处于 16 位模式,并且启用了 a20 线,因此可以进行 32 位内存访问。顺便说一下,重要的是要注意,exec_elf_kernel 在第二阶段加载器中使用的 memmove 实现存在一个错误,它无法正确加载大于 16 位段的内核,因为循环指令未正确设置为使用 ecx 寄存器。为了解决这个问题,添加了汇编指令 'a32',使汇编器生成使用 ecx 而不是 cx 作为计数器的 16 位循环逻辑。这就是为什么 s2.s 实际上有两个源文件,其中一个指定为 s2.s.DEBUG,它是一个用于查找此问题的调试版本。调试加载器很困难,因为没有在此级别工作的调试器,因此必须使用打印语句来输出寄存器内容并跟踪程序以查看其进度。.DEBUG 文件将在将来需要时提供帮助。

加载将每个段放置在由 32 位内核的编译和链接确定的物理地址处。因此,内核使用以下指令进行链接

cc $(OBJS) -o vmox -m32 -nostdinc -nostdlib -nostartfiles -nodefaultlibs -static -Ttext 0x100000

指令 -m32 编译为 32 位(因为开发机器现在是 64 位)。指令 -nostdinc -nostdlib -nostartfiles -nodefaultlibs 指示编译器不要使用标准头文件、标准库、标准启动文件和默认库。这是必需的,因为内核不能使用用户级库。指令 -static 链接可执行文件而不带任何共享对象。指令 -Ttext 0x100000 指示链接器相对于内存中的此物理地址(即第一个兆字节 RAM)组织代码/数据。此地址偏移量由 exec_elf_kernel 宏用于将内核放置在该内存偏移量处。实际使用的空间可以使用以下命令计算

size vmox
这可能会返回
text                   data            bss              dec              hex                     filename 
 129257           3696          183104         316057         4d299         vmox 

请注意,总数为 0x4d299 或十进制 316057。bss 段是最大的组成部分,受变量 BLOCK_ARRAY_SIZE(文件系统中缓冲区缓存的大小)的影响最直接。有关更多详细信息,请参阅文件 include/ox/fs/block.h。了解内核在 RAM 中的大小及其在 0x100000 的位置,内存分配器可以开始为用户和动态内核内存用户分配大于 0x100000 + kernel_memory_size (0x4d299) 地址的 RAM 页。例如,从地址 0x200000 开始可能是一个不错的起始位置。

一旦获取了 kernel_start 地址,并且内核已从 ELF 转换为平面二进制文件并重新定位到其在物理 RAM 中的起始地址,第二阶段加载器随后重新启用保护模式并跳转到 kernel_start 地址。这有效地执行了 32 位内核。

一个简单的 32 位“裸机”内核可以如下所示

基本内核

/*
 * Test kernel
 * for loader.
 */

void main ( void );

/* without C startup code,
 * we need to write our own _start
 * entry point
 */
void
_start( void )
{
         main();

}/* start */

char *message = "--> Executing test kernel <--
";

/* Force a large .bss segment for testing the loader. */
#define D_SIZE 0x76a0b
char data[D_SIZE] = {0};

void
main ( void )
{
     char *vram = (char *)0xB8000;

     while(*message) {
       *vram++        = *message++;
       *vram++        = 0x7;
     }

     for( ; ; )
        /* idle */;

}/* main */

请注意,内核通过直接写入地址 0xB8000 的视频 RAM 来简单地打印 "--> Executing test kernel <-- ",然后使 CPU 空闲。它有一个大的 .bss 段,因为请求了一个大的字符数组,这是为了测试 ELF 加载器清零这些字节的能力,但除此之外,这没有影响。

在 Virtual Box 中加载

现在可以使用虚拟机进行低级编程测试。这消除了在真实硬件上运行软件并可能在真实硬件上犯错导致不良后果(例如损坏硬盘数据)的要求。由于 OX 引导加载器从软盘加载,我们实际上可以使用 Virtual Box 模拟软盘引导。给定镜像文件 vmox.img,请遵循 [3] 中的说明。这些说明将引导您如何选择用于引导的软盘驱动器介质,Virtual Box 应该只是从驱动器读取镜像并将其加载到虚拟机中。

参考文献

  1. Fine, J. S. (1999). 保护模式编程示例,系统实用程序,构建嵌入式系统。 检索于 2013 年 4 月 14 日,来自 http://geezer.osdevbrasil.net/johnfine/index.htm
  2. Gareth, O. (1999). GazOS。检索于 1999 年 5 月,来自 http://gazos.sourceforge.net/
  3. Hoffman, C. (2013). 我如何在 VirtualBox 中使用软盘镜像?检索于 2013 年 4 月 14 日,来自 http://www.ehow.com/how_8456703_do-floppy-disc-image-virtualbox.html
© . All rights reserved.