第六章 进行链接

22 阅读6分钟

6.1相关准备

6.2 掌握正确的连接方式

下图为编译器使用ELF格式构造图像文件:

tmp6BA3.png

编译器给每个CPP文件准备一个目标文件(.o文件),用于构建程序的内存映像。(image和container)

object文件各部分详解

·ELF头文件标识操作系统、ELF文件类型、目标指令集体系结构、程序头表(不在程序中)和节头表的位置和大小 ·按类型分组的信息 ·节头表,包含关于名称、类型、标志、内存中的目标地址、文件中的偏移量和其他在线那个信息,类似于目录。

其中按类型分组的信息具体如下:

.text区段:机器代码,包含处理器要执行的所有指令 .data区段:初始化的全局对象和静态对象的所有值 .bss区段:未初始化的全局对象和静态对象的所有值,这些对象将在程序启动时初始化为0. .rodata区段:常量的所有值(只读数据) .strtab区段:一个字符串表,包含所有常量字符串 (字符串字面量) .shstrtab区段:包含所有部分名称的字符串表

tmp3C27.png

所有的.o文件通过重定位,将不同.o文件的相同区段组合在一起。这样就完成了重定位。

之后,链接器还需要解析引用。在编译过程中,一个翻译单元引用另一个翻译单元中定义的符号时,编译器读取这个声明并认为定义在某个地方,稍后提供定义。解析引用即链接器收集这些外部符号的未解析引用,查找并填充到可执行文件中对应的地址。示例如下:

tmpAE5E.png

最终形成的可执行文件结构如下:

tmp92FE.png

系统加载器会读取程序头,用以创建进程映像。头部包含一些通用信息、内存布局描述。布局中每个条目代表一个称为段的内存片段,条目指定将读取哪些节、以什么顺序、虚拟内存中的哪个地址、标志是什么等。

6.3 构建不同类型的库

静态库在类Unix系统上的后缀为.a,在windows上的后缀为.lib 动态库在类unix系统上的后缀为.so,在windows上的后缀为.dll

6.3.1 静态库

创建一个静态库,使用如下命令: add_library(<name> [<source>...]) 如果BUILD_SHARED_LIBS变量没有设置为ON,上述代码将生成静态库。 也可以通过提供关键字STATIC构建静态库 add_library(<name> [<source>...]) 静态库本质上是原始对象文件的集合。

6.3.2 动态库

创建一个动态库,使用如下命令: add_library(<name> SHARED [<name>... ]) 也可以通过设置BUILD_SHARED_LIBSON实现。

区别于静态库,动态库是使用链接器构建的,将执行链接的两个阶段。 动态库(也称为共享对象)可以在多个不同的应用程序之间共享。操作系统会加载一个该库的实例,系统为所有使用该库的程序提供相同的地址。只有.data段和.bss段分别为使用库的每个进程创建。

6.3.3 模块库

构建模块库,需要使用MODULE关键字。 add_library(<name> MODULE [<souce>...])

模块库是动态库的一种,目的是作为运行时加载的插件使用,而不是在编译的过程中链接到可执行文件。共享模块不会在程序启动时自动加载(与普通的共享库一样),只有程序通过LoadLibrary或dlopen()/dlsym()等系统调用显示地请求它时,才会使用到。

6.3.4 位置无关的代码

所有共享库和模块的源代码都应该在编译时启用位置无关代码标志。

CMake检查目标的POSITION_INDEPENDENT_CODE属性,并适当添加特定于编译器的编译标志,如gcc或clang的-fPIC

。。。

在实际的过程中,我们需要注意,当动态库链接到另一个目标时,例如静态库或对象库,目标也需要设置POSTION_INDEPENDENT_CODE属性。方法如下: set_target_properties(dependency_target PROPERTIES POSITION_INDEPENDENT_CODE ON)

6.4用定义规则解决问题

ODR,单一定义规则。

如果类型模板extern内联函数完全相同,可以在多个翻译单元中重复其定义。

// odr.h
int i;

// odr.cpp
#include "odr.h"
int main(){
    std::cout << "i:" << i <<std::endl;
    return 0;

}

// two.cpp
#include "odr.h"

// CMakeLists.txt

add_executable(odr odr.cpp two.cpp)

编译时会报multiple definition错误。

如果将odr.h文件中的i进行修改如下:

// odr.h

struct odr{
    static inline int i = 1;
}

或者 

static int i = 1;

即可,这属于odr的例外。

6.4.1 动态链接的重复符号

ODR规则对静态库的作用和对目标文件的作用完全相同,但是当用动态库构建代码时,链接器允许复制符号。

// a.cpp
#include <iostream>
void a(){std::cout << "A" << std::endl;}
void duplicated(){std::cout << "duplicated A" << std::endl;}
// b.cpp
#include <iostream>
void a(){std::cout << "B" << std::endl;}
void duplicated(){std::cout << "duplicated B" << std::endl;}
// main.cpp
extern void a();
extern void b();
extern void duplicated();
int main(){
    a();
    b();
    duplicated();
    return 0;
}
// CMakeLists.txt

add_library(a SHARED a.cpp)
add_library(b SHARED b.cpp)

add_executable(main_1 main.cpp)
target_link_libraryies(main_1 a b)

add_executable(main_2 main.cpp)
target_link_libraryies(main_2 b a)

执行main_1输出:

A
B
duplicated A

执行main_2输出:

A
B
duplicated B

如果在main.cpp中添加:

// main.cpp

void duplicated(){
    std::cout << "duplicated MAIN" << std::endl;
}

执行目标时会输出"duplicated MAIN

6.4.2 使用命名空间——不要依赖链接器

建议将库代码包装在以库命名的命名空间中。!

6.5 连接顺序和未定义符号

// outer.cpp
extern int a;
int b = a;
// nested.cpp
int a = 123;
// main.cpp
#include <iostream>
int main(){
    std::cout << "output a: " << a << std::endl;
    return 0;
}
add_library(outer outer.cpp)
add_library(nested nested.cpp)

add_executable(main main.cpp)
target_link_libraries(main nested outer)

链接器解析未定义符号的工作方式是这样的:链接器从左向右处理二进制文件。对每个二进制文件执行如下操作: 1、收集此二进制文件导出的所有未定义符号,并存储起来以备以后使用。 2、尝试用此二进制文件中定义的符号解析未定义的符号。 3、对下一个文件重复此过程。 执行完上述操作之后,若仍有符号未定义,则链接失败。

target_link_libraries(main nested outer)

在上述例子中:

1、主要处理main.o,获得对a的未定义引用,收集起来用于将来的解析。 2、处理libnested.a,没有发现未定义的引用,因此没什么需要解析的。 3、处理libouter.a,得到了对b的未定义引用,本身定义了变量a,因此解析了对A的引用。

现在我们了解到了未定义引用的处理顺序之后,就能知道了引起问题的关键所在了。我们只需要改变库的链接顺序即可。

nested库和outer库的顺序调整即可:target_link_libraries(main outer nested) 即可。

6.6分离main()进行测试

主要思路是,将main函数的执行逻辑全部提取到一个外部函数。通过调用这个外部函数,实现测试和生产环境运行完全相同的源代码的情况。

// main.cpp
// 主程序
extern int start_program(int argc, const char**);
int main(int argc, char** argv){
    return start_program(argc, argv);
}
// test.cpp
// 测试逻辑
extern int start_program(int, const char**);
int main(){
    auto exit_code = start_program(0, nullptr);
    if(exit_code == 0){
        std::cout << "Non-zero exit code expected." << std::endl;
    }
    const char* argumens[2] {"hello", "world"};
    exit_code = start_program(2, arguments);
    if(exit_code != 0){
        std::cout << "Zero exit code expected." << std::endl;
    }
    return 0;
}
// program.cpp
// 主程序逻辑
int start_program(int argc, const char** argv){
    if(argc <= 1){
        std::cout << "Not enough arguments" << std::endl;
        return 1;
    }
    return 0;
}