软件级应用的编译
本文最后更新于 2025年3月11日 晚上
软件级应用的编译
软件级是相对于程序级的概念,软件级应用往往包含多目录多文件的大量源代码,有复杂的第三方库依赖关系。
软件级应用的编译用时往往较长,并且过程相对繁琐。
我们这里介绍使用 MinGW 系列工具和 CMake 编译 cpp 软件级应用的过程和知识。
各种文件
CMakeLists.txt
CMakeLists.txt
一般用于跨平台的大型软件级项目,用于指示 CMake 生成平台对应的编译选项,也就是 Makefile
文件。
Makefile
Makefile 文件指定 make 工具编译生成 include/
,lib/
,bin/
等成品的方式,我安装的是 MinGW套件,命令是 mingw32-make
。
.dll/.so
这两个是动态链接文件,.dll 是 Windows 下的,.so 是 Linux 下的。
MinGW 默认是动态链接的,编译生成的 .exe 如果找不到 .dll 就运行不了。
.dll 或者 .so 默认只会在系统路径和工作目录两个地方去找
.o
.o 是可重定向目标文件,是汇编过程生成的源文件机器语言代码。
.a
.a 是静态链接归档文件,如果采用静态链接的方式编译,就需要在编译时加上这个。
它等价于把若干个 .o 打包在了一起。
C++ 编译速通
编译步骤:
- 预处理:把
#include
的东西全部粘贴到对应的位置,由.cc
,.cpp
生成.i
,.ii
预处理文件。 - 编译:编译器用 C/CPP 代码生成汇编代码,由
.i
,.ii
生成.s
汇编文件。 - 汇编:由汇编器将汇编代码编程二进制代码,由
.s
生成.o
目标文件。 - 链接:由链接器把汇编的机器代码
.o
,打包的静态链接库.a
,动态链接库.
静态链接和动态链接的区别:
- 静态链接在编译时便将中的实现放入了 .exe 文件,在运行 .exe 时就将代码放入内存。
- 动态链接在运行时才去 .dll 中查找实现,并在调用对应函数时将这部分代码放入内存。
无论是静态链接还是动态链接,编译时都需要指定链接库。静态和动态的区别在于将对应代码加入内存的时间。
实现通常写在 .cpp 文件中,定义通常写在 .h 文件中,#include
时一般只会 #include
.h 文件,所以如果编译时不指定链接库,链接步骤**就会报 Undefined Refference
错误。
GNU-make 工具的使用
类 UNIX 环境下它叫 make
,但是我们用的 MinGW,反正我这里叫 mingw32-make
,路径是 path/to/mingw64/bin/mingw32-make.exe
,使用时记得加环境变量。
make 和 Makefile 结合,执行一些特定的命令,完成项目的编译,避免手敲 gcc 命令。
命令格式:
1 |
|
常用可选参数:
-n
:不执行,只打印要执行的命令。-f
:指定 Makefile 的路径,默认情况下 Makefile 的(相对)路径为Makefile
或makefile
。-j[<num>]
:多线程编译,例如-j4
表示四线程编译。
目标是指 make 要执行的具体任务,如果没有指定目标,那么会执行第一个非伪目标。
Makefile 解读
一般情况下需要我们写 makefile 的机会不是很多,会读和改就可以了。
变量操作
<VAR_NAME> ?= <EXPR>
条件赋值,如果还没定义这个变量才赋值,规则同递归赋值。<VAR_NAME> = <EXPR
递归赋值计算,每次使用时重新计算。<VAR_NAME> := <EXPR
立即赋值计算,只计算一次。$(<VAR_NAME>)
:引用变量。
目标
目标的格式是 <target>:[<dependencies>]
。
当 make 执行某个目标时,会对依赖项进行检查,如果依赖项对应的文件不存在,则会将先执行依赖的名字对应的目标。
例如:
1 |
|
执行 make
时,过程如下:
没有指定目标,会找到第一个目标
foo
执行。查找其依赖
foo.o
,发现不存在,于是查找目标foo.o
。找到
foo.o
其依赖foo.c
作为一个文件存在,于是执行gcc -c foo.o foo.c
编译生成foo.o
。返回 2. 中步骤,依赖
foo.o
已经存在,执行gcc -o foo foo.o
生成foo[.exe]
。
目标规则
有时候可能有很多文件,某些文件的生成方式,例如 .o,是一致的,这时可以利用目标规则来定义目标。
查找目标时,规则优先级为:越具体越优先。
目标规则大致分为三种:
显式目标规则
见上。
显示模式规则
显式模式规则使用通配符
%
来匹配任意字符,包括目录分隔符。其余同上。下面的
$<
和$@
是自动变量,下面的部分有详细解释。1
2
3%.o: %.c
echo "Using generic rule"
gcc -c $< -o $@隐式模式规则
隐式规则模式用于指定生成对应后缀名的文件,常见的有:
.c.o:
从.c
文件生成.o
文件。.cpp.o:
从.cpp
文件生成.o
文件。.cpp.exe:
从.cpp
文件申城.exe
文件。
伪目标
一般情况下,目标会作为一个文件,通过判断文件是否已经生成来判断是否已经完成该目标。
但是,如果修改了 .cpp 源代码,但是 makefile 中有生成 .o 的过程,.o 还被保留了,那么再次 make 的时候检测到 .o 已经存在,只会用已经存在的 .o 重新链接生成一次可执行文件,没有达到重新编译的效果。所以,往往需要定义一个”清理操作”,来清理相关的文件以便重新完整编译。
“清理操作” 显然不生成文件,为了完成这个操作,引入了 “伪目标” 概念,伪目标在文件开头用 .PHONY
声明,例如:
1 |
|
这样便可以在每次执行 make
时清理缓存的 .o
文件达到重新编译的效果。
提醒:
- 伪目标表示一个过程,作为依赖时一定会被执行,如果执行返回代码为 0 才判定为完成,否则判定为没有完成,可能报错。
- 严格来说 all 也应该被定义为伪目标,实际上由于不会生成 all 文件,也就无所谓,clean 有时候不会被声明为伪目标。
make
不指定目标时,如果存在非伪目标,那么执行第一个非伪目标,否则仍然执行第一个目标。
自动变量
自动变量是执行规则时根据上下文动态生成的变量。
常用自动变量表如下:
$@
- 含义:表示当前规则的目标文件(Target)。
- 用途:常用于指定输出文件名。
- 示例:如果目标是
1
2foo.o: foo.c
gcc -c foo.c -o $@foo.o
,$@
的值就是foo.o
。
$<
含义:表示依赖列表中的第一个依赖项。
用途:常用于单个依赖项的场景,例如编译
.c
文件时指定源文件。示例:
1
2foo.o: foo.c header.h
gcc -c $< -o $@如果目标是
foo.o
,$<
的值是foo.c
。
$^
- 含义:表示依赖列表中的所有依赖项(以空格分隔)。
- 用途:常用于需要处理多个依赖项的场景,例如链接多个对象文件。
- 示例:如果目标是
1
2foo: foo.o bar.o
gcc -o $@ $^foo
,$^
的值是foo.o bar.o
。
CMake 系列工具使用
不同平台使用的编译工具不一定相同,有时候根据实际情况,也可能会选择性编译整个项目的一部分。如果只提供 Makefile 可能不方便。这个时候可以利用 CMake 系列工具和 CMakeLists.txt 来根据具体需求生成特定的 Makefile。
CMake 工具可以在这里下载。
检查一个项目根目录是否包含 CMakeLists.txt
以便确认它是否支持使用 CMake 系列工具。
CMake-GUI 有个 Bug,详见。
下面是 CMake 的 UI:
配置源代码和构建路径
两个位置需要填写源代码目录(包含 CMakeLists.txt
)和构建目录(自己创建一个)。
Configure
点击 Configure,会先让你选编译器之类的:
我们用 MinGW,翻一下找到选上就行,点 Finish 会自动读取所需配置项,读完之后你可以用 UI 界面方便的对它们进行改动。
Configure 过程也会读取平台信息等,大型项目 Configure 用时会很长。
如果改动了配置,需要再点一次 Configure。
由于 CMake 缓存相关的问题,如果第一次 Configure 爆红,也需要再点 Configure 一次解决问题。
Generate
完成 Configure 后,点 Generate,会在 build 目录生成 Makefile 和所需要的文件,在 build 目录执行 make
命令即可。
直接编译
gcc
命令格式:
1 |
|
功能参数
-o
:完成链接停止。-c
:完成汇编停止。-s
:完成编译停止。-E
:完成预处理停止。
主参数
主参数用于指定执行这次操作所需的各种文件的位置,包括 .cpp 文件、.a 文件、.o 文件等,如果这里没有显式指定,可以通过下面的参数补充指定。
包含目录
包含文件不能在主参数部分指定,必须位于包含文件搜索目录下。
#include
除了在工作目录和系统路径搜索之外,还可以使用多个-I path\to\include
来指定包含文件搜索目录。链接目录
使用
-L
参数指定链接搜索目录,需要和链接参数配合使用。链接参数
使用
-l
参数,语法规则为-l[链接名]
(注意没有空格),优先链接到链接目录下动态链接文件/path/lib[连接名].dll
文件。如果不存在动态链接,则会链接到静态链接文件/path/lib[链接名].a
文件。链接库可以在主参数中直接指定,写它的路径即可。
例如
-L ./lib/ -lzlib
等价于主参数中添加./lib/libzlib.dll
或者./lib/libzlib.a
。编译选项
--static
:强制使用静态链接。-O2
:开启 O2 优化。-g
:生成调试符号表启用 gdb 调试功能。-Wl,-rpath,<path/to/dll>
:指定运行时链接搜索目录,运行时最优先到path/to/dll
去搜索 .dll 文件,一般使用相对路径
示例(我使用了 [] 和 () 来划分部分,实际使用时需要去掉):
1 |
|
ar
使用 ar
工具将多个 .o
文件打包成一个 .a
静态库文件。例如:
1 |
|
这里,libmylib.a
是生成的静态库文件名,file1.o
和 file2.o
是输入的目标文件。
动态链接
我觉得自己造的轮子就没必要搞动态链接了,全静态吧。想起来我再补上。
软件的发布
发布 .cpp 编译生成的 .exe 软件时,需要考虑到用户的机子上没有运行环境的事实,一般采用两种方式:
- 全静态链接
--static
。 - 同时发布所需的 .dll 文件,放在 .exe 同级目录下或者使用
-rpath
编译参数。 - 折中。
练习——Windows 编译 OpenCV
OpenCV 是使用最为广泛的计算机视觉库,编译文档十分完善,也有数量可观的资料可供查询。但 OpenCV 体型巨大,编译用时较长。
编译完成后应该得到以下文件:
- 包含目录
include/
- 静态链接库
lib/
- 动态链接和其它二进制文件库
bin/
练习——Windows 系统编译 minizip
minizip 是 zlib 库的一个子库,能够支持压缩和解压。minizip 目前已经放弃维护,相比成熟的 OpenCV,可能需要修改一些编译命令才能完成编译。
你需要完成 minizip 和 zlib 的编译,得到以下文件:
- zlib 的包含目录
include/
和静态链接库lib/
。 - minizip 的包含目录
include/
和静态链接库lib/
。
提醒:
注意,官方的 makefile 中可能没有生成目录的步骤,你可能需要手动创建目录或者更改 makefile。
minizip 的 makefile 无法针对 windows 使用,请自行排查其错误并进行修改后完成编译,或者使用 g++ 手动编译生成有关文件。
你可以用 CMake 完成 zlib 的编译,也可以阅读下面文档手动编译:
To compile all files and run the test program, follow the instructions given at
the top of Makefile.in. In short “./configure; make test”, and if that goes
well, “make install” should work for most flavors of Unix. For Windows, use
one of the special makefiles in win32/ or contrib/vstudio/ . For VMS, use
make_vms.com.minizip 源代码位于
zlib/contrib/minizip/
,请自行解读代码结构并进行操作。注意其依赖的 zlib 相关的配置。
大练习——编译 NcatBot 发行包
背景简介
ncatbot 旨在让用户无门槛使用,开发者只关注业务代码,提供了一个 windows 平台下的 .exe 安装部署工具。
该工具只包括一个 main.exe
主程序,能够无下载过程的配置基础 Python 环境,并安装 ncatbot 本体,调用 ncatbot-cli 完成后续交互。
相应的代码 main.cpp
,Python 环境压缩包 package.zip
都已经给出,请编译出可执行文件 main.exe
。
有关资源
指示
main.cpp
开头部分含有编译命令,参考这一部分编译命令,你需要完成如下工作:
- 完成 zlib,minizip 的编译。
- 完成
package.zip
的编译。 - 正确书写设置 zlib,minizip 的路径和编译命令。
- 编译
main.cpp
并链接其它上述资源。
在 NcatBot-Release 中含有编译好的 zlib,minizip,package,如果你实在无法完成这些部分,可以下载使用并完成接下来的部分。
附录
命令格式书写语法
这是一种约定俗称的记号,而不是一个严格规范,每位开发者使用的书写方式不一定相同,也不会特意严格按照要求书写,下面给出我的习惯记号。
[EXPR]
:表示这一部分是可选的。[mingw32-]make
:可能是mingw32-make
或者make
。- 有些人也会写成
()
,但一般不会写成<>
。
<VAR>
:表示一个变量,需要根据实际情况更改。- 有时候你会明显感觉到不用
<>
包起来也表示一个变量,自己灵活处理,例如path/to/zlib/
可能表示C:/Program Files(x86)/zlib/
。 - 有时候也会写成
[]
。
- 有时候你会明显感觉到不用
<VAR...>
:表示这里的参数个数是动态变化的,每个参数用空格分隔,可以是 0 个。<编译选项...>
可能表示-O2 -fpermissive
或者-O2 --static
或者--static
或者啥都没有。- 有时候也会写作
[VAR...]
或者[<VAR>...]
。
更多自动变量
$?
含义:表示依赖列表中比目标文件更新的依赖项(以空格分隔)。
用途:常用于条件编译或增量构建,只处理那些真正需要更新的文件。
示例:
1
2foo.o: foo.c header.h
gcc -c $< -o $@如果
header.h
被修改,$?
的值是header.h
。
$*
含义:表示目标文件的主名(不包含扩展名)。
用途:常用于生成与目标文件相关的其他文件名,例如中间文件或依赖文件。
示例:
1
2
3%.o: %.c
gcc -c $< -o $@
echo "$* is compiled"如果目标是
foo.o
,$*
的值是foo
。
$+
含义:表示依赖列表中的所有依赖项(以空格分隔,与
$^
类似,但保留重复项)。用途:在某些需要保留重复依赖项的场景中使用。
示例:
1
2foo: foo.o foo.o bar.o
gcc -o $@ $+如果目标是
foo
,$+
的值是foo.o foo.o bar.o
。
$|
含义:表示依赖列表中的顺序依赖项(Order-only prerequisites)。
用途:用于指定那些仅用于顺序控制的依赖项,这些依赖项的更新不会触发目标的重新构建。
示例:
1
2foo: bar.o | dir
gcc -o $@ $<如果目标是
foo
,$|
的值是dir
。
$(@F)
和$(@D)
**
$(@F)
**:表示目标文件的文件名部分(不包含路径)。**
$(@D)
**:表示目标文件的目录部分(不包含文件名)。示例:
1
2
3
4build/foo.o: src/foo.c
gcc -c $< -o $@
echo "File: $($(@F))"
echo "Directory: $($(@D))"如果目标是
build/foo.o
:$(@F)
的值是foo.o
$(@D)
的值是build
$(*F)
和$(*D)
**
$(*F)
**:表示$*
的文件名部分。**
$(*D)
**:表示$*
的目录部分。示例:
1
2
3
4build/foo.o: src/foo.c
gcc -c $< -o $@
echo "File: $($(*F))"
echo "Directory: $($(*D))"如果目标是
build/foo.o
:$(*F)
的值是foo
$(*D)
的值是build