链接、装载与库笔记

目标文件🔗

目标文件的格式🔗

  • 可重定位文件 Relocatable File
    • Linux下的 .o文件
    • 一般包括目标文件与静态库
  • 可执行文件 Executable File
    • Linux下无拓展名,Windows下的 .exe
  • 共享目标文件 Shared Object File
    • Linux下的 .so文件,Windows下的 .dll文件
    • 可用于与可重定位文件组合生成目标文件
    • 也可与可执行文件组合,作为执行模块
  • 核心转储文件 Core Dump File
    • Linux下的core dump

目标文件的结构🔗

基本结构🔗

目标文件主要以下几个部分

  • 文件头
  • 各种段
  • 段表
  • 字符串表
  • 符号表
  • ...

常见段类型🔗

  • .text
    • 代码段,只读,用于存储代码
  • .data
    • 保存已初始化的全局静态变量和局部静态变量
    • 可读可写
  • .rodata
    • 只读数据段
    • 可存储如const等常数,或者字符串常量等
    • 也可用于存储ROM的一些内容
  • .bss
    • 存放未初始化的全局变量与局部静态变量
    • 该段并没有为储存的内容分配实际的物理空间,而是仅为其预留了虚拟地址的位置,仅记录了这些变量需要的内存空间
    • 未初始化的全局变量在链接前实际上没有存放于该处,而是持有一个COMMON块,用于处理强引用与弱引用等问题
    • 可执行文件中,该段不存在
  • .rodata1
    • 基本等同于.rodata
  • .comment
    • 储存编译器版本信息
  • .debug
    • 调试信息
  • .dynamic
    • 动态链接信息
  • .hash
    • 符号哈系表
  • .line
    • 调试时使用的行数表,用于将编译后指令与源码指令对应
  • .node
    • 额外的编译器信息
  • .strtab
    • 字符串表,用于储存ELF中用到的字符串
    • 储存形式很简单,即一段连续内存地址中存储一系列字符和'\0',给出一个地址,该地址到最近的'\0'即为该地址对应的字符串
    • 例:abcd\0ccc\0s\0,访问strtab[5]即可得到ccc
  • .symtab
    • 符号表,用于存储用到的各种符号
  • .shstrtab
    • 段名表,即所有段的段名储存于此处,可以由文件头直接找到该表
  • .plt .got
    • 动态链接的跳转表
  • .init .fini
    • 程序初始化与终结代码段
    • 在gcc编译链接过程中,一般不是设main函数地址为入口,而是设一个_start函数的地址为入口,用于main函数之前的初始化。.init则存储这些初始化代码。.fini则用于结束后的处理
    • 全局构造早于main函数,全局析构在main函数返回后,这些操作就由.init与.fini实现

Elf文件头的结构🔗

  • e_ident[16]:储存一些识别信息与系统信息
    • Magic:魔数
    • Class:ELF32/ELF64
    • Data
    • Version
    • OS/ABI
    • ABI Version
  • e_type:ELF文件类型
    • ET_REL:可重定向
    • ET_EXEC:可执行
    • ET_DYN:共享目标
  • e_machine:CPU平台属性
  • e_version:ELF版本号,为常数1
  • e_entry:入口地址,用于指定程序的入口函数地址
    • 可重定向文件的e_entry一般为0x0
    • 可执行文件的则由链接器设置
  • e_phoff:Start of program headers
  • e_shoff:段表的偏移地址,通过该偏移来访问段表
  • e_word:一些标志位
  • e_ehsize:文件头的大小
  • e_phentsize:Size of program headers
  • e_phnum:Number of program headers
  • e_shnum:段描述符数量,即段的数量
  • e_shstrndx:段表字符串表在段表中的下标
    • 段表字符串表为一个结构,用于储存段名等字符串
    • 该段表字符串表以段的形式存在,可以在段表中通过该下标访问

段表🔗

段表是ELF的重要结构,用于获取各个段的信息。可通过文件头提供的偏移进行访问 段表是一段连续内存地址,可以看作一个数组,数组的0位为NULL,从1开始储存每个位置存储一个段描述符,描述每个段的信息。

段描述符结构如下

  • sh_name 段名
    • 一个字符串,存储于.shstrtab段中,即段名字符串表中
  • sh_type 段类型
    • SHT_NULL 0 无效段,段表序号0处即为该类型
    • SHT_PROGBITS 1 程序段,包括代码段和数据段
    • SHT_SYMTAB 2 符号表
    • SHT_STRTAB 3 字符串表
    • SHT_RELA 4 重定位表,用于链接时某些符号的重定位
    • SHT_HASH 5 符号表的哈系表,
    • SHT_DYNAMIC 6 动态链接信息
    • SHT_NOTE 7 提示性信息
    • SHT_NOBITS 8 该段在文件中没有内容,如.bss段
    • SHT_REL 9 重定向信息
    • SHT_SHLIB 10 暂时保留
    • SHT_DNYSYM 11 动态链接符号表
  • sh_flags 段标志位
    • SHF_WRITE 1 表示可写
    • SHF_ALLOC 2 表示需要在进程空间中分配空间,一般不存在于一些指示性信息段;代码段,程序段,.bss段都有该标志
    • SHF_EXECINSTR 4 表示可执行,一般表示代码段
  • sh_addr 段虚拟地址
    • 若该段可加载,则储存加载后的进程虚拟地址空间
    • 否则为0
  • sh_offset 段偏移
    • 若段存在,则储存段在文件中的偏移
    • 否则无意义
  • sh_size 段长度
  • sh_link sh_info 段链接信息
    • sh_type SHT_DYNAMIC
      • sh_link 该段使用的字符串表在段表的下标
      • sh_info 0
    • sh_type SHT_HASH
      • sh_link 该段使用的符号表在段表中的下标
      • sh_info 0
    • sh_type SHT_REL SHT_RELA
      • sh_link 该段使用的符号表在段表中的下标
      • sh_info 该重定位表所作用的段在段表中的下标
  • sh_addralign 段地址对齐
  • sh_entsize 项的长度
    • 某些固定项的长度
    • 若无固定项,则为0

符号🔗

符号的类型🔗

  • 定义在本文件的全局符号
  • 在本文件中引用,但未在本文件定义的全局引用
  • 局部引用
  • 段名
  • 行号信息

符号表🔗

符号表为ELF中的一个段.symtab,结构为一个Elf32_Sym数组。数组序号0处为未定义符号

Elf32_Sym结构如下

  • st_name:符号名,用一个下标表示,用于对于字符串表中的字符串
  • st_value:符号值,不同类型的符号用拥有不同的意义
    • 若该符号不为COMMON块,则表示符号在该段的偏移
    • 若为COMMON快,则表示其对齐属性
    • 可执行文件中,表示符号的虚拟地址
  • st_size:符号大小
  • st_info:符号类型和绑定信息
    • 低四位表示符号类型,高28位表示符号绑定信息
    • 绑定信息
      • STB_LOCAL 0 局部符号
      • STB_GLOBAL 1 全局符号
      • STB_WEAK 2 弱引用
    • 符号类型
      • STT_NOTYPE 0 未知符号类型
      • STT_OBJECT 1 符号为数据对象,数组,变量等
      • STT_FUNC 2 符号为函数或可执行代码
      • STT_SECTION 3 该符号表示一个段,该符号一定是STB_LOCAL局部符号
      • STT_FILE 4 符号表示文件名,一般用来对应目标文件源文件名;该符号一定为局部符号,且其符号所在段一定为STN_ABS类型
  • st_other:无用
  • st_shndx:符号所在段
    • 在一般情况下,若该符号定义在本文件内,则表示该符号所对应段在段表中的下标
    • 否则
      • SHN_ABS 0xfff1 表示一个绝对的值,比如文件名
      • SHN_COMMOM 0xfff2 表示一个COMMON块,一般用于表示未初始化的全局符号
      • SHN_UNDEF 0 符号未定义,表示该符号在本文件中引用,在其他文件中定义

特殊符号🔗

某些特殊的符号被声明在ld的链接脚本里,若使用ld链接脚本则可以使用以下特殊符号 这些符号是由链接器解析,装载时定义的,链接后才有意义

  • __executable_start : 程序起始地址,不是入口地址
  • __etext:代码段结束地址
  • __data:数据段结束地址
  • __end:程序结束地址

extern "C"🔗

由于C与C++对符号的管理和修饰方式不同,在C++中,为了与C兼容,提供了extern "C",用于声明或定义一个C的符号

extern "C" {
	int func(int);
	int var;
}

extern "C" int func(int);
extern "C" int val

当C++需要使用C语言库时,由于符号管理与修饰方式不同,会无法正确链接到C语言库中的符号,需要使用extern "C";但C中不存在extern "C",为了同时兼容C和C++,C++在编译C++程序时会定义__cplusplus这个宏,可以根据这个宏为C和C++分别定义路径,或者选择是否使用extern "C"

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int , size_t);

#ifdef __cplusplus
}
#endif

强符号与弱符号🔗

强符号弱符号是定义全局符号时设定的一种性质;C/C++一般判断已初始化的全局变量为强符号,未初始化的全局变量为弱符号;强弱符号有以下性质

  • 强符号不允许被多次定义;若多次定义则报重复定义错误
  • 若某一符号的定义在一个文件中为强符号,在其他所有文件中为弱符号,则选择强符号的定义
  • 若某一符号在所有文件中的定义都为若符号,则选择其中占用空间最大的一个定义

强引用与弱引用🔗

强引用弱引用是使用一个引用时设定的一种性质 链接时,链接器会尝试需找该引用的定义

  • 若该引用为强引用,且未找到定义,则报错;
  • 若该引用为弱引用,且未找到定义,不报错,将该引用默认设置为0

弱引用可以用于库和模块的拓展和裁剪;若某个库可以添加或被裁剪,则可以把对该库的引用设为弱引用,然后根据该弱引用的值判断该库是否被链接;若存在,则调用该库,否则使用另一种方法

静态链接🔗

段合并策略🔗

可执行文件的段是由目标文件中的段合并而成,合成策略为相似段合并,即将相同类型 按序合并

符号地址的确定🔗

符号地址的确定是在段的虚拟地址确定后执行的操作。由于上面提到的段合并策略,每个符号在链接后对于段首的相对位置是确定的,所以只需要确定段的虚拟地址即可确定符号地址

重定位表🔗

重定位表实际上为一个段,若某个段需要重定位,则会存在一个对应的重定位表(.text段的重定位表为.rel.text)

所有需要被重定位的位置被称为重定位入口(Relocation entry),而重定位表则保存了对应段的重定位入口

重定位表为一个数组,其保存的结构体的结构如下

  • Elf32_Addr r_offset 存储需要修正的位置相对段首的偏移
  • ELF32_Word r_info 重定位入口的类型和符号,低8位表示入口的类型,高24位表示重定位符号在符号表中的下标

符号解析🔗

全局符号表🔗

在链接时,链接器会将所有输入的目标文件的符号表合并为一个全局符号表用于查询

符号重定位🔗

某些符号由于各种情况处于一个未定义的状态(定义在其他目标文件中的全局引用等),此时对于该符号存在一个相对应的重定位入口,通过该入口进行重定位时,则会去全局符号表中寻找该符号的目标地址,进行重定位

COMMON块🔗

common块的存在主要用于实现弱符号的功能;由于某个弱符号可能存在多个不同文件中的定义,在链接前无法确定使用哪种定义,所以使用COMMON块用于占位,仅存储定义的位长信息

在多符号定义时,存在以下三种情况

  • 存在多个强符号
  • 存在一个强符号,多个弱符号
  • 仅存在多个弱符号

第一种情况会直接报错,重复定义

第三种情况会选择多个弱符号中所需空间最大的定义

第二种情况则会选择强符号的定义;若某个弱符号的所需空间大于该强符号,则会报warning,因为该情况可能导致弱符号引用处的定义所需空间不足

未定义的全局变量与BSS段🔗

实际上,在链接前,未定义的全局变量并不存在于BSS段,而是以COMMON块的形式存在,因为链接前该变量的定义无法确定,不能确定其所占用空间;在链接后其所需空间确定,则放入BSS段,为其分配空间

C++相关🔗

重复代码消除🔗

由于C++语言特性,在编译过程中不可避免的会产生一定重复代码,比如

  • 模板,多个目标文件若使用相同类型的模板则会产生重复代码
  • 虚函数表
  • 外部内联函数
  • ...

一般的解决方案为,此类重复代码存储于一个独立的段,比如某一模板函数foo<T>(),可分别为其不同类型设置一个段,比如.temp.foo<int>.temp.foo<float>,在链接合并相似段时则可以消除重复代码

冗余代码消除🔗

由于在对某个库的某一功能进行调用时,需要将整个函数库都链接进来,这样会将大量用不到的函数与功能一并链接,造成冗余

VISUAL C++ 存在一种称为函数级别链接的编译选项,将所有的函数保存在一个单独的段中,这样在链接时就可以仅将需要的函数链接,其他的函数的段则抛弃;但这样也会降低编译和链接的效率,目标文件也会随着段的增多而膨胀

GCC 同样提供了类似的编译选项,分别为-ffunction-sections-fdata-sections,分别将函数和变量保存至独立段

C++全局构造与析构🔗

C++的全局构造先于main函数,全局析构在main函数之后;这些操作在Linux下一般由Glibc实现,在链接时设置程序入口为_start,并将初始化部分和结束后的处理进行链接,这些代码一般置于ELF文件中的.init.fini段中

静态库🔗

静态库可以看成一组目标文件的集合,比如/usr/lib/libc.a,libc.a为一个压缩文件,其中保存了一系列.o目标文件,ld链接器会根据引用解压需要的目标文件进行链接

在静态链接库中,常常出现一个目标文件中只有一个或几个函数,这样组织的原因是为了减少冗余代码,避免用户链接不需要用到的函数或模块,减小可执行文件的体积

链接控制脚本🔗

链接控制脚本用于控制链接的过程,在linux下进行链接就会调用默认的链接脚本;默认的链接脚本存储于/usr/lib/ldscripts中,ld会根据命令行参数选择合适的链接脚本,可以通过ld -T link.script来调用自定义链接控制脚本

链接控制脚本格式🔗

链接脚本分为两种语句,命令语句和赋值语句;有以下规定

  • 语句间使用 作为分隔,例外是命令语句可使用换行分割
  • 使用类似C的表达式与运算符,如 =, +, -, *, /, +=, >>=
  • 使用/* */ 作为注释

常用命令语句

  • ENTRY(symbol) 确定程序入口,即设置ELF文件头中的e_entry;ld设置入口的方式及优先级如下
    • ld命令行的 -e选项
    • 链接控制脚本的ENTRY
    • 如果定义了_start符号,则使用
    • 若存在.text段,则使用该段的第一字节地址
    • 使用0
  • STARTUP(filename) 将filename作为链接的第一个输入文件
  • SEARCH_DIR(path) 将path加入ld的链接库搜索路径,等同于命令行中的 -Lpath
  • INPUT(file file) 将file作为输入文件
  • PROVIDE(symbol) 在脚本中定义某一符号,使其可在程序中被引用
  • SECTIONS 链接脚本最核心的命令语句

SECTIONS命令🔗

/* SECTIONS命令基本格式 */

SECTIONS
{
	...
	secname : {contents}
	...
}
  • sections表示输出段名,其后必须有一个空格符用于区分段名
    • 由于a.out格式仅允许.text .data .bss这三个段名,若使用该格式,则不能使用其他段名
    • 有一个特殊的段名 /DISCARD/,即抛弃,用其作为输出名则代表输出被抛弃
  • contents用于匹配输入段,其内容即为匹配规则,链接脚本会根据规则选择对应段作为输入,举例如下
    • file1.o(.data) file1.o这个目标文件中的.data段
    • file1.o(.data .rodata) file1.o这个目标文件中的.data, .rodata段
    • file1.o file1.o这个目标文件中的所有段
    • *(.data) 所有目标文件中的.data段
    • [a-z](.text*[A-Z]) 正则,即所有a~z开头的目标文件中,以.text开头,A~Z结尾的段