之前的章节简单的介绍过FPU(浮点运算单元),在80486之前,是通过软件模拟或购买特殊的数学协处理器来处理浮点数的,在80486出现后,Intel处理器就内置了FPU浮点单元,下面就具体介绍下FPU的结构,...

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

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

    之前的章节简单的介绍过FPU(浮点运算单元),在80486之前,是通过软件模拟或购买特殊的数学协处理器来处理浮点数的,在80486出现后,Intel处理器就内置了FPU浮点单元,下面就具体介绍下FPU的结构,以及和浮点操作相关的指令。

The FPU Environment FPU结构:

    FPU浮点单元由8个数据寄存器,1个control控制寄存器(也可以叫做control word控制字),1个status状态寄存器(status word状态字),1个tag标记寄存器(tag word标记字)组成,下面对这些寄存器一一进行介绍。

The FPU register stack 8个数据寄存器构成的FPU寄存器栈:

    前面的章节,我们就接触过FPU寄存器栈,只不过当时只有个宏观的了解,其实FPU寄存器栈是由8个80位的数据寄存器构成的,这8个寄存器依次被命名为R0、R1、R2 .... R7,但是在使用这8个寄存器时,并不像EAX之类的通用寄存器那样直接用这些名字,它们被连在一起构成了一个寄存器栈,通过一个栈顶指针来访问这些寄存器,栈顶指针指向的寄存器被命名为ST(0),栈顶指针往上走,依次又可以得到ST(1)、ST(2).....ST(7),它们依次对应一个上面的R寄存器,下面通过一个简单的例子来说明:

.section .data
v1:
  .float 1
v2:
  .float 2
v3:
  .float 3
v4:
  .float 4
v5:
  .float 5
v6:
  .float 6
v7:
  .float 7
v8:
  .float 8
v9:
  .float 9
.section .text
.globl _start
_start:
  nop
  flds v1
  flds v2
  flds v3
  flds v4
  flds v5
  flds v6
  flds v7
  flds v8
  flds v9
  movl $0x1,%eax
  movl $0,%ebx
  int $0x80
 

    上面代码中通过FLD指令将v1到v9里的9个数依次压入FPU寄存器栈,在第一个FLD指令执行前,FPU里8个数据寄存器的情况如下:

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

(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file fputest.s, line 26.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/fpu/fputest

Breakpoint 1, _start () at fputest.s:26
24      flds v1
(gdb) info float
  R7: Empty   0x00000000000000000000  (st7)
  R6: Empty   0x00000000000000000000  (st6)
  R5: Empty   0x00000000000000000000  (st5)
  R4: Empty   0x00000000000000000000  (st4)
  R3: Empty   0x00000000000000000000  (st3)
  R2: Empty   0x00000000000000000000  (st2)
  R1: Empty   0x00000000000000000000  (st1)
=>R0: Empty   0x00000000000000000000 (st0)

Status Word:         0x0000                                            
                       TOP: 0

    栈顶寄存器游标ST(0)对应的R寄存器索引值存储在status状态寄存器的3个二进制位里(下面会提到),TOP 0就是那三个二进制位的值,用以表示ST(0)栈顶寄存器此时对应R0,其余的R1对应ST(1),R2对应ST(2),以此类推,当flds v1执行后,寄存器栈的情况如下:

(gdb) info float
=>R7: Valid   0x3fff8000000000000000 +1   (st0)                     
  R6: Empty   0x00000000000000000000  (st7)
  R5: Empty   0x00000000000000000000  (st6)
  R4: Empty   0x00000000000000000000  (st5)
............................................

Status Word:         0x3800                                            
                       TOP: 7

    ST(0)栈顶游标往下走,循环到顶部R7(Status Word状态寄存器也显示TOP: 7),将1存入R7里,R6对应ST(7),R5对应ST(6),以此类推。

    执行完flds v2,将2压入栈后,FPU寄存器栈情况如下:

(gdb) info float
  R7: Valid   0x3fff8000000000000000 +1       (st1)                 
=>R6: Valid   0x40008000000000000000 +2  (st0)                      
  R5: Empty   0x00000000000000000000  (st7)
  R4: Empty   0x00000000000000000000  (st6)
  R3: Empty   0x00000000000000000000  (st5)
............................................

Status Word:         0x3000                                            
                       TOP: 6

    ST(0)栈顶游标继续往下由R7移动到R6,将2存入R6中,原来的R7此时对应ST(1),R5对应ST(7),R4对应ST(6),以此类推。

    当前8个浮点数都通过FLD指令压入寄存器栈后,调试情况如下:

(gdb) info float
  R7: Valid   0x3fff8000000000000000 +1     (st7)                   
  R6: Valid   0x40008000000000000000 +2  (st6)                      
  R5: Valid   0x4000c000000000000000 +3  (st5)                      
  R4: Valid   0x40018000000000000000 +4  (st4)                      
  R3: Valid   0x4001a000000000000000 +5  (st3)                      
  R2: Valid   0x4001c000000000000000 +6  (st2)                      
  R1: Valid   0x4001e000000000000000 +7  (st1)                      
=>R0: Valid   0x40028000000000000000 +8  (st0)                      

Status Word:         0x0000                                            
                       TOP: 0
..........................................

(gdb) info all
..........................................
st0            8    (raw 0x40028000000000000000)
st1            7    (raw 0x4001e000000000000000)
st2            6    (raw 0x4001c000000000000000)
st3            5    (raw 0x4001a000000000000000)
st4            4    (raw 0x40018000000000000000)
st5            3    (raw 0x4000c000000000000000)
st6            2    (raw 0x40008000000000000000)
st7            1    (raw 0x3fff8000000000000000)
..........................................

    ST0游标转了一圈又回到R0,此时8个数据寄存器R0到R7就全部填入了Valid有效的浮点数,当再继续执行flds v9时,就会出现栈错误(因为没有更多的R寄存器可以用来存放数据了),执行完flds v9后,调试输出情况如下:

(gdb) info float
=>R7: Special 0xffffc000000000000000 Real Indefinite (QNaN) (st0)
  R6: Valid   0x40008000000000000000 +2  (st7)                      
  R5: Valid   0x4000c000000000000000 +3  (st6)                     
  R4: Valid   0x40018000000000000000 +4  (st5)
 ......................

 Status Word:         0x3a41   IE                       SF      C1      
                       TOP: 7

    ST(0)向下循环游走到R7,并将R7里原来有效的浮点数覆盖成为了一个Special特殊的QNaN类型,并在status word状态寄存器对应的二进制位上设置了IE, SF , C1的浮点栈异常标志,简单的说就是产生了浮点异常。

    以上就是FPU寄存器栈的压栈原理,以及ST0栈顶指针的游走原理,还有R数据寄存器与ST栈寄存器的对应原理,当然在平时使用FPU时,FPU内部会自动帮你处理这种对应关系,你只需了解原理即可。

    在向FPU加载数据时,除了可以使用FLD来加载浮点数外,还可以使用FILD指令来加载整数,或者使用FBLD指令来加载BCD码到FPU寄存器中,不同的数据类型在加载到FPU的数据寄存器里时,都会被自动转为浮点格式,后面还会提到一些指令用于将FPU里的数据以指定的数据格式存储到内存位置。

The FPU status, control, and tag registers FPU状态、控制以及标记寄存器:

    由于FPU是独立于主处理器的一部分,所以它不通过常规的EFLAGS寄存器来检测结果,它是通过status状态寄存器、control控制寄存器以及tag标记寄存器来检测结果和FPU的状态,下面对这三个寄存器一一进行介绍。

The status register 状态寄存器:

    status状态寄存器是一个16位的寄存器,它里面每个二进制位的含义如下表所示:

Status Bit 
状态位
Description 
描述
0 Invalid operation exception flag 
无效操作异常标志
1 Denormalized operand exception flag 
非常规操作数异常标志
2 Zero divide exception flag 
除零异常标志
3 Overflow exception flag 
溢出异常(值太大时溢出)标志
4 Underflow exception flag 
溢出异常(值太小时溢出)标志
5 Precision exception flag 
精度异常标志
6 Stack fault 
栈错误
7 Error summary status 
错误摘要状态
8 Condition code bit 0 (C0) 
条件代码位0
9 Condition code bit 1 (C1) 
条件代码位1
10 Condition code bit 2 (C2) 
条件代码位2
11-13 Top of stack pointer 
栈顶指针
14 Condition code bit 3 (C3) 
条件代码位3
15 FPU busy flag 
FPU正在计算中的忙标志

    8、9、10及14这四个条件代码位用于配合浮点异常标志来提供一些额外的错误信息。

    Error summary status(错误摘要状态位)用于当某异常发生时,如果control控制寄存器中对应的异常掩码位没被设置即没有屏蔽对应的异常,则该异常就会交由FPU执行默认处理,FPU默认处理时就会设置Error summary status标志位,并且丢弃掉错误的结果,同时FPU会产生一些异常信号来终止程序的继续执行,如果control里对应的异常掩码被设置时,对应的异常发生时就会被屏蔽掉,不会交由FPU执行默认处理,指令执行的异常结果将存储到对应的数据寄存器里,不会影响程序的继续执行,例如前面的fputest例子,默认情况下控制寄存器的掩码都是设置状态(屏蔽状态),异常的结果QNaN就会被存储在数据寄存器里而不会被FPU丢弃掉。

    status状态寄存器的前6个标志位都用于指示FPU发生的异常,当FPU计算过程中发生浮点异常时,对应的异常标志位就会被设置,这些异常标志一旦被设置,就会一直保持设置状态,除非你手动清理掉它们(比如通过重新初始化FPU的方式),另外Error summary status位被设置时也会一直保留下去除非你手动清理掉它。

    当FLD之类的指令导致寄存器栈溢出时,Stack fault栈错误标志就会被设置。

    11到13位的栈顶指针用于表示当前的ST(0)栈顶寄存器对应哪个R寄存器,这个在前面fputest例子里,在gdb调试时info float命令输出的TOP值就存储在这三个标志位里。

    在汇编程序中可以通过FSTSW指令来将status状态寄存器里的值读取到内存或读取到AX寄存器,下面的getstatus.s程式就演示了这个指令的用法:

# getstatus.s - Get the FPU Status register contents
.section .bss
    .lcomm status, 2
.section .text
.globl _start
_start:
    nop
    fstsw %ax
    fstsw status
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的代码通过FSTSW指令将FPU状态寄存器的值分别加载到AX寄存器和status内存位置,在汇编链接后,在调试器里的输出情况如下:

(gdb) x/x &status
0x804908c <status>:    0x00000000
(gdb) print/x $eax
$1 = 0x0
(gdb) info all
......................
fctrl          0x37f    895
fstat          0x0    0
ftag           0xffff    65535
......................

    从上面输出可以看到,status和EAX里的值都和info all显示的fstat状态寄存器的值一样,默认情况下status状态寄存器的值为0 。

The control register FPU控制寄存器:

    控制寄存器用于控制FPU的精度,舍入方式等,控制寄存器也是一个16位的寄存器,该寄存器的各二进制位的含义如下表所示:

Control Bits 控制位 Description 描述
0 Invalid operation exception mask 
无效操作异常掩码
1 Denormal operand exception mask 
非常规操作数异常掩码
2 Zero divide exception mask 
除零异常掩码
3 Overflow exception mask 
值过大溢出异常掩码
4 Underflow exception mask 
值过小溢出异常掩码
5 Precision exception mask 
精度异常掩码
6–7 Reserved 保留位
8–9 Precision control 精度控制
10–11 Rounding control 舍入控制
12 Infinity control 仅用于兼容286
13–15 Reserved 保留位

    头6位为异常掩码,当某个mask异常掩码被设置时,那么对应的异常发生时,就会屏蔽掉该异常,这里屏蔽的意思只是相对于FPU默认异常处理程序而言的,也就是不将异常交由FPU执行默认的处理操作,浮点的异常结果就不会被忽略掉。如果某mask异常掩码没被设置,就不会屏蔽该异常,异常发生时就会交由FPU执行默认的处理例程,默认处理例程中就会将发生异常的指令和产生的异常结果给丢弃掉,同时设置Error summary status错误摘要标志以表示默认例程捕获并处理了一个异常,还会产生异常信号来终止程序的继续执行。

    初始状态下,所有的异常掩码默认都是设置即屏蔽状态,这样即便发生了对应的浮点异常也不会产生异常信号来终止程序的继续执行。

    8到9位的Precision control精度控制位用于设置FPU内部数学计算时的浮点精度,可用的精度值如下:
  • 00 — single-precision (24-bit significand)
    单精度(24位有效二进制位)
  • 01 — not used
    没有使用
  • 10 — double-precision (53-bit significand)
    双精度(53位有效二进制位)
  • 11 — double-extended-precision (64-bit significand)
    双精度扩展(64位有效二进制位)
    默认情况下,FPU是设置为双精度扩展,当你的某些计算中不需要很高的精度时,可以将FPU设置为单精度来加快浮点计算。

    10到11位的Rounding control舍入控制位用于设置FPU如何对浮点计算的结果进行舍入操作,可用的舍入控制如下:
  • 00 — round to nearest
    舍入到最接近的值
  • 01 — round down (toward negative infinity)
    向下舍入(向负无穷大方向进行舍入)
  • 10 — round up (toward positive infinity)
    向上舍入(向正无穷大方向进行舍入)
  • 11 — round toward zero
    向零舍入
    默认情况下,FPU是设置为round to nearest(舍入到最接近的值)。

    初始状态下,control控制寄存器的默认值为0x037F,你可以使用FSTCW指令将控制寄存器的值加载到内存里。你也可以使用FLDCW指令来改变控制寄存器的值,FLDCW指令会将某内存位置里16位的值加载到控制寄存器,下面的setprec.s程式就演示了这些指令的用法:

# setprec.s - An example of setting the precision bits in the Control Register
.section .data
newvalue:
    .byte 0x7f, 0x00
.section .bss
    .lcomm control, 2
.section .text
.globl _start
_start:
    nop
    fstcw control
    fldcw newvalue
    fstcw control
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的代码里,先用fstcw control指令将control控制寄存器里的初始值存储到control内存位置,再由fldcw newvalue指令将newvalue内存里的0x007f(注意内存定义时用的小字节序)加载到control控制寄存器,这样就将control寄存器里的8和9的精度位设置为00即单精度,最后再由fstcw control指令将修改后的控制寄存器值存储到control内存里。

    下面是程序调试运行的输出情况:

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

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

Breakpoint 1, _start () at setprec.s:11
11        fstcw control

(gdb) x/x &control
0x804909c <control>:    0x00000000
(gdb) s
12        fldcw newvalue
(gdb) x/x &control
0x804909c <control>:    0x0000037f
(gdb) s
13        fstcw control
(gdb) s
14        movl $1, %eax
(gdb) x/x &control
0x804909c <control>:    0x0000007f
(gdb) info all
..........................
fctrl          0x7f    127
..........................

(gdb)

    可以看到control控制寄存器里的值一开始是0x037f,经过fldcw newvalue指令后,FPU控制寄存器里的值就被成功修改为0x7f了,从而将FPU的计算精度由双精度扩展改为单精度。

    FPU设置为单精度后并不一定会加快所有的浮点计算,主要是在一些除法和平方根计算里会提升性能。

The tag register FPU标记寄存器:

    标记寄存器用于标识FPU的8个数据寄存器里存储的是什么样的值,tag标记寄存器也是16位的寄存器,如下图所示:


图1

    tag寄存器里每2位对应一个R数据寄存器,这两位可以表示的数据内容如下:
  • A valid double-extended-precision value (code 00)
    一个有效的双精度扩展值(对应二进制值为00)
  • A zero value (code 01)
    一个0值(对应二进制值为01)
  • A special floating-point value (code 10)
    一个特殊的浮点值(对应二进制值为10)
  • Nothing (empty) (code 11)
    没有存放值即初始状态时的空值(对应二进制值为11)
    通过检测tag标记寄存器,就可以在程序里快速的判断某个数据寄存器里存放的是否是一个有效的值,而不需要手动读取寄存器的值来进行分析。

Using the FPU stack 使用FPU寄存器栈:
[zengl pagebreak]
Using the FPU stack 使用FPU寄存器栈:

    下面通过stacktest.s例子来说明如何使用FPU寄存器栈:

# stacktest.s - An example of working with the FPU stack
.section .data
value1:
    .int 40
value2:
    .float 92.4405
value3:
    .double 221.440321
.section .bss
    .lcomm int1, 4
    .lcomm control, 2
    .lcomm status, 2
    .lcomm result, 4
.section .text
.globl _start
_start:
    nop
    finit
    fstcw control
    fstsw status
    filds value1
    fists int1
    flds value2
    fldl value3
    fst %st(4)
    fxch %st(1)
    fstps result
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码中一开始就使用FINIT指令来初始化FPU,该指令可以将FPU的control控制寄存器和status状态寄存器的值初始化为默认值,但它不会修改FPU的数据寄存器,在使用FPU的程序里用FINIT指令进行初始化是个好的编程习惯。

    接着代码里通过FSTCW和FSTSW指令将控制寄存器和状态寄存器里的值分别存储到control和status内存位置,在gdb调试器下可以用x命令来查看这些存储在内存里的值:

(gdb) x/2bx &control
0x80490c8 <control>:    0x7f    0x03
(gdb) x/2bx &status
0x80490ca <status>:    0x00    0x00
(gdb)

    上面输出中,control内存里的值为0x037f,说明控制寄存器里的默认值就是0x037f,status内存里为0,说明状态寄存器的默认值就是0 。

    接下来,代码通过filds value1将value1内存里的整数加载到ST0栈顶寄存器,再由fists int1指令将ST0栈顶寄存器的值存储到int1内存位置,这里FILDS和FISTS指令都是S后缀,表示操作数是一个32位的数据,这里表示操作的是32位的整数值,执行完这两条指令后,gdb调试器的输出如下:

(gdb) info all
.......................
st0            40    (raw 0x4004a000000000000000)
.......................

(gdb) x/d &int1
0x80490c4 <int1>:    40
(gdb) x/4bx &int1
0x80490c4 <int1>:    0x28    0x00    0x00    0x00
(gdb)

    从上面的输出可以看到,ST0栈顶寄存器和int1内存里的值都为40,和value1里的整数值一致,另外,从info all的输出可以看到st0里的40对应的raw原始二进制值是双精度扩展浮点格式,也就是说FILDS指令在将整数加载到FPU后,在数据寄存器里会自动转为双精度扩展格式,而从x/4bx &int1命令的输出可以看到,在FISTS指令将FPU数据寄存器里的值存储到内存位置时,又会自动将双精度扩展浮点格式转为32位整数的二进制格式。

    再接下来,代码通过flds value2和fldl value3指令将value2里的单精度浮点数和value3里的标准双精度浮点数依次加载到FPU寄存器栈里,这两条指令执行完后,调试器的输出情况如下:

(gdb) info all
........................
st0            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
st1            92.44049835205078125    (raw 0x4005b8e1890000000000)
st2            40    (raw 0x4004a000000000000000)
........................

    st0栈顶寄存器始终指向最后一次压入栈的数据,FLDS指令使用S后缀表示要加载的数据是32位的单精度浮点数,FLDL指令使用L后缀表示要加载的数据是64位的双精度浮点数。

    在将value1、value2及value3三个内存里的值加载到FPU寄存器栈后,代码通过fst %st(4)指令来将ST0栈顶寄存器的值拷贝到ST4,在GNU汇编里引用ST寄存器时,数字需要用括号括起来,该指令执行后,调试情况如下:

(gdb) info all
...........................
st0            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
st1            92.44049835205078125    (raw 0x4005b8e1890000000000)
st2            40    (raw 0x4004a000000000000000)
st3            0    (raw 0x00000000000000000000)
st4            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
...........................

    从info all的输出可以看到ST0的值成功拷贝到ST4中。接着,代码使用fxch %st(1)指令将ST0和ST1里的值进行交换,交换的结果如下:

(gdb) info all
..........................
st0            92.44049835205078125    (raw 0x4005b8e1890000000000)
st1            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
st2            40    (raw 0x4004a000000000000000)
st3            0    (raw 0x00000000000000000000)
st4            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
..........................

    最后,程序通过fstps result指令将ST0的值弹出到result内存位置,FSTPS指令和前面的FST指令的区别在于:FST在获取FPU数据时,ST0栈顶的值保持不变,而FSTPS指令在将ST0栈顶的值存储到目标位置后,还会将ST0栈顶的值弹出寄存器栈,所有ST寄存器栈里的数据都会往上挪一格,如下所示:

(gdb) info all
............................
st0            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
st1            40    (raw 0x4004a000000000000000)
st2            0    (raw 0x00000000000000000000)
st3            221.44032100000001150874595623463392    (raw 0x4006dd70b8e086bdf800)
st4            0    (raw 0x00000000000000000000)
............................

(gdb) x/fw &result
0x80490cc <result>:    92.4404984
(gdb) x/4bx &result
0x80490cc <result>:    0x89    0xe1    0xb8    0x42
(gdb)

    可以看到原来的栈顶值被成功弹出,并存储到result内存位置。

    有关FPU寄存器栈和FPU内部寄存器的结构就介绍到这里,下一篇开始介绍FPU里的基础数学运算指令。

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

下一篇: 高级数学运算 (二) 基础浮点运算

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

相关文章

汇编开发相关工具 (二)

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

汇编里使用Linux系统调用 (三) 系统调用结束篇

汇编开发相关工具 (三) 工具介绍结束篇,kdbg,gprof,mepis

调用汇编模块里的函数 (一)

基本数学运算 (二) 减法和乘法运算