C語言程式碼區錯誤
欲想了解C語言程式碼段會有如何錯誤,我們必須首先了解編譯器是如何把C語言文字資訊編譯成為可以執行的機器碼的。????
背景介紹
- 測試使用的C語言程式碼
- 匯入標準庫,定義巨集變數,定義結構體,重新命名結構體,
- 函式原型宣告,主函式入口,函式定義
#include <stdio.h>
#define PI 3.14159
typedef struct student {
char name[8];
int age;
} Student;
void sayHi(Student);
int main() {
// we define a Student structure here.
Student mushroom = {"mushroom", 19};
/*
Output some massage.
*/
sayHi(mushroom);
printf("I know PI equels to %lf", PI);
return 0;
}
void sayHi(Student stu) {
printf("Hi! I am %s.\n", stu.name);
}
- C語言編譯基本流程
- 一共劃分為了檔案輸入,預處理,編譯,彙編,連線,最後輸出為可執行檔案。GCC正好就為我們提供了所有的這些方法,下面來一一介紹下
GCC
內建的處理C語言檔案的指令。 GCC
編譯器基本指令瞭解
指令名 | 解釋 |
---|---|
gcc -c |
編譯或彙編原始檔,但是不作連線.編譯器輸出對應於原始檔的目標檔案. |
gcc -S |
編譯後即停止,不進行彙編.對於每個輸入的非組合語言檔案,輸出檔案是組合語言檔案. |
gcc -E |
預處理後即停止,不進行編譯.預處理後的程式碼送往標準輸出. |
gcc -o filename |
指定輸出檔案為file.該選項不在乎GCC 產生什麼輸出,無論是可執行檔案,目標檔案,彙編檔案還是 預處理後的C程式碼. |
這裡只是給出了gcc
最常見的指令,如果還想了解更多的細節可以檢視下官方文件.這裡給出國內維護的GCC中文手冊
如上的GCC相關指令給出了一整套處理C語言檔案的完整方法,下面使用來完整的按流程編譯下C語言檔案。
預處理階段(preprocessing)
再看一眼
test.c
檔案
使用gcc -E filename > outputFilename
預處理檔案
gcc -E test.c
將資訊輸出到test.i
檔案之中
原本只有二十五行的程式碼,一下子被擴張到了751行
基本資料型別的重新定義
標準輸入輸出函式
外部函式
test.c
程式碼段
這裡說的程式碼段不準確,程式碼段是整個程式碼,這裡是特指
test.c
中手動寫入的程式碼區
預處理後效果分析
- 插入了標頭檔案資訊
- 巨集消失,發生了巨集替換
- 註釋刪除
編譯階段(compilation)
使用gcc -S filename
gcc -S test.i
編譯生成了
test.s
檔案,只是一個彙編程式碼檔案藉助彙編程式碼翻譯為機器碼
彙編程式碼
討論彙編程式碼已經超過了本次討論的內容,這裡附上一篇講解彙編的文章彙編程式碼,裡面正對於計算機記憶體,與CPU的關係闡釋的相當清楚,各位有時間可以看看。
可以從彙編程式碼的一些細節處看出來,這彙編程式碼實際上是原
test.c
向組合語言的翻譯,像C語言這樣計算機程式語言實際上是高階語言,簡單來說是給人看的,語法等等各個方面的都適用於人類去閱讀,但是計算機是無法理解的。計算機只能理解0和1,二進位制, (最開始電子計算機剛剛被發明出來的時候,程式設計師們程式設計就是使用的手動輸入01二進位制指令的方法程式設計的),帶有特定功能的二進位制碼被稱為指令,指令的集合被稱為指令集,(這也正是當前中國被卡脖子的地方)組合語言是機器碼的封裝,每一個彙編指令都對應與一個機器碼,這也是為什麼組合語言被成為低階語言。
梳理下編譯階段
我們不去考察
GCC
是如何巴拉巴拉變出這一堆看不懂的彙編程式碼,但我們應當明白,C語言是想把自己轉化成為彙編碼
然後直接交給CPU,我們可以這麼理解C語言是組合語言的重構封裝,使得,而編譯器就好像是翻譯器。大致理解為如下流程。
彙編階段(assembly)
gcc -c filename
gcc -c test.s
彙編階段是組合語言向機器碼轉化的過程,可以理解為組合語言的編譯過程。
強行讀取一下
雖然基本上是亂碼,但是其中還是有不少的部分如
I know PI equels to %lf
等等字串型別的資料可以被顯示出來。但是此程式還無法交給CPU計算,還缺少最後一步連線(link)
連線階段(link)
gcc filename -o targetname
-o選項實際上可以處理很多種類的中間檔案,如果沒有使用`-o'選項,預設的輸出結果是:可執行檔案為`a.out', `source.suffix '的目標檔案是`source.o',彙編檔案是 `source.s',而預處理後的C原始碼送往標準輸出.
gcc test.o -o test
最終的階段終於得到了真正的目標檔案,完成了一次人機的簡單互動。
但是這個
gcc -o
實際上給出的很曖昧,這個過程中實際上是把我們自己寫好的程式與儲存在硬碟的庫檔案連線,比如說printf()
函式,並沒有定義它,但是卻可以使用,這是由於上述操作與標準庫
建立了連線.
!!!注意!!!,所有的可執行程式必須放在CPU中才可以執行!!****
舉個例子也許會好理解一些:
你們全家人要出去郊遊,你跟媽媽說我要吃大雪糕,媽媽答應給你準備。
你把你的想法傳達給了媽媽,你們一起建立起了一個約定,可以理解為上述彙編階段產生的未連線的可執行檔案
媽媽答應了你的約定,可是,如何去履現和你的承諾呢?
在郊遊地點周圍現買.(假設郊遊地點周圍一定有)
在家裡就準備好,打包一起帶走到郊遊地點.
這匯出了兩種不同的連線方式:
- 靜態連線:一起打包好,我要的我都準備好。(對應於上述的在家裡準備雪糕)
- 動態連結: 我暫時不準備,但是我知道,它雪糕就在那兒,所以等到了之後在動態的把它拿過來執行。
很容易想到動態連結的檔案體積會相對來說少很多,但是可移植性差點。
反思
真正的可執行的程式在經過預處理,編譯,彙編,連線相關庫檔案。最終形成了程式的最終形態,當次檔案執行的時候,立馬從硬碟讀取到了記憶體中,然後立馬放置到CPU進行計算。
如何避免程式碼段出現記憶體錯誤呢?
程式碼段,是存放指令集的,所以我列舉出了以下幾個方面可能造成的錯誤.
1. 阻止C檔案翻譯為彙編程式碼 2. 阻止庫連線,或者庫連線失敗
阻止C檔案翻譯為彙編程式碼:
#include <stdio.h>
int mian() {
printf("Mushroon!!!!\n");
return 0;
}
如上程式碼,故意將函式入口main
寫成mian``int
寫成intt
雖然我們已經盡力的想去迫害C語言的編譯器,可是它還是認真的指出了我們的惡劣行經。
預處理完全沒問題,編譯階段出現了問題,失敗了,超出了C語言編譯器可以理解的符號系統
阻止庫連線
回到上面給出的一家人郊遊你卻想吃雪糕的例子,當你提出吃雪糕的時候,你媽媽給你一大耳巴子,說"吃什麼吃!不給吃!!",這就是阻止了庫的連線。自然無法完成你想要吃雪糕這件事了。
總結
實際上本文沒有得到什麼比較深刻的結論。
目前隨著編譯器越來越完善,各大IDE(整合式程式設計環境)越來越強大,語義分析,自動查詢語法錯誤,還能在不編譯的時候察覺出想C語言編譯器無法查出的問題:如陣列訪問越界。
這些工具的出現使得程式設計時程式碼準確無誤的被翻譯成為機器碼,確保可以執行,讓程式設計師更注重於程式的功能上,是否準確?是否有效?這也是時代的一大進步吧!