1. 独立式可执行程序
本章代码对应 commit :2ee1e8427f857388d9d93301d17d4464ffb2747a
概要
由于我们的目标是编写一个操作系统,所以首先我们需要创建一个独立于操作系统的可执行程序,又称 独立式可执行程序(freestanding executable) 或 裸机程序(bare-metal executable) 。这意味着所有依赖于操作系统的库我们都不能使用。比如 std 中的大部分内容(io, thread, file system, etc.)都需要操作系统的支持,所以这部分内容我们不能使用。
但是,不依赖于操作系统的 rust 的语言特性 我们还是可以继续使用的,比如:迭代器、模式匹配、字符串格式化、所有权系统等。这使得 rust 依旧可以作为一个功能强大的高级语言,帮助我们编写操作系统。
本章我们将介绍:
安装 rust(nightly 版本) 。
创建可执行的 rust 项目。
将创建的 rust 项目修改为 freestanding rust binary ,这包括 禁用 std 库 并解决由此产生的一系列问题。
安装 nightly rust
rust 包含:stable、beta、nightly 三个版本。默认情况下我们安装的是 stable 。由于在编写操作系统时需要使用 rust 的一些不稳定的实验功能,所以请根据 rust 官方教程安装 rust nightly 。
安装成功后使用 rustc --version
或 rustup show
查看当前 rust 的版本。
如果未能成功切换 rust 版本,请查看 how to switch rust toolchain
创建并执行rust binary 项目
使用 cargo new
创建一个新的 rust binary 项目,如下:
第一个应用已经能运行了!但这不是我们需要的OS,而仅仅是一个离不了OS的应用程序而已。我们需要建立直接运行在硬件上的OS。下面将进行进一步探索。
添加 no_std 属性
因为我们的目标是编写一个操作系统,所以我们不能使用任何依赖于操作系统的库。 项目默认是链接标准库的,我们需要显式地将其禁用:
如果此时执行 cargo build
构建项目,会产生以下两个错误:
现在我们来依次解决这两个问题。
FIX error: cannot find macro 'println!' in this scope
println 宏属于标准库,所以禁用标准库后自然不能再使用 println 。由于我们当前目标只是写一个可执行的文件,所以将其删除即可:
FIX error: '#[panic_handler]' function required, but not found
在程序发生 panic 时需要调用相应函数。标准库有对应函数,但是由于我们使用了 no_std 属性,所以接下来我们需要自己实现一个函数:
由于程序 panic 后就应该结束,所以用 -> ! 表示该函数不会返回。由于目前的 OS 功能还很弱小,所以只能无限循环。
解决了上述两个 error 后,再次执行 cargo build
,结果出现了新的 error:
FIX error: language item required, but not found: 'eh_personality'
eh_personality 语义项(language item)是 exception handling personality 的意思。 我们不使用该项,故直接将 dev (use for cargo build
) 和 release (use for cargo build --release
) 的 panic 的处理策略设为 abort。
通常, 当程序出现了异常 (这里指类似 Java 中层层抛出的异常), 从异常点开始会沿着 caller 调用栈一层一层回溯, 直到找到某个函数能够捕获 (catch) 这个异常. 这个过程称为 堆栈展开 (stack unwinding). 回溯的时候, caller 中也许会有局部变量需要清理, 比如 C++ 的 RAII 的析构或者 Rust 的 drop. exception handling personality 是一个函数, 它对于每个 caller 函数执行这个过程. 这是一个复杂的过程, 并且还依赖其他各种库文件.
上面设置为 abort, 是指 OS panic 了那么我们不做任何清理的意思. 这样相对简单.
再次运行 cargo build
,不出所料,又出现了新的 error:
FIX error: requires 'start' lang_item
对于大多数语言,他们都使用了 运行时系统(runtime system) ,这导致 main 并不是他们执行的第一个函数。
以 rust 语言为例:一个典型的 rust 程序会先链接标准库,然后运行 C runtime library 中的 crt0(C runtime zero) 设置 C 程序运行所需要的环境(比如:创建堆栈,设置寄存器参数等)。然后 C runtime 会调用 rust runtime 的 入口点(entry point) 。rust runtime 结束之后才会调用 main 。由于我们的程序无法访问 rust runtime 和 crt0 ,所以需要重写覆盖 crt0 入口点:
适用于 Linux ,在其他系统请 点击这里 。暂时无法编译也没关系,因为下一章会重写
_start
函数
这里 pub extern "C" fn main
就是我们需要的 start 。 #[no_mangle] 属性用于防止改名称被混淆。由于 start 只能由操作系统或引导加载程序直接调用,不会被其他函数调用,所以不能够返回。如果需要离开该函数,应该使用 exit 系统调用。但是由于我们的操作系统还没有实现 exit 系统调用,所以暂时使用无限循环防止函数返回。由于 start 函数无法返回或退出,自然也就不会调用 main 。所以将 main 函数删除,并且增加属性标签 #![no_main] 。
再次执行 cargo build
,很不幸,又出现了 error:
但幸运的是,这是我们本章所需要处理的最后一个 error!
FIX error: linking with 'cc' failed: exit code: 1
在链接 C runtime 时,会需要一些 C 标准库(libc)的内容。由于 #![no_std] 禁用了标准库,所以我们需要禁用常规的 C 启动例程。 于是将之前的 cargo build
换成如下的命令就好啦:
生成的可执行程序在 target/debug/
中,在 Cargo.toml
同级目录下执行如下命令可以看到
适用于 Linux ,在其他系统请 点击这里
可以把上述命令写到一个脚本"build.sh"中,并直接执行这个build.sh即可:
历经千辛万苦,我们终于成功构建了一个 Freestanding Rust Binary !!!
此处应有掌声
预告
下一章,我们将在 Freestanding Rust Binary 的基础上,创建 最小内核 ,将其和 bootloader 链接成为可以被 qemu 加载的 bootimage 。并且将能够在屏幕上打印 Hello World !
Last updated