当前位置: 首页 > 科技观察

数组越界以及如何避免,C语言数组越界详解

时间:2023-03-18 14:51:38 科技观察

所谓数组越界简单的说就是数组下标变量的值超出了初始定义的大小,导致在访问数组元素时出现在数组范围之外,这类错误也是C语言程序中最常见的错误之一。在C中,数组必须是静态的。也就是说,数组的大小必须在程序运行前就确定下来。由于C语言不具备Java等语言现有的静态分析工具的功能,因此可以严格检查程序中数组下标的取值范围。一旦数组上溢或下溢,就会抛出异常终止。程序。也就是说,C语言不检查数组的边界,数组的两端都可能越界,从而破坏其他变量的数据,甚至破坏程序代码。因此,数组下标的取值范围只能提前推断出一个值来确定数组的维数,检查数组的边界是程序员的责任。一般来说,数组越界错误主要有两种:数组下标值越界和数组指针越界。数组下标值越界数组下标值越界主要是指在访问数组时,下标的值不在定义数组的取值范围内,访问到的是无法获取的内存地址。例如,对于数组inta[3],其下标取值范围为[0,2](即a[0]、a[1]和a[2])。如果我们的值不在这个范围内(比如a[3]),就会出现越界错误。示例代码如下:1inta[3];2inti=0;3for(i=0;i<4;i++)4{5a[i]=i;6}7for(i=0;i<4;i++)8{9printf("a[%d]=%d\n",i,a[i]);0}显然,在上面的示例程序中,访问a[3]是非法的,会出边界错误。因此,我们要把上面的代码修改成如下形式:1inta[3];2inti=0;3for(i=0;i<3;i++)4{5a[i]=i;6}7for(i=0;i<3;i++)8{9printf("a[%d]=%d\n",i,a[i]);0}数组指针的指向范围越界Thepointingrangeofthepointertothearrayisoutofbounds意思是当定义数组时,会返回一个指向第一个变量的头指针。this指针的加减法可以将指针向前或向后移动,进而访问数组中的所有变量。但是在移动指针的时候,如果不注意移动的个数和位置,指针就会指向数组之外的位置,导致数组越界错误。下面的示例代码在移动指针时没有考虑移动的次数和数组的范围,使得程序访问的是数组以外的存储单元。1inti;2int*p;3inta[5];4/*将数组a的头指针赋值给指针p*/5p=a;6for(i=0;i<10;i++)7{8/*指针p指向变量*/9*p=i+10;10/*指针p指向下一个变量*/11p++;12}在上面的示例代码中,for循环会将指针p向后移动10次,并指向每次给指针赋值单位。但是这里数组a的下标取值范围是[0,4](即a[0]、a[1]、a[2]、a[3]和a[4])。所以最后5次操作都会给未知内存区赋值,而这种给未知内存区赋值的操作会导致系统错误。正确的操作应该是指针移动的次数与数组中变量的个数相同,如下代码所示:1inti;2int*p;3inta[5];4/*的头指针数组a赋值给指针p*/5p=a;6for(i=0;i<5;i++)7{8/*指针p指向的变量*/9*p=i+10;10/*指针p的next变量*/11p++;12}为了加深大家对数组越界的理解,下面给出一个数组越界的完整例子来演示数组会导致哪些问题编程中的越界。1#definePASSWORD"123456"2intTest(char*str)3{4intflag;5charbuffer[7];6flag=strcmp(str,PASSWORD);7strcpy(buffer,str);8returnflag;9}10intmain(void)11{12intflag=0;13charstr[1024];14while(1)15{16printf("请输入密码:");17scanf("%s",str);18flag=Test(str);19if(flag)20{21printf("错误password!\n");22}23else24{25printf("密码正确!\n");26}27}28return0;29}上面的示例代码模拟了一个密码验证的例子,结合了用户输入的密码宏定义Password123456in.显然,这个例子中最大的设计缺陷是Test()函数中的strcpy(buffer,str)调用。因为程序将用户输入的字符串复制到Test()函数的数组charbuffer[7]中。因此,当用户输入大于7个字符的缓冲区大小时,就会出现数组越界错误,即所谓的缓冲区溢出漏洞。但需要注意的是,如果此时我们根据缓冲区溢出的具体情况来填充缓冲区,不仅可以避免程序崩溃,而且会影响程序的执行过程,甚至会导致程序崩溃被执行以执行缓冲区中的代码。示例运行结果为:1请输入密码:123452密码错误!3请输入密码:1234564密码正确!5请输入密码:12345676密码正确!7请输入密码:aaaaaaa8密码正确!9请输入密码:012345610密码错误!11请输入密码:示例代码中,flag变量实际上是一个标志变量,它的值将决定程序是进入密码错误的过程(非0)还是“密码正确”(0)的过程。当我们输入错误的字符串1234567或者aaaaaaa时,程序也会输出“密码正确”。但是当输入0123456时,程序输出“wrongpassword”。为什么?其实原因很简单。当调用Test()函数时,系统会为其分配一块连续的内存空间,变量charbuffer[7]和intflag会紧挨着存放,将用户输入的字符串复制到buffer[7]中。如果此时,我们输入的字符串数量超过6个(注意一个字符串截断字符算一个),那么超出的部分会破坏紧邻的标志变量的内容。当输入的密码不是宏定义的123456时,字符串比较会返回1或-1。我们都知道内存中的数据是以4字节(DWORD)倒序存储的,所以当flag为1时,内存中存储的是0x01000000。如果我们输入了一个包含7个字符的错误密码,比如aaaaaaa,那么字符串截断字符0x00就会被写入到标志变量中,这样一字节0x00溢出数组就会恰好将倒序存储的标志变量变为0x00000000。函数返回后,一旦main函数的标志位为0,就会输出“密码正确”。这样我们就用错误的密码得到了正确密码的运行效果。对于0123456,因为比较字符串大小时小于123456,flag的值为-1,负数会根据补码存入内存,所以实际存入的不是0x01000000而是0xffffffff。那么字符串截断字符0x00被淹没后,变成了0x00ffffff,仍然是非0,所以没有进入正确的分支。实际上,这个例子只是用一个字节淹没了相邻的变量,使程序进入了正确的密码处理流程,并使设计的验证功能失效。尝试显式指定数组的边界。在C语言中,为了提高运行效率,给程序员更多的空间,给指针操作带来更多的便利,C语言本身并不检查数组下标表达式的值。是否在合法范围内,不检查指向数组元素的指针是否移出数组的合法区域。因此,在编程中使用数组时必须格外小心,在对数组进行读写操作时要进行相应的检查,以免超出数组的边界对数组进行操作,造成缓冲区溢出漏洞。为避免数组越界导致程序错误,首先需要从数组边界的定义入手。尝试显式指定数组的边界,即使它是由初始化列表隐式指定的。示例代码如下:1inta[]={1,2,3,4,5,6,7,8,9,10};显然,对于上面的数组a[],虽然编译器可以通过初始化Listofvalues来计算数组的长度。但是,如果我们明确指定数组的长度,例如:1inta[10]={1,2,3,4,5,6,7,8,9,10};它不仅使程序具有更好的可读性,而且如果数组的长度小于初始化列表的长度,大多数编译器都会警告你。当然也可以使用宏来显式指定数组的边界(其实这是最常用的方法),如下代码所示:1#defineMAX102…3inta[MAX]={1,2,3,4,5,6,7,8,9,10};另外,在C99标准中,还允许我们使用单个指标为数组的两段“分配”空间,如下代码所示:1inta[MAX]={1,2,3,4,5,[MAX-5]=6,7,8,9,10};在上面的a[MAX]数组中,如果MAX大于10,则数组的中间部分将填充0值元素(填充个数为MAX-10,从a开始进行0值填充[5]);如果MAX小于10,[MAX-5]之前的5个元素(1,2,3,4,5)将被[MAX-5]之后的5个元素(6,7,8,9,10)覆盖,示例代码如下:1#defineMAX102#defineMAX1153#defineMAX264intmain(void)5{6inta[MAX]={1,2,3,4,5,[MAX-5]=6,7,8,9,10};7intb[MAX1]={1,2,3,4,5,[MAX1-5]=6,7,8,9,10};8intc[MAX2]={1,2,3,4,5,[MAX2-5]=6,7,8,9,10};9inti=0;10intj=0;11intz=0;12printf("a[MAX]:\n");13for(i=0;i=ARRAY_NUM时)。因此,应对num参数进行越界检查。示例代码如下:1int*TestArray(intnum,intvalue)2{3int*arr=NULL;4/*越界检查(越界)*/5if(num=0&&num