1.1 空间与地址分配:
对于多个输入目标文件,链接器如何将它们各个段合并到输出文件?
1.1 按需叠加 直接将各个目标文件依次合并,该做法会产生很多内存碎片,不是一个好的解决方案
1.2 相似段合并 将相同性质的段合并到一起,比如将所有输入文件的“.text”合并到输出文件的“.text”段,接着是“.data”段,“.bss”段
“连接器为目标文件分配地址和空间”这句话中的“地址和空间”其实有两个含义:
1)在输出的可执行文件中的空间
2)在装载后的虚拟地址中的虚拟地址空间
相似段合并分两步:“两步链接”法
1) 空间与地址分配 扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中的所有符号定义和符号引用收集起来,
统一放到一个全局符号表 并建立映射关系
2) 符号解析与重定位 使用上面第一步收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等,
第二步是链接的核心,特别是重定位过程
ld a.o b.o -e main -o ab
-e main 表示将main函数作为程序的入口,ld链接器默认的程序入口为_start
-o ab 表示链接输出文件名为ab,默认为a.out
Linux下,ELF可执行文件默认从地址0x08048000开始分配
1.3 符号地址的确定
在第一步的扫描和空间的分配阶段,链接器按照前面介绍的空间分配方法进行分配,这时候输入文件中的各个段在连接后的虚拟地址就已经确定了。
具体的符号内容,可以通过符号在段内的相对位置算出: 段基址+偏移
1.4 符号解析与重定位
以2个源文件 a.c 和 b.c 为例子:
a.c:
#include "stdio.h"
extern int shared;
int main()
{
int a = 100;
swap(&a,&shared);
}
b.c:
int shared = 1;
void swap(int *a,int *b)
{
*a ^= *b ^= *a ^= *b;
}
gcc -c a.c b.c
objdump -r a.objdump
root@ubuntu:/opt/workspace/four# objdump -r a.o
a.o: file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000015 R_386_32 shared
00000021 R_386_PC32 swap
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
00000020 R_386_PC32 .text
重定位表:专门的结构用来保存与重定位相关的信息,重定位表也可以叫做重定位段,".text"被重定位的地方,会有一个相应的".rel.text"的段保存了代码段的重定位表
每个要被重定位的地方叫做一个重定位入口,我们可以看到a.o 有两个重定位入口。
重定位入口的偏移表示该入口要被重定位的段中的位置
1.4.2 符号解析
重定位的过程也伴随着符号解析
重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用就行重定位时,他就要确定这个符号的目标地址,这个过程叫做符号解析
链接器在重定位的时候会去查找所有输入目标文件的符号表组成的全局符号表,找到相应的符号后就行重定位
root@ubuntu:/opt/workspace/four# readelf -s a.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 5
8: 00000000 39 FUNC GLOBAL DEFAULT 1 main
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
如上,shared和swap都是UND类型的,即未定义类型。这种未定位的符号都是因为该目标文件中有关于他们的重定位项。所以在链接器扫描完所有输入目标文件后,
,所有的这些符号都应该能够在全局符号表中找到,否则链接器就报符号未定位错误
1.4.3 指令修正方式
32位x86平台下的ELF文件的重定位入口的修正指令的寻址方式只有两种:
绝对近址32位寻址
相对近址32位寻址
R_386_32 1 绝对寻址修正 S+A
R_386_PC32 2 相对寻址修正 S+A-P
绝对寻址修正和相对寻址修正的区别就是绝对寻址修正后的地址为该符号的实际地址
相对地址寻址修正后的地址为符号距离被修正位置的地址差
1.4.4 COMMON 块
当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号,那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占
空间比本编译单元该符号所占空间大。
GCC 的"-info-common"也允许我们把所有未初始化的全局变量不以COMMOM块的形式处理,或者使用"__artribute__(nocommon)"扩展
1.5 C++相关问题
C++的一些语言特性使之必须由编译器和链接器共同支持才能完成工作。最主要的有两个方面:
1) C++的重复代码消除
2) 全局构造与析构
重复代码:C++编译器在很多时候都可能会产生重复代码,比如模板、外部内联函数和虚函数表都有可能在不同的编译单元中生成相同的代码
重复代码消除,一个比较有效的做法是将每个模板的实例代码都单独放在一个段里,每个段只包含一个模版实例。链接器在最终链接的时候可以区分这些模板实例,然后将他们合并入最后的代码段
GCC把这种类似的须在最终链接时合并的段叫“Link Once”,他们的做法是将这种类型的段命名为“.gun.linkonce.name”,其中name是该模板函数实例的修饰名称
Visual C++编译器提供了一个编译选项叫函数级别链接(Funciton-Level Linking/Gy)
GCC 编译器也提供了类似的机制,他们有两个选择分别是"-ffunction-sections"和“-fdata-sections”。这两个选项的作用就是将每个函数或变量分别保持到独立的段中
1.5.1 全局构造与析构:
C++的全局对象的构造函数在main之前被执行,C++全局对象的析构函数在main之后被执行
Linux系统下一般程序的入口是"__start",这个函数是Linux(Glibc)的一部分。当我们的程序与Glibc库链接在一起形成最终的可执行文件以后,这个函数就是程序的初始化部分的入口
ELF文件定义了两个特殊的段:
.init 该段里面保存的是可执行指令,它构成了进程的初始化代码。
.fini 该段保存着进程终止代码指令
1.5.2 C++与ABI
我们把符号修饰标准、变量内存布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI
对于C语言的目标代码来说,以下几个方面决定目标文件之间是否二进制兼容:
内置类型的大小和在存储器中的放置方式(大端、小端、对齐方式)
组合类型的存储方式和内存分布
外部符号与用户定义的符号之间的命名方式和解析方式
函数调用方式,比如参数入栈顺序,返回值如何保存等
堆栈的分布方式,比如参数和局部变量在堆栈里的位置,参数传递方式等
寄存器使用约定,函数调用时哪些寄存器可以修改,哪些需要保存
C++:
继承类体系的内存分布,比如基类,虚基类在继承类中的位置
指向成员函数的指针的内存分布
如何调用虚函数,vtable的内容和分布形式,vtable指针在object中的位置等
template如何实例化
外部符号的修饰
全局对象的构造和析构
异常的产生和捕获机制
标准库的细节问题,RTTI如何实现等
内嵌函数访问细节
1.6 静态库链接
一个静态库可以简单地看成一些目标文件的集合
Linux上人们通常使用‘ar’压缩程序将这些目标文件压缩到一起,并且对其进行编号和索引,以便查找和检索,就形成了libc.a这个静态库文件
root@ubuntu:/usr/lib/i386-linux-gnu# ar -t libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
dso_handle.o
errno.o
init-arch.o
errno-loc.o
hp-timing.o
iconv_open.o
iconv.o
iconv_close.o
gconv_db.o
…………
使用objdump查看printf函数所在的目标文件
root@ubuntu:/usr/lib/i386-linux-gnu# objdump -t libc.a
printf.o: file format elf32-i386
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .comment 00000000 .comment
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 g F .text 00000024 __printf
00000000 *UND* 00000000 stdout
00000000 *UND* 00000000 vfprintf
00000000 g F .text 00000024 printf
00000000 g F .text 00000024 _IO_printf
操作系统内核,从本质上讲,他本身也是一个程序,比如windows的内核ntoskrnl.exe就是一个我们平常看到的PE文件,它位于\Windows\system32\ntoskrnl.exe。内核就是这个文件
使用GCC命令编译“hello.c”,使用“-verbose”表示将整个编译链接过程的中间步骤打印出来
1.7 BFD库
BFD库是一个希望通过一种统一的借口来处理不同的目标文件格式的GNU项目
如下这段程序能显示出BFD库所支持的所有目标文件格式
root@ubuntu:/study/four# cat target.c
/* target.c */
#include <stdio.h>
#include "bfd.h"
int main()
{
const char **t = bfd_target_list();
while(*t)
{
printf("%s\n",*t);
t++;
}
}
root@ubuntu:/study/four# gcc -o target target.c -lbfd
root@ubuntu:/study/four# ./target
elf32-i386
a.out-i386-linux
pei-i386
elf32-little
elf32-big
elf64-x86-64
elf32-x86-64
pei-x86-64
elf64-l1om
elf64-k1om
elf64-little
elf64-big
plugin
srec
symbolsrec
verilog
tekhex
binary
ihex
trad-core
对于多个输入目标文件,链接器如何将它们各个段合并到输出文件?
1.1 按需叠加 直接将各个目标文件依次合并,该做法会产生很多内存碎片,不是一个好的解决方案
1.2 相似段合并 将相同性质的段合并到一起,比如将所有输入文件的“.text”合并到输出文件的“.text”段,接着是“.data”段,“.bss”段
“连接器为目标文件分配地址和空间”这句话中的“地址和空间”其实有两个含义:
1)在输出的可执行文件中的空间
2)在装载后的虚拟地址中的虚拟地址空间
相似段合并分两步:“两步链接”法
1) 空间与地址分配 扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中的所有符号定义和符号引用收集起来,
统一放到一个全局符号表 并建立映射关系
2) 符号解析与重定位 使用上面第一步收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等,
第二步是链接的核心,特别是重定位过程
ld a.o b.o -e main -o ab
-e main 表示将main函数作为程序的入口,ld链接器默认的程序入口为_start
-o ab 表示链接输出文件名为ab,默认为a.out
Linux下,ELF可执行文件默认从地址0x08048000开始分配
1.3 符号地址的确定
在第一步的扫描和空间的分配阶段,链接器按照前面介绍的空间分配方法进行分配,这时候输入文件中的各个段在连接后的虚拟地址就已经确定了。
具体的符号内容,可以通过符号在段内的相对位置算出: 段基址+偏移
1.4 符号解析与重定位
以2个源文件 a.c 和 b.c 为例子:
a.c:
#include "stdio.h"
extern int shared;
int main()
{
int a = 100;
swap(&a,&shared);
}
b.c:
int shared = 1;
void swap(int *a,int *b)
{
*a ^= *b ^= *a ^= *b;
}
gcc -c a.c b.c
objdump -r a.objdump
root@ubuntu:/opt/workspace/four# objdump -r a.o
a.o: file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000015 R_386_32 shared
00000021 R_386_PC32 swap
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
00000020 R_386_PC32 .text
重定位表:专门的结构用来保存与重定位相关的信息,重定位表也可以叫做重定位段,".text"被重定位的地方,会有一个相应的".rel.text"的段保存了代码段的重定位表
每个要被重定位的地方叫做一个重定位入口,我们可以看到a.o 有两个重定位入口。
重定位入口的偏移表示该入口要被重定位的段中的位置
1.4.2 符号解析
重定位的过程也伴随着符号解析
重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用就行重定位时,他就要确定这个符号的目标地址,这个过程叫做符号解析
链接器在重定位的时候会去查找所有输入目标文件的符号表组成的全局符号表,找到相应的符号后就行重定位
root@ubuntu:/opt/workspace/four# readelf -s a.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 5
8: 00000000 39 FUNC GLOBAL DEFAULT 1 main
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
如上,shared和swap都是UND类型的,即未定义类型。这种未定位的符号都是因为该目标文件中有关于他们的重定位项。所以在链接器扫描完所有输入目标文件后,
,所有的这些符号都应该能够在全局符号表中找到,否则链接器就报符号未定位错误
1.4.3 指令修正方式
32位x86平台下的ELF文件的重定位入口的修正指令的寻址方式只有两种:
绝对近址32位寻址
相对近址32位寻址
R_386_32 1 绝对寻址修正 S+A
R_386_PC32 2 相对寻址修正 S+A-P
绝对寻址修正和相对寻址修正的区别就是绝对寻址修正后的地址为该符号的实际地址
相对地址寻址修正后的地址为符号距离被修正位置的地址差
1.4.4 COMMON 块
当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号,那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占
空间比本编译单元该符号所占空间大。
GCC 的"-info-common"也允许我们把所有未初始化的全局变量不以COMMOM块的形式处理,或者使用"__artribute__(nocommon)"扩展
1.5 C++相关问题
C++的一些语言特性使之必须由编译器和链接器共同支持才能完成工作。最主要的有两个方面:
1) C++的重复代码消除
2) 全局构造与析构
重复代码:C++编译器在很多时候都可能会产生重复代码,比如模板、外部内联函数和虚函数表都有可能在不同的编译单元中生成相同的代码
重复代码消除,一个比较有效的做法是将每个模板的实例代码都单独放在一个段里,每个段只包含一个模版实例。链接器在最终链接的时候可以区分这些模板实例,然后将他们合并入最后的代码段
GCC把这种类似的须在最终链接时合并的段叫“Link Once”,他们的做法是将这种类型的段命名为“.gun.linkonce.name”,其中name是该模板函数实例的修饰名称
Visual C++编译器提供了一个编译选项叫函数级别链接(Funciton-Level Linking/Gy)
GCC 编译器也提供了类似的机制,他们有两个选择分别是"-ffunction-sections"和“-fdata-sections”。这两个选项的作用就是将每个函数或变量分别保持到独立的段中
1.5.1 全局构造与析构:
C++的全局对象的构造函数在main之前被执行,C++全局对象的析构函数在main之后被执行
Linux系统下一般程序的入口是"__start",这个函数是Linux(Glibc)的一部分。当我们的程序与Glibc库链接在一起形成最终的可执行文件以后,这个函数就是程序的初始化部分的入口
ELF文件定义了两个特殊的段:
.init 该段里面保存的是可执行指令,它构成了进程的初始化代码。
.fini 该段保存着进程终止代码指令
1.5.2 C++与ABI
我们把符号修饰标准、变量内存布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI
对于C语言的目标代码来说,以下几个方面决定目标文件之间是否二进制兼容:
内置类型的大小和在存储器中的放置方式(大端、小端、对齐方式)
组合类型的存储方式和内存分布
外部符号与用户定义的符号之间的命名方式和解析方式
函数调用方式,比如参数入栈顺序,返回值如何保存等
堆栈的分布方式,比如参数和局部变量在堆栈里的位置,参数传递方式等
寄存器使用约定,函数调用时哪些寄存器可以修改,哪些需要保存
C++:
继承类体系的内存分布,比如基类,虚基类在继承类中的位置
指向成员函数的指针的内存分布
如何调用虚函数,vtable的内容和分布形式,vtable指针在object中的位置等
template如何实例化
外部符号的修饰
全局对象的构造和析构
异常的产生和捕获机制
标准库的细节问题,RTTI如何实现等
内嵌函数访问细节
1.6 静态库链接
一个静态库可以简单地看成一些目标文件的集合
Linux上人们通常使用‘ar’压缩程序将这些目标文件压缩到一起,并且对其进行编号和索引,以便查找和检索,就形成了libc.a这个静态库文件
root@ubuntu:/usr/lib/i386-linux-gnu# ar -t libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
dso_handle.o
errno.o
init-arch.o
errno-loc.o
hp-timing.o
iconv_open.o
iconv.o
iconv_close.o
gconv_db.o
…………
使用objdump查看printf函数所在的目标文件
root@ubuntu:/usr/lib/i386-linux-gnu# objdump -t libc.a
printf.o: file format elf32-i386
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .comment 00000000 .comment
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 g F .text 00000024 __printf
00000000 *UND* 00000000 stdout
00000000 *UND* 00000000 vfprintf
00000000 g F .text 00000024 printf
00000000 g F .text 00000024 _IO_printf
操作系统内核,从本质上讲,他本身也是一个程序,比如windows的内核ntoskrnl.exe就是一个我们平常看到的PE文件,它位于\Windows\system32\ntoskrnl.exe。内核就是这个文件
使用GCC命令编译“hello.c”,使用“-verbose”表示将整个编译链接过程的中间步骤打印出来
1.7 BFD库
BFD库是一个希望通过一种统一的借口来处理不同的目标文件格式的GNU项目
如下这段程序能显示出BFD库所支持的所有目标文件格式
root@ubuntu:/study/four# cat target.c
/* target.c */
#include <stdio.h>
#include "bfd.h"
int main()
{
const char **t = bfd_target_list();
while(*t)
{
printf("%s\n",*t);
t++;
}
}
root@ubuntu:/study/four# gcc -o target target.c -lbfd
root@ubuntu:/study/four# ./target
elf32-i386
a.out-i386-linux
pei-i386
elf32-little
elf32-big
elf64-x86-64
elf32-x86-64
pei-x86-64
elf64-l1om
elf64-k1om
elf64-little
elf64-big
plugin
srec
symbolsrec
verilog
tekhex
binary
ihex
trad-core