July 17, 2024

Program to Process

程式碼撰寫到運行

隨手學習筆記,本筆記主要講述從 Program 到 Process 的過程

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

圖源

預處理(Preprocessor)


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

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

讓我們看實際的過程

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

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

gcc還有很多參數可以使用,在網路上都可以輕易的查詢,後面也還會再介紹幾個

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

//上略
...
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> 確實都被取代了。

這邊置換成我們想要的結果後,卻多出了一些奇怪的數字那這些數字是什麼呢?

The numbers following the filename are flags:

Source

大致理解預處理器到底在幹嘛了,可能心中又會有新的疑問

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

關於這個問題先來了解標頭檔的意義

標頭檔的意義

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

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

動態連結

靜態連結

first_plt_got second_got_plt

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

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

其實 Program 填寫正確函數位置要等到連結 (Linking) 時才會真正知道函數正確的位置 ,編譯器其實並不知道外部函數、變數的位置,一切都要靠動態連結器完成。

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

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

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

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


編譯(Compiler)

考慮以下程式碼

1
array[index]= (index + 4) * (2 + 6)    

他會被分析成以下這些 token

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

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

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

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

image

組譯(Assembly)

連結(Linking)

連結器(Linker)

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

image

image

連結器又分動態連結器以及靜態連結器?

載入


參考資訊