Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】
はじめに
Part5で割り込みが動いてキーボード入力ができるようになりました!
今回は ページング を実装します。仮想メモリの世界に入っていきますよ〜
正直、ページングはめちゃくちゃ複雑で、最初はなんとなく理解するだけでも大変でした...
目次
- はじめに
- 目次
- ページングって何?
- x86_64の4段階ページテーブル
- bootloaderの設定
- memory.rsを作成
- フレームアロケータ
- main.rsの更新
- 動作確認
- アドレス変換の仕組み(おまけ)
- まとめ
ページングって何?
簡単に言うと「プログラムが見るアドレスと、実際の物理メモリのアドレスを分ける」仕組み。
プログラムA: 0x1000番地を読む → 実際は 0x50000番地
プログラムB: 0x1000番地を読む → 実際は 0x80000番地
同じ「0x1000番地」でも、プログラムごとに違う物理メモリを指せる。これで複数のプログラムが独立したメモリ空間を持てます。
用語整理
| 用語 | 意味 |
|---|---|
| 仮想アドレス | プログラムが見るアドレス |
| 物理アドレス | 実際のRAMのアドレス |
| ページ | 仮想メモリの単位(4KiB) |
| フレーム | 物理メモリの単位(4KiB) |
| ページテーブル | 仮想→物理の変換表 |
x86_64の4段階ページテーブル
x86_64では4段階のテーブルを使って変換します。深い...
仮想アドレス(48ビット)
┌────────┬────────┬────────┬────────┬────────────┐
│ PML4 │ PDPT │ PD │ PT │ Offset │
│ (9bit) │ (9bit) │ (9bit) │ (9bit) │ (12bit) │
└────────┴────────┴────────┴────────┴────────────┘
↓ ↓ ↓ ↓
L4テーブル → L3テーブル → L2テーブル → L1テーブル → 物理フレーム
各テーブルは512エントリ(2^9)で、最後の12ビットはページ内のオフセット(4KiB = 2^12)。
なぜ4段階?
1段階だと、64ビット空間全体をカバーするのに巨大なテーブルが必要。4段階にすることで、使っている部分だけテーブルを確保できます。メモリの節約!
bootloaderの設定
ページングを実装するには、物理メモリ全体にアクセスできる必要があります。bootloaderクレートの機能を使います:
# Cargo.toml
[dependencies]
bootloader = { version = "0.9.23", features = ["map_physical_memory"] }
map_physical_memoryフィーチャーを有効にすると、ブートローダーが物理メモリ全体を仮想アドレスにマップしてくれます。
physical_memory_offset
BootInfoにphysical_memory_offsetというフィールドが追加されます:
物理アドレス + physical_memory_offset = アクセスできる仮想アドレス
例えばphysical_memory_offsetが0xFFFF_8000_0000_0000なら:
- 物理アドレス
0x1000→ 仮想アドレス0xFFFF_8000_0000_1000でアクセス
memory.rsを作成
src/memory.rsを新規作成:
use x86_64::{
structures::paging::{PageTable, OffsetPageTable, PhysFrame, Size4KiB, FrameAllocator},
VirtAddr, PhysAddr,
};
use bootloader::bootinfo::{MemoryMap, MemoryRegionType};
/// ページテーブルの初期化
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {
let level_4_table = active_level_4_table(physical_memory_offset);
OffsetPageTable::new(level_4_table, physical_memory_offset)
}
アクティブなページテーブルを取得
CR3レジスタに現在のページテーブルのアドレスが入っています:
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {
use x86_64::registers::control::Cr3;
let (level_4_table_frame, _) = Cr3::read();
let phys = level_4_table_frame.start_address();
let virt = physical_memory_offset + phys.as_u64();
let page_table_ptr: *mut PageTable = virt.as_mut_ptr();
&mut *page_table_ptr
}
CR3から物理アドレスを取得→オフセットを足して仮想アドレスに変換→ポインタとしてアクセス。
フレームアロケータ
新しいページをマップするには、空いている物理フレームが必要。ブートローダーが提供するメモリマップを使います:
pub struct BootInfoFrameAllocator {
memory_map: &'static MemoryMap,
next: usize,
}
impl BootInfoFrameAllocator {
pub unsafe fn init(memory_map: &'static MemoryMap) -> Self {
BootInfoFrameAllocator {
memory_map,
next: 0,
}
}
fn usable_frames(&self) -> impl Iterator<Item = PhysFrame> {
let regions = self.memory_map.iter();
let usable_regions = regions.filter(|r| r.region_type == MemoryRegionType::Usable);
let addr_ranges = usable_regions.map(|r| r.range.start_addr()..r.range.end_addr());
let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));
frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))
}
}
unsafe impl FrameAllocator<Size4KiB> for BootInfoFrameAllocator {
fn allocate_frame(&mut self) -> Option<PhysFrame> {
let frame = self.usable_frames().nth(self.next);
self.next += 1;
frame
}
}
これは「バンプアロケータ」という方式。シンプルだけど、解放ができない...まあ今は十分。
main.rsの更新
mod memory;
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");
// メモリ情報を表示
println!("Physical memory offset: {:?}", phys_mem_offset);
// 使用可能メモリの計算
let usable_memory: u64 = boot_info.memory_map.iter()
.filter(|r| r.region_type == bootloader::bootinfo::MemoryRegionType::Usable)
.map(|r| r.range.end_addr() - r.range.start_addr())
.sum();
println!("Usable memory: {} KB ({} MB)", usable_memory / 1024, usable_memory / 1024 / 1024);
// ...
}
動作確認
cargo bootimage
Compiling bootloader v0.9.33
Compiling my-os v0.1.0 (C:\Users\Aqua\Documents\Qiita\my-os)
warning: unused variable: `mapper`
--> src\main.rs:81:9
|
81 | let mut mapper = unsafe { memory::init(phys_mem_offset) };
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore
warning: unused variable: `frame_allocator`
--> src\main.rs:82:9
|
82 | let mut frame_allocator = unsafe {
| ^^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore
warning: `my-os` (bin "my-os") generated 2 warnings
Finished `dev` profile [optimized + debuginfo] target(s) in 1.61s
Building bootloader
Finished `release` profile [optimized + debuginfo] target(s) in 2.37s
Created bootimage for `my-os` at `target\x86_64-my_os\debug\bootimage-my-os.bin`
警告は出てるけど、次回(ヒープ)で使うので無視。
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
println! test complete
Entering infinite loop...
画面に出る内容:
Physical memory offset: VirtAddr(0xffff800000000000)
Usable memory: 130048 KB (127 MB)
QEMUのデフォルトメモリ(128MB)がちゃんと認識されてる!
アドレス変換の仕組み(おまけ)
デバッグ用に、仮想アドレスを物理アドレスに変換する関数も作っておきます:
pub fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) -> Option<PhysAddr> {
use x86_64::structures::paging::page_table::FrameError;
use x86_64::registers::control::Cr3;
let (level_4_table_frame, _) = Cr3::read();
let table_indexes = [
addr.p4_index(),
addr.p3_index(),
addr.p2_index(),
addr.p1_index(),
];
let mut frame = level_4_table_frame;
// 4段階のページテーブルをたどる
for &index in &table_indexes {
let virt = physical_memory_offset + frame.start_address().as_u64();
let table_ptr: *const PageTable = virt.as_ptr();
let table = unsafe { &*table_ptr };
let entry = &table[index];
frame = match entry.frame() {
Ok(frame) => frame,
Err(FrameError::FrameNotPresent) => return None,
Err(FrameError::HugeFrame) => panic!("huge pages not supported"),
};
}
Some(frame.start_address() + u64::from(addr.page_offset()))
}
まとめ
Part6では以下を達成しました:
- ページングの概念を理解
- 4段階ページテーブルの構造
- CR3からページテーブルにアクセス
- フレームアロケータの実装
- メモリマップの表示
次回(Part7)は今回の成果を使って ヒープメモリ を実装します。BoxやVecが使えるようになる!楽しみ!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!