エミュレータ開発日記 in Rust~ELFローダ編①~

ここ半年ほど、RustでCPUエミュレータを書いています。 今更ですが、これも記録に残していこうと思います。

誰かの役に立つかもしれないですし。

今は、ELFローダを作っています。 これまでは、objcopyコマンドでraw binaryを作成し、特定アドレスにマッピングする、という実装で動いていました。 エミュレータのテストで、ELFファイルを直接動かした方が楽な状況になったので、ELFローダを作ることにしました。

ELFを解析するcrateは既に存在しますが、ELFを解析するプログラムを作ったことがないため、自作します。 何かを理解したい時には、フルスクラッチで作る、そして、文書を書く、これが一番です。

当面の目標

readelfで得られるヘッダ情報をエミュレータ内で扱えるようにバイナリをパースしていきます。 特にエントリポイントと、プログラムヘッダ情報を読み出すあたりが重要です。

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x80000000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          8692 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         6
  Section header string table index: 5

はじめの一歩

ELF Identification

ELF形式のファイルの先頭には、ELF Identificationという16バイトのデータが存在します。 先ほど示したヘッダ内の先頭にある部分です。

  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0

OS/ABIがUNIX - System Vとなっています。OS/ABIのフィールドが0x00の場合、ELFOSABI_NONEまたはELFOSABI_SYSVです。 今回は、ベアメタルで動作するバイナリを作成しているため、ELFOSABI_NONEと解釈すると良さそうです。

ということで、とりあえず使用する定数を定義しておきます。

/// 0x7f 'E' 'L' 'F'
const HEADER_MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];
const SIZE_ELF_IDENT: usize = 16;

const ELF_CLASS_32: u8 = 1;
const ELF_DATA_LSB: u8 = 1;
const EV_CURRENT: u8 = 1;
const ELF_OS_ABI_NONE: u8 = 0;

ELF magic

ELF形式のファイルは、`0x7f454c46'というマジックナンバーから始まります。 適当なELFバイナリ(test-elfとしています)を用意して、マジックナンバーが読み込めるテストからスタートします。

とりあえず、ファイル名を引数に、ElfLoaderのオブジェクトを作成し、ELF形式のファイルかどうか、を確認できるような作りで考えてみます。

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn is_elf() {
        let loader = ElfLoader::try_new("test-elf").unwrap();

        assert!(loader.is_elf(), "target file is not elf binary");
    }
}

ElfLoaderは次のようにしました。 try_newでは、ファイルを開いて、mmapしています。 newではなく、try_newとしているのは、clippyさんに怒られるからです。 clippyさん的には、newでResultを返すのはお気に召さないようです。

warning: methods called `new` usually return `Self`

is_elfでは、mmapした領域の先頭4バイトのデータが、0x7f454c45になっているか、を確認します。

use std::fs::File;
use memmap::Mmap;

// 0x7f 'E' 'L' 'F'
const HEADER_MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];

pub struct ElfLoader {
    mapped_file: Mmap,
}

impl ElfLoader {
    pub fn try_new(file_path: &str) -> std::io::Result<ElfLoader> {
        let file = File::open(&file_path)?;
        Ok(ElfLoader {
            mapped_file: unsafe { Mmap::map(&file)? },
        })
    }

    fn is_elf(&self) -> bool {
        self.mapped_file[0..4] == elf::HEADER_MAGIC
    }
}

memmap crateのMmap::map()はunsafeな関数です。 リテラシの高いRustプログラマを目指す私としては、unsafeがなぜunsafeなのか、はたまたsafeなのか、をきちんと書きたいところです。 unsafeな理由は、下記のsafetyに記述されています。

github.com

/// ## Safety
///
/// All file-backed memory map constructors are marked `unsafe` because of the potential for
/// *Undefined Behavior* (UB) using the map if the underlying file is subsequently modified, in or
/// out of process. Applications must consider the risk and take appropriate precautions when
/// using file-backed maps. Solutions such as file permissions, locks or process-private (e.g.
/// unlinked) files exist but are platform specific and limited.

プロセス内外でファイルを書き換えられると未定義動作となる、とあります。 しっかりと作るのであれば、ファイルをロックするオプションを指定して、mmapする必要がありそうですね。

後、適当なELF形式以外のファイルを用意して、同じテストが失敗することを確認しておきました。 これで、ELF magicを読みだすところまで作ることができました。

ElfIdentification struct

次に、ELF Identificationをstructで管理するようにします。 愚直に行きます。

/// File identification in elf header.
struct ElfIdentification {
    magic: [u8; 4],
    class: u8,
    endianess: u8,
    version: u8,
    os_abi: u8,
    os_abi_version: u8,
    reserved: [u8; 7], // zero filled.
}

impl ElfIdentification {
    // assumption: `binary` has enough length to read elf identification.
    fn new(binary: &[u8]) -> ElfIdentification {
        let mut magic: [u8; 4] = [0; 4];
        for (i, b) in binary[0..4].iter().enumerate() {
            magic[i] = *b;
        }
        ElfIdentification {
            magic,
            class: binary[4],
            endianess: binary[5],
            version: binary[6],
            os_abi: binary[7],
            os_abi_version: binary[8],
            reserved: [0; 7],
        }
    }
}

ちゃんと読み込めているかどうか、テストしておきましょう。

    // Check the ELF identification is as bellow:
    //   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    //   Class:                             ELF32
    //   Data:                              2's complement, little endian
    //   Version:                           1 (current)
    //   OS/ABI:                            UNIX - System V
    #[test]
    fn elf_identification() {
        let file = File::open("test-elf").unwrap();
        let mapped_file = unsafe { Mmap::map(&file).unwrap() };
        let identification = ElfIdentification::new(&mapped_file);

        assert_eq!(ELF_CLASS_32, identification.class);
        assert_eq!(ELF_DATA_LSB, identification.endianess);
        assert_eq!(EV_CURRENT, identification.version);
        assert_eq!(ELF_OS_ABI_NONE, identification.os_abi);
    }

このテストは通りました。これで、ELF Identificationの読み込みが完成です!

参考