這裡整理一些 C 程式語言相關的面試問題與解答,除了可以讓你增強 C 程式語言的能力,對於面試可能也有幫助。
gets()
函數
下面這段程式碼中有一個問題,你能找出來嗎?
#include<stdio.h> int main(void) { char buff[10]; memset(buff, 0, sizeof(buff)); gets(buff); printf("\n The buffer entered is [%s]\n", buff); return 0; }
答案
這裡的問題出在 gets()
函數的使用,這個函數會從 stdin 中讀取字串,但是卻不會檢查緩衝區的大小,這有可能會造成緩衝區溢位(buffer overflow)的問題,改用標準的 fgets()
函數會是個比較好的方式。
strcpy()
函數
下面是一段用於密碼驗證的程式碼,請問他是否有漏洞?
#include<stdio.h> int main(int argc, char *argv[]) { int flag = 0; char passwd[10]; memset(passwd,0,sizeof(passwd)); strcpy(passwd, argv[1]); if(0 == strcmp("LinuxGeek", passwd)) { flag = 1; } if(flag) { printf("Password cracked \n"); } else { printf("Incorrect passwd \n"); } return 0; }
答案
有!這段驗證密碼的程式可以透過 strcpy() 函數的漏洞來破解,由於這個函數把使用者輸入的密碼複製到 passwd
這個變數中,但是卻沒有檢查使用者所提供的密碼長度是否可以被 passwd
這個變數所容納,所以當使用者提供一個長度超過緩衝區的大小的任意字串時,就會造成緩衝區溢位,並將 flag
這個變數所在的記憶體位址覆寫(overwrite),將原本的 0
改為其他的值,接著在下面的 if
判斷式中就會判斷出 flag
是一個非零的值(也就是會得到 true
),這樣這段檢查密碼的機制就被破解了。
我們將這段程式碼編譯成 chkpwd
,則這樣就可以破解它了:
./chkpwd aaaaaaaaaaaaa
輸出為
Password cracked
所以只要輸入的字串夠長,無論密碼是否正確,都可以通過密碼的檢查。如果要避免這樣的問題,可以改用 strncpy()
函數。
註:由於現在的編譯器比較聰明,會自動偵測 stack smashing 的可能性,讓這種問題發生的機率降低,所以如果想要自己測試這段程式碼,可能要把編譯器的這個功能關閉才可以進行測試,例如使用 gcc 編譯器就要加上 -fno-stack-protector
參數,這樣才能重現這個狀況:
gcc -fno-stack-protector -o chkpwd chkpwd.c
main()
函數的傳回型態
下面這段程式碼是否可以通過編譯?如果可以,則是否有什麼問題存在?
#include<stdio.h> void main(void) { char *ptr = (char*)malloc(10); if(NULL == ptr) { printf("\n Malloc failed \n"); return; } else { // Do some processing free(ptr); } return; }
答案
這段程式碼可以通過編譯,但是通常會有一些警告訊息出現,原因在於 main()
函數的傳回值應該要是 int
而不能是 void
,這個值會被整個程式當成執行結果傳回給上層,如果這個程式是指令稿(script)的一部份,而在指令稿中要判斷這個程式的執行是否有問題,就是依據這個傳回值來判斷的。
記憶體洩漏(Memory Leak)
下面這段程式碼是否會造成記憶體洩漏(Memory Leak)?
#include<stdio.h> void main(void) { char *ptr = (char*)malloc(10); if(NULL == ptr) { printf("\n Malloc failed \n"); return; } else { // Do some processing } return; }
答案
這段程式碼並不會造成記憶體洩漏(Memory Leak)的問題。縱使它並沒有把動態配置給 ptr
的記憶體釋放掉,但是因為這段程式碼是放在程式的結尾處,在程式結束的時候,所有程式所使用的記憶體都會被系統一次收回,所以即便程式本身沒有收回記憶體,也不會造成記憶體洩漏的問題。
但是如果這段程式碼不是放在程式的結尾處,或是放在一個迴圈中,那就會有記憶體洩漏的問題了。如果你對於記憶體的釋放有興趣,可以參考 C 語言中關於記憶體釋放的議題。
free()
函數
在下面這段程式碼中,當使用者輸入「freeze」時,程式就會出現 segmentation fault(當掉),而如果輸入「zebra」則正常,你能解釋為什麼嗎?
#include<stdio.h> int main(int argc, char *argv[]) { char *ptr = (char*)malloc(10); if(NULL == ptr) { printf("\n Malloc failed \n"); return -1; } else if(argc == 1) { printf("\n Usage \n"); } else { memset(ptr, 0, 10); strncpy(ptr, argv[1], 9); while(*ptr != 'z') { if(*ptr == '') break; else ptr++; } if(*ptr == 'z') { printf("\n String contains 'z'\n"); // Do some more processing } free(ptr); } return 0; }
答案
這裡的問題在於迴圈中有更動 ptr
這個指標所儲存的位址,所以在最後呼叫 free(ptr)
時,因為指定的記憶體位址錯誤造成程式當掉,而如果使用者輸入「zebra」時,因為迴圈內的程式並沒有執行,所以 ptr
所儲存的位址沒有變動,也就不會出問題。
atexit()
與 _exit()
函數
在下面的程式碼中,atexit()
函數並不會被呼叫,你能解釋為什麼嗎?
#include<stdio.h> void func(void) { printf("\n Cleanup function called \n"); return; } int main(void) { int i = 0; atexit(func); for(;i<0xffffff;i++); _exit(0); }
答案
這裡的問題在於 _exit()
函數不會呼叫清理函數(clean-up functions,例如 atexit()
),如果要讓 atexit()
正常被呼叫,則要使用 exit()
或是 return
。
void*
與 C 語言的結構
你是否可以設計一個容許接受任何型態參數的函數,並且傳回一個整數?另外這個函數是否有辦法讓多個參數一次傳入?
答案
一個容許接受任何型態參數的函數可以這樣設計:
int func(void *ptr)
如果想要一次傳入多個參數,可以將所有要傳入的參數放在一個 structure object 中,再將這個 structure object 傳入此函數。
*
與 ++
運算子
下面這段程式執行之後的輸出為何?為什麼?
#include<stdio.h> int main(void) { char *ptr = "Linux"; printf("\n [%c] \n",*ptr++); printf("\n [%c] \n",*ptr); return 0; }
答案
這段程式的執行結果如下:
[L]
[i]
由於 *
與 ++
這兩個運算子的優先順序是一樣的,所以當放在 *ptr++
這個運算式中時,會以右邊的運算子優先處理,也就是 ++
運算子會先傳回 ptr
的位址,再由 *
取值,而得到「L」,接著因為 ++
會把 ptr
所儲存的位址加 1,所以下一個 printf()
所輸出的字元就是「i」。
更改唯讀的記憶體區段
下面這段程式碼在執行時會出現 segmentation fault(當掉),你能說明為什麼嗎?
#include<stdio.h> int main(void) { char *ptr = "Linux"; *ptr = 'T'; printf("\n [%s] \n", ptr); return 0; }
答案
因為在 *ptr = 'T'
運算式中嘗試更改 'Linux'
這個字串的第一個字元,而這個字串是儲存在唯讀的記憶體區段(亦即 code segment),這樣的動作是不合法的,會造成程式出現 segmentation fault 或 crash。
更改自己名稱的行程(Process)
你可以寫一個在執行時更改自己名稱的程式嗎?
答案
可以,下面的程式碼就可以達到這樣的功能:
#include<stdio.h> int main(int argc, char *argv[]) { int i = 0; char buff[100]; memset(buff,0,sizeof(buff)); strncpy(buff, argv[0], sizeof(buff)); memset(argv[0],0,strlen(buff)); strncpy(argv[0], "NewName", 7); // Simulate a wait. Check the process // name at this point. for(;i<0xffffffff;i++); return 0; }
傳回區域變數的記憶體位址
下面這段程式碼是否有問題?如果有問題,該如何修正?
#include<stdio.h> int* inc(int val) { int a = val; a++; return &a; } int main(void) { int a = 10; int *val = inc(a); printf("\n Incremented value is equal to [%d] \n", *val); return 0; }
答案
這段程式碼可能可以正常執行,但是在 inc()
函數中有一個嚴重的漏洞,這個函數傳回一個區域變數 a
的記憶體位址,由於區域變數的生命週期只限於 inc()
函數的執行時期,在該函數結束之後這個區域變數就不存在了,所以將其記憶體位址傳回使用將會造成不可預期的結果。
這裡可以直接將 main()
函數中 a
變數的記憶體位址傳入 inc()
函數中,這樣就可以在 inc()
函數中更改 a
變數的值,以避免這個區域變數問題。
printf()
函數的參數的處理
下面這段程式碼執行後的輸出為何?
#include<stdio.h> int main(void) { int a = 10, b = 20, c = 30; printf("\n %d..%d..%d \n", a+b+c, (b = b*2), (c = c*2)); return 0; }
答案
這段程式碼執行後的輸出為
110..40..60
會導致這樣的結果的原因在於:函數的參數處理的順序是由右至左,而輸出時的順序則是由左至右。
參考資料:The Geek Stuff