iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 22
0

前情提要


昨日實作了部份的 objdump 工具程式,大致上是完成了讀取檔案和傳送機器編碼的部份。今天就讓我們接著實作下去吧!

回顧:設計大綱


筆者根據自己的使用經驗與理解,簡化反組譯的過程,列出以下步驟:

  1. 讀取 ELF 檔案
  2. .symtab 或是同樣屬性的區段取得標籤
    1. 依位址排序完成
  3. 取出 .text 區段內容
    1. 將相對應的區段的內容餵給 rvgc 函式庫的 BinToInst
  4. 印出回傳的內容,將標籤嵌入在正確的位置

本日相當於是要從 3.1 的部份繼續下去。BinToInst 的實作大概不難,但是想必免不了繁雜的解碼動作。

重返 rvgc 函式庫


雖然解碼本身是一個蘿蔔一個坑的動作,但還是有許多可以引用的優化技巧;只是筆者在這裡就先不引用那些優化的方法,以功能正確(還有能夠預期完賽)為最高指導原則來實作。目標是一個能夠作用的 BinToInst 函式。筆者這裡的設計是,將判斷指令型態(R 型態、I 型態等)與識別字(add、jalr 等等指令名稱)交給一個名為 instType 的函數負責,之後 BinToInst 函式負責解碼。

BinToInst 函式

func BinToInst(bin []byte) string {

        nm, t := instType(bin)
        rd := bits2regs[(bin[1]&0x0f)<<1+bin[0]>>7]
        rs1 := bits2regs[(bin[2]&0x0f)<<1+bin[1]>>7]
        rs2 := bits2regs[(bin[2]&0xf0)>>4+(bin[2]&0x01)<<4]
        switch t {
        case RV_INST_R_TYPE:
                return nm + " " + rd + "," + rs1 + "," + rs2
        case RV_INST_I_TYPE:
                imm := bin[2]>>4 + bin[3]<<4
                return nm + " " + rd + "," + rs1 + "," + strconv.FormatUint(imm, 16)
        case RV_INST_S_TYPE:
                imm := (bin[3]&0xfe)<<5 + (bin[1]&0x0f)<<1 + bin[0]>>7
                return nm + " " + rs1 + "," + rs2 + "," + strconv.FormatUint(imm, 16)
        case RV_INST_B_TYPE:
                imm := ((bin[3]&0x80)<<4 + (bin[0]&0x80)<<3 + (bin[3]&0x7e)<<3 + (bin[1] & 0x0f))<<1
                return nm + " " + rs1 + "," + rs2 + "," + strconv.FormatUint(imm, 16)
        case RV_INST_U_TYPE:
                imm := bin[3]<<24 + bin[2]<<16 + (bin[1]&0xf0)<<8
                return nm + " " + rd + "," + strconv.FormatUint(imm, 16)
        case RV_INST_J_TYPE:
        default:
                return "noimp"
        }
}

這時候就完全可以看見 RISC-V 指令集的漂亮設計!暫存器的位置始終相同,解碼指令和解析暫存器的工作就能夠獨立於指令型態,這裡一開始就能取得 rd、rs1、及 rs2 暫存器,雖然不同型態不一定會全部用到,但是整理起來也有精簡化程式碼的作用。

為求簡便,筆者將讀取系列指令的回傳字串格式也統一做其餘運算指令的格式。且 J 型態的指令(也就是 jal) 暫且不予支援。

instType 函式

一道機器編碼的輸入,只須判別它的 opcode 及 funct3 區域,就能夠得知該指令識別字;有了識別字之後,當然也就能夠判讀該指令的型態為何。少數指令的次級判斷指標在 funct7 區域,在那些分類之中判斷即可。

func instType(bin []byte) (string, RV_INST_TYPE) {
        var nm string
        var t RV_INST_TYPE

        switch RV_OPCODE_TYPE(bin[0] & 0x7f) {

這個指令型態的判斷就是骨幹,取 bin[0] 的最後 7 個 bit,以下就分別展開:

        case RV_OPCODE_LOAD:
                t = RV_INST_R_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        nm = "lb"
                case 1:
                        nm = "lh"
                case 2:
                        nm = "lw"
                case 3:
                        nm = "ld"
                case 4:
                        nm = "lbu"
                case 5:
                        nm = "lhu"
                case 6:
                        nm = "lwu"
                }
                return nm, t

這是讀取的指令,根據 funct3 區域判斷,也就是在 bin[1] 中的 0x70 所對應的 3 個 bit。

        case RV_OPCODE_OP_IMM:
                t = RV_INST_I_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        nm = "addi"
                case 1:
                        nm = "slli"
                case 2:
                        nm = "slti"
                case 3:
                        nm = "sltiu"
                case 4:
                        nm = "ori"
                case 5:
                        if bin[3]&0x40 == 0 {
                                nm = "srli"
                        } else {
                                nm = "srai"
                        }
                case 6:
                        nm = "xori"
                case 7:
                        nm = "andi"
                }
                return nm, t

這是有內嵌整數的運算指令。可以看到邏輯右移與算術右移共用 funct3 的值,因此必須判斷 funct7 區域(位在 bin[3] 的第二高位 bit)。以下的 add-sub 指令也需要類似的判斷。

        case RV_OPCODE_AUIPC:
                return "auipc", RV_INST_U_TYPE

類別較小的指令就可以直接回傳了。

        case RV_OPCODE_OP_IMM_32:
                t = RV_INST_I_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        nm = "addiw"
                case 1:
                        nm = "slliw"
                case 5:
                        if bin[3]&0x40 == 0 {
                                nm = "srliw"
                        } else {
                                nm = "sraiw"
                        }
                }
                return nm, t
        case RV_OPCODE_STORE:
                t = RV_INST_S_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        nm = "sb"
                case 1:
                        nm = "sh"
                case 2:
                        nm = "sw"
                case 3:
                        nm = "sd"
                }
                return nm, t
        case RV_OPCODE_OP:
                t = RV_INST_R_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        if bin[3]&0x40 == 0 {
                                nm = "add"
                        } else {
                                nm = "sub"
                        }
                case 1:
                        nm = "sll"
                case 2:
                        nm = "slt"
                case 3:
                        nm = "sltu"
                case 4:
                        nm = "or"
                case 5:
                        if bin[3]&0x40 == 0 {
                                nm = "srl"
                        } else {
                                nm = "sra"
                        }
                case 6:
                        nm = "xor"
                case 7:
                        nm = "and"
                }
                return nm, t
        case RV_OPCODE_LUI:
                return "lui", RV_INST_U_TYPE
        case RV_OPCODE_OP_32:
                t = RV_INST_R_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        if bin[3]&0x40 == 0 {
                                nm = "addw"
                        } else {
                                nm = "subw"
                        }
                case 1:
                        nm = "sllw"
                case 5:
                        if bin[3]&0x40 == 0 {
                                nm = "srlw"
                        } else {
                                nm = "sraw"
                        }
                }
                return nm, t
        case RV_OPCODE_BRANCH:
                t = RV_INST_B_TYPE
                switch (bin[1] & 0x70) >> 4 {
                case 0:
                        nm = "beq"
                case 1:
                        nm = "bne"
                case 4:
                        nm = "blt"
                case 5:
                        nm = "bgt"
                case 6:
                        nm = "bltu"
                case 7:
                        nm = "bgtu"
                }
                return nm, t
        case RV_OPCODE_JALR:
                return "jalr", RV_INST_I_TYPE
        case RV_OPCODE_JAL:
                return "jal", RV_INST_J_TYPE
        case RV_OPCODE_SYSTEM:
                return "ecall", RV_INST_NONE
        }
        return "noimp", RV_INST_NONE
}

輸出


這個部份的挑戰在於,要將第二步排序好的標籤依照反組譯順序嵌入到正確的地方,否則將沒有什麼可讀性可言。但是筆者決定把這個部份留到日後再擴充吧!

以之前的靜態程式當作例子,直接將組語輸出,結果大概是這樣:

addi sp,sp,f0
addi t0,zero,48
sb t0,sp,0
addi t0,zero,69
sb t0,sp,1
addi t0,zero,21
sb t0,sp,2
addi t0,zero,a
sb t0,sp,3
addi t1,zero,4
addi a0,zero,1
add a1,zero,sp
addi a2,zero,4
addi a7,zero,40
ecall
addi t1,t1,ff
bne t1,zero,e8
lui t2,44434000
addi t2,t2,41
sw t2,sp,0
addi a0,zero,1
add a1,zero,sp
addi a2,zero,4
addi a7,zero,40
ecall
addi a0,zero,9
addi a7,zero,5d
ecall

沒有任何排版,但是筆者心中的激昂感已經把它排版好了!!

小結


今天終於結束了 objdump 工具程式的實作!本來按照規劃,接下來要回頭完成 strip 以及接下來的 objcopy,但是有鑑於時間已經不太夠用,筆者決定直接殺入 ld 的領域,帶領各位讀者一同更深入 ELF 檔案的可能性;也就是說,當系列文結束之時,我們將會能夠比起以前更有信心能夠回答以下問題:

  • 連結是什麼?
  • ELF 檔如何支援連結的功能?
  • RISC-V 架構在連結上有什麼特殊的地方?
  • 程式檔頭是什麼?

最後一個禮拜的鐵人挑戰,再加把勁!各位讀者,我們明日再會!


上一篇
第二十一日:objdump 實作之一
下一篇
第二十三日:連結的概念
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言