0%

链接技术

Basic

  • 链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程。

  • 链接器必须完成的两个任务

    • 符号解析(symbol resolution):目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的是将每个符号的引用和正好和一个符号定义关联起来。
    • 重定位(relocation): 编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
  • 目标文件有三种形式

    • 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
    • 可执行目标文件
    • 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并拼接。

Tools

  • 对于一段程序我们只编译可以使用gcc -c filename命令。

  • readelf : 一款可以读取elf头的实用工具,一般 linux 装了gcc的都自带。

    • 实用方法:readelf [-option] [filename]
    • -h 查看各个节的基本信息
    • -s 查看符号表

可重定位目标文件

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字大小和字节顺序。

夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含以下几个节。

  • .text :已编译好的机器代码

  • .rodata: 只读数据

  • .data :已经初始化的全局和静态C变量。

  • .bss: 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占用实际的空间,它仅仅是个占位符。

  • .symtab:一个符号表,他存放在程序定义和引用的函数和全局变量信息。

  • .rel.text: 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

  • .rel.data: 被模块引用或定义的所有全局变量的重定位信息

  • .debug :

  • .line:

  • .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的名字,字符串表就是以null结尾的字符串序列。

符号解析

解析多重定义的全局符号

  • 强符号:函数和已经初始化的全局变量
  • 弱符号:未初始化的全局变量

Linux链接器使用下面的规则来处理多重定义的符号名

  1. 不允许有多个同名强符号
  2. 如果有一个强符号和多个弱符号同名,那么选择强符号
  3. 如果有多个弱符号同名,那么从这些弱符号中任意选择一个

静态库链接

在Linux中,静态库以一种称为存档的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员的目标文件的大小和位置。存档文件名由后缀.a标识

要创建静态库,我们要使用ar工具

1
2
gcc -c [file lists]
ar rcs [libname.a] [.o lists]

动态链接共享库

共享库(shared library)是致力于解决静态库缺陷的一个现代创新船坞。共享库是一个目标模块,在运行或加载时,可以加载到任意内容的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫动态链接器的程序来执行的。共享库也称共享目标(shared object),在Linux系统中通常用.so 后缀来表示。微软的操作系统大量使用了共享库,他们称为dll。

共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用他们的可执行的文件中。其次在内存中,一个共享库.text节的一个副本可以被不同的正在运行的进程共享。

共享库生成命令

1
gcc -shared -fpic -o [libname.so] [source code list]

-fpic选项指示编译器生成与位置无关的代码。-shared选项指示链接器创建一个共享的目标文件。一旦创建了这个库,随后就可以把它连接到其他程序。(比如我们有一个main.c 和一个创建好的共享库mylib.so)

1
gcc -o prog main.c ./mylib.so

从应用程序中加载和链接共享库

我们可以在程序运行时加载和链接共享库

1
2
#include <dlfcn.h>
void *dlopen(const char* filename,int flag); // NULL 出错,否则返回句柄

flag可以取

  • RTLD_NOW,告诉链接器立即解析对外部符号的引用

  • RTLD_LASY,告诉链接器推迟符号解析,直到执行来自库中代码。

  • 与以上两个相容的选项:RTLD_GLOBAL:如果当前可执行文件是带-rdynamic选项编译的,那么对于符号解析而言,他的全局符号也是可用的。

1
2
3
void *dlsym(void *handle,char *symbol); // 返回:成功返回符号指针,出错返回NULL
int dlclose(void *handle); //返回:若成功则为0,出错-1,如果没有其他共享库还在使用这个共享库,dlclose函数就会卸载该共享库。
const char* dlerror(void); // 返回如果前面对dlopen,dlsysm,dlclose 的调用失败,则为错误信息,成功则为NULL。

如果我们使用到这些接口,我们需要在编译选项中加入 -ldl

1
gcc -o prog main.c -ldl
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
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

int (*mul_array)(int *, int);

int main()
{
void *handle = dlopen("./mylib.so", RTLD_LAZY);

if (handle == NULL)
{
printf("open shared object failed: %s\n", dlerror());
exit(1);
}

mul_array = dlsym(handle, "mul_array");

if (mul_array == NULL)
{
printf("open shared object symbol failed: %s\n", dlerror());
exit(1);
}

int arr[] = {1, 2, 3, 4};

printf("%d ", mul_array(arr, 4));

dlclose(handle);
return 0;
}

位置无关代码

可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code, PIC)。用户对gcc使用-fpic 只是GNU编译器生成pic代码。共享库的编译总是使用该选项。

  1. PIC的引用

    编译器通过运用一下有趣的事实来生成对全局变量的PIC引用:无论我们内存中的何处加载一个目标模块(包括共享目标模块),数据段与代码段的距离总是保持不变。

    想要生成对全局变量PIC引用的编译器利用了这个事实,他在数据段开始的地方创建了一个表,叫做全局偏移量表(Global Offset Table,GOT)。在GOT中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个8字节的条目。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确绝对地址。每个引用全局目标的目标模块都有自己的GOT。

  2. PIC函数的调用

    假设程序调用一个由共享库定义的函数。编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以家加载到任意位置。GNU编译系统使用了一种很有趣的技术来解决这个问题,称为延时绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。

    延时绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的,这两个数据结构是:GOT和过程链接表(PLT),GOT在数据段(可以修改),PLT在代码段(不可以修改)。

    • PLT:PLT是一个数组,其中每个条目是16个字节,PLT[0]是一个特殊的条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数,它初始化执行环境,调用main函数并处理其返回值。从PLT[2]开始的条目调用用户代码调用的函数。
    • GOT:GOT是一个数组,其中每个条目是8个字节地址。和PLT联合使用,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]时动态链接器在ld-linux.so 模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。初始时,每个GOT条目都对应PLT条目的第二个指令