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