0%

C/C++代码编译相关知识总结

编译相关知识总结

编译的过程(以GCC为例)

  • 预处理(Preprocessing): 生成.i文件. 对于宏(展开)/条件编译(展开)/include头文件(插入)/注释(删除)/#pragma(保留)进行处理, 对代码添加行号和标识位. 可以通过gcc -E来导出预处理文件
  • 编译(Compilation): 生成.s的汇编文件. 经过词法分析->语法分析->语义分析->中间代码生成->目标代码优化的过程. 通过gcc -S. 其中包含一些伪指令, 用于标志段开始/结束等.
  • 汇编(Assembly): 生成.o文件, 通过汇编器as完成, 将汇编语言转化为对应的机器语言, 生成最终目标文件. 通过gcc -c(只编译不链接). 目标文件中包括全局变量, 局部静态变量等数据段, 代码段, 符号表, 调试信息, 字符串表等链接用的信息. 编译以后的目标文件有重定位表,记录着每一个要被修正的地方(重定位入口).
  • 链接(Linking): 把目标文件和多个文件/库链接在一起, 生成可执行文件. 把在其他文件的符号(函数/变量地址)修正为正确的地址.

运行依赖: 静态库/动态库. 静态库就是对一组目标文件的打包, 常通过ar压缩. 可以通过 ar -t xxx.a 查看包含哪些目标文件. objdump -t xxx.a会打印每个目标文件下都有哪些符号.

链接的过程

可以通过ld xxx 查看其链接的库文件

  • 读取多个目标文件的段信息, 合并相同的段, 确定VMA虚拟地址, 可通过objdump -h xxx查看各个段的VMA地址
  • 建立全局表, 包含所有的符号.
  • 确定全局表中全局符号的地址
  • 重定位, 修正代码指令中符号的地址. 在编译器生成.o的过程中,由于有些符号是在外部定义的,所以当汇编那条代码的时候找不到该符号,就会在符号表中把符号标记为U,然后对应的指令地址标记为0或者其他地址,并在重定位表中标记这个位置. 如果在合并后的全局表中找不到该符号的地址 链接器就报错,找到了就修正指令地址 objdump -r xxx.o查看重定位表; objdump -d xxx查看二进制信息

COMMON段是弱符号的存放位置。没有未初始化的全局变量就是典型的弱符号. 不在bss段的原因是还不确定其占用多少空间

现代链接器处理弱符号的规则是:

在生成全局表的时候,

  1. 如果同名符号中,一个强符号,其他都是弱符号,那么就是就用强符号。此时如果弱符号size大小强符号会给出一个warning。
  2. 如果同名符号中,都是弱符号,那么就选占用空间最大的那个弱符号。

gcc可以通过-fno-common或者__attribute__((nocommon))在代码中标识的方式来把所有未初始化的全局变量不以common的形式处理, 即相当于一个强符号处理, 一旦存在同名强符号就会发生符号重复定义的编译错误.

静态库/动态库链接搜索顺序

静态库

  1. ld会去找gcc命令中的参数-L
  2. 环境变量LIBRARY_PATH
  3. 默认路径/lib、/usr/lib、/usr/local/lib

LIBRARY_PATH和LD_LIBRARY_PATH区别与使用:

  • 开发时,设置LIBRARY_PATH,以便gcc能够找到编译时需要的动态链接库。
  • 发布时,设置LD_LIBRARY_PATH,以便程序加载运行时能够自动找到需要的动态链接库。

动态库

动态共享库(ld.so)搜索顺序

  1. 编译目标代码时指定的动态库搜索路径: ELF可执行文件中动态段DT_PATH指定;gcc加入连接参数-Wl,-rpath=/path/lib指定动态库搜索路径,多个路径之间用冒号分隔;
  2. 环境变量LD_LIBRARY_PATH指定路径;
  3. /etc/ld.so.cache中缓存的动态库路径。通过配置文件/etc/ld.so.conf增删路径(修改后需要运行ldconfig命令);最优雅的方式是在ld.so.conf.d目录下创建一个自己的程序依赖的配置文件,配置文件内容为程序依赖的动态库路径,一个路径一行;最后ldconfig更新配置文件;
  4. 默认动态库搜索路径/lib/, 如果是64位,还包括/lib64/、/usr/lib64/
  5. 默认动态库搜索路径/usr/lib/ (/usr/local/lib/和/usr/local/lib64/不在标准路径之列)

与动态库链接相关的知识点

  • ld是gcc的链接程序;
  • ldd是查看可执行文件中所依赖的库,eg:ldd a.out
  • ldconfig用来更新/etc/ld.so.conf文件;
  • 缓存文件/etc/ld.so.cahche,保存已经排好序的动态链接库名字列表
  • nm查看.so库中的函数名字,标记是T的就是动态库里面生成的名字;eg:nm -C test|grep static 在程序文本段查找static
  • 查看默认库文件路径:
    1
    2
    gcc --print-search-dir
    g++ --print-search-dir

ldconfig程序

很多软件包的安装程序在系统里安装了共享库以后都会调用ldconfig,因为需要这个程序做一下事情:

  1. 为共享库目录下的各个共享库创建,删除或更新相应的SO-NAME
  2. 收集共享库SO-NAME,集中存放在/etc/ld.so.cache,方便动态链接器查找共享库,而不用遍历所有的共享库目录。

装载调试

LD_PRELOAD——利用全局符号介入测试某些函数,动态链接器固定搜索路径之前会加载所有LD_PRELOAD中设置的共享库或目标文件,不管是否用到。

LD_DEBUG——打印装载过程的一些信息: LD_DEBUG=files(显示加载文件)/bindings(显示动态链接的符号绑定)/libs(显示共享库查找过程)/versions(显示符号的版本依赖)/reloc(显示重定位过程)/symbols(显示符号表查找过程)/statistics(显示动态链接过程统计信息)/all(显示以上所有信息)/help(显示可选值的帮助信息) xxx.out

可重定位表

ELF格式文件的重定位表格式:
r_offset: 重定位入口偏移, 对于可重定位文件是要修正的位置的第一个字节相对于段起始地址的偏移;对于可执行文件和共享对象文件其值是要修正的位置的第一个字节的虚拟地址.
r_onfo: 重入类型和符号, 低8位是入口类型, 高24位是符号表中的下标. 每个CPU都不同, 对于可执行文件和共享对象文件, 重定位入口是动态链接类型的.

通过objdump -r xxx.o 查看: 其中OFFSET就是r_offset,TYPE就是r_info的低8位,VALUE就是r_info的高24位

静态库与动态库

静态链接比动态链接的文件大小大很大,因为静态链接中每个程序的可执行文件本身内部都保留着诸如printf,scanf,strlen等标准库函数以及系统库等等(静态链接和动态链接的区别。动态链接时,可执行程序的内部的系统函数标准库函数这些都只是一个符号而已,没有实际内容,实际内容在动态库中,运行是加载动态库)

动态链接真正的链接过程是在装载时(运行前)进行的;静态是编译链接期间,生成了可执行文件装载前就链接好了.

动态链接是动态链接器ld.so进行链接,静态链接是ld链接器进行的

动态库装载优化: 延迟绑定, 即在函数第一次被用到的时候才进行绑定(即进行符号查找,重定位)

动态链接比静态链接慢的原因:

  1. 模块间的数据和函数调用要进行复杂的GOT定位,间接寻址。
  2. 动态链接器在程序开始时,寻找和装载共享对象,进行符号查找,重定位以解决模块之间的函数引用等。

装载

程序运行前需要进行装载操作以完成VMA映射

装载时重定位

共享对象的装载地址是不确定的,而可执行文件的装载地址是可以定的,因为他一般是第一个被装上的。

装载重定位: 因为共享对象需要在任意位置被加载,所以共享对象需要在装载时候进行重定位,根据最终装载到某个地址上了以后,再对指令中的那些对绝对地址的引用进行重定位到真正的绝对地址上。objdump -R xxx.so 可以看到共享对象so文件的重定位信息

缺点:对指令中的地址进行修正的话,共享库就失去了共享的意义了,因为指令代码中的地址跟具体某个装载地址有关系了。
而共享库在不同的程序的装载地址是任意的,不是同一个。

PIC

共享文件中的代码需要是地址无关代码.

通过fPIC解决共享文件中地址无关问题: 把代码指令中需要被修改的地方剥离出来放到数据部分,数据部分是每个进程单独一份副本的,不是共享的.

GOT是全局偏移表, 在模块中的指令引用就引用GOT的相对地址(这相当于模块内的数据访问),然后GOT对应于真正的外部模块的变量,对应关系等到真正装载的时候再进行修改,GOT就是一个指向这些外部变量的指针数组。

查看so文件的GOT位置: objdump -h xxx.so 查看GOT中内容: objdump -R xxx.so 其中标志GLOB_DAT的TYPE类型就是.

对于数据访问: 把模块间的数据访问通过GOT转为模块内的数据访问
对于函数访问: 把模块间调用通过GOT转为相对地址的访问.

PIC的DSO(动态库)是不会包含任何代码段的重定位表的地址(TEXTREF段)readelf -d xxx.so| grep TEXTREF有任何输出就说明xxx.so不是PIC的

PIE: 以地址无关方式(PIC)编译的可执行文件.

全局符号介入

linux下的动态链接器处理全局符号介入:当一个符号被加入全局符号表时,已存在同名的,则忽略后加入的, 全局对象在bss段. 对全局对象的访问会直接访问内存, 而跳过寄存器访问, 这样会有一定的性能损失.

查看链接信息

ldd xxx 查看ELF可执行文件或共享库so文件的依赖共享库有哪些. 其后面的地址是装载地址(VMA地址)

linux-vdso.so.1是一个内核虚拟共享对象,不存在于文件系统中.

GCC常用命令

  • GDB调试: gcc xxx.c -o xxx -g
  • 指定使用的函数库: gcc xxx.c -o xxx -lxxx -L/path/lib -I/path/include
  • 编译优化: gcc -O2 xxx.c -o xxx -O1 -O2 -O3 -O默认不优化
  • 设置编译警告为错误: gcc -Werror xxx.c -o xxx
  • 警告全开: gcc -Wall xxx.c
  • 设置宏: gcc test.c -o test -DTRUE 相等于在代码第一行定义 #define TRUE 1 -DXXX=XX
  • 设置标准语言: gcc -std=gnu99 (参考https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html#C-Dialect-Options)
  • gcc加入连接参数-Wl,-rpath=/path/lib指定动态库搜索路径,多个路径之间用冒号分隔, 等价于ld -rpath xxx
  • 无视编译顺序: gcc的ld要求被依赖的库放在依赖的库后面. 通过--start-group和--end-group可以使得这个依赖顺序被忽略.gcc -Wl,--start-group liba.a libb.a -Wl,--end-group
  • 默认情况下,对于未使用到的符号(函数是一种符号),链接器不会将它们链接进共享库和可执行程序。这个时候,可以启用链接参数--whole-archive来告诉链接器,将后面库中所有符号都链接进来,参数-no-whole-archive则是重置,以避免后面库的所有符号被链接进来。
  • -Wl,-Bstatic指定链接静态库,使用-Wl,-Bdynamic指定链接共享库
  • -Wl表示是传递给链接器ld的参数,而不是编译器gcc/g++的参数
  • --verbose参数是显示编译过程
  • gcc -static静态库链接方式编译, 默认是动态链接方式.
  • 链接的时候指定入口函数为nomain, 而不是默认的main: -e nomain ld的参数: -e ADDRESS, --entry ADDRESS Set start address
  • -shared表示产生共享对象
  • -fPIC表示Position-idependent Code技术 地址无关代码(-fpic 相比于-fPIC产生的代码较小。-fPIC在对硬件平台没有限制 -fpic有。-fPIC更加具有兼容性)
  • gcc -wl,-soname,xxxx 等价于 ld -soname xxx 链接名为libxxx的so
  • gcc -wl,-export-dynamic等价于ld -export-dynamic
  • gcc -wl,-s/S等价于ld -s/S:剔除符号信息

GCC错误处理

  • 编译时遇到错误: undefined reference to symbol 'dlclose@@GLIBC_2.2.5' 原因是没有链接libdl.so. 在linux下, ld-linux.solibdl.so是ld执行的依赖. 编译添加-ldl即可.
  • relocation R_X86_64_32 against '.rodata' can not be used when making a shared object; recompile with -fPIC如提示要求添加-fPIC即可

Glibc

包含libc C语言库, libm数学库, libld装载库等组件.

Linux命名规则推荐

libxxx.so.x.y.z
Glibc是例外. 采用: libc-x.y.z.so方式

CMAKE配置

查看cmake生成的配置:

查看include的配置等: CMakeFiles/xxxx.dir/flags.make

其中-isystem开头的位置代表系统位置, 可以通过include_directories(BEFORE SYSTEM /path/xxx/include)来将/path/xxx/include头目录位置放到其前面, 这样编译的时候就优先选择这里.

查看编译与链接的执行命令: CMakeFiles/xxxx.dir/link.txt

添加一个依赖lib目标name, 它从source文件中编译出

1
2
3
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[<source>...])
  1. STATIC: 用于静态链接
  2. SHARED: 用于动态链接和动态加载
  3. MODULE: 动态加载的模块

添加一个可执行文件

1
2
3
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])

添加编译器参数

add_compile_definitions(<definition> ...)

添加编译器预编译定义Flag

add_definitions(-DFOO -DBAR ...)

添加链接搜索目录

1
2
3
4
5
6
7
8
9
10
11
12
13
target_link_directories(<target> [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)

target_link_libraries(<target> ... <item>... ...)

link_directories([AFTER|BEFORE] directory1 [directory2 ...]) // 仅影响创建于其后的targets
link_libraries([item1 [item2 [...]]]
[[debug|optimized|general] <item>] ...) //影响整个编译文件及其子目录, 指定链接位置

The named must have been created by a command such as add_executable() or add_library() and must not be an ALIAS target.

添加链接头文件搜索目录

1
2
3
4
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

By using AFTER or BEFORE explicitly, you can select between appending and prepending, independent of the default.

PRIVATE and PUBLIC items will populate the INCLUDE_DIRECTORIES property of

查找依赖LIB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
find_library (
<VAR>
name | NAMES name1 [name2 ...] [NAMES_PER_DIR]
[HINTS [path | ENV var]... ]
[PATHS [path | ENV var]... ]
[PATH_SUFFIXES suffix1 [suffix2 ...]]
[DOC "cache documentation string"]
[NO_CACHE]
[REQUIRED]
[NO_DEFAULT_PATH]
[NO_PACKAGE_ROOT_PATH]
[NO_CMAKE_PATH]
[NO_CMAKE_ENVIRONMENT_PATH]
[NO_SYSTEM_ENVIRONMENT_PATH]
[NO_CMAKE_SYSTEM_PATH]
[CMAKE_FIND_ROOT_PATH_BOTH |
ONLY_CMAKE_FIND_ROOT_PATH |
NO_CMAKE_FIND_ROOT_PATH]
)

names 是指定库一个名称
REQUIRED 标记必须被找到

参考: