每個顏色的第四位稱為加亮位 (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)了 Copy
、Clone
、Debug
、PartialEq
和Eq
這幾個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
類型類似,我們為它生成Copy
和Debug
等一系列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)和它的read
、write
方法;這些方法包裝了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.3333333333333333
。write!
宏返回的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_static
和spin
,都在操作系統開發中及其有用;我們將在未來的文章中多次使用它們。
下篇預告
下一篇文章中,我們將會講述如何配置Rust內置的單元測試框架。我們還將為本文編寫的VGA緩衝區模塊添加基礎的單元測試項目。
推薦閱讀: