文章目录
- 3.5 算术和逻辑操作
- 3.5.1 加载有效地址
- 3.5.2 一元和二元操作
- 3.5.3 移位操作
- 3.5.4 讨论
- 3.5.5 特殊的算术操作
3.5 算术和逻辑操作
下图列出了一些双字整数操作,分为四类。
二元操作有两个操作数,而一元操作只有一个操作数。
描述这些操作数的符号与 3.4 节中使用的符号完全相同。除了 leal
以外,每条指令都有对应的对字(16位)和对字节操作的指令。把后缀“l
” 换成 “w
” 就是对字的操作,而换成 “b
” 就是对字节的操作了。例如,addl
对应有 addw
和 addb
。
3.5.1 加载有效地址
加载有效地址(Load Effective Address)指令 leal
实际上是 movl
指令的变形。它的指令形式是从存储器读数据到寄存器,但实际上它根本就没引用存储器。 它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数(如寄存器)。
在图3.7 中用 C 的地址操作符 &S
来说明这种计算。这条指令可以用来为后面的存储器引用产生指针。另外,它还可以用来简洁地描述普通的算术操作。例如,如果寄存器 %edx 的值为 x,那么指令 leal 7(%edx, %edx, 4), %eax
将设置寄存器 %eax 的值为 5x+7。注意:目的操作数必须是一个寄存器。
3.5.2 一元和二元操作
第二类操作是一元操作,只有一个操作数,既作源,也作目的。这个操作数可以是一个寄存器,也可以是一个存储器位置。比如说,指令 incl (%esp)
会使栈顶元素加1。这种语法让人想起 C 中的加1运算符(++
)和减1运算符(--
)。
第三类操作是二元操作,第二个操作数既是源又是目的。这种语法让人联想到 C 中像 +=
这样的赋值运算符。不过,要注意,源操作数是第一个,目的操作数是第二个,这是不可交换操作特有的。例如,指令 subl %eax, %edx
使寄存器 %edx
的值减去 %eax
中的值。第一个操作数可以是立即数、寄存器或是存储器位置。第二个操作数可以是寄存器或是存储器位置。不过,同 movl
指令一样,两个操作数不能同时都是存储器位置。
3.5.3 移位操作
最后一类是移位操作,先给出移位量,然后是待移位的值。可以进行算术和逻辑右移。移位量用单个字节编码,因为只允许进行 0 到 31位的移位。移位量可以是一个立即数,或者放在单字节寄存器元素 %cl
中。
如图3.7 所示,左移指令有两个名字:sall
和 shll
。两者的效果都一样,都是将右边填上0。右移指令不同,sarl
执行算术移位(填上符号位),而 shrl
执行逻辑移位(填上0)。
3.5.4 讨论
除了右移操作,所有的指令都不区分有符号和无符号操作数。对列出的所有指令来说,二进制补码运算和无符号运算有同样的位级行为。
下图给出了一个执行算术操作的函数示例,以及它的汇编代码。和前面一样,省略了栈的建立和完成部分。函数参数
x
、
y
x、y
x、y 和
z
z
z 分别存放在存储器中相对于寄存器 %ebp 中地址偏移8、12 和 16 的地方。
- 指令3实现表达式 x + y x+y x+y,一个操作数 y y y 来自寄存器 %eax(由指令 1 取出),而另一个直接来自存储器。
- 指令4和5执行计算
z
∗
48
z*48
z∗48,首先使
leal
指令对伸缩化的变址寻址模式的操作数执行计算: ( z + 2 z ) = 3 z (z+2z)=3z (z+2z)=3z,然后将这个值左移 4 位,以计算 2 4 ∗ 3 z = 48 z 2^4 * 3z = 48z 24∗3z=48z。C 编译器常常用加法和移位指令来完成常数因子的乘法。 - 指令6执行 AND 操作;
- 指令7执行最后的乘法。
- 指令8将返回值移到寄存器 %eax。
在上图3.8的汇编代码中,寄存器 %eax 中的值先后对应于程序值 y y y、 t 1 t1 t1、 t 3 t3 t3 和 t 4 t4 t4(作为返回值)。通常,编译器产生的代码中,会用一个寄存器存放多个程序值,还会在寄存器之间传送程序值。
3.5.5 特殊的算术操作
下图描述的是支持产生两个 32 位数字的全 64 位乘积以及整数除法的指令。
图3.7中列出的 imull
指令称为 “双操作数” 乘法指令。它从两个 32 位操作数产生一个 32 位乘积。当将乘积截取为32位时,无符号乘和二进制补码乘的位级是一样的。
IA32 还提供了两个不同的“单操作数”乘法指令,以计算两个 32 位值的全 64 位乘积——一个是无符号数乘法(mull
),而另一个是二进制补码乘法(imull
)。这两条指令都要求一个参数必须在寄存器 %eax 中,而另一个是作为指令的源操作数给出的。然后乘积存放在寄存器 %edx(高32位)和 %eax(低32位)中。注意,虽然 imull
这个名字可以用于两个不同的乘法操作,但是汇编器能够通过计算操作数的数目,分辨出是想用哪条指令。
举个例子,假设有符号数
x
x
x 和
y
y
y 存储在相对于 %ebp 偏移量为 8 和 12 的位置,希望将它们的全 64 位乘积作为 8 个字节存放在栈顶。代码如下:
注意将两个寄存器入栈的顺序,对小端法(little-endian)机器来说是对的,在这种机器中栈是向低地址方向增长的(也就是说,乘积的低位字节的地址比高位字节的地址小)。
前面的算术运算表(图3.7)没有列出除法或模(modulus)操作。单操作数除法类似于单操作数乘法。有符号除法指令 idivl
将寄存器 %edx(高32位)和 %eax (低32位)中的 64 位数作为被除数,除数是作为指令的操作数给出的。指令将商存储在寄存器 %eax 中,将余数存储在寄存器 %edx 中。cltd
指令可以用来根据寄存器 %eax 中存放的 32 位的值形成 64 位被除数,这条指令将 %eax 符号扩展到 %edx。
如下例子,假设有符号数 x x x 和 y y y 存储在相对于 %ebp 偏移量为 8 和 12 的位置,想要将 x / y x/y x/y 和 x % y x \%y x%y 存储到栈中。代码如下:
divl
指令执行无符号除法。通常会事先将寄存器 %edx 设置为 0。