學(xué)習(xí)一門語言,經(jīng)常都是從打印“Hello孵坚,World”開始的,打過招呼后窥淆,你便可以進入程序的新世界卖宠。
就拿經(jīng)典的C語言舉例,基本上每個程序員在上學(xué)時就可以閉著眼睛寫下“Hello忧饭,World”扛伍,這也是檢測開發(fā)環(huán)境是否能正常工作常用的小程序,就像有的人看能不能上網(wǎng)就輸個百度試試(手動斜眼词裤,程序員應(yīng)該用谷歌).
//hello.c
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
我們使用gcc
編譯并運行該文件:
$ gcc hello.c -o hello
$ ./hello
輸出結(jié)果:
其實刺洒,輸出一行字符并沒有那么簡單,gcc
幫我們處理了很多步吼砂,如果你用Visual Studio
逆航,運行按鈕更是連編譯指令都不用敲了,IDE是簡化了很多步驟渔肩,但是深入探索背后的步驟是每個程序員必備的素養(yǎng)因俐,更何況很多成熟的大型項目都是需要自己構(gòu)建(Build)。
上述過程可以分為4個步驟:
- 預(yù)處理 (Prepressing)
- 編譯 (Compilation)
- 匯編 (Assembly)
- 鏈接 (Linking)
下面我們詳述這一過程:
預(yù)處理##
預(yù)處理器cpp將hello.c
及包含的頭文件周偎,這里就是stdio.h
預(yù)編譯成為一個hello.i
的文件抹剩。
我們可以用以下命令只對hello.c
進行預(yù)處理:
$ gcc -E hello.c -o hello.i
或者:
$ cpp hello.c > hello.i
你沒看錯,預(yù)處理器就是cpp
蓉坎,與C++擴展名.cpp
沒有關(guān)系澳眷,具體可以man cpp
查看手冊,其實gcc
只是把預(yù)編譯器蛉艾,編譯器钳踊,匯編器,鏈接器這一系列工具集成在一起伺通,通過不同的參數(shù)去調(diào)用不同的部分或者全部調(diào)用
預(yù)處理做的工作:
- 將所有的
#define
刪除箍土, 并且展開所有的宏定義,像#define MAX 1024
,那么代碼文件中所有的MAX
都會被1024
代替罐监。 - 處理所有的條件預(yù)編譯指令吴藻,包括
#if
、#ifdef
弓柱、#elif
沟堡、#else
侧但、#endif
。至于這些指令到底干嘛的航罗,任何一本C語言教材都會有明確的解釋禀横。 - 處理
#include
指令,將所有頭文件插入到預(yù)編譯指令的位置,這一過程是遞歸進行的粥血,也就是說丰榴,頭文件里包含的頭文件也會被插入頭文件里。良好的代碼規(guī)范都指導(dǎo)我們使用頭文件保護森渐,避免重復(fù)包含頭文件椅文。 - 刪除所有注釋
//
和/* ··· */
。注釋給人看的缔御,機器不需要看注釋抬闷。 - 添加行號和文件名標(biāo)識,比如打開剛剛的
hello.i
,int main()
之前插入了一句# 2 "hello.c" 2
耕突,以便于編譯器產(chǎn)生調(diào)試用的行號信息笤成,這樣產(chǎn)生編譯錯誤或警告時,編譯器就會給出文件名和行號眷茁。
-保留所有的#pragma
指令炕泳,編譯器會使用它們。
經(jīng)過預(yù)處理后蔼卡,文件中所有的宏被展開喊崖,包含的文件也被插入,這時候就可以給編譯器使用雇逞。
編譯##
編譯過程是整個程序構(gòu)建的核心部分荤懂,包含了大量編譯原理的知識,注明的參考書有龍書塘砸。
編譯過程可以分為以下幾個部分节仿,每個部分深究起來都很耗費功夫,有機會可以自己實現(xiàn):
- 詞法分析
- 語法分析
- 語義分析
- 中間語言生成與優(yōu)化
現(xiàn)在版本的gcc
把預(yù)處理和編譯兩個步驟合二為一掉蔬,使用一個叫cc1
的程序完成這兩個步驟廊宪,在我的計算機里位于“/usr/lib/gcc/i686-linux-gnu/4.8/cc1”。
我們可以通過以下命令生成編譯后的文件:
$ gcc -S hello.c -o hello.s
也可以直接使用cc1
:
$ /usr/lib/gcc/i686-linux-gnu/4.8/cc1 hello.c
編譯后生成匯編文件hello.s
匯編##
匯編器就是將匯編代碼轉(zhuǎn)變成機器可以執(zhí)行的指令女轿,每一條匯編語句幾乎都對應(yīng)一條機器指令箭启。所以匯編器相對簡單,只需要一一翻譯就可以蛉迹。
我們使用匯編器as
完成如上工作:
$ gcc -c hello.s -o hello.o
或者
$ as hello.s - o hello.o
也可以直接從hello.c
直接得到目標(biāo)文件:
$ gcc -c hello.c -o hello.o
鏈接##
據(jù)說鏈接器的歷史比編譯器還長傅寡,像我們的“Hello,World”程序,生成的hello.o
中包含了printf
函數(shù)荐操,頭文件只包含了函數(shù)的申明芜抒,所以最后還需要鏈接到libc.a
,其實需要鏈接的不僅僅是printf
托启,我們用鏈接器ld
鏈接以下這么多模塊才能生成最終的可執(zhí)行文件宅倒。
$ ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i686-linux-gnu/4.8/crtbeginT.o
-L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_rh -lc --end-group
/usr/lib/gcc/i686-linux-gnu/4.8/crtend.o /usr/lib/crtn.o
一個再復(fù)雜的軟件也是如此,將源代碼分別獨立編譯屯耸,再組裝起來拐迁,這個過程就叫做鏈接,鏈接的主要目的疗绣,一個是將模塊間依賴的函數(shù)調(diào)用打通唠亚,還有就是模塊間共通的變量打通。
鏈接器所做的工作主要就是“調(diào)整地址”持痰,寫匯編代碼時,有這么一句jmp foo
,其實鏈接器就幫我們把foo
翻譯成運行時的地址祟蚀。
鏈接的主要過程:
- 地址和空間分配 (Address and Storage Allocation)
- 符號決議 (Symbol Resolution)
- 重定位 (Relocation)
舉個例子工窍,可以很清楚的解釋這個過程,我們在main.c
調(diào)用了另外一個文件func.c
中的函數(shù)test()
,那么當(dāng)我們在main.c
中每使用一次test()
都必須知道test()
的地址前酿,但文件都是單獨編譯的患雏,所以我們在main.c
中的做法是暫時擱置test()
的地址,當(dāng)鏈接的時候罢维,鏈接器會根據(jù)test
符號淹仑,自動填入test()
的地址,如果func.c
重新編譯了肺孵,test()
地址會變化匀借,但是編譯時,沒有改變的main.c
并不會編譯了平窘,只是在鏈接時吓肋,會鏈接新的test()
的地址。這個修正的過程也叫作重定位瑰艘。
鏈接還分為靜態(tài)鏈接和動態(tài)鏈接是鬼,這個以后會專門說。
如果覺得還不錯紫新,請點個贊吧~