本作品采用知識共享署名 4.0 國際許可協議進行許可。轉載聯系作者並保留聲明頭部與原文鏈接https://luzeshu.com/blog/rip-relative-addressing
本博客同步在 http://www.cnblogs.com/papertree/p/6298763.html
1. 情景
在調試linux-3.0.0內核源碼過程中,碰到一處lea指令,略有疑問。
代碼如下(路徑linux/arch/x86/boot/compressed/head_64.S):
249 /*
250 * Copy the compressed kernel to the end of our buffer
251 * where decompression in place becomes safe.
252 */
253 pushq %rsi
254 leaq (_bss-8)(%rip), %rsi
255 leaq (_bss-8)(%rbx), %rdi
256 movq $_bss /* - $startup_32 */, %rcx
257 shrq $3, %rcx
258 std
259 rep movsq
260 cld
261 popq %rsi
下圖1-1是調試過程中的CPU上下文:

從“mov rcx,0x243e80”中可以看到,_bss的值為0x243e80的,這是_bss這個symbol在進行匯編時,其所在的section內的偏移位置。
從“lea rdi,[rbx+0x243e78]”中可以看到,加到%rbx的值是_bss-8,這跟匯編源代碼是一致的。
而從“lea rsi,[rip+0x243c30]” 中可以看到,加到%rip的偏移值並不是_bss-8 的值。
先說明一點,這個section是加載在0x1000000的內存位置,所以0x1000241這條指令,相對於所在section的起始偏移是0x241。
那么,上面的0x243c30這個值,是由(_bss-8)再減去0x248(下一條指令相對於section的起始偏移值)而得來。
那么,前后兩條看起來十分相似的匯編代碼為什么有這樣的區別呢?
2. RIP的特殊性以及PIC(位置無關代碼)
因為RIP寄存器存放着當前指令的地址,所以有它的特殊性。
比如上面的%rip + displacement,其中displacement存放的如果是_bss這個symbol與該指令的“距離值”,那么不管這段代碼所在的section裝載到哪個位置,都可以通過這個計算,訪問到_bss實際裝載的位置。
比如section裝載在0x1000000,那么指令的%rip為0x1000241,_bss的值為0x1243c30。
而如果裝載在0x5000000,那么指令的%rip為0x5000241,_bss的值為0x5243c30。
那么如果displacement存放的是_bss與指令之間的距離值,那么不管實際加載到哪個位置,都可以訪問到實際的_bss位置。
這里解釋了上面的問題 —— 這兩條相似匯編代碼的區別,正好利用rip的特殊性,實現了PIC的功能。
但是,還是有疑問。這里的解釋僅僅是解釋了displacement為什么有“距離值”和“實際值”兩種情況,這里的區別似乎只是停留在匯編層面,因為gas匯編器就可以這樣實現,當發現base register是%rip,那么displacement就使用_bss與當前指令的下一條指令的“距離值”,而當base register是其他寄存器時,displacement就等於_bss自身的值。
而匯編成機器碼之后,displacement的值已經由匯編器計算好了,CPU在執行的時候,%rip + displacement 和 %rbx + displacement不是一樣的模式嗎?
在搜索資料的時候,發現RIP相對尋址這個概念,這並不是一個匯編器的概念,而是CPU的,所以,既然把%rip + displacement這種尋址模式單獨拿出來,那么還是會有差別的。
此外,在維基上看到的,RIP相對尋址是在x86-64加進去的:
http://wiki.osdev.org/X86-64_Instruction_Encoding#16-bit_addressing
RIP/EIP-relative addressing
Addressing in x86-64 can be relative to the current instruction pointer value. This is indicated with the RIP (64-bit) and EIP (32-bit) instruction pointer registers, which are not otherwise exposed to the program and may not exist physically. RIP-relative addressing allows object files to be location independent.
3. RIP相對尋址
那么為了進一步從CPU層面解釋%rip + displacement和%rbx + displacement這兩種尋址模式的區別,需要來看一下CPU如何解釋機器代碼。
下面是從《Intel 64 and IA-32 Architectures Software Developer's Manual》截取的幾張圖:

這張圖展示了一條機器碼指令的結構,下面結合實際指令解釋一下。
首先,在上面圖1-1的例子中,查看一下兩條lea指令所在的內存數據:
gdb$ x /14xb 0x1000241
0x1000241: 0x48 0x8d 0x35 0x30 0x3c 0x24 0x00 0x48
0x1000249: 0x8d 0xbb 0x78 0x3e 0x24 0x00
這里兩條指令分別7個字節。
其中0x48是Prefixs,0x8d是lea指令的opcode,0x35和0xbb分別是兩條指令的ModR/M,這里面沒有SIB(下面解釋),剩下的0x243c80和0x243e78就是兩條指令的Displacement了。
Instruction Prefixs可以有很多種,上面的wiki鏈接也解釋得很全了。這里的0x48是一種64位長模式特有的REX Prefix。對於REX Prefix的解釋見下圖3-2和3-3,其中高4位0100是固定的,低四位分別作為指令其他部分的擴展位。下面再進行解釋。
那么上面的0x48,即為0100 1000,即W位為1,R X B 三個位都為0。
ModR/M 可以划分成3個field,高2位mod,中間3位reg,低3位r/m。例子中的0x35即為(00 110 101),還有0xbb即為(10 111 011),圖3-4給出了一份助記表,可以找到0x35的坐標位(disp32,ESI),還有0xbb的坐標為([EBX]+disp32, EDI)。
看回例子中的“lea rsi,[rip+disp]” 和“lea rdi, [rbx]+disp”,rip作為base register和其他通用寄存器的區別在這里。但是,我也不知道該說這特不特殊了,全部是0和1之間的差別。
SIB在這兩條指令中沒有,答案可以從圖3-4的NOTES.1中看到,當ModR/M中的mod域和R/M域為某些特定組合時,才存在SIB字節。
再看會剛剛的REX Prefix的R X B三個位,如何做其他部分的擴展在上面的wiki鏈接中挺全面。這里截了其中一個圖作為解釋性說明,見圖3-5,當其中的B位為0時,ModR/M的r/m域是符合圖3-4的,但是當B位為1時,r/m域選擇的寄存器變成了從R8、R9...這些擴展寄存器中選擇了。




