选题五题目
对于以下c程序:
#include <stdio.h>
int main()
{
int i=0;
int j=0;
switch(i)
{
case 1:
j+=1;
break;
case 2:
j+=2;
break;
case 3:
j+=3;
break;
case 4:
j+=4;
break;
case 5:
j+=5;
case 6:
j+=70;
break;
default:
j+=5;
break;
}
return 0;
}
(1)将其编译成汇编代码,找到跳转表,并分析汇编代码是如何通过跳转表来完成switch功能的;
(2)将分支条件调整为case 6,case 2,case 5,case 3,case 4,case 1(即交换一下分支条件顺序),观察跳转表的变化情况;
(3)将分支条件调整为case 5, case 3, case 2, case1,或是调整为case 138, case 106, case 2, case 9, case 68后,汇编后的代码中不包括跳转表,而是采用cmpl, je, jmp等指令来实现switch功能,请再找出几组能生成跳转表与不生成跳转表的分支条件组合,并分析编译器在哪些情况下更有可能采用跳转表的方式来实现switch功能。
(1)通过跳转表来完成switch功能
switch语句根据一个整数索引值进行多重分支,处理具有多种可能结果的测试,根据索引值来执行一个跳转表内的数组引用,确定跳转指令的目标,使用跳转表的优点是执行switch语句的时间与开关情况的数量无关。当开关情况数量比较多且值的范围跨度比较小的时候就会使用跳转表,而当开关数量少或者值的范围跨度较大时就采用cmpl, je, jmp等指令来实现switch功能,值的范围跨度超过256时就会演变成树形结构。
switch语句汇编时通常根据开关数量和稀疏程度汇编分成以下5种情况:
1.当分支比较少时(我安装的Ubuntu里case情况数少于5时)或分支很多但分支常量杂乱无规律时 ,汇编成cmpl, je, jmp等指令结构。
2.当分支比较多且分支常量连续时,生成一张大表。
3.当分支比较多,分支常量连续但分支常量中间有少量断档时,生成一张大表,断档的分支在表中用default分支地址填充。
4.当分支比较多,分支常量连续但分支常量中间有大量断档时,生成一张大表 + 一张小表,断档的分支不在占用大表的空间,仅在大表中保留一份default分支的地址。
5.断档差距超过256(小表中一个字节所能表示的范围),就会使用树形结构。
跳转表
汇编执行过程:
move $0,-8(%ebp) 将i存入%ebp-8的内存中
move $0,-4(%ebp) 将j存入%ebp-4的内存中
cmpl $6,-8(%ebp) 因为case的情况最大值为6,这里和最大情况值比较,设置条件码
ja .L2 根据上一条指令设置的条件码,判断是否跳转,i>6,跳转到 .L2(对应c程序的default分支),执行.L2后的语句;i<=6则不跳转,顺序执行ja .L2后的指令movl -8(%ebp),%eax
movl -8(%ebp),%eax
sall $2,%eax
addl .L9,%eax
跳转表实际是一个二级指针,数组内的每一个元素都是一个地址,分别对应case的各种情况,数组中每个元素可以有相同的地址。.L9是该跳转表的首地址,.L2、.L3、.L4、.L5、.L6、.L7、.L8分别对应于case 0,1,2,3,4,5,6的情况,也是跳转表中0-6号元素的值的情况。由于在c程序中只包含了1-6,所以默认case 0属于default分支的情况。其他情况类似,如果汇编是采用跳转表,case中没有的值在跳转表中对应位置存放的地址就是default代码段的地址,这里.L9-.L9+3该段内存的地址为.L2的地址(也为default代码段的地址)。
movl (%eax),%eax
jmp %eax 由于跳转表内存放的是32位地址,%eax=i,所以需要将i4,即上述算数右移2位sall $2,%eax。.L9+i4是跳转表的首地址加上偏移量得到的跳转表i号元素的地址,保存在%eax。movl (%eax),%eax取出跳转的地址(case i的分支代码的地址,也为跳转表i号元素的内容),根据间接跳转jmp%eax跳转到某一个分支代码的地址(该题即为.L2、.L3、.L4、.L5、.L6、.L7、.L8中的一个),然后执行分支代码。分支代码执行完毕跳出switch。
通过反汇编可以看到跳转表的首地址是0x80484e0,x/7xw 0x80484e0 查看跳转表的内容,每个地址即为case值为0-6所执行代码段的地址。
在本题中i=0,执行完c程序中j=0后执行switch(i),通过查看p/x $eip 下一条指令的地址,可以发现地址从0x80483c8跳转到了0x8048401去执行default:j+=5的情况,如下图所示:
(2)调整case分支顺序后观察跳转表变化
将分支条件调整为case 6,case 2,case 5,case 3,case 4,case 1(交换一下分支条件顺序),观察跳转表的变化情况:
可以发现:当c程序case情况相同,而顺序不一样时,汇编结果的跳转表里的地址仍旧按照case的值0-6从小到大保存,只是后续的汇编代码按照case值的顺序排列,default分支.L2仍旧在最后,第一段汇编代码中case 6 (.L8)在倒数第二,第二段由于case 6 在C程序switch分支最前面,相应.L8也在跳转表的汇编代码后的第一个。
(3)编译器何时采用跳转表实现switch?
将分支条件调整为case 5,case 3,case 2,case1或是调整为case 138,case 106,case 2,case 9,case 68后,汇编后的代码中不包括跳转表,而是采用cmpl,je,jmp等指令来实现switch功能。
经过在Ubuntu中运行以下情况得出相应的结论:
1.当分支条件为case 1,case 2,case 3,case 5 时汇编不用跳转表;
2.当分支条件为case 1,case 2,case 3,case 5,case 6时用跳转表;
3.当分支条件为case 2,case 9,case68,case 106,case 138时不用跳转表;
4.当分支条件为case 1001,case 1002,case 1003,case 1007,case1009,case 1012,case 1014,case 1017时采用跳转表;
5.当分支情况为case 0,case 100,case 500,case 1000,case 1500,case 2000,case 2500,case 3000时不用跳转表。
综上所述:当case的分支条件数少于5个或者case的值相差较大时汇编不采用跳转表,而是通过采用cmpl,je,jmp等指令来实现switch功能,只有当case的分支条件数大于等于5个且case值相差较小时汇编才采用跳转表形式。