定义
整型溢出指的是整型变量在进行赋值或者运算时,得到的结果超过了其取值范围从而发生截断,导致结果和预期不符的现象
整型溢出也分为无符号和有符号的溢出或者可以分为上溢出overflow或者下溢出underflow
对于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”,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。比如:
unsigned char x = 0x7f;
printf("%d\n", ++x);
上面的代码会输出:-128,因为0x7f + 0x01得到0x80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。
一般编译器都会按照这种规则对有符号数进行溢出处理,其他的整型类型也同理。
溢出的危害
1、死循环
... ...
.
... ...
short len = 0;
.
... ...
while(len< MAX_LEN) {
len += readFromInput(fd, buf);
buf += len;
}
若MAX_LEN是一个较大的整型,超过了32767,因为short类型的取值范围是-32768~32767 ,len的值永远不会超过32767,所以while中的循环体会一直循环下去不会停止
2、转型时溢出
int copy_something(char *buf, int len)
{
#define MAX_LEN 256
char mybuf[MAX_LEN];
... ...
... ...
if(len > MAX_LEN){ // <---- [1]
return -1;
}
return memcpy(mybuf, buf, len);
}
若传入的len值为负数,就能通过检查直接入参到memcoy函数中,而memcpy则需一个size_t的len,也就是一个unsigned 类型,传入函数时会进行整型提升为一个正数,这个正数会有可能大于MAX_LEN,导致mybuf中的缓冲区后面的数据被重写,越界访问。
3、分配内存
nresp = packet_get_int();
if (nresp > 0) {
response = xmalloc(nresp*sizeof(char*));
for (i = 0; i < nresp; i++)
response[i] = packet_get_string(NULL);
}
代码中,nresp是size_t类型(size_t一般就是unsigned int/long int),这个示例是一个解数据包的示例,一般来说,数据包中都会有一个len,然后后面是data。
一般在32位系统中,指针占4个字节,若nresp>0xffffffff/4,则会出现整型溢出,真正分配的内存远小于预期的分配量,在for中的循环中,访问的的内存超过所分配的空间出现内存访问越界的问题。
4、缓冲区溢出
int func(char *buf1, unsigned int len1, char *buf2, unsigned int len2 )
{
char mybuf[256];
if((len1 + len2) > 256){ //<--- [1]
return -1;
}
memcpy(mybuf, buf1, len1);
memcpy(mybuf + len1, buf2, len2);
do_some_stuff(mybuf);
return 0;
}
函数的目的是将buf1和buf2中的内容一起拷贝至mybuf中,为了防止溢出还做了检查,但是如果len1+len2的结果溢出则会躲过检查出现拷贝时越界访问内存的问题
5、size_t 的溢出
for (int i= strlen(s)-1; i>=0; i--) { ... }
for (int i=v.size()-1; i>=0; i--) { ... }
strlen()和vector::size()返回的都是 size_t,size_t在32位系统下就是一个unsigned int。如果strlen(s)和v.size() 都是0则会导致结果为 (unsigned int)(-1),最大的正整数,出现溢出的情况,造成越界访问的问题
防止溢出的方法
编译器对某些有符号的整型溢出是一个未定义的行为,在编译器编译的过程中会把这部分的代码进行偏离我们目的的优化,这种奇怪的行为会导致无法检测溢出
1、正确的检测的溢出
在运算导致的溢出之前,必须要进行溢出检测
void foo(int m, int n)
{
size_t s = m + n;
if ( m>0 && n>0 && (SIZE_MAX - m < n) ){
//error handling...
}
}
以上的代码存在两个问题
1、有符号转为无符号
2、整型溢出
所以需要在计算之前进行检查
为什么不使用n+m<SIZE_MAX的原因是因为n+m的结果溢出后截断还是小于SIZE_MAX,所以恒为真难以检测出来,另外,这个表达式中,m和n分别会被提升为unsigned
但是还是错误的,因为
1、在计算后检查,此时m+n的有符号的溢出是未定义的行为
2、所以在计算SIZE_MAX - m < n会被编译器优化
3、SIZE_MAX是size_t的最大值,size_t在64位系统下是64位的,严谨点应该用INT_MAX或是UINT_MAX
正确的代码应该为
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;
}
2、二分查找法中的溢出
一般的二分查找法
int binary_search(int a[], int len, int key)
{
int low = 0;
int high = len - 1;
while ( low<=high ) {
int mid = (low + high)/2;
if (a[mid] == key) {
return mid;
}
if (key < a[mid]) {
high = mid - 1;
}else{
low = mid + 1;
}
}
return -1;
}
1、int mid = (low + high)/2会出现溢出的问题
2、无论len是否为无符号数,若len为0则会导致向下溢出
所以应该在计算mid的时候
int mid = low + (high - low)/2;
3、上溢出和下溢出的检查
加法检查
#include <limits.h>
void f(signed int si_a, signed int si_b) {
signed int sum;
if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
/* Handle error */
return;
}
sum = si_a + si_b;
}