目录
3.6 分支控制(Branch Control)
3.6.1 条件码、CMP、TEST
Condition Code 翻译成“条件码”,不是怪怪的吗?描述的应该是运算后的状态,下文用“状态码”。
除了整数寄存器(integer registers),CPU 还维护一组单比特的状态码寄存器。
常用的状态码 | 全称 | 说明 |
---|---|---|
CF(Carry Flag) | 进位标志 | 最近的操作使最高位产生了进位。用来检查无符号操作的溢出。 |
ZF(Zero Flag) | 零标志 | 最近的操作得出的结果为0。 |
SF(Symbol Flag) | 符号标志 | 最近的操作得到的结果为负数。 |
OF(Overflow Flag) | 溢出标志 | 最近的操作导致一个补码溢出——正溢出或负溢出。 |
PF(Parity Flag) | 奇偶标志 | 对于整数: 最近的操作产生的值的最低 8 位比特中有偶数个 1; 对于浮点数: 当两个操作数中任一个是 N a N NaN NaN 时。 |
【作用】
- 用来描述最近的算术或逻辑操作后的状态。
- 用来描述比较和测试指令操作后的状态。
【注意】在 3.5 小节中的操作,只有 leaq 指令不改变任何状态码,因为它是用来进行地址计算的。其他操作具体会影响哪些标志位,这里暂时不深入扩展。
1、CMP 和 TEST 指令
CMP
和 TEST
只设置状态码,而不改变任何其他寄存器。
除了只设置状态码而不更新目的寄存器之外,CMP 与 SUB 指令的行为是一样的,TEST 与 AND 指令的行为是一样的。
指令 | 实际上的操作 | 描述 |
---|---|---|
C M P S 1 , S 2 CMP{\space\space\space}S1,S2 CMP S1,S2 | S 2 − S 1 S2-S1 S2−S1 | 比较 |
cmpb | 根据两个操作数之差来设置状态码 | 比较字节 |
cmpw | 比较字 | |
cmpl | 比较双字 | |
cmpq | 比较四字 | |
T E S T S 1 , S 2 TEST{\space\space\space}S1,S2 TEST S1,S2 | S 1 & S 2 S1\&S2 S1&S2 | 测试 |
testb | 测试字节 | |
testw | 测试字 | |
testl | 测试双字 | |
testq | 测试四字 |
2、浮点比较操作
AVX2 提供了两条用于比较浮点数值的指令,类似于上一节的 CMP 指令,根据比较操作数 S 2 S_2 S2 和 S 1 S_1 S1 的结果设置状态码(条件码)。
S1:可以是 XMM 寄存器,也可以是内存;
S2:只能是 XMM 寄存器。
指令 | 基于 | 描述 |
---|---|---|
vucomiss S1, S2 | S 2 − S 1 S_2-S_1 S2−S1 | 比较单精度值 |
vucomisd S1, S2 | S 2 − S 1 S_2-S_1 S2−S1 | 比较双精度值 |
浮点比较指令会设置三个状态码:零标志位 ZF、进位标志位 CF 和奇偶标志位 PF:
顺序 S 2 : S 1 S_2:S_1 S2:S1 | CF | ZF | PF |
---|---|---|---|
无序的 | 1 | 1 | 1 |
S 2 < S 1 S_2<S_1 S2<S1 | 1 | 0 | 0 |
S 2 = S 1 S_2=S_1 S2=S1 | 0 | 1 | 0 |
S 2 > S 1 S_2>S_1 S2>S1 | 0 | 0 | 0 |
当任一操作数为 N a N NaN NaN 时,就是出现“无序”的情况。
3.6.2 访问状态码
状态码通常不直接读取,有三种使用方法:
(1)根据状态码的某种组合,将一个字节设置为 0 或者 1;
(2)将状态码作为条件跳转到程序的某个其他的部分;
(3)可以有条件地设置为 0 或者1。
SET 指令
根据状态码的某种组合,将一个字节设置为 0 或者 1。
SET 指令的目的操作数 D 可以是:
一个寄存器的低位的 1 个字节;
or 一个字节的内存位置。
指令 | 同义名 | 效果 | 设置条件 |
---|---|---|---|
sete D | setz | D ← ZF | 相等/零 |
setne D | setnz | D ← ~ZF | 不等/非零 |
sets D | D ← SF | 负数 | |
setns D | D ← ~SF | 非负数 | |
setg D | setnle | D ← ~(SF^OF) & ~ZF | 有符号:大于 |
setge D | setnl | D ← ~(SF^OF) | 有符号:大于等于 |
setl D | setnge | D ← SF^OF | 有符号:小于 |
setle D | setng | D ← (SF^OF) | ZF | 有符号:小于等于 |
seta D | setnbe | D ← ~CF & ~ZF | 无符号:超过 |
setae D | setnb | D ← ~CF | 无符号:超过或相等 |
setb D | setnae | D ← CF | 无符号:低于 |
setbe D | setna | D ← CF | ZF | 无符号:低于或相等 |
# int comp(data_t a, data_t b)
# a in %rdi, b in %rsi
comp:
cmpq %rsi, %rdi # 比较a和b
setl %al # 设置 %eax 低位的1个字节为0或1
movzbl %al, %eax # 清除 %eax 的其余部分,以及 %rax 的其余部分
ret
# 注意 cmpq 指令的比较顺序,虽然参数列出的顺序先是 %rsi(b)再是 %rdi(a),实际上比较的是a和b。
【大多数情况下,机器代码对于有符号和无符号两种情况都使用一样的指令,这是因为许多算术运算对无符号和补码算术都有一样的位级行为。有些情况需要用不同的指令来处理有符号和无符号操作,例如,使用不同版本的右移、除法和乘法指令,以及不同的条件码组合。】
【练习】
int comp(data_t a, data_t b){ // data_t 为整数类型
return a COMP b; // COMP 是#define声明的
}
假设 a 在 %rdi 中某个部分,b 在 %rsi 中某个部分,根据下列汇编代码,推断出 data_t 类型:
汇编代码 | data_t 类型 |
---|---|
cmpl %esi,%edi setl %al | l:32位;setl:有符号小于;→ int |
cmpw %si,%di setge %al | w:16位;setge:有符号大于等于;→ short |
cmpb %sil,%dil setbe %al | b:8位;setbe:无符号小于等于;→unsigned char |
cmpq %rsi,%rdi setne %a | q:64位;setne:非零;→long、unsigned long、指针 |
int test(data_t a){
return a TEST 0; // TEST 是#define声明的
}
汇编代码 | data_t 类型 |
---|---|
testq %rdi,%rdi setge %al | q:64位;setge:有符号大于等于;→long |
testw %di,%di sete %al | w:16位;sete:等于;→short、unsigned short |
testb %dil,%dil seta %a | b:8位;seta:无符号大于;→unsigned char |
testl %edi,%edi setle %al | l:32位;setne:不等于;→int |
3.6.3 跳转指令(jump)
【直接跳转】跳转目标是作为指令的一部分进行编码的;
【间接跳转】跳转目标是根据给出的操作数,从寄存器或内存中读来的。
除了 jmp
可以有间接跳转,其他跳转都只有直接跳转。
指令 | 同义名 | 跳转条件 | 描述 |
---|---|---|---|
jp Label | 比较得到一个无序的结果 | jump on parity | |
jmp Label | 1(无条件跳转) | 直接跳转 | |
jmp *Operand | 1(无条件跳转) | 唯一的:间接跳转 | |
je Label | jz | ZF | 相等/零 |
jne Label | jnz | ~ZF | 不相等/非零 |
js Label | SF | 负数 | |
jns Label | ~SF | 非负数 | |
jg Label | jnle | ~(SF^OF) & ~ZF | 有符号大于 |
jge Label | jnl | ~(SF^OF) | 有符号大于等于 |
jl Label | jnge | SF^OF | 有符号小于 |
jle Label | jng | (SF^OF) | ZF | 有符号小于等于 |
ja Label | jnbe | ~CF & ~ZF | 无符号超过 |
jae Label | jnb | ~CF | 无符号超过或相等 |
jb Label | jnae | CF | 无符号低于 |
jbe Label | jna | CF | ZF | 无符号低于或相等 |
在汇编代码中,跳转的目的地通常用一个标号指明,🌰:
movq $0,%rax # 设置 %rax 为0
jmp .L1 # 跳转到 .L1
movq (%rax),%rdx # NULL指针解引用(被跳过了)
.L1:
popq %rdx # jmp的跳转目标
# 指令jmp.L1会导致程序跳过movq指令,而从popq指令开始继续执行。
产生目标代码文件时,汇编器会确定所有带标号的地址,并将**要跳转到的目标(目的指令的地址)**编码为跳转指令的一部分。
另一个🌰:
typedef enum {NEG, ZERO, POS, OTHER} range_t;
range_t find_range(float x)
{
int result;
if (x < 0)
result = NEG;
else if (x == 0)
result = ZERO;
else if (x > 0)
result = POS;
else // x的值为NaN时
result = OTHER;
return result;
}
# range_t find_range(float x)
# x in %xmm0
find_range:
vxorps %xmm1, %xmm1, %xmm1 # Set %xmm1 = 0
vucomiss %xmm0, %xmm1 # Compare 0:x
ja .L5 # If >, goto neg
vucomiss %xmm1, %xmm0 # Compare x:0
jp .L8 # If NaN, goto posornan
movl $1, %eax # result = ZERO
je .L3 # If =, goto done
.L8: # posornan:
vucomiss .LC0(%rip), %xmm0 # Compare x:0
setbe %al # Set result = NaN ? 1 : 0
movzbl %al, %eax # Zero-extend
addl $2, %eax # result += 2 (POS for > 0, OTHER for NaN)
ret # Return
.L5: # neg:
movl $0, %eax # result = NEG
.L3: # done:
rep; # ret Return
3.6.4 跳转指令的编码
常见两种:
1、给出“绝对”地址,用 4 个字节直接指定目标。
2、PC 相对寻址(PC-relative),将目标指令的地址与紧跟在跳转指令后面的那条指令的地址之间的差作为编码。
🌰PC 相对寻址
movq %rdi,%rax
jmp .L2
.L3:
sarq %rax
.L2:
testq %rax,%rax
jg .L3
rep; ret
链接后的程序反汇编
4004d0: 48 89 f8 mov %rdi,%rax
4004d3: eb 03 jmp 4004d8<loop+0x8>
4004d5: 48 d1 f8 sar %rax
4004d8: 48 85 c0 test %rax,%rax
4004db: 7f f8 jg 4004d5<loop+0x5>
4004dd: f3 c3 repz retq
👆根据PC相对寻址方法:
跳转指令 jmp 4004d8<loop+0x8>
, 4004d3: eb 03
中的地址偏移量 03
,就是由目标指令 test %rax,%rax
的地址与紧跟 jmp 4004d8<loop+0x8>
后的 sar %rax
指令的地址作差得到的,即:4004d8 - 4004d5 = 03
。
跳转指令 jg 4004d5<loop+0x5>
,4004db: 7f f8
中的地址偏移量 f8
,就是由目标指令 sar %rax
的地址与紧跟 jg 4004d5<loop+0x5>
后的 repz retq
指令的地址作差得到的,即:4004d5 - 4004dd = 5 - d = f8
。
🌰2
在下面的代码中,跳转目标的编码是PC相对的,且是一个4字节补码数。字节按照从最低位到最高位的顺序列出,反映出x86-64的小端法字节顺序。跳转目标的地址是什么?
4005e8: e9 73 ff ff ff jmpq XXXXXXX
4005ed: 90 nop
用相反的顺序来读这些字节,我们看到目标偏移量是 0xffffff73
,也即十进制数-141
,141
也即0x8d
,0x4005ed
(nop指令的地址)加上这个值(也即减去0x8d
)得到地址 0x400560
。
【补充】
rep
和repz
有什么用?
rep
和repz
是同义名,retq
和ret
是同义名。
使用 ~后面跟ret
的组合可以用来避免ret
指令成为条件跳转指令的目标指令。
如果没有rep
指令,当分支不跳转时,🌰中的jg
指令会继续到ret
指令。根据AMD的说法,当ret
指令通过跳转指令到达时,处理器不能正确预测ret
指令的目的。因此,rep
指令的存在,就是为了作为一种空操作——作为跳转目的插入它,除了能使代码在AMD上运行得更快之外,还不会改变代码的其他行为。
3.6.5 实现条件分支的两种方式
(1)条件控制;(2)条件传送。
方式 | 条件控制转移 | 条件数据传送 |
---|---|---|
解释 | 当条件满足时,程序沿着一条执行路径执行, 而当条件不满足时,就走另一条路径。 | 把一个条件操作的两种结果全部计算出来,然 后再根据条件是否满足从中选取一个。 |
优点 | 简单、通用 | 符合现代处理器的性能特性 |
缺点 | 在现代处理器上,低效 | 只有在一些受限制的情况,才可用 |
1、为什么基于条件数据传送的代码会比基于条件控制转移的代码性能要好?
处理器通过使用“流水线”来获得高性能。
处理器要求能够事先确定将要执行的指令序列,这才能保证“流水线”中充满了待执行的指令——实现高性能。
(1)当遇到条件控制转移时
当遇到条件控制转移时,只有当分支条件求值完成后,才能决定往哪个分支走。
在这里,处理器采取一种“分支预测逻辑”来猜测应该往哪个分支走。
——猜对了,完美;
——猜错了,接受性能下降的惩罚。
预测错误的性能惩罚计算:
假设预测错误的概率是 p,预测成功执行代码的时间是 T O K T_{OK} TOK,预测错误的惩罚时间是 T M P T_{MP} TMP 。
那么,执行该代码的平均时间为 T a v g ( p ) = ( 1 − p ) T O K + p ( T O K + T M P ) = T O K + p T M P T_{avg}(p)=(1-p)T_{OK}+p(T_{OK}+T_{MP})=T_{OK}+pT_{MP} Tavg(p)=(1−p)TOK+p(TOK+TMP)=TOK+pTMP。
(2)当遇到条件数据传送时
当遇到条件数据传送时,处理无需去预测,而是分别计算出条件操作的两种结果,然后通过状态码判断选取哪一个。
——计算量小,完美;
——计算量大,白费一半的力气。
【总结】编译器必须在浪费的计算和由于分支预测错误所造成的性能惩罚之间进行博弈,然而,事实上编译器并不具有足够的信息来做出可靠的决定。——因此,实际上,即使许多分支预测错误的开销会超过庞大的计算,GCC 还是会使用条件控制转移。
2、两种方式🌰
(1)基于条件控制转移
原始C语言代码模板 | 与之等价的 goto 控制流,方便描述汇编代码的流 |
---|---|
if ( test-expr ) then-statement else else-statement | t = test-expr ; if (!it) goto false; then-statement goto done; false: else-statement done: |
/* 原始C语言代码 */
long lt_cnt = 0;
long ge_cnt = 0;
long absdiff_se(long x, long y)
{
long resu1t;
if(x<y){
lt_ cnt++;
result = y-x;
}
else{
ge_cnt++;
result = x-y;
}
return result;
}
/* 与之等价的goto版本 */
long gotodiff_se(long x, long y)
{
long resu1t;
if(x>y)
goto x_ge_y;
lt_cnt++;
result = y-x;
return result;
x_ge_y:
ge_cnt++;
result = x-y;
return result;
}
# 产生的汇编代码
# long absdiff_se(long x, long y)
# x in %rdi, y in %rsi
absdiff_se:
cmpq %rsi,%rdi # compare x:y
jge .L2 # if x>=y : goto x_ge_y
addq $1,lt_cnt(%rip) # lt_cnt++
movq %rsi,%rax
subq %rdi,%rax # result = y-x
ret # Return
.L2: # x_ge_y:
addq $1,ge_cnt(%rip) # ge_cnt++
movq %rdi,%rax
subq %rsi,%rax # result = x-y
ret # Return
(2)基于条件数据传送
【注意】不是所有的条件表达式都可以用条件传送来编译。
【More Important】无论测试结果如何,都会对条件的两种情况进行求值。如果其中一个表达式出现 BUG,就是导致非法行为。
原始C语言代码模板 | 与之等价的 goto 控制流,方便描述汇编代码的流 |
---|---|
v = test-expr ? then-expr : else-expr | if (!test-expr ) goto false; v = then-expr goto done; false: v = else-expr done: |
/* 原始C语言代码 */
long absdiff(long x, long y)
{
long result;
if (x<y)
result = y-x;
else
result = x-y;
return result;
}
/* 与之等价的goto版本 */
long cmovdiff(long x, long y)
{
long rval = y-x;
long eval = x-y;
long ntest = x>=y;
if(ntest)
rval = eval;
return rval;
}
# 产生的汇编代码
# long absdiff(long x, long y)
# x in %rdi, y in %rsi
absdiff:
movq %rsi,%rax
subq %rdi,%rax # rval = y-x
movq %rdi,%rdx
subq %rsi,%rdx # eval = x-y
cmpq %rsi,%rdi # Compare x:y
cmovge %rdx,%rax # if x>=y rval = eval
ret
3、条件传送指令
源寄存器或内存地址 S,目的寄存器 R,源值 S 可以从内存或者源寄存器中读取,但是只有在指定的条件满足时,才会被复制到目的寄存器 R 中。
指令 | 同义名 | 传送条件 | 描述 |
---|---|---|---|
cmove S,R | cmovz | ZF | 相等/零 |
cmovne S,R | cmovnz | ~ZF | 不相等/非零 |
cmovs S,R | SF | 负数 | |
cmovns S,R | ~SF | 非负数 | |
cmovg S,R | cmovnle | ~(SF^OF) & ~ZF | 有符号大于 |
cmovge S,R | cmovnl | ~(SF^OF) | 有符号大于等于 |
cmovl S,R | cmovnge | SF^OF | 有符号小于 |
cmovle S,R | cmovng | (SF^OF) | ZF | 有符号小于等于 |
cmova S,R | cmovnbe | ~CF & ~ZF | 无符号超过 |
cmovae S,R | cmovnb | ~CF | 无符号超过或相等 |
cmovb S,R | cmovnae | CF | 无符号低于 |
cmovbe S,R | cmovna | CF | ZF | 无符号低于或相等 |
与各种 SET 和 跳转指令相同,这些指令的结果取决于状态码的值。
同样的,SET、跳转指令、cmov 指令都可以按操作数长度进一步细分。
注意,汇编器可以从目标寄存器的名字推断出指令的操作数长度,因此,对于所有的操作数长度,都可以使用同一个的指令的名字。
3.6.6 循环
1、do-while 循环
原始C语言代码模板 | 与之等价的 goto 控制流,方便描述汇编代码的流 |
---|---|
do body-statement while ( test-expr ); | loop: body-statement t = test-expr; if ( t ) goto loop; |
/* 原始C语言代码 */
long fact_do(long n)
{
long result =1;
do{
result *= n;
n = n-1;
}while(n>1);
return result;
}
/* 与之等价的goto版本 */
long fact_do_goto(long n)
{
long result =1;
loop:
result *= n;
n = n-1;
if(n>1)
goto loop;
return result;
}
# 产生的汇编代码
# long fact_do(long n)
# n in %rdi
fact_do:
movl $1,%eax # Set result =1
.L2: # loop:
imulq %rdi,%rax # Compute result *= n
subq $1,%rdi # n--
cmpq $1,%rdi # Compare n:1
jg .L2 # if n>1, goto loop
rep; ret # Return
2、while 循环
原始C语言代码模板 | 与之等价的 goto 控制流 (jump to middle,跳转到中间) | 与之等价的 goto 控制流 (guarded-do,使用较高 优化等级编译时,-O1) |
---|---|---|
while ( test-expr ) body-statement | goto test; loop: body-statement test: t = test-expr if (t) goto loop; | t = test-expr if (!t) goto done; loop: body-statement t = test-expr if (t) goto loop; done; |
/* 原始C语言代码 */
long fact_while(long n){
long result = 1;
while(n>1){
result *= n;
n = n-1;
}
return result;}
/* 与之等价的goto版本 ——jump to middle,跳转到中间 */
long fact_while_jm_goto(long n)
{
long result = 1;
goto test;
loop:
result *= n;
n = n-1;
test:
if(n>1)
goto loop;
return result;
}
# 产生的汇编代码 ——jump to middle,跳转到中间
# long fact_while(long n)
# n in %rdi
fact_while:
movl $1,%eax # Set result =1
jmp .L5 # Goto test
.L6: # loop:
imulq %rdi,%rax # Compute result *= n
subq $1,%rdi # n--
.L5: # test:
cmpq $1,%rdi # Compare n:1
jg .L6 # if n>1, goto loop
rep; ret
/* 与之等价的goto版本 ——guarded-do */
long fact_while_gd_goto(long n)
{
long result = 1;
if(n<=1)
goto done;
loop:
result *= n;
n = n-1;
if(n!=1)
goto loop;
done:
return result;
}
# 产生的汇编代码 ——guarded-do
# long fact_while(long n)
# n in %rdi
fact_while:
cmpq $1,%rdi # Compare n:1
jle .L7 # if n<=1, goto done
movl $1,%eax # Set result = 1
.L6: # loop:
imulq %rdi, %rax # Compute result *= n
subq $1, %rdi # Decrement n
cmpq $1, %rdi # Compare n:1
jne .L6 # If n!=1, goto loop
rep; ret # Return
.L7: # done:
movl $1, %eax # Compute result = 1
ret # Return
3、for 循环
原始C语言代码模板 | 转换为 while |
---|---|
for (init-expr; test-expr; update-expr) body-statement | init-expr ; while (test-expr) { body-statement update-expr ; } |
再转换为调到中间(jm) | 或转换为 guarded-do |
init-expr ; goto test ; loop: body-statement update-expr ; test: t = test-expr ; if (t) goto loop; | init-expr; t = test-expr; if (!t) goto done; loop: body-statement update-expr ; t = test-expr ; if (t) goto loop; done: |
/* 原始C语言代码 */
long fact_for(long n)
{
long i;
long result = 1;
for (i = 2; i <= n; i++)
result *= i;
return result;
}
/* 转换为 while 形式 */
long fact_for_while(long n)
{
long i = 2;
long result = 1;
while (i <= n) {
result *= i;
i++;
}
return result;
}
/* 与之等价的goto版本 */
long fact_for_jm_goto(long n)
{
long i = 2;
long result = 1;
goto test;
loop:
result *= i;
i++;
test:
if (i <= n)
goto loop;
return result;
}
# 产生的汇编代码
# long fact_for(long n)
# n in %rdi
fact_for:
movl $1, %eax # Set result = 1
movl $2, %edx # Set i = 2
jmp .L8 # Goto test
.L9: # loop:
imulq %rdx, %rax # Compute result *= i
addq $1, %rdx # Increment i
.L8: # test:
cmpq %rdi, %rdx # Compare i:n
jle .L9 # If <=, goto loop
rep; ret # Return
4、continue
long sum = 0;
long i;
for (i = 0; i < 10; i++) {
if (i & 1)
continue;
sum += i;
}
直接将 for 循环转换为 while,会怎么样?——导致死循环,i 的值无法被改变。
long sum = 0;
long i;
while(i < 10;) {
if (i & 1)
continue; // 导致死循环
sum += i;
i++;
}
转换为 goto 的形式
long sum = 0;
long i;
while(i < 10;) {
if (i & 1)
goto update;
sum += i;
update:
i++;
}
3.6.7 switch 语句
switch 根据一个整数索引值进行多重分支——使用跳转表(jump table)
- 一个数组;
- 索引 i ,对应的数组元素(表项)存放的是目标指令地址。
- 优点:执行 switch 语句的时间与 switch 中的标签数量无关,分支较多时,性能远超 if-else
【注意】跳转表的使用:当标签数量较多(如 4 个以上),并且标签值的范围跨度较小。否则,GCC会偏向于使用 if-else 结构来编译 C 语言代码中的 switch 语句。
🌰一
/* 原始C语言代码 */
void switch_eg(long x, long n, long *dest)
{
long val = x;
switch (n) {
case 100:
val *= 13;
break;
case 102:
val += 10;
/* Fall through */
case 103:
val += 11;
break;
case 104:
case 106:
val *= val;
break;
default:
val = 0;
}
*dest = val;
}
/* 与之等价的goto版本 */
void switch_eg_impl(long x, long n, long *dest)
{
/* Table of code pointers */
static void *jt[7] = {
&&loc_A, &&loc_def, &&loc_B,
&&loc_C, &&loc_D, &&loc_def,
&&loc_D
};
unsigned long index = n - 100;
long val;
if (index > 6)
goto loc_def;
/* Multiway branch */
goto *jt[index];
loc_A: /* Case 100 */
val = x * 13;
goto done;
loc_B: /* Case 102 */
x = x + 10;
/* Fall through */
loc_C: /* Case 103 */
val = x + 11;
goto done;
loc_D: /* Cases 104, 106 */
val = x*x;
goto done;
loc_def: /* Default case */
val = 0;
done:
*dest = val;
# 产生的汇编代码
# void switch_eg(long x, long n, long *dest)
# x in %rdi, n in %rsi, dest in %rdx
switch_eg:
subq $100, %rsi # Compute index = n-100
cmpq $6, %rsi # Compare index:6
ja .L8 # If >, goto loc_def
jmp *.L4(,%rsi,8) # Goto *jg[index]
.L3: # loc_A:7
leaq (%rdi,%rdi,2), %rax # 3*x
leaq (%rdi,%rax,4), %rdi # val = 13*x
jmp .L2 # Goto done
.L5: # loc_B:
addq $10, %rdi # x = x + 10
.L6: # loc_C:
addq $11, %rdi # val = x + 11
jmp .L2 # Goto done
.L7: # loc_D:
imulq %rdi, %rdi # val = x * x
jmp .L2 # Goto done
.L8: # loc_def:
movl $0, %edi # val = 0
.L2: # done:
movq %rdi, (%rdx) # *dest = val
ret # Return
jmp 指令的操作数有前缀 ‘*’ ,表明是一个间接跳转,操作数指定一个内存位置,索引由寄存器 %rsi 给出,这个寄存器保存着 index 的值。
这里的 C 代码将跳转表声明为一个 7 个元素的数组,每个元素都是一个指向代码位置的指针。这些元素跨越 index 的值 0~6,对应于 n 的值 100~106。
跳转表对重复情况,使用同样的代码标号:表项 4 和 6 用同样的代码标号 loc_D;
对于缺失的情况:使用默认的代码标号:表现 1 和 5 用默认情况的标号 loc_def。
# 跳转表声明 .section .rodata .align 8 # Align address to multiple of 8.L4: .quad .L3 # Case 100: loc_A .quad .L8 # Case 101: loc_def .quad .L5 # Case 102: loc_B .quad .L6 # Case 103: loc_C .quad .L7 # Case 104: loc_D .quad .L8 # Case 105: loc_def .quad .L7 # Case 106: loc_D
【结论】在“ .rodata ”(只读数据,Read-Only Data)的目标代码文件的段中,有一组 7 个 “8字节”,每个 “8字节”的都是与指定的汇编代码标号相关联的指令地址。
如标号 .L4
标记处这个分配地址的起始——与这个标号相对应的地址会作为间接跳转的基地址。
🌰二
下面的 C 函数省略了 switch 语句的主体。在 C 代码中,情况标号是不连续的,而有些情况有多个标号。
void switch2 (long x, long *dest) { long val = 0; switch (x) { // Body of switch statement omitted } *dest = val;}
在编译该函数时,GCC 为程序的初始部分生成了一下汇编代码,变量 x 在寄存器 %rdi 中:
# void switch2(long x, long *dest)# x in %rdiswitch2: addq $1, %rdi cmpq $8, %rdi ja .L2 jmp *.L4(,%rdi,8)
为跳转表生成以下代码:
.L4: # 索引号 x 值
.quad .L9 # 0 -1
.quad .L5 # 1 0
.quad .L6 # 2 1
.quad .L7 # 3 2
.quad .L2 # 4 3
.quad .L7 # 5 4
.quad .L8 # 6 5
.quad .L2 # 7 6
.quad .L5 # 8 7
问:switch 语句内有几种 case 的值?
分析:
跳转表有 9 行,索引范围为 0~8;
由于 addq $1, %rdi
,cmpq $8, %rdi
可知,0<x+1<8,得到 -1<x<7;
根据 ja .L2
可知,.L2
对应于 default
;
根据索引地址:x = 0 或 7 时,指向相同(.L5
),所以存在 case 0
,case 7
;
x = 2 或 4 时,指向相同(.L7
),所以存在 case 2
,case 4
;
x = 3 或 6 时,指向相同(default
),所以存在 default
;
剩下的:x = -1 时,存在(.L9
),所以存在 case -1
;
x = 1 时,存在(.L6
),所以存在 case 1
;
x = 5 时,存在(.L8
),所以存在 case 5
;
答:有 -1、0、1、2、4、5、7 七种。