鐵馬冰河入夢來——從源文件到可執行文件(待後續)

     假如在Linux系統終端,我們創建了一個.c文件,如:main.c,我們可以通過以下指令來運行它。

                                                                        gcc -o main main.c

      這個時候會增加一個叫做 main 的文件。然後輸入下一條指令:

                                                                        ./main

       該C程序就會運行。

        那麼問題來了:爲什麼main.c文件不能直接運行?從main.c到main這一過程發生了什麼?接下來,就讓我們探究一下。

        首先,我們要了解的是,上述運行過程其實包含了四個步驟,分別是:預編譯(prepressing)、編譯(complication)、彙編(assembly)、鏈接(linking)。接下來我們會對這四個過程進行具體分析。

一、預編譯(prepressing)

 我們首先創建一個C程序文件:love_01.c 其內容爲:

#include<stdio.h>

int main()
{
    int a = 2;
    int b = 5;
    printf("a + b = %d\n", a + b);
    return 0;
}

 然後我們通過以下命令只對其進行預編譯:

                                                  gcc   -o   love_01.i   -E   love_01.c

                                                 

 

這時候我們生成了一個 .i 文件,這就是對C程序文件進行預編譯後生成的文件,那麼接下里我們看看這個文件裏是什麼內容:

                                                    

      我們發現,這個文件裏總共有857行,但只有最後的幾行是我們所寫的代碼,那前面這一大堆是什麼來頭呢?自然地,我們在main函數前面寫的只有一個 #include<stdio.h> 而已,那前面這800多行難道是這個頭文件的展開?

      爲了驗證這一猜想,我們另外創建一個 love_02.c 的C程序文件,其內容爲:

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int a = 2;
    int b = 5;    
    printf("a + b = %d\n", a + b);    
    return 0;
}

           與love_01.c只有一處不同,就是多個#include<stdlib.h>,接下來還是按照老辦法,只對其進行預編譯,並查看生成的love_02.i文件.

                                           

      我們發現,僅僅是加了一個頭文件,預編譯後就從857行增加到了1847行,而我們main函數的程序,還是隻佔了寥寥幾行而已,結果不言而喻。

       接下來,我們再創建一個love_03.c文件,其內容爲:

#include<stdio.h>
#include<stdlib.h>

#define c 0      //I am a student

int main()
{
    int a = 2 + c;
    int b = 5 + c;      //How are you?
    printf("a + b = %d\n", a + b + c);      //I am fine,thank you,and you?
    return 0;
}

      這裏我們是在love_02.c 的基礎上添加了一個宏定義,#define c 0 和一些註釋,這樣預編譯後的文件love_03.i會是什麼樣子呢?如下圖所示:

                             

        我們驚奇地看到,其行數並沒有發生改變,並且我們的宏名c也被替換成了0,註釋也不見了。所以,預編譯的過程中發生了什麼呢?這裏我用《程序員的自我修養》這本書裏第39頁的內容總結:

        預編譯過程主要處理那些源代碼文件中以“#”開始的預編譯指令,其主要處理規則如下:

         1、將所有的“#define”刪除,並且展開所有宏定義

         2、處理所有的條件預編譯指令,如:“#if” “#endif”,“#ifdef”“#elif” “#else”

         3、 處理“#include”預編譯指令,將被包含的文件插入到該預編譯指令的位置。注意,這個過程是遞歸進行的,也就是說被包含的頭文件可能還包含其他文件

         4、 刪除所有的註釋“//”和“/**/”

         5、 添加行號和文件名標識,比如上面的#3 “love_03.c” 2 ,以便於編譯時編譯器產生調試用的行號信息以及用於編譯時產生編譯錯誤或警告時能夠顯示行號

          6、 保留所有的#pragma編譯器指令,因爲編譯器需要使用它們

、編譯(complication)

       還是使用love_01.c這個文件進行說明,剛纔經過預編譯這一過程,我們當前文件夾裏已經有了love_01.c 和 love_01.i 這兩個文件,接下來我們使用這條命令對 love_01.i 進行編譯:

                                                    gcc  -olove_01.s  -S  love_01.i

      然後生成love_01.s這個文件,就是編譯後的結果,讓我們來看一下里面是什麼:

                                                    

                                                    

        我相信如果學過彙編的同學對這些一定不會陌生,因爲這就是彙編代碼!

        所以編譯這一過程,是把預編譯後的文件進行一系列詞法分析、語法分析、語義分析及優化後生成的相應的彙編代碼文件。

、彙編(assembly)

          還是繼續針對love_01.c說明,我們當前有.c .和 .i以及 .s三個文件,接下來我們用以下指令對love_01.s進行彙編

                                               gcc   -o   love_01.o   -c    love_01.s

           生成的是love_01.o文件,讓我們看看裏面是什麼內容吧:

                        

     這裏面的內容讓我們一頭霧水,根本看不懂,這是很正常的,因爲這些就是隻有機器能讀懂的機器指令。

     通過彙編這一過程,將彙編代碼轉變成機器可執行的指令,每一個彙編語句幾乎都對應一條機器指令。

     而經過前面三個步驟:預編譯,編譯,彙編生成的 .o 文件我們叫做目標文件(object file)

     那麼現在,機器能識別這些機器指令了,是不是就能夠運行了呢?很可惜,是不能的。那麼原因是什麼呢?上帝視角的我們自然知道還有一步鏈接的過程沒進行,那麼鏈接到底是什麼?明明機器指令已經有了,爲甚麼計算機還是不能運行這個文件?目標文件和可執行文件的差別到底在哪裏?

       四、鏈接(linking)

        鏈接這裏是十分重要的知識點,首先發我們要知道目標文件裏有什麼,鏈接過程又做了那些事情,這些內容我會在之後的博客中進行詳細補充,此處暫且不表,有興趣的同學可以翻閱《程序員的自我修養》這本書。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1、

2、

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章