5.4 switch的真相(4)
Release版与Debug版的反汇编代码基本一致,下面将在Release版中对这种结构进行实际分析,如代码清单5-14所示。
代码清单5-14 case语句的有序线性结构—Release版
- ; 取出switch语句的参数值并放入ecx中
- 00401018 mov ecx,dword ptr [esp+8]
- 0040101C add esp,8 ; 平衡scanf函数的参数
-
- ; 将ecx减1后放入eax中,因为最小的case 1存放在case地址表中下标为0处,需要调整对齐到0下标,便于直接查表
- 0040101F lea eax,[ecx-1]
- ; 与6进行比较,有了这两步操作可以初步假设这里的代码是一个switch结构
- ; 无符号比较,大于6时跳转到地址0x00401086处
- 00401022 cmp eax,6
- 00401025 ja 00401086
- ; 下面的指令体现了switch的第二个特性:查表(case地址表)
- ; 可以确认这是一个switch结构
- ; 上一步的跳转地址00401086就是switch结尾或者是default语句块的首地址
- ; 下面指令中的地址0x00401088便是case线性地址表的首地址
- ; 可参考图5-5
- 00401027 jmp dword ptr [eax*4+401088h]
- ; 地址0x0040706C为字符串"nIndex == 1"的首地址
- ; 此处为第一个case语句块的地址
- 0040102E push 40706Ch
- ; 此处调用printf函数
- 00401033 call 004012F0
- ; 平衡printf函数破坏的栈空间
- 00401038 add esp,4
- ; 还原esp
- 0040103B pop ecx
- ; 返回,在Release版中,编译器发现switch后什么也没做就直接返回,所以
- ; 将每句break优化为了return
- ; 到此处,第一个case语句块结束,顺序向下为第二个case语句块
- 0040103C ret
- ; 第二个case语句块
- ; 以下代码相似,除地址外,不再进行注释,请读者自行分析
- ; 地址0x00407060为字符串"nIndex == 2"的首地址
- 0040103D push 407060h
- 00401042 call 004012F0
- 00401047 add esp,4
- 0040104A pop ecx
- 0040104B ret
- ; 第三个case语句块
- ; 地址0x00407054为字符串"nIndex == 3"的首地址
- 0040104C push 407054h
- 00401051 call 004012F0
- 00401056 add esp,4
- 00401059 pop ecx
- 0040105A ret
- ; 第四个case语句块
-
- ; 地址0x00407048为字符串"nIndex == 5"的首地址
- 0040105B push 407048h
- 00401060 call 004012F0
- 00401065 add esp,4
- 00401068 pop ecx
- 00401069 ret
- ; 第五个case语句块
- ; 地址0x0040703C为字符串"nIndex == 6"的首地址
- 0040106A push 40703Ch
- 0040106F call 004012F0
- 00401074 add esp,4
- 00401077 pop ecx
- 00401078 ret
- ; 第六个case语句块
- ; 地址0x00407030为字符串"nIndex == 7"的首地址
- 00401079 push 407030h
- 0040107E call 004012F0
- 00401083 add esp,4
- 00401086 pop ecx
- 00401087 ret
所有的case语句块都已找到,接下来将每个case的标号值进行还原。如何得到这个标号值呢?很简单,只要找到case线性地址表即可。此示例的case线性地址表的首地址为0x00401088,如图5-5所示。
|
| 图5-5 switch的有序线性case地址表 |
case线性地址表是一个有序表,在switch语句块中有减1操作,地址表是以0为下标开始,那么表中的第0项对应的case标号值应为(0+1=1)1,地址0x0040102E处为“case 1”。后续case语句块按此方案,请读者进行依次还原。
总结:
- mov reg, mem ; 取变量
- ; 对变量进行运算,对齐case地址表的0下标,非必要
- ; 上例中的eax也可用其他寄存器替换,这里也可以是其他类型的运算
- lea eax, [reg+xxxx]
- ; 影响标志位的指令,进行范围检查
- jxx DEFAULT_ADDR
- jmp dword ptr [eax*4+xxxx]; 地址xxxx为case地址表的首地址
当遇到这样的代码块时,可获取某一变量的信息并对其进行范围检查,如果超过case的最大值,则跳转条件成立 ,跳转目标指明了switch语句块的末尾或者是default块的首地址。条件跳转后紧跟jmp指令,并且是相对比例因子寻址方式,且基址为地址表的首地址,说明此处是线性关系的switch分支结构。对变量做运算,使对齐到case地址表0下标的代码不一定存在(当case的最小值为0时)。根据每条case地址在表中的下标位置,即可反推出线性关系的switch分支结构原型。