各位讀者,新年快樂!昨日在 rvgc 函式庫中加入了 R-type 指令的轉譯,看起來至少 add 指令的翻譯是正確的。今天的目標則是把剩下的數十道陰影指令處理掉。
不像 R-type 指令都囊括在算術邏輯指令的集合之中,目前為止我們接觸到的 I-type 指令分散在三種類別裡面:
rd = rs1[imm]
rd = rs1 <運算子> imm
0010 00
的內容,其他挪移指令都是全 0rd = pc+4; pc = rs1[imm]
他們的指令樣貌則分別類似這樣:
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
方法,這能夠達成我們所需要的:支援不同的底數與不同的空間大小。
這個類型只有讀取系列的四個指令,輕鬆寫意:
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 位置去,所以後面在處理位元的時候稍微囉唆一點。
這個部份的巨大挑戰大概就是錯綜複雜的 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)
將 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)
由於可以搭配 auipc 和 jalr 完成任何距離當前 pc 位址 32-bit 範圍內的跳躍,因此筆者這裡決定先跳過 jal 指令的支援。然而,不得不理解到的是,jal 指令是
relaxation
這個連結期優化的一個重要手段,日後如果系列文有機會推進到連結器時再說明。
ecall 指令使用的是系統指令 opcode,但是整個指令並不需要其他欄位的資訊而全部設成零。我們借用 go 語言的初始化自動設零功能,這裡直接給值就可以了:
case RV_INST_NONE:
bits |= uint32(op)
今日我們完成了全部的基本整數指令編碼轉換程式!文章的份量是比較少,但是為了寫這些 code 也是很花時間的。是時候再規劃一下接下來的方向了。讀者諸君,我們明日再會!