第11章 GOTO语句

人们普遍认为使用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所示。

..\TU\1101.tif

图11.1 Hiew

..\TU\1102.tif

图11.2 Hiew

JMP所在的opcode的第二个字节代表着跳转的相对偏移量。把这个偏移量改为0,就可以让它继续运行后续指令。这样JMP指令就不会跳过第二个printf()函数。

按下F9键(保存文件)并退出Hiew。再次运行这个可执行文件的情况应当如图11.3所示。

..\TU\1103.tif

图11.3 修改后的程序

当然,把JMP指令替换为两个NOP指令可以达到同样效果。因为NOP的opcode是0x90、只占用一个字节,所以在进行替换时要写上两个NOP指令。

11.1 无用代码Dead Code

在编译术语里,上述程序中第二次调用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!”。

11.2 练习题

请结合自己的编译器和调试器进行有关练习。


[1] 请参阅Dij68。

[2] 请参阅Knu74,以及Yur13。