CSPP学习笔记-Ch3.6 分支控制

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 时。

【作用】

  1. 用来描述最近的算术逻辑操作后的状态。
  2. 用来描述比较测试指令操作后的状态。

【注意】在 3.5 小节中的操作,只有 leaq 指令不改变任何状态码,因为它是用来进行地址计算的。其他操作具体会影响哪些标志位,这里暂时不深入扩展。

1、CMP 和 TEST 指令

CMPTEST 只设置状态码,而不改变任何其他寄存器。
除了只设置状态码而不更新目的寄存器之外,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 S2S1比较
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 S2S1比较单精度值
vucomisd S1, S2 S 2 − S 1 S_2-S_1 S2S1比较双精度值

浮点比较指令会设置三个状态码:零标志位 ZF进位标志位 CF奇偶标志位 PF

顺序 S 2 : S 1 S_2:S_1 S2:S1CFZFPF
无序的111
S 2 < S 1 S_2<S_1 S2<S1100
S 2 = S 1 S_2=S_1 S2=S1010
S 2 > S 1 S_2>S_1 S2>S1000

当任一操作数为 N a N NaN NaN​ 时,就是出现“无序”的情况。

3.6.2 访问状态码

状态码通常不直接读取,有三种使用方法:
(1)根据状态码的某种组合,将一个字节设置为 0 或者 1;
(2)将状态码作为条件跳转到程序的某个其他的部分;
(3)可以有条件地设置为 0 或者1。

SET 指令

根据状态码的某种组合,将一个字节设置为 0 或者 1。
SET 指令的目的操作数 D 可以是:
一个寄存器的低位的 1 个字节;
or 一个字节的内存位置。

指令同义名效果设置条件
sete DsetzD ← ZF相等/零
setne DsetnzD ← ~ZF不等/非零
sets DD ← SF负数
setns DD ← ~SF非负数
setg DsetnleD ← ~(SF^OF) & ~ZF有符号:大于
setge DsetnlD ← ~(SF^OF)有符号:大于等于
setl DsetngeD ← SF^OF有符号:小于
setle DsetngD ← (SF^OF) | ZF有符号:小于等于
seta DsetnbeD ← ~CF & ~ZF无符号:超过
setae DsetnbD ← ~CF无符号:超过或相等
setb DsetnaeD ← CF无符号:低于
setbe DsetnaD ← 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 Label1(无条件跳转)直接跳转
jmp *Operand1(无条件跳转)唯一的:间接跳转
je LabeljzZF相等/零
jne Labeljnz~ZF不相等/非零
js LabelSF负数
jns Label~SF非负数
jg Labeljnle~(SF^OF) & ~ZF有符号大于
jge Labeljnl~(SF^OF)有符号大于等于
jl LabeljngeSF^OF有符号小于
jle Labeljng(SF^OF) | ZF有符号小于等于
ja Labeljnbe~CF & ~ZF无符号超过
jae Labeljnb~CF无符号超过或相等
jb LabeljnaeCF无符号低于
jbe LabeljnaCF | 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,也即十进制数-141141也即0x8d0x4005ed (nop指令的地址)加上这个值(也即减去0x8d)得到地址 0x400560

【补充】reprepz 有什么用?

reprepz 是同义名,retqret 是同义名。
使用 ~后面跟 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)=(1p)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-exprif (!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,RcmovzZF相等/零
cmovne S,Rcmovnz~ZF不相等/非零
cmovs S,RSF负数
cmovns S,R~SF非负数
cmovg S,Rcmovnle~(SF^OF) & ~ZF有符号大于
cmovge S,Rcmovnl~(SF^OF)有符号大于等于
cmovl S,RcmovngeSF^OF有符号小于
cmovle S,Rcmovng(SF^OF) | ZF有符号小于等于
cmova S,Rcmovnbe~CF & ~ZF无符号超过
cmovae S,Rcmovnb~CF无符号超过或相等
cmovb S,RcmovnaeCF无符号低于
cmovbe S,RcmovnaCF | 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, %rdicmpq $8, %rdi可知,0<x+1<8,得到 -1<x<7;
根据 ja .L2 可知,.L2 对应于 default
根据索引地址:x = 0 或 7 时,指向相同(.L5),所以存在 case 0case 7
x = 2 或 4 时,指向相同(.L7),所以存在 case 2case 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 七种。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
select * from (select t1.[id] as t1_id,t1.[requestId] as t1_requestId,t1.[htqsrq] as t1_htqsrq,t1.[htjzrq] as t1_htjzrq,t1.[htbh] as t1_htbh,t1.[gf] as t1_gf,t1.[xf] as t1_xf,t1.[rq] as t1_rq,t1.[fkfs] as t1_fkfs,t1.[formmodeid] as t1_formmodeid,t1.[modedatacreater] as t1_modedatacreater,t1.[modedatacreatertype] as t1_modedatacreatertype,t1.[modedatacreatedate] as t1_modedatacreatedate,t1.[modedatacreatetime] as t1_modedatacreatetime,t1.[modedatamodifier] as t1_modedatamodifier,t1.[modedatamodifydatetime] as t1_modedatamodifydatetime,t1.[form_biz_id] as t1_form_biz_id,t1.[MODEUUID] as t1_MODEUUID,t1.[htfj] as t1_htfj,t1.[zje] as t1_zje,t1.[ds] as t1_ds,t1.[zjedx] as t1_zjedx,t1.[cspp] as t1_cspp,t1.[yfk] as t1_yfk,t1.[gxid] as t1_gxid,t1.[bz] as t1_bz,t1.[gfqymc] as t1_gfqymc,t1.[gfjc] as t1_gfjc,t1.[bh] as t1_bh,t1.[jylx] as t1_jylx,t1.[cght] as t1_cght,t1.[yf] as t1_yf,t1.[yfk1] as t1_yfk1,t1.[yf11] as t1_yf11,t1.[nf] as t1_nf,t1.[rksj] as t1_rksj,t1.[cclx] as t1_cclx,t1.[cgbt] as t1_cgbt,t1.[yfk2] as t1_yfk2,t1.[sywf] as t1_sywf,t1.[yfbl] as t1_yfbl,t1.[fhbl] as t1_fhbl,t1.[yfh] as t1_yfh,t1.[sykf] as t1_sykf,t1.[hzsdlqys] as t1_hzsdlqys,t1.[sys_workflowid] as t1_sys_workflowid,t1.[cgqzyz] as t1_cgqzyz,t1.[htwjpdf] as t1_htwjpdf,t1.[cghtlc] as t1_cghtlc,t1.[htzt] as t1_htzt,t1.[qzfs] as t1_qzfs,t1.[htwjtp] as t1_htwjtp,t1.[cgqzlc] as t1_cgqzlc,t1.[sjfk] as t1_sjfk,t1.[ydkds] as t1_ydkds,t1.[chpt] as t1_chpt,t1.[lxdhchr] as t1_lxdhchr,t1.[gxsjkx] as t1_gxsjkx,t1.[hkzt] as t1_hkzt,t1.[lcfkd] as t1_lcfkd,t1.[fkzlcid] as t1_fkzlcid,t1.[mode_top_4] as t1_mode_top_4,t1.[cgdj] as t1_cgdj,t1.[mode_top_22] as t1_mode_top_22,t2.[id] as t2_id,t2.[mainid] as t2_mainid,t2.[sld] as t2_sld,t2.[ppcj] as t2_ppcj,t2.[hsdj] as t2_hsdj,t2.[bz] as t2_bz,t2.[je] as t2_je,t2.[xhggyt] as t2_xhggyt,t2.[mxgxid] as t2_mxgxid,t2.[dqkckc] as t2_dqkckc,t2.[rkhkc] as t2_rkhkc,t2.[yf] as t2_yf,t2.[yldjbhyf] as t2_yldjbhyf,SELECT year(rksj) as 年 FROM uf_gfht as cus_年年 from uf_gfht t1 INNER join uf_gfht_dt1 t2 on t1.id = t2.mainid) tmp1 where t1 错在哪里
05-14

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值