目录 Table of Contents
switch 的汇编语言表示
这个就是依据不同情况来采用不同的实现技术, 一个是用 if-then-else 语句来实现; 再一个就是跳转表实现.
首先讲讲看怎么用跳转表 :
long switch_eg(long x, long y, long z)
{
long w - 1;
switch(x){
case 1:
w = y * z;
break;
case 2:
w = y / z;
/*Fall through*/
case 3:
w += z;
break;
case 5:
case 6:
w -= z;
break;
default:
w = 2;
}
return w;
}
有些细节要注意 :
case 5 和 case 6 实际上是对应同一段代码的; case 值并不连续; 还有个所谓的 Fall through, 就是说 case 2 完成之后, 它并没有 break 出去, 而是接着处理 case 3.
跳转表
Switch Form switch 表
switch(x){
case val_0:
Block 0
case val_1:
Block 1
......
case val_n-1:
Block n-1
}
这个相当于 switch-case 的一个 C 的代码, 然后呢对每一个 case 的处理, 是由一个指令段来处理, 也就是一个 Block 来进行处理.
针对不同的 x 的值, 跳到不同的指令段的入口地址. 我们可以专门创建一个 jump-table, 把你各个 case 下面的处理代码的入口地址放进去, 然后就形成了一个表.
然后我们可以把这个 x 的值作为这个表的下标, 或者说是索引. 然后我们访问这个表的下标, 把这个下标所对应的值取出来, 这个取出来的就是我们跳转的目标地址.
以上就是跳转表的工作原理.
Switch 语句示例 (X86-32)
long switch_eg(long x, long y, long z)
{
long w - 1;
switch(x){
case 1:
w = y * z;
break;
case 2:
w = y / z;
/*Fall through*/
case 3:
w += z;
break;
case 5:
case 6:
w -= z;
break;
default:
w = 2;
}
return w;
}
还是之前的代码, 我们将其编译为如下汇编代码 :
Setup :
switch_eg:
pushl %ebp ; Setup
movl %esp, %ebp ; Setup
pushl %ebx ; Setup
movl $1, %ebx ; w = 1
movl 8(%ebp), %ebx ; ebx = x
movl 12(%ebp), %eax ; eax = y
movl 16(%ebp), %ecx ; ecx = z
cmpl $6, %edx ; x : 6
ja .L61 ; if > goto default
jmp *.L62(, %edx, 4) ; goto JTab[x]
我们注意到 ja .L61
, 这个 a 后缀表示 above 大于. 但问题是它是什么数据类型的大于呢 ?
这个 a, above 表示的是无符号整数, 但是我们回头看一看, 这里面的数据是 long, lone 类型是 signed, 那么为什么用 ja
, 而不是用这个带符号整数的那个判断呢 ?
我们可以把它当作不带符号数来处理, 因为 default 的话就是说这个数大于 6 或者小于等于 0 的话, 它也正好满足, 所以用 ja
是正好. 就是如果你用 ja
, 把它作为一个无符号数来处理的话, 等同于你从有符号数角度来看, 大于 6 就会跳到 default 去, 小于 0 也会跳到 default 去.
这里就是说, 这个 ja 代码后面就是 default 情况
如果不满足这个的话, 就 jmp
. 注意看这个 jmp *.L62(, %edx, 4)
, .L62 就是我们刚才说的 jump-table 的起始地址, edx 就是 x, 因为每个地址都是 4 byte, 所以它乘上一个 4. 这个星 * 的意思是告诉汇编器, 我里面取出来的这个数是一个地址, 你就直接把它作为地址跳过去就行了.
这里是说, 如果不满足 default 情况, 就跳到 default 的后面, 也就是各种 case. 只针对这段代码, 其它代码具体情况具体分析.
表结构
表结构就是说, 每一个表项, 就是每一个入口的地址, 也就是每一个跳转的地址占 4 个字节, 基地址是 .L62
无条件跳转指令就是 jmp *.L62(, %edx, 4)
, 这么跳转相当于把 .L62 作为一个起始地址, .L62 + %edx 4 就是我把表项里面所对应的那个地址取出来. \ 表示这是一个间接跳转, 就是说目标地址存在于内存中, 我从内存中取出来的这个数就是我的目标地址.
文字填空题
jbe 是小于的时候跳转
L3 为 case 1
L4 为 case 2
L9 为 case 3
L6 为 case 5
L6 为 case 6
L8 为 default x <= 0 或者 > 6 或者 x = 4
L12 为 break
x = 0 时, 根据这个间接寻址公式, 就是跳到了 L7 这个基址这里, 也就是跳转表第一个值这里. 而 x = 0 正好是默认情况.
switch_eg:
pushl %ebp
movl %esp, %ebp
pushl %ebx
movl 8(%ebp), %ebx ; ebx = x
mov 12(%ebp), %eax ; eax = y
mov 16(%ebp), %ecx ; ecx = z
cmpl $6, %ebx
jbe .L11 ; x < 6 就跳转到 L11
.L11:
jmp *.L7(, %ebx, 4)
.section .rodata
.L7:
.long .L8 ; x = 0
.long .L3 ; x = 1
.long .L4 ; x = 2
.long .L9 ; x = 3
.long .L8 ; x = 4
.long .L6 ; x = 5
.long .L6 ; x = 6
.text
.L8:
movl $2, %eax
popl %ebx
popl %ebp
ret
.L6:
movl $1, %eax
subl %ecx, %eax
popl %ebx
popl %ebp
ret
.L9:
movl $1, %eax
addl %ecx, %eax
.L12:
popl %ebx
popl %ebp
ret
.L4:
movl %eax, %edx
sarl $31, %edx
idivl %ecx
addl %ecx, %eax
jmp .L12
.L3:
imull %ecx, %eax
popl %ebx
popl %ebp
ret
表项内容
.section .rodata
.align 4
.L62:
.long .L61 ; x = 0
.long .L56 ; x = 1
.long .L57 ; x = 2
.long .L58 ; x = 3
.long .L61 ; x = 4
.long .L60 ; x = 5
.long .L60 ; x = 6
.L62 表示下面是一段数据
.section, .data 以后再讲, 就是数据段
.L61 就是相对应每一段 case 代码的入口, 整个这个表的起始地址是 .L62.
0 没有, 4 没有, 那就把 0 和 4 都写上 default 呗.
X86-64 下的 switch 语句
基本上与 32 位版本是一样的, 代码地址长度为 64 位
我们也类似地定义了一个 jump-table
.section .rodata
.align 8
.L62:
.quad .L55 ; x = 0
.quad .L50 ; x = 1
.quad .L51 ; x = 2
.quad .L52 ; x = 3
.quad .L55 ; x = 4
.quad .L54 ; x = 5
.quad .L54 ; x = 6
表现形式都一样, 无非就是说我每一个表项的宽度是 .q, 8 个字节.
Switch 语句示例 (case 值很稀疏)
/* Return x / 111 if x is multiple && <= 999. -1 otherwise*/
int div111(int s)
{
switch(x){
case 0: return 0;
case 111: return 1;
case 222: return 2;
case 333: return 3;
case 444: return 4;
case 555: return 5;
case 666: return 6;
case 777: return 7;
case 888: return 8;
case 999: return 9;
default: return -1;
}
}
向这种 case 值比较稀疏的情况, 就不适合使用跳转表了. 因为取值越稀疏, 中间隔得越大, 跳转表就占了太大的空间, 而实际上有效的项没多少, 写了那么多项实际上全是 default.
这样我们只能把它转换成一系列的 if-then-else 语句了. 我们一般以二叉树的结构来组织, 提升性能.