對于unsigned整型溢出,C的規(guī)范是有定義的——“溢出后的數(shù)會(huì)以2^(8*sizeof(type))作模運(yùn)算”,也就是說,如果一個(gè)unsigned char(1字符,8bits)溢出了,會(huì)把溢出的值與256求模。例如: unsigned char x = 0xff;printf("%d\n", ++x);
上面的代碼會(huì)輸出:0 (因?yàn)?xff + 1是256,與2^8求模后就是0)
對于signed整型的溢出,C的規(guī)范定義是“undefined behavior”,也就是說,編譯器愛怎么實(shí)現(xiàn)就怎么實(shí)現(xiàn)。對于大多數(shù)編譯器來說,算得啥就是啥。比如: signed char x =0x7f; //注:0xff就是-1了,因?yàn)樽罡呶皇?也就是負(fù)數(shù)了printf("%d\n", ++x);
關(guān)于編譯器的優(yōu)化,在這里再舉個(gè)例子,假設(shè)我們有下面的代碼(又是一個(gè)相當(dāng)相當(dāng)常見的代碼): int len;char* data; if (data + len < data){ printf("invalid len\n"); exit(-1);}
上面這段代碼中,len 和 data 配套使用,我們害怕len的值是非法的,或是len溢出了,于是我們寫下了if語句來檢查。這段代碼在-O的參數(shù)下正常。但是在-O2的編譯選項(xiàng)下,整個(gè)if語句塊被優(yōu)化掉了。
你可以寫個(gè)小程序,在gcc下編譯(我的版本是4.4.7,記得加上-O2和-g參數(shù)),然后用gdb調(diào)試時(shí),用disass /m命信輸出匯編,你會(huì)看到下面的結(jié)果(你可以看到整個(gè)if語句塊沒有任何的匯編代碼——直接被編譯器和諧掉了): 7 int len = 10;8 char* data = (char *)malloc(len); 0x00000000004004d4 <+4>: mov $0xa,%edi 0x00000000004004d9 <+9>: callq 0x4003b8 <malloc@plt>910 if (data + len < data){11 printf("invalid len\n");12 exit(-1);13 }1415 } 0x00000000004004de <+14>: add $0x8,%rsp 0x00000000004004e2 <+18>: retq
對此,你需要把上面 char* 轉(zhuǎn)型成 uintptr_t 或是 size_t,說白了也就是把char*轉(zhuǎn)成unsigned的數(shù)據(jù)結(jié)構(gòu),if語句塊就無法被優(yōu)化了。如下所示: if ((uintptr_t)data + len < (uintptr_t)data){ ... ...}
關(guān)于這個(gè)事,你可以看一下C99的規(guī)范說明《 ISO/IEC 9899:1999 C specification 》第 §6.5.6 頁,第8點(diǎn),我截個(gè)圖如下:(這段話的意思是定義了指針+/-一個(gè)整型的行為,如果越界了,則行為是undefined)
下面gcc 1.17版本下的遭遇undefined行為時(shí),gcc在unix發(fā)行版下玩的彩蛋的源代碼。我們可以看到,它會(huì)去嘗試去執(zhí)行一些游戲 NetHack , Rogue 或是Emacs的 Towers of Hanoi ,如果找不到,就輸出一條NB的報(bào)錯(cuò)。 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; .......}
上面這段代碼有兩個(gè)風(fēng)險(xiǎn): 1)有符號轉(zhuǎn)無符號 , 2)整型溢出 。這兩個(gè)情況在前面的那些示例中你都應(yīng)該看到了。所以,你千萬不要把任何檢查的代碼寫在 s = m + n 這條語名后面,不然就太晚了 。undefined行為就會(huì)出現(xiàn)了——用句純正的英文表達(dá)就是——“Dragon is here”——你什么也控制不住了。(注意:有些初學(xué)者也許會(huì)以為size_t是無符號的,而根據(jù)優(yōu)先級 m 和 n 會(huì)被提升到unsigned int。其實(shí)不是這樣的,m 和 n 還是signed int,m + n 的結(jié)果也是signed int,然后再把這個(gè)結(jié)果轉(zhuǎn)成unsigned int 賦值給s)
比如,下面的代碼是錯(cuò)的: 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) 這個(gè)判斷,為什么不用m + n > SIZE_MAX呢?因?yàn)椋绻?m + n 溢出后,就被截?cái)嗔耍员磉_(dá)式恒真,也就檢測不出來了。另外,這個(gè)表達(dá)式中,m和n分別會(huì)被提升為unsigned。
所以,正確的代碼應(yīng)該是下面這樣: 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;}
在《 蘋果安全編碼規(guī)范 》(PDF)中,第28頁的代碼中:
如果n和m都是signed int,那么這段代碼是錯(cuò)的。正確的應(yīng)該像上面的那個(gè)例子一樣,至少要在n*m時(shí)要把 n 和 m 給 cast 成 size_t。因?yàn)椋琻*m可能已經(jīng)溢出了,已經(jīng)undefined了,undefined的代碼轉(zhuǎn)成size_t已經(jīng)沒什么意義了。(如果m和n是 unsigned int,也會(huì)溢出),上面的代碼僅在m和n是size_t的時(shí)候才有效。