5.4 switch的真相(3)
保证了switch的参数值在case最大值的范围内,就可以以地址0x00401198作为基地址进行寻址了,查表①后跳转到对应case地址处。地址0x00401198就是case地址表(数组)的首地址,图5-3便是代码清单5-12的case地址表信息。
|
| 图5-3 有序线性case地址表 |
图5-3以0x00401198为起始地址,每4个字节数据保存了一个case语句块的首地址。依次排序下来,第一个case语句块所在地址为0x0040112F。表中第0项保存的内容为0x0040112F,即case 1 语句块的首地址。当输入给switch的参数值为1时,编译器减1调整到case地址数组的下标0后,eax*4+401198h就变成了0 * 4 + 0x00401198,查表得到第0项,即得到case 1语句块的首地址。其他case语句块首地址的查询同理,不再赘述。case语句块的首地址可以对照代码清单5-13查询。
代码清单5-13 线性的case语句块—Debug版
- case 1: printf("nIndex == 1"); // 源码对比
- ; 取字符串"nIndex == 1"的首地址0x0004200470作为参数并压栈
- 0040112F push offset string "nIndex == 1" (00420070)
- ; 调用printf函数输出字符串,__cdecl调用方式
- 00401134 call printf (004014b0)
- ; 平衡printf参数的栈空间
- 00401139 add esp,4
- break; // 源码对比
- ; 跳转到switch结束处,以下case语句相似,不做注释说明
- 0040113C jmp $L556+0Dh (00401187)
- case 2: printf("nIndex == 2"); // 源码对比
- 0040113E push offset string "nIndex == 2" (00420064)
- 00401143 call printf (004014b0)
- 00401148 add esp,4
- break; // 源码对比
- 0040114B jmp $L556+0Dh (00401187)
- case 3: printf("nIndex == 3"); // 源码对比
- 0040114D push offset string "nIndex == 3" (00420058)
- 00401152 call printf (004014b0)
- 00401157 add esp,4
- break; // 源码对比
- 0040115A jmp $L556+0Dh (00401187)
- case 5: printf("nIndex == 5"); // 源码对比
- 0040115C push offset string "nIndex == 5" (00420048)
- 00401161 call printf (004014b0)
- 00401166 add esp,4
- break; // 源码对比
- 00401169 jmp $L556+0Dh (00401187)
- case 6: printf("nIndex == 6"); // 源码对比
- 0040116B push offset string "nIndex == 6" (00421024)
- 00401170 call printf (004014b0)
- 00401175 add esp,4
- break; // 源码对比
- 00401178 jmp $L556+0Dh (00401187)
- case 7: printf("nIndex == 7"); // 源码对比
- 0040117A push offset string "nIndex == 7" (0042003c)
- 0040117F call printf (004014b0)
- 00401184 add esp,4
- break; // 源码对比
将图5-3和代码清单5-13对比可知,每个case语句块的首地址都在表中,但有一个地址却不是case语句块的首地址0x00401187。这个地址是每句break跳转的一个地址值,显然这是switch结束的地址。这个地址值出现在图5-3表格的第3项,表格项的下标以0为起始,反推回case应该是(3+1=4)4,而实际中却没有case 4 这个语句块。为了达到线性有序,对于没有case对应的数值,编译器以switch的结束地址或者default语句块的首地址填充对应的表格项。
代码清单5-13中的每一个break语句都对应一个jmp指令,跳转到的地址都是switch的结束地址处,起到了跳出switch的作用。如果没有break语句,则会顺序执行代码,执行到下一句case语句块中,这便是case语句中没有break可以顺序执行的原因。
代码清单5-13中没有使用default语句块。当所有条件都不成立后,才会执行到default语句块,它和switch的末尾实质上是等价的。switch中出现default后,就会填写default语句块的首地址作为switch的结束地址。
如果每两个case值之间的差值小于等于6,并且case语句数大于等于4,编译器中就会形成这种线性结构。在编写代码的过程中无需有序排列case值,编译器会在编译过程中对case线性地址表进行排序,如case的顺序为3、2、1、4、5,在case线性地址表中,会将它们的语句块的首地址进行排序,将case 1语句块的首地址放在case线性地址表的第0项上,case 2语句块首地址放在表中第1项,以此类推,将首地址变为一个有序的表格进行存放。
这种switch的识别有两个关键点,取数值内容进行比较;比较跳转失败后,出现4字节的相对比例因子寻址方式。有了这两个特性,就可以从代码中正确分析出switch结构。switch结构中的case线性地址模拟图如图5-4所示。
|
| 图5-4 case线性地址表模拟图 |