先看下面幾個問題,如果你能準確地回答,那麼此篇文章將不適合你:
- 電腦中怎樣表示浮點數的,與整型的表示方法有什麼不同?
- 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 × 23單精確度: 符號位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 mainBreakpoint 1 at 0x8048415: file test.c, line 5.(gdb) runStarting program: /home/guohl/Documents/AS/test Breakpoint 1, main () at test.c:55 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 0x80484e00x80484e0: 2.5(gdb) x/2xw 0x80484e00x80484e0: 0x00000000 0x40040000(gdb) x/8tb 0x80484e00x80484e0: 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 0x80484d80x80484d8: 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