条件码
在之前的内容中,我们提到EFLAGS 寄存器中有一些条件码。这些条件码为流程控制的跳转提供了一定的能力。
CF
进位标识。最近的操作使得最高位产生的了进位。ZF
零标识。最近的操作所得的结果为0。SF
符号标识。最近的操作所得的结果为负数。OF
溢出标识。最近的操作导致补码溢出。
比如,对于C语言表达式t = a + b
,我们假设汇编后的命令是一条ADD指令。在执行ADD命令的同时,计算机同时会根据一下规则为上述的四个标识位赋值:

会改变条件码的算术逻辑指令有

特殊地:
- leaq 用来进行地址计算,不会进行任何条件码的修改
- 所有逻辑操作,CF 和 OF 会被标记为0
- 移位操作,CF 被设置成为最后一个被移出的位,OF 被置为0
- INC & DEC,不会设置CF标识
除了这些算术逻辑指令,还有两个特殊的指令CMP
和TEST
,这两个指令专门用来改变条件码。
CMP S1, S2
,计算S2-S1
的结果,根据结果更新条件码。即如果S1=S2,零标志会被设置为1;如果不相同,则可以利用其它标志判断两数大小关系。TEST S1, S2
,计算S2&S1
的结果,根据结果更新条件码。典型用法是两个操作数一样,来通过条件码得到这个数是正数,零还是负数。

条件码的使用分为三类:
- 根据条件码,通过某些指令来将一个字节设置成为0或者1;
- 进行条件跳转到程序的其他部分
- 可以有条件地传送数据
支持第一种情况的命令是SET命令,如下:

SET类指令的后缀的意思不再是长度,而是某些不同的条件
一个简单的比较第一个参数是否小于第二个参数的程序:
1
2
3
4int comp(long a, long b){ return a < b; } // a in %rdi, b in %rsi
写成汇编如下:
1
2
3
4
5comp: cmpq %rsi, %rdi # 执行a - b. setl %al # 如果ZF == 1,%al(%rax或%eax的低位)会被设置为1 movzbl %al, %eax # 将%al零扩展更新到%eax, 同时把%eax和%rax的高位清零 ret
练习:


分析:汇编语言是不会直接标记它的类型的,但是会标记它的数据的位数;我们需要根据其他的操作指令,来确认它的类型与符号
跳转命令
类似C语言的goto关键字,是jmp
指令。后可跟
- 标号(Label): 会被汇编器转成对应汇编指令的地址,直接跳转。写法
jmp .L1
,跳转到 L1 标号; - 操作符指示数:可以是寄存器或者内存地址,间接跳转。写法:
jmp *%rax
, 跳转到%rax的值代表的目标jmp *(%rax)
,跳转到%rax中的指针指向的内存地址中的的目标
除了jmp
指令之外的跳转指令都是有条件的——根据条件码来跳转。条件跳转只能是直接跳转。

一个例子:

这个例子可以帮助我们理解跳转指令在编译之后的结果中的编码情况:
- PC-relative 编码,
- 反汇编版本中,查看实际的字节编码,line 2, 跳转指令的目标编码是0x03,把它加上下一个PC,即0x5可以得到,0x8,也就是line 4的指令
- 反汇编版本中,查看实际的字节编码,line 5, 跳转指令的目标编码是0xf8,编码方式是补码(十进制为-8),把它加上下一个PC,即0xd(十进制13)可以得到,0x5,也就是line 5的指令
- 绝对地址编码
- 反汇编版本中,根据最右注释,line 2, 跳转指令的跳转目标指明是是0x8
- 反汇编版本中,根据最右注释,line 5, 跳转指令的跳转目标指明是是0x5
这些例子说明:
- 当执行PC-relative寻址时,程序计数器的值是跳转指令的下一条指令的地址值。这个是对计算机早期实现的延续和兼容。
- 跳转指令之后接的大小数字, 要按照补码的形式去解释
一个练习:

条件分支的实现
有两种方式:
- 通过「条件控制」实现条件分支
- 通过「条件传送」实现条件分支
条件控制
即将条件分支流转换成等价的goto方式,再进一步转换成由jmp指令实现跳转的条件分支。其中等价的goto版本仅是帮助人对生成的汇编代码进行分析时使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// C语言中的if-else的通用模板如下 if (test_expr) // 一个整数表达式。取值为0表示「假」 then_statement; // 分支语句 else else_statement; // 分支语句 // 对于这种情况通常转换为以下 句子对应的汇编代码 t = test_expr; if (!t) goto FALSE; then_statement; goto DONE; FALSE: else_statement DONE: // ...

这个方法中,实现条件分支的核心是通过控制语句,对应于上例的test_expr
。这种机制:
- 优点:简单,通用
- 缺点:在现代CPU上,可能会比较低效
条件传送
条件传送的核心是分支预测。条件控制来实现条件分支在现代CPU上,可能会比较低效的原因在于,现代CPU的执行过程是流水线(pipeline)的,使用条件控制不易将指令流水线化,导致效率变低。
当机器遇到条件跳转的时候,机器不能确定是否会进行跳转。处理器采用非常精密的分支预测逻辑来预测每一条条件跳转指令到底会不会执行。只要猜测还算可靠(现代处理器要求对条件跳转的预测准确度在百分之九十以上),那么流水线模型会运行得很好。一旦错误预测了一个条件跳转指令,那么意味着处理器要丢掉它为该跳转指令后所有指令已经做的工作,去一个新的,正确的跳转位置开始填充流水线。这意味着一个错误的预测会带来严重的惩罚——大约20~40个时钟周期的浪费。
对于上面的例子,条件传送的改造是:

C语言中的条件传送通用形式模板是这样的:
1
2
3
4vt = then_statement; v = else_statement; t = text_expr; if(t) v = vt; // 只有当测试条件满足时,vt的值才会被复制
可以看到:执行条件传送时,无需预测测试结果,而是把两个分支的运算都完成。处理器只是读取值,然后检查条件码,然后要么更新目标寄存器,要么保持不变。这会使得工作效率变高,因为它规避了预测错误带来的惩罚。
也不是所有的条件表达式都适合用条件传送的。如果两个表达式中有任意一个可能产生错误或者副作用,就会导致非法的行为,例如:
1
2
3
4int readpointer(int *xp) { return (xp ? *xp : 0); }
条件传送也不会总是改进效率。如果两个表达式需要做大量的计算,那么当对应的条件不满足时,所做的工作就白费了。编译器必须权衡条件传送多做的计算,和条件分支预测错误惩罚之间的相对性能。
但其实这个实现是错误的。因为条件传送对指针 xp 的引用是一定会发生的,如果 xp 是一个空指针,会导致一个间接引用空指针的错误。
乍一看似乎没什么问题:如果指针为空,则返回0,否则返回指针指向的整数。
传送指令:

循环
C语言提供多种循环结构,for、while 和 do while。但看到这里大家也明白,汇编里没有相应的高级抽象指令,而是通过用测试、条件码、跳转组合起来,形成循环的效果。
do-while
1
2
3
4
5
6
7
8
9
10
11// do-while循环的通用结构是: do body_statement while (test_expr); // 等价的goto版本 loop: body_statement t = test_expr if (t) goto loop
一个求阶乘的例子:

while
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// while循环的通用结构是: while (test_expr) body_statement; // 有两种翻译方法 // 1. jump to middle // 一般GCC带优化命令行选项-Og时产生这样的翻译 goto test loop: body_statement test: t = test_expr if (t) goto loop // 2. guarded-do // 首先使用条件分支,初始条件不成立就跳过循环,把代码变换为do-while循环 // 在使用较高优化等级编译时,如-O1,GCC会使用这样的策略 // 等价的do-while形式 t = test_expr if (!t) goto done do body_statement while (test_expr); done: // 对应的翻译 // goto 版本 t = test-expr if(!t) goto done; loop: body_statement t = test_expr if(t) goto loop; done:
一个jump to middle
阶乘的例子

for
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 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 init_expr; t = test-expr if(!t) goto done; loop: body_statement update_expr; t = test_expr if(t) goto loop; done:
Switch
switch 语句可以根据一个整数索引值进行多重分支(multi-way branching)。处理具有多种预测可能的分支时,这种语句特别有用,而且提高了C语言代码的可读性。
通过使用一种数据结构,叫做跳转表(jump table),使得实现 switch 十分的高效。跳转表是一个数组,表项 i 是一个代码段地址,代码段是当索引值等于 i 时程序应采取的动作。开关索引值就是用来执行一个跳转表内数组引用,来确定目标指令的情况。和用一组很长的 if-else 嵌套不同,使用跳转表的优点是执行开关语句的时间和开关的数量无关。
GCC根据开关的数量和开关情况值的稀疏程度翻译开关语句,例如开关情况数量在4个以上,且值的跨度较小时,会使用跳转表。
下面是一个switch例子



其中&&
是gcc拓展C语言语法,表示代码位置的指针。拓展C语言版本中,jt就是跳转表。
执行switch语句的关键步骤是通过跳转表来访问代码位置。在C代码中是第16行,一条goto语句引用了跳转表jt。GCC支持计算goto(computed goto),是对C语言的扩展。在我们的汇编代码版本中,类似的操作是在第5行,jmp指令的操作数有前缀*
表明这是一个间接跳转,操作数指定一个内存位置,索引由寄存器%rsi给出,这个寄存器保存着index的值。
最后
以上就是留胡子犀牛最近收集整理的关于c语言求阶乘和的流程图_Introduction to CSAPP(十四):流程控制指令与 C 语言条件判断与循环的全部内容,更多相关c语言求阶乘和的流程图_Introduction内容请搜索靠谱客的其他文章。
发表评论 取消回复