关于 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  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值