返回介绍

11.1 一些例子

发布于 2025-02-22 14:00:44 字数 7244 浏览 0 评论 0 收藏 0

#!bash
void f (int a)
{
    switch (a)
    {
        case 0: printf ("zero
"); break;
        case 1: printf ("one
"); break;
        case 2: printf ("two
"); break;
        default: printf ("something unknown
"); break;
    };
};

11.1.1 X86

反汇编结果如下(MSVC 2010):

清单 11.1: MSVC 2010

#!bash
tv64 = -4       ; size = 4
_a$ = 8         ; size = 4
_f  PROC
    push    ebp
    mov     ebp, esp
    push    ecx
    mov     eax, DWORD PTR _a$[ebp]
    mov     DWORD PTR tv64[ebp], eax
    cmp     DWORD PTR tv64[ebp], 0
    je      SHORT $LN4@f
    cmp     DWORD PTR tv64[ebp], 1
    je      SHORT $LN3@f
    cmp     DWORD PTR tv64[ebp], 2
    je      SHORT $LN2@f
    jmp     SHORT $LN1@f
$LN4@f:
    push    OFFSET $SG739 ; ’zero’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN7@f
$LN3@f:
    push    OFFSET $SG741 ; ’one’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN7@f
$LN2@f:
    push    OFFSET $SG743 ; ’two’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN7@f
$LN1@f:
    push    OFFSET $SG745 ; ’something unknown’, 0aH, 00H
    call    _printf
    add     esp, 4
$LN7@f:
    mov     esp, ebp
    pop     ebp
    ret     0
_f    ENDP

输出函数的 switch 中有一些 case 选择分支,事实上,它是和下面这个形式等价的:

#!cpp
void f (int a)
{
    if (a==0)
        printf ("zero
");
    else if (a==1)
        printf ("one
");
    else if (a==2)
        printf ("two
");
    else
        printf ("something unknown
");
};

当 switch() 中有一些 case 分支时,我们可以看到此类代码,虽然不能确定,但是,事实上 switch() 在机器码级别上就是对 if() 的封装。这也就是说,switch() 其实只是对有一大堆类似条件判断的 if() 的一个语法糖。

在生成代码时,除了编译器把输入变量移动到一个临时本地变量 tv64 中之外,这块代码对我们来说并无新意。

如果是在 GCC 4.4.1 下编译同样的代码,我们得到的结果也几乎一样,即使你打开了最高优化(-O3)也是如此。

让我们在微软 VC 编译器中打开/Ox 优化选项: cl 1.c /Fa1.asm /Ox

清单 11.2: MSVC

#!bash
_a$ = 8                 ; size = 4
_f  PROC
    mov     eax, DWORD PTR _a$[esp-4]
    sub     eax, 0
    je      SHORT $LN4@f
    sub     eax, 1
    je      SHORT $LN3@f
    sub     eax, 1
    je      SHORT $LN2@f
    mov     DWORD PTR _a$[esp-4], OFFSET $SG791 ; ’something unknown’, 0aH, 00H
    jmp     _printf
$LN2@f:
    mov     DWORD PTR _a$[esp-4], OFFSET $SG789 ; ’two’, 0aH, 00H
    jmp     _printf
$LN3@f:
    mov     DWORD PTR _a$[esp-4], OFFSET $SG787 ; ’one’, 0aH, 00H
    jmp     _printf
$LN4@f:
    mov     DWORD PTR _a$[esp-4], OFFSET $SG785 ; ’zero’, 0aH, 00H
    jmp     _printf
_f ENDP

我们可以看到浏览器做了更多的难以阅读的优化(Dirty hacks)。

首先,变量的值会被放入 EAX,接着 EAX 减 0。听起来这很奇怪,但它之后是需要检查先前 EAX 寄存器的值是否为 0 的,如果是,那么程序会设置上零标志位 ZF(这也表示了减去 0 之后,结果依然是 0),第一个条件跳转语句 JE(Jump if Equal 或者同义词 JZ - Jump if Zero)会因此触发跳转。如果这个条件不满足,JE 没有跳转的话,输入值将减去 1,之后就和之前的一样了,如果哪一次值是 0,那么 JE 就会触发,从而跳转到对应的处理语句上。

(译注:SUB 操作会重置零标志位 ZF,但是 MOV 不会设置标志位,而 JE 将只有在 ZF 标志位设置之后才会跳转。如果需要基于 EAX 的值来做 JE 跳转的话,是需要用这个方法设置标志位的)。

并且,如果没有 JE 语句被触发,最终,printf() 函数将收到“something unknown”的参数。

其次:我们看到了一些不寻常的东西——字符串指针被放在了变量里,然后 printf() 并没有通过 CALL,而是通过 JMP 来调用的。 这个可以很简单的解释清楚,调用者把参数压栈,然后通过 CALL 调用函数。CALL 通过把返回地址压栈,然后做无条件跳转来跳到我们的函数地址。我们的函数在执行时,不管在任何时候都有以下的栈结构(因为它没有任何移动栈指针的语句):

· ESP —— 指向返回地址
· ESP+4 —— 指向变量 a (也即参数)

另一方面,当我们这儿调用 printf() 函数的时候,它也需要有与我们这个函数相同的栈结构,不同之处只在于 printf() 的第一个参数是指向一个字符串的。 这也就是你之前看到的我们的代码所做的事情。

我们的代码把第一个参数的地址替换了,然后跳转到 printf(),就像第一个没有调用我们的函数 f() 而是先调用了 printf() 一样。 printf() 把一串字符输出到 stdout 中,然后执行 RET 语句, 这一句会从栈上弹出返回地址,因此,此时控制流会返回到调用 f() 的函数上,而不是 f() 上。

这一切之所以能发生,是因为 printf() 在 f() 的末尾。在一些情况下,这有些类似于 longjmp() 函数。当然,这一切只是为了提高执行速度。

ARM 编译器也有类似的优化,请见 5.3.2 节“带有多个参数的 printf() 函数调用”。

11.1.2 ARM: 优化后的 Keil + ARM 模式

#!bash
.text:0000014C             f1
.text:0000014C 00 00 50 E3          CMP R0, #0
.text:00000150 13 0E 8F 02          ADREQ R0, aZero     ; "zero
"
.text:00000154 05 00 00 0A          BEQ loc_170
.text:00000158 01 00 50 E3          CMP R0, #1
.text:0000015C 4B 0F 8F 02          ADREQ R0, aOne      ; "one
"
.text:00000160 02 00 00 0A          BEQ loc_170
.text:00000164 02 00 50 E3          CMP R0, #2
.text:00000168 4A 0F 8F 12          ADRNE R0, aSomethingUnkno ; "something unknown
"
.text:0000016C 4E 0F 8F 02          ADREQ R0, aTwo      ; "two
"
.text:00000170
.text:00000170                      loc_170             ; CODE XREF: f1+8
.text:00000170                                          ; f1+14
.text:00000170 78 18 00 EA          B __2printf

我们再一次看看这个代码,我们不能确定的说这就是源代码里面的 switch() 或者说它是 if() 的封装。

但是,我们可以看到这里它也在试图预测指令(像是 ADREQ(相等)),这里它会在 R0=0 的情况下触发,并且字符串“zero”的地址将被加载到 R0 中。如果 R0=0,下一个指令 BEQ 将把控制流定向到 loc_170 处。顺带一说,机智的读者们可能会文,之前的 ADREQ 已经用其他值填充了 R0 寄存器了,那么 BEQ 会被正确触发吗?答案是“是”。因为 BEQ 检查的是 CMP 所设置的标记位,但是 ADREQ 根本没有修改标记位。

还有,在 ARM 中,一些指令还会加上-S 后缀,这表明指令将会根据结果设置标记位。如果没有-S 的话,表明标记位并不会被修改。比如,ADD(而不是 ADDS)将会把两个操作数相加,但是并不会涉及标记位。这类指令对使用 CMP 设置标记位之后使用标记位的指令,例如条件跳转来说非常有用。

其他指令对我们来说已经很熟悉了。这里只有一个调用指向 printf(),在末尾,我们已经知道了这个小技巧(见 5.3.2 节)。在末尾处有三个指向 printf()的地址。 还有,需要注意的是如果 a=2 但是 a 并不在它的选择分支给定的常数中时,“CMP R0, #2”指令在这个情况下就需要知道 a 是否等于 2。如果结果为假,ADRNE 将会读取字符串“something unknown ”到 R0 中,因为 a 在之前已经和 0、1 做过是否相等的判断了,这里我们可以假定 a 并不等于 0 或者 1。并且,如果 R0=2,a 指向的字符串“two ”将会被 ADREQ 载入 R0。

11.1.3 ARM: 优化后的 Keil + thumb 模式

#!bash
.text:000000D4          f1
.text:000000D4 10 B5            PUSH    {R4,LR}
.text:000000D6 00 28            CMP     R0, #0
.text:000000D8 05 D0            BEQ     zero_case
.text:000000DA 01 28            CMP     R0, #1
.text:000000DC 05 D0            BEQ     one_case
.text:000000DE 02 28            CMP     R0, #2
.text:000000E0 05 D0            BEQ     two_case
.text:000000E2 91 A0            ADR     R0, aSomethingUnkno ; "something unknown
"
.text:000000E4 04 E0            B       default_case
.text:000000E6 ;
-------------------------------------------------------------------------
.text:000000E6          zero_case                           ; CODE XREF: f1+4
.text:000000E6 95 A0            ADR     R0, aZero           ; "zero
"
.text:000000E8 02 E0            B       default_case
.text:000000EA ;
-------------------------------------------------------------------------
.text:000000EA          one_case                            ; CODE XREF: f1+8
.text:000000EA 96 A0            ADR     R0, aOne            ; "one
"
.text:000000EC 00 E0            B       default_case
.text:000000EE          ;
-------------------------------------------------------------------------
.text:000000EE          two_case                            ; CODE XREF: f1+C
.text:000000EE 97 A0            ADR     R0, aTwo            ; "two
"
.text:000000F0                  default_case                ; CODE XREF: f1+10
.text:000000F0                                              ; f1+14
.text:000000F0 06 F0 7E F8      BL      __2printf
.text:000000F4 10 BD            POP     {R4,PC}
.text:000000F4           ; End of function f1

正如我之前提到的,在 thumb 模式下并没有什么功能来连接预测结果,所以这里的 thumb 代码有点像容易理解的 x86 CISC 代码。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文