| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> Linux下的ELF文件、链接、加载与库(含大量图文解析及例程) -> 正文阅读 |
|
[系统运维]Linux下的ELF文件、链接、加载与库(含大量图文解析及例程) |
Linux下的ELF文件、链接、加载与库
本文主要参考[原创] Linux环境下:程序的链接, 装载和库,[完结] 2020 南京大学 “操作系统:设计与实现” (蒋炎岩)两个视频课程,并有CSAPP中介绍的一些内容。 常用工具我们首先列出一些在接下来的介绍过程中会频繁使用的分析工具,如果从事操作系统相关的较底层的工作,那这些工具应该再熟悉不过了。不熟悉的读者可以先看一下这里的简单的功能介绍,我们会在后文中介绍一些详细的参数选项和使用场景。
ELF文件详解ELF文件的四种形式
可重定位ELF文件的内容分析
首先有一个64字节的ELF头 其中各个节的含义如下:
这样我们就把一个可重定位的ELF文件中的每一个字节都搞清楚了。 静态链接编译、链接的需求为了节省空间和时间,不将所有的代码都写在同一个文件中是一个很基本的需求。 为此,我们的C语言需要实现这样的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。 假如我们有三个c文件,分别是
我们在 我们编译链接这些代码,Makfile如下:
结果生成的可执行文件可以正常地输出我们想要的内容。
我们知道
输出:
我们发现,其实是能够打印出四个字节(整型为4个字节),但这四个字节是什么东西呢? C语言中的类型:C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。比如有符号整型,就按照补码解读接下来的4个字节地址;又比如浮点型,就是按照IEEE754的浮点数规定来解读接下来的4字节地址。 那我们这里将符号
输出(节选): 我们看到, 那我们接下来试图对 总结一下,通过这个例子,我们应当理解:
程序的编译 - 可重定向文件我们先用file命令来查看
输出:
我们看到这里的 既然外部的符号是在链接时才会被main程序知道,那在编译main程序,生成可重定向文件时这些外部的符号是怎么处理的呢?我们同样通过objdump工具来查看编译出的
输出: main在编译的时候,引用的外部符号就只能 ”留空(0)“ 了。 我们看到,在编译但还未链接的 另外注意这里的 我们已经知道在编译时生成的文件中外部符号的部分使用0暂时留空的,这些外部符号是待链接时再填充的。那么,我们在链接时究竟需要填充哪些位置呢?我们可以使用readelf工具来查看ELF文件的重定位信息:
这个图中上方是readelf的结果,下面是objdump的结果,笔者在这里已经将前两个外部符号的偏移量的对应关系用红色箭头指了出来,其他的以此类推。这种对应也可以证明我们上面的分析是正确的的。 应当讲,可重定向ELF文件(如 另外,注意 程序的静态链接简单讲,程序的静态链接是会把所需要的文件链接起来生成可执行的二进制文件,将相应的外部符号,填入正确的位置(就像我们上面查看的那样)。 1 段的合并首先会做一个段的合并。即把相同的段(比如代码段 .text)识别出来并放在一起。 2 重定位重定位表,可用 简单讲,就是当某个文件中引用了外部符号,在编译时编译器是不会阻止你这样做的,因为它相信你会在链接时告诉它这些外部符号是什么东西。但在编译时,它也不知到这些符号具体在什么地址,因此这些符号的地址会在编译时被留空为0。此时的重定位,就是链接器将这些留空为0的外部符号填上正确的地址。 具体的链接过程,可以通过 我们可以通过使用gcc的
我们可以通过objdump来查看静态链接完成以后生成的可执行文件
注意,这个 我们可以看到,之前填0留空的地方都被填充上了正确的数值, 静态链接库的构建与使用假如我们要制作一个关于向量的静态链接库
我们只需要这样来进行编译(两个都行):
假如我们有个程序
只需要在这样编译链接即可:
静态链接过程图示我们以使用刚才构建的静态库 可执行文件的装载进程和装载的基本概念的介绍程序(可执行文件)和进程的区别
现代操作系统如何装载可执行文件
可执行文件在装载的过程中实际上如我们所说的那样是映射的虚拟地址空间,所以可执行文件通常被叫做映像文件(或者Image文件)。 可执行ELF文件的两种视角可执行ELF格式具有不寻常的双重特性,编译器、汇编器和链接器将这个文件看作是被区段(section)头部表描述的一系列逻辑区段的集合,而系统加载器将文件看成是由程序头部表描述的一系列段(segment)的集合。一个段(segment)通常会由多个区段(section)组成。例如,一个“可加载只读”段可以由可执行代码区段、只读数据区段和动态链接器需要的符号区段组成。 区段(section)是从链接器的视角来看ELF文件,对应段表 Section Headers,而段(segment)是从执行的视角来看ELF文件,也就是它会被映射到内存中,对应程序头表 Program Headers。 我们用命令 可执行文件的程序头表我们用 我们通过 可执行ELF文件个进程虚拟地址空间的映射关系我们可以通过 该虚拟文件有6列,分别为:
总体来说,在程序加载过程中,磁盘上的可执行文件,进程的虚拟地址空间,还有机器的物理内存的映射关系如下: Linux下的装载过程接下来我们进一步探究一下Linux是怎么识别和装载ELF文件的,我们需要深入Linux内核去寻找答案 (内核实际处理过程涉及更多的过程,我们这里主要关注和ELF文件处理相关的代码)。 当我们在bash下输入命令执行某一个ELF文件的时候,首先bash进程调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件 ,内核开始真正的装载工作。 下图是Linux内核代码中与ELF文件的装载相关的一些代码:
例子:静态ELF加载器,加载 a.out 执行我们同样以刚才介绍静态链接时的 静态ELF文件的加载:将磁盘上静态链接的可执行文件按照ELF program header,正确地搬运到内存中执行。 操作系统在execve时完成:
加载完成之后,静态链接的程序就开始从ELF entry开始执行,之后就变成我们熟悉的状态机,唯一的行为就是取指执行。 我们通过readelf来查看
输出: 我们这里看到,程序的入口地址是: 上图是笔者在gdb中调试的一些内容:
调试的结果符合我们对静态程序加载时操作系统的行为的预期。 动态链接什么是动态链接以及为什么需要动态链接实际上,链接程序在链接时一般是优先链接动态库的,除非我们显式地使用-static参数指定链接静态库,像这样:
静态链接和动态链接的可执行文件的大小差距还是很显著的, 因为静态库被链接后库就直接嵌入可执行文件中了。 这样就带来了两个弊端:
动态库的出现正是为了弥补静态库的弊端。因为动态库是在程序运行时被链接的,所以磁盘上和内存中只要保留一份副本,因此节约了磁盘空间。如果发现了bug或要升级也很简单,只要用新的库把原来的替换掉就行了。 Linux环境下的动态链接对象都是以.so为扩展名的共享对象(Shared Object). 真的是动态链接的吗?我们常说gcc默认的链接类型就是动态链接,而且我们及其中运行的大部分进程也都是动态链接的,真的是这样的吗?我们不妨来做个实验验证一下。 我们通过创建一个动态链接库 首先我们有
这就是我们刚才说的一个动态链接库的源代码。我们一会儿会把他编译成
它会调用
正如我们刚才所介绍的,我们会先将
实验证明,我们的操作系统能够很好地运行这1000个进程,并且内存只多占用了 400MB。也就是说,库中的 这在操作系统内核不难实现:所有以只读方式映射同一个文件的部分(如代码部分)时,都指向同一个副本,这个过程中会创建引用计数。 动态链接的例子假如我们要制作一个关于向量的动态链接库
其中 我们现在拿一个使用到这个共享库的可执行文件来看一下,其源代码
注意我们在最后加了一个死循环是为了让进程保持运行,然后去查看进程的虚拟地址空间。 我们先编译源码,注意在同目录下可以直接按以下命令编译,之后我们会介绍将动态链接库放到环境目录后的编译命令。
然后先用
输出: 我们看到,该可执行文件是共享对象,并且是动态链接的。
输出:
输出: 我们看到,除了像静态链接时,进程地址空间中的堆、栈、 动态链接的实现机制程序头表我们同样用
可以看到编译完成之后地址是从 关键技术之前在静态链接的过程中我们提到过重定位的过程,那个时候其实属于链接时的重定位,现在我们需要装载时的重定位 ,主要使用了以下关键技术:
引入动态链接之后,实际上在操作系统开始运行我们的应用程序之前,首先会把控制权交给动态链接器,它完成了动态链接的工作之后再把控制权交给应用程序。 可以看到动态链接器的路径在 .dynamic段我们来看一下和动态链接相关的.dynamic段和它的结构,.dynamic段其实就是全局偏移表的第一项,即GOT[0]。 可以通过 它对应的是 动态链接器ld对于动态链接的可执行文件,内核会分析它的动态链接器地址,把动态链接器映射到进程的地址空间,把控制权交给动态链接器。动态链接器本身也是.so文件,但是它比较特殊,它是静态链接的。本身不依赖任何其他的共享对象也不能使用全局和静态变量。这是合理的,试想,如果动态链接器都是动态链接的话,那么由谁来完成它的动态链接呢? Linux的动态链接器是glibc的一部分,入口地址是sysdeps/x86_64/dl-machine.h中的_start,然后调用 elf/rtld.c 的_dl_start函数,最终调用 dl_main(动态链接器的主函数)。 动态链接过程图示动态链接库的构建与使用创建自己的动态链接库创建号一个动态链接库(如我们的 为了在全局使用动态链接库,我们可以将我们自己的动态链接库移动到
之后我们只要在需要使用到相关库时加上
大家也注意到了,上面的命令要用到管理员权限
命名规范动态链接库要命名为: 实现动态链接及实际ELF的动态链接想必大家看了上面一节对动态链接的介绍,已经明白动态链接以及动态链接库的大体过程和用法,但是对其中具体的实现细节还是比较迷惑。本节是笔者在听南大蒋炎岩老师的录播课程时做的笔记。蒋老师从分析实现一个简易的动态链接的三个等级的需求讲起,逐步引出了上面笔者提到的动态链接的三个关键技术:PIC、GOT并加以介绍,最后通过介绍实际中ELF的动态加载过程,介绍GOT、PLT配合实现的lazy symbol resolve。想要更加深入地理解动态链接的实现过程的朋友可以读一下本节。如果笔者的笔记有令人疑惑的地方,也可以去看蒋老师在B站的录播课程。 讲解的总体思路如下: 我们通过逐步把需求进行分解,从加载的视角理解链接:
实现动态链接与加载我们要实现动态链接,需要具体做到哪些事情呢?我们希望有一个库函数,其中包含一些代码,所有的进程链接这一段代码,这段代码在内存中只有一份拷贝。 实现动态加载(1)需求1:加载纯粹的代码 编译成位置无关代码(Position Independent Code, PIC)即可,即引用代码(跳转)全部使用PC相对寻址。x86已经是这样了。直接把代码mmap进地址空间就行了。
比如上面这段代码,它很简单,就是返回1。 实现动态加载(2)需求2:动态链接库只有纯粹的代码没有数据可不行,我们要能加载代码,并且代码有附带的数据。 这也好办,将代码和数据放在一起,然后都使用PC相对寻址就好了。 对于x86不支持rip相对寻址,我们可以通过 我们有这样一段代码:
这相当于这样一段C代码:
即在静态区定义一个变量x,然后每次调用 实现动态加载(3)需求3:比较难的是,一个文件或者一个动态链接库想要访问另外一个动态链接库导出的符号。因为我们想要知道的符号(比如 解决方法是我们用一张表,编译时编译成:
重填(相当于在运行时重做静态链接),这样行吗?不行,因为这样违背了我们动态链接的初衷:希望整个内存中只有一份代码的副本,而每次重填会导致每次都在内存中多一份代码的副本。而上面的解决方案,只有这张表,是需要复制的,这大幅减少了系统中冗余的内存。 总结总结一下,实现动态链接和加载就是两个关键点:
例子假如我们是十几种有这样一个动态链接(共享代码)的需求:
我们知道,动态加载的程序最先并不是从
所有的填表都完成之后,就跳转到 ELF 动态链接与加载上面一种简化版的动态加载过程,实际的ELF动态加载比这要复杂一点。 GOT (Global Offset Table)GOTGOT:shared object用来存储动态符号的表格。库函数有,可执行文件也有。 所以用 GOT中储存的数据
新需求新需求:能否降低实际没有调用到的符号的开销? 程序可能会引用很多符号,但执行时可能大部分符号都没用到,逐个dl_runtime_resolve的话会造成不必要的开销。 lazy symbol resolution想法:加载时设置为NULL,加载时来判断 / 解析 使用一小段 ”trampoline code“ 跳板代码
需要编译器把向printf(动态链接库)的调用翻译成call printf_internal 坏处:fast path多做一次判断:call + load + 判断 + jump,会损失一定的性能。 黑科技:让printf@GOT指向trampoline的下一条指令。
Takeaways and Wrap-up我们通过逐步把需求进行分解,从加载的视角理解链接:
入口函数和运行库入口函数初学者可能一直以来都认为C程序的第一条指令就是从我们的 我们有两个C代码:
分别编译运行这两个程序,输出结果分别为:
可见,在main开始前和结束后,其实还有一部分程序在运行。 事实上操作系统装载程序之后首先运行的代码并不是我们编写的main函数的第一行,而是某些运行库的代码,它们负责初始化main函数正常执行所需要的环境,并负责调用main函数,并且在main返回之后,记录main函数的返回值,调用atexit注册的函数,最后结束进程。以Linux的运行库glibc为例,所谓的入口函数,其实 就是指ld 默认的链接脚本所指定的程序入口_start (默认情况下) 运行库glibc = GNU C library Linux环境下的C语言运行库glibc包括:
事实上运行库是和平台相关的,和操作系统联系的非常紧密,我们可以把运行库理解成我们的C语言(包括c++)程序和操作系统之间的抽象层,使得大部分时候我们写的程序不用直接和操作系统的API和系统调用直接打交道,运行库把不同的操作系统API抽象成相同的库函数,方便应用程序的使用和移植。 Glibc有几个重要的辅助程序运行的库 其中crt1包含了基本的启动退出代码, ctri和crtn包含了关于.init段及.finit段相关处理的代码(实际上是_init()和_finit()的开始和结尾部分) Glibc是运行库,它对语言的实现并不太了解,真正实现C++语言特性的是gcc编译器,所以gcc提供了两个目标文件crtbeginT.o和crtend.o来实现C++的全局构造和析构 – 实际上以上两个高亮出来的函数就是gcc提供的,有兴趣的读者可以自己翻阅gcc源代码进一步深入学习。 几组概念的辨析动态链接的可执行文件和共享库文件的区别问题: 可执行文件和动态库之间的区别?我们在第一节中提到过动态链接的可执行文件和动态库文件 可执行文件和动态库之间的区别,简单来说:可执行文件中有main函数,动态库中没有main函数,可执行文件可以被程序执行,动态库需要依赖程序调用者。 在可执行文件的所有符号中,main函数是一个很特别的函数,对C/C++程序开发人员来说,main函数是整个程序的起点;但是,main函数却不是程序启动后真正首先执行的代码。 除了由程序员编写的源代码编译成目标文件进而链接到程序内存映射,还有一部分机器指令代码是在链接过程中添加到程序内存映射中。 比如,程序的启动代码,放在内存映射的起始处,在执行main函数之前执行以及在程序终止后完成一些任务 编译动态库时,链接器没有添加这部分代码。这是可执行文件和动态库之间的区别。 静态库和共享库库:有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程序中都会用到libc中的库函数(例如printf)。 共享库和静态库的区别:在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件调用的libc库函数仍然是未定义符号,要在运行时做动态链接。而在链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。
静态库好处:静态库中存在很多部分,链接器可以从静态库中只取出需要的部分来做链接 (比如main.c需要stach.c其中的一个函数,而stach.c中有4个函数,则打包库后,只会链接用到那个函数)。另一个好处就是使用静态库只需写一个库文件名,而不需要写一长串目标文件名。 RefComputer Systems A Programmer’s Perspective - by Randal E. Bryant & David O’Hallaron https://www.bilibili.com/video/BV1hv411s7ew https://blog.csdn.net/weixin_44966641/article/details/120616894?spm=1001.2014.3001.5501 https://www.bilibili.com/video/BV1N741177F5?p=15 https://www.jianshu.com/p/7c609b70acbd |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/15 18:39:36- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |