iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 27
0

前情提要


昨日直接地閱讀 ELF 檔而找到這些重定的樣貌,今天我們來設法將這些資訊轉給 readelf,藉此累積一些重定內容的操作方式吧!

實作目標


由於我們時間也不多了,所以筆者預計只做一個 R_RISCV_CALL 的重定。為此,接下來的實作內容將分別是:

  • readelf 能夠支援 -r 參數
  • as 能夠輸出帶有重定資訊的物件檔
  • ld 能夠連結兩個物件檔使之成為可執行檔

由於目標很明確,所以我們也無須挑戰之前總是用來當作範例的 main.o 檔,它將來必須能夠與動態函式庫連結,同時又有諸多重定型態。筆者這裡重新設計一個測試程式,也當作本系列文最後的測試程式。這個測試程式也不是重新撰寫,而是將之前的 static 程式拆分成主函式 _start 與負責輸出的 write 函式的部份。

首先是進入點的 _start 函式所在的 fin2.s:

$ cat fin2.s
.section .text
_start:
.global _start
        addi sp, sp, -16
        lui t2, 0x44434
        addi t2, t2, 577
        sw t2, 0(sp)
        add a0, zero, sp
        call write
        addi a0, zero, 0
        addi a7, zero, 93
        ecall
.end

各位讀者可以注意到這裡 lui 指令的整數部加上了 0x 字首,這是之前我們的 as 還沒有支援的語法;因為之前我們是基於 objdump -d 之後的結果當作唯一支援的格式,所以整數部分會直接當作 16 進位數字處理。但是目前我們還沒有開始在組譯器 as 實作對應的功能,而必須使用 GNU 的 as,他們必須要有這些前綴才能夠正確判讀數字。

這裡將 t2 的內容存入 0x44434241 的整數,也就是字串 ABCD 的 ASCII 碼。之後我們將這個位置放在 a0 暫存器傳入 write 函式。最後的三行則是一個程式的職責:呼叫 exit 系列的系統呼叫來正常的結束。

再來是 write 函式正式定義的 fin1.s:

$ cat fin1.s
.section .text
write:
.global write
        addi sp, sp, -16
        sd ra, 8(sp)
        sd s0, 0(sp)
        addi s0, sp, 16
        add a1, zero, a0
        addi a0, zero, 1
        addi a2, zero, 4
        addi a7, zero, 64
        ecall
        ld ra, 8(sp)
        ld s0, 0(sp)
        addi sp, sp, 16
        jalr zero, 0(ra)
.end

必須注意 .global 的組譯器選項萬萬短少不得,否則 write 無法被連結不說,連 _start 進入點都會被連結器以為不存在。

有個重要的觀念叫做呼叫慣例Calling Convention)可以在這裡順便介紹。我們可以看到函式開始的地方, sp 暫存器扣除 16 的操作,這是因為程式執行期的 stack 成長方向向下,每一次呼叫都會必須儲存之後函式結束時要回到的地方,所以這裡會存入 ra 到距離原本的 sp 最近的地方。s0 暫存器的別名是 fp,也就是框架指標(frame pointer),這個資訊代表前一個函式執行時的環境的 sp 所在之處。離開時,則是將這些資訊從堆疊中提取出來,並恢復 sp 原本的值。進入時的階段叫做 function prologue,離開時則叫做 function epilogue

當然,sp 是堆疊指標(stack pointer)的簡寫,希望大家沒有忘的太乾淨。

本體內容即是將必須的資訊傳送給 write 系統呼叫。關於這個系統呼叫,請參考筆者去年的拙作。這裡對應的是

  • a0:第一個參數,傳入 Unix 傳統的檔案描述子(file descriptor)。這裡之所以是 1,是因為這是標準輸出的值,通常在 Linux 上會被連結到操作的 terminal 上面。
  • a1:第二個參數是要印出的字串起始位址,所以這裡是從傳進來給 write 函式的 a0 暫存器取得。
  • a2:write 系統呼叫的第三個參數,代表要印出的字元數。
  • a7:系統呼叫號碼。

最後為了方便建置,這是筆者使用的 Makefile:

$ cat Makefile
AS := riscv64-unknown-linux-gnu-as
LD := riscv64-unknown-linux-gnu-ld

all: fin

fin: fin2.o fin1.o
        $(LD) -o $@ $^

*.o: *.s

run:
        qemu-riscv64 ./fin

因為我們的 as 還沒準備好,ld 還沒實作,所以先採用 GNU 的版本。

準備工作:補完重定項目


如同之前探索的,在 go 語言的 debug/elf 函式庫中,RISC-V 架構的重定資訊有所短少,所以這裡我們先將之補完。有兩個部份,一個是 R_RISCV 的重定型態常數變數集合:

type R_RISCV int

const (
...
        R_RISCV_BRANCH        R_RISCV = 16
        R_RISCV_JAL           R_RISCV = 17
        R_RISCV_CALL          R_RISCV = 18
        R_RISCV_CALL_PLT      R_RISCV = 19
        R_RISCV_GOT_HI20      R_RISCV = 20
        R_RISCV_TLS_GOT_HI20  R_RISCV = 21
        R_RISCV_TLS_GD_HI20   R_RISCV = 22
        R_RISCV_PCREL_HI20    R_RISCV = 23
...

另外一個則是為了輸出的目的而使用的字串

var rxRISCVStrings = []intName{
...
        {16, "R_RISCV_BRANCH"},
        {17, "R_RISCV_JAL"},
        {18, "R_RISCV_CALL"},
        {19, "R_RISCV_CALL_PLT"},
... 

這些都有了之後,我們就可以開始來嘗試 readelf 了。

readelf -r


先來看看偉大的 GNU 版本的輸出:

$ riscv64-unknown-linux-gnu-readelf -r fin2.o

Relocation section '.rela.text' at offset 0x108 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000014  000500000012 R_RISCV_CALL      0000000000000000 write + 0
000000000014  000000000033 R_RISCV_RELAX                        0

這裡就是將位在 0x14 的 call write 的虛擬指令展開成 auipc+jalr 配對,但是我們目前不支援輸出 R_RISCV_RELAX 重定型態,所以到時候應該這裡只有一組 R_RISCV_CALL

那麼我們可以來實作 readelf 了。先看 Output 函式顯示的內容:

192         if *args["r"].(*bool) {
193                 var output []elf.Rela64
194                 json.Unmarshal(reu.raw["r"], &output)
195
196                 fmt.Fprintln(w, "Relocation:\t\t\t")
197                 fmt.Fprintln(w, "Offset\tType\tSymbol\tAppend")
198
199                 syms, _ := reu.file.Symbols()
200
201                 for _, s := range output {
202                         if elf.R_SYM64(s.Info) == 0 {
203                                 continue
204                         }
205
206                         fmt.Fprintf(w, "%016x\t%s\t%s\t%d\n",
207                                 s.Off, elf.R_RISCV(elf.R_TYPE64(s.Info)).GoString(), syms[elf.R_SYM64(s.Info)-1].Name, s.Addend)
208                 }
209                 fmt.Fprintln(w)
210                 w.Flush()
211         }

Run 函是的相對應新增則是:

108         if *args["r"].(*bool) {
109                 var index int
110                 for i, p := range reu.file.Sections {
111                         if p.Name == ".rela.text" {
112                                 index = i
113                                 break
114                         }
115                 }
116
117                 var rela elf.Rela64
118                 str := "]"
119                 var end error
120                 r := reu.file.Sections[index].Open()
121                 for ; end == nil; end = binary.Read(r, binary.LittleEndian, &rela) {
122
123                         raw, err := json.Marshal(rela)
124                         if err != nil {
125                                 return err
126                         }
127
128                         str = "," + string(raw) + str
129                 }
130                 re, _ := regexp.Compile("^,")
131                 str = re.ReplaceAllString(str, "[")
132
133                 reu.raw["r"] = []byte(str)
134         }

首先我們取得名為 .rela.text 的區段,然後透過它的 Open 方法取得一個 io.Reader,也就是讀取的對象;然後,運用 binary 函式庫的 Read 方法取得一個一個的 Rela64 結構,其餘部份則和其他的選項沒有太多差異。

最後結果類似這樣:

/tmp/readelf -r ~/fin2.o
Relocation:
Offset           Type             Symbol Append
0000000000000014 elf.R_RISCV_CALL write  0

小結


今日筆者完成了 readelf 的重定擴充實作。各位讀者我們明日再會!


上一篇
第二十六日:探究 Rela 結構
下一篇
第二十八日:as 強化(上)
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言