上一篇文章对MMX,SSE,SSE2及SSE3技术做了基本的概述,本篇文章就先对MMX技术所提供的运算指令进行介绍 在汇编程式里执行MMX运算指令的基本步骤如下:将多个整数放在一起组成MMX所需的压缩整数类型...

    本文由zengl.com站长对汇编教程英文版相应章节进行翻译得来。

    汇编教程英文版的下载地址:点此进入原百度盘   , 点此进入Dropbox网盘   , 点此进入Google Drive  (Dropbox与Google Drive里是Assembly_Language.pdf文档)

    另外再附加一个英特尔英文手册的共享链接地址:
    点此进入原百度盘点此进入Dropbox网盘点此进入Google Drive  (在某些例子中会用到,Dropbox与Google Drive里是intel_manual.pdf文档)

    Dropbox与Google Drive可能需要通过代理访问。

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

    上一篇文章对MMX,SSE,SSE2及SSE3技术做了基本的概述,本篇文章就先对MMX技术所提供的运算指令进行介绍。

Using MMX Instructions 使用MMX指令:

    在汇编程式里执行MMX运算指令的基本步骤如下:
  1. Create packed integer values from integer values.
    将多个整数放在一起组成MMX所需的压缩整数类型
  2. Load the packed integer values into an MMX register.
    将压缩整数值加载到某个MMX寄存器中
  3. Perform an MMX mathematical operation on the packed integer values.
    通过MMX运算指令对压缩整数执行数学运算
  4. Retrieve the result from the MMX register to a memory location.
    从MMX寄存器里获取运算结果,并将结果存储到指定的内存位置
    下面就对这几个步骤进行更详细的介绍。

Loading and retrieving packed integer values 加载和获取压缩整数:

    上面提到的基本步骤里,第一步和最后一步:将压缩整数加载到MMX寄存器,以及从MMX寄存器获取结果的方法,在之前的"汇编数据处理 (二)"的文章中已经介绍过了,即可以通过MOVQ指令在内存和MMX寄存器之间进行传值。

    通过MOVQ指令赋值给MMX寄存器的整数必须是压缩整数类型,MMX提供了三种压缩整数类型:64位压缩字节整数,64位压缩字整数以及64位压缩双字整数,可以参考"汇编数据处理 (二)"的文章里的图1。下面的代码片段就演示了这几种整数类型与MMX寄存器之间的传值方式:

.section .data
packedvalue1:
	.byte 10, 20, -30, 40, 50, 60, -70, 80
packedvalue2:
	.short 10, 20, 30, 40
packedvalue3:
	.int 10, 20
.section .bss
	.lcomm result, 8
.section .text
.globl _start
_start:
movq packedvalue1, %mm0
movq packedvalue2, %mm1
movq packedvalue3, %mm2
movq %mm2, result


    上面的代码片段里,在packedvalue1标签处定义了一个64位压缩字节整数(包含8个单字节整数值),在packedvalue2处定义了一个64位压缩字整数(包含4个16位字大小的整数值),在packedvalue3处定义了一个64位压缩双字整数(包含2个32位双字大小的整数值)。在bss段定义了一个8字节的result变量,用于获取MMX寄存器里的压缩整数值。

    可以看到,MOVQ指令既可以将packedvalue1之类的内存里的压缩整数赋值给MMX寄存器,也可以将MMX寄存器里的压缩整数保存到result之类的64位尺寸的内存位置处。

Performing MMX operations 执行MMX数学运算指令:

    一旦数据被加载到MMX寄存器中,就可以通过单一的MMX指令对寄存器里的压缩整数进行并行运算了,MMX指令会对压缩整数里的每个整数单元同时进行数学运算,如下图所示:


图1

    从上图可以看出,MMX指令会对MMX0与MMX1寄存器里的相同部分的整数单元同时进行加减运算,结果存储到目标寄存器的相应整数单元中。下面就对MMX可用的运算指令分别进行介绍。

MMX addition and subtraction instructions -- MMX的加减运算指令:

    MMX加减运算指令与普通的通用寄存器的加减运算指令有所不同,当使用通用寄存器进行常规的加减运算时,可以通过EFLAGS寄存器来检测结果是否发生溢出。但是,MMX加减指令则是对压缩整数中的多对整数同时进行的运算,就无法使用EFLAGS来检测溢出(EFLAGS只有一个溢出标志位,无法记录同时执行的多个运算的溢出结果)。

    因此,MMX加减运算指令会对溢出结果执行一个预定义的操作,有以下三种预定义的操作:
  • Wraparound arithmetic 溢出时截断运算
  • Signed saturation arithmetic 有符号饱和运算
  • Unsigned saturation arithmetic 无符号饱和运算
    wraparound的溢出时截断运算方式,会在发生溢出时,将溢出的部分给截去,例如,使用稍后要介绍的PADDB指令对压缩字节整数进行加法运算时,当其中一个字节整数为240,另一个MMX寄存器的相同位置的字节整数为30时,由于加运算的结果270对应的十六进制为0x10e,超出了0xff即255的有效字节范围,因此0x10e的超过部分0x100会被截去,留下的0x0e作为最终结果保存到目标寄存器的对应位置处。因此,使用这种类型的MMX加运算指令时,需要你自己在执行指令前,预先对操作数进行检测,以确保运算结果不会发生溢出。

    至于后两种有符号与无符号的饱和运算方式,当加减运算发生溢出时,会将最终结果设置为一个预设值,如下表所示:

Data Type
数据类型
Positive Overflow Value
正溢出值
Negative Overflow Value
负溢出值
Signed byte
有符号字节整数
127 -128
Signed word
有符号字整数
32,767 -32,768
Unsigned byte
无符号字节整数
255 0
Unsigned word
无符号字整数
65,535 0

    当发生溢出时,如果是正向溢出,则将结果设置为正溢出值,如果是负向溢出,则将结果设置为负溢出值。例如,使用稍候要介绍的PADDSB指令对压缩字节整数进行有符号的饱和加运算时,当其中一个字节整数为120,另一个MMX寄存器的相同位置的字节整数为30时,由于加运算的结果150超过了127即有符号字节整数的最大值,那么就发生了正向溢出,结果就会被设置为127的正溢出值。

    这种饱和运算对溢出的处理方式,从单纯的数学角度来看,可能没有太大的意义,但是从图形计算的角度来看,是有其价值的。因为MMX技术的其中一个最大的用途就是,对图片里的像素进行并行运算,当对图像里的像素进行红,绿,蓝值的运算时,如果发生正向溢出,则可以自动将结果设置为正溢出值,例如:255,也就是白色,当发生负向溢出时,则将结果设置为负溢出值,例如:0,也就是黑色,因此,就可以方便快速的执行大量的像素处理。

    以上三种溢出运算方式都有对应的MMX加减指令,如下表所示:

MMX Instruction
MMX指令
Description
指令描述
PADDB Add packed byte integers with wraparound
使用wraparound截断溢出方式进行压缩字节整数的加运算
PADDW Add packed word integers with wraparound
使用wraparound截断溢出方式进行压缩字整数的加运算
PADDD Add packed doubleword integers with wraparound
使用wraparound截断溢出方式进行压缩双字整数的加运算
PADDSB Add packed byte integers with signed saturation
压缩字节整数的有符号饱和加运算
PADDSW Add packed word integers with signed saturation
压缩字整数的有符号饱和加运算
PADDUSB Add packed byte integers with unsigned saturation
压缩字节整数的无符号饱和加运算
PADDUSW Add packed word integers with unsigned saturation
压缩字整数的无符号饱和加运算
PSUBB Subtract packed byte integers with wraparound
使用wraparound截断溢出方式进行压缩字节整数的减运算
PSUBW Subtract packed word integers with wraparound
使用wraparound截断溢出方式进行压缩字整数的减运算
PSUBD Subtract packed doubleword integers with wraparound
使用wraparound截断溢出方式进行压缩双字整数的减运算
PSUBSB Subtract packed byte integers with signed saturation
压缩字节整数的有符号饱和减运算
PSUBSW Subtract packed word integers with signed saturation
压缩字整数的有符号饱和减运算
PSUBUSB Subtract packed byte integers with unsigned saturation
压缩字节整数的无符号饱和减运算
PSUBUSW Subtract packed word integers with unsigned saturation
压缩字整数的无符号饱和减运算

    上表所示的MMX运算指令具有类似如下所示的相同格式:

PADDSB source, destination

    source源操作数可以是一个MMX寄存器,还可以是一个64位大小的内存位置,destination目标操作数则必须是一个MMX寄存器。例如,下面的指令:

PADDSB %mm1, %mm0

    上面指令用于将MMX0与MMX1寄存器里的压缩字节整数相加,结果存储到MMX0中。

    下面的mmxadd.s程式就演示了MMX加运算指令的用法:

# mmxadd.s - An example of performing MMX addition
.section .data
value1:
	.int 10, 20
value2:
	.int 30, 40
.section .bss
	.lcomm result, 8
.section .text
.globl _start
_start:
	nop
	movq value1, %mm0
	movq value2, %mm1
	paddd %mm1, %mm0
	movq %mm0, result
	movl $1, %eax
	movl $0, %ebx
	int $0x80


    上面的代码中,在value1和value2处分别定义了一个64位的压缩双字整数,通过MOVQ指令将这两个压缩双字整数分别赋值给MMX0与MMX1寄存器,接着就可以使用PADDD指令对压缩整数里的两个32位的整数同时进行加运算,运算结果存储到MMX0寄存器,最后再由MMX0保存到result内存位置。

    我们可以用gdb调试器来运行分析该程式:

$ as -gstabs -o mmxadd.o mmxadd.s 
$ ld -o mmxadd mmxadd.o 
$ gdb -q mmxadd
Reading symbols from /root/asm_example/adv/mmxadd...done.
(gdb) b _start
Breakpoint 1 at 0x8048074: file mmxadd.s, line 12.
(gdb) r
Starting program: /root/asm_example/adv/mmxadd 

Breakpoint 1, _start () at mmxadd.s:12
12		nop
(gdb) s
13		movq value1, %mm0
(gdb) s
14		movq value2, %mm1
(gdb) s
15		paddd %mm1, %mm0
(gdb) info all
........................................................
st0            -nan(0x140000000a)	(raw 0xffff000000140000000a)
st1            -nan(0x280000001e)	(raw 0xffff000000280000001e)
........................................................


    在单步执行完两个movq指令后,MMX0即上面的st0寄存器里就存储了value1中的压缩双字整数,压缩整数中的低32位整数0x0a对应就是value1处定义的10,高32位整数0x14则对应为value1处定义的20 。同理,MMX1即st1寄存器里存储的就是value2标签处定义的压缩双字整数,其中的低32位0x1e对应value2定义的30,高32位0x28对应value2定义的40 。

    在程序执行完paddd指令,同时将MMX0的结果存储到result内存位置后,就可以通过查看result内存位置里的值来检测结果:

.........................................
15		paddd %mm1, %mm0
(gdb) s
16		movq %mm0, result
(gdb) s
17		movl $1, %eax
(gdb) x/2d &value1
0x804909c :	10	20
(gdb) x/2d &value2
0x80490a4 :	30	40
(gdb) x/2d &result 
0x80490b0 :	40	60
.........................................


    可以看到,paddd指令会将压缩整数里的两个32位整数分别进行加运算,即10 + 30 = 40,20 + 40 = 60,运算得到的结果40和60则以压缩整数的形式存储在result内存位置处。

MMX multiplication instructions -- MMX的乘法运算指令:

    MMX的乘运算指令则有点小麻烦,因为整数乘法运算的结果可以产生一个比输入操作数更大的值,例如,两个16位的整数相乘,可以产生一个32位的整数结果。因此,MMX乘运算需要分两步来完成,第一步通过PMULL指令得到结果的低16位值,如下图所示:


图2

    PMULL指令运算后,得到的结果所在的目标寄存器里,C1将存储A1 x B1结果的低16位,C2将存储A2 x B2结果的低16位,以此类推。

    第二步我们需要得到结果的高16位,可以先将已得到的低16位的结果存储到指定的内存位置处,然后再将源操作数重新加载到MMX寄存器里,最后通过PMULH指令,就可以得到结果的高16位值,在PMULH指令的作用下,上面图2里的C1将存储A1 x B1结果的高16位,C2将存储A2 x B2结果的高16位,以此类推。

    通过以上两步,我们就得到了乘法运算的低16位与高16位的完整结果了。

    此外,PMULL与PMULH指令都存在两个版本的指令:一是用于有符号整数运算的PMULLW与PMULHW指令,一是用于无符号整数运算的PMULLUW与PMULHUW指令。

    MMX乘法运算里还有一个特殊的指令:PMADDWD指令,该指令是一个有特殊用途的指令,它会先将源操作数与目标操作数里的4个有符号字整数同时进行乘运算,这样会得到4个有符号的双字整数,接着,相邻的两个双字整数会相加,最终得到两个双字整数存储到目标寄存器,如下图所示:
 

图3

MMX logical and shift instructions -- MMX逻辑和位移指令:

    MMX技术还提供了执行布尔逻辑运算和位移相关的指令,如下表所示:

Instruction
指令
Description
描述
PAND Performs a bitwise logic AND of the source and destination operands
对源和目标操作数执行按位逻辑与运算
PANDN Performs a bitwise logical NOT of the destination operand, and then an AND of the source and destination operands
先对目标操作数执行按位逻辑非运算,再将逻辑非的结果和源操作数进行按位与运算
POR Performs a bitwise logical OR of the source and destination operands
对源和目标操作数执行按位逻辑或运算
PXOR Performs a bitwise logical exclusive-OR of the source and destination operands
对源和目标操作数执行按位逻辑异或运算
PSLL Performs a logical left shift of the operand, filling empty bits with zeroes
根据源操作数,对目标操作数进行逻辑左移,左移所产生的空位用零来填充
PSRL Performs a logical right shift of the operand, filling empty bits with zeroes
根据源操作数,对目标操作数进行逻辑右移,右移所产生的空位用零来填充

    PAND,PANDN,POR及PXOR这4个逻辑运算指令的格式如下:

PAND mm/m64, mm
PANDN mm/m64, mm
POR mm/m64, mm
PXOR mm/m64, mm

    可以看到,这些指令的源操作数可以是一个MMX寄存器,还可以是一个64位大小的内存位置,而目标操作数则必须是一个MMX寄存器。逻辑指令的执行原理如下图所示:


图4

    上图可以用下面的伪表达式来说明:

PAND:DEST ← DEST AND SRC;

    因此,PAND指令的作用就是DEST目标操作数和SRC源操作数进行AND逻辑与运算,结果存储到DEST目标操作数里。同理,其他几个逻辑指令的伪表达式如下:

PANDN:DEST ← (NOT DEST) AND SRC;
POR:DEST ← DEST OR SRC;
PXOR:DEST ← DEST XOR SRC;

    至于PSLL逻辑左移指令则有如下6种格式:

PSLLW mm/m64, mm
PSLLW imm8, mm
PSLLD mm/m64, mm
PSLLD imm8, mm
PSLLQ mm/m64, mm
PSLLQ imm8, mm

    从上面的格式可以看出,PSLL指令的源操作数可以是一个MMX寄存器,或者是一个64位大小的内存位置,还可以是一个8位大小的立即数,而目标操作数则必须是一个MMX寄存器。PSLL指令会根据源操作数给出的数值来对目标操作数进行位移操作。当源操作数给出的位移数值超过15时(对于PSLLW -- 16位字操作指令而言),或者超过31时(对于PSLLD -- 32位双字操作指令而言),再或者超过63时(对于PSLLQ -- 64位四字操作指令而言),那么目标操作数里的值就会被设置为0 。

    PSLLW格式的指令会对MMX寄存器的压缩整数里的4个字整数同时进行位移操作,如下图所示:


图5

    上图中,PSLLW指令会根据源操作数的值,对目标MMX寄存器里的4个16位的字整数同时进行逻辑左移操作。如果是PSLLD指令,则会对MMX里的2个32位的双字整数同时进行逻辑左移操作。如果是PSLLQ指令,则会对MMX里的64位整数进行逻辑左移操作。

    PSRL指令也有和PSLL相同的6种指令格式,只不过是对目标操作数进行逻辑右移操作而已。

    除了PSLL与PSRL逻辑位移指令外,还有一个PSRA的算术右移指令,PSRA指令有如下4种格式:

PSRAW mm/m64, mm
PSRAW imm8, mm
PSRAD mm/m64, mm
PSRAD imm8, mm

    可以看到,PSRA指令的源操作数可以是一个MMX寄存器,或者是一个64位大小的内存位置,还可以是一个8位大小的立即数,而目标操作数则必须是一个MMX寄存器。

    算术右移指令与上面介绍的PSRL逻辑右移指令的区别在于,当目标操作数进行右移操作后,高位部分,空出来的二进制位会用符号位的初始值来填充。当源操作数提供的位移数值超过15时(对于PSRAW -- 16位字操作指令而言),或者超过31时(对于PSRAD -- 32位双字操作指令而言),那么目标操作数的每个数据元素都会被该元素的符号位的初始值填满。

MMX comparison instructions -- MMX比较指令:

    MMX还提供了一些比较指令,如下表所示:

Instruction
指令
Description
描述
PCMPEQB Compares packed byte integer values for equality
对源和目标操作数里的8个字节整数值进行比较,判断它们是否相等
PCMPEQW Compares packed word integer values for equality
对源和目标操作数里的4个16位的字整数值进行比较,判断它们是否相等
PCMPEQD Compares packed doubleword integer values for equality
对源和目标操作数里的2个32位的双字整数值进行比较,判断它们是否相等
PCMPGTB Determines whether the packed byte integer values are greater than another
对源和目标操作数里的8个字节整数值进行比较,判断目标是否大于源
PCMPGTW Determines whether the packed word integer values are greater than another
对源和目标操作数里的4个16位的字整数值进行比较,判断目标是否大于源
PCMPGTD Determines whether the packed doubleword integer values are greater than another
对源和目标操作数里的2个32位的双字整数值进行比较,判断目标是否大于源

    MMX比较指令与普通的CMP指令不同,由于MMX寄存器的压缩整数类型包含多个整数值,因此,不能通过EFLAGS标志寄存器来判断比较的结果。因此,MMX比较指令会直接将比较的结果记录到目标MMX寄存器中,例如,对于PCMPEQW指令,当其中一对字整数相等时,则目标寄存器的相同位置的字整数就会被设置为0xffff(即二进制全部被1填充),当某对字整数不相等时,则目标寄存器的对应位置的字整数值就会被设置为0,如下图所示:


图6

    上图中,由于第二对和第四对的字整数是相等的,因此,目标寄存器里对应位置的字整数值就是0xffff,而其他两对字整数因为不相等,因此,对应位置的比较结果就为0 。
 
    下面的mmxcomp.s程式就演示了MMX比较指令的用法:

# mmxcomp.s - An example of performing MMX comparison
.section .data
value1:
	.short 10, 20, -30, 40
value2:
	.short 10, 40, -30, 45
.section .bss
	.lcomm result, 8
.section .text
.globl _start
_start:
	nop
	movq value1, %mm0
	movq value2, %mm1
	pcmpeqw %mm1, %mm0
	movq %mm0, result
	movl $1, %eax
	movl $0, %ebx
	int $0x80


    上面代码中,在value1和value2标签处分别定义了一个64位的压缩整数(都包含4个16位的字整数),在代码开头,就先将value1里的压缩整数设置到MMX0寄存器,并将value2里的压缩整数设置到MMX1寄存器,接着通过pcmpeqw %mm1, %mm0指令对MMX0与MMX1里的压缩字整数进行比较,比较的结果存储在MMX0里,最后再通过movq %mm0, result指令将MMX0里的比较结果存储到result对应的内存位置处。

    我们可以在gdb调试器里查看指令的执行情况:

$ as -gstabs -o mmxcomp.o mmxcomp.s 
$ ld -o mmxcomp mmxcomp.o
$ gdb -q mmxcomp
Reading symbols from /root/asm_example/adv/mmxcomp...done.
(gdb) b _start 
Breakpoint 1 at 0x8048074: file mmxcomp.s, line 12.
(gdb) r
Starting program: /root/asm_example/adv/mmxcomp 

Breakpoint 1, _start () at mmxcomp.s:12
12		nop
(gdb) s
13		movq value1, %mm0
(gdb) s
14		movq value2, %mm1
(gdb) s
15		pcmpeqw %mm1, %mm0
(gdb) s
16		movq %mm0, result
(gdb) s
17		movl $1, %eax
(gdb) x/4dh &value1
0x804909c :	10	20	-30	40
(gdb) x/4dh &value2
0x80490a4 :	10	40	-30	45
(gdb) x/4xh &result 
0x80490b0 :	0xffff	0x0000	0xffff	0x0000
(gdb) 


    可以看到,由于value1和value2处的第一对字整数都是10,并且第三对字整数都是-30,因此,result结果中,对应位置的字整数就被设置为了0xffff,而其他两对字整数因为不相等,因此,result对应位置的字整数就被设置为了0 。

    限于篇幅,本章就到这里,下一篇介绍SSE技术提供的指令。

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

下一篇: 使用IA-32平台提供的高级功能 (三) SSE相关指令

上一篇: 使用IA-32平台提供的高级功能 (一)

相关文章

IA-32平台(二)

汇编数据处理 (二)

高级数学运算 (二) 基础浮点运算

使用内联汇编 (三) 内联汇编结束篇

优化汇编指令 (一)

汇编中使用文件 (一)