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

はじめに

Part9でファイルシステムが動きました。ファイルが読み書きできると急にOSっぽい。

今回はELFローダーを実装します。これで外部プログラムが実行できるようになる!

カーネルの中にコードを書くんじゃなくて、別のプログラムを動かす。これができるとマジでOSって感じがする。

目次

  1. ELFフォーマットとは
  2. ELFヘッダーの解析
  3. プログラムヘッダーの処理
  4. メモリへのロード
  5. 実行
  6. 動作確認

ELFフォーマットとは

Executable and Linkable Format。LinuxやBSDで使われる実行ファイル形式。

Windows: .exe (PE形式)
Linux:   なし or .elf (ELF形式)
macOS:   なし (Mach-O形式)

ELFの構造:

+------------------+
| ELF Header       |  マジックナンバー、アーキテクチャ、エントリポイント
+------------------+
| Program Headers  |  ロード情報(メモリにどう配置するか)
+------------------+
| Section Headers  |  セクション情報(デバッグ用など)
+------------------+
| .text            |  コード
+------------------+
| .data            |  初期化済みデータ
+------------------+
| .bss             |  未初期化データ
+------------------+
| ...              |
+------------------+

実行に必要なのは主にELF HeaderProgram Headers

ELFヘッダーの解析

// src/elf/mod.rs
use alloc::vec::Vec;

pub const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F'];

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Elf64Header {
    pub magic: [u8; 4],
    pub class: u8,           // 1 = 32bit, 2 = 64bit
    pub endian: u8,          // 1 = little, 2 = big
    pub version: u8,
    pub os_abi: u8,
    pub _padding: [u8; 8],
    pub elf_type: u16,       // 1 = relocatable, 2 = executable, 3 = shared
    pub machine: u16,        // 0x3E = x86_64
    pub version2: u32,
    pub entry: u64,          // エントリポイント!
    pub phoff: u64,          // Program Header offset
    pub shoff: u64,          // Section Header offset
    pub flags: u32,
    pub ehsize: u16,
    pub phentsize: u16,
    pub phnum: u16,          // Program Header数
    pub shentsize: u16,
    pub shnum: u16,
    pub shstrndx: u16,
}

impl Elf64Header {
    pub fn parse(data: &[u8]) -> Option<&Self> {
        if data.len() < core::mem::size_of::<Self>() {
            return None;
        }
        
        let header = unsafe { &*(data.as_ptr() as *const Self) };
        
        // マジックナンバーチェック
        if header.magic != ELF_MAGIC {
            return None;
        }
        
        // 64bitチェック
        if header.class != 2 {
            return None;
        }
        
        // x86_64チェック
        if header.machine != 0x3E {
            return None;
        }
        
        Some(header)
    }
}

最初マジックナンバーの確認を忘れて、適当なファイルを読ませたら即死しました。

プログラムヘッダー

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Elf64ProgramHeader {
    pub p_type: u32,    // セグメントタイプ
    pub p_flags: u32,   // フラグ (R/W/X)
    pub p_offset: u64,  // ファイル内オフセット
    pub p_vaddr: u64,   // 仮想アドレス
    pub p_paddr: u64,   // 物理アドレス(通常無視)
    pub p_filesz: u64,  // ファイル内サイズ
    pub p_memsz: u64,   // メモリ上サイズ
    pub p_align: u64,   // アライメント
}

// セグメントタイプ
pub const PT_NULL: u32 = 0;
pub const PT_LOAD: u32 = 1;  // ← これが重要!

// フラグ
pub const PF_X: u32 = 1;  // 実行可能
pub const PF_W: u32 = 2;  // 書き込み可能
pub const PF_R: u32 = 4;  // 読み込み可能

impl Elf64ProgramHeader {
    pub fn parse_all(data: &[u8], header: &Elf64Header) -> Vec<&Self> {
        let mut headers = Vec::new();
        let offset = header.phoff as usize;
        let size = header.phentsize as usize;
        
        for i in 0..header.phnum as usize {
            let start = offset + i * size;
            if start + size <= data.len() {
                let ph = unsafe { &*(data[start..].as_ptr() as *const Self) };
                headers.push(ph);
            }
        }
        
        headers
    }
}

ELFローダー

// src/elf/loader.rs
use super::*;
use crate::memory;
use x86_64::{
    structures::paging::{
        Page, PageTableFlags, Mapper, FrameAllocator, Size4KiB,
    },
    VirtAddr,
};
use alloc::vec::Vec;

pub struct LoadedElf {
    pub entry_point: u64,
    pub pages: Vec<Page<Size4KiB>>,
}

pub fn load_elf(
    data: &[u8],
    mapper: &mut impl Mapper<Size4KiB>,
    frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> Result<LoadedElf, &'static str> {
    // ELFヘッダー解析
    let header = Elf64Header::parse(data)
        .ok_or("Invalid ELF header")?;
    
    serial_println!("ELF entry point: {:#x}", header.entry);
    
    let mut pages = Vec::new();
    
    // プログラムヘッダーを処理
    for ph in Elf64ProgramHeader::parse_all(data, header) {
        if ph.p_type != PT_LOAD {
            continue;  // PT_LOAD以外は無視
        }
        
        serial_println!(
            "Loading segment: vaddr={:#x}, filesz={:#x}, memsz={:#x}",
            ph.p_vaddr, ph.p_filesz, ph.p_memsz
        );
        
        // ページフラグを設定
        let mut flags = PageTableFlags::PRESENT | PageTableFlags::USER_ACCESSIBLE;
        if ph.p_flags & PF_W != 0 {
            flags |= PageTableFlags::WRITABLE;
        }
        // Note: x86_64では実行不可フラグ(NX)を使う
        
        // ページをマップ
        let start_page = Page::containing_address(VirtAddr::new(ph.p_vaddr));
        let end_page = Page::containing_address(VirtAddr::new(ph.p_vaddr + ph.p_memsz - 1));
        
        for page in Page::range_inclusive(start_page, end_page) {
            let frame = frame_allocator
                .allocate_frame()
                .ok_or("Failed to allocate frame")?;
            
            unsafe {
                mapper.map_to(page, frame, flags, frame_allocator)
                    .map_err(|_| "Failed to map page")?
                    .flush();
            }
            
            pages.push(page);
        }
        
        // データをコピー
        let dest = ph.p_vaddr as *mut u8;
        let src = &data[ph.p_offset as usize..][..ph.p_filesz as usize];
        unsafe {
            core::ptr::copy_nonoverlapping(src.as_ptr(), dest, src.len());
            
            // .bss領域をゼロ初期化
            if ph.p_memsz > ph.p_filesz {
                let bss_start = dest.add(ph.p_filesz as usize);
                let bss_size = (ph.p_memsz - ph.p_filesz) as usize;
                core::ptr::write_bytes(bss_start, 0, bss_size);
            }
        }
    }
    
    Ok(LoadedElf {
        entry_point: header.entry,
        pages,
    })
}

簡単なテストプログラム

カーネルと同じアドレス空間で動くシンプルなプログラムを作ります:

// test_app/src/main.rs
#![no_std]
#![no_main]

#[no_mangle]
pub extern "C" fn _start() -> ! {
    // システムコールでカーネルに処理を依頼...
    // 今回は単純にループ
    loop {}
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

...と思ったけど、これだとカーネルに結果を返せない。

簡易システムコール

// src/syscall.rs
pub const SYSCALL_PRINT: u64 = 1;
pub const SYSCALL_EXIT: u64 = 60;

pub fn handle_syscall(num: u64, arg1: u64, arg2: u64, arg3: u64) -> u64 {
    match num {
        SYSCALL_PRINT => {
            let ptr = arg1 as *const u8;
            let len = arg2 as usize;
            let slice = unsafe { core::slice::from_raw_parts(ptr, len) };
            if let Ok(s) = core::str::from_utf8(slice) {
                crate::println!("{}", s);
            }
            0
        }
        SYSCALL_EXIT => {
            serial_println!("App exited with code: {}", arg1);
            0
        }
        _ => {
            serial_println!("Unknown syscall: {}", num);
            u64::MAX
        }
    }
}

関数ポインタで実行

本当のOSではユーザーモードに切り替えて実行しますが、今回は簡易的に関数ポインタでジャンプ:

pub fn execute_elf(loaded: &LoadedElf) -> i32 {
    serial_println!("Jumping to entry point: {:#x}", loaded.entry_point);
    
    // エントリポイントを関数ポインタとして呼び出し
    let entry: extern "C" fn() -> i32 = unsafe {
        core::mem::transmute(loaded.entry_point)
    };
    
    entry()
}

注意: これは超危険!本当のOSではRing 3に切り替えてからでないとセキュリティ的にダメ。

テスト用のバイナリを組み込み

ファイルシステムに実行ファイルを入れておく:

// src/fs/simple_fs.rs
impl SimpleFs {
    pub fn new() -> Self {
        let mut fs = SimpleFs {
            files: BTreeMap::new(),
        };
        
        // 組み込みテストプログラム(実際にはビルドしたELF)
        fs.files.insert("/bin/hello".to_string(), include_bytes!("../../test_app.elf").to_vec());
        
        fs
    }
}

実際にハマった話

1. アドレスの問題

ELFファイルが想定するアドレスと実際のメモリ配置が違うと死ぬ:

ELF: "俺は0x400000にロードされたい"
OS: "お前には0x800000をやる"
ELF: *絶対アドレスでジャンプ* → 死

解決策:

  • Position Independent Executable (PIE) でビルドする
  • または要求通りのアドレスにマップする

今回は要求通りにマップする方式で。

2. メモリ保護の問題

ロードしたコード: "書き込みしたい"
ページテーブル: "WRITABLEフラグないよ"
→ ページフォルト

PF_WフラグをちゃんとチェックしてWRITABLEを設定するようにした。

3. .bssセクション

static mut COUNTER: u32 = 0;  // .bss に配置される

これ、ファイルサイズ(p_filesz)は0だけど、メモリサイズ(p_memsz)はある。
コピーするデータはないけど、ゼロ初期化は必要

最初これを忘れて、.bssに謎のゴミが入って苦しんだ。

動作確認

実際にELFを読み込んで実行:

fn kernel_main(boot_info: &'static BootInfo) -> ! {
    // ... 初期化 ...
    
    println!("=== ELF Loader Demo ===");
    
    if let Some(elf_data) = fs::read_file("/bin/hello") {
        println!("Loaded /bin/hello ({} bytes)", elf_data.len());
        
        match elf::loader::load_elf(&elf_data, &mut mapper, &mut frame_allocator) {
            Ok(loaded) => {
                println!("Entry point: {:#x}", loaded.entry_point);
                let result = elf::loader::execute_elf(&loaded);
                println!("Program returned: {}", result);
            }
            Err(e) => println!("Failed to load ELF: {}", e),
        }
    } else {
        println!("/bin/hello not found");
    }
    
    // ...
}
=== ELF Loader Demo ===
Loaded /bin/hello (4096 bytes)
Loading segment: vaddr=0x400000, filesz=0x1a0, memsz=0x1a0
Entry point: 0x401000
Program returned: 42

プログラムが動いた!!!

これで関数が返した42が取れてる。外部プログラムが実行できるOSになった!

まとめ

Part10では以下を達成しました:

  • ELFフォーマットの理解
  • ELFヘッダー・プログラムヘッダーの解析
  • セグメントのメモリへのロード
  • 簡易システムコール
  • エントリポイントへのジャンプ

外部プログラムが動いた瞬間、めちゃくちゃ感動した。これでOSと言えるレベルになってきた!

次回(Part11)はシェルを実装します。コマンドを打ってプログラムを実行できるようにする!

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

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?