-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathld4.txt
201 lines (109 loc) · 10.3 KB
/
ld4.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
==静态链接==
===空间和地址分配===
将两个目标文件a.o和b.o当做输入,输出即可执行文件"ab",其中的代码段,数据段等等都是互相合并得到.
[1]按序叠加,按照顺序分别将目标文件一个个叠加起来,目标文件多的情况下,会有很多零散段出现,然而每一个段都会要求有一定的地址和空间对齐,x86的段装载空间对齐单位是一"页"(4K),段数量越多,空间浪费越多.
[2]相似段合并,每个目标文件的相同段(例如text段)合并成一个段,再叠加起来.
这里说的分配空间的意义只局限于虚拟地址空间.
$ld a.o b.o -e main -o ab; //-e main表示将main函数作为程序入口
-static //表示ld以静态链接的方式,默认是动态链接
两步链接:
扫描所有的目标文件,获取所有段的各种属性,将目标文件的所有符号放入一个全局符号表里,将相同的段合并,计算出合并后段的长度位置等等,建立映射关系;
使用上一步收集到的信息,进行符号解析和重定位,调整代码中的地址等等.
通过查看链接后的"ab"的ELF文件头中VMA和LMA,说明虚拟空间是链接过程才进行分配,
linux中,ELF可执行文件虚拟地址默认从0x08048000开始分配.(而不是一般理想中的0x00000000开始分配)
[3]符号地址确定
段初始地址+符号所在该段的偏移量
===符号解析和重定位===
通过目标文件的反汇编"objdump -d",可以看到代码段的所有字节和相关的所有指令,a.c被编译为目标文件是,编译器并不知道其中引用变量的地址,它们都定义着b.c中,所以会暂时将其地址当做"0",链接中完成了空间个地址分配时,即链接器就会修正对应未知"符号"的地址,这个过程就叫做重定位.
修正后的地址就是它们的真正的被分配到的虚拟地址,汇编call指令后跟的调用指令的下一条的偏移量,call指令所在偏移量是80480bf,和下一条指令的相对偏移量是0x09,则要求的符号的偏移量为"80480c8".(指令修正/地址计算方式???)
重定位表:
保存重定位相关的信息,重定位的目标,调整方式等等.
objdump -r target.o //只显示目标文件需要重定位相关的地方,即哪些外部引用的符号,每一个符号都可以当做是一个重定位入口.
重定位表的结构体:
typedef struct{
Elf32_addr r_offset; //重定位入口偏移,需要修正的位置在所在段的偏移量
Elf32_Word r_info; //重定位入口类型和符号,这个成员的低8位表示重定位入口类型,高24位表示重定位入口的符号在符号表中的下标.
}Elf32_rel;
符号解析:
链接中如果输入的目标文件缺少某一些符号的定义或者是库文件,都会出现"未定义的引用"这样的错误,可以用"readelf -s"查看目标文件的符号表,看有哪些符号是属于"UND"的,这些即是外部引用的符号,需要去找到相应的定义这些符号的外部目标文件.
指令修正方式:
修正方式取决于使用的处理器平台,32位的x86平台重定位入口修正的指令寻址方式有两种.
绝对近址32位寻址 R_386_32 1 S+A
相对近址32位寻址 R_386_PC32 2 S+A-P
//S-符号实际地址,A-保存在被修正位置的值,P-被修正的位置,即修正位置在所在段的偏移量or虚拟位置(r_offset).
//绝对寻址修正修正后的结果即该符号的实际地址,相对寻址修正的结果等于修正位置的偏移量和符号目标位置偏移量的差值.
===Common块===
事前声明使用空间的大小,这种空间叫做common块.
common类型链接规则都是针对弱符号的.
多个弱符号或强符号类型不一致,链接器只知道符号的"名字",无法区别类型的不同.
多个弱符号-->取长度最长的那个;
弱符号和强符号-->取强符号;
强符号和强符号-->报错,不能重复定义强符号;
弱符号长度比强符号长-->报错,强符号长度比弱符号短;
之所以之前不会吧未初始化的全局变量也放入bss段也是因为可能在其他目标文件中有相同的弱符号出现,因此不能确定这些未定义的初始化全局变量的长度,因此也无法放入bss段中去为其预先分配空间,但是最终链接过程中读取所有输入目标文件后,确定长度后就可以放入BSS段了.
gcc用"-fno-common"在编译中来把所有未初始化的全局变量不以common块的形式处理,也可以用"int global _attribute_((nocommon))"处理单个符号.
===C++相关问题===
[1]重复代码消除:
c++里的模板,外部引用函数,虚函数表都可能在多个编译单元里产生相同的代码,这样会造成的问题主要有:空间浪费,地址易出错,指令运行效率低.
解决问题的方法多是将每个模板的实例代码都单独的存放在一个段里,这样当别的编译单元也使用这个模板时,就会生成相同名字的段,链接器在链接时也可以识别这些相同的模板实例的段,将它们合并在一起再放入最后的代码段.
gcc将这种要在链接时合并的段叫做".gun.linkonce.修饰后名称",便于识别; vc++编译器里的PE文件段表结构里的IMAGE_SECTION_HEADER成员中都有IMAGE_SCN_LNK_COMDAT(0x00001000)这个标记,链接器看到这个标记就认为该段是COMDAT类型的,即在链接时会将重复的这些段丢弃掉.
函数级别的链接:
有时候需要的只是目标文件中的某一个符号,不需要引用整个目标文件,所以可以选择将所有函数都放在单独的一个段中,最终链接的时候只需要将这一个段保留下来,去除其他的多余信息,不过这样的链接过程会比一般的长,因为需要计算每个符号之间的关系,段的数量也会很大,重定位过程也会变复杂.
gcc编译器提供了"-ffunction-sections"和"-fdata-sections"两个编译选项,用于将每个函数或变量分别保存在独立的段里.
vc++编译器也提供了一个编译选项"函数级别连接"用于独立每一个函数到段里.
[2]全局构造与析构
初始化执行环境(堆的初始化malloc,free,线程子系统)-->全局对象的构造函数-->main()-->全局对象的析构函数......
段".init"和".fini",前者是进程的初始化可执行代码,后者是进程终止代码指令,一个早于main函数,一个晚域main函数,由Glibc安排执行的顺序.
[3]c++和ABI
不同的编译器对于输出的目标文件的影响:目标文件格式,符号修饰标准,变量内存分布方式,函数调用方式.
以上这些和可执行代码二进制兼容性相关的内容都可以称为ABI(application binary interface).
影响ABI的问题主要还是在于硬件平台,编程语言,编译器,链接器,操作系统之间的不兼容,各个目标文件之间也无法相互链接.
C语言的二进制兼容性:内置类型(int,float...),字节序,组合类型的存储方式和内存分布,外部符号与用户定义符号之间的命名和解析方式,函数调用,堆栈分布方式,寄存器使用约定.
对于c++的ABI影响因素更多,这也是C++二进制兼容性很差的原因.(继承类体系的内存分布,指向成员函数的指针内存分布,虚函数调用,模块实例化,外部符号修饰,全局对象构造和析构,异常产生和捕获,标准库细节,内嵌函数访问细节......)
库厂商一般提供的库都是二进制文件.
===静态库链接===
一个程序的实质:输入输出
输入输出的介质:系统API,这些API往往都是一群目标文件的集合,我们称之为"库",这些库一般有编译器的厂商提供.
$ar -t libc.a 查看libc这个静态库包含的目标文件,ar是压缩程序
$gcc -c -fno-builtin hello.c //关闭内置函数优化,可能会自动替换更有效的函数
$gcc -c -fno-builtin --verbose -static hello.c //显示编译链接过程
ccl是gcc的c语言程序编译器,as是gnu的汇编器,最后的collect2其实是封装后的ld链接器,这里可以当做就是ld.
静态运行库里的每一个目标文件一般都只有一个函数,避免引用一个目标文件而带来过多的无用函数,减小空间的浪费.
===链接过程控制===
一般使用默认的链接规则,只有一些特定的程序特定的条件(输入输出,调试信息是否保存...)会需要加上特定的编译参数.
\WINDOWS\system32\ntoskrnl.exe --> windows内核文件
[1]链接控制脚本
放在编译命令的参数中; 放在目标文件中; 链接控制脚本文件(vc++编译器把其叫做模块定义文件,后缀是.def);
gcc的链接器ld的默认链接脚本位置:/usr/lib/ldscripts/
$ld -T link.scripts //指定自己定义的脚本为链接控制脚本
[2]tinytext
输入:程序源码+涉及的函数的静态库+?
一般编译链接过程:
$gcc -c -fno-builtin target.c
$ld -static -e nomain -o target.o target
输出:一个可执行文件,还可以精简其中不影响程序运行的段.
[3]使用ld链接脚本(.lds)
实质:对目标文件的段进行操作.
/*****以下是一个链接脚本实例*****/
ENTRY(nomain) //程序入口函数指定
SECTIONS
{
. =0x08048000 + SIZEOF_HEADERS; //.代表当前虚拟地址
tinytest : {*(.text) *(.data) *(.rodata)} //右边3种类型段全部合并后放在tinytest段里,中间的空格隔开是必须的,*即通配符,*(.data)即代表所有目标文件的data段都会被选中.
/DISCARD/ : {*(.comment)} //丢弃掉comment段
}
使用脚本编译链接的过程:
$gcc -c -fno-builtin tinytest.c
$ld -static -T tinytest.lds -o tinytest.o tintest
结果中只会有合并后的段,但是仍然会有字符串表和符号表一类,这里可以用strip命令清理掉.
[4]链接脚本语法
命令语句和赋值语句,凡是脚本语言中或多或少都会允许正则表达式,所以正则用法需要多复习,
ENTRY(target) 程序入口地址,采用优先级(ld命令行的-e > 链接脚本设置 > _start() > .text段起始位置 > 位置0)
STARTUP(filename) 将"filename"作为链接的第一个输入文件
SEARCH_DIR(path) 将"path"加入到ld的链接查找目录,效果同命令行里的"-Lpath"
INPUT(file_A,file_b,...) 将指定文件作为链接输入
INCLUDE file 类似于C中的头文件包含
PROVIDE(symbol) 在链接脚本中定义这个符号
特殊的命令例如SECTIONS见实例部分 - file1(.data .rodata) file1.o *(.data) ...
===BFD库===
为不同平台的目标文件提供一种抽象的统一模型,就像VFS对于各种文件系统类型一样,让这些目标文件之间可以链接,让不同的平台也可以识别多种格式的目标文件.