上一篇介绍了加法指令,接下来介绍减法指令 汇编中使用SUB作为减法指令,SUB和ADD指令一样,既可以用于无符号整数又可以用于有符号整数...

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

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

Subtraction 减法运算:

    上一篇介绍了加法指令,接下来介绍减法和乘法指令。

The SUB instruction 减法指令:

    汇编中使用SUB作为减法指令,SUB和ADD指令一样,既可以用于无符号整数又可以用于有符号整数,该指令的格式如下:

sub source, destination

    在进行sub指令时,会使用destination目标操作数减去source源操作数,结果存储在destination目标操作数中。source源操作数和destination目标操作数可以是8位,16位,32位的寄存器或存储在某内存位置里的值,当然源和目标操作数不能同时都是内存位置,另外source源操作数还可以是一个立即数。

    和ADD指令一样,SUB指令在使用时需要在助记符后面添加操作数的大小后缀:用b来表示8位字节大小,w来表示16位字大小,以及l来表示32位的双字大小。

    下面的subtest1.s程式就演示了SUB指令的用法:

# subtest1.s - An example of the SUB instruction
.section .data
data:
    .int 40
.section .text
.globl _start
_start:
    nop
    movl $0, %eax
    movl $0, %ebx
    movl $0, %ecx
    movb $20, %al
    subb $10, %al
    movsx %al, %eax
    movw $100, %cx
    subw %cx, %bx
    movsx %bx, %ebx
    movl $100, %edx
    subl %eax, %edx
    subl data, %eax
    subl %eax, data
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    subtest1.s程式通过立即数,寄存器和内存位置来测试了基本的减法指令,下面我们通过调试来主要看下最后一条SUB指令执行前后,EAX寄存器和data内存位置里的值的变化:

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

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

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

(gdb) break 21
Breakpoint 2 at 0x80480a2: file subtest1.s, line 21.
(gdb) c
Continuing.

Breakpoint 2, _start () at subtest1.s:21
21        subl %eax, data

(gdb) print $eax
$1 = -30
(gdb) x/wd &data
0x80490b4 <data>:    40
(gdb) s
22        movl $1, %eax
(gdb) x/wd &data
0x80490b4 <data>:    70
(gdb)

    程序在执行subl %eax, data指令之前,eax里的值是-30,data内存中的值是40,执行sub指令时,使用data里的40减去eax里的-30,得到结果70,并存储到data内存中,测试的输出结果和预期的一致。

    注意:一定要记住SUB减法指令的顺序是目标操作数减去源操作数,由于GNU汇编器和Intel汇编语法的操作数位置刚好相反,所以有时候不注意的话,很容易混淆。

    SUB指令的近亲是NEG指令,该指令会使用目标操作数的补码来替换掉目标操作数的值,相当于用0减去目标操作数,例如目标操作数原来值为 -30,那么经过NEG指令后就会变为30 。

Carry and overflow with subtraction 减法运算中的carry标志和overflow标志:

    和ADD指令类似,SUB指令也会修改EFLAGS寄存器的标志位。

    无论加法运算还是减法运算,只要计算结果不是一个有效的无符号整数时,就会设置carry标志,例如对于8位的无符号整数而言,只有0到255范围的值是有效的无符号整数,那么当计算结果为256时,或者为-1时,由于都不在0到255范围内,所以都属于无效的8位无符号整数,此时就会设置carry标志,在加法运算中,结果大于有效值范围而设置carry标志的情况较多些,减法运算中,结果小于0而设置carry标志的情况较多些。

    下面的subtest2.s程式就演示了减法运算中检测carry标志的例子:

# subtest2.s - An example of a subtraction carry
.section .text
.globl _start
_start:
    nop
    movl $5, %eax
    movl $2, %ebx
    subl %eax, %ebx
    jc under
    movl $1, %eax
    int $0x80
under:
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    上面代码中先将EAX设置为5,EBX设置为2,接着使用subl %eax, %ebx指令将EBX的值2减去EAX的值5,结果存储在EBX中,如下所示:

(gdb) print $ebx
$1 = -3
(gdb)

    结果-3不是有效的无符号整数,所以会设置carry标志,接着jc指令就会根据该标志跳转到under标签处来退出程序:

$ ./subtest2
$ echo $?

0
$

    上面退出码为0,说明和预期的一样,发生了跳转。

    由于carry标志在结果为负时也会被设置,所以对于有符号整数的计算来说,检测carry标志的意义不大,因为有符号整数经常需要对负数进行操作,所以和加法指令一样,SUB减法指令也是通过检测overflow溢出标志来判断结果是否是一个有效的有符号数的。

    下面的subtest3.s程式就演示了减法运算中检测overflow标志的例子:

# subtest3.s - An example of an overflow condition in a SUB instruction
.section .data
output:
    .asciz "The result is %d\n"
.section .text
.globl _start
_start:
    nop
    movl $-1590876934, %ebx
    movl $1259230143, %eax
    subl %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
 

    上面代码中将-1590876934赋值给EBX,将1259230143赋值给EAX,然后使用SUB指令将EBX减去EAX,这样会得到一个很大的负数,该负数会超过32位有符号整数的最小负数范围,所以overflow溢出标志会被设置,jo指令会跳转到over标签处,输出结果为0,如下所示:

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

The result is 0
$

    上面的结果和预期的一样,这里ld链接时指明了链接C的动态库,这样程序中才能使用printf函数来输出显示结果。

    我们再对EAX的初始值做个调整:

movl $-1259230143, %eax

    由于-1590876934减去-1259230143,结果是个有效的负数,所以不会发生溢出,可以正常的显示出结果来,如下所示:

$ ./subtest3
The result is -331646791
$

The SBB instruction SBB指令:

    SBB指令和ADC指令一样,会自动考虑carry标志(carry在此时就是借位标志),所以适合于大的有符号整数的减法运算,在大整数减法运算中,SBB指令会自动完成借位操作。

    SBB指令的格式如下:

sbb source, destination

    destination目标操作数在减去source源操作数时,如果carry借位标志被设置,则会先将carry标志位加入到source源操作数中来完成借位,再进行减法运算,得到的结果存储到destination目标操作数中。source和destination可以是8位,16位,32位的寄存器或内存位置,但是它们不可以同时都是内存位置。

    在大整数的多字节减法运算里,一般是先对最低的数据元素使用SUB指令,其他的数据元素就可以用SBB指令,这种做法和上一篇ADD和ADC的做法是类似的,如下图所示:


图1

    下面的sbbtest.s程式就演示了SBB指令的用法:

# sbbtest.s - An example of using the SBB 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
    subl %ebx, %edx
    sbbl %eax, %ecx
    pushl %ecx
    pushl %edx
    push $output
    call printf
    add $12, %esp
    pushl $0
    call exit
 

    上面的代码将data1里.quad定义的64位大整数7252051615的低4字节存储到EBX,高4字节存储到EAX,同时将data2里的大整数5732348928存储到ECX : EDX的寄存器组中,然后对低4字节的数据元素EBX和EDX使用SUB指令,对高4字节的数据元素EAX和ECX使用SBB指令,结果存储在ECX : EDX的寄存器组中,最后通过printf函数将结果输出显示出来,运行情况如下所示:

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

The result is -1519702687
$

    上面程式其实就是用data2里的大整数减去data1里的大整数,结果和预期的一样,你可以自行调整data1和data2里的值,然后进行测试。

Incrementing and decrementing 递增和递减指令:

    在汇编中对数组元素进行循环处理的时候,基本上都要用到计数器,这就涉及到计数器的递增和递减操作,由于这种操作使用比较频繁,所以英特尔提供了一些便捷的指令来完成递增和递减操作。

    INC和DEC指令就可以用于对无符号整数值进行递增和递减,另外INC和DEC指令不会影响carry标志,所以在循环对计数器递增或递减时就不会影响到循环体中的加法和减法运算。

    INC和DEC指令的格式如下:

dec destination
inc destination

    destination目标操作数可以是一个8位,16位,32位的寄存器或内存位置。

    INC和DEC指令不影响carry标志,其他的OF, SF, ZF, AF 及 PF标志则根据结果来进行设置,这两个指令主要用于无符号整数值,有符号整数值在使用这些指令时需要自己留意正负符号位的改变。

Multiplication 乘法:

    乘法是整数运算中比较复杂的一种运算,它和加法和减法指令不同,无符号整数和有符号整数的乘法指令是不一样的,需要不同的乘法指令来处理。

Unsigned integer multiplication using MUL 无符号整数的MUL乘法指令:

    MUL指令用于将两个无符号整数进行乘法运算,它的格式如下:

mul source

    这个格式可能和你所想的不一样,格式中只有一个source源操作数,source可以是一个8位,16位,32位的寄存器或内存位置,该指令的另一个操作数是隐示的EAX寄存器,根据操作数位尺寸的大小,隐示操作数可以是8位的AL寄存器或者16位的AX寄存器或者32位的EAX寄存器。

    由于乘法运算的结果可能会比较大,所以存放结果的寄存器的尺寸是源操作数大小的两倍。

    例如:当MUL操作数的大小为8位时,乘法运算的结果就存储到AX寄存器中。

    当MUL操作数大小为16位时,为了和旧的16位处理器相兼容,所以乘法结果就存储在DX : AX的寄存器组中,DX存储高16位,AX存储低16位。

    当MUL操作数大小为32位时,结果就存储在EDX : EAX的寄存器组中,EDX存储高32位,EAX存储低32位。

    另外,需要注意的是,由于MUL乘法指令会将结果存储到EDX之类的寄存器中,所以在执行MUL指令之前,最好先将EDX等寄存器进行压栈备份。

    下表显示了不同尺寸的操作数对应的目标寄存器的情况:

Source Operand Size
源操作数大小 
Destination Operand
目标操作数
Destination Location
存储结果的寄存器
8 bits AL AX
16 bits AX DX:AX
32 bits EAX EDX:EAX

    MUL指令在GNU汇编中也要根据操作数的大小在MUL助记符后面加上尺寸后缀。

A MUL instruction example MUL指令的例子:

    在接下来的例子中,我们将计算315,814和165,432的乘积,如下图所示:


图2

    上图显示计算结果超过了32位无符号整数的最大值,所以需要使用EDX : EAX寄存器组来存储这个64位的结果,MUL指令的源和目标操作数的大小就必须是32位的尺寸。

    下面的multest.s程式就演示了这个例子:

# multest.s - An example of using the MUL instruction
.section .data
data1:
    .int 315814
data2:
    .int 165432
result:
    .quad 0
output:
    .asciz "The result is %qd\n"
.section .text
.globl _start
_start:
    nop
    movl data1, %eax
    mull data2
    movl %eax, result
    movl %edx, result+4
    pushl %edx
    pushl %eax
    pushl $output
    call printf
    add $12, %esp
    pushl $0
    call exit
 

    上面代码中将data1里的315814存储到EAX寄存器,然后使用mull data2指令将data2里的165432和EAX里的值进行乘积,结果存储到EDX : EAX寄存器组中,最后将结果存储到result内存处,并用printf函数将64位结果显示出来。

    下面是调试输出情况:

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

Reading symbols from /home/zengl/Downloads/asm_example/mul/multest...done.
(gdb) break 19
Breakpoint 1 at 0x80481d7: file multest.s, line 19.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/mul/multest

Breakpoint 1, _start () at multest.s:19
19        pushl %edx

(gdb) print/x $eax
$1 = 0x2a16c050
(gdb) print/x $edx
$2 = 0xc
(gdb) x/gd &result
0x80492ec <result>:    52245741648
(gdb) x/8bx &result
0x80492ec <result>:    0x50    0xc0    0x16    0x2a    0x0c    0x00    0x00    0x00
(gdb)

    从上面的输出可以看出来,result里存储的64位结果的高32位存储在EDX中,低32位存储在EAX中,和预期的一样。

    下面是程序运行时的输出结果:

$ ./multest
The result is 52245741648
$

Signed integer multiplication using IMUL 有符号整数的IMUL乘法指令:

    有符号数的乘法运算主要使用IMUL指令,IMUL指令有三种格式:

imul source
imul source, destination
imul multiplier, source, destination

    第一个imul source的用法和前面介绍的mul source的用法差不多,都只有一个源操作数,另一个操作数根据数据大小隐藏在AL , AX 或 EAX中,得到的结果则存放在AX 或 DX : AX 或 EDX : EAX寄存器组中。

    只不过mul source用于操作无符号数,而imul source则用于操作有符号数,例如我们将前面的multest.s程式做如下调整:

................................
data1:
    .int 0xffffffff
data2:
    .int -1
................................

    那么当代码中使用mull data2时,mul会将data1里的0xffffffff和data2里的-1都当成无符号整数即4294967295 ,这样结果就是4294967295 * 4294967295 = 18446744065119617025 (当然要显示出这个值,还需要将printf的格式化输出字符串里的%qd改为%qu表示输出无符号整数值)。

    当代码中使用imull data2时,imul就会将data1里的0xffffffff当成有符号数-1,这样结果就是 -1 * -1 = 1 。

    另外,无论是mul指令还是imul指令,尽管结果的位尺寸是源操作数位尺寸的两倍,但是只要结果的实际值的大小超过了源操作数的尺寸范围时,就会设置carry和overflow标志,例如前面的multest.s程式,虽然可以输出正确的结果52245741648,但是由于该结果超过了源操作数32位尺寸的大小,所以就会同时设置carry和overflow标志,8位和16位的操作数也是同理。

    当然虽然它设置了carry和overflow两个标志,但是由于64位的空间完全可以容纳下两个32位操作数的所有可能的乘积(8位和16位操作数也是同理),所以当使用mul source和imul source这种只有单一操作数的格式时,如果程序没有特殊需求,不考虑carry和overflow标志也没事,因为结果被完整的保存了下来。

    carry和overflow标志主要对imul source, destination和imul multiplier, source, destination这两种格式会起到作用,因为这两种格式都会将结果存储到destination目标操作数中,而destination目标操作数的尺寸和source源操作数的尺寸是一样的,所以很有可能会出现结果溢出destination的情况,此时检测carry和overflow溢出标志就有意义了。

    在imul source, destination格式中,source源操作数可以是一个16位或32位的寄存器或者某内存位置里的值,destination目标操作数则必须是一个16位或32位的通用寄存器,该格式和前面的单一操作数的格式相比,好处就在于你可以指定destination来存储结果,而不一定要存放在AX和DX中,不过缺点是destination有尺寸限制,容易发生溢出,需要检测carry和overflow标志来判断结果是否有效。

    第三个 imul multiplier, source, destination 格式中,multiplier是一个立即数,source源操作数可以是一个16位或32位的寄存器或者某内存位置里的值,destination目标操作数则必须是一个16位或32位的通用寄存器,该格式可以快速的将一个有符号的立即数和source里的值相乘,结果存储在destination中,该格式的destination也有尺寸限制,容易发生溢出,也需要检测carry和overflow标志来判断结果是否有效。

    imul和mul指令一样也需要在助记符后面指定尺寸后缀以表示操作数的大小。

An IMUL instruction example 一个IMUL指令的例子:

    imul的第一种格式和mul指令用法一样,所以下面的imultest.s程式就只演示后两种格式:

# imultest.s - An example of the IMUL instruction formats
.section .data
value1:
    .int 10
value2:
    .int -35
value3:
    .int 400
.section .text
.globl _start
_start:
    nop
    movl value1, %ebx
    movl value2, %ecx
    imull %ebx, %ecx
    movl value3, %edx
    imull $2, %edx, %eax
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码中,将value1里的10赋值给EBX ,将value2里的-35赋值给ECX ,通过imull %ebx, %ecx将两者进行乘积,结果存储在ECX中,接着将value3里的400赋值给EDX,最后由imull $2, %edx, %eax指令将EDX里的400乘以2,结果存储在EAX中。

    下面是调试输出情况:

(gdb) info reg
eax            0x320    800
ecx            0xfffffea2    -350
edx            0x190    400
ebx            0xa    10

    上面输出中ECX里的-350就是value1和value2两个有符号数的乘积,EAX里的800就是value3里的400和立即数2的乘积,都和预期的一致。

Detecting overflows 溢出检测:

    下面的imultest2.s程式是乘法运算溢出检测的例子:

# imultest2.s - An example of detecting an IMUL overflow
.section .text
.globl _start
_start:
    nop
    movw $680, %ax
    movw $100, %cx
    imulw %cx
    jo over
    movl $1, %eax
    movl $0, %ebx
    int $0x80
over:
    movl $1, %eax
    movl $1, %ebx
    int $0x80
 

    下面是程序运行结果:

$ ./imultest2
$ echo $?
1
$

    由于680和100的乘积为68000超过了16位尺寸的范围,结果中低16位即AX的最高符号位向高16位即DX中发生了carry进位,所以carry进位标志和overflow溢出标志会被同时设置,jo跳转指令就会跳转到over标签处,退出码也就为1 。

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

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

下一篇: 基本数学运算 (三) 除法和移位指令

上一篇: 基本数学运算 (一)

相关文章

汇编中使用文件 (二)

汇编函数的定义和使用 (三) 汇编函数结束篇

使用内联汇编 (二)

Moving Data 汇编数据移动 (四) 结束篇 栈操作

汇编数据处理 (一)

什么是汇编语言(一) 汇编底层原理,指令字节码