Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】
はじめに
前回(Part1)で環境構築が終わり、Hello Worldまで出せました。
でも正直、Part1のコードは「とりあえず動く」レベル。VGAバッファに毎回unsafeで直接書き込むのは辛い...
今回はno_stdの世界でもprintln!マクロが使えるように、VGAテキストモードをちゃんと抽象化していきます。
目次
no_stdで使えるもの・使えないもの
使えないもの(std専用)
-
std::io- ファイルI/O -
std::net- ネットワーク -
std::fs- ファイルシステム -
std::thread- スレッド -
std::sync::Mutex- 標準のMutex -
String,Vec,Box- ヒープを使うもの(allocクレートで復活可能) -
println!,print!- 標準出力
使えるもの(coreクレート)
-
core::fmt- フォーマット core::option::Optioncore::result::Result-
core::slice,core::str -
core::mem,core::ptr - プリミティブ型(
i32,u8, etc)
stdはcoreの上に構築されているので、coreにあるものは基本的に使えます。
重要:format_args!は使える
println!は使えないけど、format_args!マクロはcoreに含まれています。つまり、出力先さえ自分で用意すればprintln!的なことはできる!
VGAテキストバッファの構造
メモリレイアウト
アドレス 0xb8000 から:
+------+------+------+------+------+------+ ... (80文字分)
| 文字 | 属性 | 文字 | 属性 | 文字 | 属性 |
+------+------+------+------+------+------+
↓ 次の行
+------+------+------+------+------+------+ ...
| 文字 | 属性 | 文字 | 属性 | 文字 | 属性 |
+------+------+------+------+------+------+
... (25行分)
- 画面サイズ: 80列 × 25行
- 1文字あたり2バイト(文字コード + 属性)
- 合計: 80 × 25 × 2 = 4000バイト
属性バイト
ビット: 7 6 5 4 3 2 1 0
| |-----| | |-----|
| | | |
| | | +-- 前景色(0-7)
| | +-------- 前景色の明るさ
| +--------------- 背景色(0-7)
+-------------------- 点滅(または背景色の明るさ)
Writerを実装する
色の定義
まず、使える色を列挙型で定義します:
#[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,
}
#[repr(u8)]で、各バリアントが正確に指定した値を持つことを保証します。
色属性
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct ColorCode(u8);
impl ColorCode {
pub const fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
#[repr(transparent)]で、ColorCodeが内部のu8と全く同じメモリレイアウトになることを保証。
画面上の1文字
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
#[repr(C)]で、フィールドの順序がCと同じになることを保証。これがないとRustがフィールドを並び替える可能性があります。
バッファ全体
const VGA_WIDTH: usize = 80;
const VGA_HEIGHT: usize = 25;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; VGA_WIDTH]; VGA_HEIGHT],
}
Writerの実装
pub struct Writer {
column_position: usize,
row_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
impl Writer {
pub fn new() -> Writer {
Writer {
column_position: 0,
row_position: 0,
color_code: ColorCode::new(Color::White, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
}
}
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= VGA_WIDTH {
self.new_line();
}
let row = self.row_position;
let col = self.column_position;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code: self.color_code,
};
self.column_position += 1;
}
}
}
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// 印字可能なASCII文字またはnewline
0x20..=0x7e | b'\n' => self.write_byte(byte),
// 印字不可能な文字は■で表示
_ => self.write_byte(0xfe),
}
}
}
fn new_line(&mut self) {
if self.row_position < VGA_HEIGHT - 1 {
self.row_position += 1;
} else {
// スクロール
for row in 1..VGA_HEIGHT {
for col in 0..VGA_WIDTH {
self.buffer.chars[row - 1][col] = self.buffer.chars[row][col];
}
}
self.clear_row(VGA_HEIGHT - 1);
}
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..VGA_WIDTH {
self.buffer.chars[row][col] = blank;
}
}
pub fn clear_screen(&mut self) {
for row in 0..VGA_HEIGHT {
self.clear_row(row);
}
self.column_position = 0;
self.row_position = 0;
}
}
重要ポイント:スクロール処理
画面の一番下に到達したら、全体を1行上にずらして、最後の行をクリアします。これがないと25行以上出力できません。
fmt::Writeトレイトを実装する
core::fmt::Writeトレイトを実装すると、write!マクロが使えるようになります:
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
これでwrite!(writer, "Hello, {}!", "World")みたいに書けます。
println!マクロを作る
グローバルWriter
マクロからWriterにアクセスするために、グローバルな変数が必要です:
static mut WRITER: Option<Writer> = None;
pub fn init() {
unsafe {
WRITER = Some(Writer::new());
if let Some(ref mut writer) = WRITER {
writer.clear_screen();
}
}
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
unsafe {
if let Some(ref mut writer) = WRITER {
writer.write_fmt(args).unwrap();
}
}
}
注意: この実装はstatic mutを使っているのでスレッドセーフではありません。マルチタスクを実装するときにはspinlockなどで保護する必要があります。
マクロ定義
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[macro_export]をつけると、クレートの外から使えるようになります。$crateは現在のクレートを指す特別な変数。
動作確認
main.rsを更新
#![no_std]
#![no_main]
mod vga;
use core::panic::PanicInfo;
use bootloader::{entry_point, BootInfo};
entry_point!(kernel_main);
fn kernel_main(_boot_info: &'static BootInfo) -> ! {
// VGAを初期化
vga::init();
// println!マクロを使ってみる
println!("Hello, OS World!");
println!("Welcome to my-os!");
println!();
println!("This is line 4");
println!("Numbers: {} + {} = {}", 1, 2, 1 + 2);
loop {
unsafe { core::arch::asm!("hlt"); }
}
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("\n!!! KERNEL PANIC !!!");
println!("{}", info);
loop {
unsafe { core::arch::asm!("hlt"); }
}
}
ビルドと実行
cargo bootimage
WARNING: `CARGO_MANIFEST_DIR` env variable not set
Building kernel
warning: method `set_color` is never used
--> src\vga.rs:147:12
|
65 | impl Writer {
| ----------- method in this implementation
...
147 | pub fn set_color(&mut self, foreground: Color, background: Color) {
| ^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: `my-os` (bin "my-os") generated 1 warning
Finished `dev` profile [optimized + debuginfo] target(s) in 0.15s
Building bootloader
Compiling bootloader v0.9.33
Finished `release` profile [optimized + debuginfo] target(s) in 1.90s
Created bootimage for `my-os` at `target\x86_64-my_os\debug\bootimage-my-os.bin`
warningは出てるけど、ビルド成功!
qemu-system-x86_64 -drive "format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin"
画面に出た!!
Hello, OS World!
Welcome to my-os!
This is line 4
Numbers: 1 + 2 = 3
println!で数値のフォーマットもできてます。標準ライブラリがないのに!
シリアル出力も確認
qemu-system-x86_64 -drive "format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin" -serial file:serial.log
Get-Content serial.log
=== My OS Booting ===
VGA initialized
println! test complete
Entering infinite loop...
シリアル出力も問題なし。
ファイル構成
現在のプロジェクト構成:
my-os/
├── Cargo.toml
├── x86_64-my_os.json
├── .cargo/
│ └── config.toml
└── src/
├── main.rs
└── vga.rs
vga.rs(完全版)
use core::fmt;
use core::fmt::Write;
const VGA_BUFFER_ADDR: usize = 0xb8000;
const VGA_WIDTH: usize = 80;
const VGA_HEIGHT: usize = 25;
#[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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct ColorCode(u8);
impl ColorCode {
pub const fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; VGA_WIDTH]; VGA_HEIGHT],
}
pub struct Writer {
column_position: usize,
row_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
impl Writer {
pub fn new() -> Writer {
Writer {
column_position: 0,
row_position: 0,
color_code: ColorCode::new(Color::White, Color::Black),
buffer: unsafe { &mut *(VGA_BUFFER_ADDR as *mut Buffer) },
}
}
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= VGA_WIDTH {
self.new_line();
}
let row = self.row_position;
let col = self.column_position;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code: self.color_code,
};
self.column_position += 1;
}
}
}
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
0x20..=0x7e | b'\n' => self.write_byte(byte),
_ => self.write_byte(0xfe),
}
}
}
fn new_line(&mut self) {
if self.row_position < VGA_HEIGHT - 1 {
self.row_position += 1;
} else {
for row in 1..VGA_HEIGHT {
for col in 0..VGA_WIDTH {
self.buffer.chars[row - 1][col] = self.buffer.chars[row][col];
}
}
self.clear_row(VGA_HEIGHT - 1);
}
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..VGA_WIDTH {
self.buffer.chars[row][col] = blank;
}
}
pub fn clear_screen(&mut self) {
for row in 0..VGA_HEIGHT {
self.clear_row(row);
}
self.column_position = 0;
self.row_position = 0;
}
}
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
static mut WRITER: Option<Writer> = None;
pub fn init() {
unsafe {
WRITER = Some(Writer::new());
if let Some(ref mut writer) = WRITER {
writer.clear_screen();
}
}
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
unsafe {
if let Some(ref mut writer) = WRITER {
writer.write_fmt(args).unwrap();
}
}
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
まとめ
Part2では以下を達成しました:
- no_stdで使える/使えないものの整理
- VGAテキストバッファの抽象化
- Writerの実装(スクロール対応)
-
fmt::Writeトレイトの実装 -
println!マクロの作成
これでunsafeを毎回書かなくても画面出力ができるようになりました!
次回(Part3)では、ベアメタルでのHello World...の続きとして、もっと機能を追加していきます。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!
