昨日直接地閱讀 ELF 檔而找到這些重定的樣貌,今天我們來設法將這些資訊轉給 readelf,藉此累積一些重定內容的操作方式吧!
由於我們時間也不多了,所以筆者預計只做一個 R_RISCV_CALL
的重定。為此,接下來的實作內容將分別是:
由於目標很明確,所以我們也無須挑戰之前總是用來當作範例的 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
系統呼叫。關於這個系統呼叫,請參考筆者去年的拙作。這裡對應的是
write
函式的 a0 暫存器取得。write
系統呼叫的第三個參數,代表要印出的字元數。最後為了方便建置,這是筆者使用的 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 了。
先來看看偉大的 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 的重定擴充實作。各位讀者我們明日再會!