從IEEE754標准談C語言浮點數據類型


先看下面幾個問題,如果你能准確地回答,那么此篇文章將不適合你:

  • 計算機中怎樣表示浮點數的,與整型的表示方法有什么不同?
  • 32位精度的float類型和64位精度的double類型能表示浮點數最大范圍是多少?
  • 該C語言語句 printf("%d\n", 2.5);  輸出結果是什么,為什么?

我先說在此之前我如果回答,答案如下:

  • 計算機中有符號整型采用補碼進行表示,浮點型怎么表示沒想過。
  • float類型可以表示-232-1~232,double類型可以表示-264-1~264
  • 輸出格式要求輸出整型,而數是浮點型,類型轉化之后輸出結果為2。

有一點可以明確,我的回答都是錯誤的。那好吧,下面是我查看一些資料總結出來的,希望能解釋清楚其中的”奧秘“。

 

IEEE754標准(以下簡稱”標准“)是使用最廣泛的浮點數運算標准,為許多CPU與浮點運算器所采用。該標准定義了表示浮點數的格式,如下圖所示:

下面只討論二進制浮點數的表示,分成了三個部分:

符號位、指數、尾數,它們的含義可以類比科學計數法。如:

科學計數法中: 

(102.35045)10 = +1.0235045 × 102  符號位為正,指數是2,尾數是1.0235045。

(-0.00023103)10 = -2.3103 × 10-4  符號位為負,指數是-4,尾數是2.3103。

同樣在規格化二進制浮點數中:

(1001.0111010)2 = +1.001011101 × 23 符號位為正,指數是3,尾數是1.001011101。

(-0.0001010011)2 = -1.010011 × 2-4 符號位為負,指數是-4,尾數是1.010011。

由上面的實例可以知道,在二進制浮點數被規格化后,尾數的格式都是1.****,指數表示將小數點移動多少位可以實現規格化(向左指數加1,向右指數減1),因此可以正也可為負。

標准同時規定:

  • 符號位用1位表示,0表示正數,1表示負數;
  • 指數采用移碼表示(原來的實際的指數值加上一個固定值得到的),這個固定值為2e-1-1(e為指數部分比特長度),之所以加上這個偏移量,是為了將負數變成非負數,這樣兩個指數的大小很容易就可以比較。
  • 尾數采用原碼表示,正如上所說,規格化二進制浮點數最高位均為1,那么小數點前這個就沒必要用一個比特位去存儲,我們默認已經存在,稱為”隱藏位“。

標准規定了四種浮點數的表示方式:單精確度(32位)、雙精確度(64位)、延伸單精確度(43比特以上,很少使用)與延伸雙精確度(79比特以上,通常以80比特實做)。C語言中float和double浮點型分別對應的是單精度和雙精度浮點數,下面介紹這兩種浮點數的存儲格式:

如上面兩個例子,分別使用單精度和雙精度表示如下:

(1001.0111010) 2 = +1.001011101 × 2 3
單精度: 符號位0,指數位為3+127=130(10000010),尾數1.001011101隱藏最高位1之后為001011101,因此表示為:
0 10000010 00101110100000000000000
雙精度:只是在指數位上加的偏移量不同,3+1023=1026(10000000010),表示為:
0 10000000010 0010111010000000000000000000000000000000000000000000
(-0.0001010011) 2 = -1.010011 × 2 - 4
單精度:符號位1,指數位為-4+127=123(1111011),尾數1.010011 隱藏最高位1之后為010011,因此表示為:
0 01111011 01001100000000000000000
雙精度:指數位為-4+1023=1019(1111111011),表示為:
0 01111111011 0100110000000000000000000000000000000000000000000000

至此,應該已經解釋清楚了浮點數在計算機中的存儲格式和方法了,也就等於回答了上面的第一個問題,至於第二個問題,如果理解了上面所說的,求浮點數表示的范圍就應該很簡單了,下表為單精度浮點數各種極值情況:

至於最后一個問題,我們寫一個C語言程序進行測試:

#include <stdio.h>

int main()
{
        printf("%d\n", 2.5);
        return 0;
}

編譯運行結果如下:

[guohl@guohl]$ gcc -o test test.c -g
[guohl@guohl]$ ./test 
0

運行結果和我們預期的2不一樣,使用gdb調試,在main函數處插入斷點,並且反匯編main函數之后得到:

(gdb) break main
Breakpoint 1 at 0x8048415: file test.c, line 5.
(gdb) run
Starting program: /home/guohl/Documents/AS/test 

Breakpoint 1, main () at test.c:5
5        printf("%d\n", 2.5);
(gdb) disassemble 
Dump of assembler code for function main:
   0x0804840c <+0>:    push   %ebp
   0x0804840d <+1>:    mov    %esp,%ebp
   0x0804840f <+3>:    and    $0xfffffff0,%esp
   0x08048412 <+6>:    sub    $0x10,%esp
=> 0x08048415 <+9>:    fldl   0x80484e0
   0x0804841b <+15>:    fstpl  0x4(%esp)
   0x0804841f <+19>:    movl   $0x80484d8,(%esp)
   0x08048426 <+26>:    call   0x80482f0 <printf@plt>
   0x0804842b <+31>:    mov    $0x0,%eax
   0x08048430 <+36>:    leave  
   0x08048431 <+37>:    ret    
End of assembler dump.

fldl addr 指令將內存addr中的雙精度浮點數加載到FPU寄存器堆棧,fstpl value 將雙精度數據從FPU寄存器堆棧出棧,保存到value中。因此,

0x08048415 <+9>: fldl 0x80484e0 
0x0804841b <+15>: fstpl 0x4(%esp) 
首先取出內存0x80484e0處的雙精度浮點數加載到FPU寄存器st0中,再從st0中取出放到esp-4處。先使用gdb -x命令查看內存0x80484e0處的內容:
(gdb) x/fg 0x80484e0
0x80484e0:    2.5
(gdb) x/2xw 0x80484e0
0x80484e0:    0x00000000    0x40040000
(gdb) x/8tb 0x80484e0
0x80484e0:    00000000    00000000    00000000    00000000    00000000    00000000    00000100    01000000
從上可以看到,以雙字的小數查看結果為2.5,由於我們平台采用的是小端格式存儲(little-edian,低位字節存儲在低內存位置),所以將以字節查看得到的結果恢復成下面的表示方法:
01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000

我們用IEEE754標准的雙精度格式解析上面這段二進制,符號位為0,即為正;指數位為10000000000(1024)減去偏移量1023為1;尾數0100…000,加上隱藏位1,為1.01(即十進制1.25)。所以結果為+1.25×21 = 2.5,符合我們的預期。

那么fstpl指令將該浮點數加載到esp-4處作為printf函數的參數,再接着指令“movl $0x80484d8,(%esp) ”將輸出格式控制符"%d" 的指針保存到esp指向的位置作為printf函數的函數,我們可以使用gdb查看內存0x80484d8處是不是格式控制符字符串:

(gdb) x/4cb 0x80484d8
0x80484d8:    37 '%'    100 'd'    10 '\n'    0 '\000'
確實如我們所想,現在在調用printf之前函數堆棧的結構如下所示:

進入printf函數,解析第一個參數輸出格式控制字符串,遇到%d,函數從之前壓棧的參數取出一個整型即取到上圖中esp+4處的值,以整型數輸出,為0。這就是我們上面運行./test 的輸出結果,而不是我想當然的程序會將2.5強制類型轉化為整型得到2!

 

 

參考資料:

http://zh.wikipedia.org/wiki/IEEE_754

Richard Blum, Professional Assembly Language

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM