Try   HackMD

程式碼撰寫到運行

隨手學習筆記,還沒寫完,希望能比獵人還早寫完

本筆記主要講述從 Program 到 Process 的過程

理解預處理、編譯、組譯、連結、載入、運行

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

預處理(Preprocessor)

//test.c #include<stdio.h> #define it5 5 int main(void){ printf("hello world"); printf("%d",it5); }

這是大家一定都看得懂的程式碼,那我們的預處理器會對這段程式碼如何呢?它會把所有巨集(Macro)、標頭檔(其實就是所有#開頭的指令),都置換掉,置換是什麼意思?

讓我們看實際的過程

gcc -E -o test.i test.c //gcc -E(only preprocesee) -o(assign output filename) [output filename] [input filename]

gcc 是一套強大開源的Compiler Driver,支援數種語言,在這邊我們可以把它看成一套工具來幫助我們做預處理、編譯、組譯、連結就可以了

  • gcc 後面的-E參數代表只要把source code進行預處理就可以了
  • -o 是指定輸出檔案的名稱,這邊我指定名稱是test.i

*.i是經過預處理後的程式碼的副檔名

  • gcc還有很多參數可以使用,在網路上都可以輕易的查詢,後面也還會再介紹幾個
//上略 ... extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__)); # 840 "/usr/include/stdio.h" 3 4 extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); # 858 "/usr/include/stdio.h" 3 4 extern int __uflow (FILE *); extern int __overflow (FILE *, int); # 873 "/usr/include/stdio.h" 3 4 # 2 "test.c" 2 # 2 "test.c" int main(void){ printf("Hello,world"); printf("%d",5); return 0; }

礙於篇幅關係我不把全部的內容放上來,有興趣的讀者可自己根據上面的指令進行實做。

這邊我們可以看到#define it5 5#inculde<stdio.h>確實都被取代了。

這邊置換成我們想要的結果後,卻多出了一些奇怪的數字那這些數字是什麼呢?
Ans:
The numbers following the filename are flags:

  1. This indicates the start of a new file.

  2. This indicates returning to a file (after having included another file).

  3. This indicates that the following text comes from a system header file, so certain warnings should be suppressed.

  4. This indicates that the following text should be treated as being wrapped in an implicit extern "C" block.

Source:https://stackoverflow.com/questions/33089168/what-do-the-numbers-mean-in-the-preprocessed-i-files-when-compiling-c-with-gcc

說了那麼多,我們也大致理解預處理器到底在幹嘛了,那可能心中又會有新的疑問

欸,為什麼不直接 Inculde source code 就好,為什麼要創造一個 HeaderFile,然後 Include 所謂的 HeaderFile,這樣感覺有點畫蛇添足欸

關於這個問題我們先來談談標頭檔的意義

標頭檔的意義

就算你 include 標頭檔,Compiler 在編譯時根本不知道你函數正確的位置 (使用動態連結時)

這邊先打個岔,程式分成動態連結靜態連結,如果是靜態連結那就會在連結的時候把正確位置填入,而動態連結則是運行時填入。
所以下面的討論都是基於動態連結,畢竟靜態連結沒啥好說的,就是直接塞入正確位置

動態連結

  • 運行時載入正確位置透過GOT、PLT
  • todo:這邊我有另一篇在講GOT PLT (目前並沒有,不過我做了圖,有興趣的可以用 gdb trace 配我的圖去理解)
  • Lazy binding(這個會有got hijack的問題)所以目前gcc需要下額外參數才會lazy binding,不然都是在運行的一開始就解析好所有符號

靜態連結

  • 連結時填入固定位置

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

沒錯,以範例來說你 include 標頭檔是為了printf()這個函數,但其實我們去<stdio.h>裡面查看其實他只有對printf()進行宣告而已,內部功能其實並沒有在這個檔案裡面,HeaderFile作為編譯前會先被解析的部份,它(HeadFile)作為宣告的集合,是為了讓 Compiler 能認得函數的定義,知道有其函數,Compiler 才會乖乖編譯

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

那這樣為什麼他能夠置卻進 printf(),是誰告訴程式函數在哪裡?又是什麼時候告訴程式的?

其實 Program 填寫正確函數位置要等到連結 (Linking) 時才會真正知道函數正確的位置
這部分留著我們後面談到連結時再來詳細介紹,這邊姑且知道編譯器其實並不知道外部函數、變數的位置,一切都要靠比較陌生的動態連結器完成。

這邊再補充一下,其實編譯器是會對 printf() 進行編譯,一般來說呼叫函式的組語是長這樣 call [function address],只是 function address 不是填入該函數的正確位置,而是一些與組語相關的數值(就是GOT、PLT的位置),連結器則會根據數值和 Relocation table 依序填入函數或變數的正確位置。

好啊,既然連結器那麼厲害能夠知道函數和變數的位置,那回到一開始的問題,一樣都是宣告,比起 HeaderFile 我的.c檔還有定義函數行為,為什麼不 include<xxx.c> ,這樣不是更直觀更方便嗎?

如果我們直接include<xxx.c>,那這樣我們當初就沒有分成兩個檔案的必要,我們需要include的原因其一就是希望能讓檔案分離、模組化,如果直接include<xxx.c>確實可以,但這樣就失去include的意義了

延伸閱讀:標頭檔為一個完整的檔案做為插入.c/.cpp檔案中,一般標頭檔的功能為Declaration(宣告),而.c/.cpp作為Defined(定義),此做法可以加快編譯速度也可以避免重複宣告,只要include就可以使用。
Why does C++ need a separate header file?
How to use


編譯(Compiler)

  • 詞法分析(lexical analyze)
    • 經過前處理器處理完的檔案進到lexer時,他會對檔案分割成一系列的token,他需要確保檔案在進入下一個stage(Grammer Parser)時,檔案內部的變數保留字都應該符合規範,實際作法可以使用正則表達式(Regular Expression)去做檢查,常用工具:lex flex
      考慮以下程式碼
array[index]= (index + 4) * (2 + 6)    

他會被分析成這樣

token 類型
array 標識符
[ 左中括號
index 標識符
] 右中括號
= 賦值
( 左小括號
index 標識符
+ 加號
4 數字
) 右小括號
* 乘號
( 左小括號
2 數字
+ 加號
6 數字
) 右小括號
  • 語法分析(Grammer Parser)

從上一個stage接收分析好的記號,並且進行語法分析,產生語法樹(Syntax Tree),分析過程使用上下文無關語法(Context-free Grammer)的分析手段。產生的語法樹是由表達式(Expression) 為節點的樹。

可以看到整個語句被看成賦值表達式(assign expression),賦值=的左邊是一個數組表達式,右邊是一個乘法表達式,數組表達式又是由兩個符號表達式所組成的,符號和數字是最小的表達式,他們通常存在在整顆樹的(Leaf Node),另外有些符號有多重含義譬如說 c 語言中的 *,有乘法以及取值(refence)的操作,那就需要在這個階段去分類好。

然後這個也有一個工具叫做yacc(yet another compiler compiler)

image

  • 語意分析(Semantic Analyze)

    • void funciton {return 2}
  • 產生中間代碼(Generate Middle Code)

    • IR
    • 多平台開發 參見 LLVM
    • 消除語法糖之類的東西
      • 方便最佳化
  • 最佳化代碼

好懶,隨便先寫一點

組譯(Assembly)

  • 應該沒什麼好說的,可以一一對應,把組合語言轉換成機器看得懂的機器碼

連結(Linking)

連結器(Linker)

關於 Linker 最主要的功能就是把不同的 .o file 連結在一起,並且設定連結靜態庫 & 動態庫

image

  • Static Linking
    • 把 program 與 靜態連結庫整合在一起,變成一個可執行的 Binary File (Binary file include Library)
  • Dynamic Linking
    • 把 program 與 動態連結庫整合在一起,變成一個可執行的 Binary File (Binary file not include Library)
  • Dynamic Linking? Dynamic Loading?

image

載入


參考資訊

for(int i =0;i<100;i++){ int j =2; }
for(int i =0;i<100;i++){ } int j =2;