對于unsigned整型溢出,C的規范是有定義的——“溢出后的數會以2^(8*sizeof(type))作模運算”,也就是說,如果一個unsigned char(1字符,8bits)溢出了,會把溢出的值與256求模。例如: unsigned char x = 0xff;printf("%d\n", ++x);
上面的代碼會輸出:0 (因為0xff + 1是256,與2^8求模后就是0)
對于signed整型的溢出,C的規范定義是“undefined behavior”,也就是說,編譯器愛怎么實現就怎么實現。對于大多數編譯器來說,算得啥就是啥。比如: signed char x =0x7f; //注:0xff就是-1了,因為最高位是1也就是負數了printf("%d\n", ++x);
下面gcc 1.17版本下的遭遇undefined行為時,gcc在unix發行版下玩的彩蛋的源代碼。我們可以看到,它會去嘗試去執行一些游戲 NetHack , Rogue 或是Emacs的 Towers of Hanoi ,如果找不到,就輸出一條NB的報錯。 execl("/usr/games/hack", "#pragma", 0); // try to run the game NetHackexecl("/usr/games/rogue", "#pragma", 0); // try to run the game Rogue// try to run the Tower's of Hanoi simulation in Emacs.execl("/usr/new/emacs", "-f","hanoi","9","-kill",0);execl("/usr/local/emacs","-f","hanoi","9","-kill",0); // same as abovefatal("You are in a maze of twisty compiler features, all different"); 正確檢測整型溢出
在看過編譯器的這些行為后,你應該會明白——“ 在整型溢出之前,一定要做檢查,不然,就太晚了 ”。
我們來看一段代碼: void foo(int m, int n){ size_t s = m + n; .......}
上面這段代碼有兩個風險: 1)有符號轉無符號 , 2)整型溢出 。這兩個情況在前面的那些示例中你都應該看到了。所以,你千萬不要把任何檢查的代碼寫在 s = m + n 這條語名后面,不然就太晚了 。undefined行為就會出現了——用句純正的英文表達就是——“Dragon is here”——你什么也控制不住了。(注意:有些初學者也許會以為size_t是無符號的,而根據優先級 m 和 n 會被提升到unsigned int。其實不是這樣的,m 和 n 還是signed int,m + n 的結果也是signed int,然后再把這個結果轉成unsigned int 賦值給s)
比如,下面的代碼是錯的: void foo(int m, int n){ size_t s = m + n; if ( m>0 && n>0 && (SIZE_MAX - m < n) ){ //error handling... }}
上面的代碼中,大家要注意 (SIZE_MAX - m < n) 這個判斷,為什么不用m + n > SIZE_MAX呢?因為,如果 m + n 溢出后,就被截斷了,所以表達式恒真,也就檢測不出來了。另外,這個表達式中,m和n分別會被提升為unsigned。
所以,正確的代碼應該是下面這樣: void foo(int m, int n){ size_t s = 0; if ( m>0 && n>0 && ( UINT_MAX - m < n ) ){ //error handling... return; } s = (size_t)m + (size_t)n;}
在《 蘋果安全編碼規范 》(PDF)中,第28頁的代碼中:
如果n和m都是signed int,那么這段代碼是錯的。正確的應該像上面的那個例子一樣,至少要在n*m時要把 n 和 m 給 cast 成 size_t。因為,n*m可能已經溢出了,已經undefined了,undefined的代碼轉成size_t已經沒什么意義了。(如果m和n是 unsigned int,也會溢出),上面的代碼僅在m和n是size_t的時候才有效。