VGA字元模式(VGA text mode)是列印字元到屏幕的一種簡單方式。在這篇文章中,為了包裝這個模式為一個安全而簡單的介面,我們包裝unsafe代碼到獨立的模塊。我們還將實現對Rust語言格式化宏(formatting macros)的支持。

原文:os.phil-opp.com/vga-tex 原作者:@phil-opp

譯者:洛佳 華中科技大學譯文鏈接:洛佳:使用Rust編寫操作系統(三):VGA字元模式本翻譯已被許可。轉載請註明出處,商業轉載請聯繫原作者

VGA字元緩衝區

為了在VGA字元模式向屏幕列印字元,我們必須將它寫入硬體提供的VGA字元緩衝區(VGA text buffer)。通常狀況下,VGA字元緩衝區是一個25行、80列的二維數組,它的內容將被實時渲染到屏幕。這個數組的元素被稱作字元單元(character cell),它使用下面的格式描述一個屏幕上的字元:

其中,前景色(foreground color)和背景色(background color)取值範圍如下:

每個顏色的第四位稱為加亮位(bright bit)。

要修改VGA字元緩衝區,我們可以通過存儲器映射輸入輸出(memory-mapped I/O)的方式,讀取或寫入地址0xb8000;這意味著,我們可以像操作普通的內存區域一樣操作這個地址。

需要主頁的是,一些硬體雖然映射到存儲器,卻可能不會完全支持所有的內存操作:可能會有一些設備支持按u8位元組讀取,卻在讀取u64時返回無效的數據。幸運的是,字元緩衝區都支持標準的讀寫操作,所以我們不需要用特殊的標準對待它。

包裝到Rust模塊

既然我們已經知道VGA文字緩衝區如何工作,也是時候創建一個Rust模塊來處理文字列印了。我們輸入這樣的代碼:

//?in src/main.rs
mod vga_buffer;

這行代碼定義了一個Rust模塊,它的內容應當保存在src/vga_buffer.rs文件中。使用2018版次(2018 edition)的Rust時,我們可以把模塊的子模塊(submodule)文件直接保存到src/vga_buffer/文件夾下,與vga_buffer.rs文件共存,而無需創建一個mod.rs文件。

我們的模塊暫時不需要添加子模塊,所以我們將它創建為src/vga_buffer.rs文件。除非另有說明,本文中的代碼都保存到這個文件中。

顏色

首先,我們使用Rust的枚舉(enum)表示一種顏色:

// in src/vga_buffer.rs

#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}

我們使用類似於C語言的枚舉(C-like enum),為每個顏色明確指定一個數字。在這裡,每個用repr(u8)註記標註的枚舉類型,都會以一個u8的形式存儲——事實上4個二進位位就足夠了,但Rust語言並不提供u4類型。

通常來說,編譯器會對每個未使用的變數發出警告(warning);使用#[allow(dead_code)],我們可以對Color枚舉類型禁用這個警告。

我們還生成(derive)了 CopyCloneDebugPartialEqEq 這幾個trait:這讓我們的類型遵循複製語義(copy semantics),也讓它可以被比較、被調試列印。

為了描述包含前景色和背景色的、完整的顏色代碼(color code),我們基於u8創建一個新類型:

// in src/vga_buffer.rs

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ColorCode(u8);

impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}

這裡,ColorCode類型包裝了一個完整的顏色代碼位元組,它包含前景色和背景色信息。和Color類型類似,我們為它生成CopyDebug等一系列trait。

字元緩衝區

現在,我們可以添加更多的結構體,來描述屏幕上的字元和整個字元緩衝區:

// in src/vga_buffer.rs

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}

const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;

struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}

在內存布局層面,Rust並不保證按順序布局成員變數。因此,我們需要使用#[repr(C)]標記結構體;這將按C語言約定的順序布局它的成員變數,讓我們能正確地映射內存片段。

為了輸出字元到屏幕,我們來創建一個Writer類型:

// in src/vga_buffer.rs

pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &static mut Buffer,
}

我們將讓這個Writer類型將字元寫入屏幕的最後一行,並在一行寫滿或收到換行符
的時候,將所有的字元向上位移一行。column_position變數將跟蹤游標在最後一行的位置。當前字元的前景和背景色將由color_code變數指定;另外,我們存入一個VGA字元緩衝區的可變借用到buffer變數中。需要注意的是,這裡我們對借用使用顯式生命周期(explicit lifetime),告訴編譯器這個借用在何時有效:我們使用static生命周期(static lifetime),意味著這個借用應該在整個程序的運行期間有效;這對一個全局有效的VGA字元緩衝區來說,是非常合理的。

列印字元

現在我們可以使用Writer類型來更改緩衝區內的字元了。首先,為了寫入一個ASCII碼位元組,我們創建這樣的函數:

// in src/vga_buffer.rs

impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b
=> self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}

let row = BUFFER_HEIGHT - 1;
let col = self.column_position;

let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}

fn new_line(&mut self) {/* TODO */}
}

如果這個位元組是一個換行符(line feed)位元組
,我們的Writer不應該列印新字元,相反,它將調用我們稍後會實現的new_line方法;其它的位元組應該將在match語句的第二個分支中被列印到屏幕上。

當列印位元組時,Writer將檢查當前行是否已滿。如果已滿,它將首先調用new_line方法來將這一行字向上提升,再將一個新的ScreenChar寫入到緩衝區,最終將當前的游標位置前進一位。

要列印整個字元串,我們把它轉換為位元組並依次輸出:

// in src/vga_buffer.rs

impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// 可以是能列印的ASCII碼位元組,也可以是換行符
0x20...0x7e | b
=> self.write_byte(byte),
// 不包含在上述範圍之內的位元組
_ => self.write_byte(0xfe),
}

}
}
}

VGA字元緩衝區只支持ASCII碼位元組和代碼頁437(Code page 437)定義的位元組。Rust語言的字元串默認編碼為UTF-8,也因此可能包含一些VGA字元緩衝區不支持的位元組:我們使用match語句,來區別可列印的ASCII碼或換行位元組,和其它不可列印的位元組。對每個不可列印的位元組,我們列印一個符號;這個符號在VGA硬體中被編碼為十六進位的0xfe

我們可以親自試一試已經編寫的代碼。為了這樣做,我們可以臨時編寫一個函數:

// in src/vga_buffer.rs

pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};

writer.write_byte(bH);
writer.write_string("ello ");
writer.write_string("W?rld!");
}

這個函數首先創建一個指向0xb8000地址VGA緩衝區的Writer。實現這一點,我們需要編寫的代碼可能看起來有點奇怪:首先,我們把整數0xb8000強制轉換為一個可變的裸指針(raw pointer);之後,通過運算符*,我們將這個裸指針解引用;最後,我們再通過&mut,再次獲得它的可變借用。這些轉換需要unsafe語句塊(unsafe block),因為編譯器並不能保證這個裸指針是有效的。

然後它將位元組 bH 寫入緩衝區內. 前綴 b創建了一個位元組常量(byte literal),表示單個ASCII碼字元;通過嘗試寫入 "ello ""W?rld!",我們可以測試 write_string 方法和其後對無法列印字元的處理邏輯。當我們嘗試在src/main.rs中的_start函數內調用vga_buffer::print_something時,黃色的Hello Wrld!字元串將會被列印在屏幕的左下角:

QEMU顯示相應字元串

需要注意的是,?字元被列印為兩個字元。這是因為在UTF-8編碼下,字元?是由兩個位元組表述的——而這兩個位元組並不處在可列印的ASCII碼位元組範圍之內。事實上,這是UTF-8編碼的基本特點之一:如果一個字元佔用多個位元組,那麼每個組成它的獨立位元組都不是有效的ASCII碼位元組(the individual bytes of multi-byte values are never valid ASCII)。

Volatile

我們剛才看到,自己想要輸出的信息被正確地列印到屏幕上。然而,未來Rust編譯器更暴力的優化可能讓這段代碼不按預期工作。

產生問題的原因在於,我們只向Buffer寫入,卻不再從它讀出數據。此時,編譯器不知道我們事實上已經在操作VGA緩衝區內存,而不是在操作普通的RAM——因此也不知道產生的副效應(side effect),即會有幾個字元顯示在屏幕上。這時,編譯器也許會認為這些寫入操作都沒有必要,甚至會選擇忽略這些操作!所以,為了避免這些並不正確的優化,這些寫入操作應當被指定為volatile操作。這將告訴編譯器,這些寫入可能會產生副效應,不應該被優化掉。

為了在我們的VGA緩衝區中使用volatile寫入操作,我們使用volatile庫。這個(crate)提供一個名為Volatile包裝類型(wrapping type)和它的readwrite方法;這些方法包裝了core::ptr內的read_volatile和write_volatile 函數,從而保證讀操作或寫操作不會被編譯器優化。

要添加volatile包為項目的依賴項(dependency),我們可以在Cargo.toml文件的dependencies中添加下面的代碼:

# in Cargo.toml

[dependencies]
volatile = "0.2.3"

0.2.3表示一個語義版本號(semantic version number),在cargo文檔的指定依賴項章節可以找到與它相關的使用指南。

現在,我們使用它來完成VGA緩衝區的volatile寫入操作。我們將Buffer類型的定義修改為下列代碼:

// in src/vga_buffer.rs

use volatile::Volatile;

struct Buffer {
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}

在這裡,我們不使用ScreenChar,而選擇使用Volatile<ScreenChar>——在這裡,Volatile類型是一個泛型(generic),可以包裝幾乎所有的類型——這確保了我們不會通過普通的寫入操作,意外地向它寫入數據;我們轉而使用提供的write方法。

這意味著,我們必須要修改我們的Writer::write_byte方法:

// in src/vga_buffer.rs

impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b
=> self.new_line(),
byte => {
...

self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code: color_code,
});
...
}
}
}
...
}

正如代碼所示,我們不再使用普通的=賦值,而使用了write方法:這能確保編譯器不再優化這個寫入操作。

格式化宏

支持Rust提供的格式化宏(formatting macros)也是一個相當棒的主意。通過這種途徑,我們可以輕鬆地列印不同類型的變數,如整數或浮點數。為了支持它們,我們需要實現core::fmt::Write trait;要實現它,唯一需要提供的方法是write_str,它和我們先前編寫的write_string方法差別不大,只是返回值類型變成了fmt::Result

// in src/vga_buffer.rs

use core::fmt;

impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}

這裡,Ok(())屬於Result枚舉類型中的Ok,包含一個值為()的變數。

現在我們就可以使用Rust內置的格式化宏write!writeln!了:

// in src/vga_buffer.rs

pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};

writer.write_byte(bH);
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}

現在,你應該在屏幕下端看到一串Hello! The numbers are 42 and 0.3333333333333333write!宏返回的Result類型必須被使用,所以我們調用它的unwrap方法,它將在錯誤發生時panic。這裡的情況下應該不會發生這樣的問題,因為寫入VGA字元緩衝區並沒有可能失敗。

換行

在之前的代碼中,我們忽略了換行符,因此沒有處理超出一行字元的情況。當換行時,我們想要把每個字元向上移動一行——此時最頂上的一行將被刪除——然後在最後一行的起始位置繼續列印。要做到這一點,我們要為Writer實現一個新的new_line方法:

// in src/vga_buffer.rs

impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}

fn clear_row(&mut self, row: usize) {/* TODO */}
}

我們遍歷每個屏幕上的字元,把每個字元移動到它上方一行的相應位置。這裡,..符號是區間標號(range notation)的一種;它表示左閉右開的區間,因此不包含它的上界。在外層的枚舉中,我們從第1行開始,省略了對第0行的枚舉過程——因為這一行應該被移出屏幕,即它將被下一行的字元覆寫。

所以我們實現的clear_row方法代碼如下:

// in src/vga_buffer.rs

impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b ,
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}

通過向對應的緩衝區寫入空格字元,這個方法能清空一整行的字元位置。

全局介面

編寫其它模塊時,我們希望無需隨身攜帶Writer實例,便能使用它的方法。我們嘗試創建一個靜態的WRITER變數:

// in src/vga_buffer.rs

pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};

我們嘗試編譯這些代碼,卻發生了下面的編譯錯誤:

error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant

error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values

error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values

為了明白現在發生了什麼,我們需要知道一點:一般的變數在運行時初始化,而靜態變數在編譯時初始化。Rust編譯器規定了一個稱為常量求值器(const evaluator)的組件,它應該在編譯時處理這樣的初始化工作。雖然它目前的功能較為有限,但對它的擴展工作進展活躍,比如允許在常量中panic的一篇RFC文檔。

關於ColorCode::new的問題應該能使用常函數(const functions)解決,但常量求值器還存在不完善之處,它還不能在編譯時直接轉換裸指針到變數的引用——也許未來這段代碼能夠工作,但在那之前,我們需要尋找另外的解決方案。

延遲初始化

使用非常函數初始化靜態變數是Rust程序員普遍遇到的問題。幸運的是,有一個叫做lazy_static的包提供了一個很棒的解決方案:它提供了名為lazy_static!的宏,定義了一個延遲初始化(lazily initialized)的靜態變數;這個變數的值將在第一次使用時計算,而非在編譯時計算。這時,變數的初始化過程將在運行時執行,任意的初始化代碼——無論簡單或複雜——都是能夠使用的。

現在,我們將lazy_static包導入到我們的項目:

# in Cargo.toml

[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]

在這裡,由於程序不連接標準庫,我們需要啟用spin_no_std特性。

使用lazy_static我們就可以定義一個不出問題的WRITER變數:

// in src/vga_buffer.rs

use lazy_static::lazy_static;

lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}

然而,這個WRITER可能沒有什麼用途,因為它目前還是不可變變數(immutable variable):這意味著我們無法向它寫入數據,因為所有與寫入數據相關的方法都需要實例的可變引用&mut self。一種解決方案是使用可變靜態(mutable static)的變數,但所有對它的讀寫操作都被規定為不安全的(unsafe)操作,因為這很容易導致數據競爭或發生其它不好的事情——使用static mut極其不被贊成,甚至有一些提案認為應該將它刪除。也有其它的替代方案,比如可以嘗試使用比如RefCell或甚至UnsafeCell等類型提供的內部可變性(interior mutability);但這些類型都被設計為非同步類型,即不滿足Sync約束,所以我們不能在靜態變數中使用它們。

自旋鎖

要定義同步的內部可變性,我們往往使用標準庫提供的互斥鎖類Mutex,它通過提供當資源被佔用時將線程阻塞(block)的互斥條件(mutual exclusion)實現這一點;但我們初步的內核代碼還沒有線程和阻塞的概念,我們將不能使用這個類。不過,我們還有一種較為基礎的互斥鎖實現方式——自旋鎖(spinlock)。自旋鎖並不會調用阻塞邏輯,而是在一個小的無限循環中反覆嘗試獲得這個鎖,也因此會一直佔用CPU時間,直到互斥鎖被它的佔用者釋放。

為了使用自旋的互斥鎖,我們添加spin包到項目的依賴項列表:

# in Cargo.toml
[dependencies]
spin = "0.4.9"

現在,我們能夠使用自旋的互斥鎖,為我們的WRITER類實現安全的內部可變性:

// in src/vga_buffer.rs

use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}

現在我們可以刪除print_something函數,嘗試直接在_start函數中列印字元:

// in src/main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();

loop {}
}

在這裡,我們需要導入名為fmt::Write的trait,來使用實現它的類的相應方法。

安全性

經過上文的努力後,我們現在的代碼只剩一個unsafe語句塊,它用於創建一個指向0xb8000地址的Buffer類型引用;在這步之後,所有的操作都是安全的。Rust將為每個數組訪問檢查邊界,所以我們不會在不經意間越界到緩衝區之外。因此,我們把需要的條件編碼到Rust的類型系統,這之後,我們為外界提供的介面就符合內存安全原則了。

println!

現在我們有了一個全局的Writer實例,我們就可以基於它實現println!宏,這樣它就能被任意地方的代碼使用了。Rust提供的宏定義語法需要時間理解,所以我們將不從零開始編寫這個宏。我們先看看它在標準庫中的實現源碼:

#[macro_export]
macro_rules! println {
() => (print!("
"
));
($($arg:tt)*) => (print!("{}
"
, format_args!($($arg)*)));
}

宏是通過一個或多個規則(rule)定義的,這就像match語句的多個分支。println!宏有兩個規則:第一個規則不要求傳入參數——就比如println!()——它將被擴展為print!("
")
,因此只會列印一個新行;第二個要求傳入參數——好比println!("Rust能夠編寫操作系統")println!("我學習Rust已經{}年了", 3)——它將使用print!宏擴展,傳入它需求的所有參數,並在輸出的字元串最後加入一個換行符

這裡,#[macro_export]屬性讓整個包(crate)和基於它的包都能訪問這個宏,而不僅限於定義它的模塊(module)。它還將把宏置於包的根模塊(crate root)下,這意味著比如我們需要通過use std::println來導入這個宏,而不是通過std::macros::println

print!宏是這樣定義的:

#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

這個宏將擴展為一個對io模塊中_print函數的調用。$crate變數($crate variable)將在std包之外被解析為std包,保證整個宏在std包之外也可以使用。

format_args!宏將傳入的參數搭建為一個fmt::Arguments類型,這個類型將被傳入_print函數。std包中的_print函數將調用複雜的私有函數print_to,來處理對不同Stdout設備的支持。我們不需要編寫這樣的複雜函數,因為我們只需要列印到VGA字元緩衝區。

要列印到字元緩衝區,我們把println!print!兩個宏複製過來,但修改部分代碼,讓這些宏使用我們定義的_print函數:

// in src/vga_buffer.rs

#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}

#[macro_export]
macro_rules! println {
() => ($crate::print!("
"
));
($($arg:tt)*) => ($crate::print!("{}
"
, format_args!($($arg)*)));
}

#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}

我們首先修改了println!宏,在每個使用的print!宏前面添加了$crate變數。這樣我們在只需要使用println!時,不必也編寫代碼導入print!宏。

就像標準庫做的那樣,我們為兩個宏都添加了#[macro_export]屬性,這樣在包的其它地方也可以使用它們。需要注意的是,這將佔用包的根命名空間(root namespace),所以我們不能通過use crate::vga_buffer::println來導入它們;我們應該使用use crate::println

另外,_print函數將佔有靜態變數WRITER的鎖,並調用它的write_fmt方法。這個方法是從名為Write的trait中獲得的,所以我們需要導入這個trait。額外的unwrap()函數將在列印不成功的時候panic;但既然我們的write_str總是返回Ok,這種情況不應該發生。

如果這個宏將能在模塊外訪問,它們也應當能訪問_print函數,因此這個函數必須是公有的(public)。然而,考慮到這是一個私有的實現細節,我們添加一個doc(hidden)屬性 ,防止它在生成的文檔中出現。

使用println!的Hello World

現在,我們可以在_start里使用println!了:

// in src/main.rs

#[no_mangle]
pub extern "C" fn _start() {
println!("Hello World{}", "!");

loop {}
}

要注意的是,我們在入口函數中不需要導入這個宏——因為它已經被置於包的根命名空間了。

運行這段代碼,和我們預料的一樣,一個「Hello World!」 字元串被列印到了屏幕上:

QEMU列印「Hello World」字元串

列印panic信息

既然我們已經有了println!宏,我們可以在panic處理函數中,使用它列印panic信息和panic產生的位置:

// in main.rs

/// 這個函數將在panic發生時被調用
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}

當我們在_start函數中插入一行panic!("Some panic message");後,我們得到了這樣的輸出:

QEMU顯示panic詳細內容

所以,現在我們不僅能知道panic已經發生,還能夠知道panic信息和產生panic的代碼。

小結

這篇文章中,我們學習了VGA字元緩衝區的結構,以及如何在0xb8000的內存映射地址訪問它。我們將所有的不安全操作包裝為一個Rust模塊,以便在外界安全地訪問它。

我們也發現了在Rust中使用第三方提供的包是極其容易的,這要感謝便於使用的cargo。我們添加的兩個依賴項,lazy_staticspin,都在操作系統開發中及其有用;我們將在未來的文章中多次使用它們。

下篇預告

下一篇文章中,我們將會講述如何配置Rust內置的單元測試框架。我們還將為本文編寫的VGA緩衝區模塊添加基礎的單元測試項目。


推薦閱讀:
相关文章