爱技术 & 爱分享
爱蛋蛋 & 爱生活

基本数学功能

整数运算

加法

  • ADD指令

ADD指令用于把两个整数相加。ADD指令的格式如下:

add source, destination

其中source可以是立即值、内存位置或者寄存器。destination参数可以是寄存器或者内存位置中存储的值(但是不能同时使用内存位置作为源和目标)。加法的结果存放在目标位置。

ADD指令可以将8 位、16位或者32位值相加。和其他GNU汇编器指令一样,必须通过在ADD 助记符的结尾添加b(用于字节)、w (用于字)或者l (用于双字)来指定操作数的长度。

如果没有使用整个32位寄存器,就要确保使用零填充目标寄存器,使寄存器的高位中没有内容,可以使用XOR指令轻易地完成这一操。确保目标寄存器被填充为零后,可以执行不同的ADD指令。

  • 检测进位或者溢出情况

当整数相加时,总是应该注意EFLAGS寄存器,以便确保操作过程中不会发生奇怪的事情。对于无符号整数,当二进制加法造成进位情况时(即结果大于允许的最大值),进位标志(carry flag) 就会被设置为1。对于带符号整数,当出现溢出情况时(结果值小于允许的最小负值, 或者大于允许的最大正值),溢出标志(overflow flag) 就会被设置为1。当这些标志被设置为1时,就知道目标操作数的长度太小,不能保存加法的结果值。这个值将是答案的”溢出”之后的部分。

进位和溢出标志的设置与加法中使用的数据长度相关联。例如,在ADDB 指令中,如果结果超过255 , 就会把进位标志设置为1, 但是在ADDW指令中,除非结果超过65 535 , 否则是不会设置进位标志的。

处理带符号整数时,进位标志是没有用处的。不仅结果值过大时会设置它,而且只要结果值小于零,也会设置它。虽然它对无符号整数有所帮助,但是对于带符号整数,它是没有意义的(甚至会带来麻烦) 。

替换的做法是,在使用带符号整数时,必须关注溢出标志,当结果溢出正值或负值界限时,这个标志会被设置为1 。

  • ADC指今

如果必须处理非常大的、不能存放到双字数据长度(ADD指令可以使用的最大长度)中的带符号或者无符号整数,可以把值分割为多个双字数据元素,井且对每个元素执行独立的加法操作。

为了正确地完成这个工作,必须检测每个加法操作的进位标志。如果进位标志被设览为1,就必须进位到下一对相加的数据元素,当最低值的数据元素对相加时,进位标志位必须被进位到下一个值数据元素对,依此类推,直到最高的数据元素对。

手动地完成这一工作,必须使用ADD和JC (或者JO) 指令的组合,生成复杂的指令组来确定什么时候产生了进位(或者溢出)情况,并且确定什么时候需要把进位(或者溢出)添加到
下一次加法操作中。

对此 Intel提供了简单的解决方案。可以使用ADC指令执行两个无符号或者带符号整数值的加法,并且把前一个ADD指令产生的进位标志的值包含在其中。为了执行多组字节的加法操作,可以把多个ADC指令链接在一起,因为ADC指令也按照操作结果正确地设置进位和溢出标志。

ADC指令的格式如下:

adc source, destination

其中source可以是立即值或者8 位、16位或者32位寄存器或内存位置值, destination可以是8位、16位或者32位寄存器或内存位置值。(和ADD指令类似, source和destination不能同时是内存位置。)还有,和ADD指令一样, GNU汇编器要求在助记符中用附加的字符来表明操作数的长度(b,w或者l) 。

减法

  • SUB指令

减法的基本形式是SUB 指令。和ADD 指令一样,SUB指令可以用于无符号和带符号整数。SUB 指令的格式如下:

sub source, destination

其中从destination的值中减去source的值,结果存储在destination操作数的位置。源操作数和目标操作数可以是8位、16位或者32位寄存器或存储在内存中的值(但是再次强调,它们不能同时是内存位置)。源值也可以是立即数值。

和ADD指令一样,GNU汇编器需要在助记符末尾添加长度字符。长度字符仍然是通常使用的字符(b用于字节,w用于字,l用于双字)。

记住GNU 汇镐器的SUB 指令中操作数的顺序,这非常重要。使用Intel 语法会产生错误的结果。

和SUB 指令关系密切的是NEG 指令。它生成值的补码。这和使用SUB指令从零中减去这个值是相同的,但是更快。

  • 减法操作中的进位和溢出

和ADD指令类似,执行减法操作之后,SUB指令会修改EFLAGS寄存器的几个位。但是在减法操作中,进位和溢出的概念是不同的。

另外,当加法结果对于保存操作数的数据长度的正界限过大时,会把进位标志设仅为1 。显然,当减法结果超过数据长度的负界限时,会出现问题。

使用进位标志确定无符号整数的减法产生负数结果的情况。和带符号整数加法的情况一样,如果执行带符号整数的减法,进位标志是没有用处的,因为结果常常可能是负值。替换的做法是,必须依靠溢出标志来判断到达了数据长度界限的情况。

  • SBB指令

和加法操作一样,可以使用进位情况帮助执行大的无符号整数值的减法操作。SBB指令在多字节减法操作中利用进位和溢出标志实现跨越数据边界的借位特性。

SBB指令的格式如下:

sbb source, destination

其中进位位被添加到source值,然后从destination值中减去source值得到结果。结果存储在destination位置中。照例,源和目标值可以是8位、16位或者32位寄存器或内存中的值,当然,不能同时使用内存位置作为源和目标值。

SBB指令最常用于从前一条SUB指令"挖出“进位标志。当前一条SUB指令被执行并且造成进位时,进位位被SBB指令“借走”以便继续进行下一个数据对的减法。

递增递减

INC和DEC指令用于对无符号整数值进行递增(INC)和递减(DEC)操作。INC 和DEC指令不会影响进位标志,所以可以递增或者递减计数器的值,井且不会影响程序循环中涉及进位标志的任何其他加法或者减法操作。这两个指令的格式如下:

dec destination
inc destination

其中destination 可以是8位、16位或者32位寄存器,或者内存中的值。记住INC和DEC指令主要用于无符号整数。如果对设置为0的32位寄存器进行递减,新的值将是OxFFFFFFFF, 它看上去就像带符号整数-1,但是它被当作无符号整数4 294 967 295 处理(没有设置正确的标志)。如果把它们用于带符号整数,就要小心符号的变化。

乘法

  • 使用MUL进行无符号整数乘法

MUL指令用于两个无符号整数相乘。它的格式可能和预期的有所不同。MUL指令的格式如下:

mul source

其中source可以是8位、16位或者32位寄存器或内存值。目标操作数是隐含的。

首先,目标位置总是使用EAX 寄存器的某种形式,这取决于源操作数的长度。因此,根据源操作数的值的长度,乘法操作中使用的另一个操作数必须存放在AL、AX或者EAX寄存器中。

由于乘法可能产生很大的值,所以MUL指令的目标位置必须是源操作数的两倍长度。

如果源值是8位,那么目标操作数就是AX寄存器,因为结果是16位。

当和16位源操作数相乘时,EAX寄存器不被用于保存32位结果。为了向下兼容老式的处理器,Intel 使用DX: AX寄存器对保存32位乘法结果值(这一格式源自16位处理器的年代)。结果的高位字存储在DX寄存器中,低位字存储在AX寄存器中。

对于32位源值,目标位置使用64位EDX:EAX寄存器对,高位双字存储在EDX 寄存器中,低位双字在EAX寄存器中。当使用MUL的16位或者32位版本时,如果在EDX (或者DX) 寄存器中存储着数据,那么一定要把数据保存到其他位置。

源操作数长度 目标操作数 目标位置
8 位 AL AX
16位 AX DX:AX
32位 EAX EDX:AX

另外,记住这一点很重要:使用GNU汇编器时,必须在助记符的结尾加上正确的长度字符。

  • 使用IMUL进行带符号整数乘法

MUL指令只能用于无符号整数,而IMUL指令可以用于带符号和无符号整数,但是必须小心结果不使用目标的最高有效位。对干较大的值,IMUL指令只对带符号整数是合法的。为了应付比较复杂的情况,lMUL指令有3 种不同的指令格式。

IMUL指令的第一种格式使用一个操作数,其行为和MUL指令完全一样

imul source

source操作数可以是8 位、16位或者32位寄存器或内存中的值,它与位于AL 、AX 或者EAX寄存器(取决于源操作数的长度)中的隐含操作数相乘。然后,结果被存放到AX 寄存器、DX:AX寄存器对或者EDX:EAX寄存器对中。

IMUL指令的第二种格式允许指定EAX寄存器之外的目标操作数

imul source, destination

其中source可以是16位或者32位寄存器或内存中的值,destination必须是16位或者32位通用寄存器。这种格式允许指定把乘法操作的结果存放到哪个位置(而不是强制使用AX和DX寄存器)。

这种格式的缺陷在于乘法操作的结果被限制为单一目标寄存器的长度(非64位结果)。使用这种格式时必须非常小心,不要溢出目标寄存器。

IMUL指令的第三种格式允许指定3 个操作数

imul multiplier, source, destination

其中multiplier是一个立即值, source是16位或者32位寄存器或内存中的值, destination 必须是通用寄存器。这种格式允许执行一个值(source) 和一个带符号整数(multiplier) 的快速乘法操作,把结果存储到通用寄存器(destination) 中。

和MUL指令一样, 在GNU 汇偏器中使用IMUL助记符,要记住在IMUL指令的结尾添加长度字符,以便指定源和目标操作数的长度。

  • 检查溢出

当使用带符号整数和IMUL指令时,记住这一点很重要:总是要检查结果中的溢出。完成这个工作的一种方式是使用JO指令检查溢出标志(另一种方式是检查进位标志)。

除法

  • 无符号除法

DIV指令用于无符号整数的除法操作。DIV指令的格式如下:

div divisor

其中divisor (除数)是隐含的被除数要除以的值,它可以是8 位、16位或者32位寄存器或内存中的值。在执行DIV指令之前,被除数必须已经存储到了AX寄存器(对于16 位值)、DX:AX寄存器对(对于32位值)或者EDX:EAX寄存器对(对于64位值)。

允许的除数的最大值取决于被除数的长度。对于16位被除数,除数只能是8位;对于32位被除数,除数只能是16位;对于64位被除数,除数只能是32 位。

除法操作的结果是两个单独的数字:商和余数。这两个值都存储在被除数值使用的相同寄存器中。下表列出了其设置的情况。

被除数 被除数长度 余数
AX 16位 AL AH
DX:AX 32位 AX DX
EDX:EAX 64位 EAX EDX

这就是说,当除法操作完成时,会丢失被除数,所以要确保这不是这个值的唯一拷贝(除非在除法操作之后就不需要被除数的值了)。还要记住,结果会改变DX或者EDX 寄存器的值,所以也要小心其中存储的内容。

  • 带符号除法

IDIV指令的使用方式和DIV指令完全一样,但是它用于带符号整数的除法操作。它也使用隐含的被除数,被除数位于AX寄存器、DX:AX寄存器对或者EDX:EAX 寄存器对中。

和IMUL指令不同, IDIV指令只有一种格式,它指定除法操作中使用的除数:

idiv divisor

同样,其中的divisor可以是8 位、16位或者32位寄存器或内存中的值。IDIV 指令把结果返回和DIV 指令相同的寄存器中,并且商和余数的格式也是相同的(除了结果是带符号整数之外)。

对于带符号整数的除法,余数的符号总是与被除数的符号相同。

关于带符号除法,另一件要记住的事情是被除数的长度。因为它必须是除数长度的两倍,所以有时候必须把整数值扩展为适当的数据长度。使用符号扩展指令(比如MOVSX) 把除法操作的被除数扩展为适当的数据长度是很重要的。扩展操作的失败将导致错误的被除数值,并且在结果中产生错误。

  • 检查除法错误

整数除法最大的问题在于检查产生错误条件的情况,比如发生除以零的情况,或者商(或余数)溢出目标寄存器。

发生错误时,系统会产生中断,这会在Linux 系统中产生一个错误 Floating point exception

在程序中执行DIV 和IDIV 指令之前,检查除数和袚除数的值是程序员的责任。不进行这样的检查可能导致应用程序中出现错误行为。

移位指令

乘法和除法是处理器上最为耗费时间的两种指令。但是,可以运用一些技巧帮助加快应用程序的执行速度。移位指令提供了执行基于2的乘方的乘法和除法操作的快速和容易的方式。数据元素中的移位操作比执行二进制乘法操作要快得多,可以使用这种方法提高数学运算密集型程序的性能。

移位乘法

为了使整数乘以2 的乘方,必须把值向左移位。可以使用两个指令使整数值向左移位一SAL (向左箕术移位)和SHL (向左逻辑移位)。这两个指令执行相同的操作,并且是可以互换的。它们有3 种不同格式:

sal destination
sal %cl , destination
sal shifter, destination

第一种格式把destination的值向左移l 位,这等同于使值乘以2。

第二种格式把destination的值向左移动CL寄存器中指定的位数。

最后一个版本把destination的值向左移动shifter值指定的位数。

在所有的格式中,目标操作数可以是8 位、16位或者32位寄存器或内存中的值。和以往一样, GNU汇编器需要在助记符的结尾附加上一个字符,用于指出目标值的长度。

可以对带符号和无符号整数执行向左移位指令。移位造成的空位用零填充。移位造成的超出数据长度的任何位首先被存放在进位标志中,然后在下一次移位操作中被丢弃。因此,如果值的最高有效位为1′ 经过2次向左移位操作之后,最高有效位就会从进位标志中丢弃。

移位除法

通过移位进行除法操作涉及把二进制值向右移位。但是,当把整数值向右移位时,必须要注意整数的符号。

对于无符号整数,向右移位产生的空位可以被填充为零,而且不会有任何问题。不幸的是,对于带符号整数,使用零填充高位部分会对负数产生有害的影响。

为了解决这个问题,有两个向右移位指令。SHR指令清空移位造成的空位,所以它只能用于对无符号整数进行移位操作。SAR指令根据整数的符号位,要么清空,要么设置移位造成的空位。对于负数,空位被设置为1′ 但是对于正数,它们被清空为0 。

和向左移位指令一样,向右移位指令把位移出数据元素。移出数据元素的任何位(最低有效位)首先被移动到进位标志,然后移出去(丢弃)。

循环移位

和移位指令关系密切的指令是循环移位指令。循环移位指令执行的功能和移位指令一样,只不过溢出位被存放回值的另一端, 而不是被丢弃。例如,字节值的向左循环移位操作获得第7位中的值,并且把它存放到第0位的位置,其他每个位的位置向左移动1 位。下表列出可以使用的各种循环移位指令:

指令 描述
ROL 向左循环移位
ROR 向右循环移位
RCL 向左循环移位, 并且包含进位标志
RCR 向右循环移位, 井且包含进位标忐

最后两条指令使用进位标志作为附加位的位置, 来支持9位移位。循环移位指令的格式和移位指令相同, 提供3 种选择:

  • 单一操作数: 按照指定的方向把它移动1 位。
  • 2个操作数:指定循环次数的%cl寄存器和目标操作数。
  • 2个操作数:指定循坏次数的立即值和目标操作数。

十进制操作

不打包BCD的运算

二进制编码的十进制(Binary Coded Decimal, BCD) 格式是用于处理人可读的数字的常见方法,在处理器中可以快速地处理这种格式。虽然很多高级BCD处理操作位于FPU 中,但是核心处理器包含一些简化的指令,用于使用BCD值执行运算。

不打包的BCD值在一个字节中包含单个十进制位(0 到9) 。多个十进制位存储在多个字节中, 每个十进制位占用1 个字节。当应用程序需要对不打包的BCD值执行数学操作时, 应用程序假设结果也应该按照不打包BCD格式存储。幸运的是, IA-32平台提供了专门的指令用于从一般数学操作生成不打包BCD 值。

用于把二进制运算结果转换为不打包BCD格式的指令有4条:

  • AAA: 调整加法操作的结果。
  • AAS: 调整减法操作的结果。
  • AAM: 调整乘法操作的结果。
  • AAD: 准备除法操作的被除数。

这些指令必须和一般的无符号整数指令ADD 、ADC 、SUB 、SBB 、MUL和DIV组合在一起使用。AAA 、AAS 和AAM指令在它们各自的操作之后使用,把二进制结果转换为不打包BCD格式。

AAD指令有些不同,在DIV指令之前使用它,用于准备被除数以便生成不打包BCD结果。

这些指令都使用一个隐含的操作数一-AL寄存器。AAA、AAS和AAM指令假设前一个操作的结果存放在AL寄存器中,并且把这个值转换为不打包BCD格式。AAD指令假设被除数以不打包BCD格式存放在AX寄存器中,并且把它转换为DIV指令要处理的二进制格式。结果是正确的一个不打包BCD值、AL寄存器中的商和AH寄存器中的余数(按照不打包BCD格式)。当处理多字节的不打包BCD值时,必须使用进位和溢出标志才能确保计算出正确的值。

AAA 、AAS 和AAM指令都把AH寄存器和进位标志一起使用来表明何时需要进位操作。

打包BCD的运算

处理打包BCD值时, 可用的指令只有2条:

  • DAA: 调整ADD或者ADC指令的结果。
  • DAS: 调整SUB或者SBB指令的结果。

这些指令执行的功能和AAA 以及AAS 指令相同,但是操作对象是打包BCD值。它们也使用位于AL寄存器中的隐含操作数,并且把转换结果存放在AL寄存器中,把进位位存放在AH 寄存器和辅助进位标志位中。

逻辑操作

布尔逻辑

处理二进制数字时,具有可用的标准布尔逻辑功能是很方便的。提供的布尔逻辑操作如下:

  • AND
  • NOT
  • OR
  • XOR

AND、OR和XOR指令使用相同的格式:

and source, destination

其中source可以是8 位、16位或者32位立即值;寄存器或内存中的值,destination 可以是8 位、16位或者32 位寄存器或内存中的值(但是和以往一样,不能同时使用内存值作为源和目标)。

NOT指令使用单一操作数,它既是源值,也是目标结果的位置。

布尔逻辑功能对源和目标执行按位操作。就是说,使用指定的逻辑功能,按照顺序对数据元素的每个位进行单独比较。

清空寄存器的最高效的方式是使用OR指令对寄存器和它本身进行异或操作。当和本身进行XOR操作时, 每个设置为1 的位就变为0 , 每个设置
为0的位也变为0 。这确保寄存器的所有位都被设置为0, 这比使用MOV指令加载立即值0的方式要快。

位测试

有时候必须确定值内的单一位是否被设置为1了。这一功能最常见的用途是检查EFLAGS 寄存器标志的值。不需要试图比较整个寄存器的值,好的方式是检测单一标志的值。

完成这个工作的一种方式是使用AND指令,把EFLAGS寄存器和已知位值进行比较,挑选出希望检查的一个位或者多个位。但是,程序员也许不希望改变包含EFLAGS位的寄存器的值。

为了解决这个问题, IA-32平台提供了TEST指令。TEST指令在8位、16位或32位值之间执行按位逻辑AND操作,并且相应地设置符号、零和奇偶校验标志,而且不修改目标值。

TEST指令的格式和AND指令相同。尽管没有数据写入目标位置,但是仍然必须指定任意立即值作为源值。这类似于CMP指令和SUB指令的工作方式一样,但是它不会把结果存储到任何位置。

TEST指令最常见的用途是检查EFLAGS寄存器中的标志。例如,如果希望使用CPUID指令检查处理器的属性,程序员首先应该确保处理器支持CPUID指令。EFLAGS 寄存器中的ID标志(第21 位)用于确定处理器是否支持CPUID指令。如果可以修改ID标志,则说明CPUID指令是可用的。为了进行测试, 必须获得EFLAGS寄存器,反转ID标志位,然后再测试这个位,查看它是否真的改变了。

上述笔记内容学习自 AT&T Professional Assembly Language — Chapter VIII

赞(0)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。墨影 » 基本数学功能