对于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的时候才有效。