1. 解开 x86/x64 指令长度的迷惑


mik

 

《x86/x64 指令编码内幕之指令格式》 一文,见 http://www.mouseos.com/x64/format.html 中说过:指令长度为 15 bytes

那么事实上是这样吗?谁来保证指令的长度就是 15 bytes? 怎样来保证?

现在,我们来探索这个问题。

 

1.1 理论上确实有明文规定

没错, AMD manual Vol31.1 Instruction Byte Brder 节中明确地说:An instruction can be between one and 15 bytes in length.

Intel manual 上没有明说指令长度是多少,而下图的描述有些让人迷惑:

这样的描述,让人直觉认为 在饱和状态下 最多可以是 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 segment3EDS 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(反汇编器)的结果就是真实执行指令结果,有可能会不一致!

现在:我们可以回答前面提到的三个疑问:

 


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