Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】
はじめに
Part6でページングを実装しました。
今回は ヒープメモリ を実装します!これができるとBox、Vec、Stringが使えるようになる!
ぶっちゃけ、ここが一番ワクワクしたところです。Rustの強力なコレクション型がOSカーネルで使えるって、ものすごくテンション上がる。
目次
スタックとヒープ
| スタック | ヒープ | |
|---|---|---|
| サイズ | コンパイル時に決定 | 実行時に動的 |
| 確保 | 自動(関数呼び出し時) | 手動(Box::newなど) |
| 解放 | 自動(関数終了時) | 手動(Dropトレイト) |
| 速度 | 高速 | やや遅い |
no_std環境では、ヒープを使うには自分でアロケータを実装する必要があります。
依存クレートを追加
# Cargo.toml
[dependencies]
linked_list_allocator = "0.10"
linked_list_allocatorはフリーリスト方式のシンプルなアロケータ。自作してもいいけど、今回は既存クレートを使います。
build-stdの更新
allocクレートを使うために、.cargo/config.tomlを更新:
[unstable]
build-std = ["core", "compiler_builtins", "alloc"] # alloc追加!
build-std-features = ["compiler-builtins-mem"]
グローバルアロケータ
Rustでは#[global_allocator]属性でグローバルアロケータを指定します。
src/allocator.rsを新規作成:
use x86_64::{
structures::paging::{
mapper::MapToError, FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB,
},
VirtAddr,
};
use linked_list_allocator::LockedHeap;
pub const HEAP_START: usize = 0x_4444_4444_0000;
pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();
HEAP_STARTの値
0x_4444_4444_0000という値に深い意味はないです。デバッグ時に「4が並んでるからヒープだな」ってわかりやすいから選びました。
重要なのは:
- カーネルのコード領域と被らない
- スタック領域と被らない
- 48ビットアドレス空間内
ヒープ領域をマップする
ヒープ用の仮想アドレスに、物理フレームをマップします:
pub fn init_heap(
mapper: &mut impl Mapper<Size4KiB>,
frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> Result<(), MapToError<Size4KiB>> {
let page_range = {
let heap_start = VirtAddr::new(HEAP_START as u64);
let heap_end = heap_start + HEAP_SIZE as u64 - 1u64;
let heap_start_page = Page::containing_address(heap_start);
let heap_end_page = Page::containing_address(heap_end);
Page::range_inclusive(heap_start_page, heap_end_page)
};
for page in page_range {
let frame = frame_allocator
.allocate_frame()
.ok_or(MapToError::FrameAllocationFailed)?;
let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
unsafe {
mapper.map_to(page, frame, flags, frame_allocator)?.flush();
}
}
unsafe {
ALLOCATOR.lock().init(HEAP_START as *mut u8, HEAP_SIZE);
}
Ok(())
}
やっていること:
- ヒープ領域のページ範囲を計算(100KiB ÷ 4KiB = 25ページ)
- 各ページに物理フレームを割り当て
-
PRESENT | WRITABLEフラグでマップ - アロケータを初期化
allocクレートを使う
main.rsに追加:
#![no_std]
#![no_main]
#![feature(abi_x86_interrupt)]
extern crate alloc; // これを追加!
mod vga;
mod interrupts;
mod memory;
mod allocator;
use alloc::{boxed::Box, vec::Vec, string::String};
extern crate allocでallocクレートをインポート。これでBox、Vec、Stringなどが使えます。
main.rsの更新
fn kernel_main(boot_info: &'static BootInfo) -> ! {
// ... 前回までの初期化 ...
// ページングの初期化
let phys_mem_offset = x86_64::VirtAddr::new(boot_info.physical_memory_offset);
let mut mapper = unsafe { memory::init(phys_mem_offset) };
let mut frame_allocator = unsafe {
memory::BootInfoFrameAllocator::init(&boot_info.memory_map)
};
serial_print("Paging initialized\n");
// ヒープの初期化
allocator::init_heap(&mut mapper, &mut frame_allocator)
.expect("heap initialization failed");
serial_print("Heap initialized\n");
// ヒープのテスト!
println!("Testing heap allocations...");
// Boxのテスト
let heap_value = Box::new(42);
println!("Box<i32>: {}", *heap_value);
// Vecのテスト
let mut vec = Vec::new();
for i in 0..10 {
vec.push(i);
}
println!("Vec: {:?}", vec);
// Stringのテスト
let mut s = String::from("Hello");
s.push_str(", Heap!");
println!("String: {}", s);
// ...
}
動作確認
cargo bootimage
Compiling alloc v0.0.0 (...)
Compiling linked_list_allocator v0.10.5
Compiling my-os v0.1.0 (C:\Users\Aqua\Documents\Qiita\my-os)
warning: method `as_usize` is never used
warning: function `translate_addr` is never used
Finished `dev` profile [optimized + debuginfo] target(s) in 7.81s
Building bootloader
Finished `release` profile [optimized + debuginfo] target(s) in 1.76s
Created bootimage for `my-os` at `target\x86_64-my_os\debug\bootimage-my-os.bin`
allocクレートがビルドされてる!
qemu-system-x86_64 -drive format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin -serial file:serial.log
シリアルログ:
=== My OS Booting ===
VGA initialized
IDT initialized
PICs initialized
Interrupts enabled
Paging initialized
Heap initialized
Heap test complete
Entering infinite loop...
画面:
Testing heap allocations...
Box<i32>: 42
Vec: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
String: Hello, Heap!
Heap works! Try pressing keys...
動いた!!!
VecとStringが自作OSで使えるなんて...感動です。
何が起きてるのか
Box::new(42)を呼ぶと:
1. Box::new(42) 呼び出し
↓
2. alloc::alloc() が呼ばれる
↓
3. #[global_allocator] の ALLOCATOR.alloc() が呼ばれる
↓
4. linked_list_allocator がフリーリストから4バイト確保
↓
5. ヒープ領域(0x4444_4444_0000〜)から確保
↓
6. そのアドレスに42を書き込む
↓
7. Box<i32> 完成!
linked_list_allocatorの仕組み
フリーリスト方式:
初期状態:
[ Free: 100KiB ]
Box::new(42) 後:
[42][ Free: ~100KiB ]
Vec確保後:
[42][0,1,2,...,9][ Free: ~100KiB ]
解放されたブロックはリストに戻されて、隣接するフリーブロックは結合されます。
ヒープサイズについて
今回は100KiBにしました。小さいけど、デモには十分。
実際のOSでは:
- 初期サイズをもっと大きくする
- 必要に応じて拡張する仕組みを作る
まとめ
Part7では以下を達成しました:
- グローバルアロケータの設定
- ヒープ領域のマッピング
-
allocクレートの有効化 - Box、Vec、Stringが動いた!
ここまで来ると、かなりOSっぽくなってきました。
次回以降は:
- 非同期処理(async/await)
- ファイルシステム
- ユーザーモード
などを実装していく予定です。まだまだ道のりは長い...でも楽しい!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!