【深入理解计算机系统】第七章 链接

1、链接的流程

  • .cpp文件经过预处理器 编译成 .i文件。这个过程主要将头文件插入到程序文本中,并且进行宏替换。
  • .i文件经过编译器 编译成.s文件。这个过程主要将高级语言编译成汇编语言。
  • .s文件经过汇编器 编译成.o文件。这是一个二进制文件,即为可重定位的目标程序。
  • .o文件经过了链接器 编译成.exe或者.out文件,是一个可执行程序,这个过程中将库文件、代码等链接在一起。

2、目标文件

目标文件的三种形式;

  • 可重定位的目标文件。包含二进制代码和数据,在编译时可以与其他可重定位的目标文件合并成可执行的目标文件 (.o)
  • 可执行的目标文件。包含二进制代码和数据,可直接被复制到内存并执行
  • 共享目标文件。可以在加载或者运行时被动态的加载进内存并链接

2.1、可重定位的目标文件

linux中采用可执行可连接格式的文件ELF表示目标文件,下图为可重定位的目标文件的条目表示:
【深入理解计算机系统】第七章 链接_第1张图片

.text:已编译程序的机器代码

.rodata:只读数据

.data:已初始化的全局和静态变量

.bss:未初始化的全局和静态变量

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

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

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

.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及C程序源文件,编译时使用 -g选项才能获得这个表

.line:C源程序中的行号和.text节 中机器指令之间的映射,只有以-g选项调用编译器驱动程序时才能得到这张表

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

2.1.1、符号和符号表

每个可重定位目标模块m都有一个符号表,它包含模块m定义和引用的符号信息。在链接器的上下文有三种不同的符号:

  • 模块m定义并能被其他模块引用的全局符号。对应于非静态的函数和全局变量
  • 其他模块定义并被模块m引用的全局符号,即外部符号
  • 只能被模块m定义和引用的局部符号,对应于带static属性的函数和全局变量

注意:

  • static可以隐藏全局变量和函数名字,使其只对本模块或者本文件可见。

  • 局部变量被栈管理,链接器对此类符号不感兴趣

  • 不同模块的同名静态局部变量,编译器会向汇编器输出两个不同名字局部链接器符号-【深入理解计算机系统】第七章 链接_第2张图片
    上图为.symtab中的符号表,每个符号都会被分配到目标文件的某个节中,由section字段表示,每个节在节头部表中都有一个索引,用于表示该节。有三个伪节,在节头部表中没有条目:

  • ABS代表不该被重定位的符号

  • UNDEF代表未定义的符号,即在本目标模块中引用,但在其他地方定义的符号

  • COMMON表示还未被分配位置的未初始化的数据目标

只有可重定位的目标文件才存在伪节,可执行的目标文件没有。其中COMMON和.bss的区别:

COMMON 未初始化的全局变量
.bss 未初始化的静态变量,初始化为0的全局或静态变量

编译器翻译到某个模块时,遇到一个弱全局符号,不知道其他模块是否也定义了该符号,所以编译器把该符号分配至COMMON,把符号的解释权留给链接器。静态符号的构造是唯一的,所以可以分配至 .data 或 .bss

2.1.2、链接器解析符号

链接器解析符号引用的方法是将引用与他输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对于全局变量,如歌符号定义不明确,则会报错。重载函数例外,因为C++会将参数信息添加至符号中。

多重定义的全局符号

编译时,编译器向汇编器输出每个全局符号(强符号和弱符号),汇编器隐含地编码至可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

根据强弱符号定义,Linux链接器处理多重定义的符号名的规则如下:

  • 不允许有多个同名的强符号
  • 如果有一个强符号和多个弱符号同名,那么选择强符号
  • 如果有多个弱符号同名,那么选择任意一个

可以使用 -fno-common 编译选项,编译时,遇到多重定义符号,则触发一个错误

使用静态库解析引用

未定义的符号集合U,已定义的符号集合D,重定位的符号集合E:

  • 对于输入文件 f, 链接器会判断 f 是一个目标文件还是一个存档文件,如果 f 是一个目标文件,那么链接器会把 f 添加到 E,修改 U 和 D 来反映 f 中的符号定义和引用
  • 如果 f 是一个存档文件,链接器就尝试匹配 U 和 D。
  • 如果链接器完成对输入文件的扫描会,U非空,则报错

静态库的放置顺序至关重要,如果库之间不存在依赖,那么放置在结尾即可(链接器从左只有扫描输入文件),如果存在依赖,被依赖的库放在最后,例如 foo.c 调用了 liby.a 和 libz.a, 这两个库又调用了 libx.a 则:

gcc foo.c liby.a libz.a libx.a

如果 foo.c 调用了 libx.a, libx.a 调用了 liby.a,而 liby.a 又调用了 libx.a ,这种情况比较极端,:

gcc foo.c libx.a liby.a libx.a

重定位

链接器完成符号解析之后,进入重定位步骤,在这个步骤将合并模块,为每个符号分配运行时地址:

  • 重定位节和符号定义,链接器将所有相同类型的节合并为同一个类型的新的聚合节至可执行目标文件(例如将所有可重定位节的.data聚合为一个.data节),这一步完成时,程序中的每条指令和全局变量就有唯一的运行时内存地址
  • 重定位节中的符号引用。这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。执行这一步链接器需要可重定位目标模块中的重定位条目

重定位条目

当汇报器生成一个目标模块时,数据和代码最终在内存中位置是未知的,对于最终位置未知的目标引用,它会生成一个重定位条目,告诉链接器在合并成可执行文件时修改这个引用。代码的可重定位条目在 .rel.text 中,已初始化数据的重定位条目在 .rel.data 中。
【深入理解计算机系统】第七章 链接_第3张图片

offset:被修改的引用的节偏移

symbol:被修改的引用指向的符号

type:如何修改新的引用

addend:符号常数,一些类型重定位需要使用它对被修改引用的值做偏移调整

基本的重定位类型:

  • R_X86_64_PC32。重定位一个使用32为PC(程序计数器)相对地址(距离PC当前运行时值的偏移量)的引用。当CPU执行一条PC相对寻址的指令时,它将在指令中编码的32位值加上当前PC运行时值,即下一条指令在内存中的地址
  • R_X86_64_32。重定位一个使用32位绝对地址的引用。CPU直接使用指令中编码的32位值作为有效地址

这两种类型都支持小型代码模型,大于2GB的程序可以用-mcmodel=medium / large 标志来编译。

2.2、可执行目标文件

【深入理解计算机系统】第七章 链接_第4张图片

  • 它包含一个入口点,即第一条指令的地址
  • 其中.text 、.rodata、.data已经被重定位到运行时的内存地址
  • .init 定义了一个小函数,程序初始化代码会调用它。
  • 因为可执行文件是完全链接的,所有没有 .rel 节

可执行文件可以被加载到内存,下图为一个程序头部表,其中起始地址 vaddr 满足 v a d d r % a l i g n = o f f % a l i g n vaddr \% align = off \% align vaddr%align=off%align
【深入理解计算机系统】第七章 链接_第5张图片

2.2.1、加载可执行文件

linux x86-64系统中,代码段从地址0x400000处开始,在程序头部表引导下,加载器将可执行文件的片复制到代码段和数据段,接下来加载器跳转到程序的入口点(_start函数的地址,这个函数在系统目标文件ctrl.o中定义),_start函数调用启动函数__libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并在需要时把控制返回给内核。
【深入理解计算机系统】第七章 链接_第6张图片

注意:地址空间布局随机化(ASLR)可以帮助克服某些类型的缓冲区溢出攻击,ASLR可以将基数,库,堆和堆栈放在进程地址空间中的任意随机位置,这使攻击程序很难预测下一条指令的内存地址。

2.2.1、动态库

静态库更新时需要重新编译,动态库的出现解决了这一问题,在程序运行时可以被动态链接,该过程由动态链接器执行。
【深入理解计算机系统】第七章 链接_第7张图片

可执行目标文件包含一个.interp节,其包含了动态链接器的路径名,动态链接器本身就是一个共享目标(Linux系统中的ld-linux.so)。加载器会加载和运行这个动态链接器,然后动态链接器执行重定位:

  • 重定位libc.so的文本和数据到某个内存段
  • 重定位libvertor.so的文本和数据到另一个内存段
  • 重定位prog21中的所有对由libc.so和libvector.so定义的符号的引用

此时共享库的位置就固定了。Linux提供了 dlopen 函数运行程序在运行时加载和链接共享库
在这里插入图片描述
flag 包括:

RTLD_GLOBAL:库中的解析的定义变量在随后的其它的链接库中变得可以使用。

RTLD_NOW:链接器立即解析对外部符号的引用

RTLD_LAZY:链接器推迟符号解析知道执行来自库中的代码

在这里插入图片描述

dlsym输入参数:已经打开的共享库的句柄和一个symbol的名字,如果符号存在就返回地址,否则返回null
在这里插入图片描述

关闭共享库
【深入理解计算机系统】第七章 链接_第8张图片

返回一个字符串,表示以上函数最近发生的错误,没有错误就返回null

PIC数据引用

无论在内存何处加载一个目标模块(包括共享目标模块),其数据段和代码段的距离总是保持不变,因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。为了实现-fpic生成与位置无关的代码,在数据段开始的地方创建一个全局偏移量表(GOT),在GOT中,每个被这个目标模块引用的全局数据目标都有一个 8 字节的条目。编译器还为GOT中的每个条目生成一个重定位记录。加载时,动态链接器会重定位GOT中的每个条目。

PIC函数调用

GNU通过延迟绑定完成PIC函数的调用,将过程地址的绑定推迟到第一次调用时,第一次的调用过程的运行开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用。延迟绑定需要:GOT和过程链接表(PLT),如果一个目标模块调用定义在共享库中的函数,那么它就由自己的GOT和PLT,GOT是数据段的一部分,PLT是代码段的一部分。

  • PLT是一个数组,其每个条目是16字节的代码,PLT[0]是一个特殊的条目,它跳转到动态链接器,每个被可执行程序调用的库函数都有自己PLT条目,每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数(__libc_start_main),它初始化执行环境,从PLT[2]开始的条目调用用户代码调用的函数。
  • GOT是一个数组,其中每个条目是8字节的地址,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在 ld_linux.so 模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址在运行时解析。每个条目都有一个匹配的PLT条目。初始时,每个GOT条目都指向对于PLT条目的第二条指令。 【深入理解计算机系统】第七章 链接_第9张图片

过程:

  1. 程序进入PLT[2],这是addvec的条目
  2. 通过GOT[4]进行间接跳转,这个跳转只是简单地把控制传送回PLT[2]中的下一条指令
  3. 把addvec的ID(0x1)压入栈中,PLT[2]跳转到PLT[0]
  4. PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳到动态链接器,动态链接器使用两个栈条目确定addvec运行时的位置,并重写GOT[4],再把控制传递给addvec

对于链接后的代码,程序会先跳到PLT[2],后面通过GOT[4]间接跳转将控制转移到addvec

3、库打桩

库打桩:它允许截获对共享库函数的调用,取而代之执行自己的代码。该机制可以追踪某个库函数的调用次数,验证和追踪他的输入和输出值,甚至替换成一个完全不同的实现

基本思想:给定一个目标函数,创建一个包装函数,他的原型和目标函数完全一样,使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数。

打桩可以发生在编译时、链接时、程序加载和运行时。

编译时打桩

【深入理解计算机系统】第七章 链接_第10张图片

gcc -DCOMPILETIME -c mymalloc.c
gcc -I. -o intc int.c mymalloc.o

本地的malloc.h使用预处理器将包装函数替换掉了目标函数,加入-I. 参数预处理器会先搜索当前目录下的malloc.h头文件,打桩完成

链接时打桩

【深入理解计算机系统】第七章 链接_第11张图片
Linux静态链接器支持使用 –wrp f 标志进行链接时打桩,即对符号 f 的引用解析成 __wrap_f,还有把对符号__real_f 的引用解析成 f。

gcc -DLINKTIME -c mymalloc.c
gcc -c int.c
 gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o

-Wl,option标志把option传递给链接器,option中的每个逗号替换成一个空格。

运行时打桩

如果能访问可执行目标文件就可以进行运行时打桩,该机制基于动态链接器的LD_PRELOAD环境变量实现。如果该变量设置成一个共享库的路径名的列表(以空格或者逗号分隔),那么加载执行一个程序时,需要解析未定义的符号引用,会先访问LD_PRELOAD中的库。该机制甚至可以对libc.so进行打桩。
【深入理解计算机系统】第七章 链接_第12张图片

dlsym的参数 RTLD_NEXT 可以在对函数实现所在动态库名称未知的情况下完成对库函数的替代,上述源代码中的 malloc 函数中的 fput 会调用 malloc 这样就陷入了递归循环,导致程序运行失败,需要做出以下修改:

static int hooked = 0;
void *(*mallocp)(size_t size);

void *malloc(size_t size){
    if (hooked && mallocp) {
        return mallocp(size);
    }

    hooked = 1;
    char *error;

    mallocp = dlsym(RTLD_NEXT, "malloc");
    if((error = dlerror()) != NULL){
        fputs(error, stderr);
        exit(1);
    }

    char *ptr = mallocp(size);
    printf("malloc(%d)=%p\n", (int)size, ptr);
    hooked = 0;
    return ptr;
}

使用以下命令编译执行即可。

gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
gcc -o intr int.c
LD_PRELOAD="./mymalloc.so" ./intr

处理目标文件的工具

AR:创建静态库,插入、删除、列出和提取成员
STRINGS:列出一个目标文件可以打印的字符串
STRIP:从目标中删除符号表信息
NM:列出一个目标文件的符号表中定义的符号
SIZE:列出目标文件中节的名字和大小
READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息,包含SIZE和NM的功能
OBJDUMP:反汇编
LDD:列出一个可执行文件在运行时所需的共享库

你可能感兴趣的:(书籍阅读,系统架构)