深入理解计算机系统(五) —— 链接

参考书籍《深入理解计算机系统》
程序的编译过程见 深入理解计算机系统(四) ——优化程序性能 / 程序编译过程

1 目标文件

1.1 概念

目标文件是纯粹的字节块集合,这些块中有些包含程序代码,有些包含程序数据,有些包含指导链接器和加载器的数据结构,目标文件有三种形式:可重定位目标文件、可执行目标文件和共享目标文件。

1.2目标文件格式

各个系统之间,目标文件格式都不相同:

  • Windows使用的是COFF的一个变种,叫PE(可移植可执行)格式;
  • Unix系统使用的是Unix ELF(可执行可连接)格式。

1.3可重定位目标文件

包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。下图是一个典型的ELF格式的可重定位目标文件:
在这里插入图片描述

  • ELF头:ELF头以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统的字节顺序,ELF头剩下的部分包含帮助链接器解析和解释目标文件的信息,如ELF头大小、目标文件的类型、机器类型、节头部表的文件偏移以及节头部表中的表目大小和数量;
  • :夹在ELF头和节头部表之间,不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的表目。一个典型的ELF可重定位目标文件包含下面几个节:
 .text:已编译程序的机器代码
 .rodata:只读数据,如printf中的格式串、witch语句的跳转表
 .data:已初始化的全局C变量
 .bss:未初始化的全局C变量,在目标文件中这个节仅仅是一个占位符,不占实际空间
 .symtab:符号表,存放程序中被定义和引用的函数和全局变量信息
 .rel.text:保存代码重定位表目,当链接器把这个目标文件和其他文件结合时.text节中许多位置都需要修改
 .rel.data:保存数据重定位表目
 .debug:调试符号表,只有以-g选项调用编译驱动器程序才会得到这张表
 .line:源程序中的行号和.text节中机器指令之间的映射,只有以-g选项调用编译驱动器程序才会得到这张表
 .strtab:字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字,字符串表是一个以null结尾的字符串序列

1.4 可执行目标文件

包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。

1.5 共享目标文件

一种特殊的可重定位目标文件,可以在加载或运行时,被动态的加载到存储器并链接。

1.6 各个程序可生成的文件类型

  • 编译器:可重定位目标文件(包括共享目标文件)
  • 汇编器:可重定位目标文件(包括共享目标文件)
  • 链接器:生成可执行目标文件

1.7 符号和符号表

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

  • 全局符号:由该模块定义并能被其他模块引用,对应于非静态的函数以及被定义为不带static属性的全局变量
  • 外部符号:由其他模块定义并被该模块引用的全局符号,对应于定义在其他模块中的函数和变量
  • 本地符号:只被该模块定义和引用的,有的本地链接符号对应于带static属性的函数和全局变量,这些符号在该模块中的任何地方都是可见的,但不能被其他模块引用

2 链接

链接是将不同部分的代码和数据收集和组合成为一个单一文件的过程,链接可以执行于编译时(即将源代码翻译成机器代码时)、加载时(即程序被加载器加载到存器器并执行时)和运行时(由应用程序来加载),现代系统中链接是由链接器自动执行;

2.1 链接器

它是一个程序,在软件开发中因为链接器的存在使得分离编译成为了可能,理解链接器有如下好处:

  • 理解链接器如何解析引用有助于解决由于缺少模块、缺少库或者不兼容的库版本引起链接错误,有利于帮助程序员理解和构造大型程序
  • 有助于避免一些危险的编程错误,例如:Unix链接器在默认情况下,即使错误的定义了多个全局变量,也可以通过链接器但是不产生任何警告信息
  • 有助于理解语言的作用域规则是如何实现的,例如:全局和局部变量之间的区别
  • 有助于理解其他重要的系统概念,因为链接器产生的可执行目标文件在加载、运行程序、虚拟存储器、分页和存储器映射都扮演着关键角色
  • 有助于开发共享库

2.2 编译器驱动程序

大多数编译系统都提供编译驱动程序,它为用户根据需要调用语言预处理器、编译器、汇编器和链接器。

2.3 静态链接

  • 静态库:编译系统提供一种机制,将所有相关的目标模块打包为一个单独的文件,这个文件就成为静态库,它可以作为链接器的输入,静态库又称为存档文件
  • 静态库的链接:当链接器构造一个可执行文件时,只会拷贝静态库里被应用程序引用的目标模块,以Unix的ld程序为例,静态链接的过程是一组可重定位目标文件和命令行参数为输入,输出为一个完全链接的可以加载和运行的可执行目标文件
  • 步骤
    1.符号解析:将每个符号引用和一个符号定义联系起来
    2.重定位:编译器和汇编器生成从地址零开始的代码和数据节,链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储位置,从而重定位这些节

2.4 动态链接

  • 共享库:共享库是一个目标模块,在运行时可以加载到任意的存储器地址,并在存储器种和一个程序链接器起来,共享库又称为共享目标动态库
  • 动态链接器:动态链接是由动态链接器这个程序完成。
  • 链接方式
    1.应用程序被加载时链接共享库,此种方式需要在编译时链接共享库到程序
    2.运行时链接共享库,无需在编译时链接共享库库
  • PIC:PIC是一种与位置无关的代码,即编译不需要链接器修改库代码,就可以在任何地址加载和执行这些代码,用户使用-fPIC的编译选项即可生成PIC代码。PIC代码有性能缺陷,每个全局变量的引用需要五条指令而不是一条,并且还需要一个额外的对GOT的存储器引用和一个额外的寄存器来保持GOT表目地址。
  • PIC数据引用:编译器在编译共享库时为使PIC代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,并且与代码段和数据段的绝对存储器位置无关,在数据段开始的地方创建了一个全局偏移量表(GOT),GOT包含每个被目标模块引用的全局数据目标的表目,而且编译器还会为GOT中每个表目生成一个重定位记录。加载时动态链接器会重定位GOT的每个表目,使它包含正确的绝对地址。
  • PIC函数调用:ELF编译系统使用一种叫延迟绑定的技术,将过程地址的绑定推迟到第一次调用该过程时,故第一次调用过程的运行时开销很大。
  • 延迟绑定:延迟绑定是通过GOT和PLT(过程链接表)这两个数据结构之间的交互实现的。

2.5 符号解析

2.5.1概述

链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来,有本地符号引用和全局符号引用(包括外部符号)。

  • 本地符号解析和引用:因为编译器只允许每个模块中的每个本地符号只有一个定义,并且确保静态本地变量也会有本地链接器符号,拥有唯一的名字,所以本地符号的解析比较简单明了;
  • 全局符号的解析和引用:全局符号的解析和引用情况会比较复杂,如:
    1.相同的符号被多个目标文件定义,这种情况链接器常采用的策略是标志一个错误
    2.当编译器遇到一个外部符号时,它会假设该符号是在其他某个模块中定义,并生成一个链接器符号表表目,并把它交给链接器处理,如果链接器在它的任何输入模块中都找不到这个被引用的符号,它将会输出一条如下的错误信息并终止编译
    在这里插入图片描述
2.5.2 解析多处定义的全局符号
  • 强符号和弱符号:编译器输出每个全局符号给汇编器,会标识强(strong)或者弱(weak),汇编器再将这个信息隐含的编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号
  • 链接器处理规则
    1.不允许有多个强符号
    2.如果是一个强符号和多个弱符号,选择强符号
    3.如果是多个弱符号,那么从这些弱符号中任意选择一个
  • 说明:这种处理方式会有一些编码错误编译系统不会给出警告,通常在程序运行后才会表现出来,并且会远离错误发生地,如果怀疑有多定义的全局符号带来的错误时,可以用GCC-warn-common这样的选项调用链接器,这样在解析多定义的全局符号定义时,会输出一条警告信息
2.5.3 使用静态库来解析引用
  • 符号解析过程
    链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件,扫描过程链接器会维持一个可重定位目标文件的集合E、一个为解析符号集合U、一个在前面输入文件中已定义的符号集合D,此过程如下:
1.对于每个输入文件,链接器会判断是一个目标文件还是一个存档文件
2.如果是目标文件添加到E,修改U和D来反映该文件中的符号定义和引用
3.如果是存档文件,链接器将尝试匹配U中未解析的符号和由存档文件成员定义的符号,如果存在成员m定义了一个符号来解析U中的一个引用,则将m添加到E中,并修改U和D
4.重复步骤3直到U和D不再发生改变,丢弃不被包含在E中的成员
5.当链接器对所有输入文件完成上诉步骤,U集合非空,则链接器将输出错误并终止,否则将合并和重定位E中的文件,从而构建可执行文件
  • 说明
    从符号解析过程可知,库和目标文件被链接的顺序非常重要,如果在链接过程中一个符号的库出现在引用这个符号的目标文件之前,那么引用将不能被解析,链接会失败。另一方面,如果库之间不是相互独立的,则它们必须排序。

2.6 重定位

2.6.1 过程

链接器完成符号解析这一步之后,它将代码中的每个符号引用和确定的一个符合定义联系起来,在此时,链接器已经知道了输入目标模块中的代码节和数据节的确切大小,就可以开始重定位步骤了。重定位分为两步:

  • 重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节,然后将运行时存储器地址赋给新的聚合节、输入模块定义的每个节,以及输入模块定义的每个符号,这一步是使程序中的每个指令和全局变量都有唯一的运行时存储器地址了
  • 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址,这一步依赖于重定位标目的可重定位目标模块中的数据结构

2.6.2 重定位表目

  • 位置:汇编器工作时并不知道数据和代码的最终存储位置,也不知道该模块引用的外部定义的位置,故汇编器遇到对最终位置未知的目标引用,将会生成一个重定位表目,来告诉链接器将目标文件合并成可执行文件时如何修改这个引用,代码的重定位表目放在.rel.text中,已初始化数据的重定位表目放在.rel.data中
  • 格式:如下,offset为需要被修改的引用的字偏移,symbol标识被修改引用应该指向的符号,type告知链接器如何修改新的引用。
typedef strcut
{
	int offset;
	int symbol:24,
		type:8;
}Elf32_Rel;
  • 类型:ELF定义了11种不同的重定位类型,这里列举两种最基本的重定位类型:
    1.R_386_PC32:重定位一个使用32位PC相关的地址引用
    2.R_386_32:重定位一个使用32位绝对地址的引用

2.6.3 重定位符号引用

  • 重定位PC相对引用:重定位表目告诉链接器修改开始于offset处的相关引用,使它指向symbol的实际定义

refaddr = ADDR(s) + r.offset
要修改的引用的实际地址 = main函数实际地址 + 要修改的引用的相对地址
0x60d = 0x5fa + 0x13

  • 重定位PC绝对引用:没有那么多做差和偏移,直接将实际的func函数的地址赋值给操作数,但是一般用于外部变量数据的引用中

3 处理目标文件的工具

注:所有示例都在ubuntu环境下实现

  • ar:创建静态库,插入、删除、列出、和提取成员
//显示静态库的内容
$ ar -t libNmlMsg.a 
Common.o
NMLmsgExn.o
NMLmsgExn2.o
ControlCommandn.o
...
  • strings:列出一个目标文件中所有可打印的字符串
$ strings Common.o 
,$I9
+A:,$|
[]A\A]A^
ATUI
$wDH
...
  • strip:从目标文件中删除符号表信息
  • nm:列出一个目标文件的符号表中定义的符号
$ nm Common.o 
                 U access
                 U __assert_fail
                 U close
                 U closedir
                 U __cxa_allocate_exception
                 U __cxa_atexit
...
  • size:列出目标文件中节的名字和大小
$ size Common.o 
   text	   data	    bss	    dec	    hex	filename
  66271	   1464	     42	  67777	  108c1	Common.o
  • readelf:显示一个目标文件的完整结构,包括ELF头中编码的所有信息,包含size和nm的功能
//显示ELF文件头
$ readelf -h Common.o 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          177000 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         388
  Section header string table index: 387

  • objdump:显示一个目标文件中的所有信息,能反汇编.text节中的二进制指令
//显示目标文件的格式和支持的平台
objdump -i Common.o 
BFD header file version (GNU Binutils for Ubuntu) 2.30
elf64-x86-64
 (header little endian, data little endian)
  i386
elf32-i386
 (header little endian, data little endian)
  i386
...
  • ldd:列出一个可执行文件在运行时所需的共享库
$ ldd data_player 
	linux-vdso.so.1 (0x00007ffd28587000)
	libprotobuf.so.15 => /usr/local/lib/libprotobuf.so.15 (0x00007f31002f3000)
	libfastrtps.so.1 => /usr/local/lib/libfastrtps.so.1 (0x00007f30fff03000)
	libxscom.so => /usr/local/lib/libxscom.so (0x00007f30ff744000)
...
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值