FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

プログラム中に埋め込んだ機械語命令を実行するプログラムをRustで書く

何を言っているのだかわからないかもしれないが、これはつまりQEMUのまねごとをRustで実現したいという話。 QEMUではTCGによって変換されたゲストの機械語をホストに変換する際、TCG機械語に変換したものをメモリ上に配置し、これを機械語とみなして実行するという猛烈な荒業をやってのける。

  • qemu/include/tcg/tcg.h
#ifdef HAVE_TCG_QEMU_TB_EXEC
uintptr_t tcg_qemu_tb_exec(CPUArchState *env, uint8_t *tb_ptr);
#else
# define tcg_qemu_tb_exec(env, tb_ptr) \
    ((uintptr_t (*)(void *, void *))tcg_ctx->code_gen_prologue)(env, tb_ptr)
#endif

上記のプログラムがQEMUTCG実行の最もキモとなる部分である。このcode_gen_prologueのメモリ領域にTCGから変換された機械語が積み上げられており、これを実行することでホストの機械語がそのまま実行される。

  • qemu/tcg/i386/tcg-target.inc.c
/* Generate global QEMU prologue and epilogue code */
static void tcg_target_qemu_prologue(TCGContext *s)
{
    int i, stack_addend;

...
    /* Reserve some stack space, also for TCG temps.  */
    stack_addend = FRAME_SIZE - PUSH_SIZE;
    tcg_set_frame(s, TCG_REG_CALL_STACK, TCG_STATIC_CALL_ARGS_SIZE,
                  CPU_TEMP_BUF_NLONGS * sizeof(long));

    /* Save all callee saved registers.  */
    for (i = 0; i < ARRAY_SIZE(tcg_target_callee_save_regs); i++) {
        tcg_out_push(s, tcg_target_callee_save_regs[i]);
    }

#if TCG_TARGET_REG_BITS == 32
    tcg_out_ld(s, TCG_TYPE_PTR, TCG_AREG0, TCG_REG_ESP,
...

このカラクリは、メモリ領域にデータを置いて、実行許可を与えてしまえばすべてプログラムになりうるという非常に単純な原理によって実現されている。分かってしまえば単純な方式だ。

では、これをRustで実現したい。このためにはどのようにすればよいのだろう?

まず検索してみると、以下のようなStackOverFlowが見つかった。

stackoverflow.com

なるほど、mmapを使えば実現できそうだ。ファイルとして機械語を表現するかどうかはともかく、メモリ領域を確保してそこに機械語を埋め込めば、その領域のプログラムを実行できるはずだ。

unsafe fn reflect(instructions: &[u8]) {

    let map = match MemoryMap::new(
        instructions.len(),
        &[
            // MapOption::MapAddr(0 as *mut u8),
            // MapOption::MapOffset(0),
            // MapOption::MapFd(fd),
            MapOption::MapReadable,
            MapOption::MapWritable,
            MapOption::MapExecutable,
            // MapOption::MapNonStandardFlags(libc::MAP_ANON),
            // MapOption::MapNonStandardFlags(libc::MAP_PRIVATE),
        ],
    ) {
        Ok(m) => m,
        Err(e) => panic!("Error: {}", e),
    };

    std::ptr::copy(instructions.as_ptr(), map.data(), instructions.len());

    let func: unsafe extern "C" fn() -> u8 = mem::transmute(map.data());

    let ans = func();
    println!("ans = {:x}", ans);
}

map変数により命令を格納するメモリ領域を確保し、copyにより命令をコピーする。そしてfunc()としてこれをC言語の関数として取り扱うらしい?これを実行するという訳だ。

試しに、命令として戻り値レジスタraxに即値を与えて戻るだけの命令列を渡してみよう。

fn main() {
    let shellcode: [u8; 8] = [
        0x48, 0x83, 0xc7, 0x0a,  // add 0xa, %rdi
        0x48, 0x89, 0xf8,        // mov %rdi, %rax
        0xc3];                   // retq
    unsafe {
        reflect(&shellcode);
    }
}
f:id:msyksphinz:20200813235637p:plain

8ビット8個で構成されるx86の命令を渡した。即値0xaをraxに渡して、戻るだけである。これを実行してみよう。

$ cargo run
ans = a

お、いいね。戻り値が正しく取得できた。