OS1:Rust for x86上的原始内核

我决定写一篇文章,如果可能的话,再写一系列文章,以分享我对Bare Bone x86设备和操作系统组织的独立研究经验。 目前,我的黑客甚至还不能称为操作系统-它是一个小内核,可以从Multiboot(GRUB)引导,管理实际和虚拟内存,并且还可以在单​​个处理器上以多任务模式执行多项无用的功能。


在开发过程中,我并没有设定编写新Linux的目标(尽管我承认,我大约在5年前就梦想过)或给人留下深刻的印象,所以我希望您不要再给人留下特别深刻的印象了。 我真正想做的是弄清楚i386体系结构在最基本的层次上是如何工作的,以及操作系统是如何精确发挥作用的,并挖掘出炒作Rust的方法。


在我的笔记中,我将不仅尝试共享源文本(可以在GitLab上找到它们)和裸露的理论(可以在许多资源上找到它们),而且还可以分享我找到不明显答案的途径。 具体来说,在本文中,我将讨论构建内核文件,加载和初始化它


我的目标是在脑海中构造信息,并帮助那些遵循类似道路的人。 我了解网络上已经有类似的材料和博客,但是为了了解我目前的情况,我不得不将它们收集在一起很长时间。 现在,我将分享所有来源(无论如何,我记得)。


文献资料


当然,我从绝妙的OSDev资源(包括Wiki和论坛)中获得了大部分收益 。 其次,我将在他的博客中命名Philip Opperman-有关一堆Rust和iron的很多信息。


在Linux内核中发现了一些问题,Minix并非没有特殊文献的帮助,例如Tanenbaum的书“ 操作系统 设计与实现, 《罗伯特·洛夫书》 ,Linux内核。 开发过程的描述 。” 使用手册“ 英特尔64和IA-32架构软件开发人员手册第3卷(3A,3B,3C和3D):系统编程指南 ”解决了有关x86架构组织的难题。 在理解二进制格式时,布局是ld,llvm,nm,nasm和make的指南。
UPD 感谢CoreTeamTech让我想起了出色的Redox OS系统。 我没有摆脱它的源头 。 不幸的是,俄罗斯IP无法提供官方的GitLab系统,因此您可以查看GitHub


另一个序言


我意识到我不是Rust的优秀程序员,而且,这是我使用该语言编写的第一个项目(不是开始约会的最佳方法,对吗?)。 因此,该实现对您来说似乎完全不正确-事先我想对自己的代码宽大处理,我将很乐于评论和建议。 如果一位受人尊敬的读者能够告诉我前进的方向和方法,我也将不胜感激。 可以从教程中复制一些代码片段,并对其进行一些稍微的修改,但是我将尝试对此类部分进行尽可能清晰的说明,以使您不会遇到与解析它们时所遇到的问题相同的问题。 我也不假装在设计中使用正确的方法,因此,如果我的内存管理器让您想写生气的评论,我就会明白为什么。


工具包


因此,我将从开始使用的开发工具开始。 作为一个环境,我选择了一个不错且方便的VS Code编辑器,并带有Rust和GDB调试器的插件。 VS Code有时对于RLS来说不是很好,尤其是在特定目录中重新定义它时,因此,在每次Rust每晚更新之后,我都必须重新安装RLS。


选择Rust的原因有很多。 首先,其日益普及和令人愉悦的哲学。 其次,他的工作能力低下,但“脚踩脚”的可能性较低。 第三,作为Java和Maven的爱好者,我非常着迷于构建系统和依赖管理,并且工具链语言已经内置了货物。 第四,我只是想要一些新东西,而不是C。


对于低级代码,我选择了NASM, 我对Intel语法充满信心,并且对使用其指令也很满意。 我故意放弃了Rust中的汇编程序插入内容,以明确地将工作与Iron和高级逻辑分开。
LLVM LLD供应商提供的Make和链接器(作为一种更快,更好的链接器)被用作一般的装配和布局-这是一个好问题。 可能与货物的构建脚本有关。


Qemu用于启动-我喜欢它的速度,交互模式以及挂接GDB的功能。 要引导并立即获得所有硬件信息-当然是GRUB(遗产更易于组织标头,因此使用它)。


链接和布局


奇怪的是,对我而言,这已成为最困难的话题之一。 在对x86段寄存器进行长期试用之后,很难意识到段和节不是同一回事。 在针对现有环境进行编程时,无需考虑如何将程序放置在内存中-对于每种平台和格式,链接器已经具有现成的配方,因此无需编写链接器脚本。


相反,对于裸铁,有必要指出如何在内存中放置和寻址程序代码。 在此我要强调的是,我们正在谈论使用页面机制的线性(虚拟)地址。 OS1使用页面机制,但是我将在文章的相应部分中分别进行介绍。


逻辑,线性,虚拟,物理...

逻辑,线性,虚拟,物理地址。 我在这个问题上犹豫不决,因此,我想在这篇出色的文章中谈谈细节


对于使用分页的操作系统,在32位环境中,即使您安装了128 MB的RAM,每个任务也具有4 GB的内存地址空间。 发生这种情况仅是由于内存的分页组织;主内存中没有页面的情况将得到相应处理。


但是,实际上,可用的应用程序通常少于4 GB。 这是因为OS必须处理中断,系统调用,这意味着至少它们的处理程序必须在此地址空间中。 我们面临一个问题:内核地址应准确地放置在这4 GB中的什么位置,以便程序可以正常工作?


在现代程序世界中,使用了这样的概念:每个任务都认为它在处理器上占统治地位,并且是计算机上唯一正在运行的程序(在此阶段,我们不讨论进程之间的通信)。 如果您仔细地看一下编译器在链接阶段如何收集程序,事实证明它们是从零或接近零的线性地址开始的。 这意味着,如果内核映像占用的内存空间接近零,则无法执行以这种方式汇编的程序,该程序中的任何jmp指令都会导致进入内核的受保护内存,并导致保护错误。 因此,如果将来我们不仅要使用自写程序,还应合理地为应用程序提供尽可能多的接近零的内存,并将内核映像放在更高的位置。


这个概念称为高半内核(如果需要相关信息,在这里我指的是osdev.org)。 选择哪种记忆仅取决于您的食欲。 某人可以使用512 MB的内存,但我决定自己获取1 GB,因此我的内核位于3 GB + 1 MB(为了满足较低的较高内存限制,需要+ 1 MB,GRUB在1 MB之后将我们加载到物理内存中) 。
对我们来说,指定可执行文件的入口点也很重要。 对于我的可执行文件,这将是用汇编器编写的_loader函数,我将在下一部分中对其进行详细介绍。


关于入口点

您是否知道一生都对main()是该程序的切入点这一事实撒谎了? 实际上,main()是C语言及其生成的语言的约定。 如果您四处挖掘,结果会类似于以下内容。


首先,每个平台都有其自己的规范和入口点名称:对于linux,通常是_start,对于Windows,则是mainCRTStartup。 其次,可以重新定义这些要点,但是使用libc的乐趣将无效。 第三,编译器默认情况下提供这些入口点,它们位于文件crt0..crtN中(CRT-C运行时,N-主参数数)。


实际上,像gcc或vc这样的编译器会做什么-他们选择定义标准入口点的特定于平台的链接脚本,使用现成的C初始化初始化函数选择所需的目标文件,然后调用main函数并以具有标准入口点的所需格式的文件形式链接至输出。


因此,出于我们的目的,应该关闭标准入口点和CRT初始化,因为我们绝对只有铁了。


您还需要知道什么链接? 如何定位数据段(.rodata,.data),未初始化的变量(.bss,通用),并且还要记住GRUB要求在二进制文件的前8 KB中放置多重引导头。


现在,我们可以编写一个链接描述文件!


ENTRY(_loader) OUTPUT_FORMAT(elf32-i386) SECTIONS { . = 0xC0100000; .text ALIGN(4K) : AT(ADDR(.text) - 0xC0000000) { *(.multiboot1) *(.multiboot2) *(.text) } .rodata ALIGN(4K) : AT(ADDR(.rodata) - 0xC0000000) { *(.rodata*) } .data ALIGN (4K) : AT(ADDR(.data) - 0xC0000000) { *(.data) } .bss : AT(ADDR(.bss) - 0xC0000000) { _sbss = .; *(COMMON) *(.bss) _ebss = .; } } 

GRUB之后下载


如上所述,Multiboot规范要求标头必须位于启动映像的前8 KB中。 完整的规范可以在这里看到,但是我将只关注感兴趣的细节。


  • 必须注意32位对齐(4个字节)
  • 必须有一个魔术数字0x1BADB002
  • 有必要告诉多功能引导程序我们想要获得什么信息以及如何放置模块(在我的情况下,我希望内核模块与4 KB页面对齐,并获得一张存储卡以节省时间和精力)
  • 提供校验和(校验和+幻数+标志应为零)

 MB1_MODULEALIGN equ 1<<0 MB1_MEMINFO equ 1<<1 MB1_FLAGS equ MB1_MODULEALIGN | MB1_MEMINFO MB1_MAGIC equ 0x1BADB002 MB1_CHECKSUM equ -(MB1_MAGIC + MB1_FLAGS) section .multiboot1 align 4 dd MB1_MAGIC dd MB1_FLAGS dd MB1_CHECKSUM 

引导后,Multiboot保证了我们必须考虑的一些条件。


  • EAX寄存器包含魔幻数字0x2BADB002,表示下载成功
  • EBX寄存器包含结构的物​​理地址以及有关加载结果的信息(我们将在以后再讨论)
  • 处理器处于保护模式,页面存储器已关闭,段寄存器和堆栈处于未定义状态(对于我们而言),GRUB出于需要使用了它们,需要尽快重新定义。

我们需要做的第一件事是启用分页,调整堆栈,最后将控制权转移到高级Rust代码。
我不会详细介绍内存的页面组织,页面目录和页面表,因为关于这方面的文章很多( 其中之一 )。 我要分享的主要内容是页面不是细分! 请不要重复我的错误,也不要在GDTR中加载页表地址! 对于页表是CR3! 该页面在不同的体系结构中可以具有不同的大小,为了简化工作(只有一个页面表),由于包含PSE,我选择了4 MB的大小。


因此,我们要启用虚拟页面内存。 为此,我们需要将页表及其物理地址加载到CR3中。 同时,我们的二进制文件被链接为在3 GB偏移量的虚拟地址空间中工作。 这意味着所有变量地址和标签的偏移量均为3 GB。 页表只是一个数组,其中页地址包含其实际地址(与页面大小对齐)以及访问和状态标志。 因为我使用4 MB页面,所以我只需要一个包含1024个条目的PD页面表:


 section .data align 0x1000 BootPageDirectory: dd 0x00000083 times (KERNEL_PAGE_NUMBER - 1) dd 0 dd 0x00000083 times (1024 - KERNEL_PAGE_NUMBER - 1) dd 0 

桌子上有什么?


  1. 由于处理器中的所有地址都是物理地址,并且尚未执行到虚拟的转换,因此第一页应该指向当前代码段(物理内存的0-4 MB)。 缺少此页面描述符将导致立即崩溃,因为处理器在打开页面后将无法接受下一条指令。 标志:位0-存在表,位1-写入页面,位7-页面大小4 MB。 打开页面后,记录将重置。
  2. 跳到3 GB-零确保页面不在内存中
  3. 3 GB标记是我们在虚拟内存中的核心,在物理内存中引用0。 打开页面后,我们将在这里工作。 标志与第一条记录相似。
  4. 跳至4 GB。

因此,我们声明了表,现在我们希望将其物理地址加载到CR3中。 在链接阶段不要忘记3 GB的地址偏移量。 尝试按原样加载地址会将我们发送到3 GB +可变偏移量的真实地址,并导致立即崩溃。 因此,我们将BootPageDirectory的地址减去3 GB,并将其放入CR3。 我们打开CR4寄存器中的PSE,打开CR0寄存器中的页面的工作:


  mov ecx, (BootPageDirectory - KERNEL_VIRTUAL_BASE) mov cr3, ecx mov ecx, cr4 or ecx, 0x00000010 mov cr4, ecx mov ecx, cr0 or ecx, 0x80000000 mov cr0, ecx 

到目前为止,一切进展顺利,但是一旦我们将第一页重置为3 GB的上半部分,所有内容都会崩溃,因为EIP寄存器的物理地址仍在第一个兆字节区域。 为了解决这个问题,我们执行了一个简单的操作:将标记放在最近的位置,加载其地址(该地址已经有3 GB的偏移量,请记住这一点),并进行无条件的跳转。 之后,可以为将来的应用程序重置不必要的页面。


  lea ecx, [StartInHigherHalf] jmp ecx StartInHigherHalf: mov dword [BootPageDirectory], 0 invlpg [0] 

现在,这一切都变得很小了:初始化堆栈,传递GRUB结构,并且汇编程序就足够了!


  mov esp, stack+STACKSIZE push eax push ebx lea ecx, [BootPageDirectory] push ecx call kmain hlt section .bss align 32 stack: resb STACKSIZE 

您需要了解这段代码:


  1. 根据调用的C约定(它也适用于Rust),变量以相反的顺序通过堆栈传递给函数。 在x86中,所有变量都按4个字节对齐。
  2. 堆栈从结尾开始增长,因此指向堆栈的指针应指向堆栈的结尾(将STACKSIZE添加到地址)。 我采用的堆栈大小为16 KB,应该足够了。
  3. 以下内容已转移到内核:多重引导的魔数,引导加载程序结构的物理地址(为我们提供了一块宝贵的存储卡),页表的虚拟地址(在3 GB空间中的某个位置)

另外,不要忘记声明kmain是extern,而_loader是全局的。


进一步的步骤


在以下说明中,我将讨论设置段寄存器,通过VGA缓冲区简要介绍信息输出,告诉您如何组织中断处理,页面管理以及最甜蜜的事情-多任务处理-我将继续学习。


完整的项目代码可在GitLab上获得


感谢您的关注!


UPD2: 第2部分
UPD2: 第3部分

Source: https://habr.com/ru/post/zh-CN445506/


All Articles