C/C++使用过程中的溢出问题详解

 更新时间:2024年01月08日 11:58:40   作者:嵌入式学习菌  
在C/C++程序里有一类非常典型的问题,那就是:溢出问题,现在分别来分析一下常见的数组溢出,整数溢出,缓冲区溢出,栈溢出和指针溢出等,需要的朋友可以参考下

内容:在C/C++程序里有一类非常典型的问题,那就是:溢出问题。现在分别来分析一下常见的数组溢出,整数溢出,缓冲区溢出,栈溢出和指针溢出等。

1、数组溢出

在C语言中,数组的元素下标是从0开始计算的,所以,对于n个元素的数组a[n], 遍历它的时候是a[0],a[1],...,a[n-1],如果遍历到a[n],数组就溢出了。 

void print_array(int a[], int n)
{
    for (int i = 0; i < n; i++) 
    {
        a[i] = a[i+1];//当i = n-1时,就发生了数组越界
        printf(“%d\n”, a[i]);
    }
}

上面的循环判断应该改为:
for (int i = 0; i < n-1; i++)

2、整数溢出

整数的溢出分为下溢出和上溢出。比如,对于有符号的char(signed char)类型来说,它能表示的范围为:[-128,127]之间;而对于无符号的char(unsigned char)来说, 它能表示的范围为:[0,255]。

那么,对于下面的代码:

signed char c1 = 127;
c1 = c1+1;//发生上溢出,c1的值将变为-128
signed char c2 = -128;
c2 = c2-1;//发生下溢出,c2的值将变为127
unsigned char c3 = 255;
c3 = c3+1;//发生上溢出,c3的值将变为0
unsigned char c4 = 0;
c4 = c4-1;//发生下溢出,c4的值将变为255

从上面的例子可以看出,当一个整数向上溢出,将会变为最小值,而向下溢出,将会变为最大值。

来看下面的溢出代码,该代码负责提供一个小写字母转换表,但存在一个整数溢出问题:

void BuildToLowerTable( void ) /* ASCII版本*/
{
    unsigned char ch;
    /* 首先将每个字符置为它自己 */
    /*ch为unsigned char,无符号数,当ch值为UCHAR_MAX, ch++将会发生向上溢出,变为0,导致循环无法退出。*/
    for (ch=0; ch <= UCHAR_MAX;ch++)
        chToLower[ch] = ch;
    /* 将大写字母改为小写字母 */
    for( ch = ‘A'; ch <= ‘Z'; ch++ )
        chToLower[ch] = ch +'a' – ‘A';
}

该代码负责在内存中查找指定的字符ch,但也存在一个溢出问题

void * memchr( void *pv, unsigned char ch, size_t size )
{
    unsigned char *pch = (unsigned char *) pv;
    /*当size的值为0的时候,由于size是无符号整数,因此会发生下溢出,变为一个最大的整数 循环也将无法退出*/ 
    while( -- size >=0 )
    {
        if( *pch == ch )
            return (pch );
        pch++;
    }
    return( NULL );
}

3、缓冲区溢出

缓冲区溢出一般是调用了一些不安全的字符串操作函数比如:strcpy,strcat等(这些字符串操作函数在拷贝或者修改目标位置的时候,并不判断长度是否会超过目标缓存),或者设置参数超过了目标缓存能容纳的大小而造成的溢出问题。

void func1(char* s)
{
    char buf[10];
    /*此时,buf只有10个字节,如果传入的s超过10个字节,就会造成溢出*/
    strcpy(buf, s);
}
void func2(void)
{
    printf("Hacked by me.\n");
    exit(0);
}
int main(int argc, char* argv[])
{
    char badCode[] = "aaaabbbb2222cccc4444ffff";
    DWORD* pEIP = (DWORD*)&badCode[16];
    *pEIP = (DWORD)func2;
    /*badCode字符串超过了10个字节,传递给func1会造成栈上缓冲区溢出
    而且,由于badCode经过精心构造,在溢出的时候,根据函数的调用约定规则,会覆盖栈上的返回地址,
    指向了func2。所以,在func1退出的时候,会直接调用func2
    */
    func1(badCode);
    return 0;
}

4、栈溢出

无论是内核栈,还是应用层的栈,都是有一定大小限制的。如果在栈上分配的空间大于了这个限制,就会造成栈大小溢出,破坏栈上的数据。比如局部变量过多,或者递归调度嵌套太深都会造成栈溢出。比如:

int init_module(void)
{
    char buf[10000]; //buf[]分配在栈上,但10000的空间超过了栈的默认大小8KB。
    //所以发生溢出
    memset(buf,0,10000);
    printk("kernel stack.\n");
    return 0;
}
void cleanup_module(void)
{ 
    printk("goodbye.\n");
}
MODULE_LICENSE("GPL");
//应用栈的大小对少?内核栈的大小多少?什么时候容易栈溢出?

5、指针溢出

一块长度为size大小的内存buffer,buffer的首地址为p,那么buffer最后一个字节的地址:
p+size-1,而不是p+size。如果写成了p+size,就会造成溢出,比如下面的代码:

void* memchr( void *pv, unsigned char ch, size_t size )
{
    unsigned char *pch = ( unsigned char * )pv;
    unsigned char *pchEnd = pch + size;
    while( pch < pchEnd )
    {
        if( *pch == ch )
            return ( pch );
        pch ++ ;
    }
    return( NULL );
}

上面的代码用于查找内存中特定的字符位置。对于其中的while()循环,平时执行似乎都没有任何问题。但是,考虑一种特别情况,即pv所指的内存位置为末尾若干字节,那么因为pchEnd = pch+size,所以pchEnd指向最后一个字符的下一个字节,将会超出内存的范围,即pchEnd所指的位置已经不存在。
知道了问题所在,那么可以将内存的结尾计算方式改为: 

pchEnd = pv + size – 1; 
while ( pch <= pchEnd ) 
{
        if( *pch == ch )
            return ( pch );
        pch ++ ;
}

pchEnd指向了最后一个字节。但是,检查循环内部的执行情况可知,由于pch每增加到pchEnd+1时,都会发生上溢。因此,循环将无法退出。 于是,可以将程序修改为下面的代码。将用size变量来控制循环的退出。这样就不会存在任何问题了。

void *memchr( void *pv, unsigned char ch, size_t size )
{
    unsigned char *pch = ( unsigned char * )pv;
    while( size -- > 0 )
    {
        if( *pch == ch )
            return( pch );
        pch ++;
    }
    return( NULL );
}

大家知道,--size的效率一般比size--的效率高。那么是否可以将循环的判断条件改为下面的语句呢? 
while( --size >= 0 ) 
…… 

实际上这是不行的。因为当size=0时,由于size是无符号数,那么它将发生下溢,变成了size所能表示的最大正数,循环也将无法退出。 

6、字符串溢出

我们已经知道,字符串是'\0'结尾的。如果字符串结尾忘记带上'\0',那么就溢出了。注意,strlen(p)计算的是字符串中有效的字符数(不含’\0’)。考察下面拷贝字符串的代码,看看有什么问题没呢?

char *str = “Hello, how are you!”;

char *strbak = (char *)malloc(strlen(str));

if (NULL == strbak)

{

//处理内存分配失败,返回错误

}

strcpy(strbak, str);

显然,由于strlen()计算的不是str的实际长度(即不包含’\0’字符的计算),所以strbak没有结束符’\0’,而在C语言中,’\0’是字符串的结束标志,所以是必须加上的,否则会造成字符串的溢出。所以上面的代码应该是:

char *str = “Hello, how are you!”;

char *strbak = (char *)malloc(strlen(str)+1);

if (NULL == strbak)

{

    //内存分配失败,返回错误

}

strcpy(strbak, str);

到此这篇关于C/C++使用过程中的溢出问题详解的文章就介绍到这了,更多相关C/C++溢出问题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言数据结构中堆排序的分析总结

    C语言数据结构中堆排序的分析总结

    堆是计算机科学中一类特殊的数据结构的统称,通常是一个可以被看做一棵完全二叉树的数组对象。而堆排序是利用堆这种数据结构所设计的一种排序算法。本文将通过图片详细介绍堆排序,需要的可以参考一下
    2022-04-04
  • C++实现八个常用的排序算法 插入排序、冒泡排序、选择排序、希尔排序等

    C++实现八个常用的排序算法 插入排序、冒泡排序、选择排序、希尔排序等

    这篇文章主要介绍了C++如何实现八个常用的排序算法:插入排序、冒泡排序、选择排序、希尔排序 、快速排序、归并排序、堆排序和LST基数排序,需要的朋友可以参考下
    2015-07-07
  • visualstudio2022工程重命名的图文步骤

    visualstudio2022工程重命名的图文步骤

    很多时候需要用到项目重命名,本文主要介绍了visualstudio2022工程重命名的图文步骤,具有一定的参考价值,感兴趣的可以了解一下
    2024-06-06
  • C语言实现变色进度条

    C语言实现变色进度条

    这篇文章主要为大家详细介绍了C语言实现一个变色的进度条,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • VC++ 使用 _access函数判断文件或文件夹是否存在

    VC++ 使用 _access函数判断文件或文件夹是否存在

    这篇文章主要介绍了VC++ 使用 _access函数判断文件或文件夹是否存在的相关资料,需要的朋友可以参考下
    2015-10-10
  • c++ 子类构造函数初始化及父类构造初始化的使用

    c++ 子类构造函数初始化及父类构造初始化的使用

    这篇文章主要介绍了c++ 子类构造函数初始化及父类构造初始化的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • C++实现调用系统时间简单示例

    C++实现调用系统时间简单示例

    这篇文章主要介绍了C++实现调用系统时间,需要的朋友可以参考下
    2014-07-07
  • C++实现猜数字小游戏

    C++实现猜数字小游戏

    这篇文章主要为大家详细介绍了C++实现猜数字小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-10-10
  • C++内存查找实例

    C++内存查找实例

    这篇文章主要介绍了C++内存查找实例,可实现Windows程序设计中的内存查找功能,需要的朋友可以参考下
    2014-10-10
  • 一篇文章带你了解C语言内存对齐解决的问题

    一篇文章带你了解C语言内存对齐解决的问题

    内存对齐的目的是为了提高CPU读写内存里数据的速度。现代的CPU读取内存并不是一个一个字节挨着读取,这样做的效率非常低。现代的CPU一般以4个字节(32bit数据总线)或者8个字节(64bit数据总线)为一组,一组一组地读写内存里的数据
    2021-08-08

最新评论