x86/x64体系探索及编程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第一篇 x86基础

这一篇探讨x86架构的基础平台知识,有如下7章。

第1章 数与数据类型

第2章 x86/x64编程基础

第3章 编写本书的实验例子

第4章 处理器的身份

第5章 了解Flags

第6章 处理器的控制寄存器

第7章 MSR

第3章很重要,讲解如何编写、生成和运行本书的所有实验例子。在继续本书后续章节前应好好了解这一章里的内容。

第1章 数与数据类型

我们知道在计算机中处理的数是按照一定的规则进行组织和存放的。其中的每个数按特定的编码规则组织。可是光有这些数的组织规则还是不够,计算机每条指令的操作数可能会有不同的数据类型。那么计算机能处理哪些数据类型呢?在这一章里,我们将要了解数与数据类型。

1.1 数

计算机能处理各种各样的信息,计算机硬件对数据进行处理后,可呈现出各种各样的信息。

1.1.1 数字

数字是个基本的计数符号。通用的数字有10个:0,1,2,3,4,5,6,7,8,9。以这些数字组合构成的数是十进制数。

思考各个进制数的数字。

1.二进制数字

包括0和1。

2.八进制数字

包括0,1,2,3,4,5,6,7。

3.十进制数字

包括0,1,2,3,4,5,6,7,8,9。

4.十六进制数字

包括0,1,2,3,4,5,6,7,8,9及字母A,B,C,D,E,F。

各个进制以相应的数字表达的计数范围作为base值,如:二进制的base值是2,八进制的base值是8,十进制的base值是10,十六进制的base值是16。

1.1.2 二进制数

二进制数是计算机运算的基础,无论何种制式的数,在计算机中都是以二进制形式存放的。由二进制数字组成的数字序列是二进制数,如下所示。

二进制数组合里,每个数位被称为bit(位),能表达值0和1。二进制数的base值是2,那么在n个二进制数字的序列中,其值为

值=(Dn-1×2n-1)+(Dn-2×2n-2)+…+(D1×21)+(D0×20

这是一个数学上的算式。这个值是我们很容易辨识的十进制值。

1.1.3 二进制数的排列

在日常的书写或表达上,最左边的位是最高位。数的位排列从左到右,对应的值从高到低。可是在机器的数字电路上,数的高低位可以从左到右进行排列,也可以从右到左进行排列。这样就产生了MSB和LSB的概念。

什么是MSB?什么是LSB?

以一个自然的二进制表达序列上32位的二进制数为例,最右边是bit 0,最左边是bit 31。那么bit 0就用LSB(Least Significant Bit,最低有效位)来表示,bit 31就用MSB(Most Significant Bit,最高有效位)来表示。

MSB也用做符号位(1为负,0为正),但若在无符号数上,则MSB就是数的最高位,LSB是数的最低位。无论一个数在机器上是从左到右排列,还是从右到左排列,使用MSB和LSB的概念都很容易对其二进制形式进行描述说明。

小端序与大端序

二进制数在计算机的组织存放中,地址由低位到高位对应着两种排列。

① 由LSB到MSB,这就是小端序(little-endian)排法。

② 由MSB到LSB,这就是大端序(big-endian)排法。

在x86/x64体系中使用的是小端序存储格式,也就是:MSB对应着存储器地址的高位,LSB对应着存储器地址的低位。

在有些RISC(精简指令集计算机)体系里,典型的如Power/PowerPC系列,使用大端序排法。即在由低到高的地址位里,依次存放MSB到LSB。亦即:MSB存放在存储器地址的低位,LSB存放在高位。

代码清单1-1:

mov dword [Foo],1
test byte [Foo],1                                ; 测试 LSB 是否存放在低端上
jnz IS_little_endian                              ; 是小端序

上面的代码将1存放在32位的内存里,通过读取内存的低字节来判断1到底存放在低字节还是高字节,从而区分是小端序还是大端序。

某些RISC机器上是可以在大端序与小端序存储序列之间做选择的。大端序格式看上去更符合人类表达习惯,而小端序看上去不那么直观,不过这对于计算机的处理逻辑并无影响。

实验1-1:测试字节内的位排列

字节内的位是否有大端序和小端序之分?这似乎没有定论,我们不是硬件设计人员,很难做出判断。笔者倾向于认为位的排列是区分的。

从代码清单1-1我们可以测试机器是属于小端序还是大端序,原理是根据字节在内存中的存储序列进行判断。对代码稍做修改,即可用来测试位的排列,如代码清单1-2所示。

代码清单1-2(topic01\ex1-1\boot.asm):

      mov dword [Foo],2                                       ;
00000000000000000000000000000010B
      bt dword [Foo],1                                        ; 取 bit 1
      setc bl                                                     ; bit 1 是否等于 1
      movzx ebx,bl
      mov si,[message_table + ebx * 2]
      call print_message
next:
      jmp $
Foo                     dd 0
LSB_to_MSB            db 'byte order:LSB to MSB',13,10,0
MSB_to_LSB            db 'byte order:MSB to LSB',13,10,0
message_table        dw MSB_to_LSB,LSB_to_MSB,0

代码清单1-2中测试内存中的bit 1是否为存进去的值1,然后输出一条信息。下面是这个实验在真实计算机上的测试结果。

实际上这个方法未必能测出什么(如果CPU一次访问字节,这个测试结果并不能说明什么)。按这个方法测出字节内也是小端序排列的。

完整的测试代码在topic01\ex1-1\目录里。关于如何在真机上进行测试,请看第3章。

1.1.4 十六进制数

在二进制数中,每一个数位只能表达两个值。如果要表达一个很大的数,就将比较麻烦。而十六进制数能在更短的序列里表达更大的数值范围。

十六进制数由十六进制数字组成,每个数位能表达16个值:从0到15。

超过9的计数用字母来代替,A~F分别表示10~15,它的base值就是16,同样由以下的计算式子来求十六进制的值:

值=(Dn-1×16n-1)+(Dn-2×16n-2)+…+(D1×161)+(D0×160

一个十六进制数字能表达4个二进制数字。与二进制数相比,采用十六进制数进行计数大大方便了书写,也方便了阅读。

1.1.5 八进制数与十进制数

同样,八进制数由八进制数字组成,十进制数由十进制数字组成。我们的日常生活中常使用十进制数。而二进制数、八进制数及十六进制数表达的数,并不被我们直观地识别。

为了书写及识别方便,会使用前缀或者后缀对数进行修饰。

使用后缀:d(D)或默认描述为十进制数,b(B)为二进制数,h(H)为十六进制数,o(O)为八进制数。

上面是常用的修饰方式,在C语言中对十六进制数常使用前缀0x进行修饰,由于C语言的影响力现在使用0x前缀很通用。

在nasm语言里数的修饰更多了,下面是nasm的里例子。

mov     ax,200          ; decimal
mov     ax,0200         ; still decimal
mov     ax,0200d        ; explicitly decimal
mov     ax,0d200        ; also decimal
mov     ax,0c8h         ; hex
mov     ax,$0c8         ; hex again:the 0 is required
mov     ax,0xc8         ; hex yet again
mov     ax,0hc8         ; still hex
mov     ax,310q         ; octal
mov     ax,310o         ; octal again
mov     ax,0o310        ; octal yet again
mov     ax,0q310        ; octal yet again
mov     ax,11001000b    ; binary
mov     ax,1100_1000b   ; same binary constant
mov     ax,1100_1000y   ; same binary constant once more
mov     ax,0b1100_1000  ; same binary constant yet again
mov     ax,0y1100_1000  ; same binary constant yet again

1.2 数据类型

在x86/x64体系中,指令处理的数据分为fundamental(基础)和numeric(数值)两大类。基础类型包括:byte(8位),word(16位),doubleword(32位),以及quadword(64位),它们代表指令能一次性处理的数据宽度。

numeric数据类型使用在运算类指令上,总结来说x86/x64体系的运算类指令能处理下面四大类数据。

① integer(整型数):包括unsigned类型和singed类型。

② floating-point(浮点数):包括single-precision floating-point(单精度浮点数),double-precision floating-point(双精度浮点数),以及double extended-precision floatingpoint(扩展双精度浮点数)。

③ BCD(binary-code decmial integer):包括non-packed BCD码和packed-BCD码。

④ SIMD(single instruction,multiple data):这是属于packed类型的数据。

SIMD数据是在一个operand(操作数)里集成了多个integer、floating-point或者BCD数据。SIMD指令可以一性次同时处理这些数据。

1.2.1 integer数

在计算机处理中,整数会区分signed(有符号数)和unsigned(无符号数)两种情况,数值的MSB值被作为符号位。每个数值类型有自己的取值范围,如下所示。

可是在计算机中根本无法判断一个整数是signed数还是unsigned数。例如0ABh这个整数就无法知道它是signed数还是unsigned数。

计算机能做到的是:在整数的使用中,在应该使用signed数的场合下认为它是signed数,而在使用unsigned数的场合下认为它是unsigned数。

在这种假定下,即使不是signed数也会被当做signed数进行处理。既然这样,在计算机运算中就无须判断是signed数还是unsigned数,只需假定它是signed数或是unsigned数。

而在浮点数上,每个浮点数都有符号位,因此浮点数能够清楚地识别它就是signed数。所以浮点数不存在unsigned数的情况。

在x86机器上,对整数的加减法运算过程中不会识别signed数与unsigned数,而根据signed与unsigned两种运算结果进行相应的eflags标志位设置。

代码清单1-3:

mov eax,0x70000000
mov ebx,0x80000000
sub eax,ebx

上面的代码中,0x70000000和0x80000000是signed数还是unsigned数呢?

二进制运算结果值是0xF0000000,指令会同时对结果进行两种分析设置。

为signed时

假定运算双方是signed数时,这个结果是错误的,它产生了溢出。它会置eflags寄存器的OF(Overflow Flag)标志为1,以及SF(Sign Flag)标志为1,表示结果为负数。

为unsigned时

假定双方是unsigned数时,它会置CF(Carry Flag)标志为1,表示产生了借位。

因此:这条指令会同时对OF、SF及CF标志置位。而对这个结果如何运用那是程序员的职责。

另外,RISC体系的机器普遍会在指令层上做假定运算,如在MIPS机器上add是进行signed数相加,addu是进行unsigned数相加,对指令进行了区分,明确了使用场合。

x86的乘法和除法指令也进行了区分,mul是无符号数乘法,imul是符号数乘法,div是无符号除法,idiv是符号数除法。另外,所有的条件转移、条件传送、条件设置指令会对指令运算的结果进行signed与unsigned的区分。

整数运算规则

当假定它是signed数时,这个数需要使用另一种形式去解析,这就产生了signed数的表示方法。signed的表示法是以MSB(Most Significant Bit)作为符号位,MSB为1时是负数,MSB为0时为正数。

以32位的数为例:0是正数的最小值,0x7FFFFFFF是正数的最大值,0x80000000是负数的最小值,0xFFFFFFFF是负数的最大值,超过这个表达范围就产生了溢出情况。

0x7FFFFFFF+0x00000001结果为0x80000000,这个结果超过了32位正数能表达的最大值,于是就产生了溢出。两个正数相加,32位的结果为负数(负数的最小值)。这个结果是错误的。

signed数是以二进制的补码来表示,以4位二进制数为例,求出-7的二进制补码形式。

-7的补码形式是1001B,它的计算过程是:~7+1=-7。那么反过来,1001B这个值是多少呢?从-7的求值过程可以推出:~((-7)-1),从而得出

由于1001B表达的是负数,求值后要加上负号,这就是我们所知道的十进制的signed数。

1.2.2 floating-point数

现在的计算机浮点数格式都遵循IEEE754标准。在x86/x64体系中有三种浮点数。

① single-precision floating point(单精度浮点数):使用23位的精度。

② double-precision floating point(双精度浮点数):使用52位的精度。

③ double extended-precision floating point(扩展双精度浮点数):使用64位的精度。

x87 FPU的硬件上使用扩展双精度浮点类型,所有浮点数最终都要转为扩展双精度浮点数进行处理(使用64位精度)。

二进制格式

在计算机上,浮点数需要换化为二进制格式进行处理,分为3个部分:sign(符号位),exponent(指数位),以及significand(有效数位),如下所示。

最高位为符号位,单精度浮点数的exponent位是8位,significand位是23位;双精度浮点数的exponent位是11位,significand位是52位。

在单精度和双精度浮点数里,它们的significand部分有一个隐式的integer位(或被称为J-bit),这个位的值固定为1。因此,单精度浮点数的精度实际为24位,而双精度浮点数的精度实际为53位。

扩展双精度浮点数

在扩展双精度浮点数里,significand部分为64位,exponent为15位,如下所示。

在扩展双精度浮点数里,它的integer位是显式的,在normal(合规的)数里,这个位必须为1,否则属于denormal(不合规的)数。

normalized(规格化)

在IEEE754里,规格化是浮点数的基础。正常情况下机器中的浮点数使用规格化的格式,这类浮点数被称为normal数。

看看这个浮点数:0.625,在机器中是如何表示的呢?

首先需要转化为规格化的科学计数形式。

0.625=625/1000=5/8=5/23=101×2-3=1.01×2-1

于是0.625的二进制科学计数法表达是1.01×2-1

在IEEE 754中规定规格化数的significand(有效数)部分第1位是1,不能是0。如下所示。

接下来,这个浮点数被转化为单精度的二进制形式,其值为0x3F200000。如下所示。

由于单精度浮点数的significand部分含有隐式的1值,因此在二进制数格式里,significand部分的值为01000000...(即1.01中前面的1去掉)。

指数部分需要加上一个127值,这个值被称为biased notation(移码或校正值)。

biased notation(校正值)

biased notation用来解决浮点数使用integer方法进行比较时出现的问题。我们看看下面这两个浮点数大小的比较:1.00×2-1和1.00×21

前面的指数为-1,后面的指数为1,指数大的那个必定会大。因此1.0×21的值是大于1.0×2-1的。在指数相同的情况下才需要对有效数部分进行比较。

基于这种考虑,IEEE 754在浮点数的格式中,将指数部分安排在有效数前面,这样就可以使用快速的整数比较方法来比较浮点数。

可是当指数是负数时,按照这样的比较方法,会得出比指数为正数还要大的结论,这是错误的。

1.0×2-1会比1.0×21要大!(因为:-1的二进制8位值为11111111)

为了解决这个问题,于是引入了biased notation值,如下所示。

单精度的biased码是127,双精度的biased码是1023,扩展双精度的biased码是16383。这个biased码值加上指数值,就得出了一个浮点数格式中的指数值:-1+127=126,1+127=128。

算一下。

1.0×2-1的二进制序列是:001111110000000000000000000000000(0x3F000000)。

1.0×21的二进制序列是:010000000000000000000000000000000(0x40000000)。

这样就解决了在使用整数进行比较时,负的指数会比正的指数要大的问题。

在nasm汇编语言语法里,可以使用一系列的宏来获得浮点数以整数形式表现的常量值。

代码清单1-4:

mov eax,__float32__(0.625)                    ; 获得浮点数的整数形式值

__float32__(0.625)这个宏的求值结果是0x3f200000,即0.625的浮点数值是0x3f200000。

1.2.3 real number(实数)与NaN(not a number)

IEEE754标准定义了多种实数的编码格式,它们包括以下面几种格式。

① zero:包括+0.0和-0.0编码。

② denormal数字:不合规格的数,denormal数有时候被称为tiny(极小)数。

③ normal数字:合规的普通浮点数,这是一个finite(有限的)取值范围。

④ infinite(无限)数字:包括+∞(正无穷大数)和-∞(负无穷大数)。

⑤ NaN(not a number):包括SNaN和QNaN。

其编码值如下所示。

zero

在0的编码里,exponent和significand都为0。+0(正零)和-0(负零)的值是相等的。

denormal(不合规)数

denormal数是一个极小的数(即tiny数),接近于0值。它是一种不合规的表示方法。denormal数的exponent部分为0值。不同于zero值,它的significand部分不为0值(在扩展双精度下exponent为0值,J位为0值也属于denormal数)。因此,下面的数是denormal数。

① 00000001H:exponent为0,significand不为0。

② 007FFFFFH:exponent为0,significand不为0。

normal(合规)数

normal数是在finite(有限)集合里的一个数。在normal数编码中,J位的值必须为1(在扩展双精度下)。在单精度和双精度里,J位(或称integer位)是隐式的,固定为1值。

上面列出了三种浮点格式的exponent和significand取值范围,于是有以下结论。

① 单精度的表达范围,正数是0x00800000~0x7F7FFFFF,负数是0x80800000~FF7FFFFF。

② 双精度的表达范围,正数是0x00100000_00000000~0x7FEFFFFF_FFFFFFFF,负数是0x80100000_00000000~0xFFEFFFFF_FFFFFFFF。

③ 扩展双精度的表达范围,正数是0x0001_800000000_00000000~0x7FFE_FFFFFFFF_FFFFFFFF,负数是0x8001_80000000_00000000~0xFFFE_FFFFFFFF_FFFFFFFF。

用科学计数法表示如下。

① 单精度:2-126到2127×1.11...(23个1),因此X的取值就是2-126<=X<2128

② 双精度:2-1022到21023×1.11...(52个1),因此X的取值就是2-1022<=X<21024

③ 扩展双精度:2-16382到216383×1.11...(64个1),因此X的取值就是2-16382<=X<216384

infinite(无穷大)数

显然,这是与finite数相对的。在无穷大数里值是固定的,分为+∞(正无穷大)和-∞(负无穷大)。exponent和significand的值如下所示。

对于扩展双精度来说,由于它的J位是显式的,必须为1值(否则是unsupported类型),因此significand的值为0x80000000_00000000。

NaN(not a number)数

如果一个数超出infinite,那就是一个NaN(not a number)数。在NaN数中,它的exponent部分为可表达的最大值,即FF(单精度)、7FF(双精度)和7FFF(扩展双精度)。

NaN数与infinite数的区别是:infinite数的significand部分为0值(扩展双精度的bit63位为1)。而NaN数的significand部分不为0值。

NaN数包括下列两类。

① SNaN(Signaling NaN)数:SNaN数表示是一种比较严重的错误值。

② QNaN(Quiet NaN)数:在一般情况下,QNaN数是可接受的。

SNaN和QNaN数的编码区别在于significand部分的不同,如下所示。

SNaN数的significand以1.0开头(并且1.0后面的位不为0值),而QNaN数的significand是1.1开头。

x87 FPU或SSE指令遇到SNaN数时会产生#IA异常,而遇到QNaN时不产生#IA异常(部分指令除外)。

1.2.4 unsupported编码值

如果一个数的编码值不在1.2.3节所描述的格式里,那么它就属于unsupported类型的编码值。unsupported类型的编码有三类,它们的J位都为0值,如下所示。

下面的扩展双精度编码都属于unsupported类型。

① 0x7FFE_00000000_00000000。

② 0x7FFF_00000000_00000000。

③ 0x7FFF_00000000_00000001。

由于单精度和双精度中J位隐式固定为1值,因此实际上也仅有扩展双精度会出现unsupported编码。

实验1-2:打印各种编码及信息

下面,我们做个测试,输出x87 FPU的stack中的编码值及状态信息,代码如下。

代码清单1-5(topic01\ex1-2\protected.asm):

finit       ; 初始化 x87 FPU
fld TWORD [QNaN]    ; 加载 QNaN 数
fld TWORD [SNaN]    ; 加载 SNaN 数
fld TWORD [denormal]   ; 加载 denormal 数
fld TWORD [infinity]   ; 加载 infinity 数
fld TWORD [unsupported]  ; 加载 unsupported 数
fldz       ; 加载 0 值
fld1       ; 加载 1.0 值
call dump_data_register  ; 打印信息

关于这个例子的代码,请参考第20章的相关内容。

上面是运行在Bochs里的结果图:这里显示了x87 FPU的stack寄存器的8种状态,除empty状态的编码值是一个indefinite(不确定)值外,其余数的编码都是正确的。

1.2.5 浮点数精度的转换

在x86/x64体系里,由于x87 FPU硬件使用扩展双精度格式,因此必然会遇到single/double precision格式与double extended-precision格式之间的互换问题。

转换为扩展双精度数

当由单精度数或双精度转换为扩展双精度数时,exponent部分必须基于扩展双精度数的biased码来调整。于是扩展双精度数的exponent值为:

① 从单精度转化:exponent–127+16383。

② 从双精度转化:exponent–1023+16383。

而扩展双精度数的significand部分,由单/双精度数的significand部分移植过来。

以单精度数1.11...×2120为例,它转换为扩展双精度的过程如下所示。

单精度数1.11...×2120的编码值为0x7BFFFFFF,它的exponent值为0xF7(11110111B),significand部分全为1值。

于是扩展双精度数的exponent值为0xF7-127+16383=0x4077(1000000001110111B),单精度23位的significand部分直接移到扩展双精度的bit62到bit40位,低40位补0。

最终的扩展双精度编码值为0x4077_FFFFFF00_00000000。而对于双精度数来说:52位的significand部分将直接移到扩展双精度的bit62到bit11位。

扩展双精度数转换为单精度数

而从扩展双精度转换为单/双精度数的情形会复杂得多,涉及目标格式的precison(精度)问题。当扩展双精度significand部分的值超出目标格式的精度时,就会发生rounded(舍入)操作,从而引发precision异常。

要检查超出精度的significand是否为0值,如下所示。

这部分不为0值时,就会发生rounded操作。

下面,我们以扩展双精度数1.11...×2120转化为单精度格式为例进行描述。当1.11...×2120为扩展双精度格式时,它的编码值为0x4077_FFFFFFFF_FFFFFFFF。

目标格式exponent部分的计算如下。

① 单精度数:exponent-16383+127。

② 双精度数:exponent-16383+1023。

这个转换过程较为复杂,如下所示。

图中的阴影部分是超出精度的significand部分(bit 39~bit 0),它的值不为0,需要进行rounded操作,在x87 FPU中这个舍入依赖于rounded控制位。

IEEE754定义了以下4种舍入模式。

① round to nearest模式:朝±∞(正和负方向的无穷大值)方向舍入。

② round down模式:正数朝最大normal值舍入,负数朝-∞方向舍入。

③ round up模式:正数朝+∞方向舍入,负数朝最大normal值舍入。

④ round zero模式:正数和负数都朝最大normal值舍入。

上图中的舍入是朝+∞方向舍入,如图所示:bit 39的值为1,它将向bit 40进行舍入,效果等于+1值。目标格式中的significand部分舍入的结果值为0。

目标格式的exponent部分为扩展双精度的exponent-16383+127=0xF7(11110111B),可是由于significand部分还是进位值,因此目标格式的最终exponent部分为0xF8(加上1值)。

因此,最终转换的单精度值为0x7C000000,转换得到的浮点数是1.0...×2121,结果大于原来的扩展双精度浮点数。

扩展双精度数转换为双精度数

这和转换为单精度数是一致的。在双精度格式里,它的精度是52位,因此超出精度部分为bit10到bit0位。

exponent的计算是扩展双精度的exponent-16383+1023。

1.2.6 浮点数的溢出

1.2.5节所探讨的精度转换处于目标格式的finite(有限的)表达范围内,这是一种正常的转换行为。然而在某些时候会遇到目标格式不能表达的结果值,产生溢出。

在浮点数里,溢出分为两种。

① overflow(向上溢出):结果值超出了目标格式的最大normal值(即finite范围外)。

② underflow(向下溢出):结果值超出了目标格式的最小normal值(即tiny值或denormal数)。

上图是一个实数轴上的分布图,denormal(tiny)接近于0值,infinite(无穷大)数接近于NaN数,因此overflow和underflow溢出的条件如下。

在这里符号位不重要,正或负都能产生溢出。

1.2.6.1 underflow(向下溢出)

在x86/x64体系里,x87 FPU指令的rounded(舍入)结果值发生underflow时会产生#U(underflow)异常,并在x87 FPU的status寄存器里记录下来。

从前面所述我们知道,结果值达到tiny值时就表示发生了underflow溢出,各种精度tiny值如下所示。

这个将要达到tiny的临界值,就是目标精度的normal的最小值,当小于这个最小值时,就发生underflow溢出,对于underflow的处理,如下所示。

下面我们还是看看扩展双精度数转换为单精度格式。

例子:将扩展双精度值0x3F80_C0000300_00000000转换为单精度值。

这个值表示1.100...0110...000×2-127,它的bit63、bit62、bit41和bit 40位都为1值,如图所示。

这个转换过程中会对underflow进行如下处理。

判断exponent是否溢出

单精度normal数的指数部应有-126<=X<128,也就是说,在二进制编码值中exponent的值应为1<=exponent<255。关于normal的finite(有限)范围请参考1.2.3节的normal数的描述。

由于这个值的指数部分为-127,因此对于单精度格式来说,它已经是underflow溢出了。

significand右移

在发生underflow的情况下,significand部分需要向右移位。下面就是一个significand右移的例子。

右移位数的计算方法是:指数的绝对值减去-126的绝对值(即减去126)。在本例里是向右移动一位,移位后的significand部分变成0.110...0110...000。

如下所示:目标单精度格式的exponent值将为0值,significand部分(包括J位)右移1位后,J位将变为0值(这是一个denormal数)。

精度舍入

如果significand的超出精度部分不为0值,同样会发生rounded(舍入)操作。

如上所示:bit 39位向bit 40进行舍入,效果等于加上1值。0值将会写入目标单精度值的exponent部分。最终得到的目标单精度值为0x00600002。

在x87 FPU里这个操作会引发#U(underflow)异常和#P(precision)异常,关于#U和#P异常的更多信息,请参考20.2.6节的相关描述。

1.2.6.2 overflow(向上溢出)

当结果值达到infinite(无限或无穷大)数时,就会产生overflow溢出。在x87 FPU中,当伴随着目标格式的precision(精度)不能表达目标值时,就会产生#P(precision)异常和#O(overflow)异常。

如上所示:infinite值是一个overflow的临界值,等于或大于这个值时就发生overflow。因此,当将扩展双精度数1.0×2128转换为单精度,或者将扩展双精度数1.0×21024转换为双精度数时,就会产生overflow。

1.2.7 BCD码

在BCD码中,一个十进制数的每一位,使用8位的二进制进行编码。以十进制数15为例,它的BCD编码值为15H,如下所示。

每个BCD码用1个字节来表示,这是非压缩的BCD码形式。BCD可以使用在GPI(通用指令)里,如下所示。

mov ax,9     ; AX 为 9(BCD 9)
mov bx,8     ; BX 为 8(BCD 8)
add ax,bx     ; AX 值为11H
aaa       ; 执行 BCD 调整后,AX 值为 0107H(BCD 17)

在使用ADD指令相加后(假如两个BCD码相加),AAA指令将AX寄存器调整为非压缩的BCD码,AX结果值为0107H(即表示非压缩的BCD码17)。

packed BCD(压缩的BCD码)

非压缩的BCD码浪费了一半的空间,在packed BCD码里,每个BCD数字使用4位来表示,如上图所示。

packed BCD码能使用在x87 FPU指令里。FBLD指令能加载一个80位宽的packed BCD码进入x87 FPU数据寄存器里,这个80位的值可容纳18个packed BCD码。

1.2.8 SIMD数据

在SSE系列指令(SSE到SSE4.2)以及AVX指令里处理的数据分为以下两大类。

① vector(packed)与scalar类型的浮点数据。

② packed integer数据。

这些数据类型使得SIMD指令能一次性处理多个数据,加大吞吐量。

128位与256位的vector floating-point数据

从SSE到SSE4.2指令集里,使用128位的XMM寄存器,支持128位的vector(矢量)浮点数据。而在VAX和FMA指令里增加到了256位的YMM寄存器,可以使用256位的vector数据。

如下所示:在128位vector数据里,可以容纳4个单精度浮点数或者2个双精度浮点数。在VAX和FMA指令上的YMM寄存器里可以容纳8个单精度浮点数或者4个双精度浮点数。

128位的scalar floating-point数据

在SSE系列指令和AVX/FMA指令里处理的scalar数据是128位宽,如下所示。

在128位的scalar数据里,单精度scalar只使用低32位,双精度scalar只使用低64位,高位不作为数据进行运算。

128位与256位的packed integer数据

在SSE系列指令和AVX指令里处理的packed integer数据也是128位宽,包括以下几种。

① packed byte。

② packed word。

③ packed doubleword。

④ packed quadword。

多数情况下,这些整型数也区分unsigned和signed版本,由对应的SIMD指令使用。然而Intel在Ivy Bridge微架构里增加了AVX2指令,AVX2指令能够处理256位的packed integer数据。

如下所示,128位的packed integer数据能容纳16个byte,8个word,4个doubleword或者2个quadword整型值。

当使用AVX2指令(AVX的扩展指令集)时,还可以使用256位的packed integer数据,能容纳32个byte,16个word,8个doubleword或者4个quadword整型值。