LoginSignup
14
6

簡易x86エミュレータを再実装してRustを学ぶ

Last updated at Posted at 2021-12-05

はじめに

プログラミング言語のRustを学びたいと思い,今年はいくつか本を読んだり1The Rust Programming Languageの輪講会を催したりした.ところがそこまで学んでいても,Rustでは強力な型システムと所有権システムがあるため,初学者の私にはコンパイルすら一筋縄では行かなかった.

そこでCのプログラムをRustで再実装し,Rustに慣れることとした.自作エミュレータで学ぶx86アーキテクチャのx86エミュレータpx86はCで実装されているが,これを新たにRustで実装することした.px86を選んだ理由は一度Linuxで実装した経験があるため2,またコンパクトな規模の実装であるから,題材として適切と考えた.

本記事では,簡易x86エミュレータをCからRustに実装し直す上で困ったことを取り上げ,どのように対処したかを示す.

px86:簡易x86エミュレータ

今回実装したpx86は,i386の基本的な命令セットをサポートする命令レベルシミュレータである3.x86の命令が記述されたrawファイルを入力として受け取り,実行する命令のアドレスと最初の1バイトを標準出力に出力する.

エミュレータの構成を下記に示す.32ビットの汎用レジスタ8本,プログラムカウンタ,フラグレジスタを持つ.サポートする機能は,いくつかの加減算命令,分岐命令とジャンプ命令,関数呼出しに関する命令,入出力命令である.また1Mバイトのメインメモリを持ち,入力されたプログラムを実行開始時にロードする.

詳細は自作エミュレータで学ぶx86アーキテクチャを読んでいただきたい.

px86のアーキテクチャ

Rustで実装した際に困ったこと

オーバーフローでパニックになる

px86はEIPが0の場合,即ち次に実行する命令のアドレスが0番地の場合に,エミュレーションを終了する仕様である.これはエミュレーションをプログラム側で終了させるために導入された仕様であり,書籍のサンプルプログラムでは必ず最後に0番地へジャンプする.0番地へのジャンプ命令を実行する際は,下記の計算が行われる.

EIP <- EIP + <relative address> + 5

0番地へのジャンプをする場合,この計算を行うとオーバーフローが発生する.C言語での実装では問題なくEIPが0となるが,RustではDebugビルドしたプログラムでオーバーフローが発生するとパニックとなる

これを防ぎ,C言語での実装と同様にオーバーフローを許すにはwrapping_*関数を使う.wrapping_addはモジュロ加算を実装しており,オーバーフローによるパニックが発生しない.wrapping_addによる加算処理を下記に示す.

wrapping_addによる加算処理
let result = 0xFFFFFFFFu32.wrapping_add(1u32); // これはpanicにならない
// let result = 0xFFFFFFFFu32 + 1u32; // これはpanicになる

無名共用体を使えない

C言語実装のpx86では無名共用体を使った実装をしていた.下記にModRM構造体の定義を示す.opecodeとdisp8は構造体のブロック内で無名共用体を定義している.

C言語版ModRM構造体の定義
/* ModR/Mを表す構造体 */
typedef struct {
    uint8_t mod;

    /* opecodeとreg_indexは別名で同じ物 */
    union {
        uint8_t opecode;
        uint8_t reg_index;
    };  

    uint8_t rm; 

    /* SIB が必要な mod/rm の組み合わせの時に使う */
    uint8_t sib;

    union {
        int8_t disp8; // disp8 は符号付き整数
        uint32_t disp32;
    };  
} ModRM;

ところがRustでは無名共用体を使うことができず,また構造体のブロック内で定義することもできない.よって共用体を個別に定義することとなる.Rust実装を下記に示す.少し不便だが,今のところこのような定義方法しか無い.

Rust版ModRM構造体の定義
pub union OpeReg {
    pub opecode: u8,
    reg_index: u8,
}

pub union Disp {
    disp8: i8,
    disp32: u32,
}

pub struct ModRM {
    pub m: u8,
    pub opereg: OpeReg,
    rm: u8,
    sib: u8,
    disp: Disp,
}

共用体が不便

Rustで共用体を使うと,メモリ安全性の恩恵を受けられなくなる.ModRM構造体を例に取ると,C言語で共用体のメンバにアクセスする場合はobject.opecodeと記述する.対してRustでは共用体へのアクセスがunsafeであるため,下記のようなアクセス方法となる.共用体を使うとメモリ安全なコードではなくなり,またunsafeを適宜挿入する必要が出てくるため,C言語よりも不便さを感じる.

共用体へのアクセス
let value = unsafe { modrm.disp.disp32 };

この問題の対処案として,今回のような単純な共用体であれば,ある型の値を別の型の値に変換するような関連関数を定義する方法が考えられる.下記にModRM構造体のdispを返す関数を示す.メンバ変数dispに対して,read_disp8read_disp32という関数を定義した.参照したい型によって関数を使い分けることで,safeなRustコードだけでコーディングすることができる.

Disp用の関連関数によるメモリ安全なコード
pub struct ModRM {
    pub m: u8,
    pub opereg: OpeReg,
    rm: u8,
    sib: u8,
    disp: u32,
}

impl ModRM {
    fn read_disp8(&self) -> u8 {
        (disp & 0xFF) as u8
    }
    fn read_disp32(&self) -> u32 {
        disp
    }
}

safeなRustコードだけで任意のアドレスから動的にオブジェクトを参照することが難しい

C言語では,メモリ上に任意の型のオブジェクトがあるものとしてアクセスができた.Cにおける動的な参照例を下記に示す.下記の三関数はいずれもconst char*型を引数に持つが,内部ではそれぞれ別の型へのポインタとしてメモリを参照している.

C言語における動的な参照
static void PrintDB(const char *str) {
  printf("    db %d\n", *(uint8_t *)str);
}

static void PrintDW(const char *str) {
  printf("    dw %d\n", *(uint16_t *)str);
}

static void PrintDD(const char *str) {
  printf("    dd %d\n", *(uint32_t *)str);
}

Rustでもこのようなアクセスが可能だが,前節と同様にメモリ安全性を保証できずコードとなる.

byteorderクレートはそのようなアクセスをラップしており,コーダは安全なRustコードの範疇での実装が可能となる.byteorderクレートを使った参照例を下記に示す.EnumLittleEndianの関連関数read_*はリトルエンディアンで値を読むのに使うことができる.x86はリトルエンディアン方式を採用しているため,LittleEndianを介して命令とデータを参照する.

byteorderクレートを使用したアクセス
fn print_bpb_structure(buffer: &[u8; FILE_SIZE]) {
    println!("{:<20} {:02x} {:02x} {:02x}", "BS_jmpBoot:", buffer[0] as u8, buffer[1] as u8, buffer[2] as u8);

    print!("{:<20} ", "BS_OEMName:");
    for i in 0..8 {
        print!("{}", buffer[i + 3] as char);
    }
    print!("\n");

    println!("{:<20} {}", "BPB_BytsPerSec:", LittleEndian::read_u16(&buffer[11..]));
    println!("{:<20} {}", "BPB_SecPerClus:", buffer[13] as u8);
    println!("{:<20} {}", "BPB_RsvdSecCnt:", LittleEndian::read_u16(&buffer[14..]));
    println!("{:<20} {}", "BPB_NumFATs:", buffer[16] as u8);
    println!("{:<20} {}", "BPB_RootEntCnt:", LittleEndian::read_u16(&buffer[17..]));
    println!("{:<20} {}", "BPB_TotSec16:", LittleEndian::read_u16(&buffer[19..]));
    println!("{:<20} {:02x}", "BPB_Media:", buffer[21] as u8);
    println!("{:<20} {}", "BPB_FATSz16:", LittleEndian::read_u16(&buffer[22..]));
    println!("{:<20} {}", "BPB_SecPerTrk:", LittleEndian::read_u16(&buffer[24..]));
    println!("{:<20} {}", "BPB_NumHeads:", LittleEndian::read_u16(&buffer[26..]));
    println!("{:<20} {}", "BPB_HiddSec:", LittleEndian::read_u32(&buffer[28..]));
    println!("{:<20} {}", "BPB_TotSec32:", LittleEndian::read_u32(&buffer[32..]));
}

おわりに

px86をRustで実装する中で,Rust特有の困ったことに直面した.しかしこれらの困ったことはRustが想定外の挙動(オーバーフローや安全でないメモリの参照)からプログラマを守るために導入された機能であることが分かった.またこれらの困ったことは,C言語等とは異なる方法や便利なクレートで解消できることが分かった.

今回,Rustで実装したpx86はGitHubで公開した.Rustの初学者とx86について興味がある方の一助になれば幸いである.

参考

  • d0iasm/x86emu: 低レイヤガールでお馴染みのd0iasmさんによるpx86のRust実装.私の実装よりもコンパクトであるため,とても参考になる.
  • x86エミュレータをRustで実装するログ: こちらは少しずつRustで実装し,その際にあった問題を随時紹介している.こちらはハマりどころがよく分かる.
  1. 実践Rust入門実践Rustプログラミング入門がとても参考になった.

  2. 以前のポスト(『自作エミュレータで学ぶx86アーキテクチャ』をManjaro(Linux)で学ぶ)を参照のこと.

  3. 書籍の名前にはエミュレータとあるため,本ポストでは一貫してエミュレータとする.

14
6
1

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
14
6