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

news2024/12/22 18:25:58

绪论

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

简介

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

)

示例代码

本文的示例代码如下:

//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,人生尽是坦途
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1305381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

.Net中的集合

所有的集合都是继承自IEnumerable。集合总体可以分为以下几类:关联/非关联型集合,顺序/随机访问集合,顺序/无序集合,泛型/非泛型集合,线程集合。 各集合类底层接口关系图 泛型与非泛型集合类的分析 泛型集合是类型安…

自动化测试基础知识:什么是自动化测试?需要学习哪些知识与工具!

1、自动化测试概念 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。通常, 在设计了测试用例并通过评审之后,由测 试人员根据测试用例中描述的规程一步步执行测试,得到实际结果与期望结果的比较。简言之,自动化测试…

【操作系统导论】比例份额调度

本文介绍一种 比例份额(proportional-share) 调度程序,也称为 公平份额(fair-share)。 彩票调度 简介 彩票调度 的基本思想: 每隔一段时间,都会举行一次彩票抽奖,以确定接下来应该…

【上海大学数字逻辑实验报告】六、时序电路

一、 实验目的 掌握同步二进制计数器和移位寄存器的原理。学会用分立元件构成2位同步二进制加计数器。学会在Quartus II上设计单向移位寄存器。学会在Quartus II上设计环形计数器。 二、 实验原理 同步计数器是指计数器中的各触发器的时钟脉冲输入端连接在一起,接…

做题总结 707. 设计链表

做题总结 707. 设计链表 leetcode中单链表节点的默认定义我的尝试正确运行的代码(java) leetcode中单链表节点的默认定义 class ListNode {int val;ListNode next;//无参public ListNode() {}//有参:1public ListNode(int val) {this.val val;}//有参:…

【项目小结】优点分析

一、 个人博客系统 一)限制强制登录 问题:限制用户登录后才能进行相关操作解决: 1)前端: ① 写一个函数用于判断登录状态,如果返回的状态码是200就不进行任何操作,否则Ajax实现页面的跳转操作…

Apollo配置发布原理解析

📫作者简介:小明java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化,文章内容兼具广度、深度、大厂技术方案,对待技术喜欢推理加验证,就职于…

Windows下查看删除某一个端口号

背景:Java项目运行时,提示端口号被占用,然后就忘记之前是怎么处理的了,感觉还是像Linux中杀掉端口号就命令行的方式比较简单一些,然后就是各种搜索,记录一下 第一步:在cmd中查看该端口号是否被…

最强文生图跨模态大模型:Stable Diffusion

文章目录 一、概述二、Stable Diffusion v1 & v22.1 简介2.2 LAION-5B数据集2.3 CLIP条件控制模型2.4 模型训练 三、Stable Diffusion 发展3.1 图形界面3.1.1 Web UI3.1.2 Comfy UI 3.2 微调方法3.1 Lora 3.3 控制模型3.3.1 ControlNet 四、其他文生图模型4.1 DALL-E24.2 I…

Nginx的location匹配和rewrite重写

一、location匹配 常用的正则表达式 ^ :匹配输入字符串的起始位置 $ :匹配输入字符串的结束位置 * :匹配前面的字符零次或多次。如“ol*”能匹配“o”及“ol”、“oll”:匹配前面的字符一次或多次。如“ol”能匹配“ol”及“oll…

MySQL笔记-第14章_视图

视频链接:【MySQL数据库入门到大牛,mysql安装到优化,百科全书级,全网天花板】 文章目录 第14章_视图1. 常见的数据库对象2. 视图概述2.1 为什么使用视图?2.2 视图的理解 3. 创建视图3.1 创建单表视图3.2 创建多表联合视…

C++ exception类:C++标准异常的基类

C语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。你可以通过下面的语句来捕获所有的标准异常: try{//可能抛出异常的语句}catch(exception &e){//处理异常的语句} 之所以使用引用&a…

GeoPandas实操:读取数据

GeoPandas 支持读取和写入多种地理空间数据格式,如 ESRI Shapefile、GeoJSON、GeoPackage 等,以及与其他 GIS 软件兼容的格式。 1. 读取数据 1.1. 读取ESRI Shapefile数据 ESRI Shapefile(简称 Shapefile 或 .shp 文件)是一种常…

Pandas中的Series(第1讲)

Pandas中的Series(第1讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔…

PHP操作ZIP之ZipArchive类以及如何避免生成压缩文件带有目录层级的问题

常用的方法 php ZipArchive可以说是php自带的一个函数了,他可对对文件进行压缩与解压缩处理,但是使用此类之前我们必须在php.ini中把extensionphp_zip.dll前面的分号有没有去掉,然后再重启Apache这样才能使用这个类库。 ziparchive 可选参数…

解决:Component name “index“ should always be multi-word

原因 要求组件名称以驼峰格式命名,自定义组件名称应该由多单纯组成,防止和html标签冲突,所以index.vue 会报错 解决 1、按照规则驼峰格式,如:appIndex.vue 2、若有.eslintrc.js文件,并在规则中(rules)关…

排序算法4:【快速排序】、查看每趟归并后的结果,定义一个全局变量,用来计数作为总趟数

一、快速排序——时间复杂度:、 最坏的情况 1、原理: 快速排序是通过多次比较和交换来实现排序,首先,先从数列中,任意选择一个数作为基准(或叫分界值),比如,第一个数&a…

MySQL的事务以及springboot中如何使用事务

事务的四大特性: 概念: 事务 是一组操作的集合,它是不可分割的工作单元。事务会把所有操作作为一个整体,一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 注意: 默认MySQ…

图解transformer中的自注意力机制(备忘)

注意力机制 在整个注意力过程中,模型会学习了三个权重:查询、键和值。查询、键和值的思想来源于信息检索系统。所以我们先理解数据库查询的思想。 假设有一个数据库,里面有所有一些作家和他们的书籍信息。现在我想读一些Rabindranath写的书&#xff1a…

网络编程----select 模型总结

为什么要使用select模型? 答:解决基本C/S模型中,accept()、recv()、send()阻塞的问题 select模型与C/S模型的不同点 C/S模型中accept()会阻塞一直傻等socket来链接select模型只解决accept()傻等的问题,不解决recv(),send()执行…