C/C++编译知识

使用 GCC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
kang@pro:~$ gcc --help
Usage: gcc [options] file...
Options:
-pass-exit-codes Exit with highest error code from a phase.
--help Display this information.
--target-help Display target specific command line options (including assembler and linker options).
--help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...].
Display specific types of command line options.
(Use '-v --help' to display command line options of sub-processes).
--version Display compiler version information.
-dumpspecs Display all of the built in spec strings.
-dumpversion Display the version of the compiler.
-dumpmachine Display the compiler's target processor.
-foffload=<targets> Specify offloading targets.
-print-search-dirs Display the directories in the compiler's search path.
-print-libgcc-file-name Display the name of the compiler's companion library.
-print-file-name=<lib> Display the full path to library <lib>.
-print-prog-name=<prog> Display the full path to compiler component <prog>.
-print-multiarch Display the target's normalized GNU triplet, used as
a component in the library path.
-print-multi-directory Display the root directory for versions of libgcc.
-print-multi-lib Display the mapping between command line options and
multiple library search directories.
-print-multi-os-directory Display the relative path to OS libraries.
-print-sysroot Display the target libraries directory.
-print-sysroot-headers-suffix Display the sysroot suffix used to find headers.
-Wa,<options> Pass comma-separated <options> on to the assembler.
-Wp,<options> Pass comma-separated <options> on to the preprocessor.
-Wl,<options> Pass comma-separated <options> on to the linker.
-Xassembler <arg> Pass <arg> on to the assembler.
-Xpreprocessor <arg> Pass <arg> on to the preprocessor.
-Xlinker <arg> Pass <arg> on to the linker.
-save-temps Do not delete intermediate files.
-save-temps=<arg> Do not delete intermediate files.
-no-canonical-prefixes Do not canonicalize paths when building relative
prefixes to other gcc components.
-pipe Use pipes rather than intermediate files.
-time Time the execution of each subprocess.
-specs=<file> Override built-in specs with the contents of <file>.
-std=<standard> Assume that the input sources are for <standard>.
--sysroot=<directory> Use <directory> as the root directory for headers
and libraries.
-B <directory> Add <directory> to the compiler's search paths.
-v Display the programs invoked by the compiler.
-### Like -v but options quoted and commands not executed.
-E Preprocess only; do not compile, assemble or link.
-S Compile only; do not assemble or link.
-c Compile and assemble, but do not link.
-o <file> Place the output into <file>.
-pie Create a dynamically linked position independent
executable.
-shared Create a shared library.
-x <language> Specify the language of the following input files.
Permissible languages include: c c++ assembler none
'none' means revert to the default behavior of
guessing the language based on the file's extension.

Options starting with -g, -f, -m, -O, -W, or --param are automatically
passed on to the various sub-processes invoked by gcc. In order to pass
other options on to these processes the -W<letter> options must be used.

For bug reporting instructions, please see:
<file:///usr/share/doc/gcc-13/README.Bugs>.

一些参数可以单独使用,此时可以将单字母粘连,比如 -c -g 等价于 -cg
一些参数后要跟特定的值,可以用 =、空格、粘连赋值,比如 gcc -Iinclude1 -I=include2 -I include3(但也有可能一些参数只能遵循特定的形式)。

常用的参数项

1
2
3
4
5
6
7
8
9
10
11
12
-I/xxx      # 指定头文件搜索目录
-L/xxx # 指定库文件搜索目录
-l/xxx # 链接库文件

-c # 只编译和汇编,不链接,生成目标文件(.o)
-S # 只编译,不汇编,生成汇编代码文件(.S)

-g # 生成调试信息
-Wall # 打开几乎所有警告信息
-O<level> # 设置优化等级
-std=c11 # 指定使用的C语言标准
-fPIC # 生成位置无关代码

多文件

多文件编译,其实就是将每个文件编译后的 .o 文件链接后的产物,所以在 make 中会像下面一样,将每个部分单独写出来,这样修改时未被更改的部分无需重复编译(可以利用通配符在 Makefile 中进一步进行抽象)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main: main.o hello.o
gcc main.o hello.o -o main
main.o: main.c
gcc -c main.c -o main.o
hello.o: hello.c
gcc -c hello.c -o hello.o

# 进一步抽象描述
CC = gcc
SRC = main.c hello.c
OBJ = $(SRC:.c=.o)
main: $(OBJ)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $^ -o $@

#include 如果是 "",则会先在当前目录查找,失败后才会在系统路径查找;
如果是 <>,则直接在系统路径中查找。
在编译时指定的 -I,与系统目录等价,所以也可以被 <> 发现,借此避免使用冗长的相对路径格式。
虽然编译时正确找到头文件,但是代码补全插件找不到,写代码不舒服。不过既然编译没问题,可以在编译时将头文件目录这种要求搞成一个配置文件,也就是 compile_commands.json 文件。直接通过 Makefile 构建的,可以通过 Bear 生成该文件;通过 CMake 配置的,可以加一个配置选项 -DCMAKE_EXPORT_COMPILE_COMMANDS=ON,在 build 目录即可生成该文件。

链接库

下载使用第三方库

一般引用第三方库是通过包管理器(apt 等)下载相应的包,由于 apt 会下载到系统路径,所以编写代码时可以直接 #include 相应第三方库的头文件,而在编译时同样需要链接第三方库。
因为头文件的作用是让我们复制一边声明,真正的库实现需要编译时手动链接。
比如在 C++ 中使用 fmt 库,通过 -lfmt 链接该库。

因为链接时还需要添加上具体的库名,不手动去看并不知道有哪些,此时可以通过 pkg-config 获取一个包的库名字等。

1
2
3
4
5
6
7
# 获得库的链接参数
kang@pro:~$ pkg-config libgvc --libs
-lgvc -lcgraph -lcdt

# 获得头文件目录的链接参数
kang@pro:~$ pkg-config libgvc --cflags
-I/usr/include/graphviz

以上返回是由包中的 pkgconfig 目录中的内容决定的。

链接多个 .o 文件和库文件时,放置顺序会影响结果。目标文件的未定义符号,只会在它后面的目标文件或库中寻找,不会再看前面的。所以,被依赖的要放在后面

包的组成

一个第三方库大致有以下部分:

1
2
3
4
5
6
# 有些本身是工具库存在的可能没有 bin/; 同样的,一些主要提供可执行程序直接使用的可能没有 include/
.
|- bin # 可执行文件
|- include # 头文件
|- lib # 静态库和动态库
|_ share # 文档等

而直接安装一个第三方库,比如 Ubuntu 下的 deb 包,安装后以上文件会自动放入对应的系统路径 /usr/xxx 中,而在编译器编译时,这些路径会进行默认搜索,所以头文件可以直接找到,库文件也直接写名字即可链接。
而如果放在一个一般路径,在编译时则需要指定相应目录。

1
2
# -I 指定头文件目录; -L 指定库文件目录; -l 指定库名字
g++ main.c -I/xxx/include -L/xxx/lib -lfmt -o main

对于 gcc,可以通过以下命令查看默认搜索的头文件目录和库目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 打印出gcc自身安装目录和库搜索目录等
kang@pro:~$ gcc -print-search-dirs
install: /usr/lib/gcc/x86_64-linux-gnu/13/
programs: =/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/bin/
libraries: =/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/13/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/13/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/

# -E 预处理; -v 详细输出; - 从标准输入读取;
# 用一个空内容,查看预处理阶段会进行哪些操作,可以发现默认的头搜索目录
kang@pro:~$ gcc -E -v - < /dev/null
Using built-in specs.
COLLECT_GCC=gcc
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.2.0-23ubuntu4' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-uJ7kn6/gcc-13-13.2.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-uJ7kn6/gcc-13-13.2.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 13.2.0 (Ubuntu 13.2.0-23ubuntu4)
COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-linux-gnu/13/cc1 -E -quiet -v -imultiarch x86_64-linux-gnu - -mtune=generic -march=x86-64 -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -dumpbase -
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/13/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
# 0 "<stdin>"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "<stdin>"
COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=x86-64'

对于开源的第三方库(以及要编写自己的开源项目的话),可以通过更改配置来自定义编译的结果。在编译很多项目时,往往会经历 configure->make->make install 三部曲(很像使用 CMake 的流程,相比之下 CMake 新多了)。
在用户视角下,首先我们要运行开发者提供的 configure 脚本,如下图所示,这个过程会生成 Makefile 文件,用户可以选择加参数自定义配置,比如 ./configure -prefix=/xxx

随后,利用 Makefile 文件进行构建,并将软件安装到对应的位置。
开发者可以使用 GNU 构建系统(GNU Build System)来构建支持以上流程的软件体系,包含三件套:autoconf, antomakelibtool.

运行时库

为了提升C语言的开发效率,C语言标准定义了一系列常用的函数,这些函数统称为C库函数。C标准仅规定了这些函数的原型,而具体的实现则留给了各个支持C语言标准的编译器。因此,每个编译器通常会提供超出标准C的实现,形成了所谓的C运行时库(C Run Time Library)。
C运行时库中的启动函数负责程序的初始化工作,包括设置堆栈、初始化全局和静态变量、调用构造函数等;程序结束时,运行时库也负责调用析构函数、释放资源等清理工作。所以,C程序都需要链接C运行时库。
与C语言类似,C++也有自己的标准,并提供了相应的支持库,即C++运行时库或C++标准库。由于C++设计时考虑了与C语言的兼容性,C++标准库不仅包含了C标准库,还扩展了包括标准模板库(STL)在内的更多功能。

通过 ldd 可以列出可执行文件或共享库所依赖的共享库。

可以看出,默认情况下一个简单的C程序会动态链接 libc.so.6,我们也可以指定静态链接,此时无需依赖运行时库,但文件体积也相应增大。

参考:

  1. 【技术杂谈】C编译体系
  2. GNU构建系统和Autotool
  3. 终于理解了什么是c/c++运行时库,以及libcmt msvcrt等内容

C/C++编译知识
https://kaysonyu.github.io/2024/12/C_CXX_Compiler/
Author
Yu Kang
Posted on
December 9, 2024
Licensed under