Bootstrap

关于 C/C++ 中的 switch 语句,您可能不知道

关于 C/C++ 中的 switch 语句,您可能不知道

关于如何通过VC++中的逆向工程执行开关/案例的讨论

介绍

许多编程语言,如 C/C++、C#、Java 和 Pascal 都提供了让我们实现选择逻辑的语句。在某些情况下,它是 的良好替代方法,使代码更清晰、更具可读性。在实践中使用时,您可能想知道:switchif-then-elseswitch

  • 块在运行时如何执行?switch
  • 对于一长串条件,它的运行速度是否比一长串更快?if-then-else
  • 对于 n 个条件,时间复杂度是多少?switch

C/C++ 标准定义了语言元素的规范,但它没有说明如何实现该语句。每个供应商都可以自由使用任何实现,只要它符合标准。本文通过一些不同条件下的示例,讨论在 Visual C++ 中运行语句时会发生什么情况。我们将使用 Microsoft Visual Studio IDE 分析这些示例,因为它可以在编译时生成相应的程序集列表。因此,假设对英特尔(x86)汇编语言有一般的了解。正如您稍后看到的,这里的所有结果都基于逆向工程,因此本文从来都不是对编译器中实现的全面描述。如果你正在学习汇编语言编程,这篇文章可能是阅读的学习材料。switchswitchswitch

我们的第一个示例是 switch1.cpp,这是一个常用的简单块,如下所示:

C++

#include "functions.h"int main()

{

    int i =3;    // or i =20

    switch (i)

    {

        case 1: f1(); break;

        case 2: f2(); break;

        case 5: f1(); break;

        case 7: f2(); break;

        case 10: f1(); break;

        case 11: f2(); break;

        case 12: f2(); break;

        case 17: f1(); break;

        case 18: f1(); break;

        default: f3();

    }

    return 0;

}

其中在函数中定义了三个函数.cpp

C++

void f1() { cout  "f1 called\n"; }void f2() { cout  "f2 called\n"; }void f3() { cout  "f3 called\n"; }

最坏的情况可以被认为是或吗?它如何执行:它是否用尽了所有九种情况并最终到达 to 调用?让我们从汇编翻译中回答它。i=3i=20defaultf3

若要在 Visual Studio 中生成程序集列表,请打开 switch1.cpp 属性对话框,然后选择 C/C++ 下的“输出文件”类别。在右窗格中,选择“带源代码的程序集 (/FA)”选项,如下所示:

然后,当您编译 switch1.cpp 时,将生成一个名为 switch1.asm 的程序集文件。使用此选项,列表包括C++源代码,该源代码由带有行号的分号注释,如下一节所示。

两级跳台

让我们从上到下分析程序集列表。这是开始的地方:switch

安盛

; 5    :     int i =3;    // or i =20

    mov    DWORD PTR _i$[ebp], 3

; 6    : ; 7    :     switch (i)

    mov    eax, DWORD PTR _i$[ebp]

    mov    DWORD PTR tv64[ebp], eax

    mov    ecx, DWORD PTR tv64[ebp]

    sub    ecx, 1

    mov    DWORD PTR tv64[ebp], ecx

    cmp    DWORD PTR tv64[ebp], 17            ; 00000011H

    ja     SHORT $LN1@main

    mov    edx, DWORD PTR tv64[ebp]

    movzx  eax, BYTE PTR $LN15@main[edx]

    jmp    DWORD PTR $LN16@main[eax*4]

假设符号是 的别名,是 的另一个名称,则重命名为 、 和 ,并且是名为 的标签。该代码片段仅在伪代码中执行此操作:_i$[ebp]itv64[ebp]i2$LN15@maintable1$LN16@maintable2$LN1@mainDefault_Label

i2 = i;

i2 = i2-1;

if i2 > 17 goto Default_Label;

goto table2[4*table1[i2]];

此处,17 表示最后一个大小写条件值,因为整数 1 到 18 从 0 映射到 17。这就是为什么递减以使其成为从零开始的整数作为索引的原因。现在,如果大于 17(例如,),控件将转到 .否则,它会转到指向的位置。i2i2n=20defaulttable2[4*table1[i2]]

什么时候零怎么样?然后变为 -1。担心索引超出范围错误?不,它永远不会发生。回到程序集列表,您可以看到 -1 保存为 ,双字为无符号 4 字节整数。因此,它必须大于 17 并转到 。ii2DWORDdefault

让我们看一下两个表,看看它们是如何协同工作的。这很简单,起始地址为 ,您可以将其视为数组名称。table1 $LN15@main

安盛

$LN15@main:

    DB    0

    DB    1

    DB    9

    DB    9

    DB    2

    DB    9

    DB    3

    DB    9

    DB    9

    DB    4

    DB    5

    DB    6

    DB    9

    DB    9

    DB    9

    DB    9

    DB    7

    DB    8

对于这个数组,是 0、是 1、是 9,依此类推。创建这些值是为了计算 的索引,其起点为 :table1[0]table1[1]table1[2]table1[3]table2$LN16@main

安盛

$LN16@main:

    DD    $LN10@main

    DD    $LN9@main

    DD    $LN8@main

    DD    $LN7@main

    DD    $LN6@main

    DD    $LN5@main

    DD    $LN4@main

    DD    $LN3@main

    DD    $LN2@main

    DD    $LN1@main

上面的标签,从 到 ,是 C++ 中的 8 个调用目标,对于 32 种情况加 4 种情况。请注意,这表示定义字节(<> 位),同时定义四个字节(<> 位)的双字类型。这就是为什么我们需要在 .通过这个公式,我们通过和计算呼叫地址:$LN10@main$LN1@maindefaultDBDDtable2[4*table1[i2]]table1 table2

  • 如果等于 1,则为 0 且为 0,跳转到 by ,这是第一种情况。ii2table1[0]$LN10@maintable2[0]
  • 如果等于 2,则为 1 且为 1,跳转到 by ,为第二种情况。ii2table1[1]$LN9@maintable2[4*1]
  • 如果等于 3,为 2 且为 9,则跳转到默认值 by 。ii2table1[2]$LN1@maintable2[4*9]
  • ... ...

现在我们来到标记为 to 作为调用目标的代码段:LN10@main$LN1@main

安盛

收缩 ▲   

$LN10@main:; 8    :     {; 9    :         case 1: f1(); break;

    call        ?f1@@YAXXZ            ; f1

    jmp    SHORT $LN11@main

$LN9@main:; 10   :         case 2: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN11@main

$LN8@main:; 11   :         case 5: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    SHORT $LN11@main

; ... ...

$LN2@main:; 17   :         case 18: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    SHORT $LN11@main

$LN1@main:; 18   : ; 19   :         default: f3();

    call    ?f3@@YAXXZ                ; f3

$LN11@main:; 20   :     }; 21   : ; 22   :     return 0;

函数 、 和 转换为修饰的汇编过程,并以问号为前缀,如 、 和 。请注意,这是要调用的情况 1 的标签,要调用的情况 2 和要调用的标签。f1f2f3?f1@@YAXXZ?f2@@YAXXZ?f3@@YAXXZ$LN10@mainf1$LN9@mainf2$LN1@maindefaultf3

附加标签是 ,指向最后一个子句之后的位置。完成每个案例操作后,控件将跳转到 。这将实现该语句。没有它,控制权将转到下一个案例。这就是为什么该语句在 C/C++ 块中是必需的。$LN11@maindefault$LN11@mainbreakbreakswitch

显然,基于这样的两级表机制,我们有一个比较,一个乘法和两个地址跳转。此模式的时间复杂度应为 O(1)。综上所述,我们有一个这样的大图景:switch

回想一下,我们用于定义 中的索引值,以及 for 中的索引值。在此示例中,只有 18 个事例值。但定义意味着范围仅从 255 到 255。如果 中有更多情况,值数超过 <> 怎么办?DBtable1DDtable2table1DBbyteswitch

单级跳台

这是第二个示例,switch2.cpp有 1000 个案例。为简单起见,我们从条件值零开始:

C++

int main2()

{

    int i =1000;

    switch (i)

    {

        case 0: f1(); break;

        case 1: f1(); break;

        case 2: f2(); break;

        case 3: f1(); break;

      // ... ...        case 995: f1(); break;

        case 996: f1(); break;

        case 997: f1(); break;

        case 998: f2(); break;

        case 999: f1(); break;

        default: f3();

    }

    return 0;

}

不要认为这会让事情变得复杂。相反,它变得更简单。这是Visual Studio为我们生成的switch2.asm

安盛

; 5    :     int i =1000;

    mov    DWORD PTR _i$[ebp], 1000        ; 000003e8H

; 6    : ; 7    :     switch (i)

    mov    eax, DWORD PTR _i$[ebp]

    mov    DWORD PTR tv64[ebp], eax

    cmp    DWORD PTR tv64[ebp], 999        ; 000003e7H

    ja     $LN1@main2

    mov    ecx, DWORD PTR tv64[ebp]

    jmp    DWORD PTR $LN1006@main2[ecx*4]

同样,假定该符号是 的别名。由于第一种情况从零开始,因此不需要映射。所以(从)等于。数组是唯一的跳转表,标签是 。此代码段只是这样做:_i$[ebp]ii2tv64[ebp]i$LN1006@main2$LN1@main2Default_Label

i2 = i;

if i2 > 999 goto Default_Label;

goto table[4*i2];

通过在switch2.asm中搜索,我们找到所有由双字定义的代码标签:$LN1006@main2

安盛

$LN1006@main2:

    DD    $LN1001@main2

    DD    $LN1000@main2

    DD    $LN999@main2

    DD    $LN998@main2

    DD    $LN997@main2

    DD    $LN996@main2

   ; ... ...

    DD    $LN5@main2

    DD    $LN4@main2

    DD    $LN3@main2

    DD    $LN2@main2

总共 1000 个调用目标,每个标签后跟一个过程调用,从 到 :$LN1001@main2$LN2@main2

安盛

收缩 ▲   

$LN1001@main2:; 8    :     {; 9    :         case 0: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    $LN1002@main2

$LN1000@main2:; 10   :         case 1: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    $LN1002@main2

$LN999@main2:; 11   :         case 2: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    $LN1002@main2

; ... ...

$LN3@main2:; 1106 :         case 998: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN1002@main2

$LN2@main2:; 1107 :         case 999: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    SHORT $LN1002@main2

$LN1@main2:; 1108 : ; 1109 :         default: f3();

    call    ?f3@@YAXXZ                ; f3

$LN1002@main2:

; 1110 :     }; 1111 : ; 1112 :     return 0;

此处,指向 ,附加标签实现 。虽然复杂度也是 O(1),但这种机制应该有点有效,因为只需要一个地址跳转。下面是此示例的图片:$LN1@main2default$LN1002@mainbreak

到目前为止,我们看到插图执行得非常好。真的比用得好吗?对于没有匹配的条件,当然必须检查每个条件,直到最后一个条件,这是 O(n) 的最坏情况。但是对于 ,我们是否总是期望它的复杂性为 O(1)?或者是否有某种代码会导致其执行超出此范围?switchswitchif-then-elsenif-then-elseswitch

使用二叉搜索

我们将给出第三个示例,显示 switch3.cpp 中案例条件值之间的巨大差距,其中执行的行为与二叉搜索一样:switch

C++

int main3()

{

    int i =1;

    switch (i)

    {

        case 100: f1(); break;

        case 200: f2(); break;

        case 250: f2(); break;

        case 500: f1(); break;

        case 700: f2(); break;

        case 750: f2(); break;

        case 800: f2(); break;

        case 900: f1(); break;

        default: f3();

    }

    return 0;

}

通过生成程序集列表 switch3.asm,这一次,我们通过首先检查案例操作来自下而上查看:

安盛

收缩 ▲   

$LN9@main3:

; 8    :     {; 9    :         case 100: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    SHORT $LN10@main3

$LN8@main3:; 10   :         case 200: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN10@main3

$LN7@main3:; 11   :         case 250: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN10@main3

$LN6@main3:; 12   :         case 500: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    SHORT $LN10@main3

$LN5@main3:; 13   :         case 700: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN10@main3

$LN4@main3:; 14   :         case 750: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN10@main3

$LN3@main3:; 15   :         case 800: f2(); break;

    call    ?f2@@YAXXZ                ; f2

    jmp    SHORT $LN10@main3

$LN2@main3:; 16   :         case 900: f1(); break;

    call    ?f1@@YAXXZ                ; f1

    jmp    SHORT $LN10@main3

$LN1@main3:; 17   : ; 18   :         default: f3();

    call    ?f3@@YAXXZ                ; f3

$LN10@main3:; 19   :     }; 20   : ; 21   :     return 0;

非常整洁,有八个案例加一个,我们可以轻松地将此部分写入九行的列表,每行对应于目标标签,C++案例和过程名称:default

除了 ,我们还实现了该语句。接下来,我们如何跳转到这些调用目标?请看下面的代码:$LN9@main3$LN1@main3$LN10@main3break

安盛

收缩 ▲   

; 5    :     int i =1;

    mov    DWORD PTR _i$[ebp], 1

; 6    : ; 7    :     switch (i)

    mov    eax, DWORD PTR _i$[ebp]

    mov    DWORD PTR tv64[ebp], eax

    cmp    DWORD PTR tv64[ebp], 700        ; 000002bcH

    jg     SHORT $LN14@main3

    cmp    DWORD PTR tv64[ebp], 700        ; 000002bcH

    je     SHORT $LN5@main3

    cmp    DWORD PTR tv64[ebp], 250        ; 000000faH

    jg     SHORT $LN15@main3

    cmp    DWORD PTR tv64[ebp], 250        ; 000000faH

    je     SHORT $LN7@main3

    cmp    DWORD PTR tv64[ebp], 100        ; 00000064H

    je     SHORT $LN9@main3

    cmp    DWORD PTR tv64[ebp], 200        ; 000000c8H

    je     SHORT $LN8@main3

    jmp    SHORT $LN1@main3

$LN15@main3:

    cmp    DWORD PTR tv64[ebp], 500        ; 000001f4H

    je     SHORT $LN6@main3

    jmp    SHORT $LN1@main3

$LN14@main3:

    cmp    DWORD PTR tv64[ebp], 750        ; 000002eeH

    je     SHORT $LN4@main3

    cmp    DWORD PTR tv64[ebp], 800        ; 00000320H

    je     SHORT $LN3@main3

    cmp    DWORD PTR tv64[ebp], 900        ; 00000384H

    je     SHORT $LN2@main3

    jmp    SHORT $LN1@main3

逻辑并不难理解。首先,条件值保存在 和 中。为了简化起见,我们只是通过删除前导“$”和尾部“@main3”来使用此处的所有标签。像这样重写代码段:i_i$[ebp]tv64[ebp]

    i2 = i;

    if i2 > 700 goto LN14;

    if i2 == 700 goto LN5;

    if i2 > 250 goto LN15;

    if i2 == 250 goto LN7;

    if i2 == 100 goto LN9;

    if i2 == 200 goto LN8;

    goto LN1;

LN15:

    if i2 == 500 goto LN6;

    goto LN1;

LN14:

    if i2 == 750 goto LN4;

    if i2 == 800 goto LN3;

    if i2 == 900 goto LN2;

    goto LN1;

当然,编译器会优化代码,因为它首先选择 700 进行比较,而不是块中的起始大小写值 100。虽然这是通过九个条件的十个语句实现的,但它实际上应用了二叉搜索机制。下面显示了上述代码的等效决策树,您可能熟悉的二叉搜索树:switchswitchif-then

对于圆圈中所有黄色的内部节点,我们进行比较,而椭圆形的所有剩余节点表示成功的结局。默认情况下,每个故障都会变为 。比较节点的顺序遍历序列为 100、200、250、500、700、750、800 和 900;而无序遍历中所有叶子的序列正好是 、、、 和 ,如上表中的顺序。本质上,这是一种复杂度为 O(log n) 的二叉搜索算法。当 =1 时,它将通过前六个比较,然后达到 .但是当=500时,只需要四个比较。LN1LN9LN8LN7LN6LN5LN4LN3LN2idefaultLN1i

众所周知,二叉搜索的先决条件是对输入数据进行排序。我想知道这是否只是因为我在 switch3.cpp 中使用了升序大小写值。好奇心在switch4.cpp中带出了以下无序条件。switch

C++

int main4()

{

    int i =1;

    switch (i)

    {

    case 750: f2(); break;

    case 700: f2(); break;

    case 250: f2(); break;

    case 500: f1(); break;

    case 800: f2(); break;

    case 900: f1(); break;

    case 100: f1(); break;

    case 200: f2(); break;

        default: f3();

    }

    return 0;

}

令我惊讶的是,相同的二叉搜索策略出现在switch4.asm的代码中,与上面显示的决策树完全相同。唯一的区别是标签被重新编号 - 这是非常合理的,因为我们刚刚重新排序了它们!您可以检查连接的 switch4.asm 以了解详细信息。

这个实验无疑为我们提供了一些提示,以了解编译器如何神奇地对案例条件值进行排序。排序算法超过 O(log n),不值得在运行时使用它。请注意,排序在生成的程序集中不可见。这意味着排序不包含在将在运行时执行的程序集指令中。另请注意,在编译器将C++转换为机器代码之前,所有条件值都是常量且可用。现在考虑预处理器是合理的;所有已知事例值的排序可以在编译中简单地完成。这就是为什么翻译后的汇编代码只反映二叉搜索而不对代码进行排序的原因。静态排序行为(与运行时的动态行为相反)可以通过宏过程、程序集指令和运算符来实现。这样的预处理示例可以在文章末尾的参考资料中找到。

混合体

此时,我可以展示一个跳转表和二叉搜索组合的示例,如 switch5.cpp

C++

int main5()

{

    int i =1;

    switch (i)

    {

        case 3: f1(); break;

        case 5: f2(); break;

        case 6: f2(); break;

        case 7: f2(); break;

        case 100: f1(); break;

        case 200: f2(); break;

        case 250: f2(); break;

        case 700: f2(); break;

        case 750: f2(); break;

        case 900: f2(); break;

        default: f3();

    }

    return 0;

}

以下是在相应的 switch5.asm 中如何实现此块:switch

安盛

收缩 ▲   

; 5    :     int i =1;

    mov    DWORD PTR _i$[ebp], 1

; 6    : ; 7    :     switch (i)

    mov    eax, DWORD PTR _i$[ebp]

    mov    DWORD PTR tv64[ebp], eax

    cmp    DWORD PTR tv64[ebp], 100        ; 00000064H

    jg     SHORT $LN16@main5

    cmp    DWORD PTR tv64[ebp], 100        ; 00000064H

    je     $LN7@main5

    mov    ecx, DWORD PTR tv64[ebp]

    sub    ecx, 3

    mov    DWORD PTR tv64[ebp], ecx

    cmp    DWORD PTR tv64[ebp], 4

    ja     $LN1@main5

    mov    edx, DWORD PTR tv64[ebp]

    jmp    DWORD PTR $LN18@main5[edx*4]

$LN16@main5:

    cmp    DWORD PTR tv64[ebp], 700        ; 000002bcH

    jg     SHORT $LN17@main5

    cmp    DWORD PTR tv64[ebp], 700        ; 000002bcH

    je     SHORT $LN4@main5

    cmp    DWORD PTR tv64[ebp], 200        ; 000000c8H

    je     SHORT $LN6@main5

    cmp    DWORD PTR tv64[ebp], 250        ; 000000faH

    je     SHORT $LN5@main5

    jmp    SHORT $LN1@main5

$LN17@main5:

    cmp    DWORD PTR tv64[ebp], 750        ; 000002eeH

    je     SHORT $LN3@main5

    cmp    DWORD PTR tv64[ebp], 900        ; 00000384H

    je     SHORT $LN2@main5

    jmp    SHORT $LN1@main5

它由两部分组成,首先是一级跳转表,然后是二进制搜索代码。使用前面的表示法和标签命名,let 作为表和 .此代码段执行以下操作:ii2$LN18@main5$LN1@main5default

    i2 = i;

    if i2 > 100 goto LN16;

    if i2 == 100 goto LN7;    // go case 100

                              // now i2 < 100

    i2 = i2-3;                // map to zero-based index

    if i2 > 4 goto LN1;       // go default over 7

    goto table[4*i2];         // go jump table

LN16:                         // binary search

    if i2 > 700 goto LN17;

    if i2 == 700 goto LN4;    // go case 700

    if i2 == 200 goto LN6;    // go case 200

    if i2 == 250 goto LN5;    // go case 250

    goto LN1;                 // go default

LN17:

    if i2 == 750 goto LN3;    // go case 750

    if i2 == 900 goto LN2;    // go default    

    goto LN1;

其中,该表定义为:

安盛

$LN18@main5:

    DD    LN11    ; index 0, go case 3

    DD    LN1     ; index 1 for value 4, go default

    DD    LN10    ; index 2, go case 5

    DD    LN9     ; index 3, go case 6

    DD    LN8     ; index 4, go case 7

一个有趣的更改是删除 switch6.cpp 中的情况 5。正因为如此,混合成为两级跳转表和二叉搜索的组合。有关详细信息,请查看可下载示例中的 switch6.cpp 和 switch6.asm。如果尝试按某种顺序添加额外的事例,则可以找到两个单独的跳转表与二进制搜索相结合。

更多问题和更多

现在你已经从一些典型的例子中学到了一些东西。您现在应该明白为什么 C/C++ 只支持其条件表达式的整型数据类型,例如枚举类型,而不是点或类型。除此之外,您脑海中还可能会出现更直观的问题:switchswitchchar float string

  • 是否有必要按表的条件值顺序维护事例?jump
  • 如果我们使用负整数作为大小写值呢?
  • 如果标签丢失,或者出现在块中的任何位置而不是最后怎么办?default

我相信您可以通过分析包含这些问题的C++代码的程序集列表来回答这些问题。为方便起见,我在示例中附加了以下 switch7.cpp 及其 switch7.asm

C++

int main7()

{

    int i =15;

    switch (i)

    {

        case 2: f2(); break;

        case 1: f1(); break;

        case 5: f1(); break;

        case 10: f1(); break;

        case 7: f2(); break;

        default: f3(); break;

        case -3: f2(); break;

        case 12: f1(); break;

        case 17: f2(); break;

        case 18: f1(); break;

    }

    return 0;

}

尽管性能看起来比这些示例中更好,但我们仍有未回答的问题。显然,在实践中不可能列举所有执行。对实现的全面分析应该由编译器开发人员编写,而不是由盲目黑盒测试人员编写。因此,我们无法确定执行中最坏的情况,无论它是否会达到O(n)。我们也不知道在哪种情况下,编译器会选择一种实现而不是另一种实现,甚至是此处未提及的其他方法。switchif-than-elseswitchswitchswitch

作为讨论中的读者的示例,一个问题是在实现稀疏填充的开关情况时上述跳转表的内存浪费。一个只有两个案例条目的极端示例怎么样,如下所示?编译器是否会消耗跳转表中大多数无用的条目?1 0x7fffffff

C++

int main8()

{

    int i =3;

 

    switch (i)

    {

        case 1: f1(); break;

        case 0x7fffffff: f2(); break;

        default: f3(); break;

    }

 

    return 0;

对于我们的智能编译器来说,情况并非如此。 如前所述,我们不知道编译器如何选择 一种实现而不是另一种实现。请看这里,这个 两种情况的示例不选择跳转表。它只是简单地转换为,而不消耗更多的内存:switchif-then

安盛

收缩 ▲   

; 5    :     int i =3;

mov DWORD PTR _i$[ebp], 3

 ; 6    : ; 7    :     switch (i)

mov eax, DWORD PTR _i$[ebp]

mov DWORD PTR tv64[ebp], eax

cmp DWORD PTR tv64[ebp], 1

je SHORT $LN3@main8

cmp DWORD PTR tv64[ebp], 2147483647 ; 7fffffffH

je SHORT $LN2@main8

jmp SHORT $LN1@main8

 

$LN3@main8:; 8    :     {; 9    :         case 1: f1(); break;

call ?f1@@YAXXZ ; f1

jmp SHORT $LN4@main8

 

$LN2@main8:; 10   :         case 0x7fffffff: f2(); break;

call ?f2@@YAXXZ ; f2

jmp SHORT $LN4@main8

 

$LN1@main8:; 11   :         default: f3(); break;

call ?f3@@YAXXZ ; f3

$LN4@main8:

 ; 12   :     }; 13   : ; 14   :     return 0; 

同样,没有办法通过这样的黑盒测试影响或预测编译器的选择。根据问题的不同,无论是稀疏还是密集的开关情况,编译器显然可以很好地适应它们。

总结

通过仔细检查上述示例,我们公开了您可能不知道的运行时内容。要在Visual Studio中分析C / C++程序,我们可以同时进行静态和动态分析。在本文中,我们将使用编译器生成的程序集列表。另一方面,我们还可以使用调试中的 VS 反汇编窗口在运行时监视二进制执行,其中列表中的标签和过程将转换为内存地址。这样,您可以跟踪寄存器和内存分配,以了解数据字节序、堆栈帧等。在这里,就我们的目的而言,程序集列表似乎足以表明从这里跳到那里。可下载的zip文件包含七个示例,包括VS 2010中的.cpp源代码和.asm列表。早期版本的 VS 可以生成相同的列表。switch

本文不涉及 .NET 或 C# 等热门技术。汇编语言是相对传统的,现在没有太多的轰动。然而,在应用中,不同类型的汇编语言在新设备开发中起着重要作用。在学术上,汇编语言编程被认为是计算机科学中一门要求很高的课程。我在富勒顿学院教授汇编语言编程,CSCI 241,在那里,高级语言与低级执行的概念是教学的重要组成部分。特别是,学生感兴趣的主要主题之一是C / C++或Java如何在机器上运行,由低级数据结构和算法表示。因此,我希望这篇文章也可以作为学生解决问题的基本示例之一。欢迎任何意见和建议。

;