吾读:吾读 - 《深入理解计算机系统》第三章 程序的机器级表示 (二) 跳转和循环

上一篇我们初步认识了寄存器,学会了几个简单的汇编指令,例如mov,以及几种寻址模式。寻址模式不仅在mov指令中有用,各种指令都是通过这种方式来读取或写入内存中的值。

一个程序有各种函数,一个函数中也可能有复杂的结构,判断、循环等等,这些东西在汇编中又是什么样的呢?if-else, for, while, switch这些常见的结构会被翻译成怎样的汇编代码呢?

3.5 算术和逻辑操作

本节要接触多更多汇编操作指令,主要以算术及逻辑操作为主,分为四类,参见下图:

加减乘,没有除,这是为什么呢?因为除法比较特殊,需要同时操作两个寄存器,属于特殊操作指令

3.5.1 加载有效地址 lea

lea即 Load Effective Address,独成一类。该指令实际是mov的变形,指令形式是从存储器读数据到寄存器,但实际上并不会访问存储器,而只是将地址存储到目标寄存器。如同C语言中的&S结果。

3.5.2 一元和二元操作

第二类为一元操作,一元操作即只有一个操作数,即使源也是目标。如图3.7中的 Inc dec neg not指令。

第三类为二元操作,有两个操作数,且第二个操作数既作为源也作为目标。 如同C中的 x += y 中的x既作为源,又作为目标。与mov同样的,两个操作数不可以同时为存储器位置。

3.5.3 移位操作

第四类移位操作。操作数k取值范围为0到31,可以是立即数,或者存放在单字节寄存器中(即前四个寄存器的地位中)。两个左移是一样的,都是右边填充0 。右移则区分算术右移和逻辑右移。

所以左移也有两个指令是强迫症保持对称吗?

常数的乘法常常会优化为加法和移位的组合,当然对于无法拆分的则只能直接使用imul指令。例如x*48 可以拆解为两条指令 x*=3;x<<4 而x*3又可以使用 lea (%eax, %eax, 2), %edx 来实现

利用寻址方式来实现一些固定的乘法。由于寻址模式里面的s只能取值1 2 4 8,所以该方式就只能实现一些特定的乘法, 例如3x, 5x, 9x等。

3.5.5 特殊的算术操作

上图的imull和mull常规来说是需要两个操作符的,但是这里是但操作符。当他们为单操作符的时候,就是计算64位的乘法。此法要求另一个操作数要提前存储在%eax中。乘法的结果高位存在%edx中,地位存储在%eax中组合成一个64位的数字。

目前为止%eax有三个特殊用法了:

  • 整数型返回值,需要放入到%eax中
  • imull和mull的单操作数模式,使用%eax中数据作为乘数,结果的低位也存储到%eax中
  • idivl和divl被除数的低位在%eax中。

3.6 控制

程序执行的一个很重要的部分就是控制执行的顺序,像条件分支,循环,switch等等。汇编代码中基本上就只有个跳转,相当于C中的goto。所以后面将可以看到一些实例C代码的汇编,同其goto实现版本基本是一致的。

学C++的时候都说goto不好,建议不要用,是因为程序员容易用的不好?主要是容易滥用导致控制流混乱。结构良好的goto,可以很好的统一在函数结尾释放资源、处理失败情况等。

3.6.1 条件码

CPU的四个条件码寄存器:

  • CF:进位标志。最近的操作使最高位产生了进位。用于无符号数溢出
  • ZF:零标志。最近的操作结果是0 。
  • SF:符号标志。 是否为负数。
  • OF:溢出标志。最近的操作导致二进制补码溢出

例如C代码 t = a + b, 该指令操作完成后就会设置以上的四个标志位。其中:

  • CF:(unsigned t) < (unsigned a)
  • ZF:t == 0
  • SF:t < 0 负数
  • OF:(a < 0 == b < 0) && (t < 0 != a < 0)

leal指令不会改变条件码,因为它只操作地址。对于逻辑操作,xorl (异或)进位标志和溢出标志都设置成0,移位操作则进位标志设置为最后一个被移除的位,溢出标志则为0 。

cmp和test只修改条件码,不修改其他寄存器。cmp和test的区别为:cmp是比较两个参数的差,test是两个参数的与

比如cmp a,b 如果a和b相等,则有a-b == 0,所以ZF为0。所以ZF可以用来确定两个数是否相等。

通过几个条件码的组合,就能对两个数的各种情况作出明确的判断了。条件码又怎么写入到通用寄存器中呢?

3.6.2 访问条件码

条件码只有一个字节,所以它只能写入到8个单字节的寄存器中。而前面提过的movzbl,movsbl则可以将低位扩展

cmp1 %eax,%edx setl %al 读取条件码写入到al中movzb1 %al,%eax 扩展到整个寄存器

注意小于是setl, l 是 less的缩写,不是large的缩写。g则是greate的缩写。e是equls,n当然是not啦。a估计是above,b则就是below。

这么些逻辑算术看的人头大,直接死背或者用到的时候在查表还好点。

C语言的判断表达式中 && 与 & 有区别吗?例如 if ( expa && expb) 和 if (expa & expb)?
区别是 &&有短路属性,即表达式expa如果为真,则不会计算expb,而&没有这个属性,需要完整计算。

3.6.3 跳转指令编码

跟前面set的后缀完全一致,就是set换成j 。jmp完整指令则是不判断条件。其他指令则是在满足条件的情况下跳转。这里的Label也如同C语言中Goto的标签一样。

jmp *Operand指令则是跳转到寄存器或者内存中的某个地址去。 例如 jmp *%eax, jmp *(eax) 寻址方式则是与mov等指令一致。

跳转指令的常见做法是:目标地址相对PC(指令计数器)的。也就是是比如当前的PC是 0x123, 则想要调到0x126地址去,编码会是jmp 0x3

一般来说PC的值是当前指令的下一条指令的地址,而不是当前指令的地址,这是因为处理器会先将PC更新,然后才开始执行指令。

汇编代码中可能会有.p2align 4,,7类似的指令,这是条汇编器的命令,它会使后面的指令地址从16的倍数开始,而最多浪费7个字节。这有什么好处呢?它能够让处理器更优化地使用指令高速缓存存储器

3.6.4 翻译条件转移
if (test-expr) then-statement else else-statement以上语句翻译成goto版本为: t = test-expr if (t) goto True; else-statement; goto Dne;True:then-statementDone:...

汇编语言正是以goto版本来翻译if语句的。

为什么用goto,我想是因为汇编及底层只有顺序和跳转两个方式。就是说只有goto这种方式,高级语言的控制结构必须转成想应的goto结构。

3.6.5 循环

for、while和do-while循环,一般在编译成汇编的过程中,都会转换成do-while的循环结构。do-while 循环结构有下面的形式:

dobody-statementwhile(test-exp)这很容易翻译成汇编代码的结构如下:loop:body-statementt = test-expif (t)goto loop

while循环如何翻译成上面的形式?

while (test-exp)body-statement可以翻译成: t = test-exp; if (!t) goto done;loop:body-statementt = test-expif (t)goto loop;done:

这里提到一个小技巧:例如上面的

t = test-expif (t)goto loop;

看起来有三句,实际上汇编指令可能就2句,比如

decl %eax jre loop

第一句对t操作之后,立刻进行对t进行判断,不需要额外操作,因为此时条件码恰好是t这个操作相关的。所以可以节省一条判断t的指令。

for循环的格式翻译如下

for (init-expr;test-expr;update-expr)body-statement;翻译成goto方式如下: init-expr; t = test-expr; if (!t) goto done;loop:body-statement;update-expr;t = test-expr;if (t)goto loop;done:相比前面的while 只是多了init-expr和update-expr。

各种循环最终都是转变成了goto模式。这并不是实际开发中值得借鉴的方式,使用goto很显然让代码更难阅读了。

3.6.6 switch语句

早就听说switch比一堆if要快,为什么快,怎么个快法?是不是任何情况都快?这下要揭晓了。

GCC根据开关情况的数量和开关情况值的稀少程度来翻译开关语句。当开关情况数量比较多,并且值的范围跨度比较小时,就会使用跳转表。跳转表可以使得跳转时间与开关数量无关。

这么说跳转表来类似一个map,直接索引到目标,多个if-else则像是vector在遍历。

这里也说到了使用跳转表是有条件的,并不是switch必然会使用。在不使用调整表的情况下,两者的汇编代码其实是一致的。所以以后不能无脑说switch比if快了,得有理有据。

CC根据开关情况的数量和开关情况值的稀少程度来翻译开关语句。当开关情况数量比较多,并且值的范围跨度比较小时,就会使用跳转表。跳转表可以使得跳转时间与开关数量无关。

这么说跳转表来类似一个map,直接索引到目标,多个if-else则像是vector在遍历。

这里也说到了使用跳转表是有条件的,并不是switch必然会使用。在不使用调整表的情况下,两者的汇编代码其实是一致的。所以以后不能无脑说switch比if快了,得有理有据。

相关推荐

相关文章