1. 解开 x86/x64 指令长度的迷惑
mik
在 《x86/x64 指令编码内幕之指令格式》 一文,见 http://www.mouseos.com/x64/format.html 中说过:指令长度为 15 bytes
那么事实上是这样吗?谁来保证指令的长度就是 15 bytes? 怎样来保证?
现在,我们来探索这个问题。
1.1 理论上确实有明文规定
没错, AMD manual Vol3 第 1.1 Instruction Byte Brder 节中明确地说:An instruction can be between one and 15 bytes in length.
而 Intel manual 上没有明说指令长度是多少,而下图的描述有些让人迷惑:

- prefix 最多可以是 4 bytes
- opcode 最多可以是 3 bytes
- modrm 是 1 byte
- sib 是 1 byte
- displacement 是 4 bytes
- immediate 是 4 bytes
这样的描述,让人直觉认为 在饱和状态下 最多可以是 17 bytes, 事实上有可能会达到这样的饱和状态吗? 要达到这样的饱和状态,必须每个部分都要饱和
事实上这是不可能 每个部分同时达到饱和状态 的。
事实上最长的指令长度会出现在:prefix 达到饱和, ModRM/SIB 达到饱和, displacement 达到饱和,immediate 达到饱和。而这些饱和不可能与 opcode 同时发生饱和。
只有 指令 memory,immediate 这种寻址模式下才有这些饱和状态出现,即:目标操作数是 memory,源操作数是 immediate
|
事实上: opcode 在 2 bytes 和 3 bytes 下,不可能有饱和的 immediate 出现。
也就是说: 没有 4 bytes 的 immediate 会出现在2 bytes 或 3 bytes 的 opcode。指令集设计时候要遵循这个原则 |
即:指令 memory, immediate 这种寻址模式不会出现在 2 bytes 或 3 bytes 的 opcode 指令中
1.2 还是那个例子
|
lock add dword ptr es:[eax+ecx*8+0x11223344], 0x12345678 |
仅仅在 16 位模式下,这条汇编语句的 encode 是 15 bytes
因为:它在 16 位模式下,需要进行 operand size override 和 address size override,因此能达到 prefix 的饱和
从而每个部分除了 opcode 外,都达到了饱和状态。刚刚好是 15 bytes:26 66 67 F0 81 84 C8 44 33 22 11 78 56 34 12
4 group 的 prefix 都使用上了,ModRM 和 SIB 都需要,displacement 和 immediate 都是 4 bytes 的,只有 opocde 是 1 byte
这条指令在 32 位是:26 F0 81 84 C8 44 33 22 11 78 56 34 12 (13 bytes)
在 64 位下:26 67 F0 81 84 C8 44 33 22 11 78 56 34 12 (14 bytes)
那是因为 32 位下缺省的 operands size 和 address size 是 32 位,不需要作 operand size override 和 address size override
在 64 位下缺省的 operands size 是 32 位,而 address size 是 64 位,因此不需要作 operand size override,但是需要做 address size override
上面的论述可以解开我们心中的迷惑了吗?
不能! 下面继续...
1.3 总会有不守规则的现象出现
我们还存在疑惑,是因为:总有不守规则的现象出现。貌似这个才是真理 :)
编译器总是循规蹈矩的,它一定会按指令编码规则来生成规矩的 encode,除非编译器有 bug。因此:不要期待编译器会产生不守规则的现象
这个不守规则的现象是人为生产的。
1.3.1 测试 nasm 中的 ndisasm 反汇编
下面是 ndisasm 的输出:
|
00000000 26 2E 3E 66 67 F0 81 84 C8 44 33 22 11 78 56 34 12 lock add dword [dword ds:eax+ecx*8+0x11223344],0x12345678 |
ndisasm 反汇编出这条指令居然有 17 bytes!
没错,多出来的 2 bytes 是我人为加上去的!而不是编译器产生的。但是这表明 ndisasm 能正确识别这条指令(即使多了 2 bytes)
多出来的 2 bytes 是 semgent override prefix,这样导致了不 3 个字节的 segment override prefix 情况出现!
1.3.2 测试 bochs 的反汇编功能
bochs 是个超级棒的模拟器,它的反汇编功能是很不错的。
现在我来测试 bochs 的输出:
|
(0) Breakpoint 1, 0x0000000000007c00 in ?? () Next at t=153228545 (0) [0x00007c00] 0000:7c00 (unk. ctxt): lock add dword ptr ds:[eax+ecx*8+287454020], 0x00345678 ; 262e3e6667f08184c84433221178563400 <bochs:3> u /2 00007c00: ( ): lock add dword ptr ds:[eax+ecx*8+287454020], 0x00345678 ; 262e3e6667f08184c84433221178563400
00007c11: ( ): lock add dword ptr ds:[eax+ecx*8+287454020], 0x12345678 ; 263e6667f08184c84433221178563412
<bochs:4> |
上面的两条指令,一条是 17 bytes,一条是 16 bytes,第 1 条显示异常,后的 byte 以 00 代替。第 2 条正确识别。说明 bochs 最多只能对 16 bytes 的显示正常。
bochs 的反汇编与 ndisasm 是有很大区别的:ndisasm 是静态的反汇编,bochs 是动态的反汇编。
第 1 条指令的显示结果,表明:bochs 加载解析指令长度是 16 bytes,它只会加载 16 bytes,第 17 字节会抛弃。但是由于指令所需求的 immediate 是 4 bytes,因此只好以 00 代替。
1.3.3 测试 visual studio 的 disassembly 功能
最后来测试 visual studio 2010 ,visual studio 会给我们比较合理的 disassembly
|
00C81001 ?? db 26h
00C81002 ?? db 2eh 00C81003 3E F0 81 84 C8 44 33 22 11 78 56 34 12 lock add dword ptr ds:[eax+ecx*8+11223344h],12345678h |
上面这条 encode 是 15 bytes:
|
0x00C81001 26 2e 3e f0 81 84 c8 44 33 22 11 78 56 34 12 |
由于,在 visual studio 上不能测试 16 位的 disassembly,上面的测试是在 32 位下的。
visual studio 给出了另一种反汇编策略:3 个字节的 segment prefix 只识别最后一个 segment prefix 作为指令的 segment prefix
1.3.4 ndisasm,bochs 和 visual studio 的 disassembly 不同,揭示了什么?
它们的不同确实能揭示 x86/x64 指令集世界的某些特性,但并不是我们所要的最终结论。
记住:它们的不同只是 diassembly 显示策略的不同。
它揭示了 disassembly 的:
上面的论述可以解开我们心中的迷惑了吗?
没有,反而给我们带来了更多的迷惑
下面我们来将这个迷惑解开
1.4 真实的指令边界
尽管上面的 disassembly 给我们带来了更多的迷惑,但是要清楚认识到:那些并不是真实机器的指令边界。那只是 disassembler 的 disassembly 的指令边界。
1.4.1 在 bochs 上看真实的指令边界
我在 bochs 对比两条同样的指令,第 1 条是 15 bytes,第 2 条是 16 bytes,如下:
|
<bochs:3> u /4 00007c00: ( ): lock add dword ptr es:[eax+ecx*8], 0x00000000 ; 266667f08184c80000000000000000 00007c0f: ( ): nop ; 90 00007c10: ( ): lock add dword ptr ds:[eax+ecx*8], 0x00000000 ; 263e6667f08184c80000000000000000 00007c20: ( ): nop ; 90 |
为了观察行为,我将必要的调试选项打开,如下:
|
<bochs:4> trace-reg on Register-Tracing enabled for CPU0 <bochs:5> trace-mem on Memory-Tracing enabled for CPU0 <bochs:6> show int show interrupts tracing (extint/softint/iret): ON show mask is: softint extint iret |
最主要是 mem 观察和 interupt 观察打开。
下面来看一看,指令的执行情况
执行第 1 条指令:
|
<bochs:6> u 00007c00: ( ): lock add dword ptr es:[eax+ecx*8], 0x00000000 ; 266667f08184c80000000000000000 <bochs:7> s [CPU0 RD]: LIN 0x000000000000aa55 PHY 0x0000aa55 (len=4, pl=0): 0x00000000 [CPU0 WR]: PHY 0x0000aa55 (len=4): 0x00000000 00153228546: softint 0000:7c0f (0x00007c0f)
00153228546: iret 0000:7c0f (0x00007c0f) Next at t=153228546 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd6 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:00007c0f eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf (0) [0x00007c0f] 0000:7c0f (unk. ctxt): nop ; 90 |
bochs 正确执行第 1 条指令没有产生异常, 下一条指令边界在 0x7c0f 上,这条指令是 15 bytes 的。 这是机器所能接受的最长指令。
下面看一看执行 16 bytes 指令的情况如何:
|
<bochs:8> Next at t=153228547 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd6 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:00007c10 eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
(0) [0x00007c10] 0000:7c10 (unk. ctxt): lock add dword ptr ds:[eax+ecx*8], 0x00000000 ; 263e6667f08184c80000000000000000 <- 16 bytes 指令
<bochs:9> [CPU0 WR]: LIN 0x000000000000ffd4 PHY 0x0000ffd4 (len=2, pl=0): 0x0046 [CPU0 WR]: LIN 0x000000000000ffd2 PHY 0x0000ffd2 (len=2, pl=0): 0x0000 [CPU0 WR]: LIN 0x000000000000ffd0 PHY 0x0000ffd0 (len=2, pl=0): 0x7C10
[CPU0 RD]: LIN 0x0000000000000034 PHY 0x00000034 (len=2, pl=0): 0xFF53 <- 异常处理程序
[CPU0 RD]: LIN 0x0000000000000036 PHY 0x00000036 (len=2, pl=0): 0xF000
00153228548: exception (not softint) f000:ff53 (0x000fff53) <- 异常产生
Next at t=153228548 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd0 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:0000ff53 eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
(0) [0x000fff53] f000:ff53 (unk. ctxt): iret ; cf <- 跳转到 exception 处理程序 |
如上图所示,执行 0x7c10 处的 16 bytes 的指令时,产生了异常。
processor 先写入 eip 值和 eflags 值,然后读入异常处理程序。最后跳转到 f000:ff53 处的异常处理程序, 这个异常处理程序只有一条 iret 指令。
|
<bochs:10> [CPU0 RD]: LIN 0x000000000000ffd0 PHY 0x0000ffd0 (len=2, pl=0): 0x7C10 [CPU0 RD]: LIN 0x000000000000ffd2 PHY 0x0000ffd2 (len=2, pl=0): 0x0000 [CPU0 RD]: LIN 0x000000000000ffd4 PHY 0x0000ffd4 (len=2, pl=0): 0x0046 00153228549: iret 0000:7c10 (0x00007c10)
Next at t=153228549 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd6 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:00007c10 eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf (0) [0x00007c10] 0000:7c10 (unk. ctxt): lock add dword ptr ds:[eax+ecx*8], 0x00000000 ; 263e6667f08184c80000000000000000
<bochs:11> [CPU0 WR]: LIN 0x000000000000ffd4 PHY 0x0000ffd4 (len=2, pl=0): 0x0046 [CPU0 WR]: LIN 0x000000000000ffd2 PHY 0x0000ffd2 (len=2, pl=0): 0x0000 [CPU0 WR]: LIN 0x000000000000ffd0 PHY 0x0000ffd0 (len=2, pl=0): 0x7C10 [CPU0 RD]: LIN 0x0000000000000034 PHY 0x00000034 (len=2, pl=0): 0xFF53 [CPU0 RD]: LIN 0x0000000000000036 PHY 0x00000036 (len=2, pl=0): 0xF000 00153228550: exception (not softint) f000:ff53 (0x000fff53)
Next at t=153228550 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd0 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:0000ff53 eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf (0) [0x000fff53] f000:ff53 (unk. ctxt): iret ; cf |
当继续往下执行时,异常处理程序返回到异常发生点:0x7c10,接着执行又继续产生异常,又转入异常程序。
接下来,我测试一下 prefix override 的情形:
|
Next at t=153228547 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd6 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:00007c10 eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
(0) [0x00007c10] 0000:7c10 (unk. ctxt): lock add word ptr ds:[eax+ecx*8], 0x0000 ; 263e67f08184c8000000000000 <- 修改后的指令
<bochs:10> [CPU0 RD]: LIN 0x000000000000aa55 PHY 0x0000aa55 (len=2, pl=0): 0x0000 [CPU0 WR]: PHY 0x0000aa55 (len=2): 0x0000 Next at t=153228548 rax: 0x00000000:0000aa55 rcx: 0x00000000:00000000 rdx: 0x00000000:00000000 rbx: 0x00000000:00000000 rsp: 0x00000000:0000ffd6 rbp: 0x00000000:00000000 rsi: 0x00000000:000e32f8 rdi: 0x00000000:0000ffac r8 : 0x00000000:00000000 r9 : 0x00000000:00000000 r10: 0x00000000:00000000 r11: 0x00000000:00000000 r12: 0x00000000:00000000 r13: 0x00000000:00000000 r14: 0x00000000:00000000 r15: 0x00000000:00000000 rip: 0x00000000:00007c1d eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf (0) [0x00007c1d] 0000:7c1d (unk. ctxt): add byte ptr ds:[bx+si], al ; 0000 |
注意上面修改后的指令,长度为 13 bytes,这里有一个 prefix override 情况:26 是 ES segment 而 3E 是 DS segment
进行了双重 segment override 操作,但这并不妨碍指令的正确执行,最终是以 DS 为 segment 参考,这是 processor 能接受的。
结论: bochs 的 diassembler 在反汇编上,可以接受大于 15 bytes 的指令边界。并不代表它能执行大于 15 bytes 的指令。事实上:bochs 不能执行大于 15 bytes 的指令。
1.4.2 在 visual studio 上观察真实指令边界
下面来看一看 visual studio 上的表现如何:
同样我在 visual studio 2010 的 debug 模式下观察同一条指令的 15 bytes 和 16 bytes 下的执行情况:

执行第 1 条指令:26 36 3e f0 81 84 cc 00 00 00 00 00 00 00 00 (15 bytes)时,能正确执行。
当执行到另 1 条指令:26 36 2e 3e f0 81 84 cc 00 00 00 00 00 00 00 00 (16 bytes)时,产生了异常,异常的发生点就是这一条指令上 0x00161019
注意:虽然 visual studio 的 disassembler 在反汇编上不能接受 prefix override(即:多个同类型的 prefix),但是并不代表在真实执行指令上不接受 prefix override,上图的第 1 指令能正确执行,就说明这一点
结论:在 visual studio 里的例子说明:在真实机器上只能执行的最长指令边界为 15 bytes 上。大于 15 bytes 指令将会产生异常,这结论与 bochs 上的结论是一致的。
1.5 解开迷惑
到这里,我们能解开迷惑了吗?我认为:可以解开迷惑了。
disassembler 的反汇编的指令边界与真实执行指令边界可能会不一致。当然造成这种不一样的人为的,实际上这是对 disassembler 的一种考验。
不要期望编译器会产生大于 15 bytes 的指令编码!除非它有错。不要认为 disassmbler(反汇编器)的结果就是真实执行指令结果,有可能会不一致!
现在:我们可以回答前面提到的三个疑问:
- 真实的指令长度确实为 15 bytes
- processor 会给我们保证这一点
- 当加载超过 15 bytes 长度的指令时,processor 会给我们抛出 #GP 异常,它通过这种方式保证了 15 bytes 指令长度
所有权限 mik 所有,转载请注明出处