编译链接
ELF格式的文件分为4类:
ELF文件类型 说明
可重定位文件 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类. Linux的.o Windows的.obj
可执行文件 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名. /bin/bash Windows的.exe
共享目标文件 这种文件包含了代码和数据,可以在以下两种情况下使用.
第一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件.
第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行. Linux的.so /lib/glibc-2.5.so Windows的.DLL
核心转储文件 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件. Linux的core dump
ELF结构
ELF Header
.text
.data
.bss
... other sections
Section header table
String Tables
Symbol Tables
/usr/include/elf.h
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ 0x7f 0x45 0x4c 0x46:‘DEL’ ‘E’ ‘L’ ‘F’ ;0x00/0x01/0x02 无效/32位/64位; 0x00/0x01/0x02 无效/小端/大端 ;0x01 ELF主版本号;后9个字节未定义
Elf32_Half e_type; /* Object file type */ ELF文件类型 1(ET_REL,可重定位文件); 2(ET_EXEC,可执行文件); 3(ET_DYN,共享目标文件)
Elf32_Half e_machine; /* Architecture */ ELF文件的CPU平台属性 3(EM_386,Intel x86); 62(EM_X86_64,AMD x86-64); 40(EM_ARM ARM); 50(EM_IA_64,Intel Mecred)
Elf32_Word e_version; /* Object file version */ ELF版本号 一般为1
Elf32_Addr e_entry; /* Entry point virtual address */ ELF程序入口虚拟地址.可重定位值为0
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */ 段表偏移
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */ ELF文件本身大小
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */ 段描述符的大小,sizeof(Elf32_Shdr)
Elf32_Half e_shnum; /* Section header table entry count */ 段表数量
Elf32_Half e_shstrndx; /* Section header string table index */ 段表字符串表所在段在段表中的下标
} Elf32_Ehdr;
readelf -h
objdump -h
段描述符结构
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */ 段名字符串在“.shstrtab”中的偏移
Elf32_Word sh_type; /* Section type */ 操作系统不关心段名,只关心类型和标志位. (SHT_NULL,无效段)(SHT_PROGBITS,程序段.代码段,数据段都是这种类型)(SHT_SYMTAB,符号表)(SHT_STRTAB,字符串表)(SHT_RELA,重定位表)(SHT_HASH,符号表的哈希表)(SHT_DYNAMIC,动态链接信息)(SHT_NOTE,提示信息)(SHT_NOBITS,该段在文件中没有内容)(SHT_REL,重定位信息)(SHT_DNYSYM,动态链接的符号表)
Elf32_Word sh_flags; /* Section flags */ (SHF_WRITE,该段在进程空间中可写),(SHF_ALLOC,该段在进程空间中须要分配空间),(SHF_EXECINSTR,该段在进程空间中可以被执行)
Elf32_Addr sh_addr; /* Section virtual addr at execution */ 如果该段可以被加载,则为该段被加载后在进程地址空间的虚拟地址
Elf32_Off sh_offset; /* Section file offset */ 如果该段存在于文件中,表示该段在文件中的偏移
Elf32_Word sh_size; /* Section size in bytes */ 段的长度
Elf32_Word sh_link; /* Link to another section */ 如果段的类型是与链接相关的,如重定位表,符号表等,那么sh_link,sh_info两个成员的意义如下表所示
Elf32_Word sh_info; /* Additional section information */ (sh_type,sh_link,sh_info)=>{{SHT_DNNAMIC,该段所使用字符串表在段表的下标,0},{SHT_HASH,该段所使用的符号表在段表的下标,0},{SHT_REL SHT_RELA,该段所使用相应符号表在段表的下标,该重定位表所作用的段在段表的下标},{SHT_SYMTAB SHT_DYNSYM,操作系统相关的,操作系统相关的},{other,SHN_UNDEF,0}}
Elf32_Word sh_addralign; /* Section alignment */ 段地址对齐,有些段对段地址对齐有要求 如:段开始位置包含了一个double变量,Intel x86系统要求浮点数的存储地址必须为8字节整数倍,因此sh_addr必须是8的整数倍 sh_addrlign=3
Elf32_Word sh_entsize; /* Entry size if section holds table */ 有些段包含了一些固定大小的项,如符号表.表示每个项的大小.
} Elf32_Shdr;
readelf -S
ELF符号表结构
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */ 符号名,该符号名在字符串表中的下标
Elf32_Addr st_value; /* Symbol value */ 符号相对应的值,不为SHN_COMMON时为段内偏移; 是SHN_COMMON时为该符号的对齐属性. 可执行文件中表示符号的虚拟地址.
Elf32_Word st_size; /* Symbol size */ 符号大小.如果该值为0,表示该符号大小为0或未知
unsigned char st_info; /* Symbol type and binding */ 符号绑定信息(4位):{STB_LOCAL,局部符号,对于目标文件外部不可见},{STB_GLOBAL,全局符号,外部可见},{STB_WEAK,弱符号}. 符号类型(4位):{STT_NOTYPE,未知类型符号},{STT_OBJECT,数据对象,如变量,数组},{STT_FUNC,该符号是个函数或其他可执行代码},{STT_SECTION,表示一个段,必须是STB_LOCAL的},{STT_FILE,表示文件名,一定是STB_LOCAL,SHN_ABS}
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */ 符号所在段,特殊常量:{SHN_ABS,表示该符号包含了一个绝对的值},{SHN_COMMON,表示该符号是一个“COMMON块”类型的符号,一般来说未初始化的全局符号定义就是这种类型的},{SHN_UDNEF,表示符号未定义,表示该符号在本目标文件被引用,但定义在其他目标文件中}
} Elf32_Sym;
readelf -s
nm
ld会为我们定义很多特殊符号,可以直接声明并引用它.其实这些符号被定义在ld链接器的链接脚本中.链接器会在程序最终链接成可执行文件时候将其解析成正确的值.只有使用ld链接产生最终可执行文件时这些符号才会存在.
__executable_start 程序的起始地址,程序最开始的地址而不是入口地址
__etext/_etext/text 代码段结束地址
_edata/edata 数据段结束地址
_end/end 程序结束地址
符号修饰与函数签名
UNIX下的C语言规定,源代码文件中所有全局变量和函数经过编译后相对应的符号名前加上下划线“_”.Fortran为符号前后加下划线.解决了多种语言目标文件之间的符号冲突概率.
但现在的Linux下的GCC编译器默认已经去掉了C语言符号前加“_”. 但Windows平台下的编译器还保持这样的传统. 可以用GCC编译器选项-fleading-underscore -fno-leading-underscore
C++由于有类、继承、虚机制、名称空间等这些特性
GCC的基本C++名称修饰方法:
所有符号都以_Z开头
对于嵌套的名字(在名称空间或在类里面),后面紧跟N,然后是各个名称空间和类的名字,每个名字前是名字字符串的长度,再以“E”结尾.
对于函数来说,它的参数列表紧跟再“E”后.
可以用c++filt工具来解析被修饰过的名称. c++filt _ZN1N1C4funcEi
Visual C++的名称修饰规则:
名字以“?”开头
接着是以“@”结尾的函数名
后面跟着“@”结尾的名称空间再一个“@”表示函数的名称空间结束
第一个A表示调用类型为“__cdecl”
接着是函数的参数类型及返回值,由“@”结束
最后由“Z”结尾
C++可以用“extern "C"” 来声明或定义一个C符号.C++编译器会将在大括号内部代码当作C语言代码处理,C++名称机制将不再起作用.
可以用__cplusplus来区分是不是C++代码.
强符号弱符号
__attribute((weak))
__attribute__((nocommon)) / -fno-common 把所有未初始化的全局变量不以COMMON块的新式处理,然后就变成了强符号了.
强引用弱引用
__attribute((weakref))
对于未定义的弱引用,链接器默认其为0
静态链接
空间地址分配
按序叠加: 直接将各个目标文件依次合并,但有多个文件输入时将会有很多零散的段,并且由于每个段都有一定的地址空间和空间对齐要求,因此这样非常浪费空间.
相似段合并: 将相同性质的段合并在一起,如所有输入文件的“.text”段合并到输出文件的“.text”段. <==== 一般采用
第一步: 空间与地址分配 扫描所有输入目标文件,获得各个段长度,属性,位置,并且将输入目标文件的符号表所有符号定义与引用搜集起来放入一个全局符号表.将所有段合并计算输出文件中各个段长度与位置,并建立映射关系.
符号地址的确定
Linux下ELF可执行文件默认从地址0x08048000分配,这时候各个段在链接后的虚拟地址就已经确定了.链接器开始计算各个符号的虚拟地址.
因为各个符号在段内的相对位置是固定的,只不过链接器需要给每个符号加上一个偏移量.
第二部: 符号解析与重定位 使用上一步的信息,读取输入文件中段的数据与重定位信息,并进行符号解析与重定位,调整代码中的地址等.
.rel.text .rel.data 可以用objdump -r 来查看重定位表
重定位表是一个Elf32_Rel数组
typedef struct
{
Elf32_Addr r_offset; /* Address */ 重定位入口的偏移. 对于可重定位文件来说,是重定位入口所需要修正的位置的第一个字节相对于段起始偏移.对于可执行文件或共享对象来说是重定位入口所需要修正的第一个字节的虚拟地址.
Elf32_Word r_info; /* Relocation type and symbol index */ 重定位入口的类型和符号. 低8位为类型,高24位为重定位入口的符号在符号表中的下标.
} Elf32_Rel;
objdump -R
链接器查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位.
动态链接
相比与静态链接
对于全局和静态数据的访问都要进行复杂的GOT定位,然后间接寻址.
对于模块间的调用也要先定位GOT,然后再进行跳转.
动态链接的链接工作在运行时完成.程序开始执行时,动态链接器都要进行一次链接工作.动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址重定位等工作.
地址无关代码(Position-independent Code) -fPIC/-fpic : “-fPIC”产生的代码要大 “-fpic”产生的代码相对较小而且较快. 地址无关代码都是跟硬件平台相关的,不同平台有着不同实现,“-fpic”在某些平台会有一些限制,比如全局符号的数量或代码长度等,而“-fPIC”则没有这样的限制.
让程序模块中的共享指令部分在装载时不需要因为装载地址的改变而改变,所以把指令中需要被修改的部分分离开来,和数据放在一起.这样指令部分就不需要改变,而数据部分可以在每个进程中拥有一个副本.
模块内函数调用、跳转
模块内跳转、函数调用都可以时相对地址调用,或者时基于寄存器的相对调用.不需要重定位.
模块内数据访问
现代体系结构中,数据的相对寻址往往没有相对于当前指令的寻址方式,所以ELF运用了一个很巧妙的方法来得到当前PC值.
通过辅助函数来返回当前的返回地址(move (%esp),%ecx)从再加上固定的偏移量就可以访问模块内部数据了.
模块外函数调用、跳转
可以和外部数据访问一样,GOT相对应的项为保存的是目标函数的地址,当模块需要调用目标函数时可以通过GOT中的项进行间接跳转.
上面的方法简单,但存在性能问题.实际上ELF采用了一种更加复杂和精巧的方法. ———— 延迟绑定(Lazy Binding)
动态链接下,程序模块间包含了大量的函数引用(全局变量往往比较少,因为大量的全局变量会导致模块之间耦合度变大),所以程序开始时,动态链接会耗费不少时间用于解决模块间的函数引用的符号查找以及重定位.
不过可能很多函数在程序执行完时都不会被用到(比如一些错误处理函数或者时一些用户很少用到的功能模块等),如果一开始就把所有函数都链接好实际上是一种浪费.所以ELF采用了一种叫延迟绑定的做法.基本思想就是当函数第一次被调用到才进行绑定(符号查找,重定位等),如果没用到就不进行绑定.
ELF使用过程链接表(Procedure Linkage Table,PLT)来实现延迟绑定,在中间又增加了一层间接跳转.调用函数不直接通过GOT跳转,而是通过PLT项的结构来进行跳转.
ELF将GOT拆成了2个表:.got(保存全局变量地址) .got.plt(保存函数引用的地址)
PLT[0] 跳转到动态链接器 每项16字节
pushq *GOT[1]
jmpq *GOT[2]
PLT[1] 调用系统函数__libc_start_main初始化环境,调用main函数处理返回值
PLT[2] 调用用户代码调用的函数
jmpq *GOT[4]
pushq $0x1 符号引用在重定位表.rel.plt的下标
jmpq PLT[0] 将真正的地址填入GOT中
GOT[0] addr of .dynamic
GOT[1] addr of reloc entries
GOT[2] addr of dynamic linker 动态链接器在ld-linux.so模块的入口点
GOT[3] sys startup
GOT[4..] 指向对应PLT[2..]
GOT[1] GOT[2]由动态链接器在装载共享模块时负责将它们初始化.
模块外数据访问
在数据段中建立一个指向这些变量的指针数组,也被称为全局偏移量表(GOT,Global Offset Table),当代码需要引用这些全局变量时可以通过GOT的相对应的项间接引用. 而GOT相对于指令的偏移可以编译时确定.
PIC是不会有代码段重定位表的. 因此可以通过 readelf -d *** | grep TEXTREL 来确定是否为PIC(TEXTREL 代表代码段重定位表地址)
地址无关代码除了用在共享对象上也可以用在可执行文件. 地址无关可执行文件(Position-independent Executable,PIE) 参数PIE的参数为-fPIE/-fpie
共享模块的全局变量问题
当一个模块引用了一个定义在共享对象的全局变量时.当编译器编译这个模块时无法判断global是定义在同一个模块的其他目标文件上还是另外一个共享对象上(即无法判断是否是跨模块引用)
如果模块是可执行文件的一部分,由于程序主模块的代码并不是地址无关代码,因此链接器在创建可执行文件时在“.bss”段创建一个副本.而变量却定义在共享变量上.
为了解决这个问题,ELF共享库编译时,默认都把定义在模块内的全局变量当作定义在其他模块的全局变量.通过GOT表进行访问.如果变量在共享模块中初始化,动态链接器还需要将该初始值复制到程序主模块的该变量副本.
数据段的地址无关性
如果数据段有绝对地址引用(static int* p=&a),编译器和链接器会产生一个重定位表,重定位表里包含了“R_386_RELATIVE”类型的重定位入口. 当动态链接器装载共享对象时,如果发现该共享对象有这样的重定位入口,那么动态链接器就会对该共享对象进行重定位.
实际上也可以让代码段也使用这种装载时重定位的方法,而不使用地址无关代码(不用-fPIC).但这样代码就不是地址无关,就不能被多个进程共享.
动态链接情况如下
可执行文件的装载与静态链接基本相似.
首先操作系统读取可执行文件的头部,检查文件的合法性,然后从头部的“Program Header”中读取每个“Segment”的虚拟地址、文件地址和属性,并将它们映射到虚拟空间的相应位置.
在静态链接的情况下,操作系统接着就可以把控制权转交给可执行文件的入口地址,然后程序开始执行.
在动态链接的情况下,由于可执行文件依赖于很多共享对象,可执行文件中很多外部符号的引用还处于无效地址的状态,即还没有和相应的共享对象中的实际位置链接起来.所以映射完可执行文件后,操作系统会启动一个动态链接器(Dynamic Linker)————ld.so(文件位置位于.interp段).
操作系统同样通过映射的方式将它加载到进程的地址空间中,在加载完后把控制器交给动态链接的入口地址.动态链接器开始执行一系列自身初始化操作,然后根据当前环境参数,开始对可执行文件进行动态链接工作.所有链接完成后把控制权交给可执行文件入口地址.
进程初始化的时候,堆栈里面保存了关于进程执行环境和命令行参数等信息.事实上,堆栈里面还保存了动态链接器所需要的一些辅助信息数组(Auxiliary Vector)
typedef struct
{
uint32_t a_type; /* Entry type */
union
{
uint32_t a_val; /* Integer value */
/* We use to have pointer elements added here. We cannot do that,
though, since it does not work when using 32-bit definitions
on 64-bit platforms and vice versa. */
} a_un;
} Elf32_auxv_t;
a_type a_val
AT_NULL(0) 表示辅助信息数组结束
AT_EXEFD(2) 表示可执行文件的文件句柄.当进程开始执行可执行文件时,操作系统会先将文件打开,这时候就会产生文件句柄.
那么操作系统可以将文件句柄传递给动态链接器,动态链接器可以通过操作系统的文件读写操作来访问可执行文件
AT_PHDR(3) 可执行文件中程序头表(Program Header)在进程中的地址.正如前面AT_EXEFD所提到的,动态链接器可以通过操作系统的文件读写功能来访问可执行文件.
但事实上,很多操作系统会把可执行文件映射到进程的虚拟空间里面,从而动态链接器不需要通过读写文件,而是可以直接访问内存中的文件映像.
所以操作系统要么选择前面的文件句柄方式,要么选择这种映像的方式.当选择映像的方式时,操作系统必须提供后面的AT_PHENT,AT_PHNUM和AT_ENTRY这几个类型
AT_PHENT(4) 可执行文件头中程序头表中每一个入口(Entry)的大小
AT_PHNUM(5) 可执行文件头中程序头表中入口(Entry)的数量
AT_BASE(7) 表示动态链接器本身的装载地址
AT_ENTRY(9) 可执行文件入口地址,即启动地址
.dynamic段 readelf -d
动态链接最重要的结构应该是“.dynamic”段,这个段里保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的位置等.
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;
ldd
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_NEED 依赖的共享对象文件,d_ptr表示所依赖的共享对象文件名
DT_REL DT_RELA 动态链接重定位表地址
DT_RELENT DT_RELAENT 动态重定位表入口数量
内核虚拟共享对象(Kernal Virtual DSO)
位于内核区域,即地址末端的区域
动态链接重定位表 readelf -r
共享对象需要重定位的主要原因是导入符号的存在.动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他对象,也就是说由导入符号时,那么它的代码或数据中就会有对于导入符号的引用.在编译时这些导入符号的地址未知,在静态链接中,这些位置的地址引用在最终链接时被修正.但在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需重定位.
PIC的代码段不需要重定位(为地址无关),但数据段包含了绝对地址的引用,因为代码段中绝对地址相关的部分被分离了出来,变成了GOT,而GOT是数据段的一部分.
动态链接中有与静态链接类(.rel.text .rel.data)似的重定位表叫做“.rel.dyn”和“.rel.plt”.“.rel.dyn”实际上是对数据引用的修正,它修正的位置位于“.got”以及数据段;而“.rel.plt”是对函数引用的修正,它所修正的位置位于“.got.plt”.
动态链接的步骤基本上分为3步:先是启动动态链接器本身,然后装载所有需要的共享对象,最后是重定和初始化
动态链接器自举(对于动态链接器本身来说,它的重定位工作由谁来完成?它是否可以依赖于其他的共享对象?)
首先是,动态链接器本身不可以依赖于其他任何共享对象;其次是动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成.这种具有一定限制条件的启动代码往往被称为自举(Bootstrap).
动态链接器入口地址即是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码即开始执行.自举代码首先会找到它自己的GOT.而GOT的第1个入口保存的即是“.dynamic”段的偏移地址,由此找到了动态连接器本身的“.dynamic"段.通过“.dynamic”中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位.从这一步开始,动态链接器代码中才可以开始使用自己的全局变量,静态变量.动态链接器本身的函数.
装载共享对象
完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table).
然后链接器开始寻找可执行文件所依赖的共享对象,我们前面提到过“.dynamic”段中,有一种类型的入口是DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象.由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中.然后链接器开始从集合里取1个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的ELF文件头和“.dynamic”段,然后将它相应的代码段和数据段映射到进程空间中.如果这个ELF 共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中.如此循环直到所有依赖的共享对象都被装载进来为止,当然链接器可以有不同的装载顺序,如果我们把依赖关系看作一个图的话,那么这个装载过程就是一个图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图,这取决于链接器,比较常见的算法般都是广度优先的.
当一个新的共享对象被装载进来的时候,它的符号表会被合并到全局符号表中,所以当所有的共享对象都被装载进来的时候,全局符号表里面将包含进程中所有的动态链接所需要的符号.
重定位和初始化
链接器重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT中的每个需要重定位的位置进行修正.因为此时的动态链接器已经有了进程的全局符号表,所以这个修正过程显得比较容易.
重定位完成后,如果某个共享对象有“.init”段,动态链接器会执行“.init”段中的代码,用以实现共享对象特有的初始化过程(如共享对象中的C++全局/静态对象的构造).相应地,还可能有“.finit”段,当进程退出时会执行“.finit”段中地代码(类似C++全局对象析构之类的函数)
如果可执行文件也有“.init”段,那么动态链接器不会执行它,因为可执行文件中的“.init”段和“.finit”段由程序初始化部分代码负责执行.
Linux动态链接器实现
通过execve()系统调用,程序被装载到进程的地址空间.
内核在装载完ELF可执行文件以后就返回到用户空间,将控制权交给程序的入口.对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的.
静态链接的可执行文件:程序的入口就是ELF文件头的e_entry指定的入口.
动态链接的可执行文件:内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器.
动态链接器不仅是个共享对象,还是个可执行的程序.(除了文件头的标志位和扩展名有所不同外,其他都一样).共享库和可执行文件实际上没有什么区别.Windows由rundll32.exe可以把一个DLL当作可执行文件运行(利用了运行时加载原理,将指定的共享对象在运行时加载进来,然后找到某个函数DLL中是DllMain开始执行).
显示运行时链接(运行时加载)
通过动态链接器提供的API(dlopen,dlsym,dlerror,dlclose)对动态库进行操作.API实现在/lib/libdl.so.2里 它们的声明和相关常量被定义在系统标准头文件<dlfcn.h> gcc -ldl表示使用DL库,位于/lib/libdl.so.2
void* dlopen(const char* filename, int flag) 打开一个动态库,并将其加载到进程的地址空间,完成初始化过程(执行“.init”段的代码).
第一个参数是被加载动态库的路径,如果路径是绝对路径,则该函数会尝试直接打开该动态库;如果是相对路径,那么dlopen()会尝试以一定顺序取查找该动态库文件:(1)环境变量LD_LIBRARY_PATH指定的一系列目录.(2)由/etc/ld.so.cache指定的共享库路径.(3)/lib /usr/lib.这个顺序与旧的a.out装载器顺序相反(先/usr/lib 再 /lib).
如果filename设置为0,dlopen返回全局符号表的句柄.
第二个参数flag表示函数符号的解析方式.
常量RTLD_LAZY表示使用延迟绑定,当函数第一次被用到时才进行绑定,即PLT机制.
常量RTLD_NOW 表示当模块被加载时即完成所有函数绑定工作,如果由任何未定义的符号引用的绑定工作没法完成,那么dlopen()就返回错误. 这两种方式必须选其一.
常量RTLD_GLOBAL可以和上面的两者中任意一个一起使用,表示将被加载的模块的全局符号合并到进程的全局符号表中,使得以后加载的模块可以使用这些符号.
dlopen的返回值是被加载的模块的句柄,这个句柄用在dlsym或dlclose中.如果模块加载失败返回NULL.如果模块已经通过dlopen加载过了,返回的是同一个句柄.
void* dlsym(void* handle, char* symbol);
第一个参数是由dlopen()返回的动态库的句柄.
第二个参数即所要查找的符号的名字,一个以“\0”结尾的C字符串.
如果dlsym()找到了相应的符号,则返回该符号的值;没有找到相应符号,则返回NULL.如果查找的符号是函数,则返回函数的地址;如果是变量,返回变量地址.如果是常量,返回常量的值.可以通过dlerror()区分常量和未找到符号(符号找到dlerror返回NULL,没找到返回相应错误信息).
char* dlerror()
每次调用dlopen(),dlsym(),dlclose()以后都可以调用dlerror()函数来判断上一次是否调用成功.如果返回NULL,则表示上一次调用成功;如果不是,则返回相应的错误消息.
dlclose()
每次使用dlopen()加载模块时,相应的计数器加1;每次使用dlclose()卸载模块时,相应的计数器减1.只有当计数器值减到0时,模块才被真正地卸载掉.
卸载地过程跟加载相反,先执行“.finit”段地代码,然后将相应地符号从符号表中去除,取消进程空间跟模块地映射关系,然后关闭模块文件.
共享库版本
libfoo.so.x(SO-NAME,软链接) => libfoo.so.x.y.z(x:主版本号,major version number; y:次版本号,minor version number; z:发布版本号,release version number).主版本号代表重大更新,不同主版本号的库之间是不兼容的.次版本号代表增量更新,增加一些接口符号,保持原来的符号不变(高的次版本号向后兼容低的次版本号).发布版本号代表库的错误修正,性能改进,并不添加或修改任何接口(不同发布版本号之间完全兼容).
但libc-x.y.z.so 和 ld-x.y.z.so不遵循这种规定. libc.so.6 => libc-2.6.1.so; ld-linux.so => ld-2.6.1.so
每个共享库都有一个SO-NAME(共享库文件名.主版本号),系统会为每个共享库在它所在目录创建一个跟“SO-NAME”相同且指向它的软连接(Symbol Link),并且指向目录中主版本号相同,次版本号和发布版本号最新的共享库.使得所有依赖某个模块在编译链接和运行时都使用共享库的SO-NAME,而不使用详细的版本号.当动态链接器进行共享库依赖文件查找时,会根据系统中共享库目录中的SO-NAME软链接到最新的共享库.
Linux提供了“ldconfig”,它会遍历所有默认的系统库(/lib;/usr/lib等),然后更新所有的软链接,使它们指向最新的共享库.如果由新安装的共享库,ldconfig会为其创建相应的软链接.
如果需要链接libXXX.so.2.6.1只需要在编译器命令行指定-lXXX即可,编译器会根据环境,在系统相关路径(-L参数)查找最新的XXX库.会根据输出文件的情况(动态/静态)来选择适合的库. -static使用静态库.
符号版本
由于当程序依赖于较高的次版本号的共享库但运行于较低版本的共享库时,就可能会产生缺少某些符号的错误.因为次版本号只向后兼容,不保证向前兼容.对于这个问题,现代系统提供了符号版本机制.
Linux Glibc在2.1版本后开始支持基于符号的版本机制(Symbol Versioning).基本思路是让每个导出导入符号都有一个相关联的版本号,实际做法类似于名称修饰.新版本新添加的符号打上新标记.
可以通过符号版本脚本或在GCC的C/C++源代码嵌入汇编指令的模式使用:asm(".symver add, add@VERS_1.1);
GCC允许不同版本的同一符号存在同一库中
当用ld链接一个共享库时,可以使用“--version-script”参数.用GCC使用“-Xlinker --version-script”将version-script的参数传给ld链接器.
符号版本脚本 lib.ver
VERS_1.2 {
global:
foo;
locol:
*;
}
共享库查找过程
一个动态链接的模块所依赖的模块路径保存在“.dynamic”段里,由DT_NEED类型项表示.
如果DT_NEED是绝对路径,则动态链接器会尝试直接打开该动态库;
如果是相对路径,会尝试以一定顺序取查找该动态库文件:
(1)环境变量LD_LIBRARY_PATH指定的一系列用冒号隔开的目录. (类似于/lib/ld-linux.so.2 -library-path 一些列目录) LD_LIBRARY_PATH也会影响GCC编译时查找库的路径,相对于链接时GCC的“-L”
(2)由/etc/ld.so.cache指定的共享库路径.
(3)/lib /usr/lib.这个顺序 /etc/ld.so.conf
为了节约查找时间,linux通过ldconfig程序使每个共享库的SO-NAME指向正确的共享库文件,并且将这些SO-NAME收集起来存放到/etc/ld.so.cache里面.
如果动态链接器在/etc/ld.so.cache没有找到所需要的共享库,就会遍历/lib /usr/lib目录,如果还没找到就宣告失败.
所以很多软件安装包在安装程序在往系统里面安装共享库以后都会调用ldconfig.
LD_PRELOAD环境变量指定的文件会在动态链接器按固定搜索规则搜索共享库之前装载.无论程序是否依赖它们,LD_PRELOAD里面指定的共享库胡哦目标文件都会被装载(因此会覆盖掉后面相同的符号名). /etc/ld.so.preload文件和LD_PRELOAD一样.
LD_DEBUG,当设置这个变量时,动态链接器会在运行时打印各种信息.
当LD_DEBUG的值为“bindings”时:显示动态链接的符号绑定过程.
当LD_DEBUG的值为“libs”时:显示共享库的查找过程.
当LD_DEBUG的值为“versions”时:显示符号的版本依赖关系.
当LD_DEBUG的值为“reloc”时:显示重定位过程.
当LD_DEBUG的值为“symbols”时:显示符号表查找过程.
当LD_DEBUG的值为“statistics”时:显示动态链接过程中的各种统计信息.
当LD_DEBUG的值为“all”时:显示以上所有信息.
当LD_DEBUG的值为“help”时:显示上面的各种可选值的帮助信息.
共享库的创建 (gcc -shared -Wl,-soname,my_soname -o library_name source_files library_files)
创建共享库过程于一般共享对象过程基本一致.最关键使用GCC的两个参数“-shared和-fPIC”.“-shared”表示输出结果时共享库类型的,“-fPIC”表示使用地址无关代码.“-Wl”参数可以将指定参数传递给链接器(如“-Wl,-soname,my_soname”,当不使用-soname指定共享库的SO-NAME,那共享库就没有SO-NAME且ldconfig对该共享库无效果).
LD_LIBRARY_PATH可以指定共享库的查找路径,也可以用链接器的“-rpath”(GCC的-Wl,-rpath). ld -rpath /home/mylib -o program.out program.o -lsomelib
正常编译出来的共享库或可执行文件带符号信息和调试信息.但对于最终发布版本来说,用处不大.可以使用“strip”工具清除掉共享库或可执行文件的所有符号和调试信息. ld的“-s”(gcc,-Wl,-s)消除所有符号信息, ld的“-S”(gcc,-Wl,-S)消除调试符号信息.
共享库的安装,最简单就是将共享库复制到某个标准的共享库目录(如/lib,/usr/lib)然后运行ldconfig. 或通过指定共享库所在目录(ldconfig -n 目录),在编译程序时也需要指定共享库位置,GCC提供了“-L”,“-l”分别为共享库搜索目录和共享库的路径.当然也可以使用前面提到的“-rpath”.
可以为共享库被装载时设定初始化代码,只需要在函数声明时加上“__attribute__((constructor))”属性,指定该函数为共享库的构造函数,这种属性的函数会在共享库加载时被执行.而“__attribute__((destructor))”属性会在main()函数执行完后/exit()后执行,这种语法是GCC对C/C++的扩展,其他编译器不通用.当指定这两个参数时,不可以使用GCC的“-nostartfiles”后“-nostdlib”因为构造和析构函数实在系统默认的标准运行库或启动文件里面被运行的. 如果有多个构造函数,以提供参数作为优先级.
共享库不仅可以是动态链接的ELF共享对象文件(.so),也可以是符合一定格式的链接脚本文件.通过这种脚本文件,把几个现有共享库组合起来(libfoo.so的内容可以如下:GROUP /lib/libc.so.6 /lib/libm.so.2).
=====================================================================================================================
cl参数:
/Za
/Zl 关闭默认C库的链接指令
=====================================================================================================================
COFF
Image Header(IMAGE_FILE_HEADER)
Section Table(IMAGE_SECTION_HEADER[])
.text
.data
.drectve // 编译器传递给链接器的命令行参数
.debug$S // 所有以.debug开始的段都包含着调试信息: .debug$S:符号相关调试信息段 .debug$P:预编译头相关的调试信息段 .debug$T:类型相关的调试信息段
... other sections
Symbol Table
//VC\PlatformSDK\include\WinNT.h
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // Windows目前只有x86平台,因此一般为0x14C
WORD NumberOfSections; // 段的数量
DWORD TimeDateStamp; // PE文件创建时间
DWORD PointerToSymbolTable; // 符号表在PE中的位置
DWORD NumberOfSymbols; //
WORD SizeOfOptionalHeader; // Optional Header大小,只存在于PE可执行文件,COFF目标文件中为0
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8]; // 段名
union {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 该段被加载至内存后的大小
} Misc;
DWORD VirtualAddress; // 该段被加载至内存后的虚拟地址
DWORD SizeOfRawData; // 该段在文件中的大小
DWORD PointerToRawData; // 段在文件中的位置
DWORD PointerToRelocations; // 该段的重定位表在文件中的位置
DWORD PointerToLinenumbers; // 该段的行号表在文件中的位置
WORD NumberOfRelocations; //
WORD NumberOfLinenumbers; //
DWORD Characteristics; // 段的属性,属性里包含的主要是段的类型(代码,数据,bss),对齐方式,可读可写可执行权限.
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
=====================================================================================================================
PE
Image DOS Header(IMAGE_DOS_HEADER) //Image DOS Header和DOS Stub是为了兼容DOS系统而设计的.IMAGE_DOS_HEADER结构跟DOS的"MZ"可执行结构的头完全一样.其中的e_cs,e_ip指向文件中的DOS Stub,用于输出“This Program cannot be run in DOS”并结束. e_lfanew的值表示IMAGE_NT_HEADERS在文件中的偏移,如果不为0就是Windows PE可执行文件.
Image DOS Stub
PE File Header(IMAGE_NT_HEADERS)
Image Header(IMAGE_FILE_HEADER)
Image Optional Header(IMAGE_OPTIONAL_HEADER32)
Section Table(IMAGE_SECTION_HEADER[])
.text
.data
.drectve
.debug$S
... other sections
Symbol Table
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // 标记是一个常量,值为0x00004550 小端字节序 ‘P’‘E’‘\0’‘\0’
IMAGE_FILE_HEADER FileHeader; // 映像头 与COFF目标文件结构一样
IMAGE_OPTIONAL_HEADER OptionalHeader;//PE扩展头部结构, 对于PE可执行文件(包括DLL)是必需的.
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数组里面每一个元素都对应一个包含一定含义的表. WinNT.h内定义了一些以“IMAGE_DIRECTORY_ENTRY_”开头的宏,数值从0到15. 有导出表,导入表,资源表,异常表,重定位表,调试信息表,线程私有存储(TLS)等的地址和长度.
} IMAGE_OPTIONAL_HEADER_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
DLL
DLL的扩展名不一定为dll,也有可能是别的比如.ocx(OCX控件),.CPL(控制面板程序).Windows下的DLL和EXE文件实际上是一个概念,它们都是PE格式的二进制文件,稍有不同的是PE文件头部汇总有个符号位表示该文件是EXE或是DLL.
PE里有基地址(Base Address)和相对地址(RVA,Relative Virtual Address).当一个PE文件被装载时,其进程地址空间的起始地址就是基地址.对于任何一个PE文件来说都有一个优先装载的地址(Image Base).一个可执行EXE文件来说Image Base值为0x400000,DLL为0x1000000.Windows在装载DLL时会先尝试把它装载到Image Base指定的地址,若被占用PE装载器会选用其他空闲地址.而相对地址就是相对于基地址的偏移.
DLL可以有共享的数据段(用DLL来实现进程间通信) => 1个共享段 1个数据段
在ELF中,共享库中所有全局函数和变量默认情况下都可以被其他模块使用,但DLL中,需要显示地告诉编译器需要导出地符号.当使用DLL导出符号时这个过程称为导入.
使用__declspec(dllexport)/__declspec(dllimport)导出/导入符号(其实是通过目标文件的编译器指示来实现的,即“.drectve”段 objdump /DIRECTIVES xxx.obj).在C++中如果希望导入或导出符合C语言的符号修饰规范,需要在符号的定义前加上extern "C"防止C++编译器进行符号修饰.
除了使用“__declspec”扩展关键字指定导入导出符号外还可以用“.def”文件(模块定义文件)来声明导入导出符号.“.def”扩展名的文件类似于ld链接器的链接脚本文件,可以被当作link链接器的输入文件,用于控制链接过程.也可以在链接时提供/EXPORT参数(link math.obj /DLL /EXPORT:_Add)
//Math.def文件
LIBRARY Math
EXPORTS
Add=_Add@16 //对导出函数重命名(别名)
Sub @1 //@后面所跟的值为符号的序号值
Mul @2 NONAME //NONAME表示该符号仅以序号的形式导出,即Math.dll的使用者看不到Mul符号名.
Div
cl Math.c /LD /DEF Math.def => (c1 /c Math.c; link /DLL /DEF:Math.def Math.obj; dumpbin /EXPORT Math.dll)
cl /LDd Math.c => 生成 Math.dll Math.obj(编译的目标文件) Math.exp Math.lib(不包含Math.c的代码和数据,而是用来描述Math.dll的导出符号,包含了TestMath.o链接Math.dll所需要的导入符号以及一部分“桩”代码,以便将程序于DLL黏在一起,像Math.lib这样的文件又被称为导入库(Import Library))
/LDd 表示产生Debug版的DLL
/LD 表示生成Release版的DLL
不加任何参数产生EXE可执行文件
DLL显式运行时链接
LoadLibrary(char *)/LoadLibraryEx(): 用来装载一个DLL到进程的地址空间,与dlopen类似
GetProcAddress(HINSTANCE,char*): 用来查找某个符号的地址,与dlsym类似
FreeLibrary(HINSTANCE): 用来卸载某个已加载的模块,与dlclose类似
符号导出导入表
导入表
ELF将导出符号保存在“.dynsym”段中.Windows PE中,所有导出的符号被集中存放在了导出表结构中(提供了一个符号名与符号地址的映射关系).
PE文件头的DataDirectory的第一个元素就是导出表的地址和长度.导出表是一个IMAGE_EXPORT_DIRECTORY结构体被定义在Winnt.h中
//@[comment("MVI_tracked")]
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image 导出地址表(EAT,Export Address Table)
DWORD AddressOfNames; // RVA from base of image 符号名表(Name Table),函数名按ASCII顺序排序,以便动态链接器在查找函数名时可以用二分查找.
DWORD AddressOfNameOrdinals; // RVA from base of image 名字序号对应表(Name-Ordinal Table),一个导出函数的序号就是函数在EAT中的地址下标加上一个Base值(也就是IMAGE_EXPORT_DIRECTORY中的Base,默认情况为1)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
现在基本不用需要作为导入导出手段,而直接使用符号名.符号名作为导出方式时可选的.一个DLL中每一个导出函数都有一个对应唯一的序号值,而导出函数名却是可选的.
假设模块A导入了Math.dll中的Add函数.A的导入表保存了“Add”函数名,动态链接时动态链接器在Math.dll的函数名表中进行二分查找,找到“Add”函数再通过名字序号对应表找到“Add”的序号(即1),减去Math.dll的Base值1(即0),然后再EAT中找到下标0的元素,即“Add”的RVA为0x1000.
在DLL创建过程中,链接器会临时创建一个exp文件.链接器第一遍遍历所有目标文件并收集所有导出符号信息并创建DLL导出表,为方便,链接器把导出表放到一个临时EXP目标文件的“.edata”段中.第二遍就把EXP文件当作普通目标文件与其他输入的目标文件链接一起输出DLL.这时候EXP文件中的“.edata”段也就会被输出到DLL文件中并且成为导出表.(一般把它合并到只读数据段“.rdata”中)
可以在DEF文件中将某个导出符号重定向到另外一个DLL.这样导出表的地址数组元素执行的RVA位于导出表中,代表了这个符号被重定向了,而这个RVA指向了一个ASCII的字符串(它是重定向后的DLL文件名和符号名)
EXPORTS
HeapAlloc = NTDLL.RtlAllocHeap
导入表 dumpbin /IMPORTS xxx.dll
如果在某个程序使用了来自DLL的函数或变量,就把这种行为叫符号导入(Symbol Importing).相应的结构叫导入表(Improt Table).
ELF中“.rel.dyn”和“.rel.plt”分别保存了该模块需要导入的变量和函数的符号以及所在的模块等信息.而“.got”,“.got.plt”保存这些变量和函数的真正地址.
当某个PE文件加载时,Windows加载器其中一个任务就是将所有需要导入的函数地址确定并且将导入表的元素调整到正确地址.系统装载器会确保任何一个模块的依赖条件都得到满足.
导入表是一个IMAGE_IMPORT_DESCRIPTOR数组,每一个IMAGE_IMPORT_DESCRIPTOR对应一个被导入的DLL
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) 指向一个数组叫做导入名称表(Import Name Table,INT),这个数组和IAT一模一样,里面的数值也一模一样.
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; 需要导入的DLL名称
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) 指向一个导入地址数组(IAT,Import Address Table),每个元素对应一个被导入的符号,元素的值在不同情况下有不同的含义.
在动态链接器刚完成映射还没开始重定位和符号解析时,元素值表示相对于的导入符号的序号或者是符号名(最高位置1低31位为导入符号的序号值,
否则元素值指向一个IMAGE_IMPORT_BY_NAME结构的RVA,其结构的内的Hint表示可能的序号,Name表示符号名).
当使用符号名导入时,动态链接器会先使用“Hint”值去定位该符号在目标文件导出表的位置,如果未命中才用二分查找进行符号查找.
当动态链接器完成模块链接时,元素值被动态链接器改写成符号真正地址.从这一点看,导入地址数组与ELF中的GOT非常类似.
} IMAGE_IMPORT_DESCRIPTOR;
Windows的动态链接器在装载一个模块时,改写导入表的IAT.但与ELF的.got不同的是IAT是只读的,位于“.rdata”段中.
延迟载入(Delayed Load)
当链接一个支持延迟载入的DLL时,链接器会产生与普通DLL导入非常类似的数据.当延迟载入的API第一次被调用时,由链接器添加的特殊的桩代码就会启动,负责装载DLL,通过调用GetProcAddress来找到被调用API的地址.
调用导入函数: 调用IAT中的某一项(CALL DWORD PTR [0x0040D11C]//调用40D11C里的地址,即调用IAT某项内的地址) 和 ELF通过GOT间接跳转十分类似(在不考虑PLT的情况下).
因为是直接将地址常量写入指令中,PE DLL的代码段并不是地址无关的.事实上使用了重定基地址的方法.
为了让编译器区分函数是从外部导入还是模块内部定义的,MSVC引入了之前用过的__declspec(dllimport),这样编译器就知道是外部导入的,以便产生相应的指令形式.
在__declspec关键字引入之前,对于导入函数的调用,编译器不区分导入函数和导出函数,它统一产生直接调用指令.但链接器在链接时会将导入函数目标地址指向一小段桩代码(Stub),由桩代码再将控制权交给IAT中的目标地址.即对于调用函数来说,只是产生一般形式的指令“CALL XXXXXXXX”,然后链接时链接器把这个XXXXXXXX地址重定位到一段桩代码(位于LIB文件,即导入库),然后通过IAT间接跳转到导入函数.
编译器再产生导入库时,同一个导出函数会产生两个符号定义.如函数foo在导入库中有两个符号,一个是foo另外一个是__imp__foo.foo指向foo函数的桩代码,__imp__foo指向foo函数在IAT中的位置.所以用“__declspec(dllimport)”来声明foo导入函数时,编译器在编译时会在该导入函数前加上前缀“__imp__”以确保跟导入库中的“__imp__foo”正确链接.如果不使用“__declspec(dllimport)”那么编译器产生一个正常的foo符号引用以便和导入库中的foo符号定义链接.
DLL 优化
DLL的代码段和数据段都不是地址无关的,因此在装载时如果地址被占用就会引起DLL的Rebase.
DLL模块被装载时,如果目标地址被占用,那么操作系统就会分配一块新空间,并将DLL装载到该地址.因为DLL的代码段不是地址无关的,因此需要对所有涉及到绝对地址引用都进行重定位.只需要加上一个目标装载地址和实际装载地址的插值.
DLL内部的地址都是基于基地址或相对于基地址的RVA.那么所有需要重定位的地方只需要加上固定的插值,因此这个重定位相对简单,速度也比一般的重定位快.PE里面把这种特殊的重定位叫做重定基地址(Rebasing).
PE文件的重定位信息都放在了“.reloc”段.可以从PE文件头DataDirectory里得到重定位段的信息.对于EXE文件来说,MSVC编译器默认不产生重定位段,因为EXE文件是进程运行时第一个装到虚拟空间中的,它的地址不会被抢占.而一般情况下都会给DLL产生重定位信息(用“/Fixed”参数禁止DLL产生重定位信息).
但这种方法相对于ELF的代码段地址无关来说更加浪费内存,而且被重定基址的代码段需要换出时,需要将它写到交换空间中,而不像没有重定基址的DLL代码段,只需要释放物理页面,再次用到可以直接从DLL文件重新读取代码段.
但这样有个好处,它比ELF的PIC机制有着更快的运行速度.因为PE的DLL对数据段的访问不需要通过类似于GOT的机制,对于外部数据和函数引用不需要每次都计算GOT的位置.
可以用“/BASE”指定dll的基地址(link /BASE:0x10010000, 0x10000 /DLL bar.obj). 基地址必须是64K的倍数,0x10000是DLL占用空间允许的最大长度.也可以用editbin工具(早期版本的MSVC提供rebase.exe):editbin /REBASE:BASE=0x10020000 bar.dll
系统DLL如kernel32.dll,ntdll.dll,shell32.dll,user32.dll,mscvrt.dll等基本上是Windows应用程序运行时都要用到的.Windows系统就在进程空间中专门划出0x70000000~0x80000000区域用于映射这些常用的系统DLL.但为了提高安全性,首选地址可能不再是DLL实际加载地址,而是在内存中随机分配实际地址.
导入函数绑定:
大多数情况下DLL都会以同样的顺序被装载到同样的内存地址,所以它们的导出符号地址都是不变的.既然导出符号地址不变,那么每次运行程序时都要进行符号的查找、解析和重定位就有点浪费.如果把导出函数的地址保存到模块的导入表中呢?
editbin /BIND TestMath.exe //editbin对被绑定的程序的导入符号进行遍历查找,找到后就把符号的运行时的目标地址写入到被绑定程序的导入表内.和IAT一样的INT数组就是用来保存绑定符号的地址的.
dumpbin /IMPORTS TestMath.exe
但是当DLL更新导致导出函数地址发生变化或DLL在装载时发生了重定基址就会导致绑定的地址失效.
由于在绑定时对于每个导入的DLL,链接器把DLL的时间戳(Timestamp)和校验和(CheckSum,如MD5)保存到文件的导入表中.在运行时,Windows会核对要被装载的DLL与绑定时的DLL版本是否相同,并确认该DLL没有发生重定基址.如果一切正常,就不需要进行符号解析过程了.因为被装载的DLL与绑定时一样,没有发生变化.否则,就会忽略绑定的符号地址,按照正常解析过程对DLL的符号解析.
C++与动态链接
C++标准只规定了语言层面的规则,而对二进制级别却没有任何规定.
如:DLL某接口返回指针但DLL不负责指针的释放工作,用户用完指针需要调用delete释放.但如果DLL所使用的CRT版本与用户主程序或者其他DLL所使用的CRT版本不一样,就会发生内存释放错误.由于每个CRT都会有自己独立的堆,在一个CRT中申请内存而在另一个CRT中释放就导致释放出错.
当DLL某类增加新成员变量,新版对象占用字节增多而原先程序主模块对象实例化时,相当于实例化了旧版的对象,实际上相对于申请了旧版的字节数,调用构造函数时由于新版认为对象有更多字节,当访问超出旧版的字节时实际上这块区域并不属于对象,导致程序崩溃.
组件对象模型(COM,Component object model)就是为了解决在程序开发中遇到的兼容性问题. 《COM本质论》
在Windows平台下(有些意见对Linux/ELF也有效),要尽量遵循以下几个指导意见:
所有接口函数都应该是抽象的.所有的方法都应该是纯虚的.(或者inline的方法也可以)
所有全局函数都应该使用extern "C"来防止名字修饰的不兼容.并且导出函数都应该是__stdcall调用规范的(COM的DLL都使用这样的规范).这样即使用户本身的程序是默认以__cdecl方式编译的.对DLL的调用也能够正确.
不要使用C++标准库STL.
不要使用异常.
不要使用虚析构函数.可以创建一个destroy()方法并且重载delete操作符并调用destroy().
不要在DLL里面申请内存,而且在DLL外释放(或者相反).不同的DLL和可执行文件可能使用不同的堆,在一个堆里面申请内存而在另外一个堆里面释放会导致错误.比如,对于分配内存相关的函数不应该是inline的,以防止它在编译时被展开到不同的DLL和可执行文件.
不要在接口中使用重载方法(Overloaded Methods,一个方法多重参数).因为不同的编译器对于vtable的安排可能不同.
DLL 版本控制
静态链接,在编译产生应用程序时使用静态链接的方法链接它所需要的运行库,从而避免使用动态链接.
防止DLL覆盖(DLL Stomping),使用Windows文件保护(Windows File Protection,WFP)来阻止未经授权的应用程序覆盖系统的DLL.第三方应用程序不能覆盖系统DLL文件,除非它们的安装程序捆绑了Windows更新包,或者它们的安装现在安装程序运行时禁止了WFP服务.
避免DLL冲突,让每个应用程序拥有一份自己依赖的DLL,并且把问题DLL的不同版本放到该应用程序的文件夹汇总,而不是系统文件夹中.当应用程序需要装置DLL时,先从自己的文件夹下寻找需要的DLL再去系统文件中寻找.
.NET下解决DLL Hell解决方案:
在.NET框架中,一个程序集(Assembly)有两种类型:应用程序程序(exe可执行文件)集、库程序(DLL动态链接库)集.一个程序集包括一个或多个文件.所以需要一个清单文件(Manifest文件)描述程序集.Manifest文件描述了程序集的名字,版本号以及程序集的各种资源,同时也描述了该程序集运行所依赖的资源(DLL及其他资源文件等).Manifest是一个XML描述文件,每个DLL有自己的Manifest文件,每个应用程序也有自己的Manifest.对于应用程序而言,manifest文件可以和可执行文件再同一目录下,也可以是作为一个资源嵌入到可执行文件的内部(Embed Manifest).
XP以后的操作系统,在执行可执行文件时则会首先读取程序集的manifest文件去寻找对于的DLL并调用.<assemblyIdentity>属性里面还有type系统类型,version版本号,processorArchitecture平台环境,publicKeyToken公钥.所有这些加在一起成了“强文件名”.就可以根据其区分不同的版本、不同的平台.即使有多个不同版本的相同的库共存也不会发生冲突.
SxS Manager(Side-by-side Manager)利用程序集manifest文件描述,实现对相应的DLL的加载.对于每个版本DLL,在WinSxS目录下都有独立的目录,目录的命名包括了机器类型、名字、公钥和版本号.有了Manifest这种机制后动态链接的C/C++程序在运行时必须在系统中有与它在Manifest里面所指定的完全相同的DLL,否则系统就会提示运行出错,这也是为什么很多Visual C++2005或2008编译的程序无法在其他机器上运行的原因,因为它们需要与编译环境完全相同的运行库支持,所以这些程序发布的时候往往都要带上相应的运行库.\VC\redist\x86\,比如C语言运行库就在该目录下的“Microsoft.xxx.CRT”,MFC运行库位于“Microsoft.xxx.MFC”.

浙公网安备 33010602011771号