導讀:增強 C 語言程序的彈性和可靠性的五種方法?!? 本文字數:8391,閱讀時長大約:
10分鐘
https://linux.cn/article-13894-1.html
作者:Jim Hall
譯者:unigeorge
即使是最好的程序員也無法完全避免錯誤。這些錯誤可能會引入安全漏洞、導致程序崩潰或產生意外操作,具體影響要取決于程序的運行邏輯。
C 語言有時名聲不太好,因為它不像近期的編程語言(比如 Rust)那樣具有內存安全性。但是通過額外的代碼,一些最常見和嚴重的 C 語言錯誤是可以避免的。下文講解了可能影響應用程序的五個錯誤以及避免它們的方法:
1、未初始化的變量
程序啟動時,系統(tǒng)會為其分配一塊內存以供存儲數據。這意味著程序啟動時,變量將獲得內存中的一個隨機值。
有些編程環(huán)境會在程序啟動時特意將內存“清零”,因此每個變量都得以有初始的零值。程序中的變量都以零值作為初始值,聽上去是很不錯的。但是在 C 編程規(guī)范中,系統(tǒng)并不會初始化變量。
看一下這個使用了若干變量和兩個數組的示例程序:
-
#include <stdio.h>
-
#include <stdlib.h>
-
-
int
-
main()
-
{
-
int i, j, k;
-
int numbers[5];
-
int *array;
-
-
puts("These variables are not initialized:");
-
-
printf(" i = %d\n", i);
-
printf(" j = %d\n", j);
-
printf(" k = %d\n", k);
-
-
puts("This array is not initialized:");
-
-
for (i = 0; i < 5; i ) {
-
printf(" numbers[%d] = %d\n", i, numbers[i]);
-
}
-
-
puts("malloc an array ...");
-
array = malloc(sizeof(int) * 5);
-
-
if (array) {
-
puts("This malloc'ed array is not initialized:");
-
-
for (i = 0; i < 5; i ) {
-
printf(" array[%d] = %d\n", i, array[i]);
-
}
-
-
free(array);
-
}
-
-
/* done */
-
-
puts("Ok");
-
return 0;
-
}
這個程序不會初始化變量,所以變量以系統(tǒng)內存中的隨機值作為初始值。在我的 Linux 系統(tǒng)上編譯和運行這個程序,會看到一些變量恰巧有“零”值,但其他變量并沒有:
-
These variables are not initialized:
-
i = 0
-
j = 0
-
k = 32766
-
This array is not initialized:
-
numbers[0] = 0
-
numbers[1] = 0
-
numbers[2] = 4199024
-
numbers[3] = 0
-
numbers[4] = 0
-
malloc an array ...
-
This malloc'ed array is not initialized:
-
array[0] = 0
-
array[1] = 0
-
array[2] = 0
-
array[3] = 0
-
array[4] = 0
-
Ok
很幸運,i和j變量是從零值開始的,但k的起始值為 32766。在numbers數組中,大多數元素也恰好從零值開始,只有第三個元素的初始值為 4199024。
在不同的系統(tǒng)上編譯相同的程序,可以進一步顯示未初始化變量的危險性。不要誤以為“全世界都在運行 Linux”,你的程序很可能某天在其他平臺上運行。例如,下面是在 FreeDOS 上運行相同程序的結果:
-
These variables are not initialized:
-
i = 0
-
j = 1074
-
k = 3120
-
This array is not initialized:
-
numbers[0] = 3106
-
numbers[1] = 1224
-
numbers[2] = 784
-
numbers[3] = 2926
-
numbers[4] = 1224
-
malloc an array ...
-
This malloc'ed array is not initialized:
-
array[0] = 3136
-
array[1] = 3136
-
array[2] = 14499
-
array[3] = -5886
-
array[4] = 219
-
Ok
永遠都要記得初始化程序的變量。如果你想讓變量將以零值作為初始值,請額外添加代碼將零分配給該變量。預先編好這些額外的代碼,這會有助于減少日后讓人頭疼的調試過程。
2、數組越界
C 語言中,數組索引從零開始。這意味著對于長度為 10 的數組,索引是從 0 到 9;長度為 1000 的數組,索引則是從 0 到 999。
程序員有時會忘記這一點,他們從索引 1 開始引用數組,產生了“大小差一”(off by one)錯誤。在長度為 5 的數組中,程序員在索引“5”處使用的值,實際上并不是數組的第 5 個元素。相反,它是內存中的一些其他值,根本與此數組無關。
這是一個數組越界的示例程序。該程序使用了一個只含有 5 個元素的數組,但卻引用了該范圍之外的數組元素:
-
#include <stdio.h>
-
#include <stdlib.h>
-
-
int
-
main()
-
{
-
int i;
-
int numbers[5];
-
int *array;
-
-
/* test 1 */
-
-
puts("This array has five elements (0 to 4)");
-
-
/* initalize the array */
-
for (i = 0; i < 5; i ) {
-
numbers[i] = i;
-
}
-
-
/* oops, this goes beyond the array bounds: */
-
for (i = 0; i < 10; i ) {
-
printf(" numbers[%d] = %d\n", i, numbers[i]);
-
}
-
-
/* test 2 */
-
-
puts("malloc an array ...");
-
-
array = malloc(sizeof(int) * 5);
-
-
if (array) {
-
puts("This malloc'ed array also has five elements (0 to 4)");
-
-
/* initalize the array */
-
for (i = 0; i < 5; i ) {
-
array[i] = i;
-
}
-
-
/* oops, this goes beyond the array bounds: */
-
for (i = 0; i < 10; i ) {
-
printf(" array[%d] = %d\n", i, array[i]);
-
}
-
-
free(array);
-
}
-
-
/* done */
-
-
puts("Ok");
-
return 0;
-
}
可以看到,程序初始化了數組的所有值(從索引 0 到 4),然后從索引 0 開始讀取,結尾是索引 9 而不是索引 4。前五個值是正確的,再后面的值會讓你不知所以:
-
This array has five elements (0 to 4)
-
numbers[0] = 0
-
numbers[1] = 1
-
numbers[2] = 2
-
numbers[3] = 3
-
numbers[4] = 4
-
numbers[5] = 0
-
numbers[6] = 4198512
-
numbers[7] = 0
-
numbers[8] = 1326609712
-
numbers[9] = 32764
-
malloc an array ...
-
This malloc'ed array also has five elements (0 to 4)
-
array[0] = 0
-
array[1] = 1
-
array[2] = 2
-
array[3] = 3
-
array[4] = 4
-
array[5] = 0
-
array[6] = 133441
-
array[7] = 0
-
array[8] = 0
-
array[9] = 0
-
Ok
引用數組時,始終要記得追蹤數組大小。將數組大小存儲在變量中;不要對數組大小進行硬編碼(hard-code)。否則,如果后期該標識符指向另一個不同大小的數組,卻忘記更改硬編碼的數組長度時,程序就可能會發(fā)生數組越界。
3、字符串溢出
字符串只是特定類型的數組。在 C 語言中,字符串是一個由char類型值組成的數組,其中用一個零字符表示字符串的結尾。
因此,與數組一樣,要注意避免超出字符串的范圍。有時也稱之為
字符串溢出。
使用gets函數讀取數據是一種很容易發(fā)生字符串溢出的行為方式。gets函數非常危險,因為它不知道在一個字符串中可以存儲多少數據,只會機械地從用戶那里讀取數據。如果用戶輸入像foo這樣的短字符串,不會發(fā)生意外;但是當用戶輸入的值超過字符串長度時,后果可能是災難性的。
下面是一個使用gets函數讀取城市名稱的示例程序。在這個程序中,我還添加了一些未使用的變量,來展示字符串溢出對其他數據的影響:
-
#include <stdio.h>
-
#include <string.h>
-
-
int
-
main()
-
{
-
char name[10]; /* Such as "Chicago" */
-
int var1 = 1, var2 = 2;
-
-
/* show initial values */
-
-
printf("var1 = %d; var2 = %d\n", var1, var2);
-
-
/* this is bad .. please don't use gets */
-
-
puts("Where do you live?");
-
gets(name);
-
-
/* show ending values */
-
-
printf("<%s> is length %d\n", name, strlen(name));
-
printf("var1 = %d; var2 = %d\n", var1, var2);
-
-
/* done */
-
-
puts("Ok");
-
return 0;
-
}
當你測試類似的短城市名稱時,該程序運行良好,例如伊利諾伊州的Chicago或北卡羅來納州的Raleigh:
-
var1 = 1; var2 = 2
-
Where do you live?
-
Raleigh
-
<Raleigh> is length 7
-
var1 = 1; var2 = 2
-
Ok
威爾士的小鎮(zhèn)Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch有著世界上最長的名字之一。這個字符串有 58 個字符,遠遠超出了name變量中保留的 10 個字符。結果,程序將值存儲在內存的其他區(qū)域,覆蓋了var1和var2的值:
-
var1 = 1; var2 = 2
-
Where do you live?
-
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
-
<Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
-
var1 = 2036821625; var2 = 2003266668
-
Ok
-
Segmentation fault (core dumped)
在運行結束之前,程序會用長字符串覆蓋內存的其他部分區(qū)域。注意,var1和var2的值不再是起始的1和2。
避免使用gets函數,改用更安全的方法來讀取用戶數據。例如,getline函數會分配足夠的內存來存儲用戶輸入,因此不會因輸入長值而發(fā)生意外的字符串溢出。
4、重復釋放內存
“分配的內存要手動釋放”是良好的 C
語言編程原則之一。程序可以使用malloc函數為數組和字符串分配內存,該函數會開辟一塊內存,并返回一個指向內存中起始地址的指針。之后,程序可以使用free函數釋放內存,該函數會使用指針將內存標記為未使用。
但是,你應該只使用一次free函數。第二次調用free會導致意外的后果,可能會毀掉你的程序。下面是一個針對此點的簡短示例程序。程序分配了內存,然后立即釋放了它。但為了模仿一個健忘但有條理的程序員,我在程序結束時又一次釋放了內存,導致兩次釋放了相同的內存:
-
#include <stdio.h>
-
#include <stdlib.h>
-
-
int
-
main()
-
{
-
int *array;
-
-
puts("malloc an array ...");
-
-
array = malloc(sizeof(int) * 5);
-
-
if (array) {
-
puts("malloc succeeded");
-
-
puts("Free the array...");
-
free(array);
-
}
-
-
puts("Free the array...");
-
free(array);
-
-
puts("Ok");
-
}
運行這個程序會導致第二次使用free函數時出現(xiàn)戲劇性的失?。?
-
malloc an array ...
-
malloc succeeded
-
Free the array...
-
Free the array...
-
free(): double free detected in tcache 2
-
Aborted (core dumped)
要記得避免在數組或字符串上多次調用free。將malloc和free函數定位在同一個函數中,這是避免重復釋放內存的一種方法。
例如,一個紙牌游戲程序可能會在主函數中為一副牌分配內存,然后在其他函數中使用這副牌來玩游戲。記得在主函數,而不是其他函數中釋放內存。將malloc和free語句放在一起有助于避免多次釋放內存。
5、使用無效的文件指針
文件是一種便捷的數據存儲方式。例如,你可以將程序的配置數據存儲在config.dat文件中。Bash shell 會從用戶家目錄中的.bash_profile讀取初始化腳本。GNU Emacs 編輯器會尋找文件.emacs以從中確定起始值。而 Zoom 會議客戶端使用zoomus.conf文件讀取其程序配置。
所以,從文件中讀取數據的能力幾乎對所有程序都很重要。但是假如要讀取的文件不存在,會發(fā)生什么呢?
在 C 語言中讀取文件,首先要用fopen函數打開文件,該函數會返回指向文件的流指針。你可以結合其他函數,使用這個指針來讀取數據,例如fgetc會逐個字符地讀取文件。
如果要讀取的文件不存在或程序沒有讀取權限,fopen函數會返回NULL作為文件指針,這表示文件指針無效。但是這里有一個示例程序,它機械地直接去讀取文件,不檢查fopen是否返回了NULL:
-
#include <stdio.h>
-
-
int
-
main()
-
{
-
FILE *pfile;
-
int ch;
-
-
puts("Open the FILE.TXT file ...");
-
-
pfile = fopen("FILE.TXT", "r");
-
-
/* you should check if the file pointer is valid, but we skipped that */
-
-
puts("Now display the contents of FILE.TXT ...");
-
-
while ((ch = fgetc(pfile)) != EOF) {
-
printf("<%c>", ch);
-
}
-
-
fclose(pfile);
-
-
/* done */
-
-
puts("Ok");
-
return 0;
-
}
當你運行這個程序時,第一次調用fgetc會失敗,程序會立即中止:
-
Open the FILE.TXT file ...
-
Now display the contents of FILE.TXT ...
-
Segmentation fault (core dumped)
始終檢查文件指針以確保其有效。例如,在調用fopen打開一個文件后,用類似if (pfile != NULL)的語句檢查指針,以確保指針是可以使用的。
人都會犯錯,最優(yōu)秀的程序員也會產生編程錯誤。但是,遵循上面這些準則,添加一些額外的代碼來檢查這五種類型的錯誤,就可以避免最嚴重的 C
語言編程錯誤。提前編寫幾行代碼來捕獲這些錯誤,可能會幫你節(jié)省數小時的調試時間。
via: https://opensource.com/article/21/10/programming-bugs
作者:Jim Hall 選題:lujun9972 譯者:unigeorge 校對:wxy
本文由 LCTT 原創(chuàng)編譯
本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內容侵犯您的權益,請及時聯(lián)系本站刪除。