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!

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