人们普遍认为使用GOTO语句将破坏代码的结构化构造[1]。即使如此,某些情况下我们还必须使用这种语句[2]。
本章围绕以下程序进行说明:
#include <stdio.h>
int main()
{
printf ("begin\n");
goto exit;
printf ("skip me!\n");
exit:
printf ("end\n");
};
使用MSVC 2012编译上述程序,可得到如下所示的指令。
指令清单11.1 MSVC 2012
$SG2934 DB 'begin', 0aH, 00H
$SG2936 DB 'skip me!', 0aH, 00H
$SG2937 DB 'end', 0aH, 00H
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2934 ; 'begin'
call _printf
add esp, 4
jmp SHORT $exit$3
push OFFSET $SG2936 ; 'skip me!'
call _printf
add esp, 4
$exit$3:
push OFFSET $SG2937 ; 'end'
call _printf
add esp, 4
xor eax, eax
pop ebp
ret 0
_main ENDP
源代码中的goto语句直接被编译成了JMP指令。这两个指令的效果完全相同:无条件的转移到程序中的另一个地方继续执行后续命令。
只有在人工干预的情况下,例如使用调试器调整程序、或者对程序打补丁的情况下,程序才会调用第二个printf()函数。
我们用它练习patching技术。首先使用Hiew打开刚才编译的可执行文件,如图11.1所示将光标移动到JMP(0x410)处,按下F3键(编辑),再输入两个零。这样,我们就把这个地址的opcode改为了EB 00如图11.2所示。
图11.1 Hiew
图11.2 Hiew
JMP所在的opcode的第二个字节代表着跳转的相对偏移量。把这个偏移量改为0,就可以让它继续运行后续指令。这样JMP指令就不会跳过第二个printf()函数。
按下F9键(保存文件)并退出Hiew。再次运行这个可执行文件的情况应当如图11.3所示。
图11.3 修改后的程序
当然,把JMP指令替换为两个NOP指令可以达到同样效果。因为NOP的opcode是0x90、只占用一个字节,所以在进行替换时要写上两个NOP指令。
在编译术语里,上述程序中第二次调用printf()函数的代码称为“无用代码/dead code”。无用代码永远不会被执行。所以,在启用编译器的优化选项之后,编译器会把这种无用代码删除得干干净净。
指令清单11.2 Optimizing MSVC 2012
$SG2981 DB 'begin', 0aH, 00H
$SG2983 DB 'skip me!', 0aH, 00H
$SG2984 DB 'end', 0aH, 00H
_main PROC
push OFFSET $SG2981 ; 'begin'
call _printf
push OFFSET $SG2984 ; 'end'
$exit$4:
call _printf
add esp, 8
xor eax, eax
ret 0
_main ENDP
然而编译器却没能删除字符串“skip me!”。
请结合自己的编译器和调试器进行有关练习。
[1] 请参阅Dij68。
[2] 请参阅Knu74,以及Yur13。