引导至x86_64上的“Hello Rust”

2020-07-18 22:59:05

这篇文章是关于我是如何在x86_64上引导到裸机Rust的。我的目标是描述我的学习道路,希望能让你对我谈论的东西感兴趣。如果你觉得这个内容有用,我会很高兴的。请注意,我是一个乞丐,我可能在很多事情上都错了。如果你想了解更多,我会提供很多资源的链接。你可以在我的repo中找到所有的代码。

这个项目的灵感来自很多方面。我想我最先发现了cfenollosa/os-tutorial,我还读了Scratch的“编写简单的操作系统”中的几章,这是对OS dev和相关主题的很好的介绍。本教程的第一部分是关于引导加载器的。为了掌握它的全部内容,我只是阅读了教程的每一部分,并尝试自己编写汇编代码。

下面是引导过程的简要说明,以及您在每个阶段必须做的事情。首先,CPU认为它是20世纪70年代IIRC的Intel8086型号。这一阶段称为实模式。您有~1兆字节的内存和16位寄存器可供您配置。在RM你是非常有限的。但是,在此阶段,您可以使用BIOS例程(或中断)。它们就像一个外部库,你可以用它在屏幕上打印一个字符,读写硬盘等等。显然,你可以在其他模式下做这些事情,但你必须自己实现它,因为你需要做一些黑客工作才能使用中断。

使用完RM后,您可以切换到保护模式。在PM中,地址空间被扩展(您可以访问更多内存),启用了32位寄存器,寻址工作方式略有不同。要切换到PM,您必须设置一个描述符表,它是一段数据,告诉CPU如何解释地址,并描述存储器特定部分的特性。

注意:一开始,我认为在像ds这样的PM地址中:ebx的工作原理与RM-ds中的一样,ebx是该段中的偏移量(如果您想读/写它,则在“data”内存中;如果您想跳转到它,则在“code”内存中),ebx是该段中的偏移量。实际上,DS的作用就像描述符表中的索引。然后,选择的描述符用来将段基数(存储在EBX中)的偏移量转换为物理地址。您可以在此处阅读有关PM中寻址的更多信息。也可查看其网站上的其他内容。

引导加载器是一段非常短的代码-它只有512字节长。我甚至有一次空间不足,不得不删除一些字符串和不必要的代码。Bootloader的实际用途是加载和运行其他代码,书中建议引导到PM并跳转到用C编写的32位内核,我不是很喜欢用C语言编程,在这一点上我失去了动力。

还有一次,我了解到Philipp Opmann的博客OS dev in Rust。我强烈推荐它。我非常兴奋,但不幸的是,Oppermann使用了GRUB(一种引导加载程序),所以我决定在完成自己的引导加载程序后返回教程。当时,引导到用汇编语言以外的语言编写的程序并编译成单独的文件远远超出了我的能力范围,所以我暂时离开了这个项目。

几周前,我有足够的动力尝试启动一个锈蚀程序,我的计划是切换到长模式。然后,我可以运行64位内核或加载第二阶段引导加载程序。第二阶段引导加载程序不限于一个扇区(可以超过512字节长),可以是32位甚至64位,因此它可以访问更大的地址空间,可以将任意大的文件从磁盘加载到内存(只要您实现它),其目的是加载更大的内核。

第一步是将我的程序从硬盘加载到内存中,我采取了一个天真的方法。我编译了引导加载器和我的程序。我将两个二进制文件连接在一起,一个接一个,形成一个文件,如下所示:

我不在乎我是否真的可以执行Rust代码,我只是想测试一下我是否可以加载它。我在引导加载器中添加了必要的代码,并运行了模拟器。不幸的是,QEMU一直在重启,我不知道哪里出了问题,但这无关紧要,因为这是一种愚蠢的做法。我可以在简单得多的引导加载器上测试从磁盘加载数据。我首先尝试自己编写它,但过了一会儿,我只是从堆栈溢出中抓取了一些代码,并添加了一条简单的错误消息,以防BIOS例程失败。经过几次尝试,我终于设法在单个汇编程序中创建了一个两阶段引导加载程序。它的工作原理是这样的:

组织0x7c00First_Stage:;加载第二阶段。;`dl`寄存器指定要加载的磁盘;数据。它由CPU设置到同一设备;引导加载程序是从加载的。;将`es`设置为0x7e0,将`bx`设置为0x0。;例程会将数据加载到`(es<;<;4)|bx`;或0x7e00。MOV AX、0x7e0 MOV ES、AX MOV BX、0x0 MOV AL、0x1;要读取的扇区数。按下AX;存储`al`以备以后使用。;指定数据在磁盘上的存储位置。MOV CH,0x0;气缸。MOV dh,0x0;头。MOV cl,0x2;紧接引导加载器之后的扇区。;`int 0x13`,`ah`设置为0x2,将数据从磁盘加载到内存。Mov ah,0x2 int 0x13 popbx;将`al`恢复为`bl`。;检查读取的扇区数是否正确。Cmp al,bl jne错误JMP 0x7e00;跳到第二阶段。错误:;打印错误消息,然后暂停。Jmp$;用零填充第一个扇区的其余部分。;它是510而不是512,因为最后两个字节;是0xaa和0x55-幻数。乘以510-($-$$)db 0 dw 0xaa55Second_Stage:;做点什么,然后停止。Jmp$;填充以用零填充第二个扇区的其余部分。乘以1024-($-$$)数据库%0。

通过这种方式,我不必链接多个文件,NASM会为我管理扇区的填充。代码被正确编译成一个正好1024字节长的文件-两个完整的扇区。

您还可以使用Align 512而不是1024-($-$$)来允许您的代码任意大小。如果您想在汇编语言中编写引导加载程序的第二阶段,这可能会很有用。

注意:不要假设寄存器是用任何特定值初始化的。我花了半个小时左右才意识到,在这么简单的一段代码中,我没有正确设置一些寄存器。IIRC只有两件事可以确定-引导加载程序将加载到0x7c00,并且dl寄存器指向加载引导加载程序的同一设备。

在我设法加载第二个扇区后,我想用铁锈代码替换汇编代码。要检查我是否可以执行它,我需要切换到长模式,这是因为我在将Rust代码编译为32位时遇到了一些问题,我最终使用了x86_64-未知-无目标三元组,就像Oppermann的教程中一样。

长模式添加了新的内存模型(分页),并支持使用64位指令和寄存器。切换到LM需要设置分页。I身份映射的前2 MiB内存目前就足够了。我想以后可以用我的铁锈程序修改它。我阅读了以下有关寻呼的指南:

在切换到LM之前,您还必须做其他事情,比如检查cpuid指令支持和可用的最高指令操作码。

我认为要执行Rust代码,我需要将其与引导加载程序粘合在一起,以创建单个二进制文件。在这一步中,我遇到了几个难题:

要正常工作,引导加载程序必须存储在文件的前512个字节中,此扇区的最后两个字节必须是0xaa55。

我的铁锈代码也应该与扇区对齐,并完全填满最后一个扇区。当然,我会用一些垃圾值填充它,但它们必须在那里。

这两个程序都将加载到内存中的特定位置,因此我必须管理地址中的偏移量。

Linker似乎是为此而生的工具。我以前从未使用过链接器,所以我只是阅读了ld';的文档。不幸的是,我有点困惑。我不明白为什么。操作员在链接器脚本中工作。我还弄错了AT()指令,你可以看到我在Reddit上开始的线索。

第{.boot 0x7c00节:{*(.boot.*);}.hello_rust 0x7e00:{*(.rust.*);。=ALIGN(512);}=0xDeadc0de Second_Stage_Length=((.。-ADDR(.hello_rust)>;>;9);}。

请注意,我将所有以.boot开头的部分放在前512个字节中,将所有以.rust开头的部分放在0x7e00之后。为了使其正常工作,我必须重命名rustc发出的ELF文件中的所有部分,以便它们都以前缀开头。我使用objcopy(在某些链接器中,此选项称为--prefix-sections):

这个剧本里有两个把戏。第一个方法是将.rust截面的末端与最近的扇区边界对齐。=ALIGN(512)。然后使用=0xdeadc0de用一些垃圾填充其余部分。第二个技巧在最后一行。SECOND_STAGE_LENGTH变量告诉引导加载程序第二阶段占用了多少个扇区。这个部分的大小除以512=2^9。因为我知道这个部分的末尾与扇区的边界对齐,所以我只需将其右移9位即可。

这些解决方案看起来有点老套。在我的理解中,一段代码在ELF文件(最常见的目标)中的位置并不重要。对于引导加载程序则不是这样,因为代码必须与512字节对齐,并且在第一个扇区的末尾必须有一个幻数。我不确定是我缺乏知识有问题,还是链接器不适合做这样的任务。

注意:我仍然不明白为什么不能像这样将整个文件‘解压’成选项.boot和.hello_rust:

第{.boot 0x7c00:{boot.o;}.hello_rust 0x7e00节:{hello_rust.o;。=ALIGN(512);}=0xDeadc0de Second_Stage_Length=((.。-ADDR(.hello_rust)>;>;9);}。

链接器生成的二进制文件长度甚至不到1024个字节,末尾没有0xdeadc0de。在尝试运行QEMU之后,它只是不断地重新启动,所以我怀疑这个程序有很多问题。不幸的是,我对链接几乎一无所知,所以我不能告诉你为什么它不工作。

关于链接,还有一件事我不明白。当我检查hello_rust.o文件时,有很多节,比如.relo、.symtab、.strtab。我知道它们存储了关于ELF的有用信息。但是为了生成二进制文件,我必须在我的脚本中包含每个部分(否则链接器会抱怨)。我应该只写下每个部分的名称吗?我从哪里能拿到所有章节的清单呢?如果他们变了怎么办?我想我可以像以前一样给它们加前缀。但是,如果我要链接许多文件,该怎么办呢?

最后一部分--铁锈计划。到目前为止,它只在屏幕上打印HR。为了正确设置和编译这个项目,我遵循了Opmann博客上的说明。

#![no_std]#![no_main]在左上角使用core::PanicInfo;#[PanicInfo_Handler]FN PanicInfo(_info:&;PanicInfo)->;!{loop{}}#[no_mangler]pub extern";C";fn_start()->;!{//print";hr";让mut vga:*mut u16;//黑字母样式为红色=(0x0<;<;4|0x4);不安全{*vga=style<;<;8|(';H';as u16&;0xff);vga=vga.offset(1);*vga=style<;<;8|(';R';as u16&;0xff);}循环{}}

我使用Cargo-xbuild(它允许您将核心库编译到指定的目标)来发出一个目标文件,该文件稍后将与我的引导加载程序链接:

我听说您也可以在没有Cargo-xbuild的情况下做到这一点,但我还没有测试过:

然后,我将前缀添加到hello_rust.o中的部分,并将其与引导加载程序链接起来:

#add prefixobjcopy hello_rust.o hello_rust_prefixed.o--prefix-alloc-sections=';.rust';#linklld hello_rust_prefixed.o boot.o-T script.ld--o格式二进制-o img.bin