Memory 2

Introduction

This article covers advanced content, understanding what is explained in Chapter 3 “Let’s Implement Memory Management” of Build and Learn: How OS Works I (hereinafter, OS book). In the previous article, we learned the basics of memory management, but this time, we’ll look at how it’s implemented (or how it’s being implemented).

Byte-level Allocator

pub fn init_basic_runtime(
    image_handle: EfiHandle,
    efi_system_table: &EfiSystemTable,
) -> MemoryMapHolder {
    let mut memory_map = MemoryMapHolder::new();
    exit_from_efi_boot_services(image_handle, efi_system_table, &mut memory_map);
    ALLOCATOR.init_with_mmap(&memory_map);
    memory_map
}
#[global_allocator]
pub static ALLOCATOR: FirstFitAllocator = FirstFitAllocator {
    first_header: RefCell::new(None),
};

Paging

The paging mechanism (where the OS instructs the CPU about address translation) differs by architecture, so we’ll focus on x86_64.

It has evolved through: 16-bit mode 32-bit mode 64-bit mode In 64-bit mode, paging is mandatory. At the point when OS code is executed by UEFI, it’s in 64-bit mode and paging itself is already enabled.

About Hierarchical Page Tables

ページテーブルは基本的には「仮想アドレス(の一部)をイ ンデックスとして、変換先のページの物理アドレスを書き並べた配列」のようなもの

x86_64 では、一つのページテーブル構造体は 4KiB の大きさとアライメントを 持ちます。それぞれの構造体は大雑把に言えば、次の段階のページテーブル(も しくはページ)への物理アドレスが格納された配列になっています。アドレスは 64 ビット、つまり 8 バイトなので、一つのページテーブルは次の段階へのポイ ンタを 512 個書き並べたものになっている

The conceptual parts are as introduced in the previous article.

Actual Verification

pub type RootPageTable = [u8; 1024];

pub fn read_cr3() -> *mut RootPageTable {
    let mut cr3: *mut RootPageTable;
    unsafe { asm!("mov rax, cr3", out("rax") cr3) }
    cr3
}
let cr3 = wasabi::x86::read_cr3(); 
println!("cr3 = {cr3:#p}");
hexdump(unsafe { &*cr3 });

今回、cr3 の値が 0xbfc01000 になっているので、このアドレスから 4KiB の バイト列が、起点となるページテーブルのデータに相当するわけです。ページテー ブルには、8 バイトを 1 エントリとして、次のレベルのページテーブルへのポイ ンタや、その属性(読み書き可能かなど)が記録されています

cr3 = 0x00000000bfc01000
00000000: 23 20 C0 BF 00 00 00 00 00 00 00 00 00 00 00 00 |# ..............|
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
(snip)

(16 bytes per line)

ページテー ブルには、8 バイトを 1 エントリとして、次のレベルのページテーブルへのポイ ンタや、その属性(読み書き可能かなど)が記録されています。

23 20 C0 BF 00 00 00 00
=> 0x00000000_BFC02023       // Interpret as little-endian
=> 0xBFC02000 | 0x23         // Extract the lower 8 bits
=> 0xBFC02000 | 0b0010_0011  // To binary
cr3 = 0x00000000bfc01000
(snip)
Some(L4Table @ 0x00000000bfc01000 {
 entry[  0] = L4Entry @ 0x00000000bfc01000 { 0x00000000BFC02023 PWS  }
}
(snip)
Some(L3Table @ 0x00000000bfc02000 {
 entry[  0] = L3Entry @ 0x00000000bfc02000 { 0x00000000BFC03023 PWS  }
(snip)
Some(L2Table @ 0x00000000bfc03000 {
 entry[  0] = L2Entry @ 0x00000000bfc03000 { 0x00000000000000E3 PWS  }
(snip)
None

For example, considering virtual address 0, we can see it becomes index 0 of the L4 table, index 0 of the L3 table, and index 0 of the L2 table, so in that case we can see it’s converted to physical address 0.

Interrupt Handling and Exception Handling

This is content we didn’t cover much in the previous article. We had roughly implemented page_fault_handler. It seems that verifying paging behavior also becomes easier.

割り込み番号 ごとの設定を書き並べたデータ構造のことを、x86_64 では割り込み記述子テー ブル(IDT:Interrupt Descriptor Table)と呼びます

OS は IDT(= IdtDescriptor の配列)をメモリ上に構築したあと、CPU に IDT の場所を知らせる必要があります。このために用いられる命令が LIDT(Load Interrupt Descriptor Table)です。この命令に IDT のベースアドレスとサイズを 記述したデータ構造 IdtrParameters へのポインタを渡すことで、CPU は以降 の例外や割り込みを処理する際に、ここで設定された IDT を参照するようにな ります。

もう一点、IDT を設定するのに先立って設定する必要のあるものが存在しま す。それが、GDT(Global Descriptor Table)です

Setting aside the historical flow, understand it as follows:

GDT の各エントリには、ベースアドレス、リミット(サイズ)、アクセス権限(読 み取り専用、書き込み可能、実行可能など)、そして特権レベルなどが格納され ています。セグメントレジスタは合計で 6 つ(CS、DS、ES、FS、GS、SS)存 在します

IST は 1 番から 7 番までの 7 つのスタックポインタを設定できる配列で、 TSS の中に存在します

・ 割り込みには要因に応じて異なる番号が割り当てられている(割り込み番号) ・ 割り込み番号をもとに IDT を参照して、CPU は割り込み処理を行う ・ IDT のエントリには、割り込みハンドラのアドレスとスタックの設定などが入っ ている ・ GDT は 32 ビット時代に作られたものだが、64 ビット時代でも TSS のために いまだに使われている ・ TSS の中には IST があり、割り込み処理でスタックを切り替える際に参照される

Page Table Implementation

pub fn init_paging(memory_map: &MemoryMapHolder) {
    let mut table = PML4::new();
    let mut end_of_mem = 0x1_0000_0000u64;
    for e in memory_map.iter() {
        match e.memory_type() {
            CONVENTIONAL_MEMORY | LOADER_CODE | LOADER_DATA => {
                end_of_mem = max(
                    end_of_mem,
                    e.physical_start() + e.number_of_pages() * (PAGE_SIZE as u64),
                )
            }
            _ => (),
        }
    }

    table
        .create_mapping(0, end_of_mem, 0, PageAttr::ReadWriteKernel)
        .expect("Failed to create initial page mapping");
    unsafe {
        write_cr3(Box::into_raw(table));
    }
}

Thoughts/Questions

.text      ← Code
.rdata     ← Read-only data
.data      ← Initialized data
.pdata     ← Exception handling table (for x64)
.reloc     ← Relocation information

This article is written by K.Waki

Software Engineer. English Learner. Opinions expressed here are mine alone.