2. 解开 Immediate 操作数的种种迷惑


mik

 

在 x86/x64 指令系统里,指令的 immediate 操作数时常有些让人感到迷惑不解的地方,本文将探讨 immediate 操作数的方方面面。

1. Immediate 的长度

在整个 x86/x64 指令系统里,immediate 操作数的长度有:

在 x86/x64 的指令编码使用 2 组字符描述每 1 个 operand 属性:

用 1 组大写字母来表示 operand type,用 1 组小写字母来表示 operand size, 例如:Iz 表示 operand 是 immediate,大小是 z (16位/32位)
operand 表示法
opcode
immediate size
示例指令
Ib
MOV al,Ib
byte
mov al, 1
Iz
MOV Ev, Iz
word 或者 double word
mov eax, 0x00000001
Iv
MOV rax, Iv
word, double word 或者 quad word
mov rax, 0x0000000000000001

在 operand 表示法里:I 字母表示 operand type 为 Immediate

v operand size 仅仅出现在 x64 指令集中,当出现这个 operand size 时表示可以接 64 位的操作数。

 

2. 由 operand size override 造成的迷惑

就是由于上面所说的 4 种 immediate 长度,可以进行 operand size override 操作,但是对于 Ib 类型的 immediate 是不能改变其 size 的。


2.1 企图对 b 类型的 immediate 进行 override

当对 b 类型的 operand 进行 operand size override 操作时,是不会得到你想要的结果的,b 类型的 size 固定为 byte

(1)mov al, 1 的指令编码是 b0 01 那么如果下面的编码企图对 byte 进行 override 是收不得效果的:

66 b0 01

上面的意图是想使用 66H prefix 进行 override,但是 immediate 始终还是 byte 大小。

为什么得不到想要的效果呢?请看一看 operand size override 的规则表:http://www.mouseos.com/x64/prefix.html#t431

在这个规则表中 default operand size 16/32/64 可以做相应的 override 操作,但是对于 default operand size 为 bytes 这类型的 operand 不能进行 overide 操作。

本质原因是:

    对于 operand size 为 b 类型的 operand 它没有 effective operand size 可供 override所以它的 operand size 是固定 byte

(2)当然也别指望能对指令:mov byte ptr [eax], 1 进行 operand size override 操作数

mov byte ptr [eax], 1 的指令编码是: c6 00 01

 66 c6 00 01

同样 immediate 结果还是 byte

(3)看看对于 JMP Jb 指令如何:

00000000  EB00              jmp short 0x2
00000002  90                nop
00000003  90                nop

如果企图去 override JMP Jb 指令

00000000  66EB00            o16 jmp short 0x3
00000003  90                nop
00000004  90                nop

JMP Jb 指令的 immediate 还是 byte

可见:

对于 b 类型 size 的 immediate operand 来说:

    对该类型的机器指令使用 66H prefix 进行 operand size override 是不会改变它的指令边界。也就是说:不会改变 immediate 的长度。



2.2 对 z 与 v 类型的 immediate 操作数进行 override

接下来看看正常的 operand size override 情形

因此:可以使用 66H prefix 来进行调整到 word 或 double word,使用 REX prefix 可以调整到 quad word

2.2.1 将 word 调整到 double word

当指令的 default operand size16 位时,可以使用 66H prefix 调整到 32

    bits 16

    mov eax, 1

上面这段代码设指令的 default operand size 为 16 位,代码中使用了 32 位的 operand(eax 寄存器),那么编译器会进行 operand size override 操作。

00000000  66B801000000      mov eax,0x1

2.2.2 将 double word 调整到 word

当指令的 default operand size32 位时,可以使用 66H prefix 调整到 16

    bits 32

    mov ax, 1

这段代码和上面的代码是相反的,在 32 位代码下使用 16 位的 operand(ax 寄存器),同样编译器会进行 operand size override 操作。

00000000  66B80100      mov ax,0x1

immediate 变成了 16 位长,指令边界也改变了。

2.2.3 将 double word 调整到 quad word

在 64 位模式下,可以使用 REX prefix32 位 operand 调整到 64 位 operand,必须使 REX.W = 1(使用 64 位扩展 size)

    bits 64

    mov rax, 1

在 64 位代码下,大多数指令的 default operand size 是 32 位,上面的代码使用了 64 位的 operand(rax 寄存器),编译器会使用 REX prefix(REX.W = 1)进行调整到 64

00000000  48B80100000000000000      mov rax,0x1

immediate 变成了 64 位长

2.2.4 将 quad word 调整到 word

在 64 位代码下,一部分指令的 default operand size 是 64 位的,典型的指令如 push

x64 指令体系下允许使用 66H prefix 64 位 operand 调整到 16 位 operand,如下代码:

    bits 64

    push word 0x01

push 指令的 default operand size 是 64 位的,而代码中却使用了 16 位的 operand, 因此编译器会使用 66H prefix 进行 override

00000000  66680100          push word 0x1

immediate 的长度变成了 16 位,指令边界也改变了。

实际上,这里还有一些是关于 default operand size = 64 的迷惑的地方,详见另一篇:《解开 64 位模式下 operand size 的迷惑》:http://www.mouseos.com/x64/puzzle03.html



3. immediate 的符号扩展(sign-extended)

让 immediate 操作数进行符号扩展的条件是:

immediate 与指令的 operand size 不匹配的情况下,准确的说是:immediate 小于指令的 operand size 的情况下。

下面两种情况下,才有可能会发生 sign-extended

而具有 Iv 属性的 immediate 是不会产生 sign-extended 的


3.1 指令的 operand size

指令的 operand size 是指令最终的 operand size,指令最终的 operand size 并不一定是 default operand size, 指令最终的 operand size 是由 defalut operand size 和 operand size override 来决定。

关于 “指令的 operand size”详细的描述见于:http://www.mouseos.com/x64/operand_size.html 一文


3.2 Ib 属性的 immediate 操作数的符号扩展

如果指令的 operand size 是 byte 的话, 就不会存在符号扩展的现象,如这条指令:add al, 0xf0

除了上述这种情况外 Ib 属性的 immediate 操作数都会小于 指令的 operand size

bits 32
    add eax, byte 0xf0

指令的目的操作数是 32 位,源操作数是 8 位,这条指令的 operand size 是 32 位,immediate 操作数会符号扩展到 0xFFFFFFF0

对于这样的指令,所有的编译器都会编译为:83 C0 F0 它的 Opcode 码表示为:

ADD Ev, Ib

指令的 operand size 依赖于 effective operand size,要么是 default operand size 要么是 override 后是 size

目标操作数可以是 16/32/64 位,那么 operand size 在 16/32/64 位的情况下,都会产生 immediate 符号扩展行为

bits 16
    add ax, byte 0xf0

immediate 会扩展到 0xFFF0 再与 ax 寄存器相加

bits 64
    add rax, byte 0xf0

immediate 会扩展到 0xFFFFFFFFFFFFFFF0 再与 rax 寄存器相加


3.2 Iz 属性的 immediate 操作数符号扩展

这是因为当 immediate 为 Iz 时,指令的 operand size 也为 z ,也就是说:Iz 属性的 immediate 操作数为 16 位时,指令的 operand size 也为 16 位,immediate 操作数是 32 位时,指令的 operand size 也是 32 位,这样是不会存在 immediate 操作数与指令 operand size 不相符的情况。

只有当 immediate 为 32 位,而指令的 operand size 为 64 位是才会发生符号扩展,在 x64 指令体系里,绝大多数的 immediate 都是 Iz 属性的,只有少部分的 immediate 具有 Iv 属性。

具有 Iv 属性的 immediate 说明它可以为 64 位,也就是说:大部分的 immediate 操作数最大是 32 位,只有一部分 immediate 操作最大可以是 64 位

ADD Ev, Iz

上面这个 Opcode 码是 81 它表明 immeidate 是 16/32 位,目标操作数是 16/32/64

bits 32
    add cx, 0xf000
    add ecx, 0xf0000000

上面的代码无必要也不可能发生符号扩展,immediate 与指令的 operand size 是相匹配的。

bits 64
    add rcx, 0xf0000000

而上面的代码中,immediate 是 32 位,指令的 operand size 是 64 位,这样会发生 immediate 符号扩展


3.3 不会发生 sign-extended 的原因

immediate 大小与指令的 operand size 是一样时,immediate 是不会发生 sign-extended 的,正如下面这条指令:

add eax, 0xf0000000

目标操作数和源操作数都是 32 位的,也就不可能会发生符号扩展。

3.3.1 为 immediate 提供明确的 size

有时候并不是那么好判断 immediate 操作数的 size,除非你明确指定它的 size,正如下面条语句一样:

add eax, 0xf0

对于这条指令中的 immediate 操作数 0xf0,你的直觉告诉你,这个 immediate 是 byte, 那么你会认为它生成的机器指令是:

83 c0 f0

它的 Opcode 码是 83 它表达形式是:ADD Ev, Ib

但是编译器可能不会这么认为,它可能会认为这个 immediate 是 dword, 那么编译器很可能会生成这样的机器编码:

81 c0 f0 00 00 00

它的 Opcode 码是 81 它的表达形式是:ADD Ev, Iz

编译器会认为这个 immediate 应该是 0x000000f0(32 位的 immediaet 值),所产生的效果是不同的:使用 ADD Ev, Ib 这个 Opcode 形式会产生 sign-extended,而使用 ADD Ev, Iz不会产生 sign-extended 的,这样会影响到最终的计算结果。

因此,在 nasm 中可以指定 immediate 的 size,如下所示:

add eax, byte 0xf0

使用 byte 来指定 immediaet 大小是 byte,从而明确告诉编译器要使用 ADD Ev, Ib 这种形式


3.3.2 由于 operand size override 导致不会 sign-extended

bits 64
    push 0xfffffff0

上面的指令 push 一个 32 位的 immediate 入栈,这条指令的最终结果是将 0xfffffff0 符号扩展为 0xfffffffffffffff0 然后压入栈中。

这里就产生了 sign-extended 行为,这是因为在 64 位下 push 指令的 default operand size 是 64 位的,在没有 override 的情况下,指令的 operand size 就是 64 位,因此:32 位的 immediate 会符号扩展到 64 位。

但是,如果将 immediate 强行 override 到 16 位时,如下:

bits 64
    push word 0xfffffff0

产生的机器编码是:66 68 f0 ff

这里进行了 operand size override 操作,将 64 位的 operand size 改写为 16 位,指令的 operand size 最终变成了 16 位。那么这里不会产生 sign-extended 行为,因为 immediate 的大小与指令的大小是一致的,不存在不匹配的情形。也就不可能产生 sign-extended

 

4. 64 位的 immediate 操作数

只有具有 Iv 属性的 immediate 才可能有 64 位,这部分指令是极少的。绝大多数指令的 immediate 都只有 IbIz 属性。也就是说绝大部分指令的 immediate 最高只有 32 位。

在 x64 指令体系里,两类指令的 immediate 具有 Iv 属性,它们是:

关于 immediate 这方面的叙述,参见《x86/x64 指令编码内幕之immediate 值》一文:http://www.mouseos.com/x64/doc9.html

 


所有权限 mik 所有,转载请注明出处