了解指令的 operand size(操作数大小)细节,需要掌握下面几个知识点:
在 x86/x64 指令集体系上,指令的 default operand size 来自于 CS.D 以及 CS.L 标志位。这个 CS.D 标志位是什么呢?
CS.D 标志位是: code segment descriptor 的 D 标志位,它用来表示:code segment default operand size 实际上,它还用来指示 default address size 是多少 当 CS.D = 1 时:default operand size 是 32 位,default address size 也是 32 位。 |
processor 在执行代码之前,会将 code segment descriptor 所有信息加载到 CS 寄存器(当然这需要通过 各方面 check),包括 code segment descriptor 的所有标志位,其中就有 D 标志位,因此,CS.D 就是 code segment descriptor 的 D 标志位。
我们常说的 16 位模式 下就是指在 CS.D = 0(D 标志位被清为 0)的情况下,而 32 位模式 是指在 CS.D = 1(code segment descritor 的 D 标志位被设为 1)的情况下。
x64 体系的 64 位模式 需要满足两个条件:
新增的标志位 CS.L = 1 用来指示 long 模式的 64 位模式,而 CS.L = 0 指示 long 模式的 compatibility 模式,
在 64 位下必须 CS.D = 0 指示: default operand size 是 32 位,default address size 是 64 位。
CS.L = 0 表明 processor 运行在 compatibility 模式下,那么 default operand size 将分两种情况,这和 x86 体系的 protected 模式一样的:
CS.L = 0 & CS.D = 1 或 0 表示的 default operand size 与 legacy 模式是一样的。
模式 |
CS.L 标志 |
CS.D 标志 |
default operand size |
default address size |
|
legacy 模式 |
protected 模式 |
不存在 |
CS.D = 1 |
32 |
32 |
real 模式 |
CS.D = 0 |
16 |
16 |
||
long 模式 |
compatibility 模式 |
CS.L = 0 |
CS.D = 1 |
32 |
32 |
CS.D = 0 |
16 |
16 |
|||
64 位模式 |
CS.L = 1 |
CS.D = 1(保留未用) |
保留未用 |
保留未用 |
|
CS.D = 0 |
32(大部分指令) |
64 |
long 模式的 compatibility 子模式和 legacy 模式的情况是完全一致的。在 64 位模式下,default address size 是 64 位,大部分指令的 default operand size 却是 32 位,有一部分指令是 64 位的。
上面所描述的只是 default operand size,但这并不代表指令的 operand size 就是 default operand size,还要视乎有没有 operand size override 行为,这个 operand size 如何 override 是根据 effective operand size 进行的。
effective operand size 是 x86/x64 指令体系所能表达的 operand size
从上面的“表1 default operand size & default address size ”中可以看出,effective operand szie 可以是:
它们都是 x86/x64 指令体系中可以 接受的 operand size,它们由 CS.D 和 CS.L 来决定,同时,受 effective operand size 影响的两类 operand 属性:
z 属性表示:当 effective operand size 是 16 位时, operand size 是 16 位。当 effective operand size 是 32 位时,operand size 是 32 位。但是当 effective operand size 是 64 位时,z 属性所表达的 operand 还是 32 位的。z 属性所描述的 operand 它不具备 64 位的 size
v 属性表示:当 effective operand size 是 64 位时,operand size 是 64 位,v 属性描述的 operand 具有 64 位的 size,这一点和 z 属性是不同的,其它是一样的。
operand size override 就是改变指令的 operand size 行为,在可接受的 effective operand size 范围内改变指令的 default operand size
当没有 operand size override 行为时,default operand size 就是指令的 operand size,关于 operand size override 更详的描述,详见: http://www.mouseos.com/x64/doc3.html#041
经过 operand size override 后,指令的 operand size 就是被改写后的 size
形成指令的最终的 operand size 由 default operand size 和 operand size override 决定:
绝大部分情形下,指令不需要进行 operand size override,因此大部分情形下,operand size 就是 default operand size
看看下面的例子:
bits 32 |
上面的代码中,使用 bits 32 指示代码编译为 32 位的代码。 那它们的 default operand size 是 32 位,以这条指令为例:
xor eax, eax |
它的 opcode 描述是:
XOR Gv, Ev |
在当前的 32 位下,它的 default operand size 是 32 位,这条指令并不需要进行 operand size override 因此它的指令 operand size 是 32 位。
再来看看另一条指令:movzx ebx, byte [edi] 它的 opcode 描述是:
MOVZX Gv, Eb |
那么它的 operand size 到底是多少呢? 在 32 位下,它的目标操作数是 32 位,而源操作数是 8 位。这条指令的 operand size 是由 Gv 决定
在没有 operand size override 的情况下,指令的 operand size 由 default operand size 决定的,它的 operand size 是 32 位(由 default operand size 决定),因此这条指令的是 operand size 是 32 位。
在 operand size override 的情况下,指令的最终 operand size 就是 override 的 size,看看以下面的代码:
bits 16 |
bits 16 指示编译器要生成 16 位的代码(即:default operand size 为 16 位),然而指令中使用了 32 位的寄存器,nasm 会指令产生 operand size override 码,如下:
00000000 6689D8 mov eax,ebx |
生成的机器编码中使用了 66H prefix 进行 operand size override,指令的 operand size 变成了 32 位。
再来看一段 64 位下的代码:
bits 64 |
bits 64 指示 nasm 生成 64 位代码,但是它的 default operand size 却是 32 位的,指令使用了 64 位的寄存器,因此 nasm 会生产使用扩展技术的指令编码,如下:
00000000 4889D8 mov rax,rbx |
这个 48H 是 REX prefix 用于进行 64 位扩展访问。REX.W = 1 表明使用了 64 位的 operand size
实际上使用 REX prefix 也相当于进行了 operand size override 指令最终的 operand size 由 32 位变成了 64 位。
在 x64 体系上,operand size override 有一些微妙的变化,有 2 类指令的 default operand size 是 64 位的:near branches 指令和依赖于 rsp 的相关指令。
bits 64 |
上面这条指令进行了 operand size override 操作。它产生的效果有:
它的指令编码是:66 E8 00 00 offset 由 32 位变成了 16 位,而指令的最终 operand size 也变成 16 位,如下所示:
由 operand size override 引发了另一个问题:sign-extended,符号扩展行为将被改变。由于 operand size override 后,immediate 与指令的 operand size 变得一致了,因此不会存在符号扩展行为 ,看一看下面这个机器编码:
66 e8 fc ff |
这个机器编码对应的指令是:call .+0xfffc
它使用了 66H prefix 进行 operand size override, immediate 操作数是 16 位,指令的 operand size 也是 16 位的,这就不会产生 sign-extended 行为。
假设当前的 rip = 0x13f7a1038,那么:
经过截断后 rip 最终的值为:0x0000000000001034, 这个值不是 sign-extended 后的结果。