- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
11.2 许多例子
在有许多 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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论