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 | 代码优化等级, |
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:https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html。
对于第二点,在源文件内部,调用的外部函数是在头文件中声明的,所以通过#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:https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html#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!