Rolling Your Own Bootloader

来自osdev
Zhang3讨论 | 贡献2021年12月24日 (五) 02:03的版本
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索
难度等级
Difficulty 2.png
中等

有些人更喜欢将自己的软件用于所有内容,或者希望尝试对引导加载程序进行编码。 本页试图描述在编写自己的引导加载程序时要采取的步骤。 在开始编写之前,最好先了解背景 理论

什么和为什么

免责声明

好吧。你在这里是因为你不想使用主流引导加载程序。 您可能还希望将自己的引导加载程序编写为学习体验,以更好地了解它们的功能。 我们也有许多由这个社区开发的 引导加载程序 的页面,有 裸露的骨头,但是人们仍然抱怨我们没有一个页面来解释引导加载程序如何被编码。

我不会尝试给你完整的代码,因为如果那是你要找的,你会使用 预制的bootloaders 之一。 此页面计划告诉您在引导加载程序中需要什么以及希望什么,并可选地指向可以帮助您实现目标的常见问题解答部分。

是否使用自己的引导加载程序或重用现有工具完全取决于您。 如果你有一种不明白的感觉,一定要先阅读我们关于 启动顺序 的页面。

拥有自定义引导加载程序的一个很好的理由是自定义文件系统。

你需要做什么

引导加载程序最终必须将内核 (以及所有内核都需要引导) 引入内存,切换到内核会喜欢的环境,然后将控制权转移到内核。

由于本文的范围是受保护模式C内核,因此我假设 “内核会喜欢的环境” 意味着 受保护模式,内核和其他组件存储在其 “最喜欢的”,编译时已知位置,准备好足够宽的堆栈,并清除BSS部分。

您希望添加的内容

由于引导加载程序以 真实模式 运行,因此它更容易访问BIOS资源和功能。 因此,这是执行 内存映射检测的数量 检测可用视频模式,加载其他文件等的好地方。 引导加载程序将收集这些信息,并以内核能够理解的方式呈现

载入中... 请稍候...

你将在哪里加载你的内核?

您将不得不决定要在内存中加载内核的位置。 您的内核通常取决于它。

在实际模式下,最简单的方法是保持在1MB屏障以下,这意味着您实际上有512KB的内存来加载东西。 您可能希望内核加载在一个众所周知的位置,例如0x10000物理 (es = 0x1000,调用INT13h时bx = 0)。

如果您的内核比这个更大 (或预计会更大),则您可能希望内核超过1MB屏障,这意味着您需要激活 A20门 并切换到 虚幻模式 来加载内核 (单独使用A20,您不能拥有超过1MB以上的64k)。

请注意,BIOS仍然无法写入1MB以上的内存,因此您需要在1MB以下的缓冲区中读取内容,然后执行rep movsd将数据放置在它们最终应该去的地方。

你将如何找到你的内核?

内核的位在某个磁盘上 (大概是 引导磁盘,但这不是强制性的)。问题是: 磁盘上的位置? 它是 FAT格式软盘 上的常规文件吗? 它是FAT12软盘 “保留区域” 中连续扇区的集合 (在这种情况下,您可能需要专用工具来格式化磁盘并在其上安装内核)? 还是软盘简单地留下了未格式化的内核,直接用 磁盘映像工具 粘贴。

以上所有选项都是可能的。 也许我自己选择的是在FAT12软盘上保留足够的空间来存储内核文件使用的 “部门列表”。 被fully-FAT12的 “好处” 是,您不需要在每次重写内核时都重写引导扇区。

你还需要加载什么?

这主要取决于你的内核。 例如,Linux需要一个额外的 “初始化” 文件,其中将包含 “初始化过程” (作为用户级别)。 如果您的内核是模块化的,并且 文件系统 被某些模块理解,则需要将模块与内核一起加载。 磁盘/文件/内存服务等 “微内核服务” 也是如此。

如果我得到超过512个字节的引导扇区?

确保前512个字节能够加载加载程序的其余部分,并且您是安全的。 有些人使用单独的 “第二阶段” 加载器来做到这一点,其他人则通过在其ASM代码中真正插入 “512字节” 中断,确保加载器的其余部分放在引导扇区之后 (即从0x7e00开始)。

如果我想为用户提供引导多个操作系统的选项怎么办?

启动另一个OS的最简单方法是一种称为 “chainloading” 的机制。 Windows在安装的 “分区” 的引导扇区中存储类似于第二阶段引导加载程序的内容。 安装Linux时,将例如LILO或GRUB写入 “分区” 引导扇区而不是MBR也是一种选择。 现在,您的MBR引导部门可以做的是 “重新定位” 自身 (从0x0000:0x7c00复制到传统上0x0060:0x0000),解析分区表,显示某种菜单,并让用户选择从哪个分区启动。 然后,您的 (重新定位的) MBR引导扇区将该 “分区” 引导扇区加载到0x0000:0x7c00,然后跳到那里。 分区引导扇区不会比 “之前” 已经加载了一个引导扇区更明智, 并且实际上可以加载 “另一个” 引导扇区-这就是为什么它被称为 “链式加载” 的原因。 只要您不覆盖 IVT (如果设置了 “if in eglags”),您决定在哪里重新定位引导扇区并不重要, BDA EBDA

您会看到,通过以某种可理解的方式显示菜单并接受击键,这样的多选项引导加载程序可以变得相当复杂,相当快。 我们甚至没有触及从扩展分区启动的主题,这将需要在打印菜单之前顺序读取和解析多个扩展分区表。

更极端的是,像这样的引导管理器可以变得像一个简单的操作系统一样复杂 (很像GRUB,它提供了从各种文件系统的读取、引导 多引导 内核、链式加载、加载initrd ramdisks等等) -- 这样的内部将不会在这里解决。

我如何实际加载字节

BIOS interrupt 13h. 在 拉尔夫·布朗的中断列表 获取关于它的信息,确保你知道 (现在已经过时的) 软盘可能会失败一两次,你不能一次阅读超过一个轨道,你必须使用CHS寻址,你就完成了。

要从硬盘驱动器读取 (这是这些天的首选方式,CDROMs和USB记忆棒也使用),您可能需要int 13h,ah = 0x42,使用简单LBA寻址的驱动器号0x80。 中断列表中的详细信息。

如果您需要指导,请随时检查 c32-lxsdk/内核/src/sosfflppy/lowlevel.asm?view = log lowlevel.asm


还请注意,大多数 文件系统 涉及分配单元 (块/簇) 和物理 “圆柱体: 头: 扇区” 值之间的一些转换。 一旦您知道 “每个轨道的扇区” 和 “头” 计数,这些转换就很简单。 查看 OSRC 了解更多信息。 这仅与过时的软盘有关; 其他所有内容,例如硬盘驱动器,cdrom,USB记忆棒,都使用简单的LBA寻址方案。

> 有没有人有将DOS扇区转换为
> 物理扇区 (头部、圆柱体、扇区),如用于
> INT 13h?

DOS_sector_num = BIOS_sector_num - 1 + Head_num*Sectors_per_track
		+ Track_num*Sectors_per_track*Total_heads

BIOS_sector_num = 1 + (DOS_sector_num MOD Sectors_per_track)
BIOS_Head_num   = (DOS_sector_num DIV Sectors_per_track) MOD Total_heads
BIOS_Track_num  = (DOS_sector_num DIV Sectors_per_track) DIV Total_heads

如果您的加载量超过1MB,则应按2个步骤进行: 首先使用BIOS加载到 “常规” 区域,然后执行 rep movsd 将数据放置在它们最终应该去的地方。

已加载。收集信息

下一步包括收集尽可能多的信息,你可以/需要: 已安装RAM的数量,可用的 视频模式 和类似的事情在实际模式下更容易做到, 因此,在 真实模式 中进行操作要比以后尝试回到真实模式进行旅行更好。 当然确切的要求取决于你的内核。

这里一个非常简单的解决方案是将您的信息组织为一个平面表 (ala BIOS数据区域)。 另一种选择是将这些信息添加为结构化流: 您将索引保存在一个众所周知的地址 (或加载时会传递给内核的某个地址),并且该索引为每个 “键” 提供相应数据结构的地址。例如。

  organization           lookup code (eax == signature)
  +------+------+          mov esi, well_known_index_address
  | RAM. | 1234 |        .loop:
  | VBE. | 5678 |          cmp [esi],'END.'
  | MODS | 9ABC |          je .notfound
  | DISK | DEF0 |          add esi,8
  | END. | ---- |          cmp [esi-4],eax
  +------+------+          jne .loop
                           mov eax,[esi]
                           ret

准备就绪。进入 保护模式 ...

要进入保护模式,您应该首先禁用中断并设置全局描述符表。 将PE位设置为CR0后:

mov eax,cr0
or eax,1
mov cr0,eax

在它设置寄存器并做一个远跳转到内核之后。 如果数据选择器为10h,则代码选择器为8,内核偏移为10000h,请执行以下操作:

mov ax,10h
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
jmp 8:10000h

注:

  • 在这种情况下,GDT将是 “暂时的”。 实际上,加载程序不知道内核要对GDT做什么,因此它所能做的就是提供最小限度,并让内核稍后使用适当的GDT重新加载GDTR。
  • 加载程序通常会禁用中断 (当正确设置IDT时,内核将稍后启用中断)
  • 给自己时间考虑是否现在启用分页。 请记住,在没有异常处理程序帮助的情况下调试分页初始化代码可能很快就会成为一场噩梦!
  • 一旦启用了保护模式,并且在加载内核之前,可以执行更多的初始化。但是,这将要求您在单个目标文件中混合16位和32位代码,这也可能很快成为噩梦...
  • 很可能您的内核不以可执行代码开头,而是在10000h处具有ELF或PE标头。 你必须解析它才能得到要跳转到的入口点。

你还有很长的路要走...

现在,您距离使用extern和call函数的C代码非常遥远。 您将需要启用 A20,制作读取图像的东西 (这样您就可以实际启动任何.bin或.sys文件),等等。

帮帮我卡住了!

找出引导加载程序有什么问题的唯一方法是在VM中调试它。 您可能可以打印出变量,但是有限的空间使这变得非常困难。 此外,阅读 常见的错误和陷阱 可能会给你关于你的问题的想法。 de:Eigener Bootloader

你可能需要做的任务清单

  • 设置16位段寄存器和堆栈
  • 打印启动消息
  • 检查是否存在PCI、CPUID、MSRs
  • 启用并确认启用A20线路
  • 加载GDTR
  • 通知BIOS目标处理器模式
  • 从BIOS获取内存映射
  • 在文件系统中定位内核
  • 分配内存以加载内核映像
  • 将内核映像加载到缓冲区中
  • 启用图形模式
  • 检查内核映像ELF标头
  • 启用长模式,如果64位
  • 为内核段分配和映射内存
  • 设置堆栈
  • 设置COM串行输出端口
  • 设置IDT
  • 禁用PIC
  • 检查是否存在CPU功能 (NX,SMEP,x87,PCID,全局页面,TCE,WP,MMX,SSE,SYSCALL),并启用它们
  • 分配一个PAT来写合并
  • 设置FS/GS基础
  • 装载IDTR
  • 使用ACPI表中的信息启用APIC和设置
  • 设置GDT和TSS