Rust 在系統程式設計領域日漸成熟,本文將引導讀者使用 Rust 建構作業系統核心。首先,設定核心建置環境,包含建置目標與相依函式庫。接著,逐步實作核心程式碼,從入口點 _start()
函式開始,並設定 Panic 處理機制以應對程式錯誤。為了與硬體互動,我們將使用指標操作記憶體,並利用 VGA 顯示文字與色彩。文章提供程式碼範例與詳細解說,幫助讀者理解 Rust 在核心開發中的應用。
#![no_std]
#![no_main]
#![feature(core_intrinsics)]
#![feature(lang_items)]
use core::intrinsics;
use core::panic::PanicInfo;
use x86_64::instructions::hlt;
#[no_mangle]
pub extern "C" fn eh_personality() {}
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) -> ! {
unsafe {
intrinsics::abort();
}
}
#[no_mangle]
pub extern "C" fn _start() -> ! {
let framebuffer = 0xb8000 as *mut u8;
unsafe {
framebuffer.offset(1).write_volatile(0x30);
}
loop {
hlt();
}
}
11.2.1 核心核心設定
為了建立一個最基本的作業系統核心,我們需要進行一些初始設定。首先,我們定義了 build-std
和 build-std-features
,這些設定是用於指定核心的編譯和建置過程。
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
接著,我們指定了目標組態,特別是當目標作業系統為 “none” 時的設定。
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
11.2.2 核心程式碼實作
現在,我們可以開始實作核心的程式碼。首先,我們需要告知編譯器這個程式不需要標準函式庫,並且不使用傳統的 main
函式作為入口點。
#![no_std]
#![no_main]
我們還需要啟用核心的內在函式,以便使用特殊的指令。
#![feature(core_intrinsics)]
use core::intrinsics;
同時,我們需要定義一個處理器,用於處理程式在執行過程中可能發生的恐慌(panic)。
use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) ->! {
intrinsics::abort();
}
這個處理器會在程式發生恐慌時被呼叫,並且會終止程式的執行。
11.2.3 實作核心功能
現在,我們可以開始實作核心的功能。首先,我們需要初始化核心的環境,然後我們可以開始寫入預定的記憶體地址。
// 初始化核心環境
//...
// 寫入預定的記憶體地址
//...
這些步驟將在後面的章節中進行詳細的介紹。
Rust程式設計在作業系統環境下的變化
當我們設計Rust程式時,通常會依靠作業系統提供的功能和標準函式庫。但是在某些情況下,例如在嵌入式系統或是核心程式設計中,我們需要直接與硬體互動,而不需要作業系統的支援。這種情況下,Rust程式設計會有所不同。
1. 無傳回值的函式
在一般的Rust程式中,函式通常會傳回某個值。但是在作業系統環境下,某些函式可能不需要傳回值,因為它們不會被其他程式呼叫。這種情況下,我們可以使用!
型別來表示無傳回值的函式。
pub extern "C" fn _start() ->! {
//...
}
2. 關閉標準函式庫
在作業系統環境下,我們可能不需要標準函式庫提供的功能,例如動態記憶體組態。為了避免使用標準函式庫,我們可以使用#![no_std]
屬性來關閉標準函式庫。
#![no_std]
3. 使用核心內部函式
在某些情況下,我們需要使用核心內部函式來直接與硬體互動。這種函式不受Rust的穩定性保證,因此我們需要使用#![core_intrinsics]
屬性來解鎖核心內部函式。
#![core_intrinsics]
4. 關閉符號命名約定
在作業系統環境下,我們可能需要直接與硬體互動,因此需要關閉符號命名約定。這可以使用#![no_mangle]
屬性來實作。
#![no_mangle]
5. 處理panic
在作業系統環境下,panic可能會導致整個系統當機。因此,我們需要處理panic以避免這種情況。這可以使用abort()
函式來實作。
fn abort() {
//...
}
內容解密:
以上程式碼展示了Rust在作業系統環境下的變化。 #![no_std]
屬性關閉標準函式庫, #![core_intrinsics]
屬性解鎖核心內部函式, #![no_mangle]
屬性關閉符號命名約定。 _start()
函式是無傳回值的函式,使用!
型別來表示。 abort()
函式用於處理panic。
圖表翻譯:
flowchart TD A[開始] --> B[關閉標準函式庫] B --> C[解鎖核心內部函式] C --> D[關閉符號命名約定] D --> E[處理panic] E --> F[無傳回值的函式]
此圖表展示了Rust在作業系統環境下的變化流程。從關閉標準函式庫開始,然後解鎖核心內部函式,關閉符號命名約定,處理panic,最後是無傳回值的函式。
11.2.4 Panic 處理
Rust 不允許編譯沒有 panic 處理機制的程式。通常,Rust 會自行插入 panic 處理。但是,由於我們在程式開始時使用了 #[no_std]
,因此必須手動實作 panic 處理。以下程式碼片段(摘自 11.4)介紹了我們的 panic 處理功能。
#![no_std]
#![no_main]
#![feature(core_intrinsics)]
use core::intrinsics;
use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) ->! {
unsafe {
// 處理 panic 的實作
}
}
在這個範例中,我們定義了一個 panic
函式,該函式將被呼叫當發生 panic 時。這個函式的簽名為 pub fn panic(_info: &PanicInfo) ->!
,其中 _info
是一個 PanicInfo
的參照,提供了有關 panic 的資訊。!
表示該函式永不傳回。
關於作業系統開發的更多資訊
cargo bootimage
命令簡化了複雜的過程,提供了一個簡單的介面。但如果你是一個喜歡鑽研的人,你可能想要了解更多關於作業系統開發的細節。在這種情況下,你可以查閱以下資源:
- OSDev Wiki:一個關於作業系統開發的維基,包含了豐富的資訊和教程。
- Rust Embedded:一個 Rust 的嵌入式系統開發社群,提供了許多有用的資源和函式庫。
使用VGA相容文字模式寫入螢幕
在啟動時,bootloader會設定一些特殊的位元組,使得硬體切換到80x25的網格顯示模式,並設定一個固定記憶體緩衝區。這個緩衝區由玄貓解釋。
VGA相容文字模式的結構
在VGA相容文字模式中,螢幕被分成80x25的網格,每個網格代表一個字元。每個字元在記憶體中佔用一個位元組,包含了幾個欄位,包括:
is_blinking
: 是否閃爍background_color
: 背景顏色is_bright
: 是否亮顯character_color
: 字元顏色character
: 字元值
這些欄位都包含在一個單一的位元組中。
可用的字元
可用的字元是從Code Page 437編碼中繼承而來的,這是一種大致上是ASCII的擴充套件。
初始化和顯示
在啟動時,bootloader會初始化這個緩衝區,使得顯示東西變得容易。每個在80x25網格中的點都對應到記憶體中的位置。這個記憶體區域被稱為frame buffer。
我們的bootloader指定0xb8000
作為4,000位元組frame buffer的起始位置。要設定值,程式碼使用兩個新的方法:offset()
和write_volatile()
。
let mut framebuffer = 0xb8000 as *mut u8;
unsafe {
framebuffer.offset(1).write_volatile(0x41);
}
這段程式碼將frame buffer中的第二個位元組設為0x41
,也就是大寫字母"A"。
實作Panic Handler
除了使用intrinsics::abort()
外,我們還可以使用無窮迴圈作為panic handler。這種方法的缺點是任何程式錯誤都會觸發CPU核心以100%的速度執行,直到手動關閉。
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) ->! {
loop {}
}
PanicInfo
結構提供了有關panic發生位置的資訊,包括檔案名稱和行號。這些資訊在實作適當的panic handling時非常有用。
寫入螢幕
要寫入螢幕,我們需要使用VGA相容文字模式。首先,我們需要定義一個結構來代表每個字元:
struct VGACell {
is_blinking: u1,
background_color: u3,
is_bright: u1,
character_color: u3,
character: u8,
}
然後,我們可以使用這個結構來寫入螢幕:
let mut framebuffer = 0xb8000 as *mut u8;
unsafe {
framebuffer.offset(1).write_volatile(0x41);
}
這段程式碼將frame buffer中的第二個位元組設為0x41
,也就是大寫字母"A"。
11.2.5 使用指標進行記憶體存取
在 Rust 中,指標(pointer)是一種用於存取記憶體位置的型別。指標可以用於讀寫記憶體位置的內容。
移動指標
指標的 offset()
方法可以用於移動指標到新的記憶體位置。移動的步長是指標型別的大小。例如,對於一個 *mut u8
指標,呼叫 offset(1)
會將指標移動到下一個位元組的位置。
let mut ptr: *mut u8 = 0x1000 as *mut u8;
ptr = ptr.offset(1); // 移動到下一個位元組
對於一個 *mut u32
指標,呼叫 offset(1)
會將指標移動到下一個 4 個位元組的位置。
強制寫入記憶體
指標的 write_volatile()
方法可以用於強制寫入記憶體位置的內容。這個方法可以防止編譯器的最佳化器將寫入操作最佳化掉。
let mut ptr: *mut u8 = 0x1000 as *mut u8;
ptr.write_volatile(0x30); // 強制寫入 0x30 到記憶體位置
11.2.6 _start()
函式
作業系統核心不需要 main()
函式,因為它不需要傳回任何值。相反,作業系統核心使用 _start()
函式作為入口點。這個函式由連結器定義,並且需要由開發者實作。
在普通環境中, _start()
函式有三個任務:重置系統、呼叫 main()
函式和呼叫 _exit()
函式。但是在 FledgeOS 中, _start()
函式只需要實作簡單的應用功能,因此不需要呼叫 main()
函式和 _exit()
函式。
11.3 避免忙等待
FledgeOS 需要解決一個主要問題:它非常耗電。 _start()
函式目前執行在 CPU 核心上,導致 CPU 核心一直執行在 100% 的負載下。為了避免這個問題,可以使用 hlt
指令通知 CPU 沒有更多工作需要做。CPU 會在收到中斷時還原執行。
loop {
unsafe {
asm!("hlt");
}
}
這個迴圈會一直執行,直到收到中斷時還原執行。這樣可以避免忙等待,減少 CPU 的能耗。
核心驅動程式與中斷觸發
在核心驅動程式的設計中,中斷觸發是一個非常重要的機制。它允許系統在特定事件發生時執行特定的程式碼,以便能夠處理這些事件。以下是使用 x86_64
函式庫來存取 hlt
指令的範例程式碼:
use x86_64::instructions::{hlt};
#[no_mangle]
pub extern "C" fn _start() ->! {
let mut framebuffer = 0xb8000 as *mut u8;
unsafe {
framebuffer.offset(1).write_volatile(0x30);
}
loop {
hlt();
}
}
在這個範例中,hlt
指令被用於暫停 CPU 的執行,從而避免了不必要的功耗。這是一種常見的做法,因為如果 CPU 沒有任何工作要做,它就會不斷地執行無窮迴圈,導致系統變得非常熱。
使用 hlt
指令的優點
使用 hlt
指令有幾個優點:
- 降低功耗:透過暫停 CPU 的執行,系統可以避免不必要的功耗,從而降低能耗和熱量產生。
- 提高效率:當系統沒有任何工作要做時,使用
hlt
指令可以讓 CPU 進入休眠狀態,從而提高系統的整體效率。
替代方案
如果不使用 hlt
指令,CPU 就會不斷地執行無窮迴圈,導致系統變得非常熱。這種情況下,系統就像一個非常昂貴的加熱器一樣,浪費了大量的能量。
圖表翻譯:
flowchart TD A[系統啟動] --> B[檢查事件] B --> C{有事件發生} C -->|是| D[執行事件處理程式] C -->|否| E[執行 hlt 指令] E --> F[暫停 CPU 執行] F --> B
在這個流程圖中,當系統啟動後,會不斷地檢查是否有事件發生。如果有事件發生,就會執行事件處理程式;否則,就會執行 hlt
指令,暫停 CPU 的執行。
自訂例外處理:fledgeos-2
在前面的章節中,我們已經建立了基本的作業系統框架,包括啟動程式和基本的例外處理機制。然而,目前的例外處理機制仍然相當簡單,當發生異常時,它只是使用 hlt
指令讓 CPU 暫停執行。這種方法並不夠完善,因為它無法提供有用的錯誤資訊,也不能讓我們有機會去修復錯誤或還原系統。
fledgeos-2 專案
為了改善例外處理機制,我們將建立一個新的專案,稱為 fledgeos-2。這個專案根據 fledgeos-1,但增加了自訂例外處理功能。首先,讓我們來看看 fledgeos-2 的 src/main.rs
檔案。
#![no_std]
#![no_main]
use core::intrinsics;
use core::panic::PanicInfo;
use x86_64::instructions::{hlt};
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) ->! {
unsafe {
intrinsics::abort();
}
}
// 新增的例外處理函式
#[no_mangle]
pub extern "C" fn exception_handler() {
// 例外處理邏輯
loop {
hlt();
}
}
在這個版本中,我們新增了一個 exception_handler
函式,這個函式將被用於處理異常。當異常發生時,CPU 會跳轉到這個函式,並執行其中的指令。在這個例子中,函式內只有一個無窮迴圈,使用 hlt
指令讓 CPU 暫停執行。
編譯和執行
要編譯和執行 fledgeos-2 專案,你可以按照與前面相同的步驟進行。首先,確保你已經切換到正確的目錄,然後執行以下命令:
cargo build --release
接著,你可以使用 QEMU 或其他模擬器來執行你的作業系統。
qemu-system-x86_64 -kernel target/release/fledgeos-2
這樣,你就可以看到你的作業系統在執行,並且當異常發生時,它會跳轉到自訂的例外處理函式。
自訂例外處理:FledgeOS-2
FledgeOS 的下一個版本旨在改善其錯誤處理能力。雖然 FledgeOS 仍然會在發生錯誤時當機,但我們現在有了一個框架來建構更複雜的錯誤處理機制。
基本例外處理
FledgeOS 無法管理 CPU 在偵測到異常操作時產生的異常。為了處理異常,我們的程式需要定義一個例外處理個性函式(exception-handling personality function)。這個函式會在堆積疊被反向遍歷時,在每個堆積疊框架中被呼叫,以確定當前的堆積疊框架是否能夠處理異常。
堆積疊反向遍歷(Stack Unwinding)
當函式被呼叫時,堆積疊框架會累積。反向遍歷堆積疊被稱為堆積疊反向遍歷(stack unwinding)。最終,堆積疊反向遍歷會到達 _start()
函式。
由於嚴格的例外處理對於 FledgeOS 來說不是必要的,因此我們只會實作最基本的功能。以下是最小化的處理器程式碼片段:
#![feature(lang_items)]
#[lang = "eh_personality"]
fn eh_personality() {
// 空函式,表示任何異常都是致命的
}
這個空函式意味著任何異常都會被視為致命的,因為沒有任何一個會被標記為處理器。當異常發生時,我們不需要做任何事情。
_start() 函式
下面是 _start()
函式的實作:
pub extern "C" fn _start() ->! {
let mut framebuffer = 0xb8000 as *mut u8;
unsafe {
framebuffer.offset(1).write_volatile(0x30);
}
loop {
hlt();
}
}
這個函式設定了 framebuffer 並進入無窮迴圈。
使用內嵌組合語言
x86_64 函式庫提供了將組合語言指令注入我們程式碼的能力。另一個方法是使用內嵌組合語言(inline assembly)。後者在第 12.3 節中有簡要介紹。
內嵌組合語言範例
以下是使用內嵌組合語言的範例:
asm!("
mov eax, 0x30
mov [framebuffer], al
");
這個範例使用內嵌組合語言將 0x30
的值移動到 framebuffer
的位置。
Rust核心開發:實作自定義語言專案
在Rust開發中,語言專案(language items)是指實作Rust語言功能的函式庫或模組。當我們使用#[no_std]
屬性時,我們需要自己實作一些標準函式庫的功能。下面,我們將實作一個最基本的例外處理例程。
最基本的例外處理例程
首先,我們需要定義一個例外處理函式eh_personality
,這個函式是用於處理異常的。下面是它的實作:
#[no_mangle]
pub extern "C" fn eh_personality() {}
這個函式目前沒有任何實際功能,但它是必要的,因為Rust編譯器需要它來處理異常。
fledgeos-2原始碼
接下來,我們將實作fledgeos-2的原始碼。這個專案根據fledgeos-0和fledgeos-1,並增加了一些新的功能。下面是src/main.rs
檔案的內容:
#![no_std]
#![no_main]
#![feature(core_intrinsics)]
#![feature(lang_items)]
use core::intrinsics;
use core::panic::PanicInfo;
use x86_64::instructions::{hlt};
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) ->! {
unsafe {
intrinsics::abort();
}
}
這個程式碼包括了以下幾個部分:
#![no_std]
和#![no_main]
屬性,分別用於停用標準函式庫和主函式。#![feature(core_intrinsics)]
和#![feature(lang_items)]
屬性,分別用於啟用核心內在函式和語言專案功能。use
陳述式,用於匯入必要的模組和函式。panic
函式,用於處理異常。這個函式使用intrinsics::abort()
函式終止程式。
編譯和執行
要編譯和執行這個專案,你需要按照以下步驟進行:
- 下載fledgeos-2的原始碼。
- 使用以下命令編譯專案:
cargo build –release
3. 執行以下命令啟動專案: ```bash cargo run --release
這樣,你就可以成功編譯和執行fledgeos-2專案了。
Rust 程式設計:使用外部函式與裸指標
在 Rust 中,當我們需要與 C 程式碼互動或直接操作記憶體時,會使用外部函式(extern function)和裸指標(raw pointer)。下面是一個範例程式碼,展示瞭如何使用這些功能。
範例程式碼
// 標記這個函式不會被 mangle,以便可以從 C 程式碼中呼叫
#[no_mangle]
// 宣告一個外部函式,名稱為 eh_personality
pub extern "C" fn eh_personality() {}
// 標記這個函式不會被 mangle,以便可以從 C 程式碼中呼叫
#[no_mangle]
// 宣告一個外部函式,名稱為 _start,傳回值為!
pub extern "C" fn _start() ->! {
// 定義一個裸指標,指向記憶體位置 0xb8000
let framebuffer = 0xb8000 as *mut u8;
// 進入不安全區塊(unsafe block),因為我們正在直接操作記憶體
unsafe {
// 使用 offset 方法將指標偏移 1 個單位
framebuffer.offset(1)
// 將值 0x30 寫入偏移後的記憶體位置
.write_volatile(0x30);
}
}
解釋
- 外部函式(extern function):使用
extern
關鍵字宣告的函式,可以從其他語言(如 C)呼叫。extern "C"
指定了函式的呼叫規範(calling convention)為 C 標準。 - 裸指標(raw pointer):Rust 中的裸指標(如
*mut u8
)與 C 中的指標類別似,允許直接操作記憶體。但是,由於裸指標可能導致記憶體安全問題,因此需要在unsafe
區塊中使用。 - 不安全區塊(unsafe block):
unsafe
關鍵字用於標記可能存在安全風險的程式碼區塊。在這個區塊中,可以使用裸指標、呼叫外部函式等可能導致安全問題的操作。 - volatile:
write_volatile
方法確保對記憶體的寫入操作是立即生效的,而不會被編譯器或 CPU 最佳化掉。
使用注意事項
- 外部函式和裸指標應盡可能避免使用,因為它們可能導致記憶體安全問題和跨平臺相容性問題。
- 在使用外部函式和裸指標時,應該小心評估潛在風險,並確保程式碼的安全性和正確性。
- Rust 的 borrow checker 和所有權系統是設計用來防止常見的錯誤,如空指標除法和資料競爭。盡可能使用 Rust 的安全功能來寫出更可靠的程式碼。
文字輸出與彩色文字
在我們的作業系統中,實作文字輸出是非常重要的。這不僅可以幫助我們在遇到問題時正確地報告錯誤資訊,也可以讓使用者與系統進行互動。這一節,我們將探討如何向螢幕輸出文字,並且如何實作彩色文字的輸出。
寫入彩色文字
首先,我們需要定義一個列舉(enum)來代表VGA相容的文字模式色彩調色盤。這個列舉將包含各種色彩的數值常數,並且為了增強型別安全性,我們選擇使用列舉而不是一系列的const值。這樣做可以為這些值新增語義關係,使得它們被視為同一組的成員。
#[allow(unused)]
#[derive(Clone, Copy)]
#[repr(u8)]
enum Color {
Black = 0x0, White = 0xF,
Blue = 0x1, BrightBlue = 0x9,
Green = 0x2, BrightGreen = 0xA,
Cyan = 0x3, BrightCyan = 0xB,
}
內容解密:
#[allow(unused)]
:這個屬性用於告訴編譯器,即使某些程式碼沒有被使用,也不要產生警告。#[derive(Clone, Copy)]
:這個屬性自動為列舉實作Clone
和Copy
特徵,允許列舉值被複製。#[repr(u8)]
:這個屬性指定列舉應該被表示為一個無符號8位元整數(u8),這對於我們的色彩編碼是合適的。enum Color {... }
:定義了一個名為Color
的列舉,包含不同的色彩選項,每個選項都有一個對應的u8值。
實作文字輸出
要實作文字輸出,我們需要與螢幕的框架緩衝區(frame buffer)進行互動。螢幕上的每個字元都對應著框架緩衝區中的特定位置,我們需要計算出字元在螢幕上的正確位置,並將字元的ASCII碼和顏色資訊寫入到框架緩衝區中。
flowchart TD A[開始] --> B[計算字元位置] B --> C[取得字元ASCII碼和顏色] C --> D[寫入框架緩衝區] D --> E[更新螢幕顯示]
圖表翻譯:
- 這個流程圖描述了將文字輸出到螢幕的過程。
- 首先,計算字元在螢幕上的位置,這取決於字元的行列座標和螢幕的解析度。
- 然後,取得字元的ASCII碼和顏色資訊,這些資訊決定了字元如何在螢幕上顯示。
- 接下來,將這些資訊寫入到框架緩衝區中,框架緩衝區是記憶體中的一塊區域,用於儲存螢幕上每個畫素的顏色資訊。
- 最後,更新螢幕顯示,使得新寫入的字元可見。
從系統底層架構的構建視角來看,本文深入淺出地介紹了使用 Rust 語言開發作業系統核心的基本流程,涵蓋了核心設定、程式碼實作、恐慌處理、記憶體存取、以及與硬體互動等關鍵環節。分析核心程式碼的實作細節,可以發現 #![no_std]
、#![no_main]
等屬性以及 _start()
函式的運用,跳脫了傳統應用程式開發的框架,展現了系統程式設計的獨特之處。技術的限制在於需要對硬體和底層機制有深入的理解,例如直接操作記憶體和使用裸指標等,都存在一定的風險。對於開發者而言,需要謹慎處理這些底層操作,並充分利用 Rust 的安全機制來降低風險。展望未來,隨著 Rust 在嵌入式系統和作業系統開發領域的應用日益增多,相關工具鏈和生態系統也將更加完善,進而降低開發門檻,提升開發效率。玄貓認為,Rust 語言的安全性、效能和底層控制能力,使其在系統程式設計領域擁有巨大的潛力,值得深入研究和應用。