Rust程式語言雖然可以很方便地做各式高階應用,但它本質上還是屬於系統層級的程式語言,換句話說,要拿它來開發作業系統也是可以的!Rust程式語言嚴謹的編譯器可以幫助我們在開發作業系統的時候於編譯階段就避免掉許多記憶體相關的問題,讓我們更能專注在其它方面上。這篇文章將會初步介紹用Rust程式語言開發作業系統核心(Kernel)的方式。



由於用Rust開發作業系統時無法使用Rust的標準函式庫,因此建議讀者們在開始閱讀這篇文章之前,先閱讀以下連結的文章,了解一下如何撰寫出無標準函式庫的Rust程式:

建立Cargo應用程式專案

在此我們就先用cargo指令建立一個Cargo應用程式專案來開發作業系統的核心,專案名稱為magic-os。指令如下:

cargo new --bin magic-os

關閉堆疊解開(Stack Unwinding)

可以在Cargo.toml設定檔中,加入[profile.dev][profile.release]區塊,並將panic設定項目的值設為abort。如下:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

自訂編譯目標(Target)

在終端機執行以下指令:

rustc +nightly -Z unstable-options --print target-spec-json

可以查看目前Rust開發環境預設的編譯目標的詳細設定值。如下圖:

rust-os-kernel

指令中的--print target-spec-json參數即是要讓Rust編譯器以JSON格式的文字來輸出目標的設定值,而因為這個功能還不穩定,所以要加上-Z unstable-options參數才能使用。

如果要指定目標名稱,可以再加上--target參數來傳入目標名稱。例如要查看wasm32-unknown-unknown這個目標的詳細設定值,指令如下:

rustc +nightly -Z unstable-options --print target-spec-json --target wasm32-unknown-unknown

rust-os-kernel

目標的設定值中,有幾個比較重要的項目,是必須要有的,列表如下:

  • llvm-target:這個項目即為LLVM的Target Triple,顧名思義它是由三個部份組成,以減號-來分隔。第一個部份是CPU架構,例如:arm、aarch64、x86、x86_64。第二個部份是廠商名稱,例如:IBM、NVIDIA、AMD。第三個部份是作業系統,例如:Darwin、Linux。none表示不設定且不限制。其實它還可以有第四個部份,第四個部份表示環境,例如:GNU、musl、Android。
  • data-layout:這個項目是LLVM用來設定資料在記憶體中儲存的方式,每個部份用減號-來分隔。Ee前者表示要用BE(big-endian,高位資料在低記憶體位址)來儲存資料,後者表示要用LE(little-endian,低位資料在高記憶體位址)。m用來設定Name Mangling的方式,例如m:e表示要用ELF Mangling。i可以用來設定整數對齊的寬度,例如i64:64表示要將64位元的整數以64位元的寬度來對齊。f可以用來設定浮點數對齊的寬度,例如f80:128表示要將80位元的浮點數以128位元的寬度來對齊。n可以用來設定原生的整數寬度,例如n8:16:32:64即表示CPU原生支援的整數寬度有8位元、16位元、32位元和64位元。S用來設定堆疊的對齊寬度,S128表示要將堆疊以128位元的寬度來對齊。
  • arch:這個項目設定的值在Rust程式語言內可以在cfg屬性中用target_arch參數來做判斷。
  • target-endian:這個項目設定的值在Rust程式語言內可以在cfg屬性中用target_endian參數來做判斷。
  • target-pointer-width:這個項目設定的值在Rust程式語言內可以在cfg屬性中用target_pointer_width參數來做判斷。
  • target-c-int-width:這是Rust在比較後來才加入的項目,作用尚不明確,如果知道這個項目是用來做什麼的訪客歡迎告知筆者一下。從名稱來看,這應該是在指定C語言用的int型別的寬度,但應用在哪就不知道了。
  • os:這個項目設定的值在Rust程式語言內可以在cfg屬性中用os參數來做判斷。
  • linker-flavor:設個項目可以用來設定LLVM要使用的連結器,除了預設的gcc(Unix-like)、msvc(Windows)、wasm-ld(Webassembly)等連結器之外,還可以使用LLVM提供的ld.lld(Unix-like, ELF)、lld-link(Windows)、ld-wasm(WebAssembly)和ld64.lld(macOS, Match-O)等連結器。

另外還有幾個雖非必要但也很重要的項目:

  • linker:用來設定連結器的可執行檔路徑,如果連結器是用LLVM的LLD的話,通常會設為rust-lld,直接使用Rust工具鏈提供的連結器可執行檔。
  • executables:用來設定是否可以編譯出可執行程式。

稍微了解編譯目標的設定方式之後,就可以用JSON文字格式來撰寫我們自己的目標了。在剛才我們的Cargo應用程式專案根目錄下,新增x86_64-magic_os.json,填入以下內容:

{
  "llvm-target": "x86_64-unknown-none",
  "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
  "arch": "x86_64",
  "target-endian": "little",
  "target-pointer-width": "64",
  "target-c-int-width": "32",
  "os": "none",
  "linker-flavor": "ld.lld",
  "linker": "rust-lld",
  "executables": true
}

撰寫作業系統核心程式

src/main.rs的內容修改如下:

#![no_std]
#![no_main]
 
use core::panic::PanicInfo;
 
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
 
#[no_mangle]
pub extern fn _start() -> ! {
    loop {}
}

_start函數是我們預期使用的程式進入點,我們可以透過開機載入程式來執行_start函數,讓電腦進入一個無窮迴圈。至於這個開機載入程式要怎麼製作,等等會介紹。

用我們的目標設定檔來編譯作業系統核心

雖然cargo指令工具的--target參數可以直接指定一個JSON格式的編譯目標設定檔來進行編譯,但是在使用自訂的編譯目標時,因為Rust內建的core這個crate也必須要用相同的目標來編譯才行,而cargo指令工具顯然並不會自動替我們完成這件事。

rust-os-kernel

還好我們可以執行以下指令來安裝cargo-xbuild這個指令工具,來協助我們在使用自訂目標時,自動去編譯Rust內建的corecompiler_builtinsalloc這幾個crate。

cargo install cargo-xbuild

還要使用rustup指令工具來安裝Rust的原始碼,指令如下:

rustup component add rust-src

為了要能夠編譯Rust內建的crate,還必須要使用Nightly版本的Rust。在Rust程式專案的根目錄中執行以下指令,即可只讓該程式專案使用Nightly的Rust來編譯,而不會去動到其它專案的設定。

rustup override set nightly

接著就可以開始使用cargo-xbuild來編譯我們的作業系統核心了!指令如下:

cargo xbuild --target x86_64-magic_os.json

如果想要啟用編譯器優化的話,可以再加上--release參數。

rust-os-kernel

用我們製作的作業系統核心來開機

雖然用cargo-xbuild指令工具可以成功編譯我們的Cargo應用程式專案,也可以直接在目前的作業系統環境下被執行。

rust-os-kernel

但是它既然是一個作業系統核心,那就應該要能夠在電腦開機時被執行才對。為了達到這件事,我們必須要有個開機載入程序,能夠在BIOS初始化好硬體之後,去執行這個開機載入程序,然後再來執行我們的作業系統核心。

在x86_64的CPU架構上我們並不需要自行去撰寫開機載入程序,因為已經有現成的工具可以幫我們產生出來了。

bootloaderbootimage

Cargo.toml設定檔中添加以下內容,加入bootloader套件:

[dependencies]
bootloader = "0.6"

[package.metadata.bootimage]
default-target = "x86_64-magic_os.json"

然後執行以下指令安裝bootimage指令工具:

cargo install bootimage

再執行以下指令安裝LLVM的相關工具:

rustup component add llvm-tools-preview

如此以來我們就可以用cargo bootimage或是cargo bootimage --release指令來產生我們的開機映像檔啦!開機映像檔的儲存路徑為target/x86_64-magic_os/debug/bootimage-magic-os.bin或是target/x86_64-magic_os/release/bootimage-magic-os.bin。我們可以把這個開機映像檔的內容直接寫進可開機的儲存裝置中,就能用來開機了。

如果要用虛擬機器來測試的話,基於Debian的Linux發行版可以直接使用以下指令來安裝x86_64的QEMU:

sudo apt install qemu-system-x86

接著就可以使用以下格式的指令來執行開機映像檔:

qemu-system-x86_64 -drive format=raw,file=開機映像檔路徑

例如:

qemu-system-x86_64 -drive format=raw,file=target/x86_64-magic_os/debug/bootimage-magic-os.bin

如果想要用VirtualBox來測試的話,要先將開機映像檔轉為VirtualBox支援的虛擬硬碟格式,以VMDK為例,指令格式如下:

VBoxManage convertdd 開機映像檔路徑 VMDK輸出路徑 --format VMDK

不過現在我們的作業系統核心只會跑一個無窮迴圈,測了好像也沒什麼意義,所以我們要來修改一下目前的作業系統核心,讓它可以在螢幕上顯示Hello, world!

Hello World

要在螢幕上顯示Hello, world!,可以透過修改VGA文字緩衝器(VGA Text Buffer)的內容來達成。這個VGA文字緩衝器的記憶體起始位址為0xB8000,一個字元以兩個位元組來表示,第一個位元組表示字元的值,第二個位元組表示字元的前景顏色(4位元)、背景顏色(4位元)。

顏色的值0~7分別是黑(0b000)、藍(0b001)、綠(0b010)、青(0b011)、紅(0b100)、洋紅(0b101)、棕(0b110)、淺灰(0b111),第4個位元則表示顏色的亮度,0為正常、1為加亮。

簡單了解VGA文字緩衝器的用法後,就可以開始修改我們的Rust程式碼了。修改後的程式碼如下:

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

const HELLO: &[u8] = b"Hello, world!";

#[no_mangle]
pub extern fn _start() -> ! {
    let vga_buffer = 0xB8000 as *mut u8;

    for (i, &byte) in HELLO.iter().enumerate() {
        unsafe {
            *vga_buffer.offset(i as isize * 2) = byte;
            *vga_buffer.offset(i as isize * 2 + 1) = 0b01011010;
        }
    }

    loop {}
}

以上的_start函數,會在修改VGA文字緩衝器的內容為洋紅背景色且文字顏色為亮綠色的Hello, world!後,進入無窮迴圈。

開機結果如下圖:

rust-os-kernel