前面的章节中介绍了汇编里常用的数据类型,这章开始,就可以使用这些数据类型在汇编中进行数学运算了,下面先介绍汇编中和整数相关的数学运算...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第232页到第241页,对应原著第8章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为241/ 577)。

    前面的章节中介绍了汇编里常用的数据类型,这章开始,就可以使用这些数据类型在汇编中进行数学运算了,下面先介绍汇编中和整数相关的数学运算。

Integer Arithmetic 整数相关的数学运算:

    下面将分别介绍整数的加减乘除运算:

Addition 加法运算:

    将两个整数相加看起来是一个简单的过程,但是由于计算机中的整数是以二进制形式存储的,所以还必须理解二进制数相加的原理,才能灵活运用加法指令,下面就具体介绍下汇编中和加法运算相关的指令。

The ADD instruction 加法汇编指令:

    通过ADD指令就可以将两个整数进行加法运算,ADD指令的格式如下:

add source, destination

    source源操作数可以是一个立即数,或内存位置,或寄存器,destination目标操作数可以是内存位置或寄存器,不过要注意的是,source源操作数和destination目标操作数不可以同时都是内存位置,add指令会将源操作数和目标操作数里的值进行加法运算,运算结果存储到destination目标操作数中。

    add指令和其他很多指令一样,可以对8位,16位或32位的操作数进行运算,所以在使用时必须在指令后缀中指明需要操作的数据的二进制位大小,可以使用后缀字符 b 来表示8位的字节大小,w 来表示16位的字大小,或者 l 来表示32位的双字大小,如下面的例子:

addb $10, %al # adds the immediate value 10 to the 8-bit AL register
addw %bx, %cx # adds the 16-bit value of the BX register to the CX register
addl data, %eax # adds the 32-bit integer value at the data label to EAX
addl %eax, %eax # adds the value of the EAX register to itself

    上面代码中,第一条表示将立即数10和8位寄存器al里的值相加,得到结果后再存储到al中,其他几条代码就是对16位或32位数据的加法操作。

    下面通过一个完整的程式addtest1.s来演示add指令的用法:

# addtest1.s - An example of the ADD instruction
.section .data
data:
    .int 40
.section .text
.globl _start
_start:
    nop
    movl $0, %eax
    movl $0, %ebx
    movl $0, %ecx
    movb $20, %al
    addb $10, %al
    movsx %al, %eax
    movw $100, %cx
    addw %cx, %bx
    movsx %bx, %ebx
    movl $100, %edx
    addl %edx, %edx
    addl data, %eax
    addl %eax, data
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    开头的几条movl指令用于将eax之类的整个32位寄存器清零,主要是将高位清零(除了mov指令外,还可以使用后面要介绍的xor异或运算指令来清零),接下来的movb和addb指令先对eax寄存器的低8位al进行传值和加法运算,在计算出结果后,再由movsx指令将al里的有符号值以符合扩展的方式扩展到整个eax寄存器中(之前的章节中介绍过movsx指令,它可以确保eax里的有符号值等于al里的值),测试完addb指令后,又使用addw和addl指令来分别测试16位和32位数据的加法运算,下面是程序在调试器中的输出情况:

$ as -gstabs -o addtest1.o addtest1.s
$ ld -o addtest1 addtest1.o
$ gdb -q addtest1

Reading symbols from /home/zengl/Downloads/asm_example/math/addtest1...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file addtest1.s, line 9.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/math/addtest1

Breakpoint 1, _start () at addtest1.s:9
9        movl $0, %eax

(gdb) s

............................................ //省略N个s单步执行命令

(gdb) print $eax
$1 = 70
(gdb) print $ebx
$2 = 100
(gdb) print $ecx
$3 = 100
(gdb) print $edx
$4 = 200
(gdb) x/d &data
0x80490b4 <data>:    110
(gdb)

    上面单步执行完所有mov和add指令后,通过print命令将add加法指令运算后的寄存器里的值都输出显示出来,例如eax里的值先被清零,接着被初始化为20,再由addb指令将20加上10,让其变为30,最后再由addl指令将data内存里的40加入eax寄存器,所以eax就变为了70,其他寄存器的结果也都和预期的一致。

    前面我们介绍过,IA-32平台中的有符号整数是以two's complement即二进制补码的形式进行存储的,这种存储方式使得add指令既可以对正数进行加法运算,还可以对负数进行加法运算,下面的addtest2.s程式就是使用add指令对负数进行运算的例子:

# addtest2.s - An example of the ADD instruction and negative numbers
.section .data
data:
    .int -40
.section .text
.globl _start
_start:
    nop
    movl $-10, %eax
    movl $-200, %ebx
    movl $80, %ecx
    addl data, %eax
    addl %ecx, %eax
    addl %ebx, %eax
    addl %eax, data
    addl $210, data
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    下面是程序调试输出的结果:

$ as -gstabs -o addtest2.o addtest2.s
$ ld -o addtest2 addtest2.o
$ gdb -q addtest2

Reading symbols from /home/zengl/Downloads/asm_example/math/addtest2...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file addtest2.s, line 9.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/math/addtest2

Breakpoint 1, _start () at addtest2.s:9
9        movl $-10, %eax

(gdb) s

............................................ //省略N个s单步执行命令

(gdb) print $eax
$1 = -170
(gdb) print $ebx
$2 = -200
(gdb) print $ecx
$3 = 80
(gdb) x/d &data
0x80490ac <data>:    0
(gdb)

    上面程序中的eax一开始是-10,通过addl data, %eax指令将data里的-40加入eax从而变为-50,再由addl %ecx, %eax及addl %ebx, %eax将ecx里的80和ebx里的-200加入eax中,所以eax结果就变为-170 。其他寄存器的结果和data里的值也都和预期的一致。

    上面的例子说明add指令既可以处理无符号整数,又可以处理有符号整数。

Detecting a carry or overflow condition 检测carry进位标志及overflow溢出标志:

    由于add加法指令的destination目标操作数有个二进制位尺寸限制,add加法运算的结果有可能会超出destination目标操作数所能容纳的范围,所以在进行加法运算时,你需要时刻关注EFLAGS寄存器里的carry进位标志或overflow溢出标志。

    对于无符号整数而言,当运算结果超出了目标操作数的二进制位尺寸时,就会设置carry进位标志,例如对于8位的destination目标操作数而言,当add加法运算结果为256时,由于超过了8位最大值255的大小,此时就会设置carry进位标志。

    对于有符号整数而言,当结果大于目标操作数所能表示的最大的正数,或者小于所能表示的最小的负数时,就会设置overflow溢出标志,例如8位二进制所能表示的有符号整数范围为:-128 到 127,当加法运算结果大于127或小于-128时,就会设置溢出标志。

    当你发现进位或溢出标志被设置时,你就知道计算结果超出了目标操作数的范围,此时残留在目标操作数里的值其实是进位或溢出以后剩下的值,是无效的值。

    需要注意的是:进位和溢出标志是相对ADD指令的操作数尺寸来设置的,例如对于ADDB指令,当无符号数结果超过255时就会设置carry进位标志,而对于ADDW指令,由于其操作数是16位的大小,所以只有当无符号结果超过65535时,才会设置carry进位标志,overflow溢出标志也是同理。

    下面的addtest3.s程式就演示了针对无符号整数进行加法运算时,检测carry进位标志的例子:

# addtest3.s – An example of detecting a carry condition
.section .text
.globl _start
_start:
    nop
    movl $0, %ebx
    movb $190, %bl
    movb $100, %al
    addb %al, %bl
    jc over
    movl $1, %eax
    int $0x80
over:
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    上面的addtest3.s程式将AL和BL两个寄存器里的无符号值相加,结果存储在BL寄存器中,当结果超过8位寄存器最大值255时,就会设置carry进位标志,jc条件跳转指令就会根据该标志跳转到over位置处,所以当代码发生进位时,程序退出码就为0,当结果没发生进位时,退出码就是EBX中的加法结果。

    通过查看程序执行完后返回的退出码,我们就可以知道加法的运算结果是否发生了进位:

$ ./addtest3
$ echo $?

0
$

    上面的echo $?显示出addtest3的退出码为0,说明程序发生了进位,现在我们再将代码中AL和BL寄存器的初始值进行如下调整:

movb $190, %bl
movb $10, %al

    再次运行程序:

$ ./addtest3
$ echo $?

200
$

    这次输出的结果就是190和10的加法结果,说明程序没有发生进位,结果是个有效的值。

    小提示:对于无符号整数而言,检测carry进位标志是很重要的,尤其是当你不清楚运算结果是否会超出有效范围时,当然如果你确实知道计算结果不会超过范围的话,就可以不用检测carry标志。

    当对有符号整数进行计算时,carry进位标志就没什么用了,因为它不仅会在计算结果过大时被设置,同时还会在结果小于零时被设置,这对于无符号整数来说是很有用的,但是对于有符号整数来说就没什么意义了,有时候还会是种麻烦。

    所以在使用有符号整数时,就需要关注overflow溢出标志,当计算结果大于有效的正数时或小于有效的负数时,溢出标志就会被设置。

    下面的addtest4.s程式就演示了加法指令和溢出标志的用法:

# addtest4.s - An example of detecting an overflow condition
.section .data
output:
    .asciz "The result is %d\n"
.section .text
.globl _start
_start:
    movl $-1590876934, %ebx
    movl $-1259230143, %eax
    addl %eax, %ebx
    jo over
    pushl %ebx
    pushl $output
    call printf
    add $8, %esp
    pushl $0
    call exit
over:
    pushl $0
    pushl $output
    call printf
    add $8, %esp
    pushl $0
    call exit

    上面的代码通过对两个很大的负数进行加法运算,jo条件跳转指令会根据overflow溢出标志判断是否需要跳转,当溢出时就跳转到over标签处,然后由printf函数输出显示0,如果没有溢出则不跳转,而将add加法的正确结果给显示出来:

$ as -gstabs -o addtest4.o addtest4.s
$ ld -o addtest4 addtest4.o -dynamic-linker /lib/ld-linux.so.2 -lc

$ ./addtest4
The result is 0

    由于addtest4中使用了C的标准库函数printf,所以ld链接时添加了-lc参数来引入C标准库,同时通过-dynamic-linker参数来指定动态链接库的加载器(这些在前面的章节中都讲解过) ,由于两个负数都很大,相加后结果会小于32位有符号整数所能表示的最小负数,所以发生了溢出,显示结果就是零。

    下面我们将addtest4的代码做些调整,给EAX和EBX设置较小的负数:

movl $-190876934, %ebx
movl $-159230143, %eax

    再次进行测试:

$ ./addtest4
The result is -350107077
$

    可以看到由于没有发生溢出,就显示出了正确的计算结果。

    小提示:在对有符号整数进行加法运算时,检测overflow溢出标志是很重要的,它可以有效防止出现错误的结果,尤其是在你不能确定输入数据的大小时。

The ADC instruction (ADC指令):

    当你需要对非常大的整数进行加法操作时,例如当这个整数超过了32位整数范围时,那么你可以将这个大整数拆分为多个32位整数单元,然后对每部分依次进行加法运算,最低的32位部分完成加运算后,如果发生进位或溢出,则将进位的1带到第二部分一起参与第二部分的加法运算,然后是第三部分,第四部分等等,以此类推。

    下图演示了大整数相加的方式:


图1

    上图中最低的4字节数据元素相加后,carry进位标志必须被带到下一个数据元素参与加运算,以此类推,直到最后一个元素。

    如果你想手动完成上图的操作,那么你将不得不使用ADD和JC(或JO)的组合指令,通过JC或JO指令来检测是否发生进位或溢出,如果发生了进位或溢出,就跳转到某个代码处进行进位加一的操作,由于代码中存在跳转指令,所以这种手动使用组合指令的方式会让代码变得复杂和难以维护。

    庆幸的是,英特尔提供了一个简单的解决方案:你可以使用ADC指令,该指令在将两个有符号或无符号整数相加时,会自动根据carry标志的值来判断是否需要进位加一。如果一个大整数被拆分为多组数据元素后,第一组使用ADD指令后,其余的组就都可以使用ADC指令来进行加法运算了,这是因为ADC指令在进行完加法运算后,同样也会根据计算结果来设置carry进位标志和overflow溢出标志。

    下面是ADD和ADC指令组合使用的演示图:
 

图2

    ADC指令的格式如下:

adc source, destination

    source源操作数可以是一个立即数或8位,16位,32位的寄存器或内存位置里的值,destination目标操作数可以是8位,16位,32位的寄存器或内存位置里的值,但是源操作数和目标操作数不可以同时都是内存位置。和ADD指令一样,ADC指令也有尺寸后缀用于表明操作数的大小,例如后缀 b 表示8位的字节操作数,w 表示16位的字操作数,l 表示32位的双字操作数。

An ADC example ADC指令的例子:

    为了演示ADC指令的用法,下面就写个程式来举例说明,在该程式中有两个很大的整数:7,252,051,615 和 5,732,348,928 ,由于它们都超过了32位无符号整数的最大范围,所以我们需要使用两个32位的寄存器来存放它们,其中一个寄存器存放整数的低32位部分,另一个寄存器则存放整数的高32位部分。在加法运算时,低32位部分使用ADD指令相加,高32位部分则使用ADC指令来相加,下面是该程式的演示图:


图3

    如上图所示,7,252,051,615这个大整数将被存放在EAX和EBX组成的寄存器组中,EBX存放低32位部分,EAX存放高32位部分,5,732,348,928则存放在ECX和EDX组成的寄存器组中,EDX存放低32位,ECX存放高32位,低32位的EBX和EDX使用ADD指令相加,高32位的EAX和ECX则使用ADC指令相加,ADC相加时自动考虑了前面低32位相加时产生的carry进位标志。

    下面是完整的程式:

# adctest.s - An example of using the ADC instruction
.section .data
data1:
    .quad 7252051615
data2:
    .quad 5732348928
output:
    .asciz "The result is %qd\n"
.section .text
.globl _start
_start:
    nop
    movl data1, %ebx
    movl data1+4, %eax
    movl data2, %edx
    movl data2+4, %ecx
    addl %ebx, %edx
    adcl %eax, %ecx
    pushl %ecx
    pushl %edx
    push $output
    call printf
    addl $12, %esp
    pushl $0
    call exit

    上面代码中在data1和data2标签处使用.quad伪操作符定义了两个64位的大整数,还在output标签处使用.asciz定义了一个用于printf函数的格式化输出字符串,.asciz伪操作符定义的字符串会自动在末尾加上null字符(这个在前面的章节中也提到过):

data1:
    .quad 7252051615
data2:
    .quad 5732348928
output:
    .asciz "The result is %qd\n"

    上面的%qd是printf函数中用于输出显示64位有符号整数值的占位符(如果使用标准的%d则只能显示出32位的整数值)。

    代码中一开始使用movl指令将两个64位值依次加载到 EAX : EBX 和 ECX : EDX 寄存器组中:

movl data1, %ebx
movl data1+4, %eax
movl data2, %edx
movl data2+4, %ecx

    上面data1标签指向第一个大整数的低32位的起始地址,data1+4则指向高32位的起始地址,data2同理。

    在寄存器组初始化完毕后,就可以使用add和adc指令对两部分依次进行加法运算:

addl %ebx, %edx
adcl %eax, %ecx

    上面add和adc都是以 l 为后缀,表示操作数的大小是32位的。

    下面对代码进行汇编调试:

$ as -gstabs -o adctest.o adctest.s
$ ld -o adctest adctest.o -dynamic-linker /lib/ld-linux.so.2 -lc
$ gdb -q adctest

Reading symbols from /home/zengl/Downloads/asm_example/math/adctest...done.
(gdb) break *_start+1
Breakpoint 1 at 0x80481c1: file adctest.s, line 13.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/math/adctest

Breakpoint 1, _start () at adctest.s:13
13        movl data1, %ebx

(gdb) s
14        movl data1+4, %eax
(gdb) s
15        movl data2, %edx
(gdb) s
16        movl data2+4, %ecx
(gdb) s
17        addl %ebx, %edx
(gdb) info reg
eax            0x1    ....
ecx            0x1    ....
edx            0x55acb400    .....
ebx            0xb041869f     .....
.............................
(gdb)

    从上面的调试输出可以看到,在单步执行完movl指令后,eax到edx几个寄存器里的值和前面图3所示的初始值是一致的。

    再继续调试:

17        addl %ebx, %edx
(gdb) s
18        adcl %eax, %ecx
(gdb) s
19        pushl %ecx
(gdb) info reg
.............................
ecx            0x3    .....
edx            0x5ee3a9f    .....
.............................

    经过add和adc加法指令后,结果存储在ECX和EDX寄存器组中,和图3所示的计算结果也是一致的。

    最后看下程序执行后printf的显示结果:

$ ./adctest
The result is 12984400543
$

    可以看到输出结果和预期的一样,你还可以将代码中data1和data2两标签处定义的整数调整为负数,然后再进行测试,可以发现运算结果也是正确的,这是因为ADD和ADC指令既可以用于无符号整数,又可以用于有符号整数。

    限于篇幅,本篇就到这里,下一篇介绍减法等其他运算指令,转载请注明来源:www.zengl.com

    OK,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 基本数学运算 (二) 减法和乘法运算

上一篇: 汇编数据处理 (四) 数据处理结束篇

相关文章

IA-32平台(二)

全书结束篇 使用IA-32平台提供的高级功能 (四) SSE2、SSE3相关指令

汇编数据处理 (二)

Moving Data 汇编数据移动 (三) 数据交换指令

汇编里使用Linux系统调用 (一)

Moving Data 汇编数据移动 (二)