在 x86/x64 指令系统里,指令的 immediate 操作数时常有些让人感到迷惑不解的地方,本文将探讨 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 位的操作数。
就是由于上面所说的 4 种 immediate 长度,可以进行 operand size override 操作,但是对于 Ib 类型的 immediate 是不能改变其 size 的。
当对 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 |
如果企图去 override JMP Jb 指令
00000000 66EB00 o16 jmp short 0x3 |
JMP Jb 指令的 immediate 还是 byte
可见:
对于 b 类型 size 的 immediate operand 来说: 对该类型的机器指令使用 66H prefix 进行 operand size override 是不会改变它的指令边界。也就是说:不会改变 immediate 的长度。 |
接下来看看正常的 operand size override 情形
因此:可以使用 66H prefix 来进行调整到 word 或 double word,使用 REX prefix 可以调整到 quad word
当指令的 default operand size 为 16 位时,可以使用 66H prefix 调整到 32 位
bits 16 mov eax, 1 |
上面这段代码设指令的 default operand size 为 16 位,代码中使用了 32 位的 operand(eax 寄存器),那么编译器会进行 operand size override 操作。
00000000 66B801000000 mov eax,0x1 |
当指令的 default operand size 为 32 位时,可以使用 66H prefix 调整到 16 位
bits 32 mov ax, 1 |
这段代码和上面的代码是相反的,在 32 位代码下使用 16 位的 operand(ax 寄存器),同样编译器会进行 operand size override 操作。
00000000 66B80100 mov ax,0x1 |
immediate 变成了 16 位长,指令边界也改变了。
在 64 位模式下,可以使用 REX prefix 将 32 位 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 位长
在 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
让 immediate 操作数进行符号扩展的条件是:
immediate 与指令的 operand size 不匹配的情况下,准确的说是:immediate 小于指令的 operand size 的情况下。 |
下面两种情况下,才有可能会发生 sign-extended:
而具有 Iv 属性的 immediate 是不会产生 sign-extended 的
指令的 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 一文
如果指令的 operand size 是 byte 的话, 就不会存在符号扩展的现象,如这条指令:add al, 0xf0
除了上述这种情况外 Ib 属性的 immediate 操作数都会小于 指令的 operand size
bits 32 |
指令的目的操作数是 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 |
immediate 会扩展到 0xFFF0 再与 ax 寄存器相加
bits 64 |
immediate 会扩展到 0xFFFFFFFFFFFFFFF0 再与 rax 寄存器相加
这是因为当 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 |
上面的代码无必要也不可能发生符号扩展,immediate 与指令的 operand size 是相匹配的。
bits 64 |
而上面的代码中,immediate 是 32 位,指令的 operand size 是 64 位,这样会发生 immediate 符号扩展
当 immediate 大小与指令的 operand size 是一样时,immediate 是不会发生 sign-extended 的,正如下面这条指令:
add eax, 0xf0000000 |
目标操作数和源操作数都是 32 位的,也就不可能会发生符号扩展。
有时候并不是那么好判断 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 这种形式
bits 64 |
上面的指令 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 |
产生的机器编码是:66 68 f0 ff
这里进行了 operand size override 操作,将 64 位的 operand size 改写为 16 位,指令的 operand size 最终变成了 16 位。那么这里不会产生 sign-extended 行为,因为 immediate 的大小与指令的大小是一致的,不存在不匹配的情形。也就不可能产生 sign-extended
只有具有 Iv 属性的 immediate 才可能有 64 位,这部分指令是极少的。绝大多数指令的 immediate 都只有 Ib 或 Iz 属性。也就是说绝大部分指令的 immediate 最高只有 32 位。
在 x64 指令体系里,两类指令的 immediate 具有 Iv 属性,它们是:
关于 immediate 这方面的叙述,参见《x86/x64 指令编码内幕之immediate 值》一文:http://www.mouseos.com/x64/doc9.html