ELF头
Object目标文件类型
- 一个可重定位(relocation)文件保存着代码和适当的数据,用来和其他的object文件一起创建一个可执行文件或一个共享文件。
- 一个可执行(executable)文件保存着一个用来执行的程序,该文件指出exec(BA_OS)如何创建程序进程映像。
- 一个共享object文件保存着代码和合适的数据,用来被链接器链接。第一个链接器[ld(SD_CMD)],可以和其他的可重定位和共享object文件来创建其他的object;第二个动态链接器,联合一个可执行文件和其他共享object文件来创建一个进程映像。
Object文件格式
数据表示
ELF Header结构体
ELF文件头结构体及相关数据被定义在”/usr/include/elf.h“里
#define EI_NIDENT 16
typedef struct{
unsigned char e_ident[EI_NIDENT];//Magic:7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
//Class:
//Data:
//Version:
//OS/ABI:
//ABI Vesion:
Elf32_Half e_type;//ELF文件类型
Elf32_half e_machine;//ELF文件的CPU平台属性,相关常量以EM_开头
Elf32_Word e_version;//目标文件的版本,若为0,则是无效的版本,若>=1,则指示当前版本号
Elf_Addr e_entry;//入口地址,规定ELF程序的入口虚拟地址,操作系统加载该程序后从该地址开始执行进程
//可重定位文件一般没有入口地址,则这个值为0
Elf_Off e_phoff;//程序头表在文件的偏移量(以字节计数),若没有程序头表,则为0
Elf_Off e_shoff;//section头表在文件的偏移量(以字节计数),若没有section头表,则为0
Elf32_Word e_flags;//ELF标志位,用来标识一些ELF文件平台相关的属性
//相关常量格式一般是EF_<machine>_<flag>,machine为;平台,flag为标志
Elf32_Half e_ehsize;//指示ELF头的大小
Elf32_Half e_phentsize;//程序头表成员大小
Elf32_Half e_phnum;//程序头表的成员个数
Elf32_Half e_shentsize;//段表描述符的大小,一般等于sizeof(Elf32_Shdr)
Elf32_Half shnum;//section头表的成员数量
Elf32_Half e_shstrndx;//段表字符串表在段表中的下标,即 Elf32_Shdr[e_shstrndx] 为“.shstrstb"段的结构体
//若文件没有段表字符串,则该变量为SHN_UNDEF
}Elf32_Ehdr;
e_ident
e_indent[]数组解析目标文件的内容
名称 | 索引 | 说明 |
---|---|---|
EI_MAG0 | 0 | ELF标记:0x7F |
EI_MAG1 | 1 | LF标记:“E” |
EI_MAG2 | 2 | LF标记:“L” |
EI_MAG3 | 3 | LF标记:“F” |
EI_CLASS | 4 | ELF文件类型:0 表示无效文件;1 表示32位ELF文件;2 表示64位ELF文件 |
EI_DATA | 5 | 字节序:0表示无效格式;1表示小端格式;2表示大端格式 |
EI_VERSION | 6 | ELF文件主版本号,一般为1 |
EI_PAD | 7 | 未定义 |
EI_MAG0~EI_MAG3:保存着一个魔术数,用来确定该文件是否weiELF目标文件
e_type类型
确定目标文件的类型,相关常量以“ET_”开头
名称 | 值 | 描述 |
---|---|---|
ET_NONE | 0 | 无效文件类型 |
ET_REL | 1 | 可重定位文件,一般为.o文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件,一般为.so文件 |
ET_CORD | 4 | 核心转储文件 |
ET_LOPROC | 0xff00 | Processor-specific |
ET_HIPROC | 0xffff | Processor-specific |
e_machine
指出运行该程序需要的体系结构,相关常量以“EM_”开头
名称 | 值 | 描述 |
---|---|---|
EM_NONE | 0 | 无效体系架构 |
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel 80386 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 7 | Intel 80860 |
EM_MIPS | 8 | MIPS RS3000 |
ELF段
Section头表结构体
1、一是一个以“Elf_Shdr”结构体为元素的数组
2、Section表在ELF文件中的位置由ELF文件头的“e_shoff”成员决定
3、一个section对应一个section节头数据结构体,也就是说数组元素的个数等于段的个数
typedef struct
{
Elf32_Word sh_name;//Section名称,位于“.shstrtab"的字符串表
//该值是section名称字符串在.shstrtab"中的偏移
Elf32_Word sh_type;//Section类型
Elf32_Word sh_flags;//Section标志位
Elf32_Addr sh_addr;//如果该段可以被加载,则该值表示该段被加载后在进程地址空间中的虚拟地址,否则为0
Elf32_Off sh_offset;//如果该段存在于文件中,则表示该段在文件中的偏移,否则无意义
Elf32_Word sh_size;//Section的长度
Elf32_Word sh_link;//Section链接信息
Elf32_Word sh_info;//Section链接信息
Elf32_Word sh_addralign;//Section段地址对齐,该值是地址对齐数量的指数,即使2的指数
//如果sh_addralign为0或1,则表示该段没有对齐要求
Elf32_Word sh_entsize;//Section Entry Size项的长度
//有些段包含一些固定大小的项,比如符号表,包含的每个符号所占的大小都是一样的,对于这种段,sh_entsize表示每个项的大小
//如果为0,则表示该段不包含固定大小的项
} Elf32_Shdr;
段的类型sh_type
常量 | 值 | 描述 |
---|---|---|
SHT_NULL | 0 | 无效section |
SHT_PROGBITS | 1 | 程序段、代码段、数据段等可加装段 |
SHT_SYMTAB | 2 | 表示该段的内容为符号表 |
SHT_ STRTAB | 3 | 表示该段的内容为字符串表 |
SHT_RELA | 4 | 重定位表,该段包含了重定位信息 |
SHT_ HASH | 5 | 符号表的哈希表 |
SHT_DYNAMIC | 6 | I动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_RNOBITS | 8 | 表示该段在文件中没有内容,比如.bss段 |
SHT_ REL | 9 | M该段包含了重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接的符号表 |
段的标志位sh_flag
常量 | 值 | 描述 |
---|---|---|
SHF_WRITE | 1 | 表示该段在进程空间中可写 |
SHF_ALLOC | 2 | 表示该段在进程空间中须要分配空间 |
SHF_EXECINSTR | 4 | 表表示该段在进程空间中可以被执行,一般指代码段 |
段的链接信息sh_link、sh_info
如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么sh_link和sh_info这两个成员所包含的意义如表所示,对于其他类型的段,这两个成员没有意义
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | 0 |
SHT_ HASH | 该段所使用的符号表在段表中的下标 | 0 |
SHT_ REL | 该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_RELA | 该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB | 操作系统相关的 | 操作系统相关的 |
SHT_DNYSYM | 操作系统相关的 | 操作系统相关的 |
Sother | SHN_UNDEF | 0 |
各种section介绍
节名称 | 类型sh_type | 标志sh_flag | 说明 |
---|---|---|---|
.bss | SHT_NOBITS | SHF_ALLOC + SHF_WRITE | 保存未初始化的全局变量或局部静态变量数据(有些编译器会将全局的未初始化变量存放在目标代码文件.bss段,有些则不放,只预留一个未定义的全局变量符号,等到最终链接成可执行文件再再.bss段分配空间,与“弱符号与强符号”和“COMMON”相关),当程序开始运行时,系统初始化该section数据为0 |
.comment | SHT_PROGBITS | none | 存放的是编译器版本信息,如字符串:“GCC:(GNU)4.2.0 |
.data | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE | 保存初始化的全局变量或局部态变量数据 |
.debug | SHT_PROGBITS | none | 保存标号调试信息 |
.dynamic | SHT_DYNAMIC | SHF_ALLOC [+ SHF_WRITE] | 动态链接的信息 |
.dynstr | SHT_STRTAB | SHF_ALLOC | 保存动态链接时需要的字符串,一般情况下,名字字符串关联着符号表的入口 |
.dynsym | SHT_DYNSYM | SHF_ALLOC | 保存着动态符号 |
.init | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 进程的初始化代码 |
.fini | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 进程的终止代码 |
.plt | SHT_PROGBITS | 过程链接表 | |
.got | SHT_PROGBITS | 全局偏移量表 | |
.hash | SHT_HASH | SHF_ALLOC | 符号哈希表 |
.interp | SHT_PROGBITS | 程序的解释程序的路径 | |
.line | SHT_PROGBITS | none | 调试时的行号表,即源代码行号与编译后指令的对应表 |
.note | SHT_NOTE | none | 额外的编译器信息,比如程序的公司名称、发布版本号等 |
.rel | SHT_REL | 重定位信息表 | |
.rela | SHT_RELA | 重定位信息表 | |
.rodata | SHT_PROGBITS | SHF_ALLOC | 存放只读数据 ,如const修饰的变量和字符串变量 |
.shstrtab | SHT_STRTAB | none | section名称字符串表 |
…strtab | SHT_STRTAB | 字符串表,用于存储ELF文件中用到的各种字符串 | |
.symtab | SHT_SYMTAB | 符号表 | |
.text | SHT_PROGBITS | HF_ALLOC + SHF_EXECINSTR | 可执行指令 |
自定义段
在全局变量或函数之前加上”attribute((section("Name“)))"属性就可以把相应的变量或函数放到以”Name“作为段名的段中。
attribute((section("Name“))) int global = 42;
attribute((section("Name“))) void foo()
{
…
}
符号值
1、链接的粘合剂,每一个目标文件都会有一个相应的符号表(Symbol Table),记录了目标文件中所用到的所有符号;
2、每个定义的符号有一个对于的值,叫做符号值(Symbol Value),对于变量和函数,符号值就是地址;
3、符号表中的符号分类(关注第一类和第二类):
- 定义在本目标文件的全局符号,可以被其他目标文件引用,如全局变量,函数名等
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,一般叫做外部符号(External Symbal)
- 段名,这种符号往往由编译器生成,它的值就是该段的起始地址,如".text"、"data"等
- 局部符号,这类符号只在编译单元内部可见,可以是全局变量名、函数名、静态变量名等,对于链接过程没有作用
- 行号信息,即目标文件指令与源代码中代码行的对应关系,可选的
符号表
1、ELF文件中的符号表是文件的一个段"symtab"
2、是一个Elf32_Sym结构的数组,每一个Elf32_Sym结构对应一个符号,数组的第一个元素,即Elf32_Sym[0]元素为无效的”未定义“符号
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name;//符号名,为该符号在字符串表中的下标
Elf32_Addr st_value;//符号相对应的值,与符号有关,可能是一个绝对值,也有可能是一个地址,不同符号,对应的值含义不同
Elf32_Word st_size;//符号大小
//对于包含数据的符号,这个值是该数据类型的大小,如double类型
//如果该值为0,则表示该符号大小为0或未知
unsigned char st_info;//符号类型和绑定信息
unsigned char st_other;//该成员目前为0,未使用
Elf32_Half st_shndx;//符号所在的段下标
} Elf32_Sym;
符号类型和绑定信息(st_info)
该成员低4位表示符号的类型(Symbol Type),高28位标识符号绑定信息(Symbol Binding)
符号绑定信息
常量 | 值 | 描述 |
---|---|---|
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 | 该符号表示文件名,一般都是该目标文件所对应的源文件名,一定是STB_LOCAL类型,并且它的st_shndx一定是SHN_ABS |
符号所在段(st_shndx)
如果符号定义在本目标文件中,该成员表示符号所在的段在段表中的下标;如果符号不是定义在本目标文件中,或者对应有些特殊符号,sh_shddx的值如下表:
符号所在段特殊常量
常量 | 值 | 描述 |
---|---|---|
SHN_ABS | 0xfff1 | 表示该符号包含一个绝对的值,比如表示文件名的符号属于该类型 |
SHN_COMMON | 0xfff2 | 表示该符号是一个"COMMON"块类型的符号,如:一般未初始化的全局符号定义 |
SHN_UNDEF | 0 | 表示该符号未定义,这个符号表示该符号在本目标文件被引用到,但是定义在其他目标文件中 |
符号值(st_value)
如果这个符号是一个函数或变量的定义,那么符号值就是这个函数或变量的地址,否则按以下情况区别对待
- 在目标文件中,如果符号的定义并且该符号不是"COMMON"块类型(即st_shndx != SHN_COMMON),则st_value表示该符号在段中的偏移,也就是符号所对应的函数或变量位于由st_shndx指定的段,偏移st_value的位置
- 在目标文件中,如果符号是"COMMON"块类型(即st_shndx == SHN_COMMON),本身并没有存在于BSS段,则st_value表示该符号的对齐属性
- 在可执行文件中,st_value表示符号的虚拟地址,这个虚拟地址对于动态连接器十分有用
特殊符号
当使用ld作为连接器生成可执行文件时,会定义很多特殊的符号,这些符号并没有在程序中定义,但是可以在程序中声明并且引用它(其实这写符号是被定义在ld链接器的链接脚本中的)。
- __executable_start:该符号为程序其实地址,是程序的最开始的地址
- __etext或_etext或etext:该符号为代码段结束地址,即代码段最末尾的地址
- _edata或edata:该符号为数据段结束地址,即数据段最末尾的地址
- _end或end:该符号为程序结束地址
- 以上地址都是程序被装载时的虚拟地址
extern char __executable_start[];
extern char _etext, _etext, etext;
extern char _edata[], edata;
extern char _end[], _end[];
extern “C”
C++为了与C兼容,在符号管理上,C++有一个用来声明一个C的符号的**extern “C”**关键字的用法,C++编译器会将在extern “C”的大括号内部的代码当作C语言代码处理。
#ifdef __cplusplus
extern "c"{
#endif
...
声明或代码
...
#ifdef __cplusplus
}
#endif
弱符号与强符号
主要用于库发惹链接过程
1、对C/C++,编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号;
2、可以通过GCC的__attribute__((weak))定义任何一个强符号为弱符号;
3、强符号和弱符号都是针对定义来说的;
4、连接器按照如下规则处理;
- 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误
- 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么编译器选择强符号
- 如果一个符号在所有的目标文件中都是弱符号,那么编译器选择其中占用空间最大的一个
5、强引用:目标文件对外部目标文件的符号引用需要被正确处理,最终链接成可执行文件,如果找不到该符号的定义,链接器就会报符号未定义错误;
6、弱引用:处理弱引用时,如果该符号有定义,则链接器将该符号的引用正确处理,如果该符号未定义,则链接器对于该符号引用不报错,链接器默认其为0,或者一个便于程序代码识别的特殊值。在GCC中,可以通过__attribute((weakref))声明对一个外部函数的引用为弱引用;
7、通过编译时有**-lpthread**选项,可以使程序支持多线程模式(在程序中定义一个pthread_create函数的弱引用,然后在程序运行时动态判断是否链接到pthread库从而判断执行多线程版本Glibc,还是单线程版本Glibc)
#include <stdio.h>
#include <pthread.h>
int pthread_create(pthread_t *, const pthread_attr_t *, void* (*)(void *), void *) __attribute__((weak));
int main()
{
if (pthread_create) {
printf("This is multi-thread version!\n");
} else {
printf("This is single-thread version!\n");
}
}
编译运行结果如下:
$gcc pthread.c -o pt
$./pt
This is single-thread version!
$gcc pthread.c -lpthread -o pt
$./pt
This is multi-thread version!
字符串表
1、ELF文件中使用的字符串,如段名、变量名等,集中起来存到一个表,然后使用字符串在表中的偏移来引用字符串,每个字符串以‘\0’结束;
2、常见的段名为:".strtab"(字符串表:用来保存普通的字符串)或"shstrtab"(段表字符串表:用来保存段表中用到的字符串,如段名)
Segment头表结构体
/* Program segment header程序头表 */
//程序头表是一个结构体数组
typedef struct
{
Elf32_Word p_type;//段的类型
Elf32_Off p_offset;//程序段再文件中的偏移
Elf32_Addr p_vaddr;//程序段的第一个字节在进程虚拟地址空间的起始位置
Elf32_Addr p_paddr;//程序段的(LMA)物理装载地址,一般情况下与p_vaddr一样
Elf32_Word p_filesz;//程序段在ELF文件中所占的空间长度,有可能为0
Elf32_Word p_memsz;//程序段在虚拟地址空间中所占用的长度,对于LOAD类型ELF,p_memsz≥p_filesz
Elf32_Word p_flags;//程序段的权限属性,如可读(R)、可写(W)、可执行(X)
Elf32_Word p_align;//程序段的对齐属性,实际对齐字节等于2的p_align次方
} Elf32_Phdr;
静态链接
链接过程:
- 第一步 空间与地址分配 扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步,链接器能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系;
- 第二步 符号解析与重定位 使用第一步收集的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。
1、在Linux下,ELF可执行文件默认从地址0x08048000开始分配,VMA表示Virtual Memory Address虚拟地址,LMA表示Load Memory Address加载地址
2、call 指令是一条近址相对位移调用指令,后跟的地址是调用指令的下一条指令的偏移量,例如
80480ba: e8 09 00 00 00 call 80480c8
80480bf: 83 c4 24 add $0x24, %esp
//call指令的下一条指令是add,add的地址是0x080480bf
//所以”相对add指令偏移量为0x00000009“的地址为0x080480bf + 0x00000009
重定位表
1、链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段中哪些对绝对地址的引用的位置,这些重定位信息都记录在ELF文件的重定位表;
2、对于每个需要重定位的代码段(.text)或数据段(.data),都会有一个相应的重定位表(.rel.text 或 .rel.data);
3、重定位表同时也是ELF的一个段( .rela<name> 或 .rel<name>),段类型sh_type = SHT_REL,sh_link表示符号表的下标,sh_info表示作用于哪个段;
4、在完成每个目标文件空间和地址分配后,链接器进入符号解析与重定位
5、一个重定位section关联两个其他的section:一个符号表和一个可修改的section
//每个要被重定位的地方叫重定位入口(Relocation Entry)
//重定位入口的偏移表示该入口在要被重定位的段中的位置
typedef struct
{
Elf32_Addr r_offset;//重定位入口的偏移
//对于可重定位文件,该值是重定位入口要修正的位置的第一个字节相对于段起始的偏移
//对于可执行文件或共享文件,该值是重定位入口要修正的位置的第一个字节的虚拟地址
Elf32_Word r_info;//重定位入口的类型和符号
//低8位表示重定位入口的类型
//高24位表示重定位入口的符号在符号表中的下标
} Elf32_Rel;
/* Relocation table entry with addend (in section of type SHT_RELA). */
typedef struct
{
Elf32_Addr r_offset;//
Elf32_Word r_info;//
Elf32_Sword r_addend;//指定一个常量加数(用于计算将要存储于重定位域中的值)
} Elf32_Rela;
重定位类型
r_info成员低8位表示的入口类型
- A 表示用于计算可重定位的域值的加数(保存在被修正位置的值)
- B 表示在执行过程中的一个共享目标被加载到内存时的基地址
- G 表示在可执行过程中重定位入口符号驻留在全局偏移表中的偏移
- GOT 表示全局偏移表的地址
- L 表示一个符号的过程链接表入口的位置(section偏移或地址),一个过程链接表入口重定位一个函数调用到正确的目的单元,链接器创建初始的链接表,而动态链接器在执行中修改入口
- P 表示(section偏移或地址)被重定位的存储单元位置(即r_offset的值)
- S 表示索引驻留重定位入口处的符号值(即符号的实际地址)
宏定义 | 值 | 重定位修正方法 | 说明 |
---|---|---|---|
R_386_NONE | 0 | none | |
R_386_32 | 1 | S + A | 绝对寻址修正 |
R_386_PC32 | 2 | S + A - P | 相对寻址修正 |
R_386_GOT32 | 3 | G + A - P | 计算全局偏移表基地址到符号的全局偏移表入口之间的间隔,另外通知链接编辑器建立一个全局偏移表 |
R_386_PLT32 | 4 | L + A - P | 计算符号的过程链接表入口地址,并另外通知链接器建立一个过程链接表 |
R_386_COPY | 5 | none | 链接器创建该重定位类型用于动态链接 |
R_386_GLOB_DAT | 6 | S | 用于设置一个全局偏移表入口为指定符号的地址,该特定的重定位类型允许决定符号和全局偏移表入口之间的一致性 |
R_386_JMP_SLOT | 7 | S | 链接器创建该重定位类型用于动态链接,其偏移成员给出一个过程链接表入口的位置 |
R_386_RELATIVE | 8 | B + A | 链接器创建该重定位类型用于动态链接,其偏移成员给出包含表达相关地址值的一个shared object中的位置 |
R_386_GOTOFF | 9 | S + A - GOT | 计算符号值和全局偏移表地址之间的不同,另外通知链接器建立全局偏移表GOT |
R_386_GOTPC | 9 | GOT + A - P | 类似R_386_PC32,不同的是它使用全局偏移表 |
装载与动态链接
可执行文件的装载
默认的程序入口地址:0x08048000
1、进程虚拟地址空间
- PAE(Physical Address Extension):用于地址扩展,访问高于应用程序32位虚拟地址空间;
- 比如一个应用程序中0x10000000~0x20000000(256M)的虚拟地址空间用做窗口,映射高于32位虚拟地址空间(4GB)的物理空间多个256M的物理空间,根据需要将不同的物理空间映射到该虚拟空间,以供应用程序使用;在WIndows下,这种操作叫AWE(Address Windowing Extensions),Linux则采用mmap()系统调用实现;
2、页映射(Paging):利用程序的局部性原理
3、进程的建立
- 创建一个独立的虚拟地址空间,也就是创建映射函数所需要的相应的数据结构(在i386的Linux下,创建虚拟地址空间实际只是分配一个页目录)
- 读取可执行文件头(装载),并且建立虚拟空间与可执行文件的映射关系(可执行文件头部的信息建立起可执行文件和进程虚拟内存之间的映射关系)
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行(涉及内核堆栈和用户堆栈的切换、CPU运行权限的切换)
- 页错误,当CPU执行入口地址指令时,发现页面不存在,引起也错误(Page Fault),CPU将控制交与操作系统处理,其中创建映射的数据结构起到关键作用,根据这个数据结构,找出空页面的VMA,计算相应页面在可执行文件中的偏移,然后再物理内存中分配物理页面,并加载和将进程中该虚拟页与分配的内存物理页之间建立映射,最后将控制权交还进程,进程从刚才页错误的位置重新开始执行
4、进程栈初始化
- 栈顶寄存器(esp)指向的位置是初始化后堆栈的顶部
- 第一4个字节对应函数main()的argc参数,表示参数数量
- 第二4个字节开始,是4字节数组(函数main()函数的argv**数组),是参数字符串指针数组,以0结束
- 参数字符串指针数组(以0结束)后,是4字节的环境变量字符串指针数组(如指向“HOME=/home/user”),以0结束
动态链接
目的: 节省内存和磁盘空间、解决共享模块的升级开发与发布
1、当程序源代码被编译成目标文件时,编译器对外部引用的变量或函数地址伤不确定;当链接器将目标文件链接成可执行文件时,这时候链接器必须确定可执行文件所引用的变量或函数的性质,如果外部变量或函数是一个定义在其他静态目标模块中的变量或函数,那么链接器将会按照静态链接的规则,将可执行程序中的外部变量或函数地址引用重定位;如果外部变量或函数是一个定义在某个动态共享对象中的变量或函数,那么链接器就会将这个符号引用标记为一个动态链接的符号,而且在装载时再进行地址重定位
2、共享库(.so)保存完整的符号信息,因此需要把共享库作为生成可执行文件的链接输入文件,如:gcc -o Program Program.c path/Libxxx.so
3、动态链接程序运行时的地址空间分布
cat /proc/PID/maps
- 共享文件与可执行文件被操作系统用同样的方法映射到进程的虚拟地址空间,共享文件的占据的虚拟地址和长度不同
- 还使用了动态链接的C语言运行库libc-2.6.1.so
- 还使用了Linux下的动态链接器ld-2.6.so,在系统开始运行可执行程序之前,首先把控制权交给动态链接器,由动态链接器完成所有的动态链接工作之后,控制权再交与可执行程序
4、装载重定位(解决共享对象的升级与发布):再链接时,对所有的绝对地址的引用不作重定位,推迟到装载时再完成地址引用重定位。Linux和GCC支持装载重定位,如GCC的参数“-shared”和“-fPIC”,如果只使用“-shared”,那么输出的共享对象使用装载重定位的方法
5、地址无关码(解决多个进程之间变量共享):把指令中哪些需要被修改的部分分离出来,与数据部分放在一起,保持指令部分运行过程不变,而数据部分可以在每个进程拥有一个副本,也就是地址无关代码(PIC Position Independent Code
)技术,地址引用方式可分为以下几种情况
- 第一种:模块内部(本目标文件内)的函数调用、跳转等,相对位置是固定的,因此,模块内部的跳转、函数调用都可以是相对地址调用或基于寄存器的相对调用,对于这种指令不需要重定位。
- 第二种:模块内部(本目标文件内)的数据访问,比如模块中定义的全局变量、静态变量,任何一条指令与需访问的模块内部数据之间的相对位置是固定的,只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据。
- 第三种:模块外部(或其他目标文件)的数据访问,比如其他模块定义的全局变量,该地址需在装载时才能确定,因此放到数据段内。ELF在数据段建立一个指向这些变量的指针数组(4字节指针数组),叶称为全局偏移表(Global Offset Table GOT),当代码需要引用该全局变量时,通过GOT中相对应的项间接引用。[编译时确定GOT相对于当前指令的偏移,与访问模块内部变量方法一样,通过PC值加上偏移量,就可以得到GOT的位置,然后根据变量地址在GOT中的偏移就可以得到变量的地址]
- 第四种:模块外部(或其他目标文件)的函数调用、跳转等,采用第三种方法,但是GOT相应的项保存的是目标函数的地址,当模块需要调用目标函数时,通过GOT中的选项进行间接跳转。
- 第五种:共享模块的全局变量:使用全局变量(特别extern)时,无法判断变量是定义同一模块的其他目标文件,还是定义在另外一个共享对象中,而且在主模块使用时,由于程序主模块的代码不是地址无关代码(不管模块内外的函数或变量的地址都必须在链接过程中确定,所以为了使链接过程正常,链接器在创建可执行文件时,在文件中的“.bss”段创建变量的副本,如果同一变量存在多个位置,那么所有使用该变量的指令指向位于可执行文件中的副本);ELF共享库编译时,默认把定义在模块内部的全局变量当作定义在其他模块的全局变量,即通过GOT实现变量访问。[当共享模块被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器把GOT中的相对应地址指向该副本,在实际运行时只有一个实列,如果变量在共享模块中被初始化,那么动态链接器还需要将该初始化值复制到程序主模块中的变量副本,如果该全局变量在程序主模块没有副本,那么GOT中的相应地址就指向模块内部的该变量副本]
指令跳转、调用 | 数据访问 | |
---|---|---|
模块内部 | 相对跳转和调用 | 相对地址访问 |
模块外部 | 间接跳转和调用(GOT) | 间接访问(GOT) |
6、区分是否为PIC:readelf -d xxx.so | grep TEXTREL 若没有输出,不是PIC
7、PIC与PIE:地址无关可执行文件(Position Independent Executable PIE)
8、数据段地址无关性:问题:例如 static int a; static int *p = &a;那么指针P的地址就是一个绝对地址,指向变量a,而变量a的地址会随着共享对象的装载地址改变而改变。解决办法:装载时重定位解决数据段中对绝对地址引用的问题,对于数据段来说,在每个进程都有一份独立的副本,对于共享对象,如果数据段有绝对地址引用,那么编译器和链接器生成一个重定位表,表中包含“R_386_RELATIVE”类型的重定位入口。
延迟绑定(PLT)
在Glibc中,使用动态链接器的函数_dl_runtime_resolve()完成地址绑定
1、每个外部函数在PLT中都有一个相应的项,例如:
bar@plt:
jmp *(bar@GOT) //bar@GOT表示GOT中保存bar()这个函数相应的项
//如果链接器在初始化阶段已经初始化bar@GOT,并且将bar()的地址填入该校,那么直接跳转到bar()
//如果链接器初始化阶段没有将bar()地址填入到bar@GOT,而是将第二条指令"push n"地址填入,跳转到下一条指令,进行绑定(延时绑定)
push n //这个数字n,是符号引用在重定位表“.rel.plt"数组的下标
push moduleID // 将本模块的ID压栈,即指向本模块名字符串指针压栈
jump _dl_runtime_resolve //调用动态链接器的_dl_runtime_resolve()函数完成符号解析和重定位工作
//_dl_runtime_resolve()进行一系列工作后,将bar()的真正地址写入bar@GOT中,以后再次调用时,就直接跳转到bar()函数
//bar()函数返回时会根据堆栈保存的EIP直接返回到调用者,避免执行bar@plt中的第二条指令开始的那段代码
2、ELF将GOT拆分成两个表叫做".got"和".got.plt",其中".got"用来保存全局变量引用的地址,".got.plt"用来保存函数引用的地址,而且**".got.plt"的前三项有特殊意义**:
- 第一项:保存的是".dynamic"段的地址,该段描述本模块动态链接相关的信息
- 第二项:保存的是本模块的ID,指向本模块名字符串
- 第三项:保存的是_dl_runtime_resolve()的地址
其中第二项和第三项由动态链接器在装载共享模块时初始化,".got.plt"的其余项分别对应每个外部函数的引用。
3、实际PLT结构,plt的每一项长度是16个字节,刚好是3条指令
PLT0:
push *(GOT + 4) //对应.got.plt的第二项,也就是本模块ID压栈
jump *(GOT + 8) //跳转到_dl_runtime_resolve(),执行符号解析和重定位
...
bar@plt:
jmp *(bar@GOT)
push n //将符号引用在重定位表“.rel.plt"数组的下标压栈
jump PLT0
...
...
4、PLT在ELF文件中,以独立的段存放,通常叫做".plt",本身是地址无关代码
动态链接相关结构
在Linux动态链接情况下,操作系统加载完可执行文件之后,启动动态链接器Dynamic Linker,动态链接器ld.so是一个共享对象,操作系统通过映射的方式将它加载到进程的地址空间中,操作系统加载完动态链接器后,将控制权交给动态链接器的入口地址(与可执行文件一样,共享对象也有入口地址),动态链接器开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接,当所有动态链接完成之后,动态控制器将控制权交给可执行文件的入口地址,程序开始正式工作。
".interp"段
1、动态链接器的位置由ELF可执行文件决定,也就是说ELF文件中".interp"段保存的字符串就是可执行文件需要的动态链接器的路径,在Linux下,可执行文件所需要的动态链接器路径几乎都是"/lib/ld-linux.so.2",/lib/ld-linux.so.2是软链接,指向真正的动态链接器,例如指向/lib/ld-2.6.1.so
2、动态链接器在Linux下是Glibc的一部分,版号系统的Glibc库版本号一样
".dynamic"段
/* Dynamic section entry. */
typedef struct{
Elf32_Sword d_tag;/* Dynamic entry type */
union{
Elf32_Word d_val;/* Integer value */
Elf32_Addr d_ptr;/* Address value */
} d_un;
}Elf32_Dyn;
1、".dynamic"段是以Elf32_Dyn结构体的数组
2、Elf32_Dyn结构由一个类型加上一个附加的数值或指针,对于不同的类型,附加的数值或指针有着不同的含义,这些值都定义在"elf.h"里面,下表列举常见的类型值
d_tag类型 | d_un的含义 |
---|---|
DT_SYMTAB | 动态链接符号表的地址,d_ptr表示".dynsym"的地址 |
DT_STRTAB | 动态链接字符串表地址,d_ptr表示".dynstr"的地址 |
DT_STRSZ | 动态链接字符串表的大小,d_val表示大小 |
DT_HASH | 动态链接哈希表地址,d_ptr表示".hash"的地址 |
DT_SONAME | 本共享对象的"SO-NAME“ |
DT_RPATH | 动态链接共享对象搜索路径 |
DT_INIT | 初始化代码地址 |
DT_FINIT | 结束代码地址 |
DT_NEEDED | 依赖的共享对象文件,d_ptr表示所依赖的共享对象文件名 |
DT_REL或DT_RELA | 动态链接重定位表地址 |
DT_RELENT或DT_RELAENT | 动态重定位表入口数量 |
".dynsym"动态符号表
1、静态链接中,有".symtab"符号表,保存该目标文件的符号定义和引用,而动态链接中,ELF有".dynsym"(Dynamic Symbol Table)动态符号表只保存与动态链接相关的符号
2、".symtab"往往保存所有的符号,叶包括".dynsym"中的符号
3、".dynsym"需要动态符号字符串表".dynstr"保存符号名的字符串,".hash"符号哈希表是用来加快符号查找过程
4、动态链接符号表与静态链接的符号表几乎一样,可以将导入函数看作对其他目标文件中的函数的引用,把导出函数看作在本目标文件定义的函数
动态链接重定位表".rel.dyn"和".rel.plt"
1、静态链接中,目标文件包含专门用于表示重定位信息的重定位表,比如".rel.text"表示代码段的重定位,".rel.data"是数据段的重定位表
2、动态链接的文件中。类似的重定位表分别为".rel.dyn"和".rel.plt",其中 “.rel.dyn"实际上是对数据引用的修 正,所修正的位置位于”.got"以及数据段,".rel.plt"是对函数引用的修正 ,所修正的位置位于".got.plt"
3、结构体与静态重定位表一样
动态链接时进程堆栈初始化信息
1、动态链接器开始工作时,至少知道关于可执行文件和本进程的一些信息,比如可执行文件有几个段、每个段的属性、程序的入口地址等,这些信息往往由操作系统传递给动态链接器,保存在进程的堆栈里面,即进程初始化时,堆栈保存了关于进程运行环境和命令行参数等信息。事实上,堆栈还保存了动态链接器所需要的一些辅助信息数组(Auxiliary Verctor),结构被定义在"elf.h"
2、辅助信息数组位于进程堆栈的环境变量指针后面
/* This vector is normally only used by the program interpreter. The usual definition in an ABI supplement uses the name auxv_t. The vector is not usually defined in a standard <elf.h> file, but it can't hurt. We rename it to avoid conflicts. The sizes of these types are an arrangement between the exec server and the program interpreter, so we don't fully specify them here. */
typedef struct{
uint32_t a_type;// Entry type
union{
uint32_t a_val;//
} a_un;
} Elf32_auxv_t;
a_type定义 | a_type的值 | a_val的含义 |
---|---|---|
AT_NULL | 0 | 表示辅助信息数组结束 |
AT_EXEFD | 2 | 表示可执行文件的文件句柄 |
AT_PHDR | 3 | 可执行文件中的程序头表在进程中的地址 |
AT_PHENT | 4 | 可执行文件头中程序头表中每一个入口(Entry)的数量 |
AT_PHNUM | 5 | 可执行文件头表中入口(Entry)的数量 |
AT_BASE | 7 | 表示动态链接器本身的装载地址 |
AT_ENTRY | 9 | 可执行文件的入口地址,即启动地址 |
动态链接器的步骤
动态链接器自举
1、动态链接器本身不可以依赖其他任何共享对象
2、动态链接器本身所需要的全局和静态变量的重定位工作由自身完成(自举Bootstrap)
3、自举代码不可以使用任何全局变量和静态变量,也不可以调用函数
4、动态链接器入口地址是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码开始执行
- 自举代码首先找到自己的GOT,而GOT的第一个入口保存的是".dynamic"段的偏移地址,由此找到动态链接器本身的".dynamic"段;
- 通过".dynamic"段中的信息,自举代码可以获取动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,将它们重定位
- 重定位后,动态链接器代码中才可以开始使用自己的全局变量和静态变量
5、完成自举,可以自由调用函数并且可以访问全局变量
装载共享对象
1、完成基本自举后,动态链接器将可执行文件和动态链接器本身的符号表都合并到一个符号表中,称全局符号表(Global Symbol Table),当一个新的共享对象被装载进程虚拟空间地址,它的符号表也被合并到全局符号表中。
2、然后链接器开始寻址可执行文件所依赖的共享对象,即".dynamic"段中结构体类型为DT_NEEDED的成员,并将这些共享对象名字放入一个装载集合中
3、然后链接器开始从集合取一个所需要的共享对象名字,找到相应的文件后打开,读取相应共享对象的ELF文件头和".dynamic"段
4、然后将找到的共享对象相应的代码段和数据段映射到进程空间中
5、如果这个ELF共享对象还依赖其他共享对象,那么将所依赖的共享对象的名字放到装载集合中,循环直到所有共享对象都被装载为止
重定位和初始化
1、完成共享模块加载后,链接器开始重新遍历可执行文化和每个共享对象的重定位表,将它们的GOT/PLT中每个需要重定位的位置进行修正(此时链接器拥有进程的全局符号表)
2、重定位完成后,如果某个共享对象有".init"段,那么动态链接器执行".init"段的代码,实现共享对象特有的初始化过程。相应地,共享对象中还可能有".finit"段,当进程退出时执行".finit"段的代码
3、如果进程的可执行代码文件也有",init"段,那么动态链接器不会执行,因为可执行文件中的".init"和".finit"段由程序初始化部分代码负责执行
4、完成重定位和初始化后,链接器将进程的控制权交给程序入口并且开始执行
符号优先级
全局符号介入(Global Symbol Interpose):一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖
1、当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略,动态链接器的装载顺序是广度优先,即按出现次序加载
2、全局符号介入与地址无关代码
Linux动态链接器实现
1、Linux的ELF动态链接器时Glibc的一部分,源代码位于Glibc的elf目录下,实际入口地址位于sysdeps/i386/dl-manchine.h中的_start(),普通程序的入口地址_start()在sysdeps/i386/elf/start.S中。
2、_start调用位于elf/rtld.c的_dl_start()函数。_dl_start()函数首先对ld.so进行重定位(因为ld.so自己就是动态链接器,所以要完成自举),完成自举后调用其他函数访问全局变量,调用_dl_start_final,收集一些基本的运行数值,进入_dl_sysdep_start,这个函数进行一些平台相关的处理之后进入_dl_main,这才是真正意义上的动态链接器的主函数。
运行时加载
1、运行时加载也叫显示运行时链接(Explicit Run-time Linking),支持动态链接系统,让程序运行时控制加载指定的模块,并且可以在不需要改模块时将其卸载。
2、动态装载库(Dynamic Loading Library),动态库例如插件、驱动等,可以被动态链接器在运行时加载进内存并且进行重定位等操作。
3、动态库的装载通过一系列由动态链接器提供的API(实现在/lib/libdl.so.2中,声明和相关常量被定义在系统标准头文件<dlfcn.h>)进行操作,例如:打开动态库(diopen)、查找符号(dlsym)、错误处理(dlerror)、关闭动态库(dlclose)。
dlopen()
void * dlopen(const char *filename, int flag);
1、用于打开一个动态库,并将其加载到进程的地址空间,完成初始化过程
2、第一个参数是被加载动态库的路径,如果是绝对路径,则该函数将会尝试直接打开该动态库,如果是相对路径,那么dlopen()会尝试以一定顺序查找该动态库文件:
- 查找环境变量LD_LIBRARY_PATH指定的一系列目录
- 查找由/etc/ld.so.cache里面所指定的共享库路径
- /lib、/usr/lib
3、如果filename参数为0,那么dlopen返回全局符号表的句柄,也就是说可以在运行时找到全局符号表里面任何一个符号,并且执行它
4、第二个参数flag表示函数符号的解析方式,LRTLD_LAZY与RTLD_NOW二选一,RTLD_GLOBAL 可选
- LRTLD_LAZY 表示使用延迟绑定,当函数第一次被使用时才进行绑定,即PLT机制
- RTLD_NOW 表示当模块被加载时完成所有函数绑定工作,如果有任何未定义符号引用的绑定没法完成,那么dlopen返回错误
- RTLD_GLOBAL 表示将加载的模块的全局符号合并到进程的全局符号表中,使得后续加载的模块可以使用该符号
5、dlopen返回被加载的模块句柄,这个句柄在后续使用dlsym或dlclose需要用到,如果加载模块失败,则返回NULL,如果模块已经被加载过,则返回同一个句柄
6、如果被加载的模块之间有依赖关系,则优先手工加载被依赖的模块
7、dlopen会在加载模块时执行模块初始化部分代码,也就是和动态链接器一样,在完成装载、映射、重定位后,会执行".init"段的代码,然后返回
dlsym()
void *dlsym(void *handle, char *symbol)
1、通过该函数查找所需要的符号
2、第一个参数是dlopen返回动态库的句柄,第二个参数是要查找符号的字符串,一个以’\0’结尾的C字符串
3、依赖序列优先级查找符号,也即是以被dlopen打开的共享对象为根节点,对它所依赖的共享对象进行广度优先遍历,直到找到符号为止
4、如果dlsym找到相应的符号,则返回该符号的值,如果没有找到相应的符号,则返回NULL
- 如果查找的符号是函数,那么返回的值就是函数的地址
- 如果查找的符号是变量,那么返回的值就是变量的地址
- 如果查找的符号是常量,那么返回的值就是常量的值
dlerror()
1、每次调用diopen()、dlsym()、dlclose(),都可以调用dlerror()函数来判断上一次调用是否成功
2、dlerror()返回值类型为char *,如果返回NULL,则表示上一次调用成功;如果不是,则返回相应的错误信息
dlclose()
1、将一个已经加载的模块卸载
2、系统维持一个加载引用计数器,每次使用dlopen()加载某模块时,相应计数器+1,每次使用dlclose()卸载某模块时,相应计时器-1,只有当计数器值为0时,模块才真正被卸载
3、卸载的过程与加载过程相反,先执行".finit"段代码,然后将相应的符号从符号表中去除,取消进程空间跟模块的映射关系,然后关闭模块文件
Linux共享库的组织
共享库版本
- 兼容更新:所有的更新只是在原有的共享库基础上添加内容,所有原有的接口都保持不变
- 不兼容更新:共享库更新改变了原有的接口,使用该共享库原有的接口的程序可能不能运行或运行不正常
共享库版本命名
LInux规定共享库文件名规则如下:libname.so.x.y.z
1、最前面使用前缀"lib"、中间是库的名字、后缀".so",最后是三个数字组成的版本号
2、"x"表示主版本号,"y"表示次版本号,"z"表示发布版本号
3、主版本号:表示库的重大升级,不同主版本号的库之间不兼容
4、次版本号:表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变,在主版本号相同情况下,高的次版本号的库向后兼容低的次版本号库
5、发布版本号:表示库的一些错误的修正、性能的改进等,并不添加任何新的接口,也不对接口进行更改,相同主版本号、次版本号的共享库,不同的发布版本号之间完全兼容
6、有些Linux不按照上述规则命名,例如Glibc的libc-x.y.z.so或ld-x.y.z.so
SO-NAME(解决共享库主版本号)
1、每个共享库都有一个对应的"SO-NAME",即共享库文件名去掉次版本号和发布版本号,保留主版本号,如共享库libfoo.so.2.6.1,那么SO-NAME是libfoo.so.2,用来记录共享库的依赖关系,有些SO-NAME不安普通规则命名
2、在Linux系统中,系统为每个共享库在它所在的目录创建一个和"SO-NAME"相同的并且指向它的软链接(Symbol Link)
3、以SO-NAME命名的软链接目的,是的所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的SO-NAME,而不使用详细的版本号
4、当共享库的主版本号升级时,系统中就会存在多个SO-NAME,已有的程序运行不受影响
5、Linux的工具"ldconfig",当系统安装或跟新一个共享库时,运行该工具,会遍历所有默认共享库目录,如/lib、/usr/lib等,然后更新所有的软连接,指向最新版的共享库,如果安装新的共享库,那么ldconfig为其创建相应的软连接
符号版本(解决共享库次版本号)
1、Linux下的Glibc从版本2.1开始支持一**基于符合的版本机制(Symbol Versioning)**的方案:让每个导出和导入的符号都有一个相关联的版本号
2、符号版本脚本没有统一格式
共享库系统路径
- /lib 存放系统最关键和基础的共享库,如动态链接器、C语言运行库、数学库等,主要是/bin和/sbin下程序所需要的库和系统启动时需要的库
- /usr/lib 保存一些非系统运行时所需要的关键性共享库,如开发时用到的共享库、静态库、目标文件等
- /usr/local/lib 保存一些和操作系统本身不十分相关的库,主要是第三方应用程序的库
共享库的查找过程
1、任何一个动态链接器的模块所依赖的模块路径保存在".dynamic"段里面,由DT_NEEDED类型的项表示
2、如果DT_NEEDED里面保存的是绝对路径,那么动态链接器按照这个路径去查找;如果DT_NEEDED里面保存的是相对路径,那么动态链接器会在/lib、/usr/lib和/etc/ld.so.conf配置文件指定的目录中寻找
3、ld.so.conf是一个文本配置文件,可能包含其他配置文件,这些配置文件存放目录信息,如
- /usr/local/lib
- /lib/i486-linux-gnu
- /usr/lib/ii486-linux-gnu
4、当动态链接器要查找共享库是,可以直接从/etc/ld.so.cache(由ldconfig工具收集SO-NAME,存放到/etc/ld.so.cache)查找,以加快共享库的查找过程
5、如果动态链接器在/etc/ld.so.cache没有查找到需要的共享库,那么动态链接器还会遍历/lib和/lusr/lib两个目录,如果查找不到,就失败
在系统指定的共享库目录下添加、删除或者更新任何一个共享库,或者更改/etc/ld.so.conf的配置,都应该运行ldconfig工具,以便调整SO-NAME和/etc/ld.so.cache
环境变量
- 尽量避免使用环境变量LD_LIBRARY_PATH 、LD_PRELOAD
- 环境变量的格式为key=value的字符串,C语言可以使用getenv函数获取环境变量信息
LD_LIBRARY_PATH
1、默认情况下,LD_LIBRARY_PATH 为空
2、在Linux系统中,LD_LIBRARY_PATH 是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开
3、为某个进程设置LD_LIBRARY_PATH ,那么进程启动时,动态链接器首先查找由LD_LIBRARY_PATH 指定的目录
4、结合LD_LIBRARY_PATH 变量,动态链接器会按照下列顺序依次装载或查找共享对象
- 由环境变量LD_LIBRARY_PATH 指定的路径
- 由路径缓存文件/etc/ld.so.cache指定的路径
- 默认共享库目录,先/usr/lib,然后/lib
示例:
$LD_LIBRARY_PATH=/home/user /bin/ls
LD_PRELOAD
1、指定预先装载一些共享库或目标文件
2、在LD_PRELOAD指定的文件会在动态链接器按照固定规则搜索共享库之前装载
3、无论程序是否依赖LD_PRELOAD指定的共享库或目标文件,都会被装载
4、由于全局符号介入机制,LD_PRELOAD指定的共享库或目标文件中的全局符号会覆盖后面加载的同名全局符号,这使得很方便改写标准C库的某个或某几个函数而不影响其他函数
5、系统配置文件/etc/ld.so.preload,与LD_PRELOAD一样,记录的共享库或目标文件被提前装载
示例:
$LD_PRELOAD=/home/user /bin/ls
LD_DEBUG
1、可以打开动态链接器的调试功能
2、设置这个变量时,动态链接器会在运行时打印出各种有用信息,对开发和调试共享库有很大帮助
- files 显示动态链接器整个装载过程
- bindings 显示动态链接的符号绑定过程
- libs 显示共享库的查找过程
- versions 显示符号的版本依赖关系
- reloc 显示重定位过程
- symbols 显示符号表查找过程
- statistics 显示动态链接过程中的各种统计信息
- all 显示所有信息
- help 显示各种可选的帮助信息
示例:
$LD_DEBUG=files ./helloworld.out
共享库的创建和安装
共享库创建
$gcc -shared -fPIC -Wl,-soname,my_soname -o libxxx.so.x.y.z source_files_list library_files_list
- -shared 表示输出是共享库类型
- -fPIC 表示使用地址无关代码技术输出文件
- -Wl 将指定的参数传递给链接器,比如"-W1,-soname,my_soname"时,GCC将"-soname my_soname"传递给链接器,用来指定输出共享库的SO-NAME(如果不使用-soname指定共享库的SO-NAME,那么该共享库默认就没有SO-NAME,即使用ldconfig更新SO-NAME的软链接时,对该共享库没有效果)
清除符号信息
1、正常编译的共享库或可执行文件里面带有符号信息和调试信息,可以使用strip工具清除
$strip libxxx.so
2、可以使用ld的"-s"(消除所有符号信息)和"-S"(清除调试符号信息)参数,使链接器生成输出文件不产生符号信息3
3、可以在gcc中通过"Wl,-s"和"Wl,-S"给ld传递参数
共享库的安装
1、使用系统的root权限将共享库复制到某个标准的共享库目录,如/lib、/usr/lib等,然后运行ldconfig
2、在编译程序时,GCC使用"-L"和"-l",分别用于指定共享库搜索目录和共享库的路径
共享库的构造和析构函数
1、GCC提供的方法
- 共享库的构造函数:在函数声明时加上_ _ attribute _ _((constructor))的属性
- 共享库的析构函数:在函数声明时加上 _ _ attribute _ _((destructor))的属性
void __attribute__((constructor)) init_function(void);
void __attribute__((destructor)) finit_function(void);
2、使用这种构造和析构函数,必须使用系统默认的标准运行库和启动文件,即不可以使用GCC的"-nostartfiles"或"-nostdlib"这两个参数
3、有多个构造和析构函数,默认情况下,被执行顺序没有规定,可以添加一个参数,指定构造和析构函数的优先级
void __attribute__((constructor(1))) init_function1(void);
void __attribute__((constructor(2))) init_function2(void);
4、对于构造函数,属性优先级数字越小的函数将会在优先级数字大的函数之前运行
5、对于析构函数,刚好相反,属性优先级数字越大的函数将会在优先级数字小的函数之前运行
共享库脚本
通过一定格式的链接脚本,可以把几个现有的共享库通过一定的方式组合起来,从用户角度形成一个新的共享库
GROUP(/lib/libc.so.6 /lib/libm.so.2)
Linux命令行
1、file命令查看相应的文件格式
2、gcc -c SImpleSection.c (参数-c表示只编译不链接)
3、objdump -h SimpleSection.o
- -s 参数可以将所有段的内容以十六进制的方式打印出来
- -h 把文件的各个段基本信息打印出来
- -d 参数可以将所有包含指令的段反汇编
- -t 查看段名符号
- -r 查看目标文件的重定位表
- -R
4、readelf -h SimpleSection.o
- -h 查看ELF文件头部信息
- -S 查看ELF文件的节section
- -s 查看ELF文件的符号
- -l 查看ELF的程序头,描述ELF文件如何映射到进程的虚拟空间
- -d 查看".dynamic"段的内容
- -sD 查看ELF文件的动态符号表以及哈希表
- -r 查看动态链接文件的重定位表
5、size SimpleSection.o (查看ELF文件的代码段、数据段和BSS段的长度)
7、objcopy
8、nm SimpleSection.o 查看文件符号表
9、strip foo strip命令去掉ELF文件中的调试信息
10、ar -t libc.a 查看静态库文件包含了哪些目标文件
- -x 解压静态库文件中的所有目标文件到当前目录
11、查看进程的虚拟空间分布
cat /proc/PID(如21963)/maps
//第一列是VMA的地址范围
//第二列是VMA的权限,“r”表示可读,“w”表示可写,“x”表示可执行,“p”表示私有(VOW Copy on Write),“s”表示共享
//第三列是偏移,表示VMA对于的Segment在映像文件中的偏移
//第四列表示映像文件所在设备的主设备号和次设备号
//第五列表示映像文件的节点号
//第六列是映像文件的路径
12、ldd Program 查看一个程序主模块或共享模块依赖哪些共享库