所谓数组越界简单来说就是指一个指数组下标的变量取值超出初始定义的规模,从而造成数组元素被存取到数组范围以外,这种错误同样也是C语言程序常见错误。
C语言里数组一定要静止。换句话来说,数组大小一定要先决定,然后才能运行程序。因为C语言没有与Java和其他语言已有的静态分析工具相似的特性,它能严格地检查出程序内数组下标的取值范围,当检测到数组上溢和下溢时,就会因为抛出异常情况而结束程序。即C语言没有对数组边界进行测试,数组两端可能会出现越界现象,导致其它变量数据乃至程序代码损坏。
所以数组下标取值范围仅能提前推断出一个值就可以决定数组维数,测试数组边界就是程序员应尽的义务。
通常数组越界错误有2种类型:数组下标取值越边界和指针指向范围越边界。
数组的下标取值越界
数组下标取值越界现象主要表现为在访问数组时,下标并没有在定义数组取值之内,而是访问了不可得内存地址。比如对数组inta〔3〕而言,其下标取值范围为〔0,2〕(分别为〔0〕、〔1〕和〔2〕)。若我们的值不在此范围之内(例如a〔3〕)则出现越界错误。示例代码为:
{
}
{
}
显而易见,上文示例程序a[3]的存取是违法的,会出现越界错误。所以我们要把以上代码改为以下格式:
{
}
{
}
指数组中指针所指范围越出边界
指称数组的指称范围越边界就是在定义数组时将会返回指向首个变量的头部指称,对于该指称的加减运算可将该指称前移或者后移,然后存取数组内的全部变量。但是当指针运动时,若没有注意到运动的数量与位置,就会使得指针指向数组之外,从而造成数组越界出错。以下示例代码是指在移动指针过程中不考虑移动次数、数组范围等因素,导致程序进入数组外存储单元。
/×数组a中的头指针被赋予一个指针p×/
{
/*指针p所指之变量*/
/*指针p下面的变量*/
}
在上一个示例代码里,for循环将指针p后移十次,每一次都将赋值给一个指针所指的单位。但在此数组a下标的取值范围为[0, 4](分别为a~0, 1, 2, 3和4)。所以,后面五次运算都是给未知内存区域赋权重,这种给内存中未知区域赋权重的运算可能导致系统出错。适当的运算应是指针运动的数量和数组内变量的数量一样多。如下编码:
/×数组a中的头指针被赋予一个指针p×/
{
/*指针p所指之变量*/
/*指针p下面的变量*/
}
为加深人们对数组越界问题的理解,本文将以一个完整的数组越界实例展示数组越界在编程过程中会给人们带来什么样的问题。
15 {
第16 printf条(“请键入密码:”);
21 printf(“密码不对!\n”);
25. printf(“密码是对的!\n”);
以上示例代码模拟出密码验证实例,该实例通过宏定义的123456来对比用户所输入密码。显而易见,这个例子最大的设计漏洞是在Test()函数上调用strcpy(buffer)和str()。
因为程序在Test()函数上复制了一个数组charbuffer,它原封不动的复制了用户所输入字符串[7]。所以,在用户输入超过七个字符缓冲区大小时会出现数组越界错误(Bufferoverflow),人们称之为缓冲区溢出漏洞。
但需要注意的是,若此时我们针对缓冲区溢出出现的特殊情况进行缓冲区填充,不仅能避免程序运行崩溃、影响程序执行过程、甚至使程序执行缓冲区中的编码。实例的运行效果如下:
1键入密码:12345
2.密码不对!
3输入密码123456
4密码是对的!
5输入密码1234567
6密码是对的!
7输入口令:aaaaaaaaaaa
8密码是对的!
9键入密码0123456
10密码出错!
11键入密码:
示例代码中flag变量其实就是标志变量,它的值决定了程序是否进入密码错误(不是0)或“密码正确”(0)过程。在输入不正确字符串1234567或aaaaaaa时,程序还会输出“正确密码”。但是当输入0123456时程序输出了一个“密码错误”.到底是什么原因造成的?
其实道理也非常简单。调用Test()函数后,系统会为其分配一块持续的内存空间,变量char buffer[7]和int flag(7)将紧挨在一起保存,用户所输入字符串也会复制到buffer中。若此时我们所输入字符串的个数大于6(注:带有字符串截断符的也算是1),则超出部分就会损坏与其紧相邻的flag变量中的元素。
在输入密码不在宏定义123456范围内时,字符串对比会返回1或者-1。众所周知,内存的数据按4字节逆序保存(DWORD),因此flag等于1时内存保存0x01000000个。如果输入含有七个字符(例如aaaaaaa)的错误密码时,字符串截断符0x00就会被写在flag变量中,从而溢出数组中的某个字节0x00正好会使逆序存储的flag变量变成0x00000000。当功能返回时,main功能中flag一旦是0则输出“密码正确”。从而通过使用不合适的密码来获得合适密码运行结果。
而对0123456来说,由于做字符串大小比对时比123456小,flag值为-1,内存中会根据补码储存负数,因此真正储存的并不是0x01000000,而是0xffffff。然后字符串截断的后符0x00被淹没之后就成为了0x00ffffff或者非0从而未进入到正确的分支中。
在实际应用中,该实例仅仅是使用一个字节就将邻接变量淹没,从而使得程序陷入密码的正确对待过程中,使得所设计验证功能无效。
尽可能显式规定数组边界
对于C语言来说,由于其运行效率较高,给予程序员较大的使用空间,这就给指针操作提供了较大的便利, C语言本身并不会对数组下标表达式值是否属于合法范围进行检查,同时也不会对指向数组各要素的指针是否从数组合法区域上移走进行检查。所以,编程使用数组一定要特别小心,所有数组读写操作都要做相关检查,避免数组操作超出数组边界而出现缓冲区溢出漏洞。
为了避免程序因为数组越界而出错,必须从定义数组边界入手。尽可能显式规定数组边界,甚至已被初始化值列表所隐式规定。示例代码为:
明显地,对上一个数组a[],尽管编译器能够基于始化值列表计算数组长度。但若显式规定数组长度如:
不仅使得程序可读性较好,而且当数组长度低于初始化值列表时,多数编译器都会出现相应的提醒。
当然,它还能以宏图方式显式地规定数组边界(其实这种规定方式最为普遍),如下文编码:
除此,C99标准允许我们用单个指示符来给数组“分配”两段空间。如下编码:
10,则阵列中间会填充0值元素(填充数量是MAX-10且0值以a[5]为起点),若MAX<10,则[MAX-5]前五个元素(1、2、3、4、5)会有多个由[MAX-10]后五个元素(6、7、8、9、10)覆盖,实例代码为。” data-s data-pos=”0″ data-len=”164″>在上文a[MAX]阵列中,若MAX>10,则阵列中间会填充0值元素(填充数量是MAX-10且0值以a[5]为起点),若MAX<10,则[MAX-5]前五个元素(1、2、3、4、5)会有多个由[MAX-10]后五个元素(6、7、8、9、10)覆盖,实例代码为。
运行效果如下:
进行数组越界检查以保证索引值在合法范围内
为了避免数组发生越界现象,除上述说明显式地规定数组边界外,在使用数组前,可先做一次越界检查来查看数组边界及字符串是否终止(同样按数组形式存储),确保数组索引值在正当范围内。比如,编写处理数组中的函数通常要具有范围参数,而处理字符串总是要查看是否碰到空字符’\0’。
请看以下一段编码的例子:
由上int*TestArray(int num, int value)功能不难看出,这里面有一个非常明显的问题是不能确保num参数越界(即num大于=ARRAY_NUM)。因此num参数应越界检查。示例代码为
4/*越界检查*/
15 }
17 return arr;
18}
NUM)语句来越界检查以确保num参数不跨越该数组的上界,目前看来TestArray()函数应不存在任何问题和越界错误。” data-s data-pos=”0″ data-len=”93″>这就用if(numNUM)语句来越界检查以确保num参数不跨越该数组的上界,目前看来TestArray()函数应不存在任何问题和越界错误。
但只要仔细研究一下, TestArray()函数仍有个致命问题——数组下界未被研究。因为此处num参数类型属于int类型所以可能会出现负值。若num参数传递的值为负,则会导致写入arr引用内存边界外。
NUM)语句中添加另一条件来检验,如下代码:” data-s data-pos=”0″ data-len=”50″>当然也可通过在if(numNUM)语句中添加另一条件来检验,如下代码:
= 0 & numif (num > = 0 & num
{
}
但这种函数形式对于调用者而言并不是很友好(因为int类型不同,对于调用者仍能传递负数。而对于函数中如何处理则另当别论),所以最好的解决办法就是把num参数作为size_t类型进行声明,从根源上阻止其传递负数。下面给出示例代码。
1int *TestArray(size_t num,int value)
4/*越界检查*/
15 }
17 return arr;
18}
在得到数组长度后,不向指针施加sizeof操作符
在C语言里, sizeof是一个其貌不扬之人,常常引起无数程序员的叫苦。更是各大企业竞相挑选的采访必选话题。通俗的说sizeof只是单目操作符而非函数。功能是返回操作数占用内存字节数。其中操作数可为表达式或括号中类型名称,操作数存储尺寸根据操作数类型确定。比如对数组inta[5]来说,可利用sizeof(a)求出数组长度,利用sizeofa[0)求出数组元素长度。
但是要注意sizeof操作符不可以用在函数类型上,不完整类型(指有不清楚存储尺寸的数据类型。例如不清楚存储尺寸数组类型;不清楚内容结构或者联合类型;void类型)和位字段之间。如下列均为错误的格式:
1/*如果这时max的定义是intmax(),则*/
2sizeof(max)
3/*如果这时arr的定义是chararr〔MAX〕并且MAX不知道*/
4sizeof(arr)
不能在void类型中使用5/*/
6sizeof(void)
不能在位字段中使用7/*/
8struct S
9{
10 unsigned int f1:1;
11 unsigned int f2:5;
12 unsigned int f3:12;
13};
14sizeof(S.f1);
在理解了sizeof操作符后,现在请看以下示例代码。
1void Init(int arr[])
size3 _ti=0;
4 for(i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
5 {
6 arr[i]=i;
7 }
8}
9int main(void)
10{
11 int i=0;
12 int a[10];
13 Init(a);
14 for(i=0;i<10;i++)
15 {
16 printf(“%d\n”,a[i]);
17 }
18 return 0;
19}
表面上,以上编码的输出结果应在0、1、2、3、4、5、6、7、8、9之间,但是真正的效果让我们始料不及,就在图1中。
VC++2010下运行结果如图1所示,对示例代码进行处理
造成这一后果的因素有哪些?
明显上示例代码从void Init(intarr[])功能中收到int arr[]类形参并从main功能中传输a[10]类实参。同时在Init()功能中,该数组元素个数及初始化值由sizeof(arr)/size-arr[0)决定。
这里就有个大问题:因为arr的参数为形参而属于指针的类型,所以结果就是sizeof(arr=sizeof-int×)。IA-32上sizeof(arr)/sizeof-arr[0)结果是1。故最终结果见图1。
对以上示例代码,可采用传入数组长度方法解决问题。示例代码为。
1void Init(int arr[],size_t arr_len)
size 3 _ t i=0;
4 for(i=0;i
5 {
6 arr[i]=i;
7 }
8}
9int main(void)
10{
11 int i=0;
12 int a[10];
13 Init(a,10);
14 for(i=0;i<10;i++)
15 {
16 printf(“%d\n”,a[i]);
17 }
18 return 0;
19}
在此基础上,我们用指针来处理上述问题,下面是示例代码。
1void Init(int (*arr)[10])
size 3 _ t i=0;
4 for(i=0;i< sizeof(*arr)/sizeof(int);i++)
5 {
6 (*arr)[i]=i;
7 }
8}
9int main(void)
10{
11 int i=0;
12 int a[10];
13 Init(&a);
14 for(i=0;i<10;i++)
15 {
16 printf(“%d\n”,a[i]);
17 }
18 return 0;
19}
如今Init()函数的arr参数就是指向arr[10]型指针。特别指出在此千万不能用void Init(int(*arr)[])声明一个函数,而必须指出将要传入数组的尺寸,不然sizeof(*arr[])就不能算出。但在这样的情形中,重新用sizeof计算数组的尺寸已毫无意义,因为这时数组的尺寸已被规定为10。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至tbfoyi@qq.com举报,一经查实,本站将立刻删除。本文编辑:龙九,如若转载,请注明出处:https://www.yulinglongsj.com/focus/8897.html