1、数据传送指令
最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。操作数表示的通用性使得一条简单的数据传送指令能够完成在许多机器中要好几条不同指令才能完成的功能。我们会介绍多种不同的数据传送指令,它们或者源和目的类型不同,或者执行的转换不同,或者具有的一些副作用不同。在我们的讲述中,把许多不同的指令划分成指令类,每一类中的指令执行相同的操作,只不过操作数大小不同。
图3-4列出的是最简单形式的数据传送指令一MOV 类。这此指令把数据从源位置
复制到目的位置,不做任何变化。MOV类由四条指令组成:movb、movw、 movl和movq,这些指令都执行同样的操作:主要区别在于它们操作的数据大小不同:分别是1、2、4和8字节。
数据传送指令实例:
b)汇编代码
当过程开始执行时,过程参数xp和y分别存储在寄存器%rdi和%rsi中。然后,指令2从内存中读出x,把它存放到寄存器%rax中,直接实现了C程序中的操作x=*xp。稍后,用寄存器%rax从这个函数返回一个值,因而返回值就是x。指令3将y写人到寄存器%rdi中的xp指向的内存位置,直接实现了操作*xp=y。这个例子说明了如何用MOV指令从内存中读值到寄存器(第2行),如何从寄存器写到内存(第3行)。
关于这段汇编代码有两点值得注意。首先,我们看到C语言中所谓的“指针”其实就是地址。间接引用指针就是将该指针放在一一个寄存器中,然后在内存引用中使用这个寄存器。其次,像x这样的局部变量通常是保存在寄存器中,而不是内存中。访问寄存器比访问内存要快得多。
2、压入和弹出栈数据
pushq指令将一个四字值压入栈中,首先要将栈指针减8(64位),然后将值写到新的栈顶地址。
popq指令弹出一个四字操作包括从栈顶位置读取数据,然后将站指针加8。
3、算数和逻辑操作
图3-10
这些操作被分为四组:加载有效地址、一元操作、二元操作和移位。二元操作有两个操作数而一元操作有一个操作数。
(1)加载有效地址
加载有效地址(load effective address)指令leaq 实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读人数据,而是将有效地址写人到目的操作数。在图3-10中我们用C语言的地址操作符&S说明这种计算。这条指令可以为后面的内存引用产生指针。另外,它还可以简洁地描述普通的算术操作。例如,如果寄存器%rdx的值为x,那么指令leaq 7 (%rdx, %rdx,4), rax将设置寄存器%rax的值为5x+7。编译器经常发现leaq的一些灵活用法, 根本就与有效地址计算无关。目的操作数必须是一个寄存器。
(2)一元操作和二元操作
第二组中的操作是一元操作, 只有一个操作数,既是源又是目的。这个操作数可以是
一个寄存器,也可以是一个内存位置。比如说,指令incq(%rsp)会使栈顶的8字节元素
加1。这种语法让人想起C语言中的加1运算符(+ +)和减1运算符(-- )。
第三组是二元操作,其中,第二个操作数既是源又是目的。这种语法让人想起C语言中的赋值运算符,例如x-=y。不过,要注意,源操作数是第一个,目的操作数是第二个,对于不可交换操作来说,这看上去很奇特。例如,指令subq %rax, %rdx使寄存器%rdx的值减去%rax中的值。(将指令解读成“从%rdx中减去%rax"会有所帮助。)第一个操作数可以是立即数、寄存器或是内存位置。第二个操作数可以是寄存器或是内存位置。注意,当第二个操作数为内存地址时,处理器必须从内存读出值,执行操作,再把结果写回内存。
(3)移位操作
最后一组是移位操作,先给出移位量,然后第二项给出的是要移位的数。可以进行算术和逻辑右移。移位量可以是一个立即数,或者放在单字节寄存器%cl中。(这些指令很特别,因为只允许以这个特定的寄存器作为操作数。)原则上来说,1个字节的移位量使得移位量的编码范围可以达到255。x86-64 中,移位操作对w位长的数据值进行操作,移位量是由%c1寄存器的低m位决定的。例如当寄存器%cl的十六进制值为0xFF时,指令salb会移7位,salw会移15位,sall会移31位,而salq会移63位。
如图3-10所示,左移指令有两个名字: SAL和SHL。两者的效果是一样的,都是将
右边填上0。右移指令不同,SAR执行算术移位(填上符号位),而SHR执行逻辑移位(填
上0)。移位操作的目的操作数可以是一个寄存器或是一个内存位置。图3-10中用>>A(算
术)和>>l(逻辑)来表示这两种不同的右移运算。
(4)特殊的算数操作
4、控制
到目前为止,我们只考虑了直线代码的行为,也就是指令一条接着一条顺序地执行。C语言中的某些结构,比如条件语句、循环语句和分支语句,要求有条件的执行,根据数据测试的结果来决定操作执行的顺序。机器代码提供两种基本的低级机制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流。
与数据相关的控制流是实现有条件行为的更一般和更常见的方法,所以我们先来介绍它。通常,C语言中的语句和机器代码中的指令都是按照它们在程序中出现的次序,顺序执行的。用jump指令可以改变一组机器代码指令的执行顺序,jump 指令指定控制应该被传递到程序的某个其他部分,可能是依赖于某个测试的结果。编译器必须产生构建在这种低级机制基础之上的指令序列,来实现C语言的控制结构。
本文会先涉及实现条件操作的两种方式,然后描述表达循环和switch语句的方法。
(1)跳转指令
指令rep和repz有什么用
repz 是rep的同义名,而retq是ret的同义名。查阅Intel和 AMD有关rep的文档,我们发现它通常用来实现重复的字符串操作。 建议用rep后面跟ret的组合来避免使ret指令成为条件跳转指令的目标。如果没有rep指令,当分支不跳转时,jg指令会继续到ret指令。根据AMD的说法,当ret指令通过跳转指令到达时,处理器不能正确预测ret指令的目的。这里的rep指令就是作为一种空操作,因此作为跳转目的插入它,除了能使代码在AMD上运行得更快之外,不会改变代码的其他行为。
(2)用条件控制来实现条件分支
计算机科学中,条件控制语句是实现条件分支的关键工具。条件控制语句允许程序根据特定的条件来决定执行哪一段代码,从而使得程序能够根据输入、程序状态或计算结果等条件来灵活地调整其行为。常见的条件控制语句包括if语句、if...else语句、if...elif...else语句(在某些语言中称为if...elif...else或类似的结构,如Python)、switch语句(在某些语言中如C、C++、Java、Kotlin等)以及循环控制语句中的条件表达式(虽然循环控制语句本身不是直接用于条件控制的,但它们内部使用条件来控制循环的继续或终止)。
(3)用条件传送来实现条件分支
计算机科学中使用条件传送来实现条件分支的基本原理是通过计算一个条件操作的两种结果,然后根据条件是否满足从中选取一个。这种方法避免了直接使用条件跳转指令,从而减少了对处理器流水线的干扰,提高了程序的执行效率。
条件传送实现条件分支的具体步骤和原理
-
计算两种结果:首先,计算条件操作的两个可能结果。例如,在计算两个数的差的绝对值时,可以分别计算
x - y
和y - x
的结果。 -
根据条件选择结果:然后,根据条件的满足情况选择其中一个结果。例如,如果
x < y
,则选择y - x
的结果;否则,选择x - y
的结果。