-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathld3.txt
213 lines (152 loc) · 12.2 KB
/
ld3.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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
==目标文件是什么==
[1]目标文件的格式:win端的PE(portable executable),linux端的ELF(executable linkable format),这些都属于unix里最早提出的COFF(common file format)格式的变种,obj和.o文件就是编译后但未链接之前的中间文件.
不光是可执行文件,库文件都可以以可执行文件的格式存储.
可重定位文件 .o .obj
可执行文件 bash .exe
共享目标文件 .so .dll
核心转储文件 core dump
目标文件和可执行文件格式与操作系统和编译器有关,
[2]目标文件的组成
编译后的机器指令代码(放在代码段),数据(放在数据段),以及链接所需的符号表,调试信息,字符串...所有这些属性信息通常以"段"的形式存储.
目标文件简要结构如下:
EFS/obj---file_header(链接时动态还是静态,文件是否可执行,入口地址,目标硬件,目标操作系统信息,包含一个段表用于描述文件中各个段在文件中的偏移位置及属性等等)---.text section(包含程序指令)---.data section(已经初始化的程序的变量)---.bss section(保存未初始化的程序的变量)
没有初始化的变量默认是为0的,因此没有必要放在data段占用空间,所以BSS(block started by symbol)段用于为未初始化的程序的变量预留位置,没有实际内容,也不占空间.
程序编译后主要分为程序指令对应的代码段和程序数据对应的data段/bss段,分段的好处在于.第一,程序被装载后,分为两个段分别映射到两个虚存区域,代码段往往是只可读的,数据段是可读写的,权限不同避免混淆;第二,分段利于适应如今大多CPU的数据缓存和指令缓存分离的设计情况;第三,系统中运行多个相同程序副本时,对于只读的数据和指令来说它们都是一样的,除去私有的资源其他都是可以是共享的资源,因此以共享的方法节约大量内存空间.
[3]挖掘
在用gcc进行编译以后得到.o目标文件,可以使用objdump工具进行反汇编来查看目标文件的结构和内容等等
$objdump -h(打印基本信息) target.o
$objdump -x(打印更多基本信息) target.o
$objdump -s(将所有段内容以16进制方式打印) target.o
$objdump -d(将所有指令反汇编) target.o
===代码段===
利用objdump观察代码段的结构,虽然-s -d参数的结果是16进制的,但也可以看到text段的结构,包含了源代码里的函数和指令等等,
===数据段和只读数据段===
data段保存所有已经初始化的变量,这里的大小取决于数据定义的类型和变量的数量,例如32位机中int就是4位个字节变量.
rodata段存放只读数据,一般存储程序里的只读变量(例如const修饰的变量)和字符串常量.
CPU的字节序(byte order),大端和小端区别,去决定变量的读取顺序.(0x54000000-->(大端序)0x54,0x00,0x00,0x00)
===BSS段===
存储未定义的变量,更准确的是为其预留了空间.
编译器的自动优化里会把值为0的变量当做是没有初始化的,因为未初始化的变量都是0,所以在存储是也会因为优化而将值为0的变量放在BSS段.
===其他"段"===
rodata1 comment debug dynamic(动态链接信息) hash(符号hash表) line note strtab(字符串表) symtab(符号表) shstrtab(段名表) plt got init fini
用户可以自己自定义段名,但是不能以"."为前缀.
将二进制文件作为目标文件中的一个段:
$objcopy -I binary -O elf32-i386 -B i386 target.jpg target.o
$objdump -ht target.o
用户自己指定变量所处的"段",为了满足某些硬件的内存和IO的地址布局:
_attribute_((section(*FOO*))) int global = 42; 变量global被放在了"FOO"段里
_attribute_((section(*BAR*))) void foo() 函数foo()被放在了"BAR"段里
{
}
==ELF文件结构==
[1]文件头
$readelf -h target.o 查看ELF文件的文件头
包含ELF魔数,文件机器字节长度,数据存储方式,版本,允许平台,ABI,ELF重定位类型,硬件平台,硬件平台版本,入口地址,程序头入口和长度,段表位置及长度/数量等等.
ELF文件头结构即相关常数被定义在"/usr/include/elf.h",分为32和64位版本,例如"Elf32_Ehdr":
typedef struct{
unsigned char e_ident[16];
...
}Elf32_Ehdr
魔数对应e_ident[16]的16个字节,所有ELF文件的一开始都固定4个字节的内容"0x7F,0x45,0x4c,0x46",0x7F是DEL控制符的ASCII码值,后面3个分别是E,L,F这3个字母的ASCII码值,第5个字节用来确定ELF文件类型(32还是64位),第6个字节确定字节序(小端/大端),第7个字节确定ELF文件主版本号,后9个字节一般为0,用于扩展.
文件类型对应结构体中的e_type, ET_REL 1 可重定位文件; ET_EXEC 2 可执行文件; ET_DYN 3 共享目标文件(.so);
硬件平台属性对应e_machine, 常量以"EM_"开头,例如 "EM_386 3 inter_x86"
[2]段表
段表记录着段的名字,长度,位置,权限等信息,段表自己所在的位置由"ELF文件头"的e_shoff决定,
&readelf -S target.o 显示详细的段表结构,和objdump使用有差别.
Elf32_Shdr段描述符结构体:以这种数组方式存储数据,便于引用.
段名,sh_name,都是字符串,在ELF文件头的结构体成员"shstrtab"字符串表中有所有段的名字
段的类型,sh_type,以"SHT_"开头,详细种类见P100-101.
段的标志位,sh_flags, SHF_WRITE 1 可写; SHF_ALLOC 2 需要被分配空间; SHF_EXECINSTR 4 可执行,一般指代码段.
段的链接信息,sh_link,sh_info,之和段类型是链接相关的有关系,例如SHT_DYNAMIC,SHT_HASH,SHT_REL,SHT_RELA,SHT_SYMTAB,SHT_DYNSYM......sh_link表示该段使用字符串表/符号表的下标,sh_info表示重定位作用于哪一个段或者0或者操作系统相关的,对于其他类型的段这两个参数没有任何意义.
[3]重定位表
段".rel.text",sh_type为SHT_REL,针对".text"代码段的重定位表.
[4]字符串表
例如strtab和shstrtab段,都是用这种"偏移"的方式来引用字符串,e_shstrndx就表示"段shstrtab"在当前所在段表中的"下标"数值.
可以由ELF文件头得到段表和段表字符串表的位置.
==链接的接口"符号"==
链接的本质:目标文件之间的对"符号"(函数和变量)的互相引用.
链接主要关注"符号"的耦合,主要关注是目标文件定义和引用的全局"符号".
===ELF符号表结构===
ELF符号表对应段".symtab",相关的结构体"Elf32_Sym",
typedef struct{
Elf32_word st_name; 包含该符号名在字符串表中的下标
Elf32_addr st_value; "值"
Elf32_word st_size; 符号值所属数据类型的大小
unsigned char st_info; 符号类型和绑定信息
unsigned char st_other; 0
Elf32_Half st_shndx; 符号所在段
}Elf32_Sym;
符号类型和绑定信息,st_info,低4位表示符号类型,高28位表示符号绑定信息.
符号类型:
STT_NOTYPE 0 未知类型
STT_OBJECT 1 数据对象
STT_FUNC 2 函数或可执行代码
STT_SECTION 3 段
STT_FILE 4 一般为目标文件对应的源文件名
符号绑定信息:
STB_LOCAL 0 局部符号
STB_GLOBAL 1 全局符号
STB_WEAK 2 弱引用??
符号所在段,st_shndx,本目标文件中的符号,则st_shndx表示符号所在段在段表中的下标,如果符号不是定义在本目标文件中,则情况如下:
SHN_ABS 0xfff1 该符号包含一个绝对值,表示文件名的符号就属于这种类型的???
SHN_COMMON 0xfff2 表示该符号是一个"common块"类型的符号......
SHN_UNDEF 0 定义在其他目标文件,被当前目标文件引用,表示该符号未定义
符号值,st_value区别如下
1.在目标文件中,不是SHN_COMMON类型,表示该符号在段中的偏移量
2.在目标文件中,符号是common类型的,st_value表示该符号的对齐属性
3.在可执行文件中,st_value表示符号的虚拟地址,这个虚拟地址多用于动态链接器中.
利用readelf -s target.o去查看ELF文件的符号,更加清晰明确,信息的显示和结构体Elf32_Sym各个成员基本对应.
===特殊符号===
链接器链接生成可执行文件是,会自己定义很多特殊符号,用户可以直接声明并引用.(此处略)
===符号修饰和函数签名===
为防止类似符号名冲突,unix和C语言在所有变量和函数编译后相应的符号名前加"下划线_",现在linux的GGC中已经去掉了加下划线的方式,vc++还有.
C++允许多个不同参数类型的函数有一样的函数名字,即函数重载;C++支持名称空间,允许不同的名称空间有多个相同名字的符号.
int func(int);
float func(float);
class C {
int func(int);
class C {
int func(int);
};
};
namespace N{
int func(int);
class C{
int func(int);
};
}
编译器在将源代码编译为目标文件时,会将函数和变量名字进行修饰,形成特殊的符号名,因此即使函数名相同,只要有所属类和名称空间或者参数和变量的不同编译器和链接器都会认为是不同的函数,不同编译器的修饰规则也不一样,gcc的名称修饰标准详细的可以自己查资料.("_Z开头",嵌套的情况在Z后面加"N",之后"名称长度+每个名称空间或类的名字",有嵌套的情况则"E+符号类型"结尾,没有嵌套则直接"函数参数符号类型"结尾)
函数签名 修饰后符号名
int func(int); _Z4funci
int C::func(int); _ZN1C4funcEi
int C::C2::func(int); _ZN1C2C24funcEi
...
===extern "C"===
C++代码中用于声明接下来一段使用C的语法,被声明的这一段代码就不会和C++的代码有一样符号修饰,vc++平台下对C语言的修饰即符号前加下划线,gcc无变化.
extern "C"{}
extern "C" int var;
在C代码和C++代码中的对于头文件的引用也有区别,如果要在C++代码引用C代码的头文件,用extern语法就好,但是在不确定是c还是c++代码时,c代码并不支持extern语法,因此需要借助宏定义"_cplusplus"来判断当前源代码是不是c++代码.
#ifdef _cplusplus //判断当前编译代码是不是c++代码,如果是则会用extern语法引用memset函数,如果不是则直接引用memset函数.
extern "C"{
#endif
void *memset(void*,int,size_t);
#ifdef _cpulsplus
}
#endif
===弱符号和强符号===
对于c/c++语言来说,编译器默认函数和已经被初始化的全局变量为强符号,未初始化的全局变量为弱符号.
可以通过gcc的"_attribute_((weak))"来定义弱符号,例如:_attribute_((weak)) var = 2;该顶一下var变量为弱符号
1.不允许强符号被多次定义.
2.一个符号在某个目标文件时强符号,其他目标文件是弱符号,则为强符号.
3.同强弱情况下,如果一个符号在不同目标文件的长度不同,取最长的那个.
目标文件被互相连接,符号之间引用需要被正确决议,如果没有找到符号的定义.链接器就会报"未定义错误",这种被称为强引用.
弱引用连接器即使找不到符号定义也不会报错,默认符号为0或一个特殊的值,让程序代码能够识别这个值对应的符号就行.
_attribute_((weakref)) void foo(); //定义对foo函数的引用为弱引用
如果直接调用foo()函数会出现非法地址访问错误,可以如下调用:if(foo) foo();
在库函数中有很多弱符号,加上自定义的强符号,可以覆盖掉那些你可能要改动模块的引用,从而形成自定义的库函数,要恢复时也只需要去掉自己的扩展部分即可.
一个程序被设计成可以支持单线程或多线程,可以通过弱引用方法判断呈现连接到的库是多线程还是单线程的库(是否在编译时有-lpthread选项,有这个即是多线程的版本),在程序中定义一个pthread_create函数的弱引用,在程序运行时动态判断是否链接到pthread库从而决定执行多线程版本还是单线程版本.
gcc pthread.c -o pt / gcc pthread.c -lpthread -o pt......
===调试===
几乎现在所有编译器都支持的源代码的调试,设置断点,监视变量变化,单步行进等等.
gcc target.c -g -o target.o //加上参数"-g"后编译器会在目标文件上加上调试信息,调试信息也是以"段"形式存在
现在的ELF文件调试信息格式为DWARF(debug with arbitrary record format),windows端调试信息格式叫做CodeView.在linux中,可以用"strip target"来去掉目标文件-ELF文件中的调试信息.(通过删除可执行文件中ELF头的typchk段、符号表、字符串表、行号信息、调试段、注解段、重定位信息等来实现缩减程序体积的目的,strip操作后的文件不可恢复.)