4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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テキストモードをちゃんと抽象化していきます。

目次

  1. no_stdで使えるもの・使えないもの
  2. VGAテキストバッファの構造
  3. Writerを実装する
  4. println!マクロを作る
  5. 動作確認

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::Option
  • core::result::Result
  • core::slice, core::str
  • core::mem, core::ptr
  • プリミティブ型(i32, u8, etc)

stdcoreの上に構築されているので、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"

println demo

画面に出た!!

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...の続きとして、もっと機能を追加していきます。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?