跳转至

何时使用 NOP

考虑一个不显示隐形敌人生命条的游戏。隐形敌人来袭时很难察觉,如果你至少能看到它们的生命条,在战斗中会拥有巨大优势。绘制生命条的代码通常看起来像清单 8-1。

for (int i = 0; i < creatures.size(); i++) {
    auto c = creatures[i];
    if (c.isEnemy && c.isCloaked) continue;
    drawHealthBar(c.healthBar);
}

清单 8-1:drawCreatureHealthBarExample() 函数中的循环

绘制生命条时,有隐形生物的游戏可能会用 for 循环检查屏幕范围内的生物是否隐形。如果敌人没有隐形,循环调用某个函数(本例中是 drawHealthBar())显示敌人的生命条。

有了源代码,你可以通过简单地从代码中移除 if(c.isEnemy && c.isCloaked) continue; 来迫使游戏绘制甚至隐形敌人的生命条。但作为游戏黑客,你只有汇编代码,没有源代码。简化后,清单 8-1 翻译成的汇编看起来像这样的伪代码:

startOfLoop: ; for
MOV i, 0 ; int i = 0
JMP condition ; 第一个循环跳过增量
increment:
ADD i, 1 ; i++
condition:

i < creatures.size()

JNB endOfLoop ; 如果 i >= creatures.size() 则退出循环
body:
MOV c, creatures[i] ; auto c = creatures[i]
TEST c.isEnemy, c.isEnemy ; if c.isEnemy
JZ drawHealthBar ; 如果 c.isEnemy == false 则绘制条
TEST c.isCloaked, c.isCloaked ; && c.isCloaked
JZ drawHealthBar ; 如果 c.isCloaked == false 则绘制条
 JMP increment ; 继续
drawHealthBar:
CALL drawHealthBar(c.healthBar) ; 

drawHealthBar(c.healthBar)

JMP increment ; 继续
endOfLoop:

要诱使游戏绘制所有敌人的生命条(无论是否隐形),你需要移除 c.isEnemy && c.isCloaked 求值为 true 时执行的 JMP increment 命令①。但在汇编中,用不执行任何操作的指令替换不需要的代码比删除代码更容易。这就是 NOP 命令的用武之地。由于 NOP 是单字节(0x90),你可以用两条 NOP 命令覆盖 2 字节的 JMP increment 命令。处理器到达这些 NOP 命令,会越过它们,即使 c.isEnemy && c.isCloaked 求值为 true 也会落入 drawHealthBar()。