iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 14
0

前情提要


各位讀者,新年快樂!昨日在 rvgc 函式庫中加入了 R-type 指令的轉譯,看起來至少 add 指令的翻譯是正確的。今天的目標則是把剩下的數十道陰影指令處理掉。

I-type


不像 R-type 指令都囊括在算術邏輯指令的集合之中,目前為止我們接觸到的 I-type 指令分散在三種類別裡面:

  • 讀取指令:rd = rs1[imm]
  • OP-IMM 家族的算術邏輯指令,其中一個運算元是 12-bit 的整數:rd = rs1 <運算子> imm
    • 挪移指令稍微不太一樣,傳入的整數只有至多 6 個 bit
    • 只有算術右移(sra*)指令的 funct7 欄位有 0010 00 的內容,其他挪移指令都是全 0
  • jalr 指令:rd = pc+4; pc = rs1[imm]
    • rd 若是 ra 暫存器,這個指令就相當於一個呼叫(能夠透過 ret 返回)
    • GNU 工具鏈很喜歡直接拿 ra 來當作 rs1

他們的指令樣貌則分別類似這樣:

jalr    -4(ra) 
lw      a3,40(tp)
addi    a4,a4,1806 
ld      a2,1872(s6)
sraiw   a5,a3,0x1f
srliw   a5,a5,0x1a
sraiw   a5,a5,0x6
slli    a5,a5,0x3

別忘了我們昨日的字串前處理:所有逗點和括弧都會被當作空白來讀入。所以,讀取指令和 jalr 的**整數 - rs1 **順序與算術邏輯單元的順序會剛好相反,我們可以透過 opcode 的比較來完成。處理起來就會像這樣子:

                var isop int
                if (op == RV_OPCODE_OP_IMM) || (op == RV_OPCODE_OP_32_IMM) {
                        isop = 1
                }

另外一個分歧的部份是挪移指令。按照 objdump 的反組譯輸出結果來看,在整個 I-type 指令中,只有挪移指令的嵌入整數部份是用 16 進位呈現的。我們想要按這個慣例來撰寫組合語言程式的話,就必須判斷該指令是挪移與否?這個部份,筆者選擇用 funct3 欄位的值來判斷,

                f3 := funct3[inst[1]]
                var issh int
                if f3 == 0x01 || f3 == 0x05 {
                        issh = 1
                }

其中,1 和 5 分別代表左移與右移使用的 funct3 數值。

有了這兩個判斷用旗標之後,我們就可以依次取得相關欄位,然後使用類似的 bit 處理方法整合成輸出結果:

                rd := reg2bits[inst[1]]
                rs1 := reg2bits[inst[2+isop]]
                if issh {
                        shamt := strconv.ParseInt(inst[3-isop], 16, 6)

                        var f6 uint32
                        if inst[0] == "srai" || inst[0] == "sraiw" {
                                f6 = 0x10
                        } else {
                                f6 = 0
                        }

                        bits = f6<<26 | shamt<<20 | rs1<<15 | f3<<12 | rd<<7 | uint32(op)
                } else {
                        imm := strconv.ParseInt(inst[3-isop], 10, 12)
                        bits = uint32(imm)<<20 | rs1<<15 | f3<<12 | rd<<7 | uint32(op)
                }

筆者引用了 strconv 函式庫的 ParseInt 方法,這能夠達成我們所需要的:支援不同的底數與不同的空間大小。

S-type

這個類型只有讀取系列的四個指令,輕鬆寫意:

        case RV_INST_S_TYPE:
                f3 := funct3[inst[1]]
                rs2 := reg2bits[inst[1]]
                imm, _ := strconv.ParseInt(inst[2], 10, 12)
                rs1 := reg2bits[inst[3]]

                bits = uint32(imm) << 20
                bits &= 0xfe000000
                bits |= (rs2<<20 | rs1<<15 | f3<<12 | (uint32(imm)&0x1f)<<7 | uint32(op))

雖然指令位元格式不同,但是在組語檔案的語法呈獻是相同的,所以 inst[2] 會是整數而 inst[3] 是用來當作記憶體位置基準點的 rs1 暫存器,除此之外這個部份應該簡單明瞭。但畢竟 S-type 的嵌入整數部份是分割到 rd 位置去,所以後面在處理位元的時候稍微囉唆一點。

B-type

這個部份的巨大挑戰大概就是錯綜複雜的 imm 編碼了吧,

        case RV_INST_B_TYPE:
                f3 := funct3[inst[0]]
                rs1 := reg2bits[inst[1]]
                rs2 := reg2bits[inst[2]]
                imm, _ := strconv.ParseInt(inst[3], 16, 20)

                imm12 := uint32((imm & 0x800) >> 11)
                imm11 := uint32((imm & 0x400) >> 10)
                imm10_5 := uint32((imm & 0x3f0) >> 4)
                imm4_1 := uint32(imm & 0x00f)

                bits = imm12<<31 | imm10_5<<25 | rs2<<20 | rs1<<15 | f3<<12 | imm4_1<<8 | imm11<<7 | uint32(op)

U-type

將 20 個位元載入到 rd 暫存器的上半部的指令有 lui 和 auipc,因為格式簡單,所以問題也不大,但須注意整數部份會用 16 進位表示:

        case RV_INST_U_TYPE:
                rd := reg2bits[inst[1]]
                imm, _ := strconv.ParseInt(inst[2], 16, 20)
                bits = uint32(imm)<<12 | rd<<7 | uint32(op)

J-type(其實也只有 jal 指令)

由於可以搭配 auipc 和 jalr 完成任何距離當前 pc 位址 32-bit 範圍內的跳躍,因此筆者這裡決定先跳過 jal 指令的支援。然而,不得不理解到的是,jal 指令是 relaxation 這個連結期優化的一個重要手段,日後如果系列文有機會推進到連結器時再說明。

其他(ecall 指令)

ecall 指令使用的是系統指令 opcode,但是整個指令並不需要其他欄位的資訊而全部設成零。我們借用 go 語言的初始化自動設零功能,這裡直接給值就可以了:

        case RV_INST_NONE:
                bits |= uint32(op)

小結


今日我們完成了全部的基本整數指令編碼轉換程式!文章的份量是比較少,但是為了寫這些 code 也是很花時間的。是時候再規劃一下接下來的方向了。讀者諸君,我們明日再會!


上一篇
第十三日:實作 rvgc 函式庫之一
下一篇
第十五日:rvgc 函式庫測試--靜態連結程式
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言