第一篇 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整型值。