文章目錄
- 編譯原理
- 預處理
- 編譯
- 匯編
- 鏈接
- gcc的常用命令參數
- make 和 Makefile 的概念
- make的運行
- 通配符
- 自動化變量
- 偽目標.PHONE:【命令】
編譯原理
在解釋 makefile
前,首先解釋一下 .c
文件變成 .exe
文件要經過的四個步驟——預處理、編譯、匯編和鏈接(參考來源):
windows 系統下最后生成的可執行文件為 .exe
,但 Linux 系統下為 .out
。此處的可執行文件僅針對一般 .c/.cpp
代碼而言。
預處理
預處理分為四步:
- 展開所有的宏定義
#define
- 處理含有
#
部分的代碼。如:- 條件編譯
“#if”、“#ifdef”、“#elif”、“#else”、“#endif”
; - 預編譯指令
#include
,將被包含的頭文件插入到該編譯指令的位置。(這個過程是遞歸進行的,因為被包含的文件可能還包含了其他文件)
- 條件編譯
- 刪除所有的注釋
“//”
和“/* */”
。 - 添加行號和文件名標識,方便后續編譯時 編譯器產生調試用的行號 以及 在產生編譯錯誤或警告時能夠顯示行號。
- 保留所有的
#pragma
編譯指令,因為編譯器需要使用它們。
編譯
編譯過程是整個程序構建的核心部分,編譯成功,會將源代碼由 文本形式轉換成機器語言 ,編譯過程就是把預處理完的文件進行一系列 詞法分析、語法分析、語義分析以及優化后生成相應的匯編代碼文件(.s
)。
-
詞法分析: 使用一種叫做
lex
的程序實現詞法掃描,它會按照用戶之前描述好的詞法規則將輸入的字符串分割成一個個記號。產生的記號一般分為:關鍵字、標識符、字面量(包含數字、字符串等)和特殊符號(運算符、等號等),然后他們放到對應的表中。 -
語法分析: 語法分析器根據用戶給定的語法規則,將詞法分析產生的記號序列進行解析,然后將它們構成一棵語法樹。對于不同的語言,只是其語法規則不一樣。用于語法分析也有一個現成的工具,叫做:yacc。
-
語義分析: 語法分析完成了對表達式語法層面的分析,但是它不了解這個語句是否真正有意義。有的語句在語法上是合法的,但是卻是沒有實際的意義,比如說兩個指針的做乘法運算,這個時候就需要進行語義分析,但是編譯器能分析的語義也只有靜態語義。
- 靜態語義:在編譯期就可以確定的語義。 通常包括聲明與類型的匹配、類型的轉換。比如當一個浮點型的表達式賦值給一個整型的表達式時,其中隱含一個從浮點型到整型的轉換,而語義分析就需要完成這個轉換,而將一個浮點型的表達式賦值給一個指針,這肯定是不行的,語義分析的時候就會發現兩者類型不匹配,編譯器就會報錯。
- 動態語義:只有在運行期才能確定的語義。 比如說兩個整數做除法,語法上沒問題,類型也匹配,聽著好像沒毛病,但是,如果除數是0的話,這就有問題了,而這個問題事先是不知道的,只有在運行的時候才能發現他是有問題的,這就是動態語義。
-
中間代碼生成: 初始代碼是可以進行優化的,對于一些在編譯期間就能確定的值,可以直接直接進行處理,比如說 2+6,在編譯期間就可以確定他的值為8了,但是直接在語法上進行優化的話比較困難,這時優化器會先將語法樹轉成中間代碼。中間代碼一般與目標機器和運行環境無關。(不包含數據的尺寸、變量地址和寄存器的名字等)。中間代碼在不同的編譯器中有著不同的形式,比較常見的有三地址碼和P-代碼。
中間代碼使得編譯器可以分為前端和后端。編譯器前端負責產生于機器無關的中間代碼,編譯器后端將中間代碼換成機器代碼。 -
目標代碼生成與優化: 代碼生成器將中間代碼轉成機器代碼,這個過程是依賴于目標機器的,因為不同的機器有著不同的字長、寄存器、數據類型等。
最后目標代碼優化器對目標代碼進行優化,比如選擇合適的尋址方式、使用唯一來代替乘除法、刪除出多余的指令等。
匯編
匯編過程調用 匯編器 as
來完成,將匯編代碼轉換成機器可以執行的指令,每一個匯編語句幾乎都對應一條機器指令。
使用命令 as hello.s -o hello.o
或者使用 gcc -c hello.s -o hello.o
來執行匯編,對應生成的文件是 .o
文件。
鏈接
鏈接的主要內容就是將各個模塊之間相互引用的部分正確的銜接起來。它的工作就是把一些指令對其他符號地址的引用加以修正。
鏈接過程主要包括了地址和空間分配、符號決議和重定向:
-
符號決議: 有時候也被叫做符號綁定、名稱綁定、名稱決議、或者地址綁定,其實就是指用符號來去標識一個地址。
比如說 int a = 6;這樣一句代碼,用a來標識一個塊4個字節大小的空間,空間里邊存放的內容就是4.
-
重定位: 重新計算各個目標的地址過程叫做重定位。
鏈接有兩種模式:
-
靜態鏈接: 程序運行前,將每個模塊的源代碼文件編譯成目標文件
(Linux:.o Windows:.obj)
,然后將 目標文件 和 庫 一起鏈接形成最后的可執行文件。庫其實就是一組目標文件的包,就是一些最常用的代碼變異成目標文件后打包存放。最常見的庫就是運行時庫,它是支持程序運行的基本函數的集合。
-
動態鏈接: 程序運行期間,系統調用動態鏈接器(
ld-linux.so
)自動鏈接的過程。
舉例描述:
- 靜態鏈接: 如果鏈接到可執行文件中的是
靜態連接庫 libmyprintf.a
,那么虛擬內存代碼段中的 .rodata 節區
在鏈接后需要被重定位到一個絕對的虛擬內存地址,以便程序運行時能夠正確訪問該節區中的字符串信息。 - 動態鏈接: 而對于puts,因為它是動態連接庫
libc.so
中定義的函數,所以會在程序運行時通過動態符號鏈接
找出puts 函數
在內存
中的地址,以便程序調用該函數。
gcc的常用命令參數
上面提到的四個步驟可以由 編程語言譯器 gcc
來完成,gcc軟件
通過 gcc這條命令
來實現各種功能,下面來看一下 gcc命令
的常用選項:
- 無選項: 編譯鏈接
gcc test.c // 會默認生成a.out可執行程序
- -o :對生成的目標進行重命名,
gcc
編譯出來的默認文件名是a.out
。
gcc test.c -o test // 會生成名字是test可執行文件而不是默認的a.out
- -E :進行預處理,不生成文件, 需要通過
-o
把它重定向到一個輸出文件里面。
gcc -E test.c -o test.i //會生成test.i文件
- -C :在預處理的時候不刪除注釋信息,一般和
-E
使用。 - -S :進行預處理、編譯,生成
.s
文件
gcc -S test.c //會生成test.s文件
- -c :進行預處理、編譯、和匯編,生成二進制(機器指令)
.o
文件。
gcc -c test.c //會生成test.o文件
- -O :使用編譯優化級別1編譯程序。級別為0~3(0即無優化),級別越大優化效果越好,但編譯時間越長。
gcc -O1 test.c -o test
- -g :在編譯的時候加入
debug
調試信息,用于gdb
調試 - -pipe :使用管道代替編譯中的臨時文件。
gcc -pipe -o test test.c
- -include file :包含某個代碼。相當于在文件中加入
#include<file>
gcc test.c -include /root/file.h
-
-Idir :當你使用
#include”file”
的時候:如果使用
-I
指定了目錄,gcc/g++
會先在指定的目錄查找;否則,在當前目錄查找指定的頭文件。如果沒有找到,回到默認的頭文件目錄查找。
-
-idirafter dir :在
-I
的目錄里面查找失敗,則到這個目錄里面查找。 -
-llibrary :定制編譯的時候使用的庫。
gcc -lpthread test.c // 在編譯的時候要依賴pthread這個庫
-
-Ldir :指定編譯的時候搜索庫的路徑。如果有自己的庫,可以用它來定制搜索目錄,否則編譯器只在標準庫目錄里面找。
dir
是目錄的名字。 -
-M :生成文件關聯信息。包含目標文件所依賴的所有源代碼。
gcc -M hello.c
- -MM :和
-M
一樣,只不過忽略由#include
所造成的依賴關系。 - -MD :和
-M
相同,只不過將輸出導入到.d
文件里面。 - -MMD :和
-MM
相同,將輸出導入到.d
文件里面。 - -static :鏈接時使用靜態鏈接,但是要保證系統中有靜態庫。編譯出來的東西,一般都很大。
- -share :此選項盡量的使用動態庫,所以生成文件比較小,但是必須是系統有動態庫。
- -shared :生成共享目標文件,通常用在建立共享庫。
gcc -shared test.c -o libtest.so // 編譯動態庫
- -w :不生成任何警告信息。
- -Wall :生成所有警告信息。
make 和 Makefile 的概念
推薦一個非常全的關于 Makefile
的文章:跟我一起學寫 Makefile
在我們日常寫代碼中,一個工程的源文件不計其數, 按照類型、功能、模塊等分別放在若干個目錄中,這時候我們就可以利用 Makefile
來指定哪些文件先編譯,哪些后編譯,以及更復雜的操作。
make
是一個命令工具,它解釋 Makefile
中的指令。我們只需要在 Makefile
里指定所有的操作,再用 make
這個操作,即可讓整個工程自動編譯。
makefile
的格式如下:
target : prerequisitescommand
- target: 目標文件 ,可以是多個文件,以空格分開,可以使用通配符。可以是
Object File
或執行文件
。甚至還可以是一個標簽(Label
),如:clean
。 - prerequisites:
target
的 依賴對象 。如果其中的 某個文件 要比 目標文件 要新,那么,目標文件 就被認為是 過時的 ,需要重新生成。 - command: 命令行 ,如果其不與
target:prerequisites
在一行,那么,必須以[Tab鍵]
開頭,如果在一行,那么可以用分號做為分隔。
一般來說,make會以UNIX的標準Shell,也就是/bin/sh來執行命令。
寫一個 makefile
文件為例:
目標程序:
執行 make 指令:
這樣就生成了 .i,
、.s
、.o
、.out
文件。那么 make
是怎么運行的呢?
make的運行
- 在當前目錄下依次找三個文件——
GNUmakefile
、makefile
和Makefile
。其按順序找這三個文件,一旦找到,就開始讀取這個文件并執行。
也可以給
make 命令
指定一個 特殊名字 的Makefile
。這需要使用make
的-f
或是--file
參數(--makefile
參數也行)。例如,我們有個Makefile
的名字是hchen.mk
,則可以這樣執行make
命令:
make –f hchen.mk
如果在 make
的命令行中,不只一次地使用了 -f
參數,那么,所有指定的 Makefile
將會被連在一起傳遞給 make
執行。
- 接下來,它會找文件中的第一個
target
(上面例子中的test
),并把這個目標文件作為最終生成的文件。 - 如果
test
文件尚未生成;或是雖然test
已經生成,但后面的依賴對象test.o
文件的最后修改時間要比test
這個文件新(可以用命令touch
測試),那么,make
就會重新生成test
這個文件。 - 如果
test
所依賴的test.o
文件不存在,那么make
會在當前文件中找目標文件為test.o
的規則,如果找到則再根據那一個規則生成test.o
文件。 - 如果沒有目標文件為
test.o
的規則,則提前退出;否則,生成test
文件并退出。
這就是整個 make
的運行過程,make
會一層又一層地去找文件的依賴關系,直到:
- 最終編譯出第一個目標文件(默認目標)并返回退出碼;
- 或者因為缺少必要規則而直接返回退出碼。
make命令執行后有三個退出碼:
0
:表示成功執行。1
:如果make
運行時出現任何錯誤,返回1
。2
:如果你使用了make
的-q
參數,導致一些目標不需要更新,那么返回2
。
而對于所定義的命令的錯誤,或是編譯不成功,make根本不理。
通配符
可以通過通配符來簡化命令行:
~
:Unix下,~/test
表示當前用戶的$HOME
目錄下的test
目錄。而~hchen/test
則表示用戶hchen
的宿主目錄下的test
目錄。而在Windows
或是MS-DOS下
,用戶沒有宿主目錄 ,那么波浪號所指的目錄則根據環境變量HOME
而定。(make支持UNIX下的通配符用法)*
:表示任意長度的字符串,*.c
表示所有后綴為c的文件。而當文件名中有通配符,如:~
,那么可以用轉義字符\
,如\~
來表示真實的~
字符。?
:表示任意一個字符串。
自動化變量
shell 中的 自動化變量
(又名:特殊變量
) ,make 也是支持的,經常用到下面前三個自動化變量 :
$@
:目標對象 。在模式規則中,如果有多個目標,那么,$@
就是匹配于目標中模式定義的集合。$^
:所有 依賴對象 ,以空格分隔。如果在依賴目標中有多個重復的,那么這個變量會去除重復的依賴目標,只保留一份。$<
:所有 依賴對象 的 第一個 。如果依賴目標是以 模式(即%
)定義的,那么$<
將是符合模式的一系列的文件集。注意,其是一個一個取出來的。$?
:所有比 目標對象新
的 依賴對象 的集合。以空格分隔。$+
: 這個變量很像$^
,也是所有 依賴對象 的集合。只是它不去重。$%
:僅當 目標對象 是函數庫文件中、表示規則中的目標成員名。例如,如果一個目標是foo.a(bar.o)
,那么,$%
就是bar.o
,$@
就是foo.a
。如果目標不是函數庫文件(Unix下是.a
,Windows下是.lib
),那么,其值為空。$*
:這個變量表示目標模式中%
及其之前的部分。(如果 目標對象 是dir/a.foo.b
,并且 目標對象 的 模式 是a.%.b
,那么,$*
的值就是dir/a.foo
。)- 這個變量對于構造有關聯的文件名是比較有用的。(如果 目標對象 中沒有 模式 的定義,那么
$*
也就不能被推導出,但是,如果 目標文件 的后綴是make
所識別的,那么$*
就是除了后綴的那一部分。)
- 這個變量對于構造有關聯的文件名是比較有用的。(如果 目標對象 中沒有 模式 的定義,那么
例如:如果 目標對象 是
foo.c
,因為.c
是make
所能識別的后綴名,所以,$*
的值就是foo
。這個特性是GNU make
的,很有可能不兼容于其它版本的make
,所以,盡量避免使用$*
,除非是在 隱含規則 或是 靜態模式 中。如果 目標對象 中的后綴是make
所不能識別的,那么$*
就是空值。
我們可以利用 自動化變量 簡化 makefile
文件:
執行 make 命令:
我們還能進一步再簡化,可以利用通配符來表示,在多個 目標對象 的 依賴對象 和 命令行 都相似時,利用通配符 %
來減少工作量,這樣就可以不用一個個寫出每個文件的生成規則了。
偽目標.PHONE:【命令】
.PHONE: [命令] // 聲明偽目標,無論目標是否最新,每次都重新生成。
舉個 偽目標 的例子:
clean:rm *.o temp
既然我們生成了許多編譯文件,那么我們也應該提供一個清除它們的 目標 以備完整地重編譯。 (以“make clean”來使用該目標)
之所以將 clean
稱為 偽目標 , 是因為我們并不生成 clean
這個文件。偽目標 并不是一個 文件 ,只是一個 標簽 ,由于 偽目標 不是 文件 ,所以 make
無法生成它的 依賴對象 ,無法決定它是否要執行 命令行 。我們只有顯式地指明這個 目標 才能讓其生效。當然,偽目標 的取名不能和 文件名 重名,不然其就失去了 偽目標 的意義了。
因此我們需要用 .PHONY
聲明 偽目標 ,從而區分 偽目標 和 目標文件 。
.PHONY : clean
而只要有 .PHONY:clean
這個聲明,不管是否有 clean
文件,只要執行 make clean
命令,就會運行 clean
。因此,我們要在聲明后面跟上 clean
的具體內容:
.PHONY : clean
clean :rm *.o temp
通常需要生成的程序不會設置偽對象,因為每個項目的構建需要很長的時間,所以盡可能判斷不需要生成就不用重新生成。
偽目標一般沒有依賴的文件。但是,我們也可以為偽目標指定所依賴的文件。
一個示例就是,如果你的 Makefile
需要一口氣生成若干個可執行文件,但你只想簡單地敲一個 make
完事,并且,所有的目標文件都寫在一個 Makefile
中,那么你可以這樣做:
all : prog1 prog2 prog3
.PHONY : allprog1 : prog1.o utils.occ -o prog1 prog1.o utils.oprog2 : prog2.occ -o prog2 prog2.oprog3 : prog3.o sort.o utils.occ -o prog3 prog3.o sort.o utils.o
Makefile
中的第一個目標會被作為其默認目標。 我們聲明了一個 all
的偽目標,其依賴于其它三個目標。由于 默認目標總是被執行的 ,而上面的 Makefile
文件中的第一個目標(默認目標) all
又是一個偽目標。因此 all 是一定會被執行的,但又因為偽目標只是一個標簽不會生成文件,所以不會有 all
文件產生。于是,其它三個目標的規則總是會被執行。也就達到了我們一口氣生成多個目標的目的。 .PHONY : all
聲明 all
這個目標為 偽目標 。(注:這里的顯式 .PHONY : all
不寫的話一般情況也可以正確的執行,這樣 make
可通過隱式規則推導出, all
是一個偽目標,執行 make
不會生成 all
文件,而是執行后面的多個目標。建議:顯式寫出是一個好習慣。)