一、算术和逻辑操作
下图列出了x86-64的一些整数和逻辑操作。大多数操作都分成了指令类。例如 add 指令类由四条加法指令组成:addb、addw、addl、addq,分别是字节加法、字加法、双字加法、四字加法。事实上,不仅是 add 指令,下图的每个指令类都有对这四种不同大小数据的指令。分别为:加载有效地址、一元操作、二元操作和移位。二元操作有两个操作数,一元操作有一个操作数。
加载有效地址:leaq 指令是 movq 指令的变形,指令形式是从内存读数据到寄存器,但实际上没有引用内存。它可以简单描述普通的计算操作。编译器经常发现 leaq 的一些灵活用法,根本就与有效地址计算无关。例如:leaq a(b, c, d), %rax 先计算地址a + b + c * d,然后把最终地址载到寄存器rax中。
一元和二元操作:第二组中的操作是一元操作,只有一个操作数,既是源端又是目的端。这个操作数可以是一个寄存器也可以是一个内存。第三组是二元操作,其中第二个操作数既是源又是目的。
移位操作:最后一组是移位操作,先给出移位量,然后第二项给出要移位的数。可以进行算术和逻辑右移。
特殊算术操作:两个64位有符号或无符号整数相乘得到的乘积需要128位来表示。x86-64指令集对128位数的操作提供有限的支持。下图描述的是支持产生两个64位数字的全 128位乘积以及整数除法的指令。
二、控制
目前为止我们只考虑了直线代码的行为,也就是一条命令接着一条命令顺序地执行。C语言中要求有条件的执行,根据测试数据结果来决定操作执行的顺序。机器代码提供两种低级机制来实现有条件的行为:测试数据值然后根据测试结果来改变控制流或者数据流。我们先来介绍与数据相关的控制流。
1、条件码:除了整数寄存器还有以单个位的条件码寄存器。它们描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行条件分支指令。当有算术与逻辑操作发生时,这些条件码寄存器当中的值会相应的发生变化。
CF:进位标志寄存器。最近的操作是最高位产生了进位。它可以记录无符号操作的溢出,当溢出时会被设为1。
ZF:零标志寄存器,最近的操作得出的结果为0。当计算结果为0时将会被设为1。
SF:符号标志寄存器,最近的操作得到的结果为负数。当计算结果为负数时会被设为1。
OF:溢出标志寄存器,最近的操作导致一个补码溢出(正溢出或负溢出)。当计算结果导致了补码溢出时,会被设为1。
从上面可以看出,CF和OF可以判断有符号和补码的溢出,ZF判断结果是否为0,SF判断结果的符号。这是底层机器的设定,而编程用的高级语言(比如C,Java)就是靠这四个寄存器,演化出各种各样的流程控制。
2、访问条件码
对于普通寄存器来讲,使用的时候一般是直接读取它的值,而对于条件码,通常不会直接读取。常用的有如下三种方法:可以根据条件码寄存器的某个组合,将一个字节设置为0或1。可以直接条件跳转到程序的某个其它的部分。可以有条件的传送数据。
对于第一种情况,下图描述的指令便是根据条件码的某个组合,将一个字节设置为0或1,这一整类指令称为 SET 指令,它们的区别就在与它们考虑的条件码的组合是什么,这些指令名字的不同后缀指明了它们所考虑的条件码的组合。这些指令的后缀表示不同的条件而不是操作数的大小。比如指令 setl 和 setb 表示 “小于时设置(set less)”和“低于时设置(set below)”,而不是“设置长字(set long word)”和“设置字节(set byte)”
3、跳转指令
正常情况下,指令会按照他们出现的顺序一条一条地执行。而跳转指令(jump)会导致执行切换到程序中一个全新的位置,我们可以理解为方法或者函数的调用。在汇编代码中,这些跳转的目的地通常用一个标号(label)指明。
指令 jmp .L1 会导致程序跳过movq指令,而从popq指令开始继续执行。在产生目标代码文件时,汇编器会确定带标号指令的地址。并将跳转目标编码为跳转指令的一部分。、
①直接跳转:跳转目标是作为指令的一部分编码的,比如上面的直接给一个标号作为跳转目标
②间接跳转:跳转目标是从寄存器或者存储器位置中读出的,比如 jmp *%eax 表示用寄存器 %eax 中的值作为跳转目标;再比如 jmp *(%eax) 以 %eax 中的值作为读地址,从存储器中读取跳转目标。
③其他条件跳转:根据条件码的某个组合,或者跳转,或者继续执行代码序列中的下一条指令。
4、循环
C 语言提供了多种循环结构,比如 do-while、while和for。汇编中没有相应的指令存在,我们可以用条件测试和跳转指令组合起来实现循环的效果。而大多数汇编器会根据一个循环的do-while 循环形式来产生循环代码,即其他的循环一般也会先转换成 do-while 形式,然后在编译成机器代码。
比如如下 do-while 循环:
网友评论