资讯

展开

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;。

加载全部内容

相关教程
猜你喜欢
用户评论
快盘暂不提供评论功能!