來源:公衆號【很酷的程序員/RealCoolEngineer】
CMake和編譯的過程是有對應關係的,理解了編譯構建的過程,可以更加理解CMake的相關命令;理解其目的和用途,自然也就可以更好地運用CMake。
在最近的CMake系列文章中,有小夥伴在實踐使用的時候還是比較困惑,溝通之後瞭解到可能有的同學並不是計算機專業,對於編譯原理、編譯的過程可能並沒有很瞭解,所以筆者寫了一篇文章:GCC編譯過程概述,對GCC編譯的過程做了一個概述。
本文作爲這篇文章的姊妹篇,依舊以GCC爲例,在對GCC編譯過程有一定了解的基礎上,來進一步理解CMake如何通過CMakeLists.txt定義項目的編譯構建過程。
一 編譯構建的框架
在GCC編譯過程概述一文中,主要介紹了源文件如何編譯成機器碼.o
文件,以及最後鏈接器怎麼鏈接相關庫文件得到最後的可執行文件。
其實,對於構建的每一個目標,都是樹形的結構,以本系列的開源項目https://gitee.com/RealCoolEngineer/cmake-template爲例(當前commit id: ca0e593
),構建目標、源文件/.o
文件和.a
文件之間構成一棵"構建樹"(Build Tree):
對於最終的可執行文件(demo)來說,必須能夠找到所有需要的函數的實現,這些實現可能包含在單個.o
文件(demo.o,crtn.o等等)、或者打包好的庫文件(其實就是.o
文件的集合,比如libmath.a,libm.a),所以它會是構建樹的根。
對於一些庫文件(模塊)來說,它可以是最終可執行文件構建樹的子樹,也有對應的構建產物(比如這裏的libmath.a
)。
而對於構建樹的葉子節點,其實都對應到具體的源文件,只是說有時候是預編譯好的第三方庫或者系統庫。而源文件如果開源,開發者可以選擇自己從源碼編譯(比如這個項目中的gtest,就是從源碼編譯出來的,在單元測試可執行文件的構建樹裏,葉子節點就是gtest開源的源碼)。
在CMake官網有關於Build Tree的定義,可以查看鏈接:https://cmake.org/cmake/help/latest/manual/cmake.1.html#introduction-to-cmake-buildsystems
注意重在理解其含義而非形式
二 GCC編譯過程和CMake命令之間的關聯
GCC的編譯的具體過程其實是通過gcc
命令的參數進行控制的,這些參數的作用就和CMake的命令有對應的關係。
在GCC編譯過程概述文中,介紹了gcc
命令的常用參數(下面補充了-D
和-O
):
參數 | 含義 |
---|---|
-o | 指定輸出文件路徑 |
-D | 定義預處理宏,格式爲"-D <macro>=<value>" |
-E | 只對源文件進行預處理,輸出.i文件 |
-S | 對源文件進行預處理、編譯,輸出.s文件 |
-c | 對源文件進行預處理、編譯、彙編,輸出.o文件 |
-I | 大寫的i,包含頭文件路徑,如 gcc -Ireal/cool/include/ |
-L | 大寫的l,鏈接庫文件路徑,如 gcc -Lreal/cool/lib/ |
-l | 小寫的l,鏈接庫文件,如鏈接librealcool.a:gcc -lrealcool |
-fPIC | 生成位置無關代碼(position-independent code) |
-Wall | 對代碼所有可能有問題的地方發出警告 |
-g | 在目標文件中嵌入調試信息,便於gdb調試 |
-O0 | 代碼優化等級,-O0 表示不進行優化,-O3 表示最高優化 |
GCC的編譯過程大概是:
-
預處理:將源文件處理爲.ii/.i,處理各種預處理指令,如
#include
、#ifdef
、#if
等等,同時也會清除註釋; -
編譯:將
.ii/.i
處理爲.S/.asm
,即機器語言的彙編文件; -
彙編:將
.asm/.S
處理爲.o
,把彙編文件變成機器碼; -
鏈接:將各種依賴的靜態/動態庫文件、
.o
文件、啓動文件鏈接成最終的可執行文件或者共享庫文件。
其實gcc
命令的參數是針對不同的編譯階段的,下面分階段介紹gcc
參數和CMake命令的對應關係。
1 預處理
在預處理階段,主要處理各種宏,開發的過程中往往會通過#ifdef
來判斷是否定義了對應的宏,來靈活地切換不同代碼,比如:
#ifdef UPPER_CASE
#define name "REAL_COOL_ENGINEER"
#else
#define name "real_cool_engineer"
這個時候,如果需要使用大寫的版本,就可以使用gcc
的-D
參數:
gcc -DUPPER_CASE ...
而在CMake中,可以使用命令:
add_definitions(-DUPPER_CASE)
2 編譯
在編譯的時候,需要把源文件處理成機器代碼,主要有兩個方面:
- 對於源文件裏面的代碼具體怎樣進行編譯
- 源文件內部調用的外部函數怎麼查找
對於第一點,就是各種編譯選項,有很多類型:
- 編譯警告選項,比如
-Wall
、-Wextra
- 代碼優化選項,比如:
-O0
、-Ofast
- 調試選項,比如:
-g
、-fvar-tracking
- 預處理選項,比如:
-M
、-MP
- 代碼生成選項,比如:
-fPIC
、-fPIE
- 等等,還有針對不同語言特有的選項
所有的選項在GNU GCC官網上有詳細的介紹,參見:Option-Summary。
對於第二點,在源文件內部,調用的外部函數是在頭文件中聲明的,所以通過#include
的頭文件編譯器必須能夠找到,這個時候需要使用-I
參數指定頭文件的查找路徑,以確保編譯器可以找到源文件所使用的頭文件。
在使用gcc
命令時,選項直接作爲參數傳遞即可,比如:
gcc -c xxx.c -Os -g -Wall -Wextra -pedantic -Werror -o xxx.o -Isrc/c
那麼在CMake中,可以:
- 使用
add_compile_options
命令指定編譯選項 - 使用
include_directories
命令指定頭文件搜索路徑
因此上面的gcc
命令的效果等同於:
add_compile_options(-Os -g -Wall -Wextra -pedantic -Werror)
include_directories(src/c)
add_library(xxx STATIC xxx.c)
需要注意的是,因爲CMake的構建目標必須是庫或者可執行文件,所以並沒有命令僅生成.o
文件,所以這裏使用add_library
代替。
3 鏈接
鏈接需要做的就是把最終目標依賴的東西都組裝起來。
對於這裏的可執行文件來說,先從demo.o
的main函數開始,鏈接整個程序執行過程中需要的所有函數的實現;不同實現可能在不同的.o
文件或者庫文件內,通過頭文件聲明的函數名,在.o
和.a
文件裏面查找需要的實現;如果找不到,就會引發一個鏈接錯誤。
對於項目內部的構建目標庫文件及其他的.o
文件,在鏈接的時候直接使用即可,而對於外部的第三方庫或者系統的庫文件,則需要使用-L
和-l
參數來告知鏈接器。
和編譯一樣的,除了-L
和-l
,鏈接器也還有很多其他參數`比如:
-pie -pthread -r -s -static -static-pie
詳細的參數介紹詳見:Link-Options。
對應地,CMake對應可以使用的命令爲:
- 對於
-L
,使用link_directories
或者target_link_directories
命令 - 對於
-l
,使用link_libraries
或者target_link_libraries
命令 - 指定鏈接器的選項,使用
add_link_options
或者target_link_options
命令
上述命令中,以
target_
開頭的是針對特定的目標進行設置,否則是針對所有的目標。
假設目標程序使用了外部庫文件/usr/lib/libmath.a
就可以使用命令:
gcc demo.c -L/usr/lib -lmath -pthread
對應地,CMake使用的命令應該是:
add_link_options(-pthread)
add_executable(demo demo.c)
link_directories(/usr/lib)
target_link_libraries(demo math)
三 總結
最後,使用一個表格總結一下本文的核心內容。
GCC編譯過程使用的gcc
參數和CMake
命令之間的對應關係表:
gcc 參數 | CMake 命令 | 含義 |
---|---|---|
-D | add_definitions | 設置預編譯宏 |
編譯器選項 | add_compile_options | 設置編譯器的選項,控制編譯行爲 |
-I | include_directories | 設置頭文件搜索路徑 |
-L | link_directories/target_link_directories | 指定鏈接器搜索庫文件的路徑 |
-l | link_libraries/target_link_libraries | 指定要鏈接的庫文件 |
鏈接器選項 | add_link_options/target_link_options | 指定鏈接器的鏈接選項 |
Enjoy CMake!