本系列博客系轉載,出處: 知乎專欄:從零開始寫 OS
所有代碼都在:https://github.com/LearningOS/rcore_step_by_step
本小節代碼對應 commit :bc429d04d479da60fc4bd05824a124d18c5e33ac
概要
由於我們的目標是編寫一個操作系統,所以首先我們需要創建一個獨立於操作系統的可執行程序,又稱 獨立式可執行程序freestanding executable
或 裸機程序bare-metal executable
。這意味着所有依賴於操作系統的庫我們都不能使用。比如 std 中的大部分內容io, thread, file system, etc.
都需要操作系統的支持,所以這部分內容我們不能使用。
但是,不依賴與操作系統的 rust 的語言特性 我們還是可以繼續使用的,比如:迭代器、模式匹配、字符串格式化、所有權系統等。這使得 rust 依舊可以作爲一個功能強大的高級語言,幫助我們編寫操作系統。
本小節我們將介紹:
1. 安裝 rust(nightly 版本) 。
2. 創建可執行的 rust 項目。
3. 將創建的 rust 項目修改爲freestanding rust binary,這包括禁用std庫並解決由此產生的一系列問題。
安裝 nightly rust
rust 包含stable、beta、nightly
三個版本,分別對應穩定、試驗和預覽版本。默認情況下我們安裝的是stable
。由於在編寫操作系統時需要使用 rust 的一些不穩定的實驗功能,所以請根據 rust 官方教程安rust nightly
版本 。
安裝成功後使用 rustc --version
可以查看當前 rust 的版本,版本號的最後應該爲-nightly
。
如果未能成功切換 rust 版本,請查看 how to switch rust toolchain
創建 rust binary 項目
首先利用 cargo 創建一個新的 rust binary
項目:
cargo new xy_os --bin --edition 2018
xy_os
是項目的名稱,--bin
表示我需要創建一個 binary
項目,--edition 2018
指定使用 2018 版本的 rust 。
添加 no_std 屬性
因爲我們的目標是編寫一個操作系統,所以我們不能使用任何依賴於操作系統的庫。項目默認是鏈接標準庫的,我們需要顯示的將其禁用:
// main.rs
#![no_std]
fn main() {
println!("Hello, world!");
}
如果此時執行 cargo build
構建項目,會產生以下兩個錯誤:
error: cannot find macro `println!` in this scope
--> src/main.rs:6:5
|
6 | println!("Hello, world!");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
現在我們來依次解決這兩個問題。
error: cannot find macro 'println!' in this scope
println
宏屬於標準庫,所以禁用標準庫後自然不能再使用 println
函數 。由於我們當前目標只是寫一個可執行的文件,所以將其刪除即可:
// in main.rs
#![no_std]
fn main() {}
error: '#[panic_handler]' function required, but not found
在程序發生 panic
時需要調用相應函數。標準庫有對應函數,但是由於我們使用了no_std
屬性,所以接下來我們需要自己實現一個函數:
// in main.rs
use core::panic::PanicInfo;
// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
由於程序 panic
後就應該結束,所以用-> !
表示該函數不會返回。由於目前的 OS 功能還很弱小,所以只能無限循環。
解決了上述兩個 error 後,再次執行 cargo build
,結果出現了新的 error:
error: language item required, but not found: `eh_personality`
error: language item required, but not found: 'eh_personality'
eh_personality
語義項(language item)
用於標記函數:該函數在 堆棧展開(stack unwinding)
時被調用。當程序發生 panic
時,rust
會調用 堆棧展開 析構堆棧中的所有生存變量,達到釋放內存的目的。但是這是一個複雜的過程,而且依賴於一些其他的庫文件。所以我們只是簡單的將其禁用:
# Cargo.toml
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
將 dev (use for cargo build)
和 release (use for cargo build --release)
的 panic
的處理策略設爲abort
,這樣我們便不再依賴 eh_personality
語義項。
再次運行 cargo build
,不出所料,又出現了新的 error :
error: requires `start` lang_item
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
入口點:
#![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 {}
}
#[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 {}
}
適用於 Linux ,在其他系統請 點擊這裏 。暫時無法編譯也沒關係,因爲下一小節會重寫 _start
函數。
這裏 pub extern "C" fn main
就是我們需要的start
。 #[no_mangle]
屬性用於防止改名稱被混淆。由於 start
只能由操作系統或引導加載程序直接調用,不會被其他函數調用,所以不能夠返回。如果需要離開該函數,應該使用 exit 系統調用。但是由於我們的操作系統還沒有實現 exit 系統調用,所以暫時使用無限循環防止函數返回。由於 start 函數無法返回或退出,自然也就不會調用 main 。所以將 main 函數刪除,並且增加屬性標籤 #![no_main]
。
再次執行 cargo build
,很不幸,又出現了 error:
linking with `cc` failed: exit code: 1
但幸運的是,這是我們本章所需要處理的最後一個 error!
linking with 'cc' failed: exit code: 1
在鏈接 C runtime 時,會需要一些 C 標準庫(libc)的內容。由於 #![no_std] 禁用了標準庫,所以我們需要禁用常規的 C 啓動例程:
cargo rustc – -C link-arg=-nostartfiles
Compiling xy_os v0.1.0 (/mnt/xy_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.21s
適用於 Linux ,在其他系統請 點擊這裏
歷經千辛萬苦,我們終於成功構建了一個 Freestanding Rust Binary
!!!
此處應有掌聲
預告
下一章,我們將在 Freestanding Rust Binary
的基礎上,創建 最小內核 ,將其和 bootloader
鏈接成爲可以被 qemu
加載的 bootimage
。並且將能夠在屏幕上打印Hello World
!