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-stdbuild-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()函式終止程式。

編譯和執行

要編譯和執行這個專案,你需要按照以下步驟進行:

  1. 下載fledgeos-2的原始碼。
  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);
    }
}

解釋

  1. 外部函式(extern function):使用 extern 關鍵字宣告的函式,可以從其他語言(如 C)呼叫。extern "C" 指定了函式的呼叫規範(calling convention)為 C 標準。
  2. 裸指標(raw pointer):Rust 中的裸指標(如 *mut u8)與 C 中的指標類別似,允許直接操作記憶體。但是,由於裸指標可能導致記憶體安全問題,因此需要在 unsafe 區塊中使用。
  3. 不安全區塊(unsafe block)unsafe 關鍵字用於標記可能存在安全風險的程式碼區塊。在這個區塊中,可以使用裸指標、呼叫外部函式等可能導致安全問題的操作。
  4. volatilewrite_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)]:這個屬性自動為列舉實作CloneCopy特徵,允許列舉值被複製。
  • #[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 語言的安全性、效能和底層控制能力,使其在系統程式設計領域擁有巨大的潛力,值得深入研究和應用。