前言
此文為舊文新發(fā)遣臼,這是我之前翻譯文章翘单,因?yàn)楹?jiǎn)書有段時(shí)間做內(nèi)部審查彩届,不能發(fā)表文章伪冰,所以當(dāng)時(shí)寫完放在了其它平臺(tái)上,今天將這篇移到簡(jiǎn)書上來(lái)樟蠕。因?yàn)闆](méi)有用無(wú)GC
的語(yǔ)言寫過(guò)上生產(chǎn)的代碼贮聂,一直有一些遺憾,近年來(lái)Rust
崛起了寨辩,從TiKV到Libra等項(xiàng)目大膽采用Rust
吓懈,這門語(yǔ)言逐漸為人所接受也成熟起來(lái),遂借著翻譯的機(jī)會(huì)靡狞,學(xué)習(xí)這門語(yǔ)言新貴耻警,順便補(bǔ)一下操作系統(tǒng)欠下的技術(shù)債
A Freestanding Rust Binary
獨(dú)立的Rust
二進(jìn)制文件
若要編寫我們自己的操作系統(tǒng)內(nèi)核, 第一步便是創(chuàng)建一個(gè)不鏈接標(biāo)準(zhǔn)庫(kù)(注解:類比為C語(yǔ)言中的stdlib)的Rust可執(zhí)行文件。這一步是為了在沒(méi)有底層操作系統(tǒng)支持的情況下, 使得Rust能夠在裸機(jī)上運(yùn)行甘穿。
這個(gè)博客是在GitHub上公開發(fā)布的腮恩。如果您有任何問(wèn)題或疑問(wèn),歡迎提issue温兼。您也可以在底部留下評(píng)論秸滴。本文的完整源代碼可以在post-01分支中找到。
介紹
要編寫操作系統(tǒng)內(nèi)核募判,我們需要不依賴于任何操作系統(tǒng)特性的代碼荡含。這意味著我們不能使用線程、文件兰伤、堆內(nèi)存内颗、網(wǎng)絡(luò)钧排、隨機(jī)數(shù)敦腔、標(biāo)準(zhǔn)輸出或任何其他需要OS抽象或特定硬件的特性。必須如此恨溜,因?yàn)槲覀冋趪L試編寫自己的操作系統(tǒng)和驅(qū)動(dòng)程序符衔。
這意味著我們不能使用大多數(shù)Rust標(biāo)準(zhǔn)庫(kù),但是我們可以使用很多其它Rust特性糟袁。比如我們可以使用迭代器判族、閉包、模式匹配项戴、選項(xiàng)和結(jié)果形帮、字符串格式化,當(dāng)然還有所有權(quán)系統(tǒng)周叮。這些特性讓我們能用一種非常有表現(xiàn)力的辩撑、高層次的方式編寫內(nèi)核,不必?fù)?dān)心未定義的行為或內(nèi)存安全問(wèn)題仿耽。
為了在Rust中創(chuàng)建操作系統(tǒng)內(nèi)核合冀,我們需要?jiǎng)?chuàng)建一個(gè)可執(zhí)行文件,它可以在沒(méi)有底層操作系統(tǒng)運(yùn)行的情況下運(yùn)行项贺。這種可執(zhí)行文件通常被稱為"獨(dú)立的"或"裸金屬"可執(zhí)行文件君躺。
禁用標(biāo)準(zhǔn)庫(kù)
默認(rèn)情況下,所有Rust的項(xiàng)目都需要鏈接標(biāo)準(zhǔn)庫(kù)开缎,而標(biāo)準(zhǔn)庫(kù)的特性又取決于操作系統(tǒng)棕叫,比如線程、文件或網(wǎng)絡(luò)奕删。它還依賴于與操作系統(tǒng)緊密交互的C標(biāo)準(zhǔn)庫(kù)libc
谍珊。因?yàn)槲覀兊挠?jì)劃正是編寫一個(gè)操作系統(tǒng),所以我們不能使用任何依賴于操作系統(tǒng)的庫(kù)。因此砌滞,我們可以通過(guò)no_std
屬性來(lái)禁用標(biāo)準(zhǔn)庫(kù)侮邀。
我們首先要通過(guò)創(chuàng)建一個(gè)由cargo
管理依賴的(注解:Cargo是Rust程序的包管理器)Rust應(yīng)用程序。最簡(jiǎn)單的方法是通過(guò)命令行:
cargo new blog_os --bin --edition 2018
我將項(xiàng)目命名為blog_os
贝润,當(dāng)然您可以選擇自己的名稱绊茧。-—bin
指定我們想要?jiǎng)?chuàng)建一個(gè)可執(zhí)行的二進(jìn)制文件(與lib庫(kù)有所不同),--edition 2018
指定我們的項(xiàng)目使用Rust的2018版本打掘。當(dāng)我們運(yùn)行該命令時(shí)华畏,cargo為我們創(chuàng)建了以下目錄結(jié)構(gòu):
blog_os
├── Cargo.toml
└── src
└── main.rs
Cargo.toml
包含有項(xiàng)目的配置, 比如項(xiàng)目名尊蚁、作者亡笑、版本號(hào)及其依賴, src/main.rs
則包括項(xiàng)目根目錄及main
函數(shù)横朋。您可以通過(guò)cargo build
編譯您的項(xiàng)目仑乌,然后在子目錄target/debug
中運(yùn)行編譯后的blog_os
二進(jìn)制文件。
no_std
屬性
現(xiàn)在琴锭,我們的項(xiàng)目隱式地鏈接了標(biāo)準(zhǔn)庫(kù)晰甚。讓我們通過(guò)添加no_std
屬性來(lái)禁用它:
// main.rs
#![no_std]
fn main() {
println!("Hello, world!");
}
通過(guò)運(yùn)行cargo build
編譯項(xiàng)目時(shí),會(huì)發(fā)生以下錯(cuò)誤:
error: cannot find macro `println!` in this scope
--> src/main.rs:4:5
|
4 | println!("Hello, world!");
|
這個(gè)錯(cuò)誤的原因是println宏
是標(biāo)準(zhǔn)庫(kù)的一部分决帖,它會(huì)打印標(biāo)準(zhǔn)輸出厕九,這是操作系統(tǒng)提供的一個(gè)特殊的文件描述符。我們禁用后就不能再打印東西了地回。
所以扁远,讓我們刪除打印,將main
函數(shù)置空并再次嘗試:
// 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`
如上所示刻像,現(xiàn)在編譯器缺少一個(gè)#[panic_handler]
函數(shù)和一個(gè)語(yǔ)言項(xiàng)畅买。
Panic
的實(shí)現(xiàn)
panic_handler
是編譯器在發(fā)生panic
(注解:可理解為異常)時(shí)應(yīng)該調(diào)用的函數(shù)。標(biāo)準(zhǔn)庫(kù)提供了自己的panic handler
函數(shù)绎速,但是在no_std
環(huán)境中皮获,我們需要自己定義它:
// in main.rs
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
PanicInfo
參數(shù)包含發(fā)生異常的文件及其對(duì)應(yīng)的行,以及可選的異常消息纹冤。該函數(shù)應(yīng)該永遠(yuǎn)不會(huì)返回洒宝,因此需要通過(guò)返回!
類型將其標(biāo)記為一個(gè)發(fā)散函數(shù)。在這個(gè)函數(shù)中我們能做的不多萌京,所以我們就讓它無(wú)限循環(huán)吧雁歌。
eh_personality
語(yǔ)言項(xiàng)
語(yǔ)言項(xiàng)是編譯器內(nèi)部需要的特殊函數(shù)和類型。例如知残,Copy
是一個(gè)語(yǔ)言項(xiàng)靠瞎,它告訴編譯器哪些類型具有[copy](https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html)
語(yǔ)義。當(dāng)我們查看實(shí)現(xiàn)時(shí),我們看到它具有特殊的#[lang = "copy"]
屬性乏盐,該屬性將其定義為了一個(gè)語(yǔ)言項(xiàng)佳窑。
提供自己的語(yǔ)言項(xiàng)的實(shí)現(xiàn)是可能的,但這只能作為最后的手段父能。原因是語(yǔ)言項(xiàng)是高度不穩(wěn)定的實(shí)現(xiàn)細(xì)節(jié)神凑,甚至沒(méi)有類型檢查(因此編譯器甚至沒(méi)有檢查函數(shù)是否具有正確的參數(shù)類型)。幸運(yùn)的是何吝,有一種更穩(wěn)定的方法可以修復(fù)上述語(yǔ)言項(xiàng)錯(cuò)誤溉委。
eh_personality
語(yǔ)言項(xiàng)標(biāo)記了一個(gè)用于實(shí)現(xiàn)stackunwinding(注解:?)的函數(shù)爱榕。默認(rèn)情況下瓣喊,Rust使用unwind來(lái)運(yùn)行所有活動(dòng)堆棧變量的析構(gòu)函數(shù),以防出現(xiàn)異常黔酥。這確保釋放所有使用的內(nèi)存藻三,并允許父線程捕捉異常并繼續(xù)執(zhí)行。然而絮爷,unwind是一個(gè)復(fù)雜的過(guò)程趴酣,需要一些特定于操作系統(tǒng)的庫(kù)(例如Linux上的libunwind或Windows上的結(jié)構(gòu)化異常處理)梨树,所以我們不想在操作系統(tǒng)上使用它坑夯。
禁用Unwinding
還有其他一些用例不希望運(yùn)行unwinding
,所以Rust提供了一個(gè)在異常時(shí)中止的選項(xiàng)抡四。這禁止生成展開符號(hào)信息柜蜈,從而大大減小了二進(jìn)制文件的大小。我們可以在多個(gè)地方禁用unwind指巡。最簡(jiǎn)單的方法是在我們的Cargo.toml
加上以下幾行淑履。
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
這將為dev
概要文件(用于構(gòu)建開發(fā)版本)和release
概要文件(用于構(gòu)建release
版本)設(shè)置終止panic
策略(注解:直接中斷,不展示異常堆棧信息)≡逖現(xiàn)在不再需要eh_personality
語(yǔ)言項(xiàng)秘噪。
現(xiàn)在我們修正了上面的兩個(gè)錯(cuò)誤。然而勉耀,如果我們現(xiàn)在編譯它指煎,會(huì)發(fā)現(xiàn)另外一個(gè)錯(cuò)誤:
> cargo build
error: requires `start` lang_item
我們的程序缺少定義入口點(diǎn)的start
語(yǔ)言項(xiàng)。
start
屬性
有人可能認(rèn)為main
(注解:一直以來(lái)就是這樣認(rèn)為的)函數(shù)是運(yùn)行程序時(shí)調(diào)用的第一個(gè)函數(shù)便斥。然而至壤,大多數(shù)語(yǔ)言都有一個(gè)運(yùn)行時(shí)系統(tǒng),它負(fù)責(zé)垃圾收集(例如Java)或軟件線程(例如Go中的goroutines)枢纠。這個(gè)運(yùn)行時(shí)需要在main之前調(diào)用像街,因?yàn)樗枰跏蓟约骸?/p>
在鏈接標(biāo)準(zhǔn)庫(kù)的典型Rust二進(jìn)制文件中,執(zhí)行從一個(gè)名為crt0
("C runtime zero")的C運(yùn)行時(shí)庫(kù)開始,該庫(kù)為C應(yīng)用程序設(shè)置了環(huán)境镰绎。這包括創(chuàng)建堆棧并將參數(shù)放在正確的寄存器中脓斩。然后C運(yùn)行時(shí)調(diào)用Rust運(yùn)行時(shí)的入口點(diǎn),該入口點(diǎn)由start language項(xiàng)標(biāo)記畴栖。Rust只有一個(gè)非常小的運(yùn)行時(shí)俭厚,它負(fù)責(zé)一些小事,比如設(shè)置堆棧溢出保護(hù)或在panic上打印回溯驶臊。運(yùn)行時(shí)最后調(diào)用main
函數(shù)挪挤。
我們獨(dú)立的可執(zhí)行文件不能訪問(wèn)Rust運(yùn)行時(shí)和crt0
,所以我們需要定義自己的入口點(diǎn)关翎。實(shí)現(xiàn)start語(yǔ)言項(xiàng)沒(méi)有幫助扛门,因?yàn)樗匀恍枰猚rt0。相反纵寝,我們需要直接覆蓋crt0入口點(diǎn)论寨。
覆蓋入口函數(shù)
為了告訴Rust編譯器我們不想使用普通的入口鏈接函數(shù),我們添加了#![no_main]
屬性爽茴。
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
您可能會(huì)注意到我們刪除了主要功能葬凳。 原因是如果沒(méi)有調(diào)用它的底層運(yùn)行時(shí),main
就沒(méi)有意義室奏。 我們現(xiàn)在用我們自己的_start
函數(shù)覆蓋操作系統(tǒng)入口函數(shù):
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
通過(guò)使用#[no_mangle]
屬性火焰,我們禁用名稱修改以確保Rust編譯器確實(shí)輸出名為_start
的函數(shù)。如果沒(méi)有該屬性胧沫,編譯器將生成一些神秘的_ZN3blog_os4_start7hb173fedf945531caE
符號(hào)昌简,以便為每個(gè)函數(shù)提供唯一的名稱。該屬性是必需的绒怨,因?yàn)槲覀冃枰谙乱徊街懈嬷溄悠鞯娜肟邳c(diǎn)函數(shù)的名稱纯赎。
我們還必須將函數(shù)標(biāo)記為extern"C"
,告訴編譯器它應(yīng)該使用此函數(shù)的C調(diào)用約定(而不是未指定的Rust調(diào)用約定)南蹂。命名函數(shù)_start
的原因是這是大多數(shù)系統(tǒng)的默認(rèn)入口點(diǎn)名稱犬金。
符號(hào)!
返回類型意味著函數(shù)發(fā)散六剥,即不允許返回晚顷。這是必需的,因?yàn)槿魏魏瘮?shù)都不會(huì)調(diào)用入口點(diǎn)仗考,而是由操作系統(tǒng)或引導(dǎo)加載程序直接調(diào)用音同。因此,入口點(diǎn)不應(yīng)該返回秃嗜,而應(yīng)該是調(diào)用操作系統(tǒng)的退出系統(tǒng)調(diào)用权均。在我們的例子中顿膨,關(guān)閉機(jī)器可能是一個(gè)合理的操作,因?yàn)槿绻粋€(gè)獨(dú)立的二進(jìn)制返回叽赊,沒(méi)有什么可做的×滴郑現(xiàn)在,我們通過(guò)無(wú)休止地循環(huán)來(lái)滿足要求必指。
當(dāng)我們現(xiàn)在cargo build
囊咏,我們得到一個(gè)丑陋的鏈接器錯(cuò)誤。
鏈接錯(cuò)誤
鏈接器是一個(gè)將生成的代碼組合成可執(zhí)行文件的程序塔橡。 由于Linux梅割,Windows和macOS系統(tǒng)的可執(zhí)行文件格式彼此不同,因此每個(gè)系統(tǒng)都有自己的鏈接器葛家,當(dāng)然可能會(huì)引發(fā)不同的錯(cuò)誤户辞。 錯(cuò)誤的根本原因是相同的:鏈接器的默認(rèn)配置假定我們的程序依賴于C運(yùn)行時(shí)。
要解決這些錯(cuò)誤癞谒,我們需要告訴鏈接器它不應(yīng)該包含C運(yùn)行時(shí)底燎。 我們可以通過(guò)將一組參數(shù)傳遞給鏈接器或構(gòu)建裸機(jī)目標(biāo)來(lái)實(shí)現(xiàn)。
為裸機(jī)編譯目標(biāo)文件
默認(rèn)情況下弹砚,Rust會(huì)嘗試構(gòu)建一個(gè)能夠在當(dāng)前系統(tǒng)環(huán)境中運(yùn)行的可執(zhí)行文件双仍。 例如,如果您在x86_64
上使用Windows桌吃,Rust會(huì)嘗試構(gòu)建使用x86_64指令的.exe Windows可執(zhí)行文件朱沃。 此環(huán)境稱為“主機(jī)”系統(tǒng)。
為了描述不同的環(huán)境读存,Rust使用一個(gè)名為target triple
的字符串为流。 您可以通過(guò)運(yùn)行rustc --version --verbose
來(lái)查看主機(jī)系統(tǒng)的目標(biāo)三元組:
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
以上輸出來(lái)自x86_64 Linux系統(tǒng)呕屎。 我們看到主機(jī)三元組是x86_64-unknown-linux-gnu让簿,它包括CPU架構(gòu)(x86_64),供應(yīng)商(未知)秀睛,操作系統(tǒng)(linux)和ABI(gnu)尔当。
通過(guò)編譯我們的主機(jī)三元組,Rust編譯器和鏈接器假定有一個(gè)底層操作系統(tǒng)蹂安,如Linux或Windows椭迎,默認(rèn)情況下使用C運(yùn)行時(shí),這會(huì)導(dǎo)致鏈接器錯(cuò)誤田盈。 因此畜号,為了避免鏈接器錯(cuò)誤,我們可以針對(duì)沒(méi)有底層操作系統(tǒng)的不同環(huán)境進(jìn)行編譯允瞧。
這種裸機(jī)環(huán)境的一個(gè)例子是thumbv7em-none-eabihf
target triple简软,它描述了嵌入式ARM系統(tǒng)蛮拔。 細(xì)節(jié)并不重要,重要的是目標(biāo)三元組沒(méi)有底層操作系統(tǒng)痹升,由目標(biāo)三元組中的none表示建炫。 為了能夠?yàn)檫@個(gè)目標(biāo)進(jìn)行編譯,我們需要在rustup中添加它:
rustup target add thumbv7em-none-eabihf
這將下載系統(tǒng)的標(biāo)準(zhǔn)(和核心)庫(kù)的副本疼蛾。 現(xiàn)在我們可以為這個(gè)目標(biāo)構(gòu)建我們的獨(dú)立可執(zhí)行文件:
cargo build --target thumbv7em-none-eabihf
通過(guò)傳遞--target
參數(shù)肛跌,交叉編譯我們的可執(zhí)行文件用于裸機(jī)目標(biāo)系統(tǒng)。 由于目標(biāo)系統(tǒng)沒(méi)有操作系統(tǒng)察郁,鏈接器不會(huì)嘗試鏈接C運(yùn)行時(shí)衍慎,并且我們的構(gòu)建成功而沒(méi)有任何鏈接器錯(cuò)誤。
這是我們用于構(gòu)建操作系統(tǒng)內(nèi)核的方法皮钠。 我們將使用描述x86_64裸機(jī)環(huán)境的自定義目標(biāo)西饵,而不是thumbv7em-none-eabihf
。 細(xì)節(jié)將在下一篇文章中解釋鳞芙。
鏈接參數(shù)
除了為裸機(jī)系統(tǒng)進(jìn)行編譯之外眷柔,還可以通過(guò)將一組參數(shù)傳遞給鏈接器來(lái)解決鏈接器錯(cuò)誤。 這不是我們將用于內(nèi)核的方法原朝,因此本節(jié)是可選的驯嘱,僅提供完整性。 單擊下面的“鏈接器參數(shù)”以顯示可選內(nèi)容喳坠。
總結(jié)
最小的獨(dú)立Rust二進(jìn)制文件如下所示:
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
要構(gòu)建這個(gè)二進(jìn)制文件鞠评,我們需要根據(jù)相應(yīng)的裸機(jī)來(lái)編譯,例如thumbv7em-none-eabihf
:
cargo build --target thumbv7em-none-eabihf
或者壕鹉,我們可以通過(guò)傳遞其他鏈接參數(shù)編譯成不同的操作系統(tǒng)所所需的可執(zhí)行文件:
# 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"
請(qǐng)注意剃幌,這只是一個(gè)獨(dú)立的Rust二進(jìn)制文件的最小示例。 這個(gè)二進(jìn)制文件需要各種各樣的東西晾浴,例如在調(diào)用_start函數(shù)時(shí)初始化堆棧负乡。 因此,對(duì)于任何實(shí)際使用這種二進(jìn)制文件脊凰,可能需要更多步驟抖棘。
下一步
下一篇文章我們將會(huì)逐步討論二進(jìn)制文件轉(zhuǎn)換為最小操作系統(tǒng)內(nèi)核所需的步驟。 這包括創(chuàng)建自定義編譯目標(biāo)狸涌,將可執(zhí)行文件與引導(dǎo)加載程序相結(jié)合切省,以及學(xué)習(xí)如何在屏幕上打印內(nèi)容。
支持我
如果您喜歡這篇文章并想支持我帕胆,可以通過(guò)Donorbox朝捆,Patreon或Liberapay聯(lián)系我。 謝謝懒豹!
原文: