QEMUによる分岐命令の実現方法が分かったので、次はロードストア命令について実現方法を見ていきたいと思う。例えばRISC-VにおけるLD
命令とSD
命令を実行する場合、どのようにx86命令に変換されるのだろうか?
LD x10, 0(data) SD x10, 8(data)
以下のようなテストベンチを作って動作を確認してみる。
.section .text _start: la x10, data_region j 1f 1: ld x11, 0(x10) j 1f 1: addi x11, x11, 0x100 j 1f 1: sd x11, 8(x10) .section .data data_region: .word 0xdeadbeef
ロードストア命令のの直後にジャンプ文を入れているのは、ジャンプすることで必ずそこで分岐が発生するのでブロックが小さくなり、生成されるx86コードを小さくして確認しやすくする。
以下のようにしてテストコードをコンパイルし実行する。
$ make load_test.asm.riscv riscv64-unknown-elf-gcc -march=rv64g -O3 -o load_test.riscv load_test.S -DPREALLOCATE=1 -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -static -nostdlib -nostartfiles -lm -lgcc -Ttest.ld /home/msyksphinz/riscv64/lib/gcc/riscv64-unknown-elf/7.2.0/../../../../riscv64-unknown-elf/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000080000000 riscv64-unknown-elf-objdump -d -r load_test.riscv > load_test.riscv.dmp
QEMUで実行して動作を確認する。
qemu-system-riscv64 --machine virt --d in_asm,op,op_opt,op_ind,out_asm \ --nographic --trace enable=myriscvx_trap \ --kernel load_test.riscv 2>&1 | tee load_test.riscv.log
対象となるのは以下の命令だ。
IN: Priv: 3; Virt: 0 0x000000008000000c: 00053583 ld a1,0(a0) 0x0000000080000010: 0040006f j 4 # 0x80000014
以下のようなTCGに変換されることが分かる。qemu_ld_i64
というのがポイントらしい。
mov_i64 tmp2,x10/a0 qemu_ld_i64 tmp3,tmp2,leq,3 mov_i64 x11/a1,tmp3
これはgen_ldst_i64
によって生成されていることが分かる。
qemu/tcg/tcg-op.c
void tcg_gen_qemu_ld_i64(TCGv_i64 val, TCGv addr, TCGArg idx, MemOp memop) { MemOp orig_memop; uint16_t info; ... addr = plugin_prep_mem_callbacks(addr); gen_ldst_i64(INDEX_op_qemu_ld_i64, val, addr, memop, idx); plugin_gen_mem_callbacks(addr, info); ...
qemu_ld_i64
がどのように変換されるのかを見てみる。
qemu/tcg/i386/tcg-target.inc.c
static inline void tcg_out_op(TCGContext *s, TCGOpcode opc, const TCGArg *args, const int *const_args) { TCGArg a0, a1, a2; int c, const_a2, vexop, rexw = 0; ... switch (opc) { case INDEX_op_exit_tb: /* Reuse the zeroing that exists for goto_ptr. */ if (a0 == 0) { ... case INDEX_op_qemu_ld_i64: tcg_out_qemu_ld(s, args, 1); break; ...
qemu/tcg/i386/tcg-target.inc.c
/* XXX: qemu_ld and qemu_st could be modified to clobber only EDX and EAX. It will be useful once fixed registers globals are less common. */ static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args, bool is64) { TCGReg datalo, datahi, addrlo; TCGReg addrhi __attribute__((unused)); TCGMemOpIdx oi; MemOp opc; #if defined(CONFIG_SOFTMMU) int mem_index; tcg_insn_unit *label_ptr[2]; #endif ... #if defined(CONFIG_SOFTMMU) mem_index = get_mmuidx(oi); tcg_out_tlb_load(s, addrlo, addrhi, mem_index, opc, label_ptr, offsetof(CPUTLBEntry, addr_read)); /* TLB Hit. */ tcg_out_qemu_ld_direct(s, datalo, datahi, TCG_REG_L1, -1, 0, 0, is64, opc); /* Record the current context of a load into ldst label */ add_qemu_ldst_label(s, true, is64, oi, datalo, datahi, addrlo, addrhi, s->code_ptr, label_ptr); #else ...
どうもTLBの設定の後で、ロード命令が生成されるらしい。 この辺りのTCGの構成が良く分からなかった。うーん、何をしているんだ?
static inline void tcg_out_tlb_load(TCGContext *s, TCGReg addrlo, TCGReg addrhi, int mem_index, MemOp opc, tcg_insn_unit **label_ptr, int which) { ...
そして、tcg_out_qemu_ld_direct
によりメモリアドレスのロードが行われるらしい。
static void tcg_out_qemu_ld_direct(TCGContext *s, TCGReg datalo, TCGReg datahi, TCGReg base, int index, intptr_t ofs, int seg, bool is64, MemOp memop) ... case MO_Q: if (TCG_TARGET_REG_BITS == 64) { tcg_out_modrm_sib_offset(s, movop + P_REXW + seg, datalo, base, index, 0, ofs); if (bswap) { tcg_out_bswap64(s, datalo); }
どうも良く分からないので、生成されるx86のアセンブリコードを読み解いていくことにしてみる。
OUT: [size=152] 0x7f4b44000500: 8b 5d f0 movl -0x10(%rbp), %ebx 0x7f4b44000503: 85 db testl %ebx, %ebx 0x7f4b44000505: 0f 8c 55 00 00 00 jl 0x7f4b44000560 // x10の値をロードする。 // tcg_out_mov(s, tlbtype, r0, addrlo); 0x7f4b4400050b: 48 8b 5d 50 movq 0x50(%rbp), %rbx // rdi = reg[x10] // rdi = reg[x10] >> 7 // tcg_out_shifti(s, SHIFT_SHR + tlbrexw, r0, // TARGET_PAGE_BITS - CPU_TLB_ENTRY_BITS); 0x7f4b4400050f: 48 8b fb movq %rbx, %rdi 0x7f4b44000512: 48 c1 ef 07 shrq $7, %rdi // tcg_out_modrm_offset(s, OPC_AND_GvEv + trexw, r0, TCG_AREG0, // TLB_MASK_TABLE_OFS(mem_index) + // offsetof(CPUTLBDescFast, mask)); 0x7f4b44000516: 48 23 7d e0 andq -0x20(%rbp), %rdi // tcg_out_modrm_offset(s, OPC_ADD_GvEv + hrexw, r0, TCG_AREG0, // TLB_MASK_TABLE_OFS(mem_index) + // offsetof(CPUTLBDescFast, table)); 0x7f4b4400051a: 48 03 7d e8 addq -0x18(%rbp), %rdi // tcg_out_modrm_offset(s, OPC_LEA + trexw, r1, addrlo, s_mask - a_mask); 0x7f4b4400051e: 48 8d 73 07 leaq 7(%rbx), %rsi // tgen_arithi(s, ARITH_AND + trexw, r1, tlb_mask, 0); 0x7f4b44000522: 48 81 e6 00 f0 ff ff andq $0xfffff000, %rsi /* cmp 0(r0), r1 */ // tcg_out_modrm_offset(s, OPC_CMP_GvEv + trexw, r1, r0, which); 0x7f4b44000529: 48 3b 37 cmpq (%rdi), %rsi /* Prepare for both the fast path add of the tlb addend, and the slow path function argument setup. */ // tcg_out_mov(s, ttype, r1, addrlo); 0x7f4b4400052c: 48 8b f3 movq %rbx, %rsi /* jne slow_path */ // tcg_out_opc(s, OPC_JCC_long + JCC_JNE, 0, 0, 0); 0x7f4b4400052f: 0f 85 37 00 00 00 jne 0x7f4b4400056c // tcg_out_modrm_offset(s, OPC_ADD_GvEv + hrexw, r1, r0, // offsetof(CPUTLBEntry, addend)); 0x7f4b44000535: 48 03 77 18 addq 0x18(%rdi), %rsi // 実際のメモリロードは以下。 0x7f4b44000539: 48 8b 1e movq (%rsi), %rbx // ロード結果をx11に格納する。 0x7f4b4400053c: 48 89 5d 58 movq %rbx, 0x58(%rbp) 0x7f4b44000540: 66 66 90 nop // J命令にジャンプ。 0x7f4b44000543: e9 00 00 00 00 jmp 0x7f4b44000548 0x7f4b44000548: bb 14 00 00 80 movl $0x80000014, %ebx 0x7f4b4400054d: 48 89 9d 00 02 00 00 movq %rbx, 0x200(%rbp) 0x7f4b44000554: 48 8d 05 e5 fe ff ff leaq -0x11b(%rip), %rax 0x7f4b4400055b: e9 b8 fa ff ff jmp 0x7f4b44000018 0x7f4b44000560: 48 8d 05 dc fe ff ff leaq -0x124(%rip), %rax 0x7f4b44000567: e9 ac fa ff ff jmp 0x7f4b44000018 // slow pathの場合はここに飛ぶ。 0x7f4b4400056c: 48 8b fd movq %rbp, %rdi 0x7f4b4400056f: ba 33 00 00 00 movl $0x33, %edx 0x7f4b44000574: 48 8d 0d c1 ff ff ff leaq -0x3f(%rip), %rcx 0x7f4b4400057b: ff 15 0f 00 00 00 callq *0xf(%rip) 0x7f4b44000581: 48 8b d8 movq %rax, %rbx 0x7f4b44000584: e9 b3 ff ff ff jmp 0x7f4b4400053c 0x7f7580000589: 90 nop 0x7f758000058a: 90 nop 0x7f758000058b: 90 nop 0x7f758000058c: 90 nop 0x7f758000058d: 90 nop 0x7f758000058e: 90 nop 0x7f758000058f: 90 nop 0x7f7580000590: .quad 0x000000000048e5ae
slow_path
はTLBに当該アドレスがヒットしない場合に呼び出されるのだが、この時の仕組みはどのようになっているのかというと、
- TLBにヒットしない場合、
jne 0x7f4b4400056c
が呼び出されることによりslow_path
を用意するルーチンに飛ぶ。 - 第1引数:
movq %rbp, %rdi
:rbp
つまりenv
が設定される。 - 第2引数:
rsi
: TLBエントリの場所が設定される。 - 第3引数:
rdx
: 0x33、これは引数としてはsize
だが、何のサイズなのか良く分からない。 - 第4引数:
rcx
:retaddr
これはTLB処理を行った後にどこに戻ってくるのかを記憶するためのものらしい。現在の場所から-0x3f
なので0x7f4b4400053c
となり、ちょうどロード命令の場所だ。 callq *0xf(%rip)
により現在の場所よりも0xf
離れた場所に格納されたアドレス、つまり0x000000000048e5ae
にジャンプする。
この0x000000000048e5ae
は、helper_le_ldq_mmu
であり、helper_le_ldq_mmu()
が呼び出されることが分かる。
000000000048e5ae <helper_le_ldq_mmu>: 48e5ae: 55 push %rbp 48e5af: 48 89 e5 mov %rsp,%rbp 48e5b2: 48 83 ec 20 sub $0x20,%rsp 48e5b6: 48 89 7d f8 mov %rdi,-0x8(%rbp) 48e5ba: 48 89 75 f0 mov %rsi,-0x10(%rbp) 48e5be: 89 55 ec mov %edx,-0x14(%rbp) 48e5c1: 48 89 4d e0 mov %rcx,-0x20(%rbp) 48e5c5: 48 8b 4d e0 mov -0x20(%rbp),%rcx 48e5c9: 8b 55 ec mov -0x14(%rbp),%edx 48e5cc: 48 8b 75 f0 mov -0x10(%rbp),%rsi 48e5d0: 48 8b 45 f8 mov -0x8(%rbp),%rax 48e5d4: 48 83 ec 08 sub $0x8,%rsp 48e5d8: 68 ae e5 48 00 pushq $0x48e5ae 48e5dd: 41 b9 00 00 00 00 mov $0x0,%r9d 48e5e3: 41 b8 03 00 00 00 mov $0x3,%r8d 48e5e9: 48 89 c7 mov %rax,%rdi 48e5ec: e8 c0 f7 ff ff callq 48ddb1 <load_helper> 48e5f1: 48 83 c4 10 add $0x10,%rsp 48e5f5: c9 leaveq 48e5f6: c3 retq
qemu/accel/tcg/cputlb.c
uint64_t helper_be_ldq_mmu(CPUArchState *env, target_ulong addr, TCGMemOpIdx oi, uintptr_t retaddr) { return load_helper(env, addr, oi, retaddr, MO_BEQ, false, helper_be_ldq_mmu); }
ここはもうホストマシンのコードであり、これがslow_path()
と呼ばれるゆえんである。ここは普通にC言語で記述されたコードが実行され、TLB Fillが行われる。
最終的に実行されるのはriscv_cpu_tlb_fill()
だ。これで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; ...
--d mmu
オプションを追加すると、TLBがFillされるログが表示される。
riscv_cpu_tlb_fill ad 80000000 rw 2 mmu_idx 3 riscv_cpu_tlb_fill address=80000000 ret 0 physical 0000000080000000 prot 7