前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编译、链接到载入、运行的大致过程----2.链接

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

作者头像
qsjs
发布2020-06-08 12:15:45
8000
发布2020-06-08 12:15:45
举报
文章被收录于专栏:MyPanda的学习笔记

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

以下是用gcc对两个源文件进行编译链接的完整过程示例:
代码语言:javascript
复制
[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的地址怎么计算:

代码语言:javascript
复制
[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 信息如下:

代码语言:javascript
复制
[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 对齐的方式来进行调整
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 以下是用gcc对两个源文件进行编译链接的完整过程示例:
  • 什么文件才有section , segment 的概念呢? 两者有什么区别?
  • 为什么上述两种方式得到的同一个segment的地址会有偏差呢?
  • 总结一下:
    • 1. 链接后的文件有segment的描述,也有section描述,而编译后的文件只有section.
      • 2. 只有type 为“LOAD”的segment 会在程序加载的时候被载入内存
        • 3. 程序虚拟地址中, segment的地址计算可以通过 readelf -l FILE_PATH 来读取,也可以通过 其包含的的section来计算;
          • 4. 无论怎么算出来的segment 的地址范围,都需要通过page 对齐的方式来进行调整
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档