詳解C語言數(shù)組越界
在C語言中,數(shù)組必須是靜態(tài)的。換而言之,數(shù)組的大小必須在程序運行前就確定下來。由于C語言并不具有類似Java等語言中現(xiàn)有的靜態(tài)分析工具的功能,可以對程序中數(shù)組下標取值范圍進行嚴格檢查,一旦發(fā)現(xiàn)數(shù)組上溢或下溢,都會因拋出異常而終止程序。也就是說,C語言并不檢驗數(shù)組邊界,數(shù)組的兩端都有可能越界,從而使其他變量的數(shù)據(jù)甚至程序代碼被破壞。因此,數(shù)組下標的取值范圍只能預先推斷一個值來確定數(shù)組的維數(shù),而檢驗數(shù)組的邊界是程序員的職責。一般情況下,數(shù)組的越界錯誤主要包括兩種:
數(shù)組下標取值越界與指向數(shù)組的指針的指向范圍越界
。數(shù)組下標取值越界
數(shù)組下標取值越界主要是指訪問數(shù)組的時候,下標的取值不在已定義好的數(shù)組的取值范圍內,而訪問的是無法獲取的內存地址。例如,對于數(shù)組?int a[3]
,它的下標取值范圍是[0,2]
(即a[0]、a[1] 與 a[2]
)。如果我們的取值不在這個范圍內(如 a[3]),就會發(fā)生越界錯誤。示例代碼如下所示: 1int?a[3];
2int?i=0;
3for(i=0;i<4;i )
4{
5????a[i]?=?i;
6}
7for(i=0;i<4;i )
8{
9????printf("a[%d]=%d\n",i,a[i]);
10}
很顯然,在上面的示例程序中,訪問 a[3] 是非法的,將會發(fā)生越界錯誤。因此,我們應該將上面的代碼修改成如下形式: 1int?a[3];
2int?i=0;
3for(i=0;i<3;i )
4{
5????a[i]?=?i;
6}
7for(i=0;i<3;i )
8{
9????printf("a[%d]=%d\n",i,a[i]);
10}
指向數(shù)組的指針的指向范圍越界
指向數(shù)組的指針的指向范圍越界是指定義數(shù)組時會返回一個指向第一個變量的頭指針,對這個指針進行加減運算可以向前或向后移動這個指針,進而訪問數(shù)組中所有的變量。但在移動指針時,如果不注意移動的次數(shù)和位置,會使指針指向數(shù)組以外的位置,導致數(shù)組發(fā)生越界錯誤。下面的示例代碼,就是移動指針時沒有考慮到移動的次數(shù)和數(shù)組的范圍,從而使程序訪問了數(shù)組以外的存儲單元。 1int?i;
2int?*p;
3int?a[5];
4/*數(shù)組a的頭指針賦值給指針p*/
5p=a;
6for(i=0;i<10;i )
7{
8????/*指針p指向的變量*/
9????*p=i 10;
10????/*指針p下一個變量*/
11????p ;
12}
在上面的示例代碼中,for循環(huán)會使指針p向后移動10次,并且每次向指針指向的單元賦值。但是,這里數(shù)組a的下標取值范圍是?[0,4]
(即?a[0]、a[1]、a[2]、a[3] 與 a[4]
)。因此,后5次的操作會對未知的內存區(qū)域賦值,而這種向內存未知區(qū)域賦值的操作會使系統(tǒng)發(fā)生錯誤。正確的操作,應該是指針移動的次數(shù)與數(shù)組中的變量個數(shù)相同,如下面的代碼所示: 1int?i;
2int?*p;
3int?a[5];
4/*數(shù)組a的頭指針賦值給指針p*/
5p=a;
6for(i=0;i<5;i )
7{
8????/*指針p指向的變量*/
9????*p=i 10;
10????/*指針p下一個變量*/
11????p ;
12}
為了加深大家對數(shù)組越界的了解,下面通過一段完整的數(shù)組越界示例來演示編程中數(shù)組越界將會導致哪些問題。 1#define?PASSWORD?"123456"
2int?Test(char?*str)
3{
4????int?flag;
5????char?buffer[7];
6????flag=strcmp(str,PASSWORD);
7????strcpy(buffer,str);
8????return?flag;
9}
10int?main(void)
11{
12????int?flag=0;
13????char?str[1024];
14????while(1)
15????{
16????????printf("請輸入密碼:??");
17????????scanf("%s",str);
18????????flag?=?Test(str);
19????????if(flag)
20????????{
21????????????printf("密碼錯誤!\n");
22????????}
23????????????else
24????????????{
25????????????????printf("密碼正確!\n");
26????????????}
27????}
28????return?0;
29}
上面的示例代碼模擬了一個密碼驗證的例子,它將用戶輸入的密碼與宏定義中的密碼123456
進行比較。很顯然,本示例中最大的設計漏洞就在于 Test() 函數(shù)中的?strcpy(buffer,str)
?調用。由于程序將用戶輸入的字符串原封不動地復制到 Test() 函數(shù)的數(shù)組?char buffer[7]
?中。因此,當用戶的輸入大于 7 個字符的緩沖區(qū)尺寸時,就會發(fā)生數(shù)組越界錯誤,這也就是大家所謂的緩沖區(qū)溢出Buffer overflow?漏洞。但是要注意,如果這個時候我們根據(jù)緩沖區(qū)溢出發(fā)生的具體情況填充緩沖區(qū),不但可以避免程序崩潰,還會影響到程序的執(zhí)行流程,甚至會讓程序去執(zhí)行緩沖區(qū)里的代碼。示例運行結果為: 1請輸入密碼:12345
2密碼錯誤!
3請輸入密碼:123456
4密碼正確!
5請輸入密碼:1234567
6密碼正確!
7請輸入密碼:aaaaaaa
8密碼正確!
9請輸入密碼:0123456
10密碼錯誤!
11請輸入密碼:
在示例代碼中,flag 變量實際上是一個標志變量,其值將決定著程序是進入密碼錯誤
的流程(非 0)還是“密碼正確”的流程(0)。當我們輸入錯誤的字符串1234567
或者aaaaaaa
,程序也都會輸出“密碼正確”。但在輸入0123456
的時候,程序卻輸出“密碼錯誤”,這究竟是為什么呢?其實,原因很簡單。當調用 Test() 函數(shù)時,系統(tǒng)將會給它分配一片連續(xù)的內存空間,而變量?char buffer[7]
?與?int flag
?將會緊挨著進行存儲,用戶輸入的字符串將會被復制進 buffer[7] 中。如果這個時候,我們輸入的字符串數(shù)量超過 6 個(注意,有字符串截斷符也算一個),那么超出的部分將破壞掉與它緊鄰著的 flag 變量的內容。當輸入的密碼不是宏定義的123456
時,字符串比較將返回 1 或 -1。我們都知道,內存中的數(shù)據(jù)按照 4 字節(jié)(DWORD)逆序存儲,所以當 flag 為 1 時,在內存中存儲的是0x01000000
。如果我們輸入包含 7 個字符的錯誤密碼,如aaaaaaa
,那么字符串截斷符 0x00 將寫入 flag 變量,這樣溢出數(shù)組的一個字節(jié) 0x00 將恰好把逆序存放的 flag 變量改為?0x00000000
。在函數(shù)返回后,一旦 main 函數(shù)的 flag 為 0,就會輸出“密碼正確”。這樣,我們就用錯誤的密碼得到了正確密碼的運行效果。而對于0123456
,因為在進行字符串的大小比較時,它小于123456
,flag的值是 -1,在內存中將按照補碼存放負數(shù),所以實際存儲的不是?0x01000000
?而是?0xffffffff
。那么字符串截斷后符 0x00 淹沒后,變成?0x00ffffff
,還是非 0,所以沒有進入正確分支。其實,本示例只是用一個字節(jié)淹沒了鄰接變量,導致程序進入密碼正確的處理流程,使設計的驗證功能失效。盡量顯式地指定數(shù)組的邊界
在 C 語言中,為了提高運行效率,給程序員更大的空間,為指針操作帶來更多的方便,C 語言內部本身不檢查數(shù)組下標表達式的取值是否在合法范圍內,也不檢查指向數(shù)組元素的指針是不是移出了數(shù)組的合法區(qū)域。因此,在編程中使用數(shù)組時就必須格外謹慎,在對數(shù)組進行讀寫操作時都應當進行相應的檢查,以免對數(shù)組的操作超過數(shù)組的邊界,從而發(fā)生緩沖區(qū)溢出漏洞。要避免程序因數(shù)組越界所發(fā)生的錯誤,首先就需要從數(shù)組的邊界定義開始。盡量顯式地指定數(shù)組的邊界,即使它已經(jīng)由初始化值列表隱式指定。示例代碼如下所示:1int?a[]={1,2,3,4,5,6,7,8,9,10};
很顯然,對于上面的數(shù)組 a[],雖然編譯器可以根據(jù)始化值列表來計算出數(shù)組的長度。但是,如果我們顯式地指定該數(shù)組的長度,例如:1int?a[10]={1,2,3,4,5,6,7,8,9,10};
它不僅使程序具有更好的可讀性,并且大多數(shù)編譯器在數(shù)組長度小于初始化值列表的長度時還會發(fā)生相應警告。當然,也可以使用宏的形式來顯式指定數(shù)組的邊界(實際上,這也是最常用的指定方法),如下面的代碼所示:1#define?MAX?10
2…
3int?a[MAX]={1,2,3,4,5,6,7,8,9,10};
除此之外,在 C99 標準中,還允許我們使用單個指示符為數(shù)組的兩段“分配”空間,如下面的代碼所示:1int?a[MAX]={1,2,3,4,5,[MAX-5]=6,7,8,9,10};
在上面的?a[MAX]
數(shù)組中,如果 MAX 大于 10,數(shù)組中間將用 0 值元素進行填充(填充的個數(shù)為?MAX-10
,并從 a[5] 開始進行 0 值填充);如果 MAX 小于 10,[MAX-5]
之前的 5 個元素(1,2,3,4,5)
中將有幾個被[MAX-5]
之后的 5 個元素(6,7,8,9,10)
所覆蓋,示例代碼如下所示: 1#define?MAX?10
2#define?MAX1?15
3#define?MAX2?6
4int?main(void)
5{
6????int?a[MAX]={1,2,3,4,5,[MAX-5]=6,7,8,9,10};
7????int?b[MAX1]={1,2,3,4,5,[MAX1-5]=6,7,8,9,10};
8????int?c[MAX2]={1,2,3,4,5,[MAX2-5]=6,7,8,9,10};
9????int?i=0;
10????int?j=0;
11????int?z=0;
12????printf("a[MAX]:\n");
13????for(i=0;i14????{
15????????printf("a[%d]=%d?",i,a[i]);
16????}
17????printf("\nb[MAX1]:\n");
18????for(j=0;j19????{
20????????printf("b[%d]=%d?",j,b[j]);
21????}
22????printf("\nc[MAX2]:\n");
23????for(z=0;z24????{
25????????printf("c[%d]=%d?",z,c[z]);
26????}
27????printf("\n");
28????return?0;
29}
運行結果為:1a[MAX]:
2a[0]=1?a[1]=2?a[2]=3?a[3]=4?a[4]=5?a[5]=6?a[6]=7?a[7]=8?a[8]=9?a[9]=10
3b[MAX1]:
4b[0]=1?b[1]=2?b[2]=3?b[3]=4?b[4]=5?b[5]=0?b[6]=0?b[7]=0?b[8]=0?b[9]=0?b[10]=6?b[11]=7?b[12]=8?b[13]=9?b[14]=10
5c[MAX2]:
6c[0]=1?c[1]=6?c[2]=7?c[3]=8?c[4]=9?c[5]=10
對數(shù)組做越界檢查,確保索引值位于合法的范圍之內
要避免數(shù)組越界,除了上面所闡述的顯式指定數(shù)組的邊界之外,還可以在數(shù)組使用之前進行越界檢查,檢查數(shù)組的界限和字符串(也以數(shù)組的方式存放)的結束,以保證數(shù)組索引值位于合法的范圍之內。例如,在寫處理數(shù)組的函數(shù)時,一般應該有一個范圍參數(shù);在處理字符串時總檢查是否遇到空字符‘\0’。來看下面一段代碼示例: 1#define?ARRAY_NUM?10
2int?*TestArray(int?num,int?value)
3{
4????int?*arr=NULL;
5????arr=(int?*)malloc(sizeof(int)*ARRAY_NUM);
6????if(arr!=NULL)
7????{
8????????arr[num]=value;
9????}
10????else
11????{
12????????/*處理arr==NULL*/
13????}
14????return?arr;
15}
從上面的int*TestArray(int num,int value)
函數(shù)中不難看出,其中存在著一個很明顯的問題,那就是無法保證 num 參數(shù)是否越界(即當num>=ARRAY_NUM
的情況)。因此,應該對 num 參數(shù)進行越界檢查,示例代碼如下所示: 1int?*TestArray(int?num,int?value)
2{
3????int?*arr=NULL;
4????/*越界檢查(越上界)*/
5????if(num 6????{
7????????arr=(int?*)malloc(sizeof(int)*ARRAY_NUM);
8????????if(arr!=NULL)
9????????{
10????????????arr[num]=value;
11????????}
12????????else
13????????{
14????????????/*處理arr==NULL*/
15????????}
16????}
17????return?arr;
18}
這樣通過if(num語句進行越界檢查,從而保證 num 參數(shù)沒有越過這個數(shù)組的上界?,F(xiàn)在看起來,TestArray() 函數(shù)應該沒什么問題,也不會發(fā)生什么越界錯誤。但是,如果仔細檢查,TestArray() 函數(shù)仍然還存在一個致命的問題,那就是沒有檢查數(shù)組的下界。由于這里的 num 參數(shù)類型是 int 類型,因此可能為負數(shù)。如果 num 參數(shù)所傳遞的值為負數(shù),將導致在 arr 所引用的內存邊界之外進行寫入。當然,你可以通過向if(num語句里面再加一個條件進行測試,如下面的代碼所示:1if(num>=0