Open10

QEMUもどきをRustで作る

流星 彗流星 彗

皆様はじめまして。今夜は星が見えますか?流星彗と申します。ながほしすいと読みます。

さて、今回はQEMUと呼ばれる軽量かつ万能なオープンソースエミュレータのパチモンRustバージョンを作っていきます。

以下のリポジトリにて開発を進めていきます。
https://github.com/SuiNagahoshi/remu

流星 彗流星 彗

QEMUとは

QEMUはFabrice Bellard氏を中心に開発されている、フリーでオープンソースなエミュレータです。
QEMUは多種多様なプロセッサ環境のエミュレーションが可能で、Arm、MIPS、PowerPC、RISC-V、s390x、SPARC、x86などのアーキテクチャをサポートしています。フルシステムエミュレーションの他にLinuxのユーザランドも独立してエミュレーションすることができます。
CPUの他にもPS/2、SATA、USB、サウンドカード、フロッピーディスク等のデバイスをエミュレートできます。
また、ホスト上のGDBと接続、仮想マシンの監視などICEのような使い方や埋め込みVNC、SPICEサーバによりリモートマシンの制御も可能です。
一方で仮想化支援機能が少なく、VMwareやVirtualBoxよりは低速です。


公式サイト
https://www.qemu.org/

流星 彗流星 彗

開発方針のようなもの

いきなりQEMUのような機能をもたせることはできないので、まずはTD4をエミュレートできるようにします。
TD4は名著「CPUの創りかた」で作成する4bit CPUです。ちなみにTD4は「とりあえず動作する4bit CPU」の略だそうです。

TD4の次はこちらも名著「30日でできる!OS自作入門」で作る「はりぼてOS」が動作するようにします。はりぼてOSはその名前からはかけ離れた高機能なOSで、マルチウィンドウ、ファイルシステム、インベーダーゲームなど一通りの機能は揃っています。

その次はいよいよ第三形態として、またまた名著「ゼロからのOS自作入門」で作る「MikanOS」が動作するようにします。MikanOSははりぼてOSの上位互換に近い自作OSで64bit動作、UEFIブート、USB3.0などのより近代的な機能を備えています。

最後に、いよいよ終わりのない戦いが始まります。ネットワーク、SATA、GPUなどのドライバ関連の機能、有名どころのプロセッサ環境のエミュレート、などなどだんだんと実機に近づけていきます。

どこまで続けられるか自信はありませんが、応援お願いします。

流星 彗流星 彗

ファイル読み込み

実行時に引数で渡されたパスのファイルを読み込み、println!でストリームに流す機能を実装しました。
BufReaderでバッファを取り、行ごとにベクタへ足していき、最後にfor inループで要素ごと(一行ごと)に表示します。
コード全文は以下になります。

main.rs
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.len() < 2 {
        panic!("ERROR: Invalid args.");
    }

    let file = BufReader::new(File::open(args.get(1).unwrap()).expect("ERROR: File not found."));
    let operations = file
        .lines()
        .map(|line| line.unwrap())
        .collect::<Vec<String>>();
    for i in &operations {
        println!("{}", i);
    }
}

流星 彗流星 彗

IO

入出力関連ということでIO、いわゆるInput、Outputを実装しました。
実装といっても至ってシンプルで、Portという名前の構造体を用意し、受け取った値を突っ込んで返すだけです。たぶん。
コードは以下

io.rs
pub struct Port {
    input: u8,
    output: u8,
}

impl Port {
    pub fn new(input: u8, output: u8) -> Self {
        Self { input, output }
    }

    pub fn input(&self) -> u8 {
        self.input
    }

    pub fn output(&self) -> u8 {
        self.output
    }

    pub fn set_output(&mut self, im: u8) {
        self.output = im;
    }
}


書き忘れていましたが、TD4エミュレータの作成は下記のサイトを参考に、ほぼそのままの形で実装させていただいています。
https://blog-dry.com/entry/2020/12/25/194511

流星 彗流星 彗

ROM

続いて実装が単純なものその2として、ROMの実装を行いました。
バイナリの並んだベクタ内u8 as usize位置にある4bitのバイナリデータを返すEom::read関数、バイナリ長を返すRom::size関数があります。
コードは以下

rom.rs
pub struct Rom {
    pub memory_array: Vec<u8>,
}

impl Rom {
    pub fn new(memory_array: Vec<u8>) -> Self {
        Self { memory_array }
    }

    pub fn read(&self, program_counter: u8) -> u8 {
        self.memory_array[program_counter as usize]
    }

    pub fn size(&self) -> u8 {
        self.memory_array.len() as u8
    }
}
流星 彗流星 彗

Opcode & Register

ある意味最も重要とも言える、OpcodeRegisterを実装しました。
こちらもやはりそこまで複雑ではなく、enumstructでまとめて、値を代入したり、返したりする関数を用意するだけです。
Opcodeに関しては、enumの中に命令名と対応するバイナリを並べているだけで、関数などは一切ありません。

opcode.rs
use num_derive::FromPrimitive;

#[derive(Debug, PartialEq, FromPrimitive)]
pub enum Opcode {
    AddA = 0b0000,
    AddB = 0b0101,
    MovA = 0b0011,
    MovB = 0b0111,
    MovA2B = 0b0001,
    MovB2A = 0b0100,
    Jmp = 0b1111,
    Jnc = 0b1110,
    InA = 0b0010,
    InB = 0b0110,
    OutB = 0b1001,
    OutIm = 0b1011,
}
register.rs
pub struct Register {
    register_a: u8,
    register_b: u8,
    carry_flag: u8,
    program_counter: u8,
}

impl Default for Register {
    fn default() -> Self {
        Self {
            register_a: u8::default(),
            register_b: u8::default(),
            carry_flag: u8::default(),
            program_counter: u8::default(),
        }
    }
}

impl Register {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn program_counter(&self) -> u8 {
        self.program_counter
    }

    pub fn set_program_counter(&mut self, new_value: u8) {
        self.program_counter = new_value;
    }

    pub fn increment_program_counter(&mut self) {
        self.program_counter += 1;
    //以下略
流星 彗流星 彗

Emulator

いよいよエミュレータ本体を作成します。
RegisterPortRomの入った構造体を作り、implでくっつけていきます。
バイナリをデコードするfn decode、デコードされた命令をもとにエミュレータの命令を呼ぶfn exec、そして各命令本体があります。
コードは以下(一部抜粋)

emulator.rs
pub struct CpuEmulator {
    register: Register,
    port: Port,
    rom: Rom,
}

impl CpuEmulator {
    pub fn with(register: Register, port: Port, rom: Rom) -> Self {
        assert!(
            rom.size() <= 16,
            "Maximum memory size is 16. This program can't work."
        );
        Self {
            register,
            port,
            rom,
        }
    }
//~~~
    fn decode(&self, data: u8) -> Result<(Opcode, u8), EmulatorError> {
        let opelation = data >> 4;
        let immediate = data & 0x0f;

        if let Some(opcode) = FromPrimitive::from_u8(opelation) {
            match opcode {
                Opcode::AddA
                | Opcode::AddB
                | Opcode::MovA
                | Opcode::MovB
                | Opcode::MovA2B
                | Opcode::MovB2A
                | Opcode::Jmp
                | Opcode::Jnc
                | Opcode::OutIm => Ok((opcode, immediate)),
                Opcode::InA | Opcode::InB | Opcode::OutB => Ok((opcode, 0)),
            }
        } else {
            Err(EmulatorError::new("No match for opcode."))
        }
    }
    pub fn exec(&mut self) -> Result<(), EmulatorError> {
        loop {
            let data = self.fetch();
            let (opcode, immediate) = self.decode(data)?;

            match opcode {
                Opcode::MovA => self.mov_a(immediate),
                Opcode::MovB => self.mov_b(immediate),
                Opcode::AddA => self.add_a(immediate),
                Opcode::AddB => self.add_b(immediate),
                Opcode::MovA2B => self.mov_a2b(),
                Opcode::MovB2A => self.mov_b2a(),
                Opcode::Jmp => self.jmp(immediate),
                Opcode::Jnc => self.jnc(immediate),
                Opcode::InA => self.in_a(),
                Opcode::InB => self.in_b(),
                Opcode::OutB => self.out_b(),
                Opcode::OutIm => self.out_immedilate(immediate),
            };

            if opcode != Opcode::Jmp && opcode != Opcode::Jnc {
                self.register.increment_program_counter();
            }

            if self.does_halt() {
                return Ok(());
            }
        }
    }
    fn mov_a(&mut self, immediate: u8) {
        self.register.set_register_a(immediate);
        self.register.set_carry_flag(0);
    }
//~~~
流星 彗流星 彗

あとはバイナリに対応すれば完成・・・なのですが、私の技術ではバイナリを4bitでsplitする事ができなかったので、参考サイトに載っているコンパイラを実装しました。
多分バイナリを二進数が記述されたテキストデータとして強引に読めば行けそうな気がするので、時をみて試してみます。

さて、コンパイラとは言いますが、仕組み自体は非常にシンプルで、例えばmov A 0001といった簡易的なアセンブラをwhitespaceで分割してやると命令->レジスタ->といった順で解釈できるようになります。

その後generate_binary_codeなどの関数が、各命令のバイナリとイミディエイトデータを結合し、u8のベクタに詰めていく、といった流れで動作します。

parser.rs
//~~~
let operation = operation.unwrap();

            if operation == "mov" {
                self.position += 1;
                let lhs = self
                    .source
                    .get(self.position)
                    .ok_or_else(|| EmulatorError::new("Failed to parse mov left hand"))?;

                self.position += 1;

                let rhs = self
                    .source
                    .get(self.position)
                    .ok_or_else(|| EmulatorError::new("Failed to parse mov right hand"))?;

                let token = if lhs == "B" && rhs == "A" {
                    Token::MovBA
                } else if lhs == "A" && rhs == "B" {
                    Token::MovAB
                } else {
                    Token::Mov(
                        Register::from(lhs.to_string()),
                        self.from_binary_to_decimal(rhs)?,
                    )
                };

                result.push(token);
            }
//~~~
fn from_binary_to_decimal(&self, text: impl Into<String>) -> Result<u8, EmulatorError> {
        let ret = text.into();
        let binary_to_decimal = u8::from_str_radix(&ret, 2);
        binary_to_decimal
            .map_err(|_| EmulatorError::new(&format!("Failed to parse string: {}", ret)))
    }
//~~~
compiler.rs
//~~~
for token in tokens {
            let program = match token {
                Token::Mov(Register::A, immediate) => self.generate_binary_code(0b0011, immediate),
                Token::Mov(Register::B, immediate) => self.generate_binary_code(0b0111, immediate),
                Token::MovAB => self.generate_binary_code_with_zero_padding(0b0001),
                Token::MovBA => self.generate_binary_code_with_zero_padding(0b0100),
                Token::Add(Register::A, immediate) => self.generate_binary_code(0b0000, immediate),
                Token::Add(Register::B, immediate) => self.generate_binary_code(0b0101, immediate),
                Token::Jmp(immediate) => self.generate_binary_code(0b1111, immediate),
                Token::Jnc(immediate) => self.generate_binary_code(0b1110, immediate),
                Token::In(Register::A) => self.generate_binary_code_with_zero_padding(0b0010),
                Token::In(Register::B) => self.generate_binary_code_with_zero_padding(0b0110),
                Token::OutB => self.generate_binary_code_with_zero_padding(0b1001),
                Token::OutIm(immediate) => self.generate_binary_code(0b1011, immediate),
            };
            result.push(program);
        }

        Ok(result)
    }

    fn generate_binary_code(&self, operation: u8, immediate: u8) -> u8 {
        let shift_operation = operation << 4;
        let shift_data = immediate & 0x0f;
        shift_operation | shift_data
    }
//~~~
流星 彗流星 彗

これでひとまずTD4エミュレータは実装できました。
簡易アセンブラを書いたファイルを引数に渡して実行すると、中間報告とともに実行結果をしゅつりょくしてくれます。

~~~
    Finished dev [unoptimized + debuginfo] target(s) in 50.54s
     Running `target\debug\remu.exe examples/test/test.txt`
["mov A 0001", "add A 0001", "mov B A", "out B"]
["mov", "A", "0001"]
["add", "A", "0001"]
["mov", "B", "A"]
["out", "B"]
[Mov(A, 1), Add(A, 1), MovBA, OutB]
[49, 1, 64, 144]
Port B Output: 2

ちゃんと1+1の計算結果である2が出力されています。
これにて第一フェーズTD4エミュレータの実装はおしまいです。
次回からははりぼてOSエミュレータ編となります。
終りが見えない笑


大変お世話になった参考サイト
https://blog-dry.com/entry/2020/12/25/194511
リポジトリ
https://github.com/yuk1ty/cpu-4bit-emulator

本スクラップにて開発中のrust製エミュレータ、remu
https://github.com/SuiNagahoshi/remu/tree/TD4-phase