Makefile 簡介
makefile文件最常用的作用是,告訴make程序,如何來編譯以及連接程序,最終生成可執行的二進制文件。
Makefile 最基礎的規則格式如下:
Target … : Prerequisites …
Recipe
…
…
Target 是目標文件,通常就是程序編譯最終所要產生的文件,比如二進制可執行文件,或者是obj文件
Prerequisite是先決條件,也就是生成目標文件所需的輸入文件,一個目標文件通常依賴于多個文件
Recipe是執行命令。可以有多個執行命令,可以在同一行,也可以分成多行。特別要注意的是,每個Recipe之前都要加Tab
通常Recipe中會包含Prerequisite,并且任意一個Prerequisite文件有更改時,都會重新生成Target文件
生成目標hex文件的流程
makefile最廣泛的應用就是把一堆代碼文件編譯成二進制的可執行文件,比如hex文件。
首先把所有的.c文件以及其包含的.h文件,分別都編譯為.o文件。
然后把所有生成的.o文件與link文件一起,生成.elf文件和.map文件
最后由.elf文件生成.hex文件
循序漸進的例子
1. 首先準備好編譯器和demo代碼
可以在hightec的官網上下載hightec的評估版軟件
然后在HighTec中新建一個HelloSerial的Demo例子
創建完就會自動生成如下的.c和.h文件,這個就可以直接在Hightec中編譯了。
2. 最最直接的makefile
為了說明makefile是如何工作的,先從最簡單的情況開始說明。
首先為了消除路徑的影響,先把所有的.c和.h文件還有link文件(.ld文件)都放到同一個文件夾內,然后新建一個名叫makefile的文件
下面我們看一下最最最直接的makefile長什么樣,相信在任何一個項目中,都不會有人這么寫makefile的,暫且叫他版本0
紅色的就是Target,所謂目標文件。比如我們在文件夾路徑下的命令窗口輸入MakeAll
就是告訴make命令,想要生成all這個目標,然后all這個目標又依賴于HelloSerial_Demo.elf 這個Prerequisite 也就是先決條件
于是就要生成HelloSerial_Demo.elf,這時,HelloSerial_Demo.elf就變成了目標文件。
然后為了要生成HelloSerial_Demo.elf這個目標文件,就需要hello.o,system_tc27x.o,uart_init_poll.o,uart_poll.o,usr_sprintf.o這些Prerequisite
于是這些.o文件又變成了目標文件,需要對應的.c文件和其中包含的.h文件來生成
比如要生成hello.o 這個文件,就需要hello.c,led.huart_poll.h,system_tc2x.h,usr_sprintf.h 這些文件,由于這些文件已經在目錄下面了,所以就可以直接用了。
然后還需要Recipe,也就是執行命令來生成目標文件,這邊的執行命令就是"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc"-c hello.c
其中"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc"是調用HighTec安裝的編譯器,tricore-gcc.exe
這個不是固定的,我們需要用哪個編譯器來編譯.c文件,那么這邊就調用哪個編譯器的.exe文件。
-c 表示編譯源文件,但不進行link。這條指令就是告訴tricore-gcc.exe把hello.c這個文件編譯成hello.o
類似的,把所有的.c文件都編譯為.o文件后,就會進行link的操作,生成HelloSerial_Demo.elf這個文件。具體的執行命令如下:
"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc" \\
-o "HelloSerial_Demo.elf" \\
-T"iROM.ld" \\
hello.o system_tc27x.o uart_init_poll.o uart_poll.ousr_sprintf.o \\
-Wl,--gc-sections -mcpu=tc27xx \\
-Wl,--no-warn-flags \\
-Wl,-Map="HelloSerial_Demo.map"
其中
"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc" 是調用編譯器
-o "HelloSerial_Demo.elf" 是指定輸出文件的名字叫"HelloSerial_Demo.elf"
-T"iROM.ld" 是指定link文件
hello.o system_tc27x.o uart_init_poll.o uart_poll.o usr_sprintf.o 這些是要被link的.o文件
-Wl 由于這邊是通過tricore-gcc 編譯器驅動鏈接的指令,所以對鏈接的指令需要加前綴'-Wl',
--gc-sections -mcpu=tc27xx 表示section的分配是按照 tc27xx 這個系列的cpu來執行的
--no-warn-flags 表示不會產生警告信息
-Map="HelloSerial_Demo.map" 表示會生成一個map文件
""是換行符
3. makefile中的變量
在版本0中的makefile中,只用了makefile最基本的規則,這樣的makefile具體做什么,看著是挺清楚的,但寫起來很麻煩。
makefile中可以使用變量,這樣,會看起來簡單一些
比如對于版本0中的makefile,需要用到的.o文件定義了多次,編譯器的路徑也定義了多次,那我們就可以把這些做成一個變量,使用的時候就可以直接使用了。
定義變量的方法是:<變量名> = <內容>
使用變量的方法時:$(變量名)
我們把所有的.o文件對象和編譯器路徑定義為變量,得到如下的makefile版本1
4. <-I "dir">指令
對于版本1的makefile還有個問題,就是每次要編譯.c文件為.o文件時,需要把這個.c文件所用的.h文件,以及.h文件里用到的.h文件都要寫出來。
這樣寫當然也有好處,就是任何.h文件有變化時,包含這個.h文件的.c文件也會重新編譯一個新的.o文件。
但是如果有成百上千個.c文件時,把所有.c文件用到的.h文件都找出來,這個工作量也是巨大的。
所以可以用一種妥協的方法,就是無論編譯哪個.c文件時,把所有的.h文件都包含進來,這樣就不用管這個.c文件到底用到了哪些.h文件。
想要實現這點,那就可以在編譯.c文件時,增加<-I "dir">這個指令
于是我們可以得到版本2的makefile,這樣看起來又簡單了一些。不過這種方法會有一個問題,就是.h文件變更時,不會導致對應的.c文件重新編譯
這個問題其實也是可以解決的,后面引入依賴文件時,就可以 解決這個問題
相比于之前的makefile,這里生成.o文件時的先決條件中就沒有.h文件了,而是在執行命令中添加了包含.h文件夾的參數。
- vpath指令
可能大家也發現,現在每個.c文件的編譯命令都一樣了,只有文件名不一樣,這個是不是可以優化呢,當然是可以的
vpath指令可以在指定的目錄下面找特定的文件,比如我要找在某個文件夾下面所有的.c文件,指令如下:
vpath %.c E:/c10_Workspace/complier_demo
這就表示需要的時候,make就會在E:/c10_Workspace/complier_demo找,看有沒有make需要的.c文件,%.c 表示所有后綴名為.c的文件
利用vpath指令,我們就可以繼續簡化makefile,得到版本2的makefile。
這里把.c編譯為.o的指令合并為一個指令了。所有需要的.o文件都可以在vpath指定的路徑中找到對應的.c文件(這里是make的一個默認規則,.o和.c文件的名稱是一致的),然后進行編譯
- 生成依賴文件 (.d)
我們可以讓編譯器在編譯.c文件的時候,同時生成一個.d的依賴文件,在依賴文件中,會列出這個.c文件所包含的所有.h文件。
生成的.d文件如下,會把這個.d文件作為一個Traget,Prerequisite文件就是其對應的.c文件,以及.c文件所引用的所有頭文件。
這樣就把這個.c文件所生成的.d文件和這個.c文件引用的所有.h文件就關聯起來了。
當某一個.h文件有修改時,引用這個頭文件的.c文件所生成的.d文件就會被認為要重新生成,再配合上對應生成規則,就可以保證當某一個.h文件修改時,包含這個.h的所有.c文件都會重新編譯。
于是就得到了版本3的makefile
首先增加了依賴文件的變量定義(DEPS),里面定義了所有的依賴文件。
其次在.c編譯為.o文件時,增加了指令 -MMD -MP -MF,以及-MT。這些指令的作用都是生成.d的依賴文件,"(@:%.o=%.d)"是依賴文件的文件名,由于依賴文件和目標的.o文件是同名,只是后綴由.o變為.d,所以這邊"(@:%.o=%.d)"的作用就是把目標.o文件的后綴改為.d,作為生成的依賴文件的文件名。
同時增加了目標文件.d對.c文件的規則,表示.d文件如果需要重新生成的話,就會按照這個規則,把.c文件重新編譯一份.o文件。由于這邊目標文件就是.d文件,所以這里就直接使用了"$@"來指定生成.d文件的名稱。
最后增加了include的指令,把所有的生成的依賴文件都包含進來。include的作用就是把include的文件里的內容直接加到當前的makefile中。也就是增加.d文件和對應.c文件和.h文件的規則。保證了當.h文件被修改時,對應的.d也會被要求重新生成,從而去重新編譯對應的.c文件,生成新的.o文件。
- 通過定義代碼文件夾,自動推導出編譯所需的文件
通過之前的步驟,一個功能比較完善的makefile就寫完了。但還有一個問題,目前所有的.c和.h文件都在一個文件夾內,而且生成的.o和.d文件也都在同一個文件夾,這樣很不利于管理。
我們把文件夾路徑調整的跟實際更為貼切一些,如下圖所示,.c分件分別在5個不同source_code的文件夾中,.h文件分別在6個不同的include_file中,makefile和link文件在complie_env中,編譯完產生的map文件和elf文件,也在complie_env中,同時在編譯時,complie_env中還會生成一個臨時文件夾tmp,用來存放.o和.d文件。
同時,手動定義.o文件和.d文件也很麻煩,我們也可以直接從給定的文件夾中尋找所有的.c文件,然后根據文件夾的相對路徑推導出.o和.d文件
于是就有了版本4的makefile
首先定義存放臨時文件(.d和.o文件)的文件夾,makefile中都推薦使用相對路徑,這樣不管工程放在什么路徑下面,只要文件夾結構不變就都能正常運行。"./"表示makefile當前文件夾的路徑。
然后定義包含所有需要參與編譯的.c文件的文件夾,這些文件里所有的.c文件都會參與編譯,"../"表示makefile當前文件夾的上一層文件夾路徑。
接著定義所有被引用的.h文件的文件夾。
再接著根據定義的.c文件的文件夾,找出這些文件夾里所有的.c文件。這里有兩個指令可以解釋一下,一個是 foreach,"foreach DIR, **(SRC_DIRS), **(wildcard (DIR)/*.c)"表示對SRC_DIRS中定義的所有文件夾做一個for循環,當前循環的文件夾名叫DIR。另一個是wildcard,"(wildcard $(DIR)/*.c"表示找到DIR這個文件夾下面所有的.c文件。這樣就把SRC_DIRS中定義的所有的文件夾里所有的.c文件都找出來了,給到SRCS這個變量中。
有了所有.c文件后,就可以推導出所有的.o文件,由于.o文件的位置和.c文件不一樣,所以具體推導.o文件的步驟就是將.c文件的文件夾路徑去掉,只保留.c文件的文件名,然后加上將要存放.o文件的路徑,以及把.c的后綴換成.o。這一些列的動作,都可以用"OBJS = **(patsubst %.c, {TMP_DIR}/%.o,(notdir **{SRCS}))"這個命令來實現。"nodir"就是去掉文件夾路徑,"patsubst"是替換指令,這里是把所有.c文件替換為.o文件,并加上路徑。
最后就是根據.o文件定義.d依賴文件,由于.d文件和.o文件都會放在臨時文件夾中,所以只要簡單的將.o文件的后綴.o換成.d就可以了。
由于.o文件和.d文件的位置發生了變化,執行語句的命令也要做修改,.o和.d的目標文件都加了路徑,命令中也加了"-o",明確指定了編譯后的.o文件存放的位置。
- 將makefile按功能分類
現在可以看到,由于加了代碼文件夾和頭文件文件夾的定義,makefile文件變長了不少,如果是大型項目,那么代碼文件夾和頭文件夾的數目會更多。為了更好的維護makefile,我們可以按照功能多定義幾個makefile,然后在主makefile中include這些makefile,這樣使得層次更加清晰,也更好維護。
比如可以新建一個source.mk的文件,里面定義想要生成的二進制文件的名稱,c代碼文件夾,頭文件文件夾,臨時文件夾,推導出所有的.c,.o,.d文件
還可以再建一個congif.mk的文件,里面定義跟編譯器相關的一些設置,比如編譯器的路徑,刪除命令的定義,編譯時的參數等
這樣我們的主makefile又變得簡潔了,而且這個文件基本就可以不用修改了。如果編譯設置要修改的話,就修改config.mk,如果需要編譯代碼的內容有修改的話,就去修改source.mk。維護起來就很清晰了,美滋滋~
- 刪除編譯產生的文件
我們有時不但需要編譯文件,也需要刪除之前編譯的文件,這個也可以在makefile中完成。
我們可以添加clean功能,在clean為目標的情況下,執行刪除命令,刪除.o文件,.d文件,elf文件和map文件
這邊介紹一下PHONY功能,因為all和clean作為Target時,這兩個命令并不是真正的文件名,也就是并不是要生成名為all或者clean的文件。但是萬一有文件名叫all或者clean的文件話,就會產生歧義,這時就可以用.PHONY來定義all和clean,告訴make,這兩個是命令,而不是文件,防止發生其想不到的問題。
另外,由于執行clean命令時,僅僅是想刪除文件,并不需要去推導依賴文件,所以這邊使用ifneq來判斷,如果命令是clean的話,就不包含依賴文件了。
同時,也增加了rebuild功能,如果執行make rebuild的話,就會先clean,刪除之前的文件,然后再生成目標文件。
- 調用makefile
如果makefile的名字就叫makefile或Makefile,那么可以在cmd的命令框中,將路徑切換到makefile的路徑,直接輸入make all,進行編譯,或者make clean,刪除之前編譯的文件。
但makefile的名字也不一定非要這么叫,我們可以任意起名字,調用的時候只要增加參數 "-f" 然后加上文件名就可以了,比如:
make -f makefile_ver5 all,或者make -f makefile_ver5 clean
另外,還有一個參數比較使用的就是"-j"加數字,比如
make all -j8
數字可以根據自己電腦cpu的線程數進行調整,這個表示可以多個線程并行處理命令,也就是可以同時編譯多個.c文件,這樣可以提高編譯速度。
另外如果嫌每次要打開cmd窗口比較麻煩的話,也可以建一個build.bat的批處理文件,這樣想要編譯時,就直接雙擊這個文件就可以了。
Cmd /k的作用是運行完仍然保留cmd窗口,這樣如果有錯誤的話就能看到了。
build.bat文件的內容如下:
后記
這篇文將主要是介紹makefile的規則和大概的運行原理,知道這些之后,大家可以根據實際需要來自己寫makefile,或者把之前項目中的makefile按自己的理解優化。
關于make的參數和詳細說明可以參考"make.pdf",關于編譯器的參數詳細說明可以參考“tricore-gcc.pdf”,關于鏈接的參數詳細說明,可以參考“tricore-ld.pdf”。這些都在百度網盤里,代碼和makefile也在里面。感興趣的可以下下來看看。或者根據項目實際的情況查閱對應的make,編譯和link的對應文檔。
-
單片機
+關注
關注
6061文章
44903瀏覽量
646407 -
HEX文件
+關注
關注
0文章
26瀏覽量
13071 -
編譯器
+關注
關注
1文章
1654瀏覽量
49844 -
Makefile
+關注
關注
1文章
125瀏覽量
19557
發布評論請先 登錄
淺談Linux內核源碼的Makefile、Kconfig和.config文件

如何寫Makefile編譯匯編和C文件

windows平臺下makefile操作教程

在Linux下實現進度條程序,通過makefile進行編譯
Linux0.11-Makefile 文件

Linux內核的Makefile、Kconfig和.config文件
交叉編譯鏈下的Makefile(STM32F4xx)

評論