前面的章节介绍了整数类型,这篇开始介绍更复杂的数据类型:浮点数。早期的80286和80386之类的处理器芯片都只能处理整数的数学运算,如果要进行浮点运算则要么通过软件程式和整数来模拟计算,要么就额外购买专用于浮点计算的FPU芯片...

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

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

Floating-Point Numbers 浮点数:

    前面的章节介绍了整数类型,这篇开始介绍更复杂的数据类型:浮点数。早期的80286和80386之类的处理器芯片都只能处理整数的数学运算,如果要进行浮点运算则要么通过软件程式和整数来模拟计算,要么就额外购买专用于浮点计算的FPU芯片。

    从80486处理器开始,IA-32平台开始可以直接进行浮点运算了,这对汇编开发浮点计算相关的程序提供了很大的帮助。

    下面就具体的介绍下浮点数据类型。

What are floating-point numbers 什么是浮点数:

    在数学里面,除了1,2,3,4这样的整数外,还有像3.14159这样的小数,这些整数和小数构成了一个完整的实数集合,为了能在计算机中表示这些实数,就需要引入浮点数,要在二进制的世界里表示浮点数并不容易,因为最难处理的部分就是小数部分,它还涉及到小数精度问题。

    首先我们要将十进制的小数转为二进制格式,才能将数据存储到计算机中。例如5.125,第一步整数部分5可以按常规的十进制整数到二进制的转换方法:

(1) 5 / 2 商为2 余数为1 则二进制第一位(最低位)为1
(2) 2 / 2 商为1 余数为0 则第二位为0
(3) 1 / 2 商为0 余数为1 则第三位为1 (商为0则转换结束)
(4) 5对应的二进制为101

    第二步将0.125转为二进制小数格式:

(1) 0.125 * 2 乘积为0.25 整数部分为0 则二进制小数点后第一位为0
(2) 0.25 * 2 乘积为0.5 整数部分为0 则第二位为0
(3) 0.5 * 2 乘积为1 整数部分为1 则第三位为1 (乘积刚好等于1则转换结束,乘积大于1或小于1都不算结束)
(4) 0.125对应的二进制小数格式为:.001 (注意001前面有小数点)

    将整数部分的二进制和小数部分的二进制拼在一起就得到5.125的完整二进制小数格式为:101.001 

    为了能将101.001写入计算机中,还需要用到二进制科学记数法:101.001可以写为1.01001 * 2^2的形式(其中1.01001被称作系数,^ 符号后面的指数2被称作指数,* 为乘法运算符,^ 为指数幂运算符),这个科学记数形式最后会按照IEEE754浮点标准存储到内存中:

图1

    上图中Significand对应科学记数的系数部分,由于IEEE标准中像1.01001这种系数在小数点前的1是固定的,而且小数点前只能有一个1(我们可以通过调整后面的指数,让指数根据需要为正或为负,从而确保小数点前有且只有一个1,前提是这个数不是0),所以在Significand中并没包括这个固定的1,只包含小数点后的二进制位01001 。

    Exponent对应科学记数的指数部分,上面2^2中2的指数为2,但由于指数可正可负,所以还需要将指数加上127,才能让指数以8位无符号整数的形式存储到内存中,这里2 + 127 = 129 对应的二进制就如上图所示:10000001 。

    最高位Sign为符号位,1表示负浮点数,0表示正浮点数,本例中为正的5.125,所以Sign就为0 。

    这样浮点数就以二进制浮点格式存储到内存中了,接着就可以将内存中这些二进制格式的浮点数加载到FPU中进行浮点运算了。

    上面提到的IEEE754是IEEE(Institute of Electrical and Electronics Engineers 电气和电子工程师协会)于1985年创建的二进制浮点格式标准。上面图1所显示的是32位的单精度浮点数的格式,单精度浮点数有效二进制位即图中Significand部分,有23位,再加上小数点前固定的1,则一共有24位有效的二进制位,Exponent部分有8位,可以表示0到255的无符号整数,但是0不会用于浮点数的科学记数,因为当Exponent和Significand都为0时编译器会把它当作实数0来处理,而不会按常规的科学记数来处理,所以Exponent的范围为1到255,将它们减去127的偏移值,则科学记数中有效的单精度浮点指数为-126到128 ,2的-126次方约为1.18 * 10^-38 ,2的128次方约为3.40 * 10^38 ,虽然单精度可以表示的十进制范围为1.18 * 10^-38到3.40 * 10^38 ,但是由于有效二进制位只有24位,所以可以比较精确表示的十进制正整数范围只有:16777215 (即1.1111...111 * 2^23) 到 0 ,可以比较精确表示的纯小数范围为:0 到 0.9999999.... (即1.1111....1111 * 2^-1) ,所以大约只有7或8位有效的十进制位,多于7或8位的例如前面0.9999999...后面的省略号部分的数就是些不精确的估计数字。

    IEEE754标准中还有一个双精度浮点格式:
 

图2

    如上图所示,双精度浮点格式一共有64位,其中Significand有效二进制位为52位,再加上小数点前固定的1,一共是53位有效二进制位,Exponent部分有11位,所以用于指数计算的偏移值为1023 ,二进制科学记数中有效的双精度指数范围为-1022到1024 ,可以表示的十进制范围为2^-1022约为2.23 * 10^-308 到2^1024约为1.79 * 10^308 ,53位有效二进制位对应大约15到16位的有效十进制位。

IA-32 floating-point values IA-32平台的浮点数:

   IA-32平台既采用了IEEE 754标准中的单精度和双精度浮点格式,同时还使用了它自己的双精度扩展浮点格式,这三种格式在执行浮点数学运算时提供了不同的二进制位精度。

    双精度扩展浮点格式是FPU寄存器所使用的浮点格式,内存中的单精度和双精度数据在加载到FPU寄存器中后,都会自动转为双精度扩展格式,在浮点运算结束后,从FPU寄存器获取数据到内存中时,系统又会自动将寄存器中的双精度扩展格式转为标准的单精度或双精度格式。

    由于FPU寄存器是80位的寄存器,所以其内部使用的双精度扩展浮点格式一共有80位,其中Significand有效二进制位为64位,这64位中已经包含了二进制科学记数小数点前的固定的1,所以有效二进制位就是64,不需要像单精度和双精度那样再加1,Exponent部分有15位,所以用于指数计算的偏移值为16383 ,科学记数中有效的指数范围为-16382到16384 ,可以表示的十进制范围为2^-16382约为3.37 * 10^-4932到2^16384约为1.18 * 10^4932 ,64位有效二进制位对应大约19到20位的有效十进制位。

    下表显示了IA-32平台所使用的三种浮点格式的基本信息:

Data Type
数据类型
Length
总位数
Significand
有效二进制位
Exponent
指数
Range
十进制范围
Single
单精度
32位 24位 8位 1.18 * 10^-38 到 
3.40 * 10^38
Double
双精度 
64位 53位 11位 2.23 * 10^-308 到 
1.79 * 10^308
Double
双精度扩展 
80位 64位 15位 3.37 * 10^-4932 到
1.18 * 10^4932

Defining floating-point values in GAS 在汇编程序中定义浮点数:

    在gnu汇编器gas中可以使用.float伪操作符来定义32位的单精度浮点数,还可以使用.double伪操作符来定义64位的双精度浮点数。

Moving floating-point values 加载浮点数:

    可以使用FLD指令将浮点数加载到FPU寄存器栈中(寄存器栈的概念在前一章已经讲解过),FLD指令的格式如下:

fld source

    source源操作数可以是指向32位或64位浮点数的内存位置。

    下面的floattest.s例子演示了浮点数的定义和使用的方法:

# floattest.s - An example of using floating point numbers
.section .data
value1:
    .float 12.34
value2:
    .double 2353.631
.section .bss
    .lcomm data, 8
.section .text
.globl _start
_start:
    nop
    flds value1
    fldl value2
    fstl data
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    代码在value1标签处使用.float伪操作符定义了一个12.34的单精度浮点数,value2处则使用.double定义了2353.631的双精度浮点数,接着在bss未初始化段中通过.lcomm定义了一个名为data的8字节缓冲区域 (lcomm声明的标签名有点像C语言中的局部变量),后面的代码中会通过浮点指令将浮点数存储到该data缓冲区域中。

    另外,代码里使用 flds 指令来加载单精度浮点数到FPU寄存器栈中(后缀s表示single单精度的意思),使用 fldl 指令来加载双精度浮点数,最后通过 fstl 指令将栈顶寄存器st0里的浮点数以标准双精度浮点格式存储到data缓冲区域中,如果想以单精度浮点格式进行存储,则可以使用 fsts 指令。

    对代码进行汇编调试:

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

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

Breakpoint 1, _start () at floattest.s:13
13        flds value1

(gdb)

    在调试具体的代码之前,先查看下value1和value2标签处定义的浮点数在内存中的存储格式:

(gdb) x/4bx &value1
0x8049094 <value1>:    0xa4    0x70    0x45    0x41
(gdb) x/8bx &value2
0x8049098 <value2>:    0x8d    0x97    0x6e    0x12    0x43    0x63    0xa2    0x40

    上面12.34的单精度二进制浮点格式和2353.631的双精度二进制浮点格式可以通过前面的5.125的例子同理得出,不过这里12.34的二进制浮点格式中,二进制位在最后出现了舍入操作,0xa4中4的二进制0100是由0011舍入的结果,可以将代码中的.float改为.double进行测试,可以发现12.34对应的二进制格式其实是需要双精度才能完整的显示出来的,所以使用.float定义的12.34存在一个精度误差在里面,可以使用 x/f 命令来查看到:

(gdb) x/wf &value1
0x8049094 <value1>: 12.3400002

    上面输出中00002就是精度误差产生的,如果使用.double定义12.34就可以看到完整的值而不会出现误差值,.double定义的2353.631也存在这样的精度误差:

(gdb) x/gf &value2
0x8049098 <value2>: 2353.6309999999999
(gdb)

    所以,当十进制小数对应的二进制小数的科学记数形式中系数部分的二进制位数超过了单精度或双精度Significand部分能容纳的位数时,就会发生精度误差。

    在查看单精度浮点数时,需要使用 x/wf 命令,wf 表示将4字节的数据以浮点格式进行输出显示,4字节的浮点格式也就是32位的单精度浮点格式,查看双精度的浮点数则需使用 x/gf 命令。

    接下来单步调试代码:

13    flds value1
(gdb) s
14    fldl value2
(gdb) info all
...................  //省略N行输出
st0   12.340000152587890625    (raw 0x4002c570a40000000000)

    上面通过s命令单步执行完flds value1指令后,value1处定义的12.34单精度浮点数就被加载到浮点寄存器栈中了,并且保持在st0栈顶寄存器中,从 (raw 0x4002c570a40000000000) 的输出可以看到单精度浮点格式在寄存器内部被自动转为了双精度扩展格式,这里需要注意的是双精度扩展格式中Significand部分已经包含了科学记数小数点前的固定的1,这个前面提到过。

    继续单步执行:

14    fldl value2
(gdb) s
15    fstl data
(gdb) info all
...................  //省略N行输出
st0   2353.6309999999998581188265234231949    (raw 0x400a931a189374bc6800)
st1   12.340000152587890625    (raw 0x4002c570a40000000000)

    fldl指令将value2里的2353.631双精度浮点数加载到寄存器栈中后,被保存在st0栈顶寄存器中,之前载入的12.34则保持在st1中。

    再继续执行:

15    fstl data
(gdb) s
16    movl $1, %eax
(gdb) x/8bx &data
0x80490a0 <data>:    0x8d    0x97    0x6e    0x12    0x43    0x63    0xa2    0x40
(gdb) x/gf &data
0x80490a0 <data>:    2353.6309999999999
(gdb) info all
...................  //省略N行输出
st0   2353.6309999999998581188265234231949    (raw 0x400a931a189374bc6800)
st1   12.340000152587890625    (raw 0x4002c570a40000000000)

    如上所示,执行完fstl指令后,st0寄存器里的2353.63.....的值以双精度的格式存储到data缓冲区域中。

Using preset floating-point values 使用预设的浮点值:

    下表显示了IA-32平台中可以加载到FPU寄存器中的预设浮点值:

Instruction
浮点指令
Description
描述
FLD1 Push +1.0 into the FPU stack 
将正的1.0压入FPU寄存器栈
FLDL2T Push log(base 2) 10 onto the FPU stack 
将log以2为底10的对数压入FPU寄存器栈
FLDL2E Push log(base 2) e onto the FPU stack 
将log以2为底e的对数压入FPU寄存器栈
FLDPI Push the value of pi onto the FPU stack 
将3.14159...的pi的值压入FPU寄存器栈
FLDLG2 Push log(base 10) 2 onto the FPU stack 
将log以10为底2的对数压入FPU寄存器栈
FLDLN2 Push log(base e) 2 onto the FPU stack 
将log以e为底2的对数压入FPU寄存器栈
FLDZ Push +0.0 onto the FPU stack 
将正的零值压入FPU寄存器栈

    通过上面的指令可以将数学中常用的数学常量压入FPU寄存器栈,这些指令中你可能会注意到一个比较奇怪的指令:FLDZ ,该指令将+0.0压入栈,在前面的浮点格式介绍中我们知道二进制浮点格式的最高位用于表示符号位,所以对于浮点数而言是存在+0.0和-0.0的,在大多数操作中他们是等效的,不过在除法运算中,他们会产生正无穷大和负无穷大的两种不同结果。

    下面的fpuvals.s例子就演示了上表中各种指令的用法:

# fpuvals.s - An example of pushing floating point constants
.section .text
.globl _start
_start:
    nop
    fld1
    fldl2t
    fldl2e
    fldpi
    fldlg2
    fldln2
    fldz
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    进行汇编调试:

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

Reading symbols from /home/zengl/Downloads/asm_example/floattest/fpuvals...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048055: file fpuvals.s, line 6.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/floattest/fpuvals

Breakpoint 1, _start () at fpuvals.s:6

6     fld1
(gdb) s
7     fldl2t
(gdb) s
8     fldl2e
(gdb) s
9     fldpi
(gdb) s
10   fldlg2
(gdb) s
11   fldln2
(gdb) s
12   fldz
(gdb) s
13   movl $1, %eax
(gdb) info all
...................  //省略N行输出
st0   0    (raw 0x00000000000000000000)
st1   0.6931471805599453094286904741849753    (raw 0x3ffeb17217f7d1cf79ac)
st2   0.30102999566398119522564642835948945   (raw 0x3ffd9a209a84fbcff799)
st3   3.1415926535897932385128089594061862    (raw 0x4000c90fdaa22168c235)
st4   1.4426950408889634073876517827983434    (raw 0x3fffb8aa3b295c17f0bc)
st5   3.3219280948873623478083405569094566    (raw 0x4000d49a784bcd1b8afe)
st6   1    (raw 0x3fff8000000000000000)
st7   0    (raw 0x00000000000000000000)

    上面输出中st0浮点寄存器里存储的是最后一次压入栈的值即fldz指令压入的+0.0,st1存储的是倒数第二次压入栈的值即fldln2指令压入的对数,以此类推,所以st0到st6中显示的值的顺序是和指令的执行顺序相反的。

    限于篇幅,本篇就到这里,下一篇介绍SSE技术中引入的浮点格式,转载请注明来源:www.zengl.com

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

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

上一篇: 汇编数据处理 (二)

相关文章

使用IA-32平台提供的高级功能 (二)

汇编函数的定义和使用 (一)

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

汇编数据处理 (一)

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

汇编开发相关工具 (二)