天天看點

MIPS架構下LW指令的重定位過程

通常我們不會去關心指令重定位(relocation)的細節,編譯器的ld過程已經幫助我們做好了。由于最近在移植CRIU,涉及到指令的重定位計算,不得不細細研究代碼重定位的細節知識。之前的文章介紹了MIPS架構下函數跳轉指令bal和jal和重定位過程,感興趣的同學可以跳轉到https://blog.csdn.net/weixin_38669561/article/details/100536803檢視,本章通過分析lw 指令來分析從記憶體加載資料到寄存器的重定位的過程。

本文有點燒腦,看完注意休息 “_”

一、準備工作和基礎知識

可以跳過

首先看下面的示例彙編語句:

//test.S
ENTRY(__export_parasite_head_start)
	.set noreorder
	lw	a0, __value
	jr ra

__value:
	.long 0
END(__export_parasite_head_start)
           

這裡lw a0,__value 就是我們要分析的彙編指令。我們用gcc編譯

$ gcc -save-temps -g -c -fno-builtin -mno-abicalls test.S 
           

其中 -save-temps 參數意味着保留中間臨時檔案,是以這條指令執行完成會發現目前目錄多了兩個檔案,test.s和test.o。其中test.s就是GCC編譯生成的中間編譯檔案,而test.o是彙編檔案。編譯檔案和彙編檔案有什麼差別呢?首先要了解GCC編譯是有4個過程,預編譯(生成.i檔案)-> 編譯(生成.s檔案)->彙編(生成.o檔案)->連結(生成可執行檔案,預設為a.out)。這裡的test.s就是編譯階段産生的檔案,test.o就是彙編階段産生的檔案,編譯和彙編的差別可以通過檢視檔案内容得知:

//test.s
 .section .head.text, "ax"
.globl __export_parasite_head_start; .align 4; .type __export_parasite_head_start, @function; __export_parasite_head_start:
 .set noreorder
 lw $4, __export_parasite_cmd
 jr $31
__export_parasite_cmd:
 .long 2
.size __export_parasite_head_start, . - __export_parasite_head_start
           

可以看出test.s和test.S幾乎沒有差别,都是彙編指令。

test.o已經是ELF格式的檔案了,是以不能直接打開,需要使用objdump指令

$ objdump -D test.o > test.o.dump
           

然後打開檔案test.o.dump

test.o:     檔案格式 elf64-tradlittlemips
	...
Disassembly of section .head.text:
0000000000000000 <__export_parasite_head_start>:
   0:	3c040000 	lui	a0,0x0
   4:	3c010000 	lui	at,0x0
   8:	64840000 	daddiu	a0,a0,0
   c:	0004203c 	dsll32	a0,a0,0x0
  10:	0081202d 	daddu	a0,a0,at
  14:	8c840000 	lw	a0,0(a0)
  18:	03e00008 	jr	ra

000000000000001c <__export_parasite_cmd>:
  1c:	00000002 	srl	zero,zero,0x0
      ...

           

彙編過程就是将彙編代碼(test.s)轉變成機器可以執行的指令(test.o),有些彙編語句和機器指令一一對應,不需要擴充,比如上面的

"jr ra"

指令。有些彙編指令可能擴充成多條機器指令,比如test.S裡裡面的

"lw a0, __value"

擴充成了6條指令

0:	3c040000 	lui	a0,0x0
   4:	3c010000 	lui	at,0x0
   8:	64840000 	daddiu	a0,a0,0
   c:	0004203c 	dsll32	a0,a0,0x0
  10:	0081202d 	daddu	a0,a0,at
  14:	8c840000 	lw	a0,0(a0)
           

同時我們有知道,此時的test.o還是沒有做重定位的指令集,從起始位址

"0000000000000000"

就可以看出,或者使用file指令

$ file test.o
test.o: ELF 64-bit LSB relocatable, MIPS, MIPS64 rel2 version 1 (SYSV), not stripped
           

這裡

"relocatable"

就意味着這是需要重定位的檔案,或者說需要做指令修正的檔案。

那麼重定位什麼時候做呢?連結階段。上面我在使用gcc工具時有一個參數

"-c"

。這個值意思是制作編譯、彙編,不進行連結。連結過程主要包括了位址和空間配置設定、符号決議和重定位。

下面我們使用ld工具來進行test.o的連結過程。為了便于分析LW指令的重定位的過程,ld使用自定義的連結腳本,内容如下:

// ld.lds
OUTPUT_ARCH(mips)
ENTRY(__export_parasite_head_start) /*指定了程式入口函數*/
SECTIONS
{
	. = 0x120000000; /*0xfff70c4000;  指定目前虛拟位址*/
	tinytext : {  
		*(.head.text)
        	*(.text*)
		*(.data)
		*(.rodata)
	}

	/DISCARD/ : { /*釋義:需要丢棄的輸入段*/
	*(.comment)
	*(.pdr)
	}

/* Parasite args should have 4 bytes align, as we have futex inside. */
. = ALIGN(4);
__export_parasite_args = .;
}
           

在這個檔案中,你隻要關注兩點, 一、

"ENTRY(__export_parasite_head_start)"

指定了程式運作的入口位址,沒有它,接下來的ld指令會失敗 。二、

". = 0x120000000"

指定了目前虛拟起始位址。接下來執行l指令。

$ ld -static -T ld.lds -o test test.o
           

這時檢視生成的test檔案已經是可執行的,relocation已經完成。

$ file test
test: ELF 64-bit LSB executable, MIPS, MIPS64 rel2 version 1 (SYSV), statically linked, not stripped
           

二、relocation分析

敲黑闆上面的内容都是準備工作,接下來開始分析重定位(relocation)的過程。

首先反彙編test檔案

$ objdump -D test > test.dump
           

打開test.dump檔案

test:     檔案格式 elf64-tradlittlemips
	...

Disassembly of section tinytext:
 000000fff70c4030 <__export_parasite_head_start>:
   fff70c4030:   3c040000        lui     a0,0x0
   fff70c4034:   3c01f70c        lui     at,0xf70c
   fff70c4038:   64840100        daddiu  a0,a0,256
   fff70c403c:   0004203c        dsll32  a0,a0,0x0
   fff70c4040:   0081202d        daddu   a0,a0,at
   fff70c4044:   8c84404c        lw      a0,16460(a0)
   fff70c4048:   03e00008        jr      ra
  
 000000fff70c404c <__export_parasite_cmd>:
  fff70c404c:   00000002        srl     zero,zero,0x0

           

發現和上面的test.o檔案反彙編檔案的不同點了嗎?我把差別标記下來并開始分析:

MIPS架構下LW指令的重定位過程

其中藍色部分是做了重定向的結果。 R_MIPS_HIGHEST、R_MIPS_HI16 、R_MIPS_HIGHER、R_MIPS_LO16是重定位入口類型。每種類型的指令修正方式可以通過檢視mipsabi文檔可以找到

目前我從https://elinux.org網站上下載下傳下來的mipsabi.pdf文檔裡對重定向入口類型介紹的也不全,最好的分析辦法是看 binutils 的源碼

還要明确一下,我目前運作的是在龍芯處理器上。mips寄存器為64位,指令32位,尋址48位(我還不确定)。lw指令的描述是 lw rt,offset(base) ,這裡base可以尋址64位。按上面的

"lw a0,16460(a0)"

為例,base值應該是0xfff70c0000(等于ld.lds裡面設定的初始虛拟位址) 。16460是10進制數,對應的16進制數為0x404c。那麼尋址後的a0值應該為0xfff70c0000+0x404c = 0xfff70c404c。base值應該是(也就是上面的a0)

MIPS架構下LW指令的重定位過程

接下來我開始分析上面的每一條指令來了解lw a0,__export_parasite_cmd是怎麼加載上來的。

第一條指令 lui a0,0x0

lui 功能為上位加載立即數,描述為a0 = 0x0<<16位,操作後的a0值為:

a0 :0x0000 0000 0000 0000

第二條指令 lui at,0xf70c

這裡使用了at寄存器做中間變量,描述為at = 0xf70c<<16,操作後的at值為:

at:0xffff ffff f70c 0000

這裡是最讓人費解的地方,f70c左移16位後應該是0x0000 0000 f70c 0000。而這裡卻是0xffff ffff f70c 0000 這是我通過gdb調試确認過的結果,可能是MIPS實作48位記憶體尋址的政策

第三條指令 daddiu a0,a0,256

daddiu 功能為64位立即數加法,描述為a0 = a0+256 ,256為10進制對應0x0100,操作後的a0值為:

a0:0x0000 0000 0000 0100

第四條指令 dsll32 a0,a0,0x0

dsll32 功能為32位左移,描述為a0 = a0<<(32+0x0),操作後的a0值為:

a0:0x0000 0000 0100 0000

第五條指令 daddu a0,a0,at

daddu功能同daddiu,為64位加法,但是操作數不是立即數而是寄存器。描述為a0 = a0+at,結果為:

a0:0x0000 00ff f70c 0000

此時你可能明白點為啥at值的高32位填充全f的原因沒?

分析的最後一條 lw a0,16460(a0)

lw 功能為32位加載,64位CPU上進行符号擴充。 描述為a0 = memory(0xff f70c 0000+0x404c),a0結果為:

a0 = 2 //如果不信,你可以使用gdb調試

也就是lw執行後,a0可以加載到記憶體位址為fff70c404c上的資料。

到這裡,我們已經通過重定位的指令分析了lw的基址計算過程。如果讓你去實作relocation過程,你該怎麼做?可能我們不被逼到死路是不會考慮這個問題。此刻,我就在死路上。

敲黑闆:指令修正方式

上面提到了重定位入口類型R_MIPS_HIGHEST、R_MIPS_HI16 、R_MIPS_HIGHER、R_MIPS_LO16,但是分析過程一點沒有用到,那是由于ld已經幫我們根據這幾個類型實作了指令修正。重定位入口類型隻有在test.o 到可執行檔案test 的彙編過程才會用到。test.o是ELF格式的檔案,ELF格式中會存儲哪些段需要重定位以及指令修正的類型等資訊。我們可以通過readelf指令檢視

$ readelf -r test.o

重定位節 '.rela.head.text' 位于偏移量 0x4b0 含有 4 個條目:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000  00040000001d R_MIPS_HIGHEST    0000000000000000 .head.text + 1c
                    Type2: R_MIPS_NONE      
                    Type3: R_MIPS_NONE      
000000000004  000400000005 R_MIPS_HI16       0000000000000000 .head.text + 1c
                    Type2: R_MIPS_NONE      
                    Type3: R_MIPS_NONE      
000000000008  00040000001c R_MIPS_HIGHER     0000000000000000 .head.text + 1c
                    Type2: R_MIPS_NONE      
                    Type3: R_MIPS_NONE      
000000000014  000400000006 R_MIPS_LO16       0000000000000000 .head.text + 1c
                    Type2: R_MIPS_NONE      
                    Type3: R_MIPS_NONE      
           

offset是指目前指令在elf檔案中的位置,Type即為重定位入口類型,Addend會參與到指令修正的運算。

MIPS上的重定位類型對應的計算方式可以通過mipsabs.pdf檢視到一些(但不是全部),如下圖:

MIPS架構下LW指令的重定位過程

在此我就上面的幾個類型結合代碼講解test.o中relocation的計算過程。

重定位類型 R_MIPS_HIGHEST

通過elf格式解析過程能夠看到test.o中第一條指令 lui a0,0x0 ,指令碼為 3c040000,重定位類型為R_MIPS_HIGHEST。怎麼計算呢?mipsabi上還真沒有,我參考了binutils的源碼中mips.cc得出計算過程為:

((vbase(0xfff70c4000)+ 0x800080008000llu)>>48) & 0xffff

上述的計算結果(0x0)放在lui指令的低16位。修正後的lui指令碼還是 3c040000

重定位類型 R_MIPS_HI16

test.o中第二條指令 lui at,0x0,指令碼為 3c010000,重定位類型 R_MIPS_HI16,計算方式根據不同的符号類型有不同的計算方式,此處的符号類型為Local,計算過程為:

((vbase(0xfff70c4000)+ 0x8000)>>16) & 0xffff

上述的計算結果(0xf70c)放在lui指令的低16位,修正後的lui指令碼為 3c01f70c

重定位類型 R_MIPS_HIGHER

第三條指令 daddiu a0,a0,256 ,指令碼為 64840000 ,重定位類型為R_MIPS_HIGHER,計算方式也隻能參考binutils的源碼中mips.cc檔案

((vbase(0xfff70c4000)+ 0x80008000)>>32) & 0xffff

上述計算結果(0x0100)放在daddiu指令的低16位,修正後的daddiu指令碼為64840100

重定位類型 R_MIPS_LO16 ( fixme)

第六條指令 lw a0,0(a0) ,指令碼為 8c840000 ,重定位類型為R_MIPS_LO16,計算方式根據不同的符号類型有不同的計算方式,此處的符号類型為Local,計算過程為:

(vbase(0xfff70c4000)& 0xffff)+A (0x4c)

上述計算結果(0x404c)放在lw指令的低16位,修正後的lw指令碼為8c84404c

繼續閱讀