rv32emu next - ccc-sp/riscv2os GitHub Wiki

rv32emu-next -- RISC-V 虛擬機實作

install

guest@localhost:~/rv32emu-next$ su root
Password:
root@localhost:/home/guest/rv32emu-next# sudo apt install libsdl2-dev
Reading package lists... Done
Building dependency tree       
Reading state information... Done
libsdl2-dev is already the newest version (2.0.8+dfsg1-1ubuntu1.18.04.4).
0 upgraded, 0 newly installed, 0 to remove and 416 not upgraded.

make & run

guest@localhost:~/rv32emu-next$ make
  CC    c_map.o
  CC    riscv.o
  CC    io.o
  CC    elf.o
  CC    main.o
  CC    syscall.o
  CC    syscall_sdl.o
  LD    build/rv32emu
guest@localhost:~/rv32emu-next$ make check
(cd build; ../build/rv32emu hello.elf)
Hello World!
inferior exit code 0
(cd build; ../build/rv32emu puzzle.elf)
success in 2005 trials
inferior exit code 0

工作流程

  1. IO 以及記憶體、虛擬機初始化。
  2. run() or run_and_trace() 根據不同情況決定每個 cycle 要執行多少個指令後再呼叫 rv_step()
  3. rv_step() 拿出指令並判斷其類型 (load, jump, store, branch...) 後呼叫 op() ,讓其做 dispatch 和後續動作。
  • rv_step() 達到 cycle 目標之前重複以下動作:

    1. 將指令從 pc 指向的記憶體位置讀取出來。
    2. 讀取出來之後,將指令交給 op handler: op() 進行處理。
    • op() 在 riscv.c 的第 787 行處有預先定義好 RV32I 各類指令的 opcode (前五碼)。
    static const opcode_t opcodes[] = {
    //  000        001          010       011          100        101       110   111
        op_load,   op_load_fp,  NULL,     op_misc_mem, op_op_imm, op_auipc, NULL, NULL, // 00
        op_store,  op_store_fp, NULL,     op_amo,      op_op,     op_lui,   NULL, NULL, // 01
        op_madd,   op_msub,     op_nmsub, op_nmadd,    op_fp,     NULL,     NULL, NULL, // 10
        op_branch, op_jalr,     NULL,     op_jal,      op_system, NULL,     NULL, NULL, // 11
    };        
    

    只定義前 5 碼是因為 RV32I opcode 的後兩碼都是固定的 (xxxxx11),我們也可以在 rv_step() 中看到待執行指令 inst 的預處理:

    // standard uncompressed instruction
        if ((inst & 3) == 3) {
            const uint32_t index = (inst & INST_6_2) >> 2;
    

    簡單來說就是判斷 inst 是否屬於 RV32I 指令(末兩碼是否為 11 。),如果是的話我們就將末兩碼移除並且做 right_shift

    補充 1 : INST_6_2 定義在 riscv_private.h 中,其值為 0b00000000000000000000000001111100

    補充 2 : 除了 RVC 指令集外,其他合法 RISC-V 指令集的 OPCODE 末兩碼都是 11

    題外話: 在上面的原始碼中就有大量的 bitwise 操作,再次凸顯它的重要性。

  • op() op 其實只是函式指標,透過 rv_step() 指定指令的 handler 後,再去做相關操作。 這邊以 op_op_imm() 這個關於整數操作的 handler 舉例:

        static bool op_op_imm(struct riscv_t *rv, uint32_t inst)
    {
        // i-type decode
        const int32_t imm = dec_itype_imm(inst);
        const uint32_t rd = dec_rd(inst);
        const uint32_t rs1 = dec_rs1(inst);
        const uint32_t funct3 = dec_funct3(inst);
    
        // dispatch operation type
        switch (funct3) {
        case 0:  // ADDI
            rv->X[rd] = (int32_t)(rv->X[rs1]) + imm;
            break;
        case 1:  // SLLI
            rv->X[rd] = rv->X[rs1] << (imm & 0x1f);
            break;
        case 2:  // SLTI
            rv->X[rd] = ((int32_t)(rv->X[rs1]) < imm) ? 1 : 0;
            break;
        case 3:  // SLTIU
            rv->X[rd] = (rv->X[rs1] < (uint32_t) imm) ? 1 : 0;
            break;
        case 4:  // XORI
            rv->X[rd] = rv->X[rs1] ^ imm;
            break;
        case 5:
            if (imm & ~0x1f) {
                // SRAI
                rv->X[rd] = ((int32_t) rv->X[rs1]) >> (imm & 0x1f);
            } else {
                // SRLI
                rv->X[rd] = rv->X[rs1] >> (imm & 0x1f);
            }
            break;
        case 6:  // ORI
            rv->X[rd] = rv->X[rs1] | imm;
            break;
        case 7:  // ANDI
            rv->X[rd] = rv->X[rs1] & imm;
            break;
        default:
            rv_except_illegal_inst(rv);
            return false;
        }
    
        // step over instruction
        rv->PC += 4;
    
        // enforce zero register
        if (rd == rv_reg_zero)
            rv->X[rv_reg_zero] = 0;
        return true;
    }
    

    handler 會將傳入指令做解碼,也就是將傳入的 inst 根據上圖的 I type 切割成四塊:

    1. funct3
    2. imm
    3. rs1
    4. rd funct3 會決定要做哪一種操作,如: ADDI, SLLI, ORI... 。 假設現在要做的是 ADDI 操作,模擬器就會按照 RISC-V 中 ADDI 指令所定義的行為執行。

    定義: 常數部分為 sign-extended 12-bit ,會將 12-bit 做 sign-extension 成 32-bit 後,再與 rs1 暫存器做加法運算,將結果寫入 rd 暫存器,addi rd, rs1, 0 可被用來當做 mov 指令。

    將結果寫回相關暫存器後, handler 會將 Program counter 指到下一個記憶體位置後回傳結果。

參考文獻