FPGA開発日記

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

Binary Translation型エミュレータを作る(x86命令ディスアセンブルのサポート)

Binary Translation方式の命令セットエミュレータのRust実装、ある程度進んできたが、どんどん複雑なテストパタンを確認していかなければならない。デバッグ機能についてだが、ホスト命令(つまりx86命令)のディスアセンブル機能を実装したい。つまり、変換後の命令に対してディスアセンブラを適用し、どのような命令に変換されたのかを確認する機能だ。

これには、iced-x86というクレートを活用することにした。

github.com

https://crates.io/crates/iced-x86

使い方については、ほとんどREADMEに追従するような形で使用した。まあ変換後の命令を渡してそのままstdoutに出力してほしいだけなので簡単だ。

                TCGX86::tcg_gen(&self, pc_address, tcg, &mut mc_byte);
                for be in &mc_byte {
                    let be_data = *be;
                    self.m_tcg_tb_vec.push(be_data);
                }
                // disassemble_x86を呼び出す。
                disassemble_x86(mc_byte.as_slice(), self.m_tb_text_mem.data());
                pc_address += mc_byte.len() as u64;
            }
...
pub fn disassemble_x86(bytes: &[u8], host_code_addr: *const u8) {
    let mut decoder = Decoder::new(EXAMPLE_CODE_BITNESS, bytes, DecoderOptions::NONE);
    decoder.set_ip(unsafe { host_code_addr.offset(0) as u64 });

    // Formatters: Masm*, Nasm*, Gas* (AT&T) and Intel* (XED)
    let mut formatter = GasFormatter::new();

    // Change some options, there are many more
    formatter.options_mut().set_digit_separator("_");
    formatter.options_mut().set_first_operand_char_index(10);

    // String implements FormatterOutput
    let mut output = String::new();

    // Initialize this outside the loop because decode_out() writes to every field
    let mut instruction = Instruction::default();

    // The decoder also implements Iterator/IntoIterator so you could use a for loop:
    //      for instruction in &mut decoder { /* ... */ }
    // or collect():
    //      let instructions: Vec<_> = decoder.into_iter().collect();
    // but can_decode()/decode_out() is a little faster:
    while decoder.can_decode() {
        // There's also a decode() method that returns an instruction but that also
...

デバッグモード二次出力するようにオプションを整える。実行してみよう。

$ cargo run -- --debug --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-p-simple
Guest PC Address = 0000004c
  f1402573 : 
  00051063 : 
00007F7BA53E0000 48BF201316C8FF7F0000 movabs    $0x7FFF_C816_1320,%rdi
00007F7BA53E000A 48BE0A00000000000000 movabs    $0xA,%rsi
00007F7BA53E0014 48BA0000000000000000 movabs    $0,%rdx
00007F7BA53E001E 48B9140F000000000000 movabs    $0xF14,%rcx
00007F7BA53E0028 FF9518030000         callq     *0x318(%rbp)
Added offset. code_ptr = 3e
00007F7BA53E0000 488B8558000000       mov       0x58(%rbp),%rax
00007F7BA53E0007 483B8508000000       cmp       8(%rbp),%rax
00007F7BA53E000E 0F850A000000         jne       0x0000_7F7B_A53E_001E[f:id:msyksphinz:20200930225446p:plain]
00007F7BA53E0000 48B85400000000000000 movabs    $0x54,%rax
00007F7BA53E000A 48898508020000       mov       %rax,0x208(%rbp)
00007F7BA53E0011 E9B7FF0200           jmp       0x0000_7F7B_A540_FFCD
Offset is set 58
00007F7BA53E0000 48B85000000000000000 movabs    $0x50,%rax
00007F7BA53E000A 48898508020000       mov       %rax,0x208(%rbp)
00007F7BA53E0011 E9A1FF0200           jmp       0x0000_7F7B_A540_FFB7

上手く行ったようだ。f140257300051063RISC-Vの2命令 (本当はcsrr a0, mhartidbnez a0, pc + 0命令)がどのようなx86命令に変換されているかを確認することができるようになった。

f:id:msyksphinz:20200930225446p:plain

Binary Translation型エミュレータを作る(ステップ実行のサポート)

Binary Translation方式の命令セットエミュレータのRust実装、ある程度進んできたが、どんどん複雑なテストパタンを確認していかなければならない。その際に問題となるのはデバッグ機能だ。デバッグ機能については、いくつかやらなければならないことがある。

  • ホスト命令をBasicBlock単位に変換するのではなく、1命令単位で変換して実行する。これにより命令毎にレジスタの値をダンプすることができるようになり、デバッグの幅が広がる。
  • ホスト命令のディスアセンブル機能。どのようなホスト命令が変換ターゲットとなるのか確認できる。
  • ゲスト命令のディスアセンブル機能。ホスト命令がどのようなゲスト命令に変換されたのかを確認できる。

この3つについて実装を進めていこうと思う。まずは1命令単位でエミュレーションをする機能について。

f:id:msyksphinz:20200930225126p:plain

通常QEMUをはじめとするBinary Translation型のエミュレータは、BasicBlock単位で命令の変換を行う。つまり、分岐命令に到達するまですべての命令をホスト命令に変換し、分岐命令に到達した時点で変換処理をいったん中止する。

この場合、やっかうなのがデバッグだ。分岐命令に到達するまで一気に命令が進むので、レジスタの状態などを命令毎にダンプすることができず、途中の結果などの細かな情報がどうしても取得することができない。

そこで、BasicBlock単位で変換を行うのではなく、命令毎に変換して、命令毎にブロックを区切ることによりそのたびにレジスタダンプなどが行えるようにする。これによりデバッグ時に取得できる情報が増える。

実際のコードを見る。通常は、いかの処理をブロックが終了するまで連続で実行している。

            #[allow(while_true)]
            while true {
                let guest_inst = unsafe {
                    ((self
                        .m_guest_text_mem
                        .data()
...
                let mut tcg_inst = TranslateRiscv::translate(id, &inst_info);
                self.m_tcg_vec.append(&mut tcg_inst);

self.m_tcg_vecに対してTCGを次々と追加し、それをX86命令に変換するのだが、デバッグ時は1つ分TCGを追加するだけで処理を中断する。

                let mut tcg_inst = TranslateRiscv::translate(id, &inst_info);
                self.m_tcg_vec.append(&mut tcg_inst);
                // これを追加。デバッグモードの時はすぐにTBを中断しホストコードに戻るTCGを追加する。
                if debug {
                    let mut exit_tcg = vec![TCGOp::new_0op(TCGOpcode::EXIT_TB)];
                    self.m_tcg_vec.append(&mut exit_tcg);
                }
...
                if debug {
                    break;      // When Debug Mode, break for each instruction
                }
            }

これにより、1命令毎に変換して処理が完了する。こうすれば1命令毎に様々な情報を出力することができるようになる。やってみよう。

$ cargo run -- --debug --dump-gpr \
    --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-p-simple
========= BLOCK START =========
Guest PC Address = 00000000
  04c0006f : j       pc + 0x4c
tb_address  = 0x7f3476e00000
x00 = 0000000000000000  x01 = 0000000000000000  x02 = 0000000000000000  x03 = 0000000000000000
x04 = 0000000000000000  x05 = 0000000000000000  x06 = 0000000000000000  x07 = 0000000000000000
x08 = 0000000000000000  x09 = 0000000000000000  x10 = 0000000000000000  x11 = 0000000000000000
x12 = 0000000000000000  x13 = 0000000000000000  x14 = 0000000000000000  x15 = 0000000000000000
x16 = 0000000000000000  x17 = 0000000000000000  x18 = 0000000000000000  x19 = 0000000000000000
x20 = 0000000000000000  x21 = 0000000000000000  x22 = 0000000000000000  x23 = 0000000000000000
x24 = 0000000000000000  x25 = 0000000000000000  x26 = 0000000000000000  x27 = 0000000000000000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

========= BLOCK START =========
Guest PC Address = 0000004c
  f1402573 : csrr    a0, mhartid
tb_address  = 0x7f3476dc0000
helper_csrrs(emu, 10, 0, 0xf14) is called!
x00 = 0000000000000000  x01 = 0000000000000000  x02 = 0000000000000000  x03 = 0000000000000000
x04 = 0000000000000000  x05 = 0000000000000000  x06 = 0000000000000000  x07 = 0000000000000000
x08 = 0000000000000000  x09 = 0000000000000000  x10 = 0000000000000000  x11 = 0000000000000000
x12 = 0000000000000000  x13 = 0000000000000000  x14 = 0000000000000000  x15 = 0000000000000000
x16 = 0000000000000000  x17 = 0000000000000000  x18 = 0000000000000000  x19 = 0000000000000000
x20 = 0000000000000000  x21 = 0000000000000000  x22 = 0000000000000000  x23 = 0000000000000000
x24 = 0000000000000000  x25 = 0000000000000000  x26 = 0000000000000000  x27 = 0000000000000000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

========= BLOCK START =========
Guest PC Address = 00000050
  00051063 : bnez    a0, pc + 0
tb_address  = 0x7f3476df0000
x00 = 0000000000000000  x01 = 0000000000000000  x02 = 0000000000000000  x03 = 0000000000000000
x04 = 0000000000000000  x05 = 0000000000000000  x06 = 0000000000000000  x07 = 0000000000000000
x08 = 0000000000000000  x09 = 0000000000000000  x10 = 0000000000000000  x11 = 0000000000000000
x12 = 0000000000000000  x13 = 0000000000000000  x14 = 0000000000000000  x15 = 0000000000000000
x16 = 0000000000000000  x17 = 0000000000000000  x18 = 0000000000000000  x19 = 0000000000000000
x20 = 0000000000000000  x21 = 0000000000000000  x22 = 0000000000000000  x23 = 0000000000000000
x24 = 0000000000000000  x25 = 0000000000000000  x26 = 0000000000000000  x27 = 0000000000000000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

こんな感じで、1命令ずつレジスタをダンプできるようになる。上手く行ったようだ。

Binary Translation型エミュレータを作る(fsgnj命令の実装)

Binary Translation方式のエミュレータをRustで作る実装、浮動小数点命令を実装している。次はfsgnj命令の実装だ。fsgnj命令は単精度・倍精度を合わせて合計6つ存在している。

FSGNJ.D      fd, fs1, fs2        // f[rd] = {f[rs2][63], f[rs1][62:0]}
FSGNJN.D    fd, fs1, fs2        // f[rd] = {~f[rs2][63], f[rs1][62:0]}
FSGNJX.D    fd, fs1, fs2        // f[rd] = {f[rs2][63] ^ f[rs2][63], f[rs1][62:0]}
FSGNJ.S     fd, fs1, fs2        // f[rd] = {f[rs2][31], f[rs1][30:0]}
FSGNJN.S    fd, fs1, fs2        // f[rd] = {~f[rs2][31], f[rs1][30:0]}
FSGNJX.S    fd, fs1, fs2        // f[rd] = {f[rs1][31] ^ f[rs2][31], f[rs1][30:0]}

倍精度のFSGNJ命令については、頑張ってすべてTCGで実装した。

    fn tcg_gen_sgnj_64bit(emu: &EmuEnv, pc_address: u64, tcg: &TCGOp, mc: &mut Vec<u8>) -> usize {
        let op = tcg.op.unwrap();
        let arg0 = tcg.arg0.unwrap();
        let arg1 = tcg.arg1.unwrap();
        let arg2 = tcg.arg2.unwrap();

        assert_eq!(op, TCGOpcode::SGNJ_64BIT);
        assert_eq!(arg0.t, TCGvType::Register);
        assert_eq!(arg1.t, TCGvType::Register);
        assert_eq!(arg2.t, TCGvType::Register);

        let mut gen_size: usize = pc_address as usize;

        gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RCX, 0x7fffffff_ffffffff, mc);
        gen_size += Self::tcg_modrm_64bit_out(X86Opcode::AND_GV_EV, X86ModRM::MOD_10_DISP_RBP, X86TargetRM::RCX, mc);
        gen_size += Self::tcg_out(emu.calc_fregs_relat_address(arg1.value) as u64, 4, mc);

        gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RAX, 0x80000000_00000000, mc);
        gen_size += Self::tcg_modrm_64bit_out(X86Opcode::AND_GV_EV, X86ModRM::MOD_10_DISP_RBP, X86TargetRM::RAX, mc);
        gen_size += Self::tcg_out(emu.calc_fregs_relat_address(arg2.value) as u64, 4, mc);

        gen_size += Self::tcg_modrm_64bit_out(X86Opcode::OR_GV_EV, X86ModRM::MOD_11_DISP_RCX, X86TargetRM::RAX, mc);
        gen_size += Self::tcg_gen_store_fregs_64bit(emu, X86TargetRM::RAX, arg0.value, mc);

        return gen_size;
    }

このように実装すると、SGNJ.DX86命令で驚異的な短さで実装できる。

0x4001cfe0b8:  48 b9 ff ff ff ff ff ff  movabsq  $0x7fffffffffffffff, %rcx
0x4001cfe0c0:  ff 7f
0x4001cfe0c2:  48 23 8d 10 01 00 00     andq     0x110(%rbp), %rcx
0x4001cfe0c9:  48 b8 00 00 00 00 00 00  movabsq  $0x8000000000000000, %rax
0x4001cfe0d1:  00 80
0x4001cfe0d3:  48 23 85 18 01 00 00     andq     0x118(%rbp), %rax
0x4001cfe0da:  48 0b c1                 orq      %rcx, %rax
0x4001cfe0dd:  48 89 85 08 01 00 00     movq     %rax, 0x108(%rbp)

で、これを32ビット版で同じように実装するとどうもテストパタンが落ちてしまうので調べていると、32ビット版の場合はNaN Boxingを適用しなければならないのだった。

    #[inline]
    fn convert_nan_boxing (i: u64) -> u32 {
        if i & 0xffffffff_00000000 == 0xffffffff_00000000 {
            (i & 0xffffffff) as u32
        } else {
            0x7fc00000
        }
    }

ちょっと面倒くさいなー、と思い、結局NaN Boxingも含めて32ビット版はRustで実装してしまった。。。でもよく考えると、TCGもラベルジャンプを実装していることだし、全部TCGで実装できないことはなさそうだな。。。一応テストパタンは通ったけど、最適化として一応考えてみようかな。

    pub fn helper_func_fsgnj_s(emu: &mut EmuEnv, rd: u32, fs1: u32, fs2: u32, _dummy: u32) -> usize {
        let fs1_data = Self::convert_nan_boxing(emu.m_fregs[fs1 as usize]) as u32;
        let fs2_data = Self::convert_nan_boxing(emu.m_fregs[fs2 as usize]) as u32;
        let mut flag = ExceptionFlags::default();
        flag.set();
        emu.m_fregs[rd as usize] = (fs1_data & 0x7fffffff | fs2_data & 0x80000000) as u64 | 0xffffffff_00000000;
        flag.get();
        let ret_flag = flag.bits();
        println!("fsgnj_s(emu, {:}, {:}, {:}) is called!", rd, fs1, fs2);
        emu.m_csr.csrrw(CsrAddr::FFlags, ret_flag as i64);
        return 0;
    }

QEMUはどのようにMMUをエミュレートしているのか

QEMUがどのようにしてMMUによるアドレス変換を実現しているのかを観察する。

まず以下のオプションでテストパタンを実行しログの様子を観察する。

./build-riscv/riscv64-softmmu/qemu-system-riscv64 \
    -nographic -d in_asm,mmu \
    -kernel /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-v-add 2>&1 | \
    tee rv64ui-v-add.riscv.log

mmuオプションを追加してMMUの動作の様子を観察する。

riscv_cpu_tlb_fill ad 1000 rw 2 mmu_idx 3
riscv_cpu_tlb_fill address=1000 ret 0 physical 0000000000001000 prot 7
----------------
IN: 
Priv: 3; Virt: 0
0x0000000000001000:  00000297          auipc           t0,0            # 0x1000
0x0000000000001004:  02028593          addi            a1,t0,32
0x0000000000001008:  f1402573          csrrs           a0,mhartid,zero

riscv_cpu_tlb_fill()という関数が呼ばれている。

bool riscv_cpu_tlb_fill(CPUState *cs, vaddr address, int size,
                        MMUAccessType access_type, int mmu_idx,
                        bool probe, uintptr_t retaddr)
{
    RISCVCPU *cpu = RISCV_CPU(cs);
    CPURISCVState *env = &cpu->env;
...

内部の実装を見る前に、どのようにしてこの関数が呼ばれたかを観察する。

riscv_cpu_tlb_fill()は、CPU環境のCPUClasstlb_fillに関数ポインタとして登録されている。

  • qemu/target/riscv/cpu_helper.c
static void riscv_cpu_class_init(ObjectClass *c, void *data)
{
    RISCVCPUClass *mcc = RISCV_CPU_CLASS(c);
    CPUClass *cc = CPU_CLASS(c);
    DeviceClass *dc = DEVICE_CLASS(c);
...
#ifdef CONFIG_TCG
    cc->tcg_initialize = riscv_translate_init;
    cc->tlb_fill = riscv_cpu_tlb_fill;
#endif

これがどこから呼ばれているのか分からないのでGDBを使ってスタックフレームを観察する。

#0  0x00000000083ab21d in riscv_cpu_tlb_fill (cs=0x8e049b0, address=4096, size=0, access_type=MMU_INST_FETCH, mmu_idx=3, probe=false, retaddr=0)
    at /home/msyksphinz/work/riscv/qemu/target/riscv/cpu_helper.c:703
#1  0x00000000082f4fc0 in tlb_fill (cpu=0x8e049b0, addr=4096, size=0, access_type=MMU_INST_FETCH, mmu_idx=3, retaddr=0)
    at /home/msyksphinz/work/riscv/qemu/accel/tcg/cputlb.c:1017
#2  0x00000000082f5620 in get_page_addr_code_hostp (env=0x8e0d3d0, addr=4096, hostp=0x0) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cputlb.c:1172
#3  0x00000000082f574d in get_page_addr_code (env=0x8e0d3d0, addr=4096) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cputlb.c:1204
#4  0x000000000830dac3 in tb_htable_lookup (cpu=0x8e049b0, pc=4096, cs_base=0, flags=3, cf_mask=4278714368) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:337
#5  0x000000000830cfed in tb_lookup__cpu_state (cpu=0x8e049b0, pc=0x7ffff7e8f558, cs_base=0x7ffff7e8f550, flags=0x7ffff7e8f54c, cf_mask=4278714368)
    at /home/msyksphinz/work/riscv/qemu/include/exec/tb-lookup.h:43
#6  0x000000000830dd84 in tb_find (cpu=0x8e049b0, last_tb=0x0, tb_exit=0, cf_mask=524288) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:404
#7  0x000000000830e684 in cpu_exec (cpu=0x8e049b0) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:731
#8  0x00000000082d0e9b in tcg_cpu_exec (cpu=0x8e049b0) at /home/msyksphinz/work/riscv/qemu/cpus.c:1405
#9  0x00000000082d16f1 in qemu_tcg_cpu_thread_fn (arg=0x8e049b0) at /home/msyksphinz/work/riscv/qemu/cpus.c:1713
#10 0x000000000875fa62 in qemu_thread_start (args=0x8e1a280) at /home/msyksphinz/work/riscv/qemu/util/qemu-thread-posix.c:519
#11 0x00007ffffdb77164 in start_thread (arg=<optimized out>) at pthread_create.c:486
#12 0x00007ffffda9adef in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

なるほど、上から順に追っていこう。

  • qemu/accel/tcg/cpu-exec.c
/* main execution loop */

int cpu_exec(CPUState *cpu)
{
    CPUClass *cc = CPU_GET_CLASS(cpu);
...
            tb = tb_find(cpu, last_tb, tb_exit, cflags);
            cpu_loop_exec_tb(cpu, tb, &last_tb, &tb_exit);
  • qemu/accel/tcg/cpu-exec.c
static inline TranslationBlock *tb_find(CPUState *cpu,
                                        TranslationBlock *last_tb,
                                        int tb_exit, uint32_t cf_mask)
{
    TranslationBlock *tb;
    target_ulong cs_base, pc;
    uint32_t flags;

    tb = tb_lookup__cpu_state(cpu, &pc, &cs_base, &flags, cf_mask);
    ...
  • qemu/include/exec/tb-lookup.h
/* Might cause an exception, so have a longjmp destination ready */
static inline TranslationBlock *
tb_lookup__cpu_state(CPUState *cpu, target_ulong *pc, target_ulong *cs_base,
                     uint32_t *flags, uint32_t cf_mask)
{
...
    tb = tb_htable_lookup(cpu, *pc, *cs_base, *flags, cf_mask);
...
  • qemu/accel/tcg/cpu-exec.c
TranslationBlock *tb_htable_lookup(CPUState *cpu, target_ulong pc,
                                   target_ulong cs_base, uint32_t flags,
                                   uint32_t cf_mask)
{
...
    phys_pc = get_page_addr_code(desc.env, pc);
  • qemu/accel/tcg/cputlb.c
tb_page_addr_t get_page_addr_code(CPUArchState *env, target_ulong addr)
{
    return get_page_addr_code_hostp(env, addr, NULL);
}
  • qemu/accel/tcg/cputlb.c
tb_page_addr_t get_page_addr_code_hostp(CPUArchState *env, target_ulong addr,
                                        void **hostp)
{
    uintptr_t mmu_idx = cpu_mmu_index(env, true);
    uintptr_t index = tlb_index(env, mmu_idx, addr);
    CPUTLBEntry *entry = tlb_entry(env, mmu_idx, addr);
    void *p;

    if (unlikely(!tlb_hit(entry->addr_code, addr))) {
        if (!VICTIM_TLB_HIT(addr_code, addr)) {
            tlb_fill(env_cpu(env), addr, 0, MMU_INST_FETCH, mmu_idx, 0);
            index = tlb_index(env, mmu_idx, addr);
            entry = tlb_entry(env, mmu_idx, addr);
...

なるほど、フェッチの部分からもTLBをサーチして失敗すればPhysicalAddressを計算するルーチンに移行する訳か。

riscv_cpu_tlb_fill()に立ち戻ってもう一度処理の流れを追いかけてみる。

bool riscv_cpu_tlb_fill(CPUState *cs, vaddr address, int size,
                        MMUAccessType access_type, int mmu_idx,
                        bool probe, uintptr_t retaddr)
{
    RISCVCPU *cpu = RISCV_CPU(cs);
    CPURISCVState *env = &cpu->env;
...

まず、マシンモードでロードストアアクセスの場合はMSTATUSから特権モードを取ってくる。

    if (mode == PRV_M && access_type != MMU_INST_FETCH) {
        if (get_field(env->mstatus, MSTATUS_MPRV)) {
            mode = get_field(env->mstatus, MSTATUS_MPP);
        }
    }

マシンモードでない場合は仮想アドレスから物理アドレスへの変換を行う。Hypervisorでない場合場合は通常これは2ステップで行われる。

    if (riscv_cpu_virt_enabled(env) || m_mode_two_stage || hs_mode_two_stage) {
        /* Two stage lookup */
        ret = get_physical_address(env, &pa, &prot, address, access_type,
                                   mmu_idx, true, true);

            /* Second stage lookup */
            im_address = pa;

            ret = get_physical_address(env, &pa, &prot, im_address,
                                       access_type, mmu_idx, false, true);

...

そうでない場合は1ステップでマシンモードへの変換を行う。

    } else {
        /* Single stage lookup */
        ret = get_physical_address(env, &pa, &prot, address, access_type,
                                   mmu_idx, true, false);

        qemu_log_mask(CPU_LOG_MMU,
                      "%s address=%" VADDR_PRIx " ret %d physical "
                      TARGET_FMT_plx " prot %d\n",
                      __func__, address, ret, pa, prot);
    }

ちなみにget_physical_address()はゴリゴリにC言語で書いてあった。やはりこれはTCGではないらしい。

/* get_physical_address - get the physical address for this virtual address
 *
 * Do a page table walk to obtain the physical address corresponding to a
 * virtual address. Returns 0 if the translation was successful
 *
 * Adapted from Spike's mmu_t::translate and mmu_t::walk
 *
 * @env: CPURISCVState
 * @physical: This will be set to the calculated physical address
 * @prot: The returned protection attributes
 * @addr: The virtual address to be translated
 * @access_type: The type of MMU access
 * @mmu_idx: Indicates current privilege level
 * @first_stage: Are we in first stage translation?
 *               Second stage is used for hypervisor guest translation
 * @two_stage: Are we going to perform two stage translation
 */
static int get_physical_address(CPURISCVState *env, hwaddr *physical,
                                int *prot, target_ulong addr,
                                int access_type, int mmu_idx,
                                bool first_stage, bool two_stage)
{
...

半導体業界における「IP」とは何なのかを説明したい

RISC-V」という言葉が徐々にエンジニア界隈に普及し始め、技術界隈のニュースサイトだけでなく、一般的なニュースを扱うような新聞社の記事でも見かけるようになってきました。例えば以下のような記事です。

www.nikkei.com

半導体エンジニアではない人がこのような記事を書く場合、「設計IP」について正しい知識を持っておかないと、少しおかしなことになってしまいます。しかしこれは記事を書いている記者だけを責めることは出来ません。半導体設計業界はソフトウェア開発業界に比べて小さな業界で、プレーヤの数も少なく、ネット上にあまり情報も出てきません。時事ネタを速攻で記事に起こさないといけない新聞記者が「IPってなんだっけ?」「リスクファイブってなんぞや?」ということをいちいち厳密に調べてられない、ということも理解できます。

そこで、非エンジニア(というか非半導体産業の方)でも理解できるように、RISC-VやARMを中心とする「CPU設計とIP」の関係ということについて可能な限り説明してみたいと思います。この記事は半導体のことを熟知している方のためではなく、非半導体産業の方や、情報処理技術界隈に詳しくない方のためのものです。

「ARM」とは結局何なのか?

現在、世の中には「ARMコア」を採用したチップが大量に溢れています。身近なところではRaspberry-Piや、その他隠れたIoT機器などにも「ARMコア」というものが採用されています。

ARMコアは、Arm社が開発し販売しているCPUコアです(本記事では、会社として「Arm社」、コアとして「ARM」という用語を使います)。より具体的にいうと、この「ARM」という言葉には2つの側面があります。

  • 命令仕様としての「ARMアーキテクチャ
  • 命令仕様に基づいた回路設計としての「ARMコア」

具体的に言ってしまえば、「命令仕様としてのARM」は仕様書なので「紙に書かれた文書」と考えてもらっても差し支えありません。これは一般的に「ARMアーキテクチャ」や「AArch64(前述のARMは32ビット仕様なのに対し、これを64ビット仕様に拡張したもの)」などと言います。

一方で、回路設計データとしての「ARM」は、上記の仕様書としてのARMを実際の回路として実装したものになります。Arm社が開発したARMコアは有名なものに「Cortex」シリーズがあります。「Cortex-A」「Cortex-R」「Cortex-M」といったものがあります。

このように、回路設計データと命令仕様を分離することで、様々なARMコアが登場します。「Cortex-A73」と「Cortex-A72」はどちらともARMの命令セット「AArch64(=ARMv8)」を採用しており、逆に言えば、一度Cortex-A72用に書いたソフトウェアは特に何の変更も必要なくCortex-A73で動かすことができます。

f:id:msyksphinz:20201003225239p:plain

ARM社のビジネスモデルは、自社で開発したCortexシリーズのCPUコア設計データを他社に買ってもらい、ライセンス料を払って儲ける仕組みです。この「回路設計データ」のことを「IP(intellectual property)」と呼ばれ、実体は回路情報の含まれるデジタルデータです。実体はなく、データとして提供されます。

「IP」という言葉は半導体産業では非常に一般的に使用される用語です。しかしこの用語はあいまいで、"Intellectual Property"(知的財産)ってどういうことよ?と思われると思います。私も正直良く分かりません1。とにかく、回路設計データのことを、IPと呼んでいるのです。

f:id:msyksphinz:20201003225259p:plain

世の中に出ているARMコアは、このようにARM社から買ってきた回路情報データを自社のチップ上に実装して、製品として売り出しているものになります。一方でQualcomm社などの複数社は、ARM仕様書に基づき自分で一からARMコアを実装できるライセンスを購入しており2、SnapdragonシリーズなどはQualcomm自身によって開発されたチップであり、Snapdragonの内部にはKryoと呼ばれるQualcomm社が独自に開発したARMコアが搭載されています。AppleのAシリーズも同様です。

f:id:msyksphinz:20201003225326p:plain

RISC-V」は「命令仕様としてのARM」と立場的に同等のもの

RISC-Vの話に立ち戻ります。ここではっきりさせておかなければならないのは、「RISC-V」は「命令仕様:命令セットアーキテクチャ:ISA」である、ということです。つまり、前述した「命令仕様としてのARM」と立場的には同じものです。そこには紙ペラしかなく、具体的な回路設計情報は含まれていません。

RISC-V Foundationのウェブサイトを見てみましょう。公開されているのはPDFファイルに変換された仕様書のみで、回路設計データは存在していません。この仕様書を管理しているのが「RISC-V Foundation」という非営利団体です。このRISC-V Foundationには大手半導体メーカーなども加入していますが、基本的に仕様の管理はこの非営利団体によって行われています。ARMの仕様はArm一社によって管理されていますが、RISC-Vは様々な組織や人物が使用の策定に加わっています。そこが大きな違いと言えるでしょう。

そして、このRISC-Vの仕様に基づいて様々な企業が「RISC-V CPUコア」の開発に取り組んでいます。RISC-Vの初期仕様の開発に携わった人々が立ち上げたベンチャー「SiFive社」、「Andes Technology」、日本だとNSITEXE社がRISC-V CPUコアの開発を行っています。これらのIPコアは、ARMで言うCortexシリーズと立場的に同等なものです。

f:id:msyksphinz:20201003225759p:plain

RISC-V」は「IP」ではない

RISC-V」はあくまでも「命令セットアーキテクチャ(仕様)」であり、「実際の回路設計データ」ではないことははっきりしておかなければなりません。

RISC-V」がフリーでオープンとはどういうことなのか?

では、RISC-Vが「フリーでオープンである」とはどういうことなのか、ここでもう一度確認しておく必要があります。RISC-Vが設計IPのことを示すわけではないため、設計IPがフリーであるということではないということは自明です。RISC-Vがフリーであるというのは、RISC-V ISAに対応したCPUの開発やソフトウェアエコシステムの開発を、RISC-Vに対してライセンス料を支払わずに行うことができることを意味します。

上述したように、ARMアーキテクチャを使用してCPUを開発するためには「アーキテクチャライセンス」という非常に高額なライセンス料をArm社に支払う必要があります。私は正式にArm社からライセンスを購入したことが無いので分かりませんが、一般に数億円であると言われています。このライセンス料が、RISC-Vの場合には不要となります。

また、「ISAがオープンソース」であるということについては、私の理解では「仕様をオープンソースとして公開している」という所だと思います。ARMアーキテクチャはArm社内部でのみ使用の策定議論が行われ、外部の開発者はARMのいうことに従うほかありません。しかし、RISC-Vの仕様書自体がGitHub上で公開されており、そして私たちは誰でもPull Requestを投げることができます(!)このように、誰でもRISC-Vの仕様の策定に参加できるというのが、RISC-Vの大きな特徴だと言えます。

一つの例として、RISC-VのVector Extension仕様(https://github.com/riscv/riscv-v-spec)には毎週のようにMeeting Minutesがリポジトリ上に追加されています(https://github.com/riscv/riscv-v-spec/tree/master/minutes)。このように、議論の流れもすべて公開されているというのが、RISC-Vの大きな特徴だと言えるでしょう。

私の考える「半導体IPに関する不適切な用語」

まとめとして、私の考える「不適切なRISC-V関連の用語」をまとめておきます。

  • RISC-VはオープンソースCPUである」は誤り。RISC-Vは命令セットであるためCPU実装のことを直接示すわけではありません。RISC-Vであってもクローズソースで開発してライセンス販売を行うことは許されています。
    • と思ったら私も過去の雑誌で「オープンソースCPU」という言葉を使っているな... あまり良くない。
  • RISC-Vという回路設計IP」は誤り。RISC-Vは命令セットのことを示しており、その仕様に基づいて開発される回路設計IPとは切り離して考えなければなりません。

  1. 私が半導体の勉強を始めた学生時代のころからIPという言葉は一般的に使用されており、その時教授に「なんで知的財産なんですか?」と聞いた記憶がある。その時の答えは「俺も知らん」だった。

  2. Arm社のIPを使用するためには「プロセッサーライセンス」が必要ですが、さらにARMの仕様に基づいてCPUコアを自社開発するためには「アーキテクチャライセンス」の購入が必要となります。アーキテクチャライセンスはプロセッサーライセンスよりも高額だといわれています。

KVM on SpikeでRISC-Vのハイパーバイザーを試す

前回のQEMUを使ったRISC-V Linuxの起動およびKVMでのLinux起動、実はSpikeでも実現できることが分かった。

同じプロジェクト内の以下のWikiを参考にした。というか、違いはQEMUをビルドする代わりにSpikeをビルドするだけだった。

github.com

git clone https://github.com/riscv/riscv-isa-sim.git
cd riscv-isa-sim
./configure
make
cd ..
$ ./riscv-isa-sim/spike -m512 --isa rv64gch --kernel ./opensbi/build-riscv64/arch/riscv/boot/Image \
    --initrd ./rootfs_kvm_riscv64.img opensbi/opensbi/build/platform/generic/firmware/fw_jump.elf

こちらでも問題なく起動した。動作速度はQEMUの方が若干速いかな?それでもこの速度で動作するSpikeはすごいな。

f:id:msyksphinz:20200925215157g:plain

KVM on QEMUでRISC-Vのハイパーバイザーを試す

RISC-Vのハイパーバイザー拡張は現在Version0.61まで進んでいるが、実際にアプリケーションを試す手法としてLinux上でKVMを動かしてさらにLinuxを立ち上げるというものがある。チュートリアルが公開されているので試してみたい。

以下の資料に基づいて試してみることにする。

github.com

まずQEMUをビルドする。本家からのダウンロードではないが、ハイパーバイザ―向けに拡張が実装してあるのかどうかは良く分からない。

git clone https://github.com/kvm-riscv/qemu.git
cd qemu
./configure --target-list="riscv32-softmmu riscv64-softmmu"
make
cd ..

次にOpenSBIをダウンロードしてビルドする。これについても知識が足りずに良く分からないのだが、M-Mode動作するランタイムファームウェアのことらしい。詳しいことは全く分からない。

git clone https://github.com/riscv/opensbi.git
cd opensbi
export CROSS_COMPILE=riscv64-unknown-linux-gnu-
make PLATFORM=generic
cd ..

次はLinuxカーネルのビルドだ。今回はホストとゲストで同じカーネルを使用するため2種類ビルドする必要はない。

git clone https://github.com/kvm-riscv/linux.git
export ARCH=riscv
export CROSS_COMPILE=riscv64-unknown-linux-gnu-
mkdir build-riscv64
make -C linux O=`pwd`/build-riscv64 defconfig
make -C linux O=`pwd`/build-riscv64

次に必要なのはlibfdtというライブラリで、これはクロスコンパイラでライブラリとして必要らしい。KVMTOOLをコンパイルするためにはこのlibfdtが必要なようだ。従ってクロスコンパイラがインストールされているディレクトリに対してビルドしたライブラリを格納することになる。

git clone git://git.kernel.org/pub/scm/utils/dtc/dtc.git
cd dtc
export ARCH=riscv
export CROSS_COMPILE=riscv64-unknown-linux-gnu-
export CC="${CROSS_COMPILE}gcc -mabi=lp64d -march=rv64gc"
TRIPLET=$($CC -dumpmachine)
SYSROOT=$($CC -print-sysroot)
make libfdt
make EXTRA_CFLAGS="-mabi=lp64d" DESTDIR=$SYSROOT PREFIX=/usr LIBDIR=/usr/lib64/lp64d install-lib install-includes
cd ..

上記のコマンドを試しているうちに、

TRIPLET=$($CC -dumpmachine)
SYSROOT=$($CC -print-sysroot)

でなぜかエラーが発生する。原因も良く分からなかったので決め打ちで変数を設定してしまった。

$ riscv64-unknown-linux-gnu-gcc -mabi=lp64d -march=rv64gc -dumpmachine
riscv64-unknown-linux-gnu
$ TRIPLET=riscv64-unknown-linux-gnu
$ riscv64-unknown-linux-gnu-gcc -mabi=lp64d -march=rv64gc -print-sysroot
/home/msyksphinz/riscv64-ctng-linux/riscv64-unknown-linux-gnu/sysroot
$ SYSROOT=/home/msyksphinz/riscv64-ctng-linux/riscv64-unknown-linux-gnu/sysroot

ここでさらに問題発生、make EXTRA_CFLAGS="-mabi=lp64d" DESTDIR=$SYSROOT PREFIX=/usr LIBDIR=/usr/lib64/lp64d install-lib install-includesを実行すると以下のエラーを吐いて落ちてしまった。

        CHK version_gen.h
         PYMOD pylibfdt/_libfdt.so
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: build/temp.linux-x86_64-3.7/libfdt_wrap.o: error adding symbols: file in wrong format
collect2: error: ld returned 1 exit status

これは同じページのIssueで調べてみると、別のクロスコンパイラツールチェインが必要らしい。

https://github.com/kvm-riscv/howto/issues/2

という訳で https://toolchains.bootlin.com/ からRISC-V向けのツールチェインを新たにダウンロードして、dtc/MakefilePythonのパスを書き換えた。

  • dtc/Makefile
git diff
diff --git a/Makefile b/Makefile
index c187d5f..6472882 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ BISON = bison
 LEX = flex
 SWIG = swig
 PKG_CONFIG ?= pkg-config
-PYTHON ?= python3
+PYTHON ?= $(HOME)/riscv-toolchains/riscv64--glibc--bleeding-edge-2020.02-2/bin/python3

 INSTALL = /usr/bin/install
 INSTALL_PROGRAM = $(INSTALL)

再度ビルドすると上手く行ったようだ。

$ make EXTRA_CFLAGS="-mabi=lp64d" DESTDIR=$SYSROOT PREFIX=/usr LIBDIR=/usr/lib64/lp64d install-lib install-includes
        CHK version_gen.h
        UPD version_gen.h
         DEP util.c
        CHK version_gen.h
         CC util.o
         LD convert-dtsv0
         LD dtc
         LD fdtdump
         LD fdtget
         LD fdtput
         LD fdtoverlay
## Skipping pylibfdt (install python dev and swig to build)
         INSTALL-LIB
         INSTALL-INC

次にKVMTOOLをビルドする。これがKVMの本体なのか?

git clone https://github.com/kvm-riscv/kvmtool.git
export ARCH=riscv
export CROSS_COMPILE=riscv64-unknown-linux-gnu-
cd kvmtool
make lkvm-static
${CROSS_COMPILE}strip lkvm-static
cd ..

最後に、RootFSをビルドして、なかにKVMTOOLおよびゲストとして起動するLinuxカーネルをコピーしイメージとしてまとめ上げる。

export ARCH=riscv
export CROSS_COMPILE=riscv64-unknown-linux-gnu-
git clone https://github.com/kvm-riscv/howto.git
wget https://busybox.net/downloads/busybox-1.27.2.tar.bz2
tar -C . -xvf ./busybox-1.27.2.tar.bz2
mv ./busybox-1.27.2 ./busybox-1.27.2-kvm-riscv64
cp -f ./howto/configs/busybox-1.27.2_defconfig busybox-1.27.2-kvm-riscv64/.config
make -C busybox-1.27.2-kvm-riscv64 oldconfig
make -C busybox-1.27.2-kvm-riscv64 install
mkdir -p busybox-1.27.2-kvm-riscv64/_install/etc/init.d
mkdir -p busybox-1.27.2-kvm-riscv64/_install/dev
mkdir -p busybox-1.27.2-kvm-riscv64/_install/proc
mkdir -p busybox-1.27.2-kvm-riscv64/_install/sys
mkdir -p busybox-1.27.2-kvm-riscv64/_install/apps
ln -sf /sbin/init busybox-1.27.2-kvm-riscv64/_install/init
cp -f ./howto/configs/busybox/fstab busybox-1.27.2-kvm-riscv64/_install/etc/fstab
cp -f ./howto/configs/busybox/rcS busybox-1.27.2-kvm-riscv64/_install/etc/init.d/rcS
cp -f ./howto/configs/busybox/motd busybox-1.27.2-kvm-riscv64/_install/etc/motd
cp -f ./kvmtool/lkvm-static busybox-1.27.2-kvm-riscv64/_install/apps
cp -f ./build-riscv64/arch/riscv/boot/Image busybox-1.27.2-kvm-riscv64/_install/apps
cd busybox-1.27.2-kvm-riscv64/_install; find ./ | cpio -o -H newc > ../../rootfs_kvm_riscv64.img; cd -

これで作業ディレクトリの先頭にrootfs_kvm_riscv64.imgが出来上がる。これがQEMUで実行すべきイメージの本体だ。

早速QEMUで実行してみたいと思う。

$ ./qemu/riscv64-softmmu/qemu-system-riscv64 -cpu rv64,x-h=true -M virt -m 512M \
    -nographic -bios opensbi/build/platform/generic/firmware/fw_jump.bin \
    -kernel ./build-riscv64/arch/riscv/boot/Image -initrd ./rootfs_kvm_riscv64.img \
    -append "root=/dev/ram rw console=ttyS0 earlycon=sbi"

注記:こっちの方が正しい

./qemu/riscv64-softmmu/qemu-system-riscv64 -cpu rv64,x-h=true -M virt -m 512M -nographic -bios opensbi/opensbi/build/platform/generic/firmware/fw_jump.bin -kernel opensbi/build-riscv64/arch/riscv/boot/Image -initrd ./rootfs_kvm_riscv64.img -append "root=/dev/ram rw console=ttyS0 earlycon=sbi"
[    0.641813] mousedev: PS/2 mouse device common for all mice
[    0.649317] goldfish_rtc 101000.rtc: registered as rtc0
[    0.651412] goldfish_rtc 101000.rtc: setting system clock to 2020-09-25T12:05:29 UTC (1601035529)
[    0.656422] syscon-poweroff soc:poweroff: pm_power_off already claimed (____ptrval____) sbi_shutdown
[    0.658333] syscon-poweroff: probe of soc:poweroff failed with error -16
[    0.661627] usbcore: registered new interface driver usbhid
[    0.662493] usbhid: USB HID core driver
[    0.666716] NET: Registered protocol family 10
[    0.677816] Segment Routing with IPv6
[    0.679190] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
[    0.685464] NET: Registered protocol family 17
[    0.689903] 9pnet: Installing 9P2000 support
[    0.691462] Key type dns_resolver registered
[    0.750053] Freeing unused kernel memory: 232K
[    0.753651] Run /init as init process
           _  _
          | ||_|
          | | _ ____  _   _  _  _
          | || |  _ \| | | |\ \/ /
          | || | | | | |_| |/    \
          |_||_|_| |_|\____|\_/\_/

               Busybox Rootfs

Please press Enter to activate this console.
/ #

いいぞ、起動した。次にLinux上でKVMを立ち上げてされにLinuxを起動する。

$ ./apps/lkvm-static run -m 128 -c2 --console serial -p "console=ttyS0 earlycon=uart8250,mmio,0x3f8" -k ./apps/Image --debug
          | ||_|
          | | _ ____  _   _  _  _
          | || |  _ \| | | |\ \/ /
          | || | | | | |_| |/    \
          |_||_|_| |_|\____|\_/\_/

               Busybox Rootfs

Please press Enter to activate this console.
/ # ./apps/lkvm-static run -m 128 -c2 --console serial -p "console=ttyS0 earlyco
n=uart8250,mmio,0x3f8" -k ./apps[   15.277346] random: fast init done
/Image --debug
  # lkvm run -k ./apps/Image -m 128 -c 2 --name guest-45
  Info: (riscv/kvm.c) kvm__arch_load_kernel_image:116: Loaded kernel to 0x80200000 (17191724 bytes)
  Info: (riscv/kvm.c) kvm__arch_load_kernel_image:128: Placing fdt at 0x81800000 - 0x87ffffff
  # Warning: The maximum recommended amount of VCPUs is 1
  Info: (virtio/mmio.c) virtio_mmio_init:326: virtio-mmio.devices=0x200@0x10000000:5
  Info: (virtio/mmio.c) virtio_mmio_init:326: virtio-mmio.devices=0x200@0x10000200:6
  Info: (virtio/mmio.c) virtio_mmio_init:326: virtio-mmio.devices=0x200@0x10000400:7
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[    0.000000] Linux version 5.9.0-rc3-00338-g0b92c1839cb1 (msyksphinz@DESKTOP-P42Q0NR) (riscv64-unknown-linux-gnu-gcc (crosstool-NG 1.24.0) 8.3.0, GNU ld (crosstool-NG 1.24.0) 2.32) #1 S
MP Thu Sep 24 23:07:36 JST 2020
[    0.000000] earlycon: uart8250 at MMIO 0x00000000000003f8 (options '')
[    0.000000] printk: bootconsole [uart8250] enabled
[    0.000000] Zone ranges:
[    0.000000]   DMA32    [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000]   Normal   empty
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] software IO TLB: mapped [mem 0x83e3b000-0x87e3b000] (64MB)
[    0.000000] SBI specification v0.1 detected
[    0.000000] riscv: ISA extensions acdfimsu
[    0.000000] riscv: ELF capabilities acdfim
...
[    5.766641] IP-Config: Got DHCP answer from 192.168.33.1, my address is 192.168.33.15
[    5.817703] IP-Config: Complete:
[    5.838870]      device=eth0, hwaddr=02:15:15:15:15:15, ipaddr=192.168.33.15, mask=255.255.255.0, gw=192.168.33.1
[    5.895798]      host=192.168.33.15, domain=, nis-domain=(none)
[    5.931718]      bootserver=192.168.33.1, rootserver=0.0.0.0, rootpath=
[    5.931835]      nameserver0=192.168.33.1
[    6.059811] VFS: Mounted root (9p filesystem) on device 0:15.
[    6.115593] devtmpfs: mounted
[    6.193090] Freeing unused kernel memory: 232K
[    6.264232] Run /virt/init as init process
Mounting...
/ # [   10.820410] random: fast init done

Linuxの上でLinuxが立ち上がったぞ(正確にはWindows上で動かしているWSLの上でQEMUを立ち上げてそのうえでRISC-V Linuxを動かしその上でゲストのRISC-V Linuxを動かしている)ややこしい。

f:id:msyksphinz:20200925211715g:plain