程序的机器级表示2
程序的流程控制,包括条件循环和分支结构,如if,for,while,switch语句,本文从汇编的角度,来理解这些结构
条件码

例如有两个值运算,有可能会溢出,有可能会产生进位,有可能为负,运算后,这些信息都通过条件码的形式保存下来。
- CF:进位标志位,针对无符号数。若产生进位或借位,CF会被置位为1(无符号数进位或借位表示无符号位溢出)
- SF:符号标志位,针对有符号数。表示当前指令运算结果的符号,非负数置位为0,负数置位为1
- ZF:0标志位。运算结果为0时,会被置位为1
- OF:溢出标志位,针对有符号数。有符号数运算有可能出现溢出而非进位,若产生溢出,则置位为1
lead
指令只进行地址运算,不会修改条件码
一元,二元,移位操作都会修改寄存器,并且设置条件码
有两类指令只设置条件码,不修改其他寄存器: CMP
和 TEST
指令
CMP
对操作数之间运算比较,基于S2-S1
1 | CMP S1,S2 |
TEST
对操作数进行测试,基于S2&S1
1 | TEST S1,S2 |
例如 testq %rax,%rax
可以检查%rax是正数负数还是零
每条指令都对应四种类型 b,w,l,q
对应 字节,字,双字,四字
上面是设置条件码的一些指令,下面是一种根据条件吗,设置的指令SET(读取条件码)
访问条件码
SET
指令会根据条件码设置值,有三种使用方法
- 根据条件码,设置字节
- 跳转程序
- 有条件传送数据
1 | comp: |
set指令

跳转指令
jump指令又可以分为直接跳转,间接跳转,有条件跳转
- 直接跳转
jmp .L1
- 间接跳转
jmp *(%rax)
- 有条件跳转,一般和cmp或test组合使用,并且只能是直接跳转到代码段

跳转指令的编码方式常见的采用pc相对寻址(基址+偏移量)来编码,csapp中有个例子
1 | 1 movq %rdi, %rax |
.o
反汇编
1 | 0:48 89 f8 mov %rdi , %rax |

条件结构
条件结构可以用条件控制和条件跳转来实现
条件控制的条件结构
1 | //C语言中的if-else |
翻译为goto版本
1 | t = test; |

&&在c中存在短路现象,即t1&&t2,若前面为假,第二个条件直接不检查,下面从汇编的角度理解下(csappT3.16)
源码
1 | void cond(long a,long *p) |
gcc下汇编代码
a in %rdi ,p in %rsi
1 | cond: |
可以看到,若第一个为假,会直接跳转到 .L1
段,跳过检查第二个条件的代码
条件跳转的条件结构
条件传送比起条件控制,更符合现代处理器的性能特性
- 条件控制->判断,满足条件,执行,错误,重新导入指令
- 条件传送->判断,传送or不执行
分支预测的处罚
假设预测错误的概率是 p,如果没有预测错误,执行代码的时间是 TOK,而预测错误的处罚是 TMP。 那么,作为 p 的一个函数,执行代码的平均时间是 Tavg(p)=(1-p) TOK+p(TOK+TMP)。 如果已知 TOK和 Tran(当 p=0.5 时的平均时间),代入等式,我们有 Tran=Tavg(0.5)=TOK+0.5TMP, 所以有 TMP=2(Tran-TOK)。因此,已知 TOK=8 和 Tran=17.5,我们有 TMP=19。

条件传送指令

条件控制的代码
1 | if (!test-expr) |
条件传送的代码
1 | v=then-expr; |

条件传送相当于不进行预测了,把分支的表达式都算出来,最后当满足条件的时候再进行赋值操作,这种对简单的表达式才有效,而且有时候可能会失效,不能采用这种方法,例如如下代码,如采用条件传送的话,假设指针为空,会异常
1 | long cread(long *xp){ |
循环
c中的循环,包括while,do-while,for循环
do-while
1 | do |
直到型循环,至少执行一次,翻译为goto
1 | loop: |

while
1 | while(test-expr) |
跳转到中间, 它执行一个无条件跳转跳到循环结尾处的测试,以此来执行初始的测试
1 | goto test; |

guarded-do,首先用条件分支,如果初始条件不成立就跳过循环,把代码变换为 do-while 循环
1 | t=test-expr; |
goto代码
1 | t=test-expr; |

for
1 | for(init-expr;test-expr;update-expr){ |
for可以翻译成while代码
1 | init-expr; |
中间策略
1 | init-expr; |
guarded-do
1 | init-expr; |
switch语句
switch语句通过使用跳转表(jump table)这种数据结构,可以根据一个索引值进行多重分支
跳转表,是一个指针数组,数组元素都是指向代码段的指针,通过 &&
符号声明
switch.c
1 |
|
gcc下运行结果
1 | 1 |
采用指针数组的方法对比
1 |
|
switch语句关键步骤是通过跳转表访问代码位置。
跳转表对重复的情况就是用同样的代码标号,对跳出程序的情况使用默认标号
csappT3.30
下面的 C 函数省略了 switch 语句的主体。在 C 代码中,情况标号是不连续的,而有些情况有多个标号。
1 | void switch2(long x, long *dest) { |
汇编
1 |
|
为跳转表生成以下代码:
1 | .L4 |
A. switch 语句内情况标号的值分别是多少?
-1、0、1、2、4、5 和 7
B. C 代码中哪些情况有多个标号?
.L5
的情况为 0 和 7,.L7
的情况标号为 2 和 4。