CPU如何正确解码可变长度指令?

4

在大多数架构中,指令都是固定长度的。这使得程序的加载和执行变得简单直接。在x86/x64上,指令长度是可变的,因此反汇编后的程序可能会像这样:

File Type: EXECUTABLE IMAGE

  00401000: 8B 04 24           mov         eax,dword ptr [esp]
  00401003: 83 C4 04           add         esp,4
  00401006: FF 64 24 FC        jmp         dword ptr [esp-4]
  0040100A: 55                 push        ebp
  0040100B: E8 F0 FF FF FF     call        00401000
  00401010: 50                 push        eax
  00401011: 68 00 30 40 00     push        403000h
  00401016: E8 0D 00 00 00     call        00401028
  0040101B: 83 C4 08           add         esp,8
  0040101E: 33 C0              xor         eax,eax
  00401020: 5D                 pop         ebp
  00401021: 83 C4 04           add         esp,4
  00401024: FF 64 24 FC        jmp         dword ptr [esp-4]
  00401028: FF 25 00 20 40 00  jmp         dword ptr ds:[00402000h]

  Summary

        1000 .data
        1000 .rdata
        1000 .reloc
        1000 .text

很难想象CPU是如何“知道”一条指令在哪里结束,下一条指令从哪里开始。例如,如果我在XOR EAX,EAX操作码的中间添加字节0x90(NOP),那么程序将被反汇编为:

File Type: EXECUTABLE IMAGE

  00401000: 8B 04 24           mov         eax,dword ptr [esp]
  00401003: 83 C4 04           add         esp,4
  00401006: FF 64 24 FC        jmp         dword ptr [esp-4]
  0040100A: 55                 push        ebp
  0040100B: E8 F0 FF FF FF     call        00401000
  00401010: 50                 push        eax
  00401011: 68 00 30 40 00     push        403000h
  00401016: E8 0D 00 00 00     call        00401028
  0040101B: 83 C4 08           add         esp,8
  0040101E: 33 90 C0 5D 83 C4  xor         edx,dword ptr [eax+C4835DC0h]
  00401024: 04 FF              add         al,0FFh
  00401026: 64 24 FC           and         al,0FCh
  00401029: FF
  0040102A: 25
  0040102B: 00 20              add         byte ptr [eax],ah
  0040102D: 40                 inc         eax

  Summary

    1000 .data
    1000 .rdata
    1000 .reloc
    1000 .text

很可预见地,运行时会崩溃。

我很好奇这个额外的字节对指令解码器有何影响,使其认为0040101E行有6个字节长,并且最初位于00401028的行有四条独立的指令。


4
CPU以与您的反汇编器完全相同的方式执行它。 - David Schwartz
是的,但我想知道为什么有了额外的字节后,它们都会完全搞砸解码后续指令。 - Govind Parmar
1
如果您将“10 32 5”更改为“10132 5”,原因是相同的,只需更改空格就可以更改“10”和“32”。 - David Schwartz
1
关于0xFF/0x25,那可能目前是未定义的。在此之前,没有出错 - 你所看到的就是这些字节现在编码的内容。在运行时,CPU会触发一个无效指令错误(假设它还没有在xor上产生AV),并且永远不会执行add/inc对。 - 500 - Internal Server Error
1个回答

9
当CPU获取指令时,首先分析第一个字节(操作码)。有时只需知道指令的总长度即可。有时它会告诉CPU分析后续字节以确定长度。但总体而言,编码不含歧义。
如果随意在中间插入随机字节,命令流就会出错。这是可以预料的,因为不是每个字节序列都构成有效的机器代码。
现在,谈谈你的特定示例。原始命令是XOR EAX, EAX(33 C0)。XOR的编码是依赖于第二个字节的编码之一。第一个字节-33-表示XOR。第二个字节是ModR/M字节。它对操作数进行编码——无论是寄存器对、寄存器和内存位置等等。32位模式下的初始值C0对应于操作数EAX,EAX。你插入的值90对应于操作数EDX,[EAX+offset],并且它意味着ModR/M字节后面跟着32位偏移量。命令流的下四个字节不再被解释为命令——它们是混淆的XOR命令的偏移量。
所以通过操纵第二个字节,你把一个2字节的命令变成了一个6字节的命令。
然后CPU(和反汇编器)在那四个字节后恢复获取。它位于ADD ESP, 4指令的中间,但CPU无法知道这一点。它从04字节开始,也就是ADD编码中的第三个字节。此时前几个字节仍然作为命令有意义,但由于你已经处于中间状态,原始指令序列已完全丢失。

1
有趣的事实:可以制作混淆的机器码,跳回到先前执行的指令中间。从该点解码会给出不重复该跳跃的不同指令。这会困扰许多反汇编程序,但 CPU 像往常一样解码指令。关键是指令解码需要起始地址,因为 x86 指令没有对齐要求,并且可以长达 1 到 15 字节(包括前缀字节:第一个字节甚至不必是操作码)。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接