cmake應用:從編譯過程理解CMake

來源:公衆號【很酷的程序員/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的編譯過程大概是:

  1. 預處理:將源文件處理爲.ii/.i,處理各種預處理指令,如#include#ifdef#if等等,同時也會清除註釋;
  2. 編譯:將.ii/.i處理爲.S/.asm,即機器語言的彙編文件;
  3. 彙編:將.asm/.S處理爲.o,把彙編文件變成機器碼;
  4. 鏈接:將各種依賴的靜態/動態庫文件、.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 編譯

在編譯的時候,需要把源文件處理成機器代碼,主要有兩個方面:

  1. 對於源文件裏面的代碼具體怎樣進行編譯
  2. 源文件內部調用的外部函數怎麼查找

對於第一點,就是各種編譯選項,有很多類型:

  1. 編譯警告選項,比如-Wall-Wextra
  2. 代碼優化選項,比如:-O0-Ofast
  3. 調試選項,比如:-g-fvar-tracking
  4. 預處理選項,比如:-M-MP
  5. 代碼生成選項,比如:-fPIC-fPIE
  6. 等等,還有針對不同語言特有的選項

所有的選項在GNU GCC官網上有詳細的介紹,參見:Option-Summary

對於第二點,在源文件內部,調用的外部函數是在頭文件中聲明的,所以通過#include的頭文件編譯器必須能夠找到,這個時候需要使用-I參數指定頭文件的查找路徑,以確保編譯器可以找到源文件所使用的頭文件。

在使用gcc命令時,選項直接作爲參數傳遞即可,比如:

gcc -c xxx.c -Os -g -Wall -Wextra -pedantic -Werror -o xxx.o -Isrc/c

那麼在CMake中,可以:

  1. 使用add_compile_options命令指定編譯選項
  2. 使用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對應可以使用的命令爲:

  1. 對於-L,使用link_directories或者target_link_directories命令
  2. 對於-l,使用link_libraries或者target_link_libraries命令
  3. 指定鏈接器的選項,使用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!

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