系列文章
文章目录
3.6 控制
- 程序的顺序结构是直线代码,也就是指令一条接着一条执行。
选择和循环结构要求有条件的执行 - 机器代码提供两种低级机制来实现有条件的行为:
1.条件指令:测试数据值,然后根据测试的结果来改变控制流或数据流
2.跳转指令:使用 jump 指令进行跳转
3.6.1 条件码
- CPU维护着一组单个位的条件码寄存器,描述了最近的算术或逻辑操作的属性,可以通过检测这些寄存器来执行条件分支指令。
- 最常用的条件码:
CF:进位标志。 最近的操作使最高位产生了进位。可以用来检查无符号数的溢出
ZF:零标志。 最近的操作的结果为 0
SF:符号标志。 最近的操作的结果为负数
OF:溢出标志。 最近的操作导致了补码溢出 - leaq 指令不改变条件码,其余的所有算术和逻辑指令都会设置条件码。
- 两类特殊指令,只设置条件码不更新目的寄存器:
1.CMP:除了不更新目的寄存器外与 SUB指令的行为相同,可以用来比较操作数的大小关系
2.TEST:除了不更新目的寄存器外与 AND指令的行为相同,可用来比较两个操作数是否相等
3.6.2 访问条件码
- 通常不直接读取条件码,使用条件码的三种方法:
1.根据条件码的某种组合,将一个字节设置为0或者1
2.条件跳转到程序的某个其他部分
3.有条件地传送数据 - SET指令(中的一类),其不同后缀表示它们考虑的条件码组合。(条件码由CMP,TEST指令或者其他算术逻辑运算产生)目的操作数是低位单字节寄存器元素之一或一个字节的内存位置。set 会将该字节设置为 0 或 1
- 后缀及设置条件记忆:
- set:e,相等设0,s,为负数设1,加n就是非0非负数的意思
- setn:g,a大于等于,l,b小于等于,加e代表仅小于,a,b无符号
- 比较大小的条件组合指令也需要分别处理有符号和无符号操作。
3.6.3 跳转指令
- 有条件的跳转指令和SET指令的尾缀差不多
- 直接跳转:跳转目标作为指令的一部分,汇编中”jump.print“,单纯的程序跳转,不依赖栈(call和ret)
- 间接跳转:跳转目标从内存或寄存器中读出,汇编中”jump *%rax“,软链接库和浮动程序
- 条件跳转只能是直接跳转
3.6.4 跳转指令的编码
- 跳转指令最常用的是PC相对的(PC-ralative):根据程序计数器(PC)的当前值,结合偏移量计算目标地址。第二种是给出”绝对地址“
- 相对寻址的例子:跳转至L2.判断条件,跳转至L3,循环
movq %rdi, %rax
jmp .L2
.L3:
sarq %rax
.L2:
testq %rax, %rax
jg .L3
rep; ret
- 反汇编:
0: 48 89 f8 mov %rdi,%rax
3: eb 03 jmp 8 <loop+0x8> #跳转至8
5: 48 d1 f8 sar %rax
8: 48 85 c0 test %rax,%rax
b: 7f f8 jg 5 <loop+0x5> #跳转至5
d: f3 c3 repz retq #这里的rep是空操作避免ret成为条件跳转指令的目标而不能正常返回
被链接后,指令会被重定位到不同的位置,通过PC跳转仍然可以对应,目标代码无需改变就可以移动到内存的其他位置。
3.6.5 用条件控制来实现条件分支
- 结合有条件跳转和无条件跳转实现条件表达式
- C语言中的goto语句类似于汇编中的无条件跳转
- 在 C 语言中,if-else 语句的通用形式模板如下:
这test-expr取值为 0或者为非 0。两个分支语句中(then-statement 或 else-statement)只会执行一个。
if (test-expr)
then-statement
else
else-statement
- 汇编实现通常会使用下面这种形式,这里,我们用 C 语法来描述控制流:
t = test-expr;
if (!t)
goto false; //有条件
then-statement
goto done; //无条件
false:
else-statement
done:
3.6.6 用条件传送来实现条件分支
- 控制的条件转移:条件满足时沿一条执行路径执行,否则走另一条路径
- 数据的条件转移:根据特定条件决定数据的传输或赋值
提前将两种结果的数据计算好,根据条件是否满足用一条简单的条件传送指令来实现它,这样更符合现代处理器的性能特性 - 流水线(4,5章),一条指令的处理需要经过一系列的阶段,重叠连续指令来获得高性能,遇到分支时需要分支确定后才决定往哪边走
- 处理器采取精密的分支预测逻辑来猜测跳转指令是否执行(90%以上),如果预测失误就将浪费15-30个时钟周期,容易预测时调用函数大约8个周期,随机时大约是17.5周期
- 提前将数据准备好,即使准备数据需要时间,使控制流不依赖于数据,使处理器保持流水线是满的
- 条件传送指令(CMOV)允许在不使用条件跳转的情况下,根据某个条件来决定是否进行数据传送。
- C语言例子
//三目运算符
v = test-expr ? then-expr : else-expr;
//条件控制
if (!test-expr)
goto false;
v = then-expr;
goto done;
false:
v = else-expr;
done:
//条件传送
v = then-expr;
ve = else-expr;
t = test-expr;
if (!t) v = ve;
- 虽然条件传送与现代处理器更契合,但需要关注两个注意事项
- 我们对then-expr,else-expr都进行了计算而他们可能产生错误条件或者副作用例如,我们判断指针是否为空,然后进行引用计算。。。。
- 如果对两个分支的求值需要大量计算,那开销比条件控制还大,编译器需要考虑这些,但编译器不具有充足的信息支持它做出正确判断只有表达式非常易于计算时,才会使用条件传送。编译器是趋向保守的,许多预测错误的开销大于较复杂的计算时,GCC还是使用条件控制
3.6.7 循环
1. do-while:执行循环体,测试表达式。测试为真,再执行一次循环。
//do-while 语句的通用形式
do
body-statement
while (test-expr);
//通用形式可以被翻译成如下所示的条件和 goto 语句
loop:
body-statement
t = test-expr;
if (t)
goto loop;
- 逆向工程循环
理解汇编代码与原始源代码的关系,关键是找到程序值和寄存器之间的映射关系。对复杂的程序来说,编译器常常会重组计算,有些C代码中的变量在机器代码中没有对应的值;而有时,机器代码中又会引入源代码中不存在的新值。此外,编译器还常常试图将多个程序值映射到一个寄存器上,来最小化寄存器的使用率。
逆向工程循环的一个通用策略。看看在循环之前如何初始化寄存器,在循环中如何更新和测试寄存器,以及在循环之后又如何使用寄存器。这些步骤中的每一步都提供了一个线索,组合起来就可以解开谜团。其中有些情况很明显是编译器能够优化代码(指令重排,内联函数),而有些情况很难理解编译器为什么要使用那些奇怪的策略。GCC的有些变化,非但不能带来性能好处,反而可能降低代码性能。(寄存器分配)
2. while:测试表达式,执行循环体。测试表达式,测试为真,再执行一次循环。
//while 语句的通用形式
while (test-expr)
body-statement
//1.jump to middle
goto test;
loop:
body-statement
test:
t = test-expr;
if (t)
goto loop;
//2.guarded-do(优化级别O1)
//初始条件不成立就跳过,编译器常常可以优化初始的测试(提前知道第一次是否满足)
t = test-expr;
if (!t)
goto done;
loop:
body-statement
t = test-expr;
if(t)
goto loop;
done:
3. for循环:初始表达式,测试条件,更新表达式
//for循环的通用形式
for(init-expr; test-expr; update-expr)
body-statement
//等价于
init-expr;
while (test-expr){
body-statement
update-expr;
}
//1.jump to middle
init-expr;
goto test;
loop:
body-statement
update-expr;
test:
t = test-expr;
if (t)
goto loop;
//2.guarded-do(优化级别O1)
init-expr;
t = test-expr;
if (!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if(t)
goto loop;
done:
- for循环中的continue通常是goto实现,跳转到update部分,防止死循环
- C语言中的三种形式的所有循环,都可以用一种简单的策略实现,产生一个或多个条件分支的代码。控制的条件转移提供了将循环翻译成机器代码的基本机制。
3.6.8 switch语句
- switch根据一个整数索引值进行多重分支,通过使用**跳转表(jump table)**使实现更高效,跳转表是一个数组,第i项是一个代码段的地址,当开关(分支)数量较多,值的跨度较小时,就会使用跳转表, 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;
}
//c过程
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;
}
//汇编
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 *jt[index] */
.L3:
leaq (%rdi,%rdi,2), %rax /* loc_A: 3*x */
leaq (%rdi,%rax,4), %rdi /* val = 13*x */
jmp .L2 /* Goto done */
.L5:
addq $10, %rdi /* loc_B: x = x + 10 */
.L6:
addq $11, %rdi /* loc_C: val = x + 11 */
jmp .L2 /* Goto done */
.L7:
imulq %rdi, %rdi /* loc_D: val = x * x */
jmp .L2 /* Goto done */
.L8:
movl $0, %edi /* loc_def: val = 0 */
.L2:
movq %rdi, (%rdx) /* done: *dest = val */
ret /* Return */
//跳转表
.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 */