CSAPP阅读笔记-汇编语言初探(控制类指令)-来自第三章3.6的笔记-P135-P163

1.正溢出与负溢出:

  首先,一个正数与一个负数相加,不可能溢出,因为结果的绝对值一定小于两个加数的绝对值,既然两个加数能合理表示出来,结果一定也能合理表示出来。

  其次,正溢出是由于两个很大的正数相加,导致符号位变成1的情况如0110+0011=1001(假设最大只能运算4位)

  负溢出则是两个很小的负数相加,导致符号位变成0的情况,如1011(-5)+1011(-5)=10110->0110溢出,如1111(-1)+1111(-1)=11110->1110则没溢出。

  因此,正溢出的判断标准是符号位或最高位有进位。

     负溢出的判断标准是符号位和最高位只有一个发生了进位。符号位和最高位同时发生进位则没溢出。

     注意,这里的最高位指的是去掉符号位后的最高位,即符号位后面一位。

     可以结合上面列举的负溢出的例子理解。

2.条件码寄存器:

  CPU维护着一组条件码寄存器,它们只有一个位,它们会记录最近算术或逻辑操作带来的变化,常用的有:

  CF:进位标志,代表最近的操作使最高位产生了进位,用于检测无符号操作的溢出。

  ZF:零标志,代表最近的操作结果为0。

  SF:符号标志,代表最近的操作结果为负数。

  OF:溢出标志,代表最近的操作导致了正溢出或负溢出。

那么,系统是怎么根据操作来设置条件码寄存器的呢?以什么为判断基准?

比如系统用一条ADD指令完成了等价于t=a+b的功能,这时候会用以下表达式为判断基准,来设置条件码寄存器:

  CF:  (unsigned)t < (unsigned)a  无符号溢出

  ZF: (t==0)  零

  SF: (t<0)    负数

  OF: (a<0 == b<0) && (t<0 != a<0) 有符号溢出

  解释下CF: CF可用于检测无符号操作的溢出,若t与a,b都无符号,则都>=0,若此时t<a说明溢出了,而无符号操作溢出时的表现就是最高位(此时因为是无符号操作,最高位已经对应符号位了)出现了进位,因此对应CF。

  解释下OF: OF代表发生了溢出,需要满足两个条件,一是两个加数符号相同,二是结果的符号要和任意一个加数相反。

  leaq不是算术或逻辑指令,不会改变条件码。

  逻辑操作中,XOR会使CF和OF标志被设置为0

  移位操作中,CF为最后一个被移出的位,OF为0  为什么?

  INC和DEC会设置OF和ZF,但不会改变CF

3.CMP和TEST指令

  CMP和TEST指令都有b,w,l,q版本,分别对应字节,字,双字,四字

  CMP指令等价于SUB,区别就是它不会把计算结果更新到目的寄存器。CMP S1,S2会计算S2-S1并根据结果设置条件码。   

  TEST指令等价于AND,区别就是它不会把计算结果更新到目的寄存器。TEST S1,S2会计算S1&S2并根据结果设置条件码。

  TEST指令可用来判断某个操作数是正数,负数还是0,比如 testq %rax,%rax

 

4.访问条件码

  一般不直接访问条件码,而是根据条件码的组合设置某个字节为0或1,对应的就是SET指令。如下图:

  

 其后缀不是用来标志操作数大小的,只是用来代表不同的比较条件的。

 一个例子:

 比较a(位于%rdi)和b(位于%rsi)时,汇编如下:(两者都是64位long)

 comp:

  cmpq  %rsi,%rdi

  setl   %al

  movzbl  %al,%eax

  ret

 此时比较的是%rdi(a)-%rsi(b),结果被设置到%al中,movzbl在设置%eax的高3个字节为0时,还会把%rax的高4个字节一起清0。

 

 取其中的一条指令分析一下:

  setl,代表有符号的<,是以SF^OF作判定条件的,当没有溢出(OF为0)且结果为负数(SF为1)时,显然代表a-b<0,即代表有符号<成立,设置为1。当发生溢出时(OF为1),若正溢出(OF为1)且结果为非负数(SF为0),显然代表a-b<0,同样,当负溢出(OF为1)且结果为非负数(SF为0)时,也代表a-b<0,综上,用异或可以作判定条件。

5.条件控制转移指令

  

  如上图所示,跳转指令分有条件跳转和无条件跳转。

  jmp是无条件跳转,在汇编代码中,它后面直接加标号,汇编器把它变为.o文件时,会将标号对应的目标地址编码为跳转指令的一部分。

  它又分为直接和间接跳转,前者会直接把目标地址作为跳转指令的一部分,后者则从寄存器或内存中读出目标地址。

  直接跳转的表现形式是直接在jmp后加标号,如jmp .L1。间接跳转的表现形式是*后面加操作数指示符,如jmp *%rax,又如jmp *(%rax)。

  表中其它跳转指令都是条件跳转指令,条件跳转只能是直接跳转。

  那么跳转指令在编码机器代码时是如何确定目标地址的呢?

  如下:

            

  左侧为一段汇编代码,右侧为对应的机器代码(.o)及反汇编代码。

  跳转指令在变成机器代码时,最常用的编码方式是把目标地址和跳转指令后面那条指令对应的地址之差作为编码。当然,也有直接给出目标地址的编码方式。

  右图中,.L2下面的第一条指令的地址为8,jmp的下一条指令地址是5(.L3是标号,不是指令),相差8-5=3,符合之前的规律。

  这种表达方式的优点是表示的跳转目标都是相对值,因此当程序被重定位时(比如被链接后),改变的只是这段代码的绝对地址,但机器码仍可以不用变。

例子:

  40042f: 74 f4    je xxxxxx

  400431:  5d       pop %rbq

  请写出xxxxxx的地址:

    0xf4 = -12 , 12 = 0xc ,  400431-0xc = 400425

6.条件传送指令:

  条件控制转移指令存在一种缺陷,处理器是通过流水线的方式处理指令的,在取一条指令的同时,可能同时在执行前一条指令的算术运算。因此需要预先确定好指令的执行序列。当出现条件跳转时,处理器会对分支进行预测,虽然准确率很高,但一旦预测失败,处理器需要丢掉它为此跳转指令后面所做的所有工作,重新填充流水线。这会导致程序性能下降。

  而条件传送指令则是先把条件分支的多个值计算出来,比如说一个是a,一个是b,随后的操作是固定的,比如对a操作,++a什么的,此时若发现选的是b分支,则只需要b=a这么复制一下就行了,优势就在于无需为此丢掉跳转指令后面所做的工作,当然代价就是需要多做一次计算,因此条件传送指令的适用条件有限,编译器需要根据浪费的计算和分支预测错误导致的性能处罚中作权衡,然而实际上它无法很好地判断,因此,只有当两个表达式都十分容易计算时,编译器才会选用条件传送指令,有时候即使分支预测错误的开销更大,仍会选择条件控制转移指令。

  

以上是条件传送指令,和SET以及JMP一样,只有满足指定条件才能传送数据,源操作数可以是寄存器或内存地址,目的地是寄存器,它和MOV类指令类似,区别是指令名无需写上传送数据的长度(b,w,l,q),汇编器可从寄存器中推断出操作数长度(因为目的地不可能是内存地址,所以可以直接推断出)。

条件传送指令不支持单字节传送。 

条件传送指令有使用限制,因为它必须计算所有的条件表达式,因此若任意一个有错误条件,会导致非法行为。

比如:

          

对于左侧这段C代码,汇编形式如右侧所示,因为即使xp计算为0,cmove仍会计算0(%rax)和*xp(%rax)的值,而此时xp为空指针,会发生引用空指针的错误。

7. do while和while的实现

首先看下do while的实现例子:

清晰易懂,无需解释。

while循环则有两种实现形式:

第一种,被称为跳转到中间的实现形式,如下:

第二种形式是guarded-do形式,如下:

  

第二种形式其实是一种更高级别的优化,它主要对初始值进行优化,大部分情况下,初始条件下的while的循环条件是满足的,此时对第一种情况,仍会做一个跳转(可以看到第一种形式的b图中的goto test必然会被执行),而第二种形式对此作了优化,它会先判断一下初始是否满足while循环条件,满足时继续执行不跳转,不满足时才跳转到结束段,这种手法相当于把while改造成了do while,比起do while,仅仅多了一层初始的判断。

8. for循环的实现:

for循环都能转换为while,转换思路如下图:

    

具体转换例子见书。

下面做一个练习:

将下面的for循环先转换为跳转到中间的while形式,再转换为guarded-do形式:

答案:

以第一种形式转换:

以第二种形式转换:

这里有个需要注意的地方,在由第一种向第二种转换时,开头的那个判断条件是n<=1,那是因为它只对初始条件判断就行,因为循环不退出条件为i<=n,而开始时i=2,所以开始时无法进入循环的条件是n<=1。

第二个练习:

答案:

A:直接翻译后如下图所示:

此时continue会直接跳过本次循环进入下次循环,相对的也会跳过i++,导致i一直不变,永远跳不出while循环。

改变方法:使用goto代替continue:

8.switch语句

switch通过跳转表实现,它是一个数组,里面每一项都是一个代码段的地址,GCC根据开关数量决定是否使用跳转表(如大于4个,且值跨度较小会用)

 看一个switch的例子:

上面两张是switch语句的C语言使用及翻译,需要注意几点:首先&&在这里是一个C语言中的扩展运算符,用于创建一个指向某个代码位置的指针。

void *jt[7],jt是一个指针数组,这里unsigned long index=n-100配合if(index>6)可以把n的值限制在[100,106],因为n<100时n-100为负数,转换为unsigned后是个很大的正数。

下面左边那张是对应的汇编代码,注意jmp *.L4(,%rsi,8),这里是一个间接跳转,从.L4的地址为起始找到8*%rsi指代的索引对应的地址,乘8是因为一个内存地址占8个字节(64位)

右边那张是对应的跳转表,从中寻址到对应跳转目的地的地址,用间接跳转跳转过去。

原文地址:https://www.cnblogs.com/czw52460183/p/10067372.html