Rust的二进制文件

前言

此文为旧文新发,这是我之前翻译文章,因为简书有段时间做内部审查,不能发表文章,所以当时写完放在了其它平台上,今天将这篇移到简书上来。因为没有用无GC的语言写过上生产的代码,一直有一些遗憾,近年来Rust崛起了,从TiKVLibra等项目大胆采用Rust,这门语言逐渐为人所接受也成熟起来,遂借着翻译的机会,学习这门语言新贵,顺便补一下操作系统欠下的技术债


A Freestanding Rust Binary

独立的Rust二进制文件

若要编写我们自己的操作系统内核, 第一步便是创建一个不链接标准库(注解:类比为C语言中的stdlib)的Rust可执行文件。这一步是为了在没有底层操作系统支持的情况下, 使得Rust能够在裸机上运行。

这个博客是在GitHub上公开发布的。如果您有任何问题或疑问,欢迎提issue。您也可以在底部留下评论。本文的完整源代码可以在post-01分支中找到。

介绍

要编写操作系统内核,我们需要不依赖于任何操作系统特性的代码。这意味着我们不能使用线程、文件、堆内存、网络、随机数、标准输出或任何其他需要OS抽象或特定硬件的特性。必须如此,因为我们正在尝试编写自己的操作系统和驱动程序。

这意味着我们不能使用大多数Rust标准库,但是我们可以使用很多其它Rust特性。比如我们可以使用迭代器闭包模式匹配选项结果字符串格式化,当然还有所有权系统。这些特性让我们能用一种非常有表现力的、高层次的方式编写内核,不必担心未定义的行为或内存安全问题。

为了在Rust中创建操作系统内核,我们需要创建一个可执行文件,它可以在没有底层操作系统运行的情况下运行。这种可执行文件通常被称为"独立的"或"裸金属"可执行文件。

禁用标准库

默认情况下,所有Rust的项目都需要链接标准库,而标准库的特性又取决于操作系统,比如线程、文件或网络。它还依赖于与操作系统紧密交互的C标准库libc。因为我们的计划正是编写一个操作系统,所以我们不能使用任何依赖于操作系统的库。因此,我们可以通过no_std属性来禁用标准库。

我们首先要通过创建一个由cargo管理依赖的(注解:Cargo是Rust程序的包管理器)Rust应用程序。最简单的方法是通过命令行:

cargo new blog_os --bin --edition 2018

我将项目命名为blog_os,当然您可以选择自己的名称。-—bin指定我们想要创建一个可执行的二进制文件(与lib库有所不同),--edition 2018指定我们的项目使用Rust的2018版本。当我们运行该命令时,cargo为我们创建了以下目录结构:

blog_os
├── Cargo.toml
└── src
    └── main.rs

Cargo.toml包含有项目的配置, 比如项目名、作者、版本号及其依赖, src/main.rs则包括项目根目录及main函数。您可以通过cargo build编译您的项目,然后在子目录target/debug中运行编译后的blog_os二进制文件。

no_std属性

现在,我们的项目隐式地链接了标准库。让我们通过添加no_std属性来禁用它:

// main.rs

#![no_std]

fn main() {
    println!("Hello, world!");
}

通过运行cargo build编译项目时,会发生以下错误:

error: cannot find macro `println!` in this scope
 --> src/main.rs:4:5
  |
4 |     println!("Hello, world!");
  |

这个错误的原因是println宏是标准库的一部分,它会打印标准输出,这是操作系统提供的一个特殊的文件描述符。我们禁用后就不能再打印东西了。

所以,让我们删除打印,将main函数置空并再次尝试:

// main.rs

#![no_std]

fn main() {}
> cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`

如上所示,现在编译器缺少一个#[panic_handler]函数和一个语言项。

Panic的实现

panic_handler是编译器在发生panic(注解:可理解为异常)时应该调用的函数。标准库提供了自己的panic handler函数,但是在no_std环境中,我们需要自己定义它:

// in main.rs

use core::panic::PanicInfo;

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

PanicInfo参数包含发生异常的文件及其对应的行,以及可选的异常消息。该函数应该永远不会返回,因此需要通过返回!类型将其标记为一个发散函数。在这个函数中我们能做的不多,所以我们就让它无限循环吧。

eh_personality语言项

语言项是编译器内部需要的特殊函数和类型。例如,Copy是一个语言项,它告诉编译器哪些类型具有[copy](https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html)语义。当我们查看实现时,我们看到它具有特殊的#[lang = "copy"]属性,该属性将其定义为了一个语言项。

提供自己的语言项的实现是可能的,但这只能作为最后的手段。原因是语言项是高度不稳定的实现细节,甚至没有类型检查(因此编译器甚至没有检查函数是否具有正确的参数类型)。幸运的是,有一种更稳定的方法可以修复上述语言项错误。

eh_personality语言项标记了一个用于实现stackunwinding(注解:?)的函数。默认情况下,Rust使用unwind来运行所有活动堆栈变量的析构函数,以防出现异常。这确保释放所有使用的内存,并允许父线程捕捉异常并继续执行。然而,unwind是一个复杂的过程,需要一些特定于操作系统的库(例如Linux上的libunwind或Windows上的结构化异常处理),所以我们不想在操作系统上使用它。

禁用Unwinding

还有其他一些用例不希望运行unwinding,所以Rust提供了一个在异常时中止的选项。这禁止生成展开符号信息,从而大大减小了二进制文件的大小。我们可以在多个地方禁用unwind。最简单的方法是在我们的Cargo.toml加上以下几行。

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

这将为dev概要文件(用于构建开发版本)和release概要文件(用于构建release版本)设置终止panic策略(注解:直接中断,不展示异常堆栈信息)。现在不再需要eh_personality语言项。

现在我们修正了上面的两个错误。然而,如果我们现在编译它,会发现另外一个错误:

> cargo build
error: requires `start` lang_item

我们的程序缺少定义入口点的start语言项。

start属性

有人可能认为main(注解:一直以来就是这样认为的)函数是运行程序时调用的第一个函数。然而,大多数语言都有一个运行时系统,它负责垃圾收集(例如Java)或软件线程(例如Go中的goroutines)。这个运行时需要在main之前调用,因为它需要初始化自己。

在链接标准库的典型Rust二进制文件中,执行从一个名为crt0("C runtime zero")的C运行时库开始,该库为C应用程序设置了环境。这包括创建堆栈并将参数放在正确的寄存器中。然后C运行时调用Rust运行时的入口点,该入口点由start language项标记。Rust只有一个非常小的运行时,它负责一些小事,比如设置堆栈溢出保护或在panic上打印回溯。运行时最后调用main函数。

我们独立的可执行文件不能访问Rust运行时和crt0,所以我们需要定义自己的入口点。实现start语言项没有帮助,因为它仍然需要crt0。相反,我们需要直接覆盖crt0入口点。

覆盖入口函数

为了告诉Rust编译器我们不想使用普通的入口链接函数,我们添加了#![no_main]属性。

#![no_std]
#![no_main]

use core::panic::PanicInfo;

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

您可能会注意到我们删除了主要功能。 原因是如果没有调用它的底层运行时,main就没有意义。 我们现在用我们自己的_start函数覆盖操作系统入口函数:

#[no_mangle]
pub extern "C" fn _start() -> ! {
    loop {}
}

通过使用#[no_mangle]属性,我们禁用名称修改以确保Rust编译器确实输出名为_start的函数。如果没有该属性,编译器将生成一些神秘的_ZN3blog_os4_start7hb173fedf945531caE符号,以便为每个函数提供唯一的名称。该属性是必需的,因为我们需要在下一步中告知链接器的入口点函数的名称。

我们还必须将函数标记为extern"C",告诉编译器它应该使用此函数的C调用约定(而不是未指定的Rust调用约定)。命名函数_start的原因是这是大多数系统的默认入口点名称。

符号返回类型意味着函数发散,即不允许返回。这是必需的,因为任何函数都不会调用入口点,而是由操作系统或引导加载程序直接调用。因此,入口点不应该返回,而应该是调用操作系统的退出系统调用。在我们的例子中,关闭机器可能是一个合理的操作,因为如果一个独立的二进制返回,没有什么可做的。现在,我们通过无休止地循环来满足要求。

当我们现在cargo build,我们得到一个丑陋的链接器错误。

链接错误

链接器是一个将生成的代码组合成可执行文件的程序。 由于Linux,Windows和macOS系统的可执行文件格式彼此不同,因此每个系统都有自己的链接器,当然可能会引发不同的错误。 错误的根本原因是相同的:链接器的默认配置假定我们的程序依赖于C运行时。

要解决这些错误,我们需要告诉链接器它不应该包含C运行时。 我们可以通过将一组参数传递给链接器或构建裸机目标来实现。

为裸机编译目标文件

默认情况下,Rust会尝试构建一个能够在当前系统环境中运行的可执行文件。 例如,如果您在x86_64上使用Windows,Rust会尝试构建使用x86_64指令的.exe Windows可执行文件。 此环境称为“主机”系统。

为了描述不同的环境,Rust使用一个名为target triple的字符串。 您可以通过运行rustc --version --verbose来查看主机系统的目标三元组:

rustc 1.35.0-nightly (474e7a648 2019-04-07)
binary: rustc
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
commit-date: 2019-04-07
host: x86_64-unknown-linux-gnu
release: 1.35.0-nightly
LLVM version: 8.0

以上输出来自x86_64 Linux系统。 我们看到主机三元组是x86_64-unknown-linux-gnu,它包括CPU架构(x86_64),供应商(未知),操作系统(linux)和ABI(gnu)。

通过编译我们的主机三元组,Rust编译器和链接器假定有一个底层操作系统,如Linux或Windows,默认情况下使用C运行时,这会导致链接器错误。 因此,为了避免链接器错误,我们可以针对没有底层操作系统的不同环境进行编译。

这种裸机环境的一个例子是thumbv7em-none-eabihf target triple,它描述了嵌入式ARM系统。 细节并不重要,重要的是目标三元组没有底层操作系统,由目标三元组中的none表示。 为了能够为这个目标进行编译,我们需要在rustup中添加它:

rustup target add thumbv7em-none-eabihf

这将下载系统的标准(和核心)库的副本。 现在我们可以为这个目标构建我们的独立可执行文件:

cargo build --target thumbv7em-none-eabihf

通过传递--target参数,交叉编译我们的可执行文件用于裸机目标系统。 由于目标系统没有操作系统,链接器不会尝试链接C运行时,并且我们的构建成功而没有任何链接器错误。

这是我们用于构建操作系统内核的方法。 我们将使用描述x86_64裸机环境的自定义目标,而不是thumbv7em-none-eabihf。 细节将在下一篇文章中解释。

链接参数

除了为裸机系统进行编译之外,还可以通过将一组参数传递给链接器来解决链接器错误。 这不是我们将用于内核的方法,因此本节是可选的,仅提供完整性。 单击下面的“链接器参数”以显示可选内容。

总结

最小的独立Rust二进制文件如下所示:

src/main.rs:

#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points

use core::panic::PanicInfo;

#[no_mangle] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
    // this function is the entry point, since the linker looks for a function
    // named `_start` by default
    loop {}
}

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

Cargo.toml:

[package]
name = "crate_name"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]

# the profile used for `cargo build`
[profile.dev]
panic = "abort" # disable stack unwinding on panic

# the profile used for `cargo build --release`
[profile.release]
panic = "abort" # disable stack unwinding on panic

要构建这个二进制文件,我们需要根据相应的裸机来编译,例如thumbv7em-none-eabihf

cargo build --target thumbv7em-none-eabihf

或者,我们可以通过传递其他链接参数编译成不同的操作系统所所需的可执行文件:

# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"

请注意,这只是一个独立的Rust二进制文件的最小示例。 这个二进制文件需要各种各样的东西,例如在调用_start函数时初始化堆栈。 因此,对于任何实际使用这种二进制文件,可能需要更多步骤。

下一步

下一篇文章我们将会逐步讨论二进制文件转换为最小操作系统内核所需的步骤。 这包括创建自定义编译目标,将可执行文件与引导加载程序相结合,以及学习如何在屏幕上打印内容。

支持我

如果您喜欢这篇文章并想支持我,可以通过DonorboxPatreonLiberapay联系我。 谢谢!

原文:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343