返回介绍

11.2 许多例子

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

在有许多 case 分支的 switch() 语句中,对编译器来说,转换出一大堆 JE/JNE 语句并不是太方便。

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

11.2.1 x86

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

清单 11.3: 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], 4
    ja      SHORT $LN1@f
    mov     ecx, DWORD PTR tv64[ebp]
    jmp     DWORD PTR $LN11@f[ecx*4]
$LN6@f:
    push    OFFSET $SG739 ; ’zero’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN5@f:
    push    OFFSET $SG741 ; ’one’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN4@f:
    push    OFFSET $SG743 ; ’two’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN3@f:
    push    OFFSET $SG745 ; ’three’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN2@f:
    push    OFFSET $SG747 ; ’four’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN1@f:
    push    OFFSET $SG749 ; ’something unknown’, 0aH, 00H
    call    _printf
    add     esp, 4
$LN9@f:
    mov     esp, ebp
    pop     ebp
    ret     0
    npad    2
$LN11@f:
    DD  $LN6@f ; 0
    DD  $LN5@f ; 1
    DD  $LN4@f ; 2
    DD  $LN3@f ; 3
    DD  $LN2@f ; 4
_f     ENDP

好的,我们可以看到这儿有一组不同参数的 printf() 调用。 它们不仅有内存中的地址,编译器还给它们带上了符号信息。顺带一提,这些符号标签也都存在于$LN11@f 内部函数表中。

在函数最开始,如果 a 大于 4,控制流将会被传递到标签$LN1@f 上,这儿会有一个参数为“something unknown”的 printf() 调用。

如果 a 值小于等于 4,然后我们把它乘以 4,加上$LN1@f 的函数地址。这就是在函数表内部构造地址的方法,这样可以正好指向我们需要的元素。比如 a 等于 2。 那么,2×4=8(在 32 位进程下,所有的函数表元素的长度都只有 4 字节),$LN11@f 的函数表地址+8——这样就能取得$LN4@f 标签的位置。 JMP 将从函数表中获得$LN4@f 的地址,然后跳转向它。

这个函数表,有时候也叫做跳转表(jumptable)。

然后,对应的,printf() 的参数就是“two”了。 字面意思, JMP DWORD PTR $LN11@f[ECX4] 指令意味着“ 跳转到存储在$LN11@f + ecx 4 地址上的双字”。 npad(64)是一个编译时语言宏,它用于对齐下一个标签,这样存储的地址就会按照 4 字节(或者 16 字节)对齐。这个对于处理器来说是十分合适的,因为通过内存总线、缓存从内存中获取 32 位的值是非常方便而且有效率的。

让我们看看 GCC 4.4.1 生成的代码:

清单 11.4: GCC 4.4.1

#!bash
        public f
f       proc near ; CODE XREF: main+10

var_18  = dword ptr -18h
arg_0   = dword ptr 8
        push    ebp
        mov     ebp, esp
        sub     esp, 18h ; char *
        cmp     [ebp+arg_0], 4
        ja      short loc_8048444
        mov     eax, [ebp+arg_0]
        shl     eax, 2
        mov     eax, ds:off_804855C[eax]
        jmp     eax
loc_80483FE:                    ; DATA XREF: .rodata:off_804855C
        mov     [esp+18h+var_18], offset aZero ; "zero"
        call    _puts
        jmp     short locret_8048450
loc_804840C:                    ; DATA XREF: .rodata:08048560
        mov     [esp+18h+var_18], offset aOne ; "one"
        call    _puts
        jmp     short locret_8048450
loc_804841A:                    ; DATA XREF: .rodata:08048564
        mov     [esp+18h+var_18], offset aTwo ; "two"
        call    _puts
        jmp     short locret_8048450
loc_8048428:                    ; DATA XREF: .rodata:08048568
        mov     [esp+18h+var_18], offset aThree ; "three"
        call    _puts
        jmp     short locret_8048450
loc_8048436:                    ; DATA XREF: .rodata:0804856C
        mov     [esp+18h+var_18], offset aFour ; "four"
        call    _puts
        jmp     short locret_8048450
loc_8048444:                    ; CODE XREF: f+A
        mov     [esp+18h+var_18], offset aSomethingUnkno ; "something unknown"
        call    _puts
locret_8048450:                 ; CODE XREF: f+26
                                ; f+34...
        leave
        retn
f       endp

off_804855C dd offset loc_80483FE ; DATA XREF: f+12
            dd offset loc_804840C
            dd offset loc_804841A
            dd offset loc_8048428
            dd offset loc_8048436

基本和 VC 生成的相同,除了少许的差别:参数 arg_0 的乘以 4 操作被左移 2 位替换了(这集合和乘以 4 一样)(见 17.3.1 节)。 然后标签地址从 off_804855C 处的数组获取,地址计算之后存储到 EAX 中,然后通过 JMP EAX 跳转到实际的地址上。

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

#!bash
00000174                f2
00000174 05 00 50 E3            CMP     R0, #5                  ; switch 5 cases
00000178 00 F1 8F 30            ADDCC   PC, PC, R0,LSL#2        ; switch jump
0000017C 0E 00 00 EA            B       default_case            ; jumptable 00000178 default case
00000180                ; -------------------------------------------------------------------------
00000180
00000180                loc_180                         ; CODE XREF: f2+4
00000180 03 00 00 EA            B       zero_case       ; jumptable 00000178 case 0
00000184                ; -------------------------------------------------------------------------
00000184
00000184                loc_184                         ; CODE XREF: f2+4
00000184 04 00 00 EA            B       one_case        ; jumptable 00000178 case 1
00000188                ; -------------------------------------------------------------------------
00000188
00000188                loc_188                         ; CODE XREF: f2+4
00000188 05 00 00 EA            B       two_case        ; jumptable 00000178 case 2
0000018C                ; -------------------------------------------------------------------------
0000018C
0000018C                loc_18C                         ; CODE XREF: f2+4
0000018C 06 00 00 EA            B       three_case      ; jumptable 00000178 case 3
00000190                ; -------------------------------------------------------------------------
00000190
00000190                loc_190                         ; CODE XREF: f2+4
00000190 07 00 00 EA            B       four_case       ; jumptable 00000178 case 4
00000194                ; -------------------------------------------------------------------------
00000194
00000194                zero_case                       ; CODE XREF: f2+4
00000194                                                ; f2:loc_180
00000194 EC 00 8F E2            ADR     R0, aZero       ; jumptable 00000178 case 0
00000198 06 00 00 EA            B       loc_1B8
0000019C                ; -------------------------------------------------------------------------
0000019C
0000019C one_case                                       ; CODE XREF: f2+4
0000019C                                                ; f2:loc_184
0000019C EC 00 8F E2            ADR     R0, aOne        ; jumptable 00000178 case 1
000001A0 04 00 00 EA            B       loc_1B8
000001A4                ; -------------------------------------------------------------------------
000001A4
000001A4                two_case                        ; CODE XREF: f2+4
000001A4                                                ; f2:loc_188
000001A4 01 0C 8F E2            ADR     R0, aTwo        ; jumptable 00000178 case 2
000001A8 02 00 00 EA            B       loc_1B8
000001AC                ; -------------------------------------------------------------------------
000001AC
000001AC                three_case                      ; CODE XREF: f2+4
000001AC                                                ; f2:loc_18C
000001AC 01 0C 8F E2            ADR     R0, aThree ; jumptable 00000178 case 3
000001B0 00 00 00 EA            B       loc_1B8
000001B4 ; -------------------------------------------------------------------------
000001B4
000001B4                four_case                       ; CODE XREF: f2+4
000001B4                                                ; f2:loc_190
000001B4 01 0C 8F E2            ADR     R0, aFour       ; jumptable 00000178 case 4
000001B8
000001B8                loc_1B8                         ; CODE XREF: f2+24
000001B8                                                ; f2+2C
000001B8 66 18 00 EA            B       __2printf
000001BC ; -------------------------------------------------------------------------
000001BC
000001BC                default_case                    ; CODE XREF: f2+4
000001BC                                                ; f2+8
000001BC D4 00 8F E2            ADR     R0, aSomethingUnkno ; jumptable 00000178 default case
000001C0 FC FF FF EA            B       loc_1B8
000001C0                ; End of function f2

这个代码利用了 ARM 的特性,这里 ARM 模式下所有指令都是 4 个字节。

让我们记住 a 的最大值是 4,任何更大额值都会导致它输出“something unknown ”。

最开始的“CMP R0, #5”指令将 a 的值与 5 比较。

下一个“ADDCC PC, PC, R0, LSL#2”指令将仅在 R0<5 的时候执行(CC = Carry clear , 小于)。所以,如果 ADDCC 并没有触发(R0>=5 时),它将会跳转到 default _case 标签上。

但是,如果 R0<5,而且 ADDCC 触发了,将会发生下列事情:

R0 中的值会乘以 4,事实上,LSL#2 代表着“左移 2 位”,但是像我们接下来(见 17.3.1 节)要看到的“移位”一样,左移 2 位代表乘以 4。

然后,我们得到了 R0 * 4 的值,这个值将会和 PC 中现有的值相加,因此跳转到下述其中一个 B(Branch 分支)指令上。

在 ADDCC 执行时,PC 中的值(0x180)比 ADDCC 指令的值(0x178)提前 8 个字节,换句话说,提前 2 个指令。

这也就是为 ARM 处理器通道工作的方式:当 ADDCC 指令执行的时候,此时处理器将开始处理下一个指令,这也就是 PC 会指向这里的原因。

如果 a=0,那么 PC 将不会和任何值相加,PC 中实际的值将写入 PC 中(它相对之领先 8 个字节),然后跳转到标签 loc_180 处。这就是领先 ADDCC 指令 8 个字节的地方。

在 a=1 时,PC+8+a_4 = PC+8+1_4 = PC+16= 0x184 将被写入 PC 中,这是 loc_184 标签的地址。

每当 a 上加 1,PC 都会增加 4,4 也是 ARM 模式的指令长度,而且也是 B 指令的长度。这组里面有 5 个这样的指令。

这 5 个 B 指令将传递控制流,也就是传递 switch()中指定的字符串和对应的操作等等。

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

#!bash
000000F6                        EXPORT  f2
000000F6                f2
000000F6 10 B5                  PUSH    {R4,LR}
000000F8 03 00                  MOVS    R3, R0
000000FA 06 F0 69 F8            BL      __ARM_common_switch8_thumb ; switch 6 cases
000000FA                ;
-------------------------------------------------------------------------
000000FE 05                     DCB 5
000000FF 04 06 08 0A 0C 10      DCB 4, 6, 8, 0xA, 0xC, 0x10 ; jump table for switch
statement
00000105 00                     ALIGN 2
00000106
00000106                zero_case                       ; CODE XREF: f2+4
00000106 8D A0                  ADR       R0, aZero       ; jumptable 000000FA case 0
00000108 06 E0                  B       loc_118
0000010A ;
-------------------------------------------------------------------------
0000010A
0000010A                    one_case                    ; CODE XREF: f2+4
0000010A 8E A0                  ADR       R0, aOne        ; jumptable 000000FA case 1
0000010C 04 E0                  B       loc_118
0000010E ;
-------------------------------------------------------------------------
0000010E
0000010E                    two_case                    ; CODE XREF: f2+4
0000010E 8F A0                  ADR       R0, aTwo        ; jumptable 000000FA case 2
00000110 02 E0                  B       loc_118
00000112 ;
-------------------------------------------------------------------------
00000112
00000112                    three_case                  ; CODE XREF: f2+4
00000112 90 A0                  ADR       R0, aThree      ; jumptable 000000FA case 3
00000114 00 E0                  B       loc_118
00000116 ;
-------------------------------------------------------------------------
00000116
00000116                    four_case                   ; CODE XREF: f2+4
00000116 91 A0                  ADR       R0, aFour       ; jumptable 000000FA case 4
00000118
00000118                    loc_118                     ; CODE XREF: f2+12
00000118                                                ; f2+16
00000118 06 F0 6A F8            BL        __2printf
0000011C 10 BD                  POP       {R4,PC}
0000011E ;
-------------------------------------------------------------------------
0000011E
0000011E                    default_case                ; CODE XREF: f2+4
0000011E 82 A0                  ADR       R0, aSomethingUnkno ; jumptable 000000FA default
case
00000120 FA E7                  B         loc_118

000061D0                        EXPORT __ARM_common_switch8_thumb
000061D0                    __ARM_common_switch8_thumb ; CODE XREF: example6_f2+4
000061D0 78 47                  BX          PC
000061D0                    ;
---------------------------------------------------------------------------
000061D2 00 00                  ALIGN 4
000061D2                    ; End of function __ARM_common_switch8_thumb
000061D2
000061D4                        CODE32
000061D4
000061D4                    ; =============== S U B R O U T I N E
=======================================
000061D4
000061D4
000061D4                    __32__ARM_common_switch8_thumb  ; CODE XREF:
    __ARM_common_switch8_thumb
000061D4 01 C0 5E E5            LDRB    R12, [LR,#-1]
000061D8 0C 00 53 E1            CMP     R3, R12
000061DC 0C 30 DE 27            LDRCSB  R3, [LR,R12]
000061E0 03 30 DE 37            LDRCCB  R3, [LR,R3]
000061E4 83 C0 8E E0            ADD     R12, LR, R3,LSL#1
000061E8 1C FF 2F E1            BX      R12
000061E8                ; End of function __32__ARM_common_switch8_thumb

一个不能确定的事实是 thumb、thumb-2 中的所有指令都有同样的大小。甚至可以说是在这些模式下,指令的长度是可变的,就像 x86 一样。

所以这一定有一个特别的表单,里面包含有多少个 case(除了默认的 case),然后和它们的偏移,并且给他们每个都加上一个标签,这样控制流就可以传递到正确的位置。 这里有一个特别的函数来处理表单和处理控制流,被命名为__ARM_common_switch8_thumb。它由“BX PC”指令开始,这个函数用来将处理器切换到 ARM 模式,然后你就可以看到处理表单的函数。不过对我们来说,在这里解释它太复杂了,所以我们将省去一些细节。

但是有趣的是,这个函数使用 LR 寄存器作为表单的指针。还有,在这个函数调用后,LR 将包含有紧跟着“BL __ARM_common_switch8_thumb”指令的地址,然后表单就由此开始。

当然,这里也不值得去把生成的代码作为单独的函数,然后再去重用它们。因此在 switch()处理相似的位置、相似的 case 时编译器并不会生成相同的代码。

IDA 成功的发觉到它是一个服务函数以及函数表,然后给各个标签加上了合适的注释,比如 jumptable 000000FA case 0。

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

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

发布评论

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