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【完結】

はじめに

Part5で割り込みが動いてキーボード入力ができるようになりました!

今回は ページング を実装します。仮想メモリの世界に入っていきますよ〜

正直、ページングはめちゃくちゃ複雑で、最初はなんとなく理解するだけでも大変でした...

目次

ページングって何?

簡単に言うと「プログラムが見るアドレスと、実際の物理メモリのアドレスを分ける」仕組み。

プログラム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

BootInfophysical_memory_offsetというフィールドが追加されます:

物理アドレス + physical_memory_offset = アクセスできる仮想アドレス

例えばphysical_memory_offset0xFFFF_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)は今回の成果を使って ヒープメモリ を実装します。BoxVecが使えるようになる!楽しみ!

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

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?