這裡整理一些 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