【程序员的自我修养04】目标文件生成可执行文件过程

2023-12-13 04:14:28

绪论

大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。

简介

在前两章的内容中,我们已经了解目标文件的基本布局以及部分局部细节,单独的可执行文件并没有什么实际作用。我们需要多个目标文件链接成可执行文件,或生成动态库。接下来的几章,我会详细介绍目标文件链接成可执行文件的过程,希望大家能够坚持,坚持必有所获

)

示例代码

本文的示例代码如下:

//a.c
extern int shared;
extern void swap(int* a, int* b);
int main()
{
        int a = 100;
        swap(&a,&shared);
        return 0;
}

//b.c
int shared = 1;

void swap(int* a, int* b)
{
        *a ^= *b ^= *a = ^= b;
}

编译:

gcc -c a.c b.c

得到a.ob.o两个目标文件。

如何合并目标文件

我们知道ELF文件格式的目标文件中,有.text.data.bss等多个段。如果是你,会如何将多个目标文件按照什么规则合并成一个可执行文件呢

略加思考,我们应该也会想到两个方式:

一、按序叠加。这种没有什么特别的规则,来一个目标文件,就将其依次叠加起来。如下图。

分析:由图可知,这种方式的确很简单,大大减轻了链接器的工作和复杂度。但是存在两个致命的问题。

  • 浪费内存空间。段的装载地址和空间是由对齐要求的。比如x86的硬件来说,段的装载地址和空间的对齐单位是页,即4096字节。稍微规模大些的应用程序可能由几百,上千的目标文件组成,那么最终生成的可执行程序的段就会非常多。而每一个段都有内存对齐要求,则会造成很多的内存浪费。
  • 访问效率不高。我们前面说过,计算机喜欢利用局部性原理,增加cache命中率。提高访问效率。若是这种组合方式,很明显,并不能有效利用该特性。

综上所述,该方式虽然简单,但不是一个好的方式。

二、相似段合并。将相同性质的段合并到一起,比如将所有输入文件的.text段合并输出到文件的.text段。如下图所示:

现在的链接器基本都是采用上述方式,因为它避免了方式一的缺陷。

链接步骤

采用方式二合并的链接器,整个链接过程可以分为两个步骤。空间与地址分配符号解析与重定位

空间与地址分配

扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。并将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系

这里的**“地址和空间”**有两层含义:

  • 输出可执行文件中的空间分布。即各目标文件中的段在可执行文件中分布,如上如。
  • 可执行文件在装载后的虚拟地址中的虚拟地址空间。即确定程序加载后,各个段在虚拟空间中的地址。也就是说,像.text.data等在内存中实际存在的段,其虚拟地址,在链接成可执行程序时,就已经确定了。这也是可以通过pc指针,定位到代码行数的原因
yihua@ubuntu:~/test/static-linker$ readelf -S a.o
There are 12 section headers, starting at offset 0x2c8:

Section Headers:
  [Nr] Name              Type             Address           Offset      Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000    0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040    0000000000000029  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000220    0000000000000030  0000000000000018   I       9     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000069    0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000069    0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .comment          PROGBITS         0000000000000000  00000069    000000000000002a  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  00000093    0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  00000098    0000000000000038  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000250    0000000000000018  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000d0    0000000000000120  0000000000000018          10     8     8
  [10] .strtab           STRTAB           0000000000000000  000001f0    000000000000002c  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  00000268    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)
yihua@ubuntu:~/test/static-linker$ readelf -S b.o
There are 11 section headers, starting at offset 0x270:
yihua@ubuntu:~/test/static-linker$ readelf -S b.o
There are 11 section headers, starting at offset 0x270:

Section Headers:
  [Nr] Name              Type             Address           Offset      Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000    0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040    000000000000004b  0000000000000000  AX       0     0     1
  [ 2] .data             PROGBITS         0000000000000000  0000008c    0000000000000004  0000000000000000  WA       0     0     4
  [ 3] .bss              NOBITS           0000000000000000  00000090    0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .comment          PROGBITS         0000000000000000  00000090    000000000000002a  0000000000000001  MS       0     0     1
  [ 5] .note.GNU-stack   PROGBITS         0000000000000000  000000ba    0000000000000000  0000000000000000           0     0     1
  [ 6] .eh_frame         PROGBITS         0000000000000000  000000c0    0000000000000038  0000000000000000   A       0     0     8
  [ 7] .rela.eh_frame    RELA             0000000000000000  00000200    0000000000000018  0000000000000018   I       8     6     8
  [ 8] .symtab           SYMTAB           0000000000000000  000000f8    00000000000000f0  0000000000000018           9     8     8
  [ 9] .strtab           STRTAB           0000000000000000  000001e8    0000000000000011  0000000000000000           0     0     1
  [10] .shstrtab         STRTAB           0000000000000000  00000218    0000000000000054  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),

生成可执行程序ab,可能会出现以下错误提示。

yihua@ubuntu:~/test/static-linker$ ld a.o b.o -e main -o ab
a.o: In function `main':
a.c:(.text+0x46): undefined reference to `__stack_chk_fail'
yihua@ubuntu:~/test/static-linker$

若出现如上错误,那是因为编译目标文件时,默认增加了栈保护。关闭即可。如下:

yihua@ubuntu:~/test/static-linker$ gcc -c -fno-stack-protector  a.c b.c
yihua@ubuntu:~/test/static-linker$ ld a.o b.o -e main -o ab
yihua@ubuntu:~/test/static-linker$

可执行程序ab的段信息如下:

yihua@ubuntu:~/test/static-linker$ readelf -S ab
There are 9 section headers, starting at offset 0x1258:

Section Headers:
  [Nr] Name              Type             Address           Offset      Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000    0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         00000000004000e8  000000e8    0000000000000074  0000000000000000  AX       0     0     1
  [ 2] .eh_frame         PROGBITS         0000000000400160  00000160    0000000000000058  0000000000000000   A       0     0     8
  [ 3] .got.plt          PROGBITS         0000000000601000  00001000    0000000000000018  0000000000000008  WA       0     0     8
  [ 4] .data             PROGBITS         0000000000601018  00001018    0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  0000101c    0000000000000029  0000000000000001  MS       0     0     1
  [ 6] .symtab           SYMTAB           0000000000000000  00001048    0000000000000180  0000000000000018           7    10     8
  [ 7] .strtab           STRTAB           0000000000000000  000011c8    0000000000000048  0000000000000000           0     0     1
  [ 8] .shstrtab         STRTAB           0000000000000000  00001210    0000000000000043  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)

通过对比a.ob.oab的段信息,我们可以得出两点信息。

  1. 只有可执行程序才会确定虚拟地址。在链接之前,目标文件中的所有段的VMA都是0,因为虚拟空间并没有被分配,只有当链接为可执行程序ab后,才会分配虚拟地址。比如:ab程序运行时,.text段会加载到虚拟地址0x00000000004000e8中;.data段,加载到虚拟地址0x0000000000601018中。
  2. 并不是所有的段都会加载到内存中。即使是可执行程序ab,也并不是所有的段,都设置了虚拟地址。仅仅.texteh_fream.got.plt.data段设置了虚拟地址,其它段并没有。那是其他段在程序真正运行时,并不需要了。 比如.comment段,记录调试信息的,运行时并不需要。.symtab段,记录所有的符号,用于链接阶段的符号解析和重定位。当程序运行时,也不再需要了

空间和地址分配流程大致如下:

符号解析和重定位

通过空间与地址分配后,各段在可执行程序中的虚拟地址是确认的。也就是说.text段内函数符号,.data段内的变量符号。其虚拟地址都是已经确认的(段的基地址加上符号在本段的offset)。

比如:a.o中的main函数相对于a.o.text段偏移X。但经过空间地址分配之后,a.o.text位于虚拟地址0x00000000004000e8中,那么main的虚拟地址为0x00000000004000e8+X。实际上X=0,那么main的虚拟地址为0x00000000004000e8。通过命令查看,确实如此。

yihua@ubuntu:~/test/static-linker$ readelf -s ab

Symbol table '.symtab' contains 16 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000004000e8     0 SECTION LOCAL  DEFAULT    1
     2: 0000000000400160     0 SECTION LOCAL  DEFAULT    2
     3: 0000000000601000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000601018     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     7: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
     8: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS
     9: 0000000000601000     0 OBJECT  LOCAL  DEFAULT    3 _GLOBAL_OFFSET_TABLE_
    10: 0000000000400111    75 FUNC    GLOBAL DEFAULT    1 swap
    11: 0000000000601018     4 OBJECT  GLOBAL DEFAULT    4 shared
    12: 000000000060101c     0 NOTYPE  GLOBAL DEFAULT    4 __bss_start
    13: 00000000004000e8    41 FUNC    GLOBAL DEFAULT    1 main
    14: 000000000060101c     0 NOTYPE  GLOBAL DEFAULT    4 _edata
    15: 0000000000601020     0 NOTYPE  GLOBAL DEFAULT    4 _end

上述的方式就是符号解析通过完全一样的计算方式,我们可以得知所有符号的地址

我们知道a.c中引用b.c中的swap函数和shared变量。实际上a.o是不可能知道swapshared的虚拟地址的。那么a.o是如何使用这两个外部符号的呢?可通过反汇编查看。

由反汇编可知, a.o对外部符号的引用,暂时将地址设置为0。当符号解析完成,获取符号的虚拟地址,再进行修改。这个过程就是符号重定位

总结

本章节概述了目标文件链接成可执行文件的过程。由如何合并多个目标文件开始,了解到相似段合并的优点

进而介绍了链接过程的主要两个步骤:空间与地址分配符号解析和重定位。并用示例分析其过程。内容较多,希望读者能够自己本地操作一遍,认真思考,推敲。若有疑问,也可与我沟通。

最后,大家可以思考一个问题:符号重定位是非常重要的步骤,那么链接器是如何知道哪些符号需要修正的呢?

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
在这里插入图片描述

文章来源:https://blog.csdn.net/xieyihua1994/article/details/134958023
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。