Raymond Chen 2004年11月11日
了解你的x86机器代码的优势
简要
这篇文章介绍了一些在调试汇编语言时可能会用到的x86机器代码技巧。作者列举了几种单字节操作码,如NOP、INT 3、JZ/JNZ等,它们可以用来临时修改或跳过代码,以及如何撤销这些修改。此外,还提到了如何将条件跳转转换为无条件跳转,或者将跳转转换为永不执行的跳转。文章还讨论了如何安全地覆盖函数调用,以及如何避免栈损坏的问题。作者强调,使用单字节补丁比完全擦除代码更可取,因为它们更容易撤销,从而在调试过程中提供了更大的灵活性。
正文
当你下次需要在汇编语言环境下进行调试(这对于一些开发者来说,是他们**唯一的**调试手段)时,以下是一些你可能会考虑尝试的机器代码技巧:
90
是一个单字节的无操作(NOP)指令。如果你需要临时替换代码,而不想深入思考,只需简单地用90来覆盖它。撤销这个操作时,只需重新覆盖上原始的代码字节。
CC
是一个单字节的中断3(INT 3)指令,它能够触发进入调试器。
74/75
是零跳转(JZ)和非零跳转(JNZ)的指令码。如果你希望改变一个条件测试的逻辑,可以通过互换这两个指令码来实现。其他有用的指令码对包括72/73(小于跳转/不小于跳转)、76/77(小于等于跳转/大于跳转)、7C/7D(小于跳转/大于等于跳转)以及7E/7F(小于等于跳转/大于跳转)。你无需记住这些指令码的具体数值,只需了解改变最低位可以改变测试的逻辑。要恢复原状,再次改变该位即可。
EB
是无条件的短跳转指令。如果你想将条件跳转改为无条件跳转,可以将74(例如)更改为EB。撤销这个更改,你需要记得原来的字节是什么。
00
如果你希望将条件短跳转改为一个永不执行的跳转,可以将第二个字节设置为零。例如,将“74 1C”更改为“74 00”。这样,尽管跳转指令还在,但它实际上只会跳到下一条指令,从而不会产生任何效果。要恢复原状,你需要记住原来的跳转偏移量。
B8/E8
是将立即数移入EAX寄存器(MOV EAX,immed32)和调用(CALL)指令的操作码。我通常使用它们来修改函数调用。如果我不希望调用某个函数,我会选择将E8更改为B8,而不是简单地用90覆盖它。恢复原状时,只需将B8改回E8。
需要注意的是,这种方法仅适用于不需要栈参数的函数;否则,你的栈可能会损坏。一种更通用的方法是使用`83 C4 XX 90 90`(将ESP增加XX字节;无操作;无操作),其中XX代表你需要弹出的字节数。我个人不太记得这些指令的机器代码,因此我更倾向于修改CALL指令,使其指向函数末尾的“RETD”。
相比于完全用90覆盖代码,我更倾向于使用这些单字节的补丁,因为如果你意识到需要恢复到修改前的代码状态,它们更容易撤销。