用Rust寫作業系統(1)一個獨立的rust二進位制程式[翻譯]
一個獨立的rust二進位制程式
原文
https://
os。phil-opp。com/freesta
nding-rust-binary/
原作者 phil-opp 譯者 readlnh
建立一個不依賴於標準庫的rust可執行檔案是我們建立屬於自己的作業系統核心的第一步。這將使得在不依賴於底層作業系統的情況下在裸機bare metal 上執行一個rust程式成為可能。
這個系列的blog在GitHub上開放開發,如果你有任何問題,請在這裡開一個issuse來討論。當然你也可以在底部留言。你可以在這裡找到這篇文章的完整原始碼。
簡介
為了編寫一個作業系統核心,我們的程式碼不能依賴與任何與操系統相關的功能。也就是說,我們不能使用執行緒,檔案,記憶體堆疊,網路,隨機數字,標準輸入輸出和其他的一些依賴於作業系統抽象和特定硬體特性的功能。這其實很好理解,畢竟我們要寫的是自己的作業系統和自己的驅動。
這也就意味著我們不能使用大部分Rust標準庫的大部分內容,不過還有很多我們可以用的Rust的特性。舉例來說,我們可以使用迭代器,閉包,模式匹配,option和result,字串格式化,當然還有所有權系統。這些功能能讓我們在不需要擔心未定義行為和記憶體安全的情況下寫出富有表達性的高抽象層級的程式碼。
為了用Rust來構建作業系統核心,我們需要先建立一個可以在不依賴於底層作業系統執行的可執行程式。這些可執行程式通常被稱為“freestanding”或“bare-mental” 程式。
這篇文章描述了建立一個freetsanding的Rust二進位制程式的必要步驟並解釋了為什麼需要這些步驟,如果你只對最終的程式碼實現感興趣,你可以直接
跳轉到小結
禁用標準庫
在預設情況下,所有的Rust crates都和標準庫相關,標準庫依賴於作業系統的功能諸如程序,檔案,網路等。它還依賴於C標準庫
libc
,一個與作業系統服務緊密相連的庫。由於我們的目標是實現一個作業系統,所以我們不能使用任何依賴於作業系統的庫。所以我們必須透過 [
no_std
attribute]來禁用標準庫自動引用。
我們從使用cargo建立一個新工程開始。這一步最簡單的方法就是使用下面這條命令。
> cargo new blog_os ——bin ——edition 2018
我個人把這個專案命名為
blog_os
,當然你也可以選擇你自己的名字。
——bin
flag代表我們是要建立一個二進位制客執行程式,
——edition 2018
表示我們的crate要用的是2018 edition的Rust。當我們執行這條命令時,cargo會為我們建立如下結構。
blog_os
├── Cargo。toml
└── src
└── main。rs
Cargo。toml
包含了crate的設定,例如crate(包)名,作者,semantic版本,和依賴。
src/main。rs
檔案包含了crate的根模組和
main
函式。你可以透過
cargo build
命令來編譯你的crate,然後執行位於
target/debug
子目錄下的
blog_os
二進位制檔案。
no_std
Attribute
目前我們的crate暗中連結了標準庫。現在讓我們透過新增 [
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
macro]是標準庫的一部分,而我們不再把標準庫包含在內了。所以我們不能再列印任何東西了。這很好理解,因為
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]
函式和一個language item。
Panic的實現
panic_handler
屬性定義了一個函式,當[painc]發生時它就會被呼叫。標準庫會提供它自己的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 的引數包含了panic發生的檔案和行數以及可選的panic資訊。這個函式永遠會返回,所以它也透過將返回值型別設定為“never” type 記作!來被標記為diverging function。目前我們沒有什麼可以對這個函式做的,所以我們讓它無限迴圈。
eh_personality
Language Item
Language items是一些編譯器需要的特殊的函式或型別。舉例來說, [
Copy
] trai就是一個典型的language item,用來告訴編譯器那些型別需要遵循 [copy semantics(複製語義)][
Copy
]。當我們深入 implementation(實現)時,我們會發現一個特殊的
#[lang = “copy”]
attribute(屬性)將其定義為一個language item。
自己實現languange items是可能的,但這應該作為最後的手段。因為languages itmes是高度不穩定的語言細節實現甚至都沒有型別檢查(所以編譯器甚至不會檢查函式的引數型別是否正常)。幸運的是,還有別的更穩定的辦法來修復上述language item錯誤。
eh_personality
language item會標記那些用於實現 [stack unwinding(棧展開)]的函式。在預設情況下,當panic發生時Rust會使用展開來析構那些活躍在棧上的變數。這會確保所有使用的記憶體都會被釋放,並允許父程序捕獲panic,處理並繼續執行。然而,棧展開是一個非常複雜的過程,通常需要依賴作業系統的庫(例如Liunx上的 libunwind ,Windows上的 structured exception handling ),所以我們不打算在我們的作業系統裡使用它。
禁用展開
在很多情況下我們並不需要棧展開,所以Rust提供了[abort on panic(panic時終止)]作為替代。這個標誌能禁用棧展開相關的識別符號的生成從而縮小生成的二進位制從檔案的大小。我們有很多辦法來禁用展開,最簡單的辦法就是在
Cargo。toml
裡新增以下內容:
[profile。dev]
panic = “abort”
[profile。release]
panic = “abort”
這些將panic策略設定為abort的設定不僅僅對dev配置(用於
cargo build
)生效,還對release配置(用於
cargo build ——realease
)生效。現在,編譯器不會再要求
eh_personality
language item了。
現在我們已經修復了上述錯誤了。然而,當我們再次嘗試編譯,發現編譯器又要求另一個language item。
> cargo build
error: requires `start` lang_item
start
attribute
很多人通常覺得
main
函式是程式執行時第一個被呼叫的函式。然而,實際上大部分語言都有一個 runtime system(執行時系統),用來負責垃圾回收(比如Java)或軟體執行緒(比如go的協程(ps:softsware threads這個詞我有點拿捏不準 ))。這些runtime需要在
main
函式被呼叫前啟動,並初始化自身。
一個普通的連線到標準庫的Rust二進位制程式,是從一個叫
crt0
(“C runtime zero”)的C執行時庫開始執行的,這個庫會設定一個適合C語言應用程式執行的環境。這其中包括了棧的建立以及將引數放置到正確的暫存器裡等。C執行時會呼叫 entry point of the Rust runtime(Rust runtime入口),這個入口點被標記為
start
language item。Rust只有一個非常小的執行時,僅僅管理一些很簡單的事諸如設定棧溢位邊界和在panic列印一個backtrace。這個執行時最後會呼叫
main
函式。
我們的freestanding可執行檔案並沒有進入Rust runtime和
crt0
,所以我們需要自己來定義入口。在這裡實現
start
language item並沒有什麼幫助,程式仍然會要求
crt0
。因此,我們需要直接覆寫
crt0
入口。
覆寫入口
這裡我們新增
#![no_main]
attribute來告訴Rust編譯器我們不需要預設的入口鏈。
#![no_std]
#![no_main]
use
core
::
panic
::
PanicInfo
;
/// This function is called on panic。
#[panic_handler]
fn
panic
(
_info
:
&
PanicInfo
)
->
!
{
loop
{}
}
你可那已經注意到了我們移除了
main
函式,因為既然runtime不會再呼叫
main
了,它也就沒用了。作為替代,我們需要重寫作業系統的入口點。
入口點(entry point)的寫法通常和作業系統有關。這裡你最好先學習一下Linux的寫法即使你用的是別的作業系統因為接下來在我們的核心裡我們會採用這種寫法。
Linux
在Linux上,預設的入口點是
_start
。連結器(linker)會尋找帶有這個名字的函式,並將這個函式設定為可執行程式的入口點。所以,為了重寫我們的入口點,我們需要定義我們自己的
_start
程式:
#[no_mangle]
pub
extern
“C”
fn
_start
()
->
!
{
loop
{}
}
這裡有一個重要的點,我們透過
no_mangle
attribute來關閉 name mangling,否則編譯器會生成諸如
_ZN3blog_os4_start7hb173fedf945531caE
這樣編譯器識別不了的隱晦符號。這裡我們還需要把函式標記為
ertern C
來告訴編譯器為這個函式生成 C calling convention(C呼叫約定)。
!
返回型別表示這個函式是發散的,也就是說,它不允許返回。這是因為entry point不能被任何函式呼叫,而應該由作業系統或者bootloader直接呼叫。所以,entry point應該呼叫作業系統的 [
exit
system call]而不是程式返回。在我們的情況裡,對於這樣一個沒有任何事可以做的freestanding二進位制程式,關機或許是一個不錯的選擇。現在,我們可以新增一個無限迴圈來滿足永不返回的條件。
現在如果我們嘗試構建它,會出現下面這樣一段醜陋的錯誤:
error: linking with `cc` failed: exit code: 1
|
= note: “cc” “-Wl,——as-needed” “-Wl,-z,noexecstack” “-m64” “-L”
“/…/。rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib”
“/…/blog_os/target/debug/deps/blog_os-f7d4ca7f1e3c3a09。0。o” […]
“-o” “/…/blog_os/target/debug/deps/blog_os-f7d4ca7f1e3c3a09”
“-Wl,——gc-sections” “-pie” “-Wl,-z,relro,-z,now” “-nodefaultlibs”
“-L” “/…/blog_os/target/debug/deps”
“-L” “/…/。rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib”
“-Wl,-Bstatic”
“/…/。rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-dd5bba80e2402629。rlib”
“-Wl,-Bdynamic”
= note: /usr/lib/gcc/x86_64-linux-gnu/5/。。/。。/。。/x86_64-linux-gnu/Scrt1。o: In function `_start‘:
(。text+0x12): undefined reference to `__libc_csu_fini’
/usr/lib/gcc/x86_64-linux-gnu/5/。。/。。/。。/x86_64-linux-gnu/Scrt1。o: In function `_start‘:
(。text+0x19): undefined reference to `__libc_csu_init’
/usr/lib/gcc/x86_64-linux-gnu/5/。。/。。/。。/x86_64-linux-gnu/Scrt1。o: In function `_start‘:
(。text+0x25): undefined reference to `__libc_start_main’
collect2: error: ld returned 1 exit status
這個問題出現的原因是編譯器依賴於一些C標準庫
libc
的符號,因此它仍然會連結到C執行時的啟動環境,而我們在程式碼裡已經使用了
no_std
attribute來避免連結到標準庫。所以在這裡我們需要完全拋棄C啟動環境。我們可以透過把
-nostartfiles
flag傳遞連結器裡來實現這點。
透過
cargo runstc
這個命令可以實現透過cargo來傳遞連結屬性到編譯器。這個命令的表現和
cargo build
類似,不過它允許向Rust的底層編譯器
rustc
傳遞可選項。
rustc
有一個
-C link-arg
flag可以向連結器傳遞引數。將兩者稍作組合,我們的新構建命令如下:
> cargo rustc —— -C link-arg=-nostartfiles
現在我們成功從我們的crate中構建出了一個freestanding的可執行程式!
Windows
在Windows上,連結器需要兩個入口點 depending on the used subsystem。 對於
CONSOLE
子系統,我們需要一個叫
mainCRTStartup
的函式,它會呼叫
main
函式。就像在Linux上一樣,我們也需要定義
no_mangle
函式來覆寫入口點。
#[no_mangle]
pub
extern
“C”
fn
mainCRTStartup
()
->
!
{
main
();
}
#[no_mangle]
pub
extern
“C”
fn
main
()
->
!
{
loop
{}
}
macOS
macOS 不支援靜態連結到二進位制庫,所以我們需要連結到
libSystem
庫。它的入口點是
main‘
:
#[no_mangle]
pub
extern
“C”
fn
main
()
->
!
{
loop
{}
}
為了構建它並連結到
libSystem
,我們需要執行:
> cargo rustc —— -C link-arg=-lSystem
小結
一個獨立(freestanding)微型的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
;
/// This function is called on panic。
#[panic_handler]
fn
panic
(
_info
:
&
PanicInfo
)
->
!
{
loop
{}
}
入口點定義依賴於目標作業系統。Linux如下:
#[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
{}
}
Windows如下:
#[no_mangle]
pub
extern
“C”
fn
mainCRTStartup
()
->
!
{
main
();
}
#[no_mangle]
pub
extern
“C”
fn
main
()
->
!
{
loop
{}
}
macOS如下:
#[no_mangle]
pub
extern
“C”
fn
main
()
->
!
{
loop
{}
}
獨立於作業系統外的
Cargo。toml
如下:
[package]
name = “crate_name”
version = “0。1。0”
authors = [“Author Name
# 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
二進位制檔案可由以下命令編譯生成:
# Linux
> cargo rustc —— -C link-arg
=
-nostartfiles
# Windows
> cargo build
# macOS
> cargo rustc —— -C link-arg
=
-lSystem
注意,這僅僅是一個freestanding Rust二進位制程式的小例子。這樣一個二進位制程式執行還需要很多條件,比如在
_start
函式被呼叫時需要有一個初始化完成的棧。
所以為了真正執行這樣的一個二進位制程式,還有很多步驟需要做
下節預告
下一篇文章會講解如何在我們這個最小獨立二進位制程式的基礎上構建一個最小作業系統核心的步驟。下一篇還會講解如何配置目標作業系統的核心,如何使用bootloader,以及如何把一些內容輸出到螢幕上。
ps: 本系列文章已經有 @洛佳 同學翻譯了,我只是出於好玩才翻的,然後我在翻譯的時候並沒有完全直譯,部分是我個人的理解的內容,如有錯誤請及時告知我,或到
提issue,發起pr。