从机器码理解RIP 相对寻址

587f976c09e7da181c804e2d Luzeshu 星期四, 2017年1月19日 4

本作品采用知识共享署名 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上下文:

图1-1

从“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》截取的几张图:

图3-1

这张图展示了一条机器码指令的结构,下面结合实际指令解释一下。
首先,在上面图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...这些扩展寄存器中选择了。

图3-2

图3-3

图3-4

图3-5
4 Comments:
58bfcd159f348d62a938c766
2017-03-08 17:21:25

我来点个赞

391d8c9e-5e19-4139-ab7d-a16097eb82c2

2017-05-19 16:56:19

原来你的世界是这样子的

592c280169d57113596661eb
2017-05-29 21:54:09

文章中说“这个section是加载在0x1000000的内存位置” 但是我记得,执行到此处的时候,内核并没有开始解压缩,这里的虚拟地址应该是1M多点才对,为什么已经跑到16M处了

2017-06-04 22:00:12

.config文件中的CONFIG_RELOCATABLE=y 和 CONFIG_PHYSICAL_START=0x1000000 可以配置这个地址,这个地址是从grub跳到内核的。内核decompress_kernel之后的地址也是16M

声明:电子邮箱用于接收回复的通知邮件(来自<mail@luzeshu.com>,请勿回复至此邮箱)。
163邮箱可正常接收,gmail邮箱注意查看垃圾邮件,qq邮箱由于ip频率限制可能无法成功投递。