用asp.net实现微博系统_用Rust写操作系统(一)——实现最小内核

本次实验的问题总结,有需要的小伙伴可以戳这里↓

GMN23362:用Rust写操作系统(一)——问题汇总​zhuanlan.zhihu.com

一、概要说明

本实验分为三个部分:第一部分安装必要的工具链;第二部分编写裸机程序(独立式可执行程序);第三部分构建最小的“内核”系统。由于我们的目标是编写一个操作系统,所以我们需要创建一个独立于操作系统的可执行程序,又称独立式可执行程序(freestanding executable)或裸机程序(bare-metal executable)。这意味着所有依赖于操作系统的库我们都不能使用。比如std中的大部分内容(io, thread, file system, etc.)都需要操作系统的支持,所以这部分内容我们不能使用。 但是,不依赖于操作系统的rust的语言特性我们还是可以继续使用的,比如:迭代器、模式匹配、字符串格式化、所有权系统等。这使得rust依旧可以作为一个功能强大的高级语言,帮助我们编写操作系统。

二、安装工具链

1. 安装 Rust

1) 下载 Rust

• 访问Rust的官网,下载64位的rustup-init.exe。

ae18e68fd1eef28ff88e5404d50e0804.png

• 运行rustup-init.exe,选择1) Proceed with installation(default)。

238d7537b619ab71ffbe0c3e7099fefd.png

224e0834784bc8ff3217e27bba2e33ea.png

a77f7ed32a3b334fe793ad746b4bf8d1.png

2) 安装nightly版本:在cmd中输入“rustup install nightly”。

06be2d937a718e65acfcef7b029053df.png

3) 默认使用nightly版本:输入“rustup default nightly”。

9704b61d26f55f80817978126307501a.png

4) 安装bootimage,xbuild和rust-src等

• 安装bootimage: 输入“cargo install bootimage --version “ˆ0.7.3”。

0b5d244318e4b93b75ce2fa56f92d195.png

• 安装xbuild:输入“cargo install cargo-xbuild”。它封装了cargo build;但它可以自动交叉编译core库和一些编译器内建库。

bbcc600fca9b3de8b4cfc532836a0d0a.png

106abf9087b8956c8cdd492b73f293c4.png

• 安装rust-src:cargo xbuild依赖于Rust的源代码,输入“rustup component add rust-src” 安装源代码。

b9019e0ee39982f6025a502b9fde7872.png

• 安装llvm-tools-preview:为了运行bootimage以及编译引导程序,我们需要输入“rustup component add llvm-tools-preview”安装rustup模块。

522bbb6f179c6b0542b60988dfdeff0f.png

2. 安装其他工具

• 安装 QEMU:访问QEMU官网,下载20200814版64位的QEMU。

5a13193c473327f66f578a52cc060faa.png

三、创建裸机程序

1. 禁用标准库

1) 创建项目

• 输入“cargo new sjy_os”。

568b621887b153d975aa72f11c972311.png

• 发现在C:UsersGMN23362目录下创建的“sjy_os”项目。

6623665ece822022a1d454cd7fec223f.png

• 其下的子目录结构为:

sjy_os

├── Cargo.toml

└── src

└── main.rs

• 其中Cargo.toml文件包含了包的配置,如包的名称、作者、server版本和项目依赖项。

1b90ebb6371ea0b65eaf1758bfdf705e.png

• src/main.rs文件包含包的根模块和main函数。

• 我们可以使用cargo build来编译这个包,注意当前路径要设置为sjy_os。

582b7235580baa39e47b81cb605e07ce.png

然后在target/debug文件夹内就可以找到编译好的sjy_os二进制文件了。

27ca7f2205122bb7d74b68637bc42d58.png

2) 在默认情况下,所有的Rust crates都和标准库相关,标准库依赖于操作系统的功能诸如进程,文件,网络等。它还依赖于C标准库libc,一个与操作系统服务紧密相连的库。由于我们的目标是实现一个操作系统,所以我们不能使用任何依赖于操作系统的库。

• 在main.rs的第一行添加代码“#![no_std]”来禁止包与标准库链接。

484668c35ccfb789aab76cd7192324a5.png

• 输入“cargo build”进行编译,报告如下错误。

9435b88efe4abad7fbcd2993ef4d5651.png

因为禁用了标准库,所以println函数没有定义。

2. 实现panic_handler

panic_handler 属性定义了一个函数,当[painc]发生时它就会被调用。标准库会提供它自己的panic handler函数,但是在一个no_std环境里我们需要自己来定义它。

aa03dbf8ad3e2b774f429232db8c0157.png

PanicInfo类包含了panic发生的文件名、代码行数和可选的错误信息。这个函数从不返回,所以他被标记为发散函数(diverging function)。发散函数的返回类型称作Never类型("never" type),记为“!”。

3. eh_personality语言项

语言项是一些编译器需要的特殊的函数或类型。eh_personality语言项会标记那些用于实现 [stack unwinding(栈展开)]的函数。在默认情况下,当panic发生时Rust会使用展开来析构那些活跃在栈上的变量。这会确保所有使用的内存都会被释放,并允许父进程捕获panic,处理并继续运行。然而,栈展开是一个非常复杂的过程,通常需要依赖操作系统的库(例如Linux上的libunwind,Windows上的structured exception handling),所以我们不打算在我们的操作系统里使用它。

在很多情况下我们并不需要栈展开,所以Rust提供了[abort on panic(panic时终止)]作为替代。这个标志能禁用栈展开相关的标识符的生成从而缩小生成的二进制从文件的大小。在Cargo.toml中添加如下代码用来禁用展开。

cd66090db1ca3842267bba9107459479.png

4. 输入“cargo build”进行编译,另外一个语言包‘start’报错。

cfb9efa37ac1412b287571ad40b90470.png

大部分语言都有一个运行时系统,用来负责垃圾回收(如Java)或软件线程(如go的协程)。这些runtime需要在main函数被调用前启动,并初始化自身。

一个普通的链接到标准库的Rust二进制程序,是从一个叫crt0(“C runtime zero”)的C运行时库开始执行的,这个库会设置一个适合C语言应用程序运行的环境。这其中包括了栈的创建以及将参数放置到正确的寄存器里等。C运行时会调用Rust runtime入口,这个入口点被标记为start语言项。

我们的可执行文件并没有进入Rust runtime和crt0,所以我们需要自己来定义入口。在这里实现start语言项并没有什么帮助,程序仍然会要求crt0。因此,我们需要直接覆写crt0入口。

• 修改main.rs:

* 添加“#![no_main]”禁用所有Rust层的入口点。

* 删除main函数。

* 定义_start(默认的入口点)程序,用“extern C”让编译器为它生成C调用约定。

* 添加“[no_mangle]”关闭name mangling,防止编译命名混乱。

e64e7f7e5a6018edebc8dcc7e83d5a4b.png

5. 再次输入“cargo build”进行编译,报告以下错误,提示链接失败。

e2574dac983e606f951b846e60bfe91c.png

报错的原因是因为链接器没有找到入口点。在Windows系统下,默认的入口点函数名由使用的子系统决定。对CONSOLE子系统,链接器将寻找名为mainCRTStartup的函数;而对WINDOWS子系统,它将寻找WinMainCRTStartup。我们的_start函数并非这两个名称,所以为了使用它,我们要向链接器传递/ENTRY参数。Windows可执行程序可以使用不同的子系统。对一般的Windows程序,使用的子系统将由入口点的函数名推断而来:如果入口点是main函数,将使用CONSOLE子系统;如果是WinMain函数,则使用WINDOWS子系统。由于我们的_start函数名称与上两者不同,我们需要显式指定使用的子系统:

74d1172308ad2740bb87d39f0ef32b98.png

编译成功,至此已经成功创建好了裸机程序。

6. 总结:最小的独立的Rust二进制文件

429f89b6668d8721116335a05b70ceee.png

44e5eb6bbcd264e41c38cd8c7442662c.png

注意:这只是独立Rust二进制文件的一个最小示例。该二进制文件需要进行各种操作,例如在调用_start函数时初始化堆栈。因此要真正使用这样的二进制文件,需要更多的步骤。

四、实现最小内核

我们基于设计好的独立的Rust二进制文件创建一个可引导的磁盘映像实现在屏幕打印。

1. 启动过程

当你打开一台计算机,它开始执行存储在主板ROM中的固件代码。这段代码执行开机自测,检测可用的RAM,并预初始化CPU和硬件。然后,它寻找一个可引导的磁盘,并开始引导操作系统内核。

x86上有两个固件标准:“基本输入/输出系统”(BIOS)和较新的“统一可扩展固件接口”(UEFI)。BIOS标准已经过时了,但是很简单,而且自上世纪80年代以来在任何x86机器上都得到了很好的支持。相比之下,UEFI更现代,有更多的特性,但是设置更复杂。

1) BIOS启动

几乎所有的x86硬件系统都支持BIOS启动,这也包含新式的、基于UEFI、用模拟BIOS的方式向后兼容的硬件系统。这很好,因为即便是过去几个世纪的机器上也都可以使用同样的启动逻辑;但这种兼容性有时也是BIOS引导启动最大的缺点,因为这意味着在系统启动前,你的CPU必须先进入一个16位系统兼容的实模式,这样1980年代古老的引导固件才能够继续使用。

让我们从头开始,理解一遍BIOS启动的过程:

当电脑启动时,主板上特殊的闪存中存储的BIOS固件将被加载。BIOS固件将会上电自检、初始化硬件,然后它将寻找一个可引导的存储介质。如果找到了,那电脑的控制权将被转交给引导程序:一段存储在存储介质的开头的、512字节长度的程序片段。大多数的引导程序长度都大于512字节——所以通常情况下,引导程序都被切分为一段优先启动、长度不超过512字节、存储在介质开头的第一阶段引导程序,和一段随后由其加载的、长度可能较长、存储在其它位置的第二阶段引导程序。

引导程序:

* 决定内核的位置,并将内核加载到内存。

* 将CPU从16位的实模式,先切换到32位的保护模式,最终切换到64位的长模式:此时,所有的64位寄存器和整个主内存才能被访问。

* 从BIOS查询特定的信息,并将其传递到内核;如查询和传递内存映射表。

编写一个引导程序并不简单,因为需要使用汇编语言,而且必须经过许多意图并不明显的步骤——比如,把一些魔术数字写入某个寄存器。因此,我们不会讲解如何编写自己的引导程序,而是推荐bootimage工具——它能够自动而方便地为你的内核准备一个引导程序。

2) UEFI

2. 最小内核

目标:创建一个内核的磁盘映像,在启动时向屏幕输出一行“Hello World!”

我们通过cargo构建了独立二进制程序,但是根据操作系统,我们需要不同的入口点名称和编译标志。这是因为cargo在默认情况下是为主机系统构建的,即正在运行的系统。这不是我们想要的内核,因为运行在Windows之上的内核没有多大意义。相反,我们希望的是编译一个明确定义的目标系统。

1) 安装Rust nightly

Rust语言有三个发行频道,分别是stable、beta和nightly。《Rust程序设计语言》详细解释了这三个频道的区别。为了搭建一个操作系统,我们需要一些只有nightly会提供的实验性功能,所以我们需要安装一个nightly版本的Rust。

rustup:

* 允许同时安装nightly、beta和stable版本的编译器,而且容易进行Rust的更新。

* rustup override add nightly:选择在当前目录使用nightly版本的Rust。

* rustc --version:检查nightly是否已经安装,返回的版本号末尾应该包含-nightly。

Nightly允许我们在文件的开头添加所谓的feature flags来选择使用某些实验性的功能。例如,我们可以通过在main.rs文件头部添加例如 #![feature(asm)] 来启用实验性的内敛汇编[asm! macro(宏)]。注意,像这样的实验性功能是完全不稳定的,这也意味着这些功能可能在没有提取说明的情况下就在某个版本里变动甚至被移除了。基于这个理由,我们只在必须的情况下才使用他们。

2) 目标配置

cargo可以通过--target参数来支持不同的目标系统。这个目标系统由所谓目标三元组来描述,它描述了CPU的架构,供应商,操作系统以及ABI。举个例子,x86_64-unknown-linux-gnu三元目标组描述了一个基于x86_64 CPU,不明确的供应商,采用GNU ABI的Linux操作系统的结构。Rust支持很多不同的目标三元组,包括安卓的arm-linux-androideabi和WebAssembly的wasm32-unknown平台。

对于我们的目标系统,我们需要一些特殊的配置参数(例如,不依赖于底层操作系统),所以现有的目标三元组都不适用。幸运的是,Rust允许我们通过一个JOSN文件来定义我们自己的目标平台。

大部分字段需要有LLVM来为该平台生成代码。例如,[data-layout]字段定义了各种整型,浮点型,指针类型的大小。还有一些用于Rust条件编译的字段,诸如target-pointer-width。还有的字段则定义了该如何构建一个crate,例如pre-link-args字段,指定了传递给链接器的参数。我们将把内核编译到x86_64架构,所以配置清单将和上面的例子相似。

• 在sjy_os下创建一个名为x86_64-sjy_os.json的文件:

10ee2ce67a6fbcfe2e9dc5efc0dfc6f8.png

* 因为要在裸机上运行内核,所以修改llvm-target的内容,将os配置项的值改为none。

* "linker-flavor": "ld.lld", "linker": "rust-lld":使用跨平台的和Rust打包发布的LLD链接器。

* "panic-strategy": "abort",:我们的编译目标不支持panic时的栈展开,所以我们选择直接在panic时中止。这和在Cargo.toml文件中添加panic = "abort"选项的作用是相同的,所以我们可以不在这里的配置清单中填写这一项。

* "disable-redzone": true,:我们正在编写一个内核,所以我们应该同时处理中断。要安全地实现这一点,我们必须禁用一个与红区有关的栈指针优化:因为此时这个优化可能会导致栈被破坏。

* "features": "-mmx,-sse,+soft-float",:用来启用或禁用某个目标CPU特征。通过在它们前面添加-号,禁用mmx和sse特征;添加前缀+号,启用soft-float特征。mmx和sse特征决定了是否支持单指令多数据流(SIMD)相关指令,这些指令常常能显著地提高程序层面的性能。然而,在内核中使用庞大的SIMD寄存器,可能会造成较大的性能影响:因为每次程序中断时,内核不得不储存整个庞大的SIMD寄存器以备恢复——这意味着,对每个硬件中断或系统调用,完整的SIMD状态必须存到主存中。由于SIMD状态可能相当大(512~1600个字节),而中断可能时常发生,这些额外的存储与恢复操作可能显著地影响效率。但是x86_64架构的浮点数指针运算默认依赖于SIMD寄存器,为了解决这一问题,我们启用soft-float特征,它将使用基于整数的软件功能,模拟浮点数指针运算。

3) 编译我们的内核

我们将使用Linux系统的编写风格(这可能是LLVM的默认风格)编译我们的内核。这意味着,我们在main.rs中需要一个名为“_start”的入口点。注意,无论是什么主机操作系统,入口点都应该被命名为_start。

• 通过--target传入JSON文件名来编译我们的内核:

4f791a844a2ab6a0f8e8820873e3405e.png

报错无法编译。这是因为Rust编译器找不到core或compiler_builtins包;而所有no_std上下文都隐式地链接到这两个包。core包包含基础的Rust类型,如Result、Option和迭代器等;compiler_builtins包提供LLVM需要的许多底层操作,比如memcpy。问题是core库是作为预编译库和Rust编译器一起发布的。这时,core库只对支持的宿主系统有效,而对我们定义的目标系统无效。如果我们想为其它系统编译,就需要为这些系统重新编译core。

• 为此我们引入[cargo xbuild],它是一个可以自动交叉编译core和其他内置库的cargo build的封装。输入“cargo install cargo-xbuild”进行安装:

f815d8a7cea8c7511b5102f814a36862.png

• 输入“cargo xbuild --target x86_64-sjy_os.json”代替build重新编译:

7a632050eafcbcb388ec82e8891020e5.png

cargo xbuild为我们自定义的目标交叉编译了core、compiler_builtin和alloc三个部件。由于这些库用了大量的unstable的功能,所以只能在nightly版本的Rust编译器中工作。

• 为了避免每次调用cargo-xbuild都得向--target传递参数,我们指定一个默认的target。在sjy_os下新建一个.cargo文件夹,再在其中创建一个名为config的cargo配置文件:

5e307e07c13e65fefa91c101784a1a38.png

这里的配置告诉cargo在没有显式声明目标时,使用x86_64-sjy_os.json作为目标配置。

• 直接使用“cargo xbuild”编译内核:

ba4de92054b75bf1ac8fcf88367f1f95.png

4) 打印到屏幕

现阶段向屏幕打印字符的最简单的方式就是通过VGA text buffer了。这是一块映射到VGA硬件的特殊的内存区域,它里面包含了要显示到屏幕上的内容。它通常由25行组成,每行包含80个字符单元。每个字符单元显示一个包含前景和背景色的ASCII字符。在屏幕上显示效果如下:

ec607872e3c1eab8f69cfe3c3bc5e6ee.png

这段缓冲区的地址是0xb8000,且每个字符单元包含一个ASCII码字节和一个颜色字节。

• 在main.rs中添加以下代码:

0c925d89e8e056b6e0f4eb1d30a6e1b7.png

在这段代码中,我们预先定义了一个字节字符串类型的静态变量,名为HELLO。我们首先将整数0xb8000转换为一个裸指针。这之后,我们迭代HELLO的每个字节,使用enumerate函数获得一个额外的序号变量i。在for语句的循环体中,我们使用offset偏移裸指针,解引用它,来将字符串的每个字节和对应的颜色字节——0xb代表淡青色——写入内存位置。

注意所有的裸指针内存操作都在一个unsafe块中。因为编译器不能确保我们创建的裸指针是有效的;一个裸指针可能指向任何一个内存位置;直接引用并写入它可能会损坏正常的数据。使用unsafe块时,程序员其实在告诉编译器语句块内的操作是有效的。unsafe语句块并不会关闭Rust的安全检查机制;它只是允许一些额外的操作(解除对原始指针的引用/调用不安全的函数或方法/访问或修改可变静态变量/实现不安全的特性/准入共有领域)。

使用unsafe块要求程序员有足够的自信,需要强调,肆意使用unsafe语句块并不是Rust编程的一贯方式。在缺乏足够经验的时候,直接在unsafe语句块内操作裸指针容易把事情弄得很糟糕;比如,在不注意的情况下,我们很可能会意外地操作缓冲区以外的内存。

在这样的前提下,我们希望最小化unsafe语句块的使用。使用Rust语言,我们能够将不安全操作将包装为一个安全的抽象模块。举个例子,我们可以创建一个VGA缓冲区类型,把所有的不安全语句封装起来,来确保从类型外部操作时,无法写出不安全的代码:通过这种方式,我们只需要最少的unsafe语句块来确保我们不破坏内存安全。

3. 运行我们的内核

首先,我们将编译完毕的内核与引导程序链接,来创建一个引导映像;这之后,我们可以在QEMU虚拟机中运行它,或者通过U盘在真机上运行。

1) 创建引导映像

要将可执行程序转换为可引导的映像,我们需要把它和引导程序链接。这里,引导程序将负责初始化CPU并加载我们的内核。编写引导程序并不容易,所以我们不编写自己的引导程序,而是使用已有的bootloader包;无需依赖于C语言,这个包基于Rust代码和内联汇编,完整地实现了BIOS引导程序。

• 为了用它启动内核,我们要将它添加为一个依赖项,在Cargo.toml中添加下面的代码:

ff7e1ba4dcab314e68a82ecfc10f540f.png

只添加引导程序为依赖项,并不足以创建一个可引导的磁盘映像;我们还需要内核编译完成之后,将内核和引导程序组合在一起。然而,截至目前,原生的cargo并不支持在编译完成后添加其它步骤。

• 为了解决这个问题,我们建议使用bootimage工具——它将会在内核编译完毕后,将它和引导程序组合在一起,最终创建一个能够引导的磁盘映像。输入“cargo install bootimage --version "^0.7.3"”安装这款工具。

• 输入“rustup component add llvm-tools-preview”以运行bootimage以及编译引导程序。

• 在安装完bootimage并添加llvm-tools-preview组件后我们输入“cargo bootimage”创建磁盘启动镜像:

41b1b37425d5ad79f12ab4255b57e5af.png

可以看到的是,bootimage工具开始使用cargo xbuild来编译内核,所以它将增量编译我们修改后的源码。在这之后,它会编译内核的引导程序,这可能将花费一定的时间;但和所有其它依赖包相似的是,在首次编译后,产生的二进制文件将被缓存下来,这将显著地加速后续的编译过程。最终,bootimage将把内核和引导程序组合为一个可引导的磁盘映像。

运行这行命令之后,在target/x86_64-sjy_os/debug目录内可以找到我们的映像文件bootimage-blog_os.bin。它可以在虚拟机内启动,也可以刻录到U盘上以便在真机上启动。(注意因为文件格式不同,这里的bin并不是光驱映像,所以将它刻录到光盘不会起作用。)

4c1ee1ac9aa3fbcc9f7f3035ac11d7a9.png

bootimage工具在底层实际做了这些工作:

* 将内核编译成ELF文件。

* 将bootloader依赖编译成独立的可执行文件。

* 将内核ELF文件按字节和bootloader拼接起来。

在启动的时候,bootloader会读取并解析附加的ELF文件。然后它会将代码段映射到页表的虚拟地址里,将.bss小节设置0,并设置堆栈。最后,它会读取入口点的地址(在我们的内核里是_start函数),并跳转到相应位置。

2) 在QEMU中启动内核

• 找到qemu目录,file=bootimage-sjy_os.bin的路径:

af7e4d679f965261c0047e1f49cc0ac2.png

23b42b431fcecd93a26bb111c7ef5a42.png

3) 使用“cargo run”

• 在.cargo/config中添加以下代码:

454ffe278931b7bada4a3529fdca606b.png

在这里,target.'cfg(target_os="none")'筛选了三元组中宿主系统设置为"none"的所有编译目标(包括x86_64-sjy_os.json)。runner的值规定了运行cargo xrun使用的命令;这个命令将在成功编译后执行,而且会传递可执行文件的路径为第一个参数。

命令bootimage runner由bootimage包提供,将给定的可执行文件与项目的引导程序依赖项链接,然后在QEMU中启动它。

• 将qemu文件中的全部内容拷贝到sjy_os文件夹下,输入“cargo xrun”运行:

且会传递可执行文件的路径为第一个参数。

命令bootimage runner由bootimage包提供,将给定的可执行文件与项目的引导程序依赖项链接,然后在QEMU中启动它。

• 将qemu文件中的全部内容拷贝到sjy_os文件夹下,输入“cargo xrun”运行:

84e65613b29db61644b9d70deffe2190.png

[1][2]

参考

  1. ^本实验部分翻译参考 https://www.zhihu.com/people/xu-ling-xiao-63-98
  2. ^本实验部分翻译参考 https://www.zhihu.com/people/luojia-14-40
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值