關於C語言的部分BUG
目錄
scanf格式匹配引發的錯誤
執行如下程式時,出現這類錯誤:`*** stack smashing detected ***: ./test_global terminated`。錯誤原因可能是因為`scanf("%d%d", &row, &col)`接收的是`int`型,但是我使用的是`short int`,長度是`Int`的一半。修改成`int`後錯誤消失。
#include<stdio.h> int main(){ int row, col; scanf("%d%d", &row, &col); printf("%d %d", row, col); return 0; }
使用gcc編譯時出現的警告如下:

出現的錯誤如下:

區域性變數被釋放引發的bug
執行如下程式時,會無終止地列印-1。原因是變數p所指向的變數k在addr()函式執行後自行銷燬,k所使用的記憶體被分配給loop()中的變數i,從而導致p指向i。而此時對p的操作是減1,對i的操作是加1,導致i的值始終為-1,無法跳出迴圈。
#include<stdio.h> void addr(); void loop(); long *p; int main(){ addr(); loop(); } void addr(){ long k; k = 0; p = &k; } void loop(){ long i, j; j = 0; for (i = 0; i<10;i++){ (*p)--; j++; printf("%d\n", i); } }
程式執行輸出結果如下:

程式除錯結果如下:

陣列寫入超出索引維度
雖然執行下面程式碼不會出錯,但是對陣列a[10]的寫操作超出了維度,導致在地址為a+10的地方也寫入了資料,但是容易引發潛在bug。
#include<stdio.h> int main() { int i; int a[10]; for (i = 0; i <= 10; ++i) { a[i] = 0; printf("%d\n", i); } exit(0); }
指標的指標引發的思考
對於將指標作為引數進行傳遞時,如果是將在子函式內賦值給一個新申請的空間,那麼就要注意在傳遞指標時,需要傳遞指標的地址,即指標的指標。錯誤程式如下:
#include<stdio.h> void allocateInt(int * i, int m); void main() { int m = 5; int * i = &m; printf("i address: %x\n", &i); allocateInt(i, m); printf("*i = %d\n", *i); } void allocateInt(int * i, int m) { printf("i address: %x\n", &i); i = (int *) malloc(sizeof(int)); *i = 3; }
指標的指標引發的思考——思考
雖然對該問題的解釋一般是:在傳遞引數時,系統為子函式的變數新申請一部分空間,因此在 void allocateInt(int * i)
中,i的地址和在 void main()
中的地址是不同的,而 void allocateInt(int * i)
中的i是區域性變數,在子函式執行結束會被釋放掉,因此 void main()
中的i是無法得到malloc的地址的,更不可能得到新的賦值。
下面通過gdb除錯以及反彙編來進行說明:
- 程式在執行至main函式中的
allocateInt(i, m);
語句時,變數i和m的記憶體地址如下圖所示,&i=0x7fffffffdaf0,&m=0x7fffffffdaec:
- 之後使用命令si對組合語言進行單步除錯,連續執行5次si命令後(主要是保留變數i和m的值),程式進入
allocateInt
函式。進入時,i=0x7ffff7ffe168, m=0,也就是說i和m還並沒有被傳遞賦值,結果如下所示:
但此時,變數i和m的地址是不同的,&i=0x7fffffffdac8,&m=0x7fffffffdac4,如下圖所示:
- 再執行5次彙編指令後,才將引數的完成傳遞賦值,程式的指標才開始指向
void allocateInt(int * i, int m)
中的printf("i address: %x\n", &i);
,如下圖所示:
此時的i和m已經被賦值,i=(int *) 0x7fffffffdaec, m=5。
- 針對在第3點提到的4次彙編指令,這裡進一步說明。
- 第1條指令是
push %rbp
,也就是把rbp暫存器入棧; - 第2條指令是
mov %rsp,%rbp
,其中rsp是堆疊指標。也就是把堆疊指標的值賦值給rbp暫存器; - 第3條指令是
sub $0x10,%rsp
,也就是把堆疊指標所指向的地址減少16個位元組。這是因為變數i和m一共佔用了16個位元組; - 第4條指令是
mov %rdi,-0x8(%rbp)
,也就是把暫存器rdi的值(rdi=0x7fffffffdaec,如下圖所示)賦值給i。因為i的地址就是rbp-0x8; - 第5條指令是
mov %esi,-0xc(%rbp)
,作用類似於第4條,將暫存器esi的值(esi=0x5,如下圖所示)賦值給m。
- 第1條指令是
- 關於暫存器的相關知識、gdb的除錯命令可以參考下面的參考資料;
- 關於彙編指令中出現的
lea
命令可以網上查詢,主要就是一種更加有效的mov方法; - 關於彙編指令中出現的
callq 0x4004a0 <printf@plt>
,意思是呼叫print函式。但是這裡並不是直接呼叫print函式,而是呼叫類似於print函式在程序中的別名。因為這是公用庫中的函式,因此不同程序中都會呼叫,所以只在程序中存留一個函式地址或者別名就好。具體參見stackoverflow上的一篇文章 ofollow,noindex" target="_blank">What does @plt mean here? 。
題外話
- 在編寫時注意區域性性原理,提高效能。一般cache會把某次訪問的記憶體地址附近區域的內容都載入進去。如果在編寫程式時相鄰語句訪問的資料是在記憶體中連續的,那麼就會調高cache的命中率。
- 在編寫時注意分支預測導致的效能問題。在向下跳轉的情況下,優先將最有可能執行的語句放在if分支下,減少分支預測時的開銷(向下跳轉在靜態分支預測中一般預設不跳轉;向上跳轉在靜態分支預測中一般預設跳轉),例如:
int a = -5; int b = 0; ................................................ if(a > 0){if(a <= 0){ b = 1;b = 2; }} else{else{ b = 2;b=1; }}
關於分支預測的一些預測方式可以參考一篇部落格 C++效能榨汁機之分支預測器