在作業系統的核心開發中,控制螢幕輸出是不可或缺的一環。本文將以 Rust 語言為例,示範如何利用 VGA 相容的文字模式色彩表和螢幕緩衝區實作文字和色彩的顯示。首先,我們會利用 repr(u8) 屬性定義色彩列舉型別 Color,確保其與 VGA 規範相容。接著,我們將設計 Cursor 結構體,用於管理螢幕上的遊標位置、前景色彩和背景色彩。為了將文字列印到螢幕,我們需要操作記憶體地址 0xb8000 處的螢幕緩衝區,並使用 write_volatile 函式確保資料寫入的即時性。最後,我們將探討如何自訂 Panic 處理器,以便在程式發生錯誤時,能以更友好的方式向使用者顯示錯誤訊息,並結合 core::fmt::Write trait 實作格式化的錯誤輸出。透過這些技術,我們可以打造更健全、更易於除錯的核心繫統。
11.5 列舉型別(Enums)與 VGA 相容的文字模式色彩表
在 Rust 中,列舉型別(Enums)是一種強大的工具,能夠讓我們定義一組具名的值。當我們需要控制列舉型別在記憶體中的表示時,可以使用 repr 屬性來指定特定的整數型別,例如 u8、i16 等。
11.5.2 控制列舉型別在記憶體中的表示
在某些情況下,我們需要控制列舉型別在記憶體中的表示,以滿足外部系統的需求。例如,VGA 相容的文字模式色彩表需要使用單個 byte 來表示色彩值。以下是如何使用 repr 屬性來指定色彩列舉型別的表示:
#[repr(u8)]
enum Color {
Red = 0x4,
BrightRed = 0xC,
Magenta = 0x5,
BrightMagenta = 0xD,
Brown = 0x6,
Yellow = 0xE,
Gray = 0x7,
DarkGray = 0x8,
}
使用 repr 屬性來指定列舉型別的表示有一些缺點,例如減少了編譯器的彈性,並且可能會阻止 Rust 進行空間最佳化。
11.5.3 為什麼使用列舉型別?
我們可以使用其他方法來表示色彩,例如使用數值常數:
const BLACK: u8 = 0x0;
const BLUE: u8 = 0x1;
但是,使用列舉型別可以提供額外的安全性,避免非法值的使用。
11.5.4 建立一個可以列印到 VGA 框架緩衝區的型別
要列印到螢幕上,我們需要建立一個 Cursor 型別,它可以處理原始記憶體操作,並且可以在我們的 Color 型別和 VGA 框架緩衝區預期的值之間進行轉換。以下是 Cursor 型別的定義和方法:
struct Cursor {
position: isize,
foreground: Color,
background: Color,
}
impl Cursor {
fn color(&self) -> u8 {
//...
}
}
這個 Cursor 型別將負責管理我們的程式碼和 VGA 框架緩衝區之間的介面。
顯示核心:列印文字到螢幕
在顯示核心中,列印文字到螢幕是一個基本的功能。這涉及到將文字字元和其對應的顏色資訊寫入到螢幕的緩衝區中。下面是相關的程式碼片段:
顏色編碼
let fg = self.foreground as u8;
let bg = (self.background as u8) << 4;
let color = fg | bg;
這段程式碼計算了字元的顏色編碼。fg代表前景色(字元的顏色),bg代表背景色。透過將背景色左移4位然後與前景色進行位元OR操作,得到最終的顏色編碼。
列印文字
fn print(&mut self, text: &[u8]) {
let framebuffer = 0xb8000 as *mut u8;
for &character in text {
unsafe {
framebuffer.offset(self.position).write_volatile(character);
framebuffer.offset(self.position + 1).write_volatile(color);
}
self.position += 2;
}
}
這個print函式負責將輸入的文字列印到螢幕上。它首先取得螢幕的緩衝區地址(0xb8000),然後遍歷每個字元。對於每個字元,它將字元和其對應的顏色資訊寫入到螢幕的緩衝區中。寫入操作使用了write_volatile方法,以確保對緩衝區的修改立即生效。最後,更新了列印位置指標self.position,準備列印下一個字元。
圖表翻譯:
flowchart TD
A[開始] --> B[取得螢幕緩衝區地址]
B --> C[遍歷每個字元]
C --> D[寫入字元和顏色資訊]
D --> E[更新列印位置指標]
E --> F[結束]
這個流程圖描述了列印文字到螢幕的步驟。從取得螢幕緩衝區地址開始,遍歷每個字元,寫入字元和顏色資訊,更新列印位置指標,直到結束。
輸出到螢幕
在螢幕上顯示文字需要使用 Cursor 來設定其位置,然後傳遞一個參考給 Cursor.print()。以下的程式碼片段是從清單 11.16 中提取出來的,擴充套件了 _start() 函式以輸出到螢幕。
#[no_mangle]
pub extern "C" fn _start() ->! {
let text = b"Rust in Action";
let mut cursor = Cursor {
position: 0,
foreground: Color::BrightCyan,
background: Color::Black,
};
cursor.print(text);
loop {
hlt();
}
}
fledgeos-3 的原始碼
fledgeos-3 繼續在 fledgeos-0、fledgeos-1 和 fledgeos-2 的基礎上進行開發。其 src/main.rs 檔案包含了本文中新增的內容。完整的檔案如下所示,並可在 code/ch11/ch11-fledgeos-3/src/main.rs 下載。要編譯這個專案,請重複 11.2.1 節中的指令,並將所有對 fledgeos-0 的參照替換為 fledgeos-3。
清單 11.15:示範螢幕輸出
使用前景顏色作為基礎,佔據較低的 4 位元。將背景顏色左移以佔據較高的位元,然後合併這些一起。 為了方便,輸入使用原始的 byte 流,而不是結構化的資料。
Mermaid 圖表:螢幕輸出的流程
flowchart TD
A[設定 Cursor 位置] --> B[傳遞參考給 Cursor.print()]
B --> C[輸出到螢幕]
C --> D[迴圈等待]
圖表翻譯:
此流程圖描述瞭如何在螢幕上顯示文字的步驟。首先,設定 Cursor 的位置,然後傳遞一個參考給 Cursor.print() 以輸出指定的文字到螢幕上。最後,程式進入迴圈等待狀態。
內容解密:
在這個範例中,我們定義了一個 _start() 函式,該函式是程式的入口點。首先,我們定義了一個 byte 陣列 text,包含了要輸出的文字。然後,我們建立了一個 Cursor 例項,並設定其位置、前景顏色和背景顏色。接下來,我們呼叫 cursor.print(text) 將文字輸出到螢幕上。最後,程式進入一個無限迴圈,使用 hlt() 函式暫停執行。
核心核心開發:正確編碼的保證
在開發作業系統核心時,正確的編碼和資料表示是非常重要的。為了保證正確的編碼,我們需要使用適當的資料型別和編碼方式。在這個章節中,我們將探討如何在 Rust 中使用核心函式庫和內建函式來實作正確的編碼。
核心函式庫和內建函式
Rust 的核心函式庫提供了許多內建函式和特性,可以幫助我們實作正確的編碼。例如,core::intrinsics 模組提供了許多內建函式,可以用於實作低階別的操作。
// 啟用核心函式庫和內建函式
#![feature(core_intrinsics)]
#![feature(lang_items)]
// 停用標準函式庫和主函式
#![no_std]
#![no_main]
// 匯入核心函式庫和內建函式
use core::intrinsics;
use core::panic::PanicInfo;
use x86_64::instructions::{hlt};
色彩編碼
在圖形顯示中,色彩編碼是一個非常重要的方面。為了保證正確的色彩編碼,我們需要使用適當的列舉型別和編碼方式。以下是一個簡單的色彩列舉型別的例子:
// 定義色彩列舉型別
#[allow(unused)]
#[derive(Clone, Copy)]
#[repr(u8)]
enum Color {
Black = 0x0,
White = 0xF,
Blue = 0x1,
BrightBlue = 0x9,
Green = 0x2,
BrightGreen = 0xA,
}
內容解密:
在上面的例子中,我們定義了一個色彩列舉型別 Color,它包含了六個不同的色彩值。每個色彩值都對應著一個特定的編碼值。這種列舉型別可以用於圖形顯示中,保證正確的色彩編碼。
圖表翻譯:
以下是色彩列舉型別的 Mermaid 圖表:
classDiagram
Color <|-- Black
Color <|-- White
Color <|-- Blue
Color <|-- BrightBlue
Color <|-- Green
Color <|-- BrightGreen
class Color {
+ Black: u8 = 0x0
+ White: u8 = 0xF
+ Blue: u8 = 0x1
+ BrightBlue: u8 = 0x9
+ Green: u8 = 0x2
+ BrightGreen: u8 = 0xA
}
這個圖表顯示了色彩列舉型別的結構和編碼值。它可以幫助我們理解色彩列舉型別的內部結構和編碼方式。
顏色與遊標結構
在終端機或命令列介面中,顏色和遊標的呈現對於使用者經驗至關重要。為了實作這些功能,我們需要定義顏色和遊標的結構。
顏色定義
首先,我們定義了一組顏色常數,包括基本顏色和亮色版本。這些顏色被編碼為十六進位制值,以便於在程式中使用。
enum Color {
Black = 0x0,
Red = 0x1,
Green = 0x2,
Yellow = 0x3,
Blue = 0x4,
Magenta = 0x5,
Cyan = 0x6,
White = 0x7,
BrightBlack = 0x8,
BrightRed = 0x9,
BrightGreen = 0xA,
BrightYellow = 0xB,
BrightBlue = 0xC,
BrightMagenta = 0xD,
BrightCyan = 0xE,
BrightWhite = 0xF,
}
遊標結構
接下來,我們定義了遊標的結構,包括其位置、前景顏色和背景顏色。這些屬性對於控制遊標在終端機中的顯示位置和顏色至關重要。
struct Cursor {
position: isize,
foreground: Color,
background: Color,
}
顏色編碼
為了方便地在程式中使用顏色,我們實作了一個方法,可以將前景顏色和背景顏色編碼為一個單一的位元組。這個位元組的低四位用於表示前景顏色,而高四位則用於表示背景顏色。
impl Cursor {
fn color(&self) -> u8 {
let fg = self.foreground as u8;
let bg = (self.background as u8) << 4;
fg | bg
}
}
內容解密:
在上述程式碼中,我們定義了 Cursor 結構體和 color 方法。Cursor 結構體包含三個屬性:position、foreground 和 background,分別代表遊標的位置、前景顏色和背景顏色。color 方法將前景顏色和背景顏色編碼為一個單一的位元組,以便於在程式中使用。
圖表翻譯:
classDiagram
class Cursor {
- position: isize
- foreground: Color
- background: Color
+ color(): u8
}
class Color {
+ Black = 0x0
+ Red = 0x1
+ Green = 0x2
+ Yellow = 0x3
+ Blue = 0x4
+ Magenta = 0x5
+ Cyan = 0x6
+ White = 0x7
+ BrightBlack = 0x8
+ BrightRed = 0x9
+ BrightGreen = 0xA
+ BrightYellow = 0xB
+ BrightBlue = 0xC
+ BrightMagenta = 0xD
+ BrightCyan = 0xE
+ BrightWhite = 0xF
}
此圖表顯示了 Cursor 和 Color 的類別關係,以及其屬性和方法。它清晰地展示瞭如何將前景顏色和背景顏色編碼為一個單一的位元組,以便於在程式中使用。
顯示核心:在 FledgeOS 中實作文字輸出
在上一節中,我們探討瞭如何構建一個基本的作業系統。現在,我們將深入研究如何實作文字輸出功能。這是任何作業系統的核心部分,因為它允許使用者與系統進行互動。
實作 print 方法
為了實作文字輸出,我們需要定義一個 print 方法。這個方法將負責將輸入的文字寫入螢幕。以下是 print 方法的實作:
fn print(&mut self, text: &[u8]) {
let framebuffer = 0xb8000 as *mut u8;
for &character in text {
unsafe {
framebuffer.offset(self.position).write_volatile(character);
framebuffer.offset(self.position + 1).write_volatile(color);
}
self.position += 2;
}
}
在這個實作中,我們首先定義了一個指向螢幕緩衝區(framebuffer)的指標 framebuffer。螢幕緩衝區是一塊記憶體,儲存著螢幕上的所有畫素。
接下來,我們使用一個迴圈遍歷輸入的文字,並將每個字元寫入螢幕緩衝區。為了確保資料的正確性,我們使用 write_volatile 方法來寫入資料。
最後,我們更新 position 變數,以便在下一次呼叫 print 方法時,從正確的位置開始寫入。
處理恐慌
在 Rust 中,當發生恐慌(panic)時,程式會終止執行。為了處理這種情況,我們需要定義一個恐慌處理器(panic handler)。以下是 FledgeOS 中的恐慌處理器:
#[panic_handler]
fn panic_handler(info: &PanicInfo) ->! {
// 處理恐慌
}
這個恐慌處理器會在發生恐慌時被呼叫,並提供有關恐慌的資訊。
內容解密:
print方法:負責將輸入的文字寫入螢幕。framebuffer:指向螢幕緩衝區的指標。write_volatile方法:用於寫入資料,以確保資料的正確性。position變數:用於追蹤目前的寫入位置。- 恐慌處理器(panic handler):用於處理發生恐慌時的情況。
圖表翻譯:
graph LR
A[文字輸入] -->|傳遞給 print 方法|> B[print 方法]
B -->|寫入螢幕緩衝區|> C[螢幕緩衝區]
C -->|更新 position 變數|> D[position 變數]
D -->|準備下一次寫入|> B
這個圖表展示了文字輸出的流程,從文字輸入到寫入螢幕緩衝區,再到更新 position 變數。
自訂 Panic 處理與 Rust 入口點
在 Rust 中,當程式發生 panic 時,預設會呼叫 std::panic::hook 函式來處理。然而,在某些情況下,我們可能需要自訂 panic 處理邏輯。下面我們將探討如何實作自訂 panic 處理和 Rust 入口點的設定。
自訂 Panic 處理
Rust 提供了 std::panic::set_hook 函式來設定自訂 panic 處理 hook。以下是設定自訂 panic 處理的範例:
use std::panic;
// 自訂 panic 處理函式
fn custom_panic_handler(info: &panic::PanicInfo) ->! {
// 在這裡實作自訂的 panic 處理邏輯
println!("發生 panic:{}", info);
std::process::abort();
}
fn main() {
// 設定自訂 panic 處理 hook
panic::set_hook(Box::new(custom_panic_handler));
// 測試 panic
panic!("測試 panic");
}
在上面的範例中,我們定義了一個 custom_panic_handler 函式來處理 panic。然後,在 main 函式中,我們使用 panic::set_hook 函式來設定自訂 panic 處理 hook。
Rust 入口點
Rust 的入口點是 _start 函式,這個函式是由 linker 指定的。以下是 _start 函式的範例:
#[no_mangle]
pub extern "C" fn _start() ->! {
// 在這裡實作 Rust 程式的入口點邏輯
let text = b"Rust in Action";
println!("{}", text);
loop {}
}
在上面的範例中,我們定義了一個 _start 函式,這個函式是 Rust 程式的入口點。注意,這個函式的傳回型別是 !,表示這個函式永不傳回。
結合自訂 Panic 處理和 Rust 入口點
現在,我們可以結合自訂 panic 處理和 Rust 入口點來實作一個完整的 Rust 程式。以下是完整的範例:
use std::panic;
// 自訂 panic 處理函式
fn custom_panic_handler(info: &panic::PanicInfo) ->! {
// 在這裡實作自訂的 panic 處理邏輯
println!("發生 panic:{}", info);
std::process::abort();
}
#[no_mangle]
pub extern "C" fn _start() ->! {
// 設定自訂 panic 處理 hook
panic::set_hook(Box::new(custom_panic_handler));
// 在這裡實作 Rust 程式的入口點邏輯
let text = b"Rust in Action";
println!("{}", text);
// 測試 panic
panic!("測試 panic");
}
在上面的範例中,我們結合了自訂 panic 處理和 Rust 入口點來實作一個完整的 Rust 程式。當程式發生 panic 時,會呼叫自訂的 panic 處理函式來處理。
自訂 Panic 處理器:向使用者報告錯誤
在嵌入式開發或在微控制器上執行 Rust 時,瞭解如何報告 Panic 的發生位置至關重要。一個好的起點是使用 core::fmt::Write 這個 trait。這個 trait 可以與 Panic 處理器相關聯,以顯示一條訊息,如圖 11.3 所示。
重新實作 Panic 處理器使用 core::fmt::Write
圖 11.3 中顯示的輸出是由清單 11.17 所產生的。現在,Panic 處理器已經被修改為兩階段過程。第一階段,Panic 處理器會清除螢幕。第二階段涉及 core::write! 巨集。core::write! 巨集需要一個實作 core::fmt::Write trait 的目的物件(在本例中為 cursor)作為其第一個引數。
以下清單(摘自清單 11.19)提供了一個 Panic 處理器的範例,該處理器使用此過程報告錯誤的發生:
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) ->! {
// 清除螢幕
//...
// 使用 core::write! 巨集將錯誤訊息寫入 cursor
core::write!(cursor, "Panic occurred: {:?}", _info).unwrap();
// 無限迴圈
loop {
hlt();
}
}
在這個範例中,Panic 處理器會清除螢幕,然後使用 core::write! 巨集將錯誤訊息寫入 cursor。錯誤訊息包括 _info 中的資訊,該資訊包含了 Panic 的相關細節。最後,處理器會進入無限迴圈。
實作 core::fmt::Write Trait
要使用 core::write! 巨集,必須實作 core::fmt::Write trait。這個 trait 定義瞭如何將資料寫入某個目的物件。在本例中,目的物件是 cursor。
以下是 core::fmt::Write trait 的實作範例:
use core::fmt;
struct Cursor {
position: usize,
foreground: Color,
background: Color,
}
impl fmt::Write for Cursor {
fn write_str(&mut self, s: &str) -> fmt::Result {
// 實作將字串 s 寫入 cursor 的邏輯
//...
Ok(())
}
}
在這個範例中,Cursor 結構體實作了 core::fmt::Write trait。write_str 方法定義瞭如何將字串寫入 cursor。
實作核心:核心格式化寫入
實作核心格式化寫入需要呼叫一個方法:write_str()。這個特性定義了其他幾個方法,但編譯器可以在 write_str() 實作後自動生成這些方法。以下清單展示了實作細節。
清除螢幕並顯示訊息
清除螢幕的過程涉及到呼叫 print() 方法,並將 UTF-8 編碼的 &str 轉換為 [u8] 型別,使用 to_bytes() 方法。這個過程允許使用者讀取訊息並手動重新啟動機器。
實作 core::fmt::Write
要實作 core::fmt::Write,我們需要定義 write_str() 方法。這個方法負責將字串寫入目標。其他方法,如 write_fmt(),可以由編譯器自動生成。
pub fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> {
// 將字串轉換為 UTF-8 編碼的 byte 列表
let bytes = s.as_bytes();
// 寫入 byte 列表
for &byte in bytes {
// 實際寫入邏輯
self.print(byte);
}
Ok(())
}
清除螢幕和顯示訊息的實作
清除螢幕的過程可以透過列印空白字元來實作。以下是相關的 Rust 程式碼:
pub fn panic(info: &PanicInfo) ->! {
let mut cursor = Cursor {
position: 0,
foreground: Color::White,
background: Color::Red,
};
// 清除螢幕
for _ in 0..(80*25) {
cursor.print(b" ");
}
cursor.position = 0;
// 顯示訊息
write!(cursor, "{}", info).unwrap();
loop {}
}
這段程式碼首先清除螢幕,然後顯示 panic 訊息,並最終進入無限迴圈,等待手動介入。
實作可寫入的Cursor結構
為了實作對Cursor結構的寫入功能,我們需要定義一個實作了fmt::Write特性的版本。這樣做的目的是使得Cursor可以直接被用於寫入字串。
impl fmt::Write for Cursor {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.print(s.as_bytes());
Ok(())
}
}
在這個實作中,write_str方法直接呼叫了Cursor的print方法,並將輸入的字串s轉換為位元組slice (s.as_bytes()),然後傳遞給print方法進行處理。這樣就完成了對Cursor結構的寫入功能的實作。
FledgeOS的使用者友好型panic處理程式碼
以下是FledgeOS中用於處理panic的使用者友好型程式碼。這部分程式碼位於ch11/ch11-fledgeos-4/src/main.rs中。要編譯這個專案,請重複11.2.1節中的指示,但將所有對fledgeos-0的參照替換為fledgeos-4。
#![feature(core_intrinsics)]
#![feature(lang_items)]
#![no_std]
#![no_main]
use core::fmt;
use core::panic::PanicInfo;
use core::fmt::Write;
use x86_64::instructions::{hlt};
#[allow(unused)]
#[derive(Copy, Clone)]
#[repr(u8)]
這段程式碼首先啟用了一些Rust的特性,包括core_intrinsics和lang_items,並且聲明瞭這個專案不使用標準函式庫 (#![no_std]) 也不需要主函式 (#![no_main])。然後,它引入了必要的模組,包括core::fmt、core::panic::PanicInfo和x86_64::instructions。最後,它定義了一個代表panic資訊的列舉型別,並使用了某些屬性(如#[allow(unused)]和#[derive(Copy, Clone)])來組態這個型別的行為。
內容解密:
在上面的Rust程式碼中,我們實作了對Cursor結構的寫入功能,並展示了FledgeOS中用於處理panic的使用者友好型程式碼。關鍵點在於我們定義了一個實作了fmt::Write特性的Cursor版本,這樣Cursor就可以直接被用於寫入字串。同時,FledgeOS的程式碼展示瞭如何啟用Rust的特性,宣告專案屬性,引入必要的模組,和定義列舉型別等。
圖表翻譯:
flowchart TD
A[啟動Rust特性] --> B[宣告專案屬性]
B --> C[引入必要模組]
C --> D[定義列舉型別]
D --> E[實作Cursor寫入功能]
E --> F[展示FledgeOS panic處理程式碼]
這個流程圖描述了我們如何一步一步地構建這個專案,從啟動Rust特性開始,到宣告專案屬性、引入必要模組、定義列舉型別,最後實作Cursor的寫入功能和展示FledgeOS的panic處理程式碼。每一步驟都緊密相連,共同完成了專案的構建。
列舉與結構體:基礎與實踐
在 Rust 中,enum 和 struct 是兩種基本的自定義型別,它們分別用於定義列舉和結構體。列舉型別(enum)允許你定義一組具名的值,而結構體(struct)則可以包含多個欄位。
從底層實作到高階應用的全面檢視顯示,有效運用 Rust 的列舉型別(enum)和結構體(struct)對於建構穩健且功能完善的作業系統至關重要。本文深入探討瞭如何利用 repr 屬性精確控制列舉型別在記憶體中的表示方式,以此滿足 VGA 文字模式色彩表的硬體需求,並同時分析了列舉型別相較於常數的優勢,強調其在提升程式碼安全性和可讀性方面的價值。文章進一步闡述了 Cursor 結構體的設計與實作,包含其在螢幕輸出、顏色編碼、以及與 VGA 框架緩衝區互動的關鍵作用,並以流程圖和程式碼範例清晰地展現了文字列印的底層機制。此外,本文還涵蓋了自訂 panic 處理器的重要性和實作方式,突顯了其在嵌入式系統開發中對於錯誤診斷和使用者經驗的提升。對於追求更進階的錯誤處理,文章詳細說明瞭如何利用 core::fmt::Write trait 將格式化的錯誤訊息輸出至螢幕,提供更友善的除錯資訊。最後,文章以 FledgeOS 的程式碼為例,完整展示了這些技術的整合與應用,為讀者提供了實務參考。展望未來,隨著 Rust 在作業系統開發領域的日益普及,深入理解和掌握這些基礎概念將成為建構高效、安全且可靠作業系統的根本。對於注重效能和底層控制的系統程式設計,Rust 的型別系統和記憶體管理機制將持續賦能開發者,創造更多可能性。