编译、链接到载入、运行的大致过程----2.链接

December 09, 2023
测试
测试
测试
测试
15 分钟阅读

编译完成之后,需要的步骤就是 链接.编译仅仅转换源代码到二进制的机器码,但是并没有把程序运行需要的所有资源整合到一起,所以编译后的"目标文件"是没办法直接运行的;在实际的项目中,通常是由多个源代码文件,每个源代码文件都可以进行编译后生成"目标文件“. 这些目标文件 和需要的其他资源被整合到一起,最终才生成我们常见的程序(典型的比如windows下的各种exe文件,linux 下的elf LSB executable 文件,linux 下的elf LSB shared object 等). 这个整合的过程就是“链接”.

以下是用gcc对两个源文件进行编译链接的完整过程示例:
[root@www ~]# cat my.c           #源文件my.c 
#include<stdio.h>
void myfunction() {
printf("Hello,I am a function, name is : myfunction.\n");
}
[root@www ~]# cat main.c           #源文件main.c 
#include <stdio.h>
int main() {
myfunction();
printf("Hello,I am the main function!\n");
}
[root@www ~]# gcc -c my.c -o my.obj          #编译my.c 为my.obj 
[root@www ~]# gcc -c main.c -o main.obj    #编译main.c 为main.obj 
[root@www ~]# gcc -o my_exe  my.obj main.obj    # 这一步进行链接操作,如果把“目标文件”换成 源文件,那么编译,链接都在这一条命令里面完成了;
[root@www ~]# ./my_exe  #运行最终的可执行文件
Hello,I am a function, name is : myfunction.
Hello,I am the main function!
[root@www ~]# 

链接的命令介绍完了,但是要了解程序载入的大致过程,需要对程序的segment head, section head有大概的了解.因为程序加载到内存时候的时候会依赖segment head 记录的信息,而segment head 是在section 的基础上生成的;所以这里进一步了解segment , section.

什么文件才有section , segment 的概念呢? 两者有什么区别?

section 是编译时候生成的,而segment是为了程序加载而存在的概念;segment 通常包含有多个section. 一般有相同属性的section会被安排在同一个segment里面;所以 segment 是section的集合; 对于编译生成的“目标文件”, 是没有segment信息的,但是存在section信息; 链接后的文件既有segment head ,也有section head信息;以/usr/bin/cat 这个程序为例,着重了解下segment的地址怎么计算:

[root@www ~]# readelf -l `which cat`     # -l 参数用来查看程序cat 的segment head 信息;
Elf file type is EXEC (Executable file)
Entry point 0x402624
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000000adcc 0x000000000000adcc  R E    200000
  LOAD           0x000000000000bc48 0x000000000060bc48 0x000000000060bc48
                 0x00000000000006d8 0x0000000000001060  RW     200000
  DYNAMIC        0x000000000000bde8 0x000000000060bde8 0x000000000060bde8
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000009a94 0x0000000000409a94 0x0000000000409a94
                 0x000000000000030c 0x000000000000030c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x000000000000bc48 0x000000000060bc48 0x000000000060bc48
                 0x00000000000003b8 0x00000000000003b8  R      1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .data.rel.ro .dynamic .got 

上面的结果表示: 一共有9个segments, 其中只有Type为"LOAD" 对应的segments 在程序载入内存的时候会被加载到内存,因此这里只讨论LOAD类型的segment. segment 和section的具体的mapping 关系在上面的结果中有表示; 上述的结果中有一个叫做: VirtAddr的值,这个字段表示 segment对应的“程序虚拟地址”, 也有叫做“文件虚拟地址”的,我觉得都是一个意思:就是说 这个segment 是从这个 虚拟地址开始的,那么结束怎么计算呢? 看FileSiz 字段的值,这个值表示segment 的长度,开始地址加上长度就是结束地址了; 从上面readelf -l 输出的segment head的信息中: 对于cat (/usr/bin/cat)这个程序,编号为02的LOAD的segment 的地址范围是: 0x400000~0x40adcc,这个非常容易理解, 开始地址加上长度就是结束地址, 至此,我们已经获得了其中一个type为LOAD的segment的地址了

但是我们前面已经说了,每个segment 都包含有多个section, 从上面的结果可以看到这个编号为02的segment 对应的section有: .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame ,因此,我们也可以从包含的section 来计算segment 的地址,从而做一个验证: 首先查看cat (/usr/bin/cat) 程序的section 信息如下:

[root@www ~]# readelf -S `which cat`
There are 31 section headers, starting at offset 0xcbd0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000768  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400a20  00000a20
       000000000000031d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400d3e  00000d3e
       000000000000009e  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400de0  00000de0
       0000000000000060  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400e40  00000e40
       0000000000000090  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400ed0  00000ed0
       0000000000000690  0000000000000018  AI       5    25     8
  [11] .init             PROGBITS         0000000000401560  00001560
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000401580  00001580
       0000000000000470  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         00000000004019f0  000019f0
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000401a00  00001a00
       000000000000731a  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000408d1c  00008d1c
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000408d40  00008d40
       0000000000000d53  0000000000000000   A       0     0     32
  [17] .eh_frame_hdr     PROGBITS         0000000000409a94  00009a94
       000000000000030c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         0000000000409da0  00009da0
       000000000000102c  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       000000000060bc48  0000bc48
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       000000000060bc50  0000bc50
       0000000000000008  0000000000000008  WA       0     0     8
  [21] .jcr              PROGBITS         000000000060bc58  0000bc58
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .data.rel.ro      PROGBITS         000000000060bc60  0000bc60
       0000000000000188  0000000000000000  WA       0     0     32
  [23] .dynamic          DYNAMIC          000000000060bde8  0000bde8
       00000000000001d0  0000000000000010  WA       6     0     8
  [24] .got              PROGBITS         000000000060bfb8  0000bfb8
       0000000000000030  0000000000000008  WA       0     0     8
  [25] .got.plt          PROGBITS         000000000060c000  0000c000
       0000000000000248  0000000000000008  WA       0     0     8
  [26] .data             PROGBITS         000000000060c260  0000c260
       00000000000000c0  0000000000000000  WA       0     0     32
  [27] .bss              NOBITS           000000000060c320  0000c320
       0000000000000988  0000000000000000  WA       0     0     32
  [28] .gnu_debuglink    PROGBITS         0000000000000000  0000c320
       0000000000000010  0000000000000000           0     0     4
  [29] .gnu_debugdata    PROGBITS         0000000000000000  0000c330
       0000000000000780  0000000000000000           0     0     1
  [30] .shstrtab         STRTAB           0000000000000000  0000cab0
       000000000000011e  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@www ~]# 

从上述的结果中可以看到: .interp 这个section的开始地址是 :0x400238, .en_frame的结束地址是: 0x409da0+0x102c=0x40adcc , 所以通过section的分析 发现编号为2的segment 地址应该是: 0x400238~0x40adcc ; 到这里总结一下上面两个方式得到的同一个segment( /usr/bin/cat 这个程序的第一个load segment)的地址: 直接读取程序头的segment ,获得的地址是: 0x400000~0x40adcc 通过计算segment所包含的section, 获得的地址是: 0x400238~0x40adcc

为什么上述两种方式得到的同一个segment的地址会有偏差呢?

因为程序虚拟地址空间的分配是 以page为单位的,而每个page的大小默认为4KB. 如果segment 的开始地址不是在page的开头,结束地址不是在page的结尾,那么这两个地址都需要 进行page 对齐的调整;所以对上述的地址按照page对齐(4KB对齐就是 以0x1000为单位进行对齐)进行调整,调整后结果如下: 0x400000~0x40adcc ----->0x400000~0x40b000 0x400238~0x40adcc ----->0x400000~0x40b000 所起,无论是从segment head读取的地址,还是通过section 计算出来的地址,通过 page 对齐调整后,都是同一个地址;

总结一下:

1. 链接后的文件有segment的描述,也有section描述,而编译后的文件只有section.
2. 只有type 为“LOAD”的segment 会在程序加载的时候被载入内存
3. 程序虚拟地址中, segment的地址计算可以通过 readelf -l FILE_PATH 来读取,也可以通过 其包含的的section来计算;
4. 无论怎么算出来的segment 的地址范围,都需要通过page 对齐的方式来进行调整

继续阅读

更多来自我们博客的帖子

如何安装 BuddyPress
由 测试 December 17, 2023
经过差不多一年的开发,BuddyPress 这个基于 WordPress Mu 的 SNS 插件正式版终于发布了。BuddyPress...
阅读更多
Filter如何工作
由 测试 December 17, 2023
在 web.xml...
阅读更多
如何理解CGAffineTransform
由 测试 December 17, 2023
CGAffineTransform A structure for holding an affine transformation matrix. ...
阅读更多