0.1 連續使用兩個scanf輸入兩個數字:
#include <stdio.h>
int main()
{
int a;
scanf("%d",&a); // 連續輸入若干個數字回車(標準輸入輸出是行緩沖)
printf("%d\n",a);
int b;
scanf("%d",&b); // 連續輸入若干個數字回車
printf("%d\n",b);
getchar();// 試圖停住控制臺窗口
}
上面并沒有停住控制臺窗口,原因是在輸入的緩沖區內有一個回車字符可讀取,并不需要等待輸入。
上面的輸入流程是這樣的:連續輸入若干個數字回車,連續輸入的數字給到a,但在輸入緩沖區內還有一個回車字符可讀取,當再輸入一串數字回車后,scanf讀取數值型數據的規則是,忽略掉前面的包括回車符在內的前導空白字符(white space)。所以會忽略掉前面的回車符,但后面的回車符還留在緩沖區內,此時被getchar()接收到,程序結束。
下面的程序可以驗證并停住控制臺窗口
#include <stdio.h>
int main()
{
int a;
scanf("%d",&a); // 輸入一串數字后回車,行緩沖
printf("%d\n",a);
int b;
scanf("%d",&b); // 輸入一串數字后回車
printf("%d\n",b);
char ch=getchar();// 試圖停住控制臺窗口
printf("%d\n",ch); // 10,回車的ASCII是10
getchar();
}
也就是說,字符串的輸入并不會忽略掉空白字符:
#include <stdio.h>
int main()
{
int a;
scanf("%d",&a); // 輸入一串數字后回車
printf("%d\n",a);
char b;
scanf("%c",&b); // 輸入一個字符后回車
printf("%c\n",b);
char ch=getchar();// 試圖停住控制臺窗口
printf("%d\n",ch); // 10,回車的ASCII是10
getchar();
}
在兩個輸入之間清理一下緩沖區:
#include <stdio.h>
int main()
{
int a;
scanf("%d",&a); // 輸入一串數字后回車
printf("%d\n",a);
fflush(stdin); // 將緩沖區的數據刷掉
char b;
scanf("%c",&b); // 輸入一個字符后回車
printf("%c\n",b);
fflush(stdin); // 將緩沖區的數據刷掉
// c++:
// std::cin.ignore();// 忽略掉回車字符'\n'
getchar();
}
輸入輸出緩沖區是用于輸入輸出的,一塊臨時存儲數據的內存區域,可以理解為一個一定大小(由函數庫定義)的字符數組。
緩沖區中有一個隱式指針用于跟蹤字符處理的位置。
C++的三類緩沖區:
2.1 數據批量準備后批量處理。
2.2 輸入輸出設備與內存讀寫速度匹配。
從內存中讀取數據要比從文件中讀取數據要快得多。
對文件的讀寫需要用到open、read、write等系統底層函數,而用戶進程每調用一次系統函數都要從用戶態切換到內核態,等執行完畢后再返回用戶態,這種切換要花費一定時間成本(對于高并發程序而言,這種狀態的切換會影響到程序性能)。
在stdio.h中定義一個宏BUFSIZ,定義了緩沖區的大小。
// STDIO.H
/* Buffered I/O macros */
#if defined(_M_MPPC)
#define BUFSIZ 4096
#else /* defined (_M_MPPC) */
#define BUFSIZ 512
#endif /* defined (_M_MPPC) */
文件輸入輸出的緩沖區大小在定義文件的結構體中定義:
struct _iobuf {
char *_ptr; // 文件輸入的下一個位置
int _cnt; // 當前緩沖區的相對位置
char *_base; // 文件的起始位
int _flag; // 文件標志
int _file; // 有效性驗證
int _charbuf; // 緩沖區的檢査,若無此成員,則不讀取
int _bufsiz; // 文件大小
char *_tmpfname; // 臨時文件名
};
typedef struct _iobuf FILE;
也可以設置緩沖區:
setbuf(stdin,NULL);
緩沖區 分為三種類型:全緩沖、行緩沖和不帶緩沖。
4.1 全緩沖
在這種情況下,當填滿標準I/O緩存后才進行實際I/O操作。全緩沖的典型代表是對磁盤文件的讀寫。
文件操作演示全緩沖
#include<fstream>
using namespace std;
int main()
{
//創建文件test.txt并打開
ofstream outfile("test.txt");
//向test.txt文件中寫入4096個字符’a’
for(int n=0;n< 4096;n++)
{
outfile << 'a';
}
//暫停,按任意鍵繼續
system("PAUSE");
//繼續向test.txt文件中寫入字符’b’,也就是說,第4097個字符是’b’
outfile << 'b';
//暫停,按任意鍵繼續
system("PAUSE");
return 0;
}
上面這段代碼的目的是驗證Windows下全緩沖的大小是4096個字節,并驗證緩沖區滿后會刷新緩沖區,執行真正的I/O操作。
此時打開工程所在文件夾,您會發現test.txxt是空的,這說明4096個字符“a”還在緩沖區,并沒有真正執行I/O操作。敲一下回車鍵,此時會發現test.txt文件的大小雖然是0KB,但打開文件你會發下該文件中已經有了4096個字符“a”。這說明全緩沖區的大小是4K(4096),緩沖區滿后執行了I/O操作,而字符“b”還在緩沖區。
再次敲一下回車鍵,你會發現test.txt文件的大小變成了5KB,此時再打開test.txt文件,您就會發現字符“b”也在其中了。這一步驗證了文件關閉時刷新了緩沖區。
4.2 行緩沖
在這種情況下,當在輸入和輸出中遇到換行符時,執行真正的I/O操作。這時,我們輸入的字符先存放在緩沖區,等按下回車鍵換行時才進行實際的I/O操作。典型代表是鍵盤輸入數據。
鍵盤操作演示行緩沖
#include <iostream>
using namespace std;
int main()
{
char c;
//第一次調用getchar()函數
//程序執行時,您可以輸入或復制一串字符并按下回車鍵,按下回車鍵后該函數才返回
c=getchar();
//顯示getchar()函數的返回值
cout << c << endl;
//暫停
system("PAUSE");
//循環多次調用getchar()函數
//將每次調用getchar()函數的返回值顯示出來
//直到遇到回車符才結束
while((c=getchar())!='\n')
{
printf("%c",c);
}
//暫停
system("PAUSE");
return 0;
}
getchar()函數的執行就是采用了行緩沖。第一次調用getchar()函數,會讓程序使用者(用戶)輸入一行字符并直至按下回車鍵 函數才返回。此時用戶輸入的字符和回車符都存放在行緩沖區。
再次調用getchar()函數,會逐步輸出行緩沖區的內容。
你將上個實例文件test.txt中的字符復制粘入到控制臺,您會發現無法繼續輸入字符,說明緩沖區已滿。
4.3 不帶緩沖
也就是不進行緩沖,標準出錯情況stderr是典型代表,這使得出錯信息可以直接盡快地顯示出來。
下列情況會引發緩沖區的刷新:
① 緩沖區滿時;
② 執行flush或fflsuh語句;(flush:沖洗,沖走)
③ 執行endl語句;(end line)
④ 關閉文件。
可見,緩沖區滿或關閉文件時都會刷新緩沖區,進行真正的I/O操作。另外,在C++中,我們可以使用flush函數來刷新緩沖區(執行I/O操作并清空緩沖區),如:cout << flush; //將顯存的內容立即輸出到顯示器上進行顯示。刷新字面上的意思是用刷子刷,把原來舊的東西變新了,這里就是改變的意思,例如像緩沖區溢出的時候,多余出來的數據會直接將之前的數據覆蓋,這樣緩沖區里的數據就發生了改變。
endl控制符的作用是將光標移動到輸出設備中下一行開頭處,并且清空緩沖區。
cout < < endl;
相當于
cout << ”\n”<< flush;
cache分為好幾類,有物理上的緩存,也有磁盤上對文件的暫存:
6.1 CPU的Cache
CPU的Cache,它中文名稱是高速緩沖存儲器,讀寫速度很快,幾乎與CPU一樣。由于CPU的運算速度太快,內存的數據存取速度無法跟上CPU的速度,所以在cpu與內存間設置了cache為cpu的數據快取區。當計算機執行程序時,數據與地址管理部件會預測可能要用到的數據和指令,并將這些數據和指令預先從內存中讀出送到Cache。一旦需要時,先檢查Cache,若有就從Cache中讀取,若無再訪問內存,現在的CPU還有一級cache,二級cache。簡單來說,Cache就是用來解決CPU與內存之間速度不匹配的問題,避免內存與輔助內存頻繁存取數據,這樣就提高了系統的執行效率。
6.2 磁盤的Cache
磁盤也有cache,硬盤的cache作用就類似于CPU的cache,它解決了總線接口的高速需求和讀寫硬盤的矛盾以及對某些扇區的反復讀取。
6.3 瀏覽器的Cache
瀏覽器緩存(Browser Caching)是為了節約網絡的資源加速瀏覽,瀏覽器在用戶磁盤上對最近請求過的文檔進行存儲,當訪問者再次請求這個頁面時,瀏覽器就可以從本地磁盤顯示文檔,這樣就可以加速頁面的閱覽,并且可以減少服務器的壓力。這個過程與下載非常類似,不過下載是用戶的主動過程,并且下載的數據一般是長時間保存,游覽器的緩存的數據只是短時間保存,可以人為的清空。
Buffer的核心作用是用來緩沖,緩和沖擊(對輸出設備的沖擊,包括磁盤、打印機、顯示器)。比如你每秒要寫100次硬盤,對系統沖擊很大,浪費了大量時間在忙著處理開始寫和結束寫這兩件事嘛。用個buffer暫存起來,變成每10秒寫一次硬盤,對系統的沖擊就很小,寫入效率高了,日子過得爽了,極大的緩和了沖擊。
Cache的核心作用是加快取用的速度(加快讀取速度,包括CPU讀內存(塊讀取放到物理上的緩存)、內存讀磁盤、用戶通過瀏覽器請求資源)。比如你一個很復雜的計算做完了,下次還要用結果,就把結果放手邊一個好拿的地方存著,下次不用再算了。加快了數據取用的速度。
緩存是內存上的一塊臨時存儲區域。
緩沖區溢出概念中的緩沖區與上述的緩沖區概念又稍有區別,緩沖區溢出的緩沖區是指應用程序用來保存用戶輸入輸出的數據、臨時存放數據的內存空間。
如果用戶輸入的數據長度超出了程序為其分配的內存空間,這些數據就會覆蓋程序為其它數據分配的內存空間,形成緩沖區溢出。如果程序存在緩沖區溢出的漏洞,用戶向程序傳遞一個走出其長度的字符串時,如果不是刻意構造的字符串,一般只會出現分段錯誤Segmentation fault),而不能達到攻擊的目的。如果攻擊者通過制造緩沖區溢出使程序運行一個用戶shell,再通過shell執行其它命令。如果該程序屬于root且有suid權限的話,攻擊者就獲得了一個有root權限的shell,可以對系統進行任意操作了。
#include <stdio.h>
#include <string.h>
#define PASS_WORD "1234567"
int verify_password(char * password)
{
int authentitated;
char buffer[8]; // 棧按字長對齊
authentitated=strcmp(password,PASS_WORD); // 如果兩個字符串相等,返回值是0
strcpy(buffer,password); // 溢出后的字符存入authentitated
return authentitated;
}
int main()
{
int valid_flag=0;
char password[1024]={0};
while (1)
{
printf("please input password:");
scanf("%s",password); // 如果輸入任意的8個字符,最后一個字符'\0'會溢出,
// 溢出的字符會占用authentitated的空間
valid_flag=verify_password(password);
if(valid_flag)
printf("incorrect password!\r\n");
else
printf("Congratulation ! you have passed the verification !\r\n");
}
return 0;
}
棧幀圖:
-----------------
| buffer[0] |
-----------------
| buffer[…] |
-----------------
| buffer[7] |
-----------------
| authenticated |
-----------------
| EBP |
-----------------
緩沖區溢出除了上述的靜態數據溢出,通常還包括棧溢出和堆溢出,也就是數據超出了堆戈 棧的存儲空間。
-End-
者: threepwn 合天智匯
和我一樣,有一些計算機專業的同學可能一直都在不停地碼代碼,卻很少關注程序是怎么執行的,也不會考慮到自己寫的代碼是否會存在棧溢出漏洞,借此機會我們一起走進棧溢出。
在了解棧溢出之前我們先了解一下程序執行過程
程序的執行過程可看作連續的函數調用。當一個函數執行完畢時,程序要回到call指令的下一條指令繼續執行,函數調用過程通常使用堆棧實現
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
test1(1);
test2(2);
test3(3);
return 0;
}
int test1(int test1){
int a=6;
printf("1");
return 1;
}
int test2(int test2){
printf("2");
return 2;
}
int test3(int test3){
printf("3");
return 3;
}
編譯成32位可執行文件,放在ollydbg中就行調試,來詳細看一下執行過程
因為程序的執行可以看做一個一個函數的執行(main函數也一樣),因此我們挑選其中一個即可,在test1()函數設置斷點
F7單步調試第一步mov dword ptr ss:[esp],0x1,進行傳參,簡潔明了。
第二步call mian.00401559,進入test(),這里我們關注一下esp和棧頂值,將該指令的下一條指令的地址進行壓棧,既然有壓棧那么就會有出棧,這就與函數中的retn指令形成呼應。
第三步push ebp,就是把ebp的值進行壓棧,那么這個ebp是什么呢?有什么用呢?
EBP叫做擴展基址指針寄存器(extended base pointer) ,里面放一個指針,該指針指向系統棧最上面一個棧幀的底部,用于C運行庫訪問棧中的局部變量和參數。那么這一步的意義就是:保存舊棧幀中的幀基指針以便函數返回時恢復舊棧幀
第四步,mov ebp,esp,將esp的值放在ebp中,我們再來了解一下什么是esp?
ESP(Extended Stack Pointer)為擴展棧指針寄存器,是指針寄存器的一種,用于存放函數棧頂指針,指向棧的棧頂(下一個壓入棧的活動記錄的頂部),也就是它不停在變,剛才提到的ebp指向棧底,在函數內部執行過程中是不變。
那么我們再看一下這一步的作用:
從第三步可以知道esp存儲的值是舊棧幀中的幀基指針,而esp值棧頂指針,隨時都在變,因此為了函數結束后能恢復,把esp值(外層函數棧底地址)保存在本函數棧底ebp中。簡而言之,將內部函數ebp的值作為地址,它存放外函數的ebp的值。這一步在末尾也存在逆向指令leave。
第五步是sub esp,0x28,開辟該函數的局部變量空間
緊接著第六步mov dword ptr ss:[ebp-0xC],0x6,給變量a一個大小是0xC的空間,并且賦值。
然后就是傳參字符1的ascii碼,調用printf函數,把返回值放到eax。
我們重點來看leave指令,可以發現ebp的值恢復了,esp的值也變了,相當于mov esp,ebp;pop ebp
最后執行retn指令,至此一個函數執行完畢,esp和eip的值都被改變,相當于pop eip,然后程序繼續執行。EIP是指令寄存器,存放當前指令的下一條指令的地址。CPU該執行哪條指令就是通過EIP來指示的
分析完這一過程,相信大家對函數是怎么執行的應該明朗了,那么我們言歸正傳,繼續聊一下棧溢出。首先我們先看一下什么是棧?
棧可以看作是一個漏斗,棧底地址大,棧頂地址小,然后在一個存儲單元中,按照由小到大進行存儲,它的目的是賦予程序一個方便的途徑來訪問特定函數的局部數據,并從函數調用者那邊傳遞信息。
棧溢出屬于緩沖區溢出,指的是程序向棧中某個變量中寫入的字節數超過了這個變量本身所申請的字節數,因而導致與其相鄰的棧中的變量的值被改變。
另外,我們也不難發現,發生棧溢出的基本前提是:程序必須向棧上寫入數據、寫入的數據大小沒有被良好地控制。引用一個例子來了解一下棧溢出
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}
很顯然符合以上兩個條件,gets()成為突破口我們在主函數處下斷點,運行和調試
lea eax,dword ptr ss:[ebp-0x14] 這時開辟一個空間給變量,也即是s,如圖所示
我們想執行sucess()函數,要怎么辦呢?
執行完vulnerable()函數后,會還原ebp,改變esp的值(leave),然后retn,也就是pop eip,然后CPU根據eip指針指向的指令繼續運行。
我們能抓到的點就是控制eip,怎么控制?通過控制棧頂的值,那么棧頂的值是什么?棧頂的值是進入該函數時儲存的下一條指令的地址。這里提一點,進入函數,要保存兩個值:下一條命令的地址、EBP舊棧幀的幀基指針,只有這樣才能完全恢復。
此時我們可以構造payload,來控制我們要控制的地方,棧中存儲EBP值的存儲單元的上一個存儲單元,也就是圖中的存儲address的存儲單元
我們先試驗一下輸入0x14 *'A'+BBBB+0000,發生的變化
很好,按照我們的預想進行(python -c 'print "A"* 0x18+p32(0x00401520)') 就可以達到棧溢出的效果
還沒有入門,只是個人的見解,如有錯誤,希望各位大佬指出。參考:https://en.wikipedia.org/wiki/Stack_buffer_overflowhttps://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/stackoverflow-basic-zh/
高級棧溢出技術—ROP實戰
http://hetianlab.com/expc.do?ce=a763263a-de03-4368-a917-76bfb39f8c96
(學習ROP概念及其思路,了解高級棧溢出時需要注意的事項,并掌握解決方法)
聲明:筆者初衷用于分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為后果自負,與合天智匯及原作者無關!
點表示對形如的有理數進行編碼。
直到 20 世紀 80 年代,每個計算機制造商都設計了自己的表示浮點數的規則,以及對浮點數執行運算的細節。另外,它們常常不會太多地關注運算的精確性,而把實現的速度和簡便性看得比數字精確性更重要。
大約在1985 年,這些情況隨著IEEE 標準754 的推出而改變了,這是一個仔細制訂的表示浮點數及其運算的標準。這項工作是從1976 年開始由Intel 贊助的,與8087 的設計同時進行,8087 是一種為8086 處理器提供浮點支持的芯片。他們請William Kahan(加州大學伯克利分校的一位教授)作為顧問,幫助設計未來處理器浮點標準。他們支持Kahan加人一個IEEE 資助的制訂工業標準的委員會。這個委員會最終采納的標準非常接近于Kahan 為Intel 設計的標準。目前,實際上所有的計算機都支持這個后來被稱為IEEE 浮點的標準。這大大提高了科學應用程序在不同機器上的可移植性。
IEEE浮點標準用的形式來表示一個數:
① 符號(sign),s決定這個數是負數(s=l)還是正數(s=0),而對于數值0的符號位解釋,作為特殊情況處理。
② 尾數(significand),M是一個二進制小數,它的范圍是1~2-,或者是0~1-。
③ 階碼(exponent),E的作用是對浮點數加權,這個權重是2的E次冪(可能是負數)。
E在后面所述的規格化和非規格化表示時有所區別。
將浮點數的位表示劃分為三個字段,分別對這些值進行編碼:
(1) 一個單獨的符號位s,直接編碼符號s。
(2) k位的階碼字段,編碼階碼E。
(3) n位小數字段,編碼尾數M,但是編碼出來的值也依賴于階碼字段的值是否等于0。
在單精度浮點格式(C 語言中的float)中,s、 exp 和 frac 字段分別為 1 位、k=8 位和 n=23 位,得到一個 32 位的表示。
在雙精度浮點格式(C 語言中的double)中,s、exp 和 frac 字段分別為 1 位、k=11 位和 n=52 位,得到一個64 位的表示。
階碼的值決定了這個數是規格化的、非規格化的或特殊值:
規格化的值的階碼字段被解釋為以偏置(biased)形式表示的有符號整數。也就是說,階碼的值是E=e-Bias,其中e 是無符號數,是一個等于(單精度是127,雙精度是1023)的偏置值。由此產生指數的取值范圍,對于單精度是-126~+127, 而對于雙精度是-1022~+1023。尾數定義為M=1+f。
當階碼域為全0時,所表示的數是非規格化形式。在這種情況下,階碼值是E=1-Bias,而尾數的值是M=f,也就是小數字段的值,不包含隱含的開頭的1。
使階碼值為1- Bias 而不是簡單的-bias 似乎是違反直覺的。這種方式提供了一種從非規格化值平滑轉換到規格化值的方法。
6 位浮點格式可表示的值(k=3的階碼位和 n=2的尾數位。偏置量是 3:
需要注意的是,浮點數加法和乘法不滿足結合律 ,也不滿足乘法對加法的分配律,以下舉例說明:
(3.14+1e10)-1e10=0, // 3.14因為精度被略掉了
3.14+(1e10-1e10)=3.14,
(1e20 *1e20) * 1e-20=inf,
1e20 * (1e20 * 1e-20)=1e20
1e20 * (1e20 - 1e20)=0.0,
1e20 * 1e20 - 1e20 * 1e20=NaN
這些特殊的數學性質對于科學計算程序員和編譯器的優化限制都具有重要意義,舉例如下:
x=a + b + c;
y=b + c + d;
// 編譯器可能試圖通過產生下列代碼來省去一個浮點加法
t=b + c;
x=a + t;
y=t + d;
// 但是對x來說,這個計算可能會產生于原始值不同的值,因為它使用了加法運算的不同結合方式
浮點數的加減運算分為五步:
如有
X=0.1011×2^3
Y=0.1001×2^4
3.1 對階
對階是指對齊小數位,遵循“小階向大階看齊”的原則,以便結果的精度更高。
對階還是比較好理解的。把指數小的數(X)的指數(3)轉化成和指數高的數(Y)的指數(4)相等,同時指數小的數(X)的尾數的符號位后邊補兩個數指數之差的絕對值個(1個)0。對于本例來說,就是把X變為:
X=0.01011 ×2^4
3.2 尾數相加減
按照例子來說,尾數相加減:
00 . 0 1 0 1 1
+
0 . 1 0 0 1 (注意看是怎么對齊的)
等于
00 . 1 1 1 0 1
這是相加,相減是把減數換成對應的補碼再做相加運算即可。
3.3 規格化
不滿足規格化的尾數進行規格化處理。當尾數發生溢出可能(尾數絕對值大于1)時,應調整階碼。
當出現以下兩種情況時需要進行規格化。
① 兩個符號位不相同,右規:兩個符號位不同,說明運算結果溢出。此時要進行右規,即把運算結果的尾數右移一位。需要右規的只有如下兩種情況:01××××和10××××。01×××右移一位的結果為001×××;10××××右移一位的結果為110×××。最后將階碼(指數)+1。
② 兩個符號位相同,但是最高數值位與符號位相同,左規:兩個符號位相同,說明沒有溢出。此時要把尾數連續左移,直到最高數值位與符號位的數值不同為止。需要左規的有如下兩種情況:111×××和000×××。111×××左移一位的結果為11×××0;000×××左移一位的結果為00×××0。最后將階碼(指數)減去移動的次數。
3.4 舍入
執行右規或者對階時,有可能會在尾數低位上增加一些值,最后需要把它們移掉。比如說,原來參與運算的兩個數(加數和被加數)算上符號位一共有6個數,通過上邊三個操作后運算結果變成了8個數,這時需要把第7和8位的數去掉。如果直接去掉,會使精度受影響,通常有下邊兩個方法:
① 0舍1入法:
假設運算結果:X=0.11010111,假設原本加數和被加數算上符號位一共有6個數,結果X是10個數,那么要去掉后四個數(0111)。由于0111首位是0(即要去掉的數的最高位為0),這種情況下,直接去掉這四個數就可以。該例最后結果為 X=00.1101
假設運算結果 Y=00.11001001,這時要去掉的數為1001四個數,由于這四個數的首位為1(即要去掉的數的最高位為1),這種情況下,直接去掉這四個數,再在去掉這四個數的新尾數的末尾加1。如果+1后又出現了溢出,繼續進行右規操作。該例最后結果為 Y=00.1101。
② 置1法
這個比較簡單,去掉多余的尾數,然后保證去掉這四個數的新尾數的最后一位為1(即是1不用管,是0改成1)即可。比如 Z=00.11000111,置1法之后的結果為Z=00.11001。
3.5 階碼溢出處理
階碼溢出在規格化和右移的過程中都有可能發生,若階碼不溢出,加減運算正常結束(即判斷浮點數是否溢出,不需要判斷尾數是否溢出,直接判斷階碼是否溢出即可)。若階碼下溢,置運算結果為機器0(通常階碼和尾數全置0)。若上溢,置溢出標志為1。
// 有問題的版本
#include <stdio.h>
int main() {
float sum=0.0f; // sum是浮點數
for (int i=0; i < 10000; i++)
sum +=i + 1; // 整數i+1會轉換為浮點表示,當達到16777215后,浮點表示會有精度丟失
// 1累加到5793會超過16777215
printf("Sum: %f\n", sum); // 50002896.000000
return 0;
}
// 1 + 2 + 3 + … + 10000=10000 * (10000 + 1) / 2=50005000 ?
// 修正的版本
#include <stdio.h>
int main() {
float sum=0.0f, corr=0.0f; /* corrective value for rounding error */
for (int i=0; i < 10000; i++) {
float y=(i + 1) - corr; /* add the correction to specific item */
float t=sum + y; /* bits might be lost */
corr=(t - sum) - y; /* recover lost bits */
sum=t;
}
printf("Sum: %f\n", sum);
return 0;
}
16777215的浮點表示:
https://www.ixigua.com/7031572604158738981
https://www.ixigua.com/7021185140311196196
https://www.toutiao.com/i7038433663641551397/
-End-