『operating system-1』static link
静态链接
一、编译与链接
GCC编译过程分解:
预编译:处理源代码文件中以“#”开头的预编译指令
- 展开所有的宏定义 #define
- 处理所有的条件预编译指令,如 #if、#ifdef、#elif、#endif 等
- 递归处理预编译指令 #include,将被包含文件插入到预编译指令的位置
- 删除所有注释
- 添加行号信息和文件名标识,便于编译器产生调试所用的行号信息
- 保留所有的编译器指令 #pragma(编译器之后会使用这些指令)
编译:对预处理的文件进行词法分析、语法分析、语义分析、指令优化等操作,转化为汇编代码
Linux下C语言的编译程序是cc1、C++是cc1plus、Java是jc1
注意:gcc只是多个后台命令的包装,会根据传入参数调用编译程序cc1,汇编器as或链接器ld
汇编:将汇编代码转变为机器可执行的命令(即翻译)
链接:将每个源代码模块独立地编译,再将各模块正确地衔接
静态链接:地址和空间分配 + 符号决议 + 重定位
- 重定位:链接时将跨模块调用的地址补充完整(填补之前地址一般为0)
二、目标文件
可执行文件格式:Portable Executable (Windows) + Executable Linkable Format (ELF)
动态链接库(DLL)和静态链接库(SLL)也可以按可执行文件格式存储
ELF文件格式 描述 实例 可重定位文件 包含代码和数据,可链接成可执行文件 Linux的.o & Windows的.obj 可执行文件 可以直接执行的程序 Linux的/bin/bash & Windows的.exe 共享目标文件 能与其它可重定位文件链接成新的目标文件,或与其它可执行文件链接成新的进程映像文件 Linux的.so & Windows的DLL 核心转储文件 进程意外终止时可将其地址空间内容转储到核心转储文件 Linux下的core dump 目标文件格式:按文件信息的不同属性分为“段”或“节”(表示一定长度)
ELF文件头:描述了整个文件的信息,以及段表
代码段(.code & .text):存放编译后的机器指令
数据段(.data):存放初始化的全局变量和局部静态变量
只读数据段(.rodata):存放只读数据(如const修饰的变量或字符串常量)
.bss段:预留未初始化的全局变量和局部静态变量的位置(本身无数据)
.comment段:存放编译器版本信息
.dynamic段:动态链接信息
...
注意:段名都以“.”作为前缀,表示系统保留字;应用程序的自定义段名不能用“.”作为前缀
文件分段的好处:为何要将指令(.text + .code)和数据段(.data)分离?
- 指令一般是只读的,数据一般是可写的,便于分别设置权限
- 现代CPU一般都有指令cache和数据cache,将指令和数据分离有助于提高缓存命中率
- 作为只读区域,指令便于共享,可以节省空间
自定义段:可以指定变量所处的段,如利用gcc扩展机制,可将变量var放入自定义的name段
1
2__attribute__((section("name"))) int var = 1;
__attribute__((section("func"))) void foo() {}ELF文件结构:可使用binutils中的readelf命令查看ELF文件
文件头(Elf32_Ehdr):包括ELF魔数、机器字节长度、运行平台,程序入口与长度等信息
ELF魔数(16位):前四个字节相同,为0x7f、0x45、0x4c、0x46,分别表示DEL、E、L、F
第5个字节标识文件类型:0x01表示32位、0x02表示64位
第6个字节标识字节序:0x01表示小端、0x02表示大端
文件类型(e_type):ET_REL指可重定位、ET_EXEC指可执行、ET_DTN指共享目标文件
机器类型(e_machine):如EM_386表示文件可以在Intel x86机器上运行
注意:不同机器上的ELF文件都遵循同一套ELF标准
程序虚拟入口(e_entry):进程开始执行的指令虚拟地址;可重定位文件的入口一般设0
...
注意:ELF头中使用了typedef定义了自定义变量体系,使得在不同编译环境下都有相同的字段长度
如Elf32_Addr类型表示原始的uint32_t类型(4字节);Elf32_Half表示原始的uint16_t类型(2字节)
段表:保存各段的基本属性(段名、段长、段偏移等),ELF头的e_shoff指明了段表在ELF文件的位置
段表本质上是一个以Elf32_Shdr结构体为元素的数组;Elf32_Shdr结构称作段描述符,对应了一个段
ELF头的e_shnum表示ELF中拥有的段数量,即段表内的元素个数
段描述符:即Elf32_Shdr结构体
段名(sh_name):一个指向“.shstrtab”字符串表的偏移量(注意是段表字符串表)
段类型(sh_type):以关键字“SHT”开头,如代码段和数据段的类型都是SHT_PROGBITS .hash段的段类型为SHT_HASH;.bss段的段类型为SHT_NOBITS等
注意:段名(像.text)并不能真正表示段的类型
段的标志位(sh_flag):以关键字“SHF”开头,表示段在进程虚拟地址空间中的属性 SHF_WRITE指该段可写、SHF_ALLOC指需要在进程地址空间中为该段分配空间(如.data、.text、.bss)、SHF_EXECINSTR指可执行代码(如.text)
注意:只有段类型和段标志位对操作系统是有意义的,段名只对编译器和链接器有意义
段的链接信息(sh_link、sh_info):标明与链接相关的段的信息,如SH_REL段(重定位表)
段偏移(sh_offset)与段地址(sh_addr):分别表示该段在ELF文件中位置的偏移;段在进程虚拟地址空间中的虚拟地址
段地址对齐(sh_addralign):对齐的段地址需满足 sh_addr % (2 ^ addralign) = 0 若无需对齐,sh_addralign取0或1
重定位表(.rel.xxx):包含对应段xxx在链接过程中的重定位信息,用于填补目标文件中的绝对地址引用;一张重定位表也是ELF文件中的一个段
字符串表(.strtab)与段表字符串表(.shstrtab):保存ELF文件中符号的集合
字符串表:保存ELF文件中普通的字符串
段表字符串表:保存段表中用到的字符串(如段名)
注意:ELF头中的e_shoff表示段表位置;e_shstrndx表示.shstrtab段在段表中对应的下标
我们使用字符串在字符串表中的下标来引用对应的符号
符号:链接的接口
定义与引用:设目标文件B使用了目标文件A中的函数foo 目标文件A定义了函数foo 目标文件B引用了函数foo
符号与符号值:“符号”指函数或变量,“符号值”一般指函数或变量的地址
- 全局符号:定义在本目标文件的全局符号,可被其它目标文件引用
- 外部符号:在本目标文件中引用的全局符号,但没定义在本目标文件
- 段名:其值是段的起始地址(如.text)
- 局部符号:不可被外部链接的符号(如static修饰的符号)
- 行号信息:目标文件指令和源代码间行的对应关系
ELF符号表结构:符号表本身是一个段,段名为 .symtab
符号表是一个元素为结构体Elf32_Sym的数组(其首元素总是未定义的)
符号结构体:对应于ELF文件中的符号
符号名(st_name):一个指向该符号在字符串表中的下标
符号所在段(st_shndx):对于变量或函数的定义,其表示符号所在段在段表中的下标,特殊情况下:
- SHN_ABS:绝对的值,如ELF文件名
- SHN_COMMON:表示COMMON块类型的符号,如未初始化的全局变量
- SHN_UNDEF(0):表示该符号未定义,如被引用的符号
符号值(st_value):对于变量和函数的定义,其值是该符号的地址,具体讨论如下:
- 在本目标文件定义的非COMMON块符号,其值是st_shndx指定段中的偏移量
- 目标文件中的COMMON块符号,st_value表示该符号的对齐属性
- 在可执行文件中,st_value是虚拟地址
符号类型与绑定信息(st_info):低4位表符号类型,高28位表绑定信息
- 符号类型:以“STT”关键字开头,如: STT_OBJECT:指变量或数组 STT_FUNC:指函数或可执行代码 STT_SECTION:表示一个段(其符号总是STB_LOCAL) STT_FILE:表示源文件名(其符号总是STB_LOCAL的,其st_shndx总是SHN_ABS)
- 符号绑定信息:以“STB”关键字开头,如:
STB_LOCAL:指外部不可见的局部变量
STB_GLOBAL:指外部可见的全局变量
特殊符号:被定义在ld链接器的链接脚本中,可在程序中直接声明引用;以下特殊符号均表示虚拟地址
- __executable_start:程序起始地址(而非入口地址)
- __extext:代码段结束地址
- _edata:数据段结束地址
- _end:程序结束地址
符号修饰与函数签名:
符号修饰:修饰原始的符号名,防止同名函数冲突(_Z)
函数签名:函数名 + 参数类型 + 所在命名空间(或类)
注意:函数签名指向了唯一的函数
extern "C":在C++中声明一个代码块,将代码块中的代码按C语言代码处理
弱符号与强符号:仅讨论定义(而非引用)的符号 初始化的全局变量称为强符号,未初始化的全局变量为弱符号 链接时选择多次定义的强弱符号遵循以下规则:
- 不同的目标文件中不能有同名的强符号(否则链接器会报重复定义错误)
- 若某个符号在某个目标文件中是强符号,在其它文件中都是弱符号,则链接时选择强符号
- 若某个符号在所有目标文件中都是弱符号,则选择占用空间最大的弱符号
注意:可以使用__attribute__((weak)) var = 1 将强符号var转化为弱符号
弱引用与强引用:链接器需要针对所有的外部引用符号找到其符号定义
- 强引用:引用的外部符号在链接时未找到定义则报错
- 弱引用:引用的外部符号若被定义则根据其符号规则决议;若未被定义也不报错,为其分配一个特殊值
弱符号与弱引用的用途:
- 用户自定义的强符号可以覆盖标准库的弱符号
- 将对扩展模块的引用声明为弱引用,便于扩展模块的装卸
注意:可以使用 __attribute__((weakref)) void foo(); 将外部引用的函数转为弱引用
调试信息:包括源代码映射、堆栈跟踪信息、函数参数或局部变量等
调试:设置断点、监视变量变化、单步行进等
调试前提:编译器记录了源代码和目标代码间的关系
在目标文件中添加调试信息:“-g”参数;删除目标文件中的调试信息:“strip”命令
注意:调试信息占用空间很大且对用户无用处,应该在发布前被删除
三、静态链接
空间与地址分配:将多个目标文件合并为一个输出文件,并为其分配空间
按序叠加:直接将多个目标文件按次序合并,非常浪费空间
相似段合并:将各目标文件中相同的段合并为一个段,一般分两步:
- 空间与地址分配:获得各输入文件的段长度,合并后计算各段合并后的长度与地址
- 符号解析与重定位:读取输入文件中的重定位信息,解析符号、调整代码地址、进行重定位
注意:各段合并后(链接后)所使用的地址是进程中的虚拟地址
确定符号地址:这里指加载到进程地址空间中的虚拟地址
- 第一步:确定合并后各段的起始虚拟地址
- 第二步:根据各符号于其所在段的偏移量,通过叠加段始址计算各符号的虚拟地址
符号解析与重定位:
重定位:填补本目标文件中外部引用的地址
重定位表:保存有关重定位信息的表,专门占有一个段“.rel.xxx” 重定位表项是元素类型为Elf32_Rel结构的数组,一个结构体元素对应一个重定位入口
- 重定位入口:指代码中需要被重定位的位置,其包含的外部引用地址将会被填补
- 重定位入口的偏移(r_offset):0x04: call <addr> EC
00 00 00 00 \(\rightarrow\) r_offset = 0x05
- 可重定位文件:代表待修正地址的首字节相对段始址的偏移数
- 可执行文件:代表待修正地址的首字节的虚拟地址
- 重定位入口的类型与符号(r_info):
- 低8位:表示重定位入口类型
- 高24位:表示重定位入口的符号在符号表中的下标
符号解析:全局undefined的符号(即外部引用)应当于链接后在全局符号表中找到,否则会报未定义错误
指令修正方式:链接完成后,修正重定位入口中包含的地址,以x86_32为例:
绝对寻址(mov):直接根据实际虚拟地址取值
绝对寻址修正(R_386_32):符号实际虚拟地址 + 待修正位置的值
相对寻址(call):实际虚拟地址(即跳转地址) = call的下一条指令地址 + 相对地址
相对寻址修正(R_386_PC32):符号实际虚拟地址 + 待修正位置的值 - 被修正位置的虚址
注意:绝对修正后的地址即为实际地址;相对修正后的地址为符号位置离被修正位置的地址差
特别注意“被修正位置”指的是待填写地址首字节的位置,而不是所在指令的首地址
COMMON块
未初始化的全局变量:链接前不分配段空间,并标记为SHN_COMMON
不把未初始化的全局变量直接分配在目标文件.bss段的原因:
未初始化的全局变量是弱符号,链接前不知道各目标文件同名弱符号的相对大小;链接器选出了最大的弱符号后,才可将其分配在输出文件的.bss段中
注意:链接时不允许有弱符号的大小大于强符号,否则会报错
API与ABI:
API:源代码级别的接口,遵循相同API标准的系统拥有相同的接口函数原型
ABI:二进制层面的接口,涉及内存分布、函数调用方式、寄存器使用约定等问题,比API更严格
注意:即便API相同,ABI也不一定相同;硬件、编程语言、编译器、操作系统都可能影响ABI
静态库链接
静态库:一组目标文件的压缩集合,一个目标文件包含一个函数(节省空间、便于管理)
glibc :GNU发布的libc库,glibc是Linux系统中最底层的API
libc.a :Linux中最常用的C语言静态库文件,是glibc项目的一部分,共包含1400个目标文件
如何使用静态库函数?将源目标文件和libc.a链接即可
链接过程控制:
如何控制连接过程?
- 为链接器ld传递参数
- 将链接指令放在目标文件中(如COFF中的.drectve段)
- 使用链接控制脚本
链接脚本:默认链接脚本存放在/usr/lib/ldscripts路径下,负责指示链接器ld如何进行链接
ld会根据命令行要求使用相应的链接脚本文件控制连接过程:
- elf_i386.x:生成可执行文件的链接脚本
- elf_i386.xs:生成共享目标文件的链接脚本
- 自定义脚本:使用ld命令的 “-T” 参数指定自定义的 .lds
ld链接脚本的使用:输入目标文件和库文件,输出可执行文件
链接脚本扩展名:.lds
链接脚本语法:由一系列命令语句 + 赋值语句组成
赋值语句:点符号 . 表示当前虚拟地址,指定后续输出段的始址
ENTRY(symbol):指定符号symbol的值作为入口虚拟地址,指定入口的优先级:
- ld命令行的 -e 选项
- 链接脚本中存在命令语句ENTRY(symbol)
- 若定义了**_start** 符号,则使用符号 _start 的值
- 若存在 .text 段,则使用.text段的首字节地址
- 使用默认值0
STARTUP(filename):将文件filename作为链接中的第一个输入文件
SEARCH_DIR(path):将路径path加入到ld链接器中的库查找目录(以寻找对应库)
INPUT(file1, file2, ...):指定若干文件作为链接输入
INCLUDE filename:将文件filename包含入本链接脚本
PROVIDE(symbol):在链接脚本中定义符号,如特殊符号__executable_start
SECTIONS命令语句:
1
2
3
4
5
6
7
8
9
10
11SECTIONS
{
...
/* secname 表示输出段的段名 */
/* contents 表示若干个输出段应符合的条件 */
secname : { contents }
...
/* DISCARD 段表示丢弃符合条件的输入文件,不作链接 */
/DISCARD/ : { contents }
...
}下面是规则contents的语法示例:将符合条件的所有段合并为输出段secname
- file1.o(.data):输入文件file1.o中的.data段符合条件
- file1.o(.data, .rodata):输入文件file1.o中的.data 或 .rodata段符合条件
- file1.o:输入文件file1.o的所有段都符合条件
- *(.data):表示所有输入文件的.data段都符合条件
注意:ld链接脚本的语法风格类似C语言,且支持正则表达式匹配
BFD库(Binary File Descriptor Library):通过统一的接口处理不同的目标文件格式
- BFD库的目的:解决不同硬件/软件平台下目标文件格式的差异
- BFD库的优势:将编译器和链接器本身与目标文件隔离,便于扩展支持新的目标文件格式
- BFD库的使用:安装BFD开发库后,#include "BFD.h"
四、Windows PE/COFF
PE与COFF:
"PE"(Portable Executable):微软引入的一种可执行文件格式(Win32平台)
"COFF"(Common Object File Format):PE格式(Windows)和ELF格式(Linux格式)的共同起源
注意:PE/COFF格式同样以段的格式组织,代码段为.code、数据段为.data
Microsoft Visual C++编译环境:编译器cl(compiler)、链接器(link),可执行文件查看器(dumpbin)
COFF文件结构:文件头 + 若干个段
文件头:映像头 + 段表
映像头(IMAGE_FILE_HEADER):与Elf32_Ehdr的作用类似
- Machine:目标机器类型,如x86、ARM等
- NumberOfSections:目标文件中段的数量
- TimeDateStamp :PE文件的创建时间
- PointerToSymbolTable:符号表在PE文件中的偏移
段表:记录每个段的信息,是一个IMAGE_SECTION_HEADER结构的数组
段表项(IMAGE_SECTION_HEADER):一个段表项对应一个段
- VirtualSize:该段被加载到内存后的大小
- VirtualAddress:该段被加载到内存后的虚拟地址
- SizeOfRawData:该段在文件中的大小(由于.bss段的存在,SizeOfRawData不一定与VirtualSize相等)
- Characteristics:段的属性,如段类型、对齐方式、读写权限等
段:COFF各段的存储方式和内容与ELF几乎相同,新增了“.drectve”段和“.debug$...”段
.drectve段:包含了编译器向链接器传递的链接指令
.debug$段:以 .debug\$ 关键字开头的段,如:
- .debug$S:表示符号相关的调试信息段
- .debug$P;表示预编译头文件的调试信息段
- .debug$T:表示包含类型相关的调试信息段
注意:.drectve段和.debug$段的文件数据都位于 SECTION HEADER 下方的 RAW DATA 中
符号表:与ELF文件符号表类似,包含符号名、符号类型、是否可外部链接等信息
PE文件结构:基于COFF的扩展
DOS MZ可执行文件格式的文件头和桩代码:Windows为适应传统的DOS而扩展的文件头 DOS桩代码的唯一功能是向终端输出“当前文件无法在DOS上运行”的信息
IMAGE_NT_HEADERS :PE文件头,在COFF的IMAGE_FILE_HEADER基础上新增了PE扩展头部
- 标记:常量值0x00004550(即'P'、'E'、'\0'、'\0')
- 映像头(IMAGE_FILE_HEADER):即原先COFF中的映像头
- PE扩展头部(IMAGE_OPTIONAL_HEADER):包括数据目录、导入导出表、重定位表等
PE数据目录(IMAGE_DATA_DIRECTORY[ ]):PE扩展头部的成员结构,名称为DateDirectory的数组
- 虚拟地址(VirtualAddress):PE文件的装载虚拟地址
- 长度(Size):PE文件装载所占内存大小
注意:DateDirectory元素中的各表项对应一个表的地址和大小信息(如导出表、重定位表等)