linux静态连接
作者:快盘下载 人气:从文本到机器指令
从文本到代码到机器指令可分为propressing >compilation->assembly->linking四个步骤。他们像流水线一样从上一步取结果处理完输出给下一步。每一个过程都有特定的软件来实现。如propressing 是gcc -E 变成file.i文件。 compilation 是gcc -S 变成file.s文件 。assembly 是gcc -c 或者as 变更file.o文件。assembly 是ld 变成file文件。 经过前三步文件已经变成二进制文件了。但是为了让程序跑起来必须还要进行linking。linking就是修改二进制文件中的跳转地址;以确保函数的跳转正常不报错。
连接的本质
计算机的本质是一个状态机。通过一条一条指令的运行来修改各个地方存储的变量。一开始的计算机是没有连接这个过程的。机器指令中的地址都是写死的。但是随着发展;这样一次性写死指令之后稍微插入一个标点符号就要修改所有的地址。这种情况其实还不是最要命的。关键是随着系统的复杂性变高;代码必须按功能分别写在不同的文件中。然后每个文件单独编译。这时候单独的机器指令如果需要相互调用 就必须通过 连接;liniking;这个过程才能正常使用。下面是还没有被链接的目标文件。他们已经被翻译成二进制指令了。但是里面的变量和函数地址都是空的。
[root;localhost 桌面]# objdump -d a.o
a.o; 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>: #函数地址零
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: 48 89 c6 mov %rax,%rsi
16: bf 00 00 00 00 mov $0x0,%edi <------shared变量的地址。绝对地址引用;没连接之前地址是0
1b: e8 00 00 00 00 callq 20 <main;0x20> <------swap函数地址;相对地址引用;没连接之前指向下面那条指令
20: b8 00 00 00 00 mov $0x0,%eax
25: c9 leaveq
26: c3 retq
2个文件被链接成可执行文件之后。之前标记的 空地址都会被填充上。
00000000004004ed <main>:#main有地址了
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: 48 83 ec 10 sub $0x10,%rsp
4004f5: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
4004fc: 48 8d 45 fc lea -0x4(%rbp),%rax
400500: 48 89 c6 mov %rax,%rsi
400503: bf 2c 10 60 00 mov $0x60102c,%edi <------绝对地址被确定了
400508: e8 07 00 00 00 callq 400514 <swap> <------相对地址也被确定了
40050d: b8 00 00 00 00 mov $0x0,%eax
400512: c9 leaveq
400513: c3 retq
COFF文件的格式
上面说的可执行文件和目标文件都是计算机中存储代码的一种载体。不同的载体有不同的作用。就像我们搬家时候整理的箱子。不同的类型的东西放在不同的箱子里面。
目标文件;又名重定位文件 ;relocatable file ;;指源代码编译成代码;但是里面的跳转都是空的;需要被连接才能运行。windows 是.obj 文件。linux是.o文件可执行文件 ;executable ;各个平台名字都不一样 wiindows 是PE ;protable executable ;。linux是 ELF;executable linkable format ;在编译环节连接的库文件叫做静态链接库 ;static linking library ;。windows 是.lib linux是.a文件程序运行时候连接的库文件叫做动态链接库;dynamic linking library;;windows 是.dll linux是.so文件上面怎么多文件虽然名字不一样但是本质都一样。我们写完的代码编译完成之后;所有的内容会被分开放;比如未初始化全局变量和局部变量放在一起。只读变量;例如字符串常量;放在一起;还有被翻译的机器指令放在一起。这种被重新排列组合的文件就叫COFF;common file format;。我们按照COFF标准存储;操作系统就按照COFF标准读取。然后提取需要的东西来运行程序。
在linux 中可以使用 file命令查看文件格式
[root;localhost 桌面]# file ab
ab: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=61cbca9d6b34175fdf139126894bf12898208369, not stripped
下面是ELF规格文档;里面的Specifications中Generic就是COFF规范 ;骨灰级玩家可以看看
https://elinux.org/Executable_and_Linkable_Format_%28ELF%29#ELF_file_layout
ELF文件
ELF;executable linkable format ;是COFF文件的一种。他是linux 动态库.so ;静态库.a ;目标文件.o; 可执行文件的存储格式。可以说只要里面有代码的都是elf 文件。ELF被分为不同的区块。每一个区块都有自己的含义。.
文件头
文件头是elf 文件最开头一个区间。他就像一本书简介一样告诉载入程序;loader;elf文件的版本。是一个非常重要的区间。我们可以用如下readelf命令去查看这个分区。
[root;localhost 桌面]# readelf -h a.o
ELF 头;
Magic; 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 <-----从文件开头第一个字节开始的数据。表明这个文件是elf文件
类别: ELF64
数据: 2 补码;小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件) <-----这是目标文件需要被连接
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址; 0x0 <----- 程序运行的第一条指令地址 连接后确定,目标文件中是零
程序头起点; 0 (bytes into file) <------ 指向程序头。描述elf如何映射到进程的逻辑地址;目标文件还没连接所以是0
Start of section headers: 664 (bytes into file) <-----段表在文件中的位置
标志; 0x0
本头的大小; 64 (字节)
程序头大小; 0 (字节)
Number of program headers: 0
节头大小; 64 (字节)
节头数量; 12
上面Start of section headers: 664 。标准里叫做 e_shoff ;他的值是664。表示从文件开头第665个字节地方存着 section table
段表
段表存着一个数组。每一个数组元素都是一个段描述符;section describer;。
[root;localhost 桌面]# readelf -S a.o
共有 12 个节头;从偏移量 0x298 开始;
节头;
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000027 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000001f0
0000000000000030 0000000000000018 I 9 1 8
[ 3] .data PROGBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 00000067
000000000000002e 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000095
0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000220
0000000000000018 0000000000000018 I 9 7 8
[ 9] .symtab SYMTAB 0000000000000000 000000d0
0000000000000108 0000000000000018 10 8 8
[10] .strtab STRTAB 0000000000000000 000001d8
0000000000000016 0000000000000000 0 0 1
[11] .shstrtab STRTAB 0000000000000000 00000238
0000000000000059 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS Processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
[root;localhost 桌面]#
全体大小;表示该段子元素的大小。例如符号表中的每个符号都是固定大小。
旗标; 表示段的属性;最终的内存属性会参考这个值。比如能不能写入;能不能运行;A表示这个段是有实际内容的。需要分配逻辑地址空间
链接 信息;这2个信息不同的段有不同的意思。如果是REL/RELA重定位表的话表示对应的符号表的下标和重定位表所作用表的下标。例如rela.text 是9和1 表示。.symtab 和.text
还有一个常用命令叫做objdump命令;他在gnu开发环境的binutils工具集中;所以只要你windows安装了gnu的开发套件。你就可以使用他看PE文件。
[root;localhost 桌面]# objdump -h a.o
a.o; 文件格式 elf64-x86-64
节;
Idx Name Size VMA LMA File off Algn
0 .text 00000027 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000067 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000067 2**0
ALLOC
3 .comment 0000002e 0000000000000000 0000000000000000 00000067 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000095 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[root;localhost 桌面]#
SIZE; 是section的大小;
File off; 是section 相对于文件开头的偏移量,
ALGN; 是对其方式2*、*2 代表四字节;2**0 为一字节;例如上面的.eh_frame 是八字节对齐的所以 size/8 肯定是被整除的;如果section不够就补0 来填充到能整除
VMA /LMA: virtual memory address 和load memory address 分别标识运行时候的逻辑地址。和自身被装载在哪。这些都是在链接的时候会被确认;一般都是相同(嵌入式系统可能不同;不展开说;我也没接触过)。
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA这些就是每个section的属性;影响section行为和存储形式。
objdump -s a.o 能16进制显示整个文件
objdump -d a.o 能把所有code 属性的section 反汇编 ;这个一般是我们用objdump的原因
回到段表。这个段表就像目录一样告诉操作系统该如何把一个可执行文件或者动态库加载到内存;又是如何运行。比如类型和旗标会影响内存分段分页的属性设置。地址就是逻辑地址;连接和信息会指导连接器的工作。
常见section
text 段是放代码的;data是存放为初始化的全局变量和局部静态变量。rodata 只读数据段;bss 是存放为未初始化的全局变量和局部静态变量。rodata 只读数据段;const就和字符串变量;;comment编译器信息段;note.GNU-stack堆栈提示段。含有debug的都是和调试有关的段;DWARF标准;,分段太多了也有很多自定义的。例如我们可以自己定义一个mp4段;然后插一段视频到里面;objcopy 命令可以操作;。每一个断的功能作用和解析他的程序有关。上面段都是加点的;表示系统保留;用户就不要定义加点的section了。不然容易冲突;把程序搞崩。
符号表
何为符号;symbol;呢。我们代码中定义和引用的变量和函数是符号;我们的代码中的行号也是符号。符号就像c语言中的指针一样。有了他我们就能找到我们想要的东西。
[root;localhost 桌面]# readelf -s a.o
Symbol table ;.symtab; contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 39 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
[root;localhost 桌面]#
Ndx; 表示符号在哪个段的下标。ABS表示这个符号是绝对值不会变的比如上面文件名;
UND 表示本模块引用了;但是不知道在哪。这个就是连接的核心地方。还有个叫common的;未初始化的变量会被标记成这个。
value; 存储信息的地方;不同的符号和文件类型有不同的意思;对于Ndx =common的符号。表示的对齐方式。Ndx =und就是需要被重定位的变量。而对于Ndx =数字的目标文件。它表示符号在对应段的偏移量。比如上面的main函数就从text段第0个字节开始到39个字节的。对于Ndx =数字 的可执行文件;他就是逻辑地址。一定要记着这个参数。他是连接的核心
Bind; 和连接有关系的一个参数;global 才能被别人连接。还有个weak表示是一个弱符号;符号冲突的时候起作用;。
Vis : 暂时没定义都是default
Ndx=COM这里要单独说一下。因为他涉及到强弱符号。连接器对于符号唯一的判断就是名字是不是一样的。如果出现多个类型不同名字相同的情况;只要其中只有一个强符号。那么就是可以连接。不然会报错提示符号冲突。且分配空间也按照强符号的对齐方式来分。如果都是弱符号那么就按照最大的对其方式来分。而这个common 的意思也就是 common block ;大家都能放的下意思。
相对强弱符号概念的变量;还有一个强弱引用的概念是给函数使用的。如果连接器找不到函数定义。对于弱引用来说连接时候是不会报错的;运行还是会报错;。因为弱引用是开发人员指定的。所以我们写的函数默认都是强引用类型的。一旦冲突了。连接器肯定是直接报错的。一般都是写库的时候指定这个;方便别人加载和不加载我们的库。
重定位表
连接之前。目标文件还没合并成一个文件。所以对于A文件来说。我是不知道B文件中的变量函数在B文件中什么位置的;就更加不知道合并成一个整体之后这些变量和函数在哪了。所以我在A文件中把B相关的引用都先记在一个叫重定位表的地方。在连接的时候读取B的符号表。这样我就知道这些引用在B的什么位置了。
[root;localhost 桌面]# objdump -d a.o
a.o; 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: 48 89 c6 mov %rax,%rsi
16: bf 00 00 00 00 mov $0x0,%edi #不知道shared在哪;先用0代替 记到重定位表中
1b: e8 00 00 00 00 callq 20 <main;0x20>#不知道swap在哪;先用0代替 ;记到重定位表中
20: b8 00 00 00 00 mov $0x0,%eax
25: c9 leaveq
26: c3 retq
[root;localhost 桌面]# readelf -r a.o
重定位节 ;.rela.text; 位于偏移量 0x1f0 含有 2 个条目;
偏移量 信息 类型 符号值 符号名称 ; 加数
000000000017 00090000000a R_X86_64_32 0000000000000000 shared ; 0
00000000001c 000a00000002 R_X86_64_PC32 0000000000000000 swap - 4
重定位节 ;.rela.eh_frame; 位于偏移量 0x220 含有 1 个条目;
偏移量 信息 类型 符号值 符号名称 ; 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text ; 0
偏移量 代码段中需要被重定位的地方;例如上面要替换的地址分别是17和1c处的地址。
信息 前面的数字表示符号在符号表中下标;后面数字表示类型。这个数据不用看。后面的类型符号值 名称都帮我们解析出来了
类型 ; 不同的类型代表机器指令的寻址方式。比如上面分别是绝对寻址和相对寻址。
加数此成员指定常量加数;用于计算将存储在可重定位字段中的值。。不自己写重定位表也不用关注;这个连接有计算方式
静态连接
连接时连接器会把各个属性相同的段合并;符号表也进行合并;然后分配分配地址空间;把所有的偏移量变成逻辑地址;最后通过重定位表记录信息重定位。我们一起来试试。
//a.c
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 -o a.o
gcc -c b.c -o b.o
#自己用连接器进行连接
ld a.o b.o -e main -o ab # -e 指定入口点
我们来看看最终的可执行文件
(base) [root;10 桌面]# objdump -d -s ab
ab; 文件格式 elf64-x86-64
Contents of section .text:
4000e8 554889e5 4883ec10 c745fc64 00000048 UH..H....E.d...H
4000f8 8d45fcbe 00106000 4889c7b8 00000000 .E....;.H.......
400108 e8020000 00c9c355 4889e548 897df848 .......UH..H.}.H
400118 8975f048 8b45f88b 10488b45 f08b0848 .u.H.E...H.E...H
400128 8b45f88b 30488b45 f08b0031 c6488b45 .E..0H.E...1.H.E
400138 f8893048 8b45f88b 0031c148 8b45f089 ..0H.E...1.H.E..
400148 08488b45 f08b0031 c2488b45 f889105d .H.E...1.H.E...]
400158 c3 .
Contents of section .eh_frame:
400160 14000000 00000000 017a5200 01781001 .........zR..x..
400170 1b0c0708 90010000 1c000000 1c000000 ................
400180 68ffffff 27000000 00410e10 8602430d h...;....A....C.
400190 06620c07 08000000 1c000000 3c000000 .b..........<...
4001a0 6fffffff 4a000000 00410e10 8602430d o...J....A....C.
4001b0 0602450c 07080000 ..E.....
Contents of section .data:
601000 01000000 ....
^-----------------------------------------------------------------shared 的内容
^--------------------------------------------------------------------------shared 变量的位置
Contents of section .comment:
0000 4743433a 2028474e 55292034 2e382e35 GCC: (GNU) 4.8.5
0010 20323031 35303632 33202852 65642048 20150623 (Red H
0020 61742034 2e382e35 2d343429 00 at 4.8.5-44).
Disassembly of section .text:
00000000004000e8 <main>:
4000e8: 55 push %rbp
4000e9: 48 89 e5 mov %rsp,%rbp
4000ec: 48 83 ec 10 sub $0x10,%rsp
4000f0: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) <----局部变量a直接写死了
4000f7: 48 8d 45 fc lea -0x4(%rbp),%rax <----a变量地址赋值给了%rax
4000fb: be 00 10 60 00 mov $0x601000,%esi
^<-------shared 变量地址;赋值给了%esi寄存器;swap会用到;绝对寻址,地址就0x601000是
400100: 48 89 c7 mov %rax,%rdi <----a变量地址又进一步赋值给了%rdi;swap会用到
400103: b8 00 00 00 00 mov $0x0,%eax
400108: e8 02 00 00 00 callq 40010f <swap>
^---------------------------------------------------swap函数的地址;这是一个相对寻址;相对于下一个指令;2的地方;x86用的是小段寻址;所以是00 00 00 02;;也解释0x40010d ;0x2=40010f
40010d: c9 leaveq
40010e: c3 retq
000000000040010f <swap>:
^--------------------------------------------------------------swap所在位置
40010f: 55 push %rbp
400110: 48 89 e5 mov %rsp,%rbp
400113: 48 89 7d f8 mov %rdi,-0x8(%rbp) <---------a变量地址存在swap的栈帧里面
400117: 48 89 75 f0 mov %rsi,-0x10(%rbp) <---------a变量地址存在swap的栈帧里面
40011b: 48 8b 45 f8 mov -0x8(%rbp),%rax <---------读取a变量的值
40011f: 8b 10 mov (%rax),%edx
400121: 48 8b 45 f0 mov -0x10(%rbp),%rax <---------读取b变量的值
400125: 8b 08 mov (%rax),%ecx
400127: 48 8b 45 f8 mov -0x8(%rbp),%rax
40012b: 8b 30 mov (%rax),%esi
40012d: 48 8b 45 f0 mov -0x10(%rbp),%rax
400131: 8b 00 mov (%rax),%eax
400133: 31 c6 xor %eax,%esi
400135: 48 8b 45 f8 mov -0x8(%rbp),%rax
400139: 89 30 mov %esi,(%rax)
40013b: 48 8b 45 f8 mov -0x8(%rbp),%rax
40013f: 8b 00 mov (%rax),%eax
400141: 31 c1 xor %eax,%ecx
400143: 48 8b 45 f0 mov -0x10(%rbp),%rax
400147: 89 08 mov %ecx,(%rax)
400149: 48 8b 45 f0 mov -0x10(%rbp),%rax
40014d: 8b 00 mov (%rax),%eax
40014f: 31 c2 xor %eax,%edx
400151: 48 8b 45 f8 mov -0x8(%rbp),%rax
400155: 89 10 mov %edx,(%rax)
400157: 5d pop %rbp
400158: c3 retq
。。。。。。
对于 静态库文件;linux 是.a windows 是.lib ;他们其实就是一堆目标文件的打包形式。
这里简单说一些;上面需要被替换的地址都是4字节;32位;的。在intell 微指令中又很多寻址方法。但是在重定位中 只有相对和绝对寻址;对应上面的call和mov。这个可以不用深究上面的命令已经帮我们显示出最后的地址了。
操作细节
我们拿一个最简单的一个程序实际操作一些。
//main.c
#include<stdio.h>
int main(){
printf(;hello world!
;);
return 0;
}
~
因为 gcc 编译器会帮我们做优化 所以我们要设置一下
gcc -static --verbose -fno-builtin main.c -o main
static 表示静态连接。 verbose 把所有过程信息输出 fno-builtin 禁止为了优化而替换函数。
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
目标;x86_64-redhat-linux
配置为;../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c;;,objc,obj-c;;,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
线程模型;posix
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
COLLECT_GCC_OPTIONS=;-static; ;-v; ;-fno-builtin; ;-mtune=generic; ;-march=x86-64;
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1 -quiet -v main.c -quiet -dumpbase main.c -mtune=generic -march=x86-64 -auxbase main -version -fno-builtin -o /tmp/ccX5t99N.s
^ ccl 就是gcc 的c语言编译器
GNU C (GCC) 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (x86_64-redhat-linux)
由 GNU C 版本 4.8.5 20150623 (Red Hat 4.8.5-44) 编译;GMP 版本 6.0.0;MPFR 版本 3.1.1;MPC 版本 1.0.1
GGC 准则;--param ggc-min-expand=100 --param ggc-min-heapsize=131072
忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include-fixed”
忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/include”
#include ;...; 搜索从这里开始;
#include <...> 搜索从这里开始;
/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include
/usr/local/include
/usr/include
搜索列表结束。
GNU C (GCC) 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (x86_64-redhat-linux)
由 GNU C 版本 4.8.5 20150623 (Red Hat 4.8.5-44) 编译;GMP 版本 6.0.0;MPFR 版本 3.1.1;MPC 版本 1.0.1
GGC 准则;--param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 231b3394950636dbfe0428e88716bc73
COLLECT_GCC_OPTIONS=;-static; ;-v; ;-fno-builtin; ;-mtune=generic; ;-march=x86-64;
as -v --64 -o /tmp/ccyTno6a.o /tmp/ccX5t99N.s
^as 是汇编器
GNU assembler version 2.27 (x86_64-redhat-linux) using BFD version version 2.27-44.base.el7_9.1
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS=;-static; ;-v; ;-fno-builtin; ;-mtune=generic; ;-march=x86-64;
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --hash-style=gnu -m elf_x86_64 -static /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. /tmp/ccyTno6a.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
^collect2 是ld连接器的包装;做一些初始化工作
/bin/ld: 找不到 -lc
collect2: 错误;ld 返回 1
最后结果提示失败。信息显示ld 找不到libc.a(c的标准库)。然后我在我centos 7 上搜索了一些全是没有libc.a这个文件。所以连接器找不到就报错了;下面2个命令可以显示连接器搜索的范围。我去里面看了一下确实只有libc.so这个动态连接用的文件。
gcc -print-search-dirs
ld --verbose | grep SEARCH
安装glibc的静态库文件
yum install glibc-static
然后运行静态连接的文件就可以连接成功了。我们重点聊一下libc.a这个静态库文件。运行下面命令
(base) [root;10 桌面]# ar -t /lib64/libc.a | grep printf
vfprintf.o
vprintf.o
printf_fp.o
reg-printf.o
。。。。。。
可以看到;库文件里面有好多目标文件。我们接着用readelf 或者objdump查找我们的printf符号再哪个目标文件中。
(base) [root;10 桌面]# readelf -s /lib64/libc.a | grep -A15 ;a(printf.o;
文件;/lib64/libc.a(printf.o)
Symbol table ;.symtab; contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 3
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 158 FUNC GLOBAL DEFAULT 1 __printf
8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND stdout
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND vfprintf
10: 0000000000000000 158 FUNC GLOBAL DEFAULT 1 printf <------ 我们的函数。看value和size值你会发现_IO_printf;__printf其实都是这个printf
11: 0000000000000000 158 FUNC GLOBAL DEFAULT 1 _IO_printf
(base) [root;10 桌面]#
这时候你可能会有疑惑;为啥库里面要分怎么多目标文件。这是因为连接器合并目标文件成可执行文件的过程中。连接器不管你用不用的到的代码;都会给你保留下来。所以把每个函数单独弄成一个目标文件可以大大减少连接时的连接量。
内存映射
为了了解elf中的地址和逻辑地址的关系。我们写一个简单的程序。
#sleep.c
#include<stdio.h>
int main(){
while(1){
printf(;hello world!
;);
}
return 0;
}
我们静态编译它;然后让他再后台运行
(base) [wen;10 桌面]$ gcc -static sleep.c -o sleep
(base) [wen;10 桌面]$ nohup ./sleep &
[1] 4420
(base) [wen;10 桌面]$ cat /proc/4420/maps
00400000-004bd000 r-xp 00000000 fd:00 8552348 /home/wen/桌面/sleep
006bc000-006bf000 rw-p 000bc000 fd:00 8552348 /home/wen/桌面/sleep
006bf000-006c1000 rw-p 00000000 00:00 0
0108f000-010b2000 rw-p 00000000 00:00 0 [heap]
7fc0be75d000-7fc0be75e000 rw-p 00000000 00:00 0
7fff38375000-7fff38396000 rw-p 00000000 00:00 0 [stack]
7fff383b0000-7fff383b2000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
(base) [wen;10 桌面]$ readelf -S sleep
共有 34 个节头;从偏移量 0xd1be8 开始;
节头;
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 00000190
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 00000000004001b0 000001b0
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 00000000004001d8 000001d8
0000000000000108 0000000000000018 AI 0 25 8
[ 4] .init PROGBITS 00000000004002e0 000002e0
000000000000001a 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 0000000000400300 00000300
00000000000000b0 0000000000000000 AX 0 0 16
[ 6] .text PROGBITS 00000000004003b0 000003b0
0000000000092356 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 0000000000492710 00092710
0000000000001aef 0000000000000000 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 0000000000494200 00094200
00000000000000b2 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 00000000004942b4 000942b4
0000000000000009 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 00000000004942c0 000942c0
0000000000019758 0000000000000000 A 0 0 32
[11] __libc_atexit PROGBITS 00000000004ada18 000ada18
0000000000000008 0000000000000000 A 0 0 8
[12] __libc_subfreeres PROGBITS 00000000004ada20 000ada20
0000000000000050 0000000000000000 A 0 0 8
[13] .stapsdt.base PROGBITS 00000000004ada70 000ada70
0000000000000001 0000000000000000 A 0 0 1
[14] __libc_thread_sub PROGBITS 00000000004ada78 000ada78
0000000000000008 0000000000000000 A 0 0 8
[15] __libc_IO_vtables PROGBITS 00000000004ada80 000ada80
00000000000006a8 0000000000000000 A 0 0 32
[16] .eh_frame PROGBITS 00000000004ae128 000ae128
000000000000e45c 0000000000000000 A 0 0 8
[17] .gcc_except_table PROGBITS 00000000004bc584 000bc584
0000000000000115 0000000000000000 A 0 0 1
[18] .tdata PROGBITS 00000000006bceb0 000bceb0
0000000000000020 0000000000000000 WAT 0 0 16
[19] .tbss NOBITS 00000000006bced0 000bced0
0000000000000038 0000000000000000 WAT 0 0 16
[20] .init_array INIT_ARRAY 00000000006bced0 000bced0
0000000000000010 0000000000000008 WA 0 0 8
[21] .fini_array FINI_ARRAY 00000000006bcee0 000bcee0
0000000000000010 0000000000000008 WA 0 0 8
[22] .jcr PROGBITS 00000000006bcef0 000bcef0
0000000000000008 0000000000000000 WA 0 0 8
[23] .data.rel.ro PROGBITS 00000000006bcf00 000bcf00
00000000000000e4 0000000000000000 WA 0 0 32
[24] .got PROGBITS 00000000006bcfe8 000bcfe8
0000000000000008 0000000000000008 WA 0 0 8
[25] .got.plt PROGBITS 00000000006bd000 000bd000
0000000000000070 0000000000000008 WA 0 0 8
[26] .data PROGBITS 00000000006bd080 000bd080
0000000000001690 0000000000000000 WA 0 0 32
[27] .bss NOBITS 00000000006be720 000be710
0000000000002158 0000000000000000 WA 0 0 32
[28] __libc_freeres_pt NOBITS 00000000006c0878 000be710
0000000000000030 0000000000000000 WA 0 0 8
[29] .comment PROGBITS 0000000000000000 000be710
000000000000002d 0000000000000001 MS 0 0 1
[30] .note.stapsdt NOTE 0000000000000000 000be740
0000000000000f88 0000000000000000 0 0 4
[31] .symtab SYMTAB 0000000000000000 000bf6c8
000000000000baa8 0000000000000018 32 822 8
[32] .strtab STRTAB 0000000000000000 000cb170
00000000000068fc 0000000000000000 0 0 1
[33] .shstrtab STRTAB 0000000000000000 000d1a6c
000000000000017b 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
[1]; 已终止 nohup ./sleep
你可以看到我们的elf 有很多分区;但是内存映射的分区就几个。这是因为系统再把elf映射到内存的时候把属性的相同的分区合并到一起塞到内存里面了。而elf中有一个叫做program header结构会告诉操作系统如何合并。
(base) [wen;10 桌面]$ readelf -l sleep
Elf 文件类型为 EXEC (可执行文件)
入口点 0x400ecd
共有 6 个程序头;开始于偏移量64
程序头;
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000bc699 0x00000000000bc699 R E 200000
^这些被合并section 组成的新分区叫做segment
LOAD 0x00000000000bceb0 0x00000000006bceb0 0x00000000006bceb0
0x0000000000001860 0x00000000000039f8 RW 200000
NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190
0x0000000000000044 0x0000000000000044 R 4
TLS 0x00000000000bceb0 0x00000000006bceb0 0x00000000006bceb0
0x0000000000000020 0x0000000000000058 R 10
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x00000000000bceb0 0x00000000006bceb0 0x00000000006bceb0
0x0000000000000150 0x0000000000000150 R 1
Section to Segment mapping: <----被合并section
段节...
00 .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_atexit __libc_subfreeres .stapsdt.base __libc_thread_subfreeres __libc_IO_vtables .eh_frame .gcc_except_table
01 .tdata .init_array .fini_array .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
02 .note.ABI-tag .note.gnu.build-id
03 .tdata .tbss
04
05 .tdata .init_array .fini_array .jcr .data.rel.ro .got
这些被合并section 有个新名字;叫做segment。如果学过cpu的保护模式的话就是里面的分段。
Type; LOAD类型的段会被映射到内存。其他note tls gnu_xxxx都是装载时起辅助作用
Offset; 在整个可执行文件中的偏移量
VirtAddr; virtual memory address ;逻辑地址
PhysAddr; load memory address ;逻辑地址 一般和上面一样。嵌入式系统可能不一样
FileSiz; 在可执行文件中占的空间
**MemSiz;**在内存文件中占的空间;大于等于 FileSiz ;因为像.bss这样的不占FileSiz的section需要被开配内存空间。
Flags; 属性。R可读;W可写。E可执行
Align; 对齐方式
回到上面的分段;程序头中显示第一个分段是从400000->4bc699,第二个分段是6bceb0->6c08a8;但是00400000-004bd000 和006bc000-006bf000 。这是因为现在主流操作系统对内存管理的基本单位是4KB也是0x1000;所以分配的内存也必须是xxx000这样三个零结尾。所以第一个段结尾是c699-d000都会被填上零。第二个段是从必须要从6bc000开开始;如果从d000开始映射那么;c000-ceb0之间的数据就丢了。
至此我们把静态连接在概念上算是讲完了。上面说的所有结构解析;运行逻辑;操作步骤本质上都是操作系统中各个程序完成的;有的程序解析elf文件格式;有的分配内存;有的加载程序。如果想从最底层上搞清楚这些细节;还是需要去看源代码的。本人功力有限只能先管中窥豹的记录一下静态连接的lsb;Linux Standard Base;。
加载全部内容