🌈hello,你好鸭,我是Ethan,一名不断学习的码农,很高兴你能来阅读。
✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。
🏃人生之义,在于追求,不在成败,勤通大道。加油呀!
🔥个人主页:Ethan Yankang
🔥专栏:CSAPP笔记||书籍网课笔记
🔥温馨提示:划到文末发现专栏彩蛋
🔥本篇概览:详细介绍程序从编写到执行的全过程、可重定位文件(目标文件)、可执行文件等链接全过程。
目录
查看ELF header 的内容 readelf -h main.o
查看ELF section 的内容 readelf -S main.o
什么是链接
关于链接就是将可重定位目标文件以及必要的系统文件组合起来形成可执行文件,最终的文件可以加载到内存中执行。
在大型文件的开发中,我们不可能将所有的功能都放在一个一个源文件中,而是分而治之,有多个小文件组成——
理解链接的好处?
1、理解编程语言中的作用域规则是如何实现的,例如全局变量和局部变量的作用域是如何实现的?当我们看到一个static的函数或者变量时,实际的变量意义到底是什么?
2、除此之外,理解链接还可以帮助我们理解系统概念——比如程序的加载和运行、虚拟内存、内存映射等等。
3、更好的利用共享库。
实际链接过程
编译命令过程
全解析
如果不特定指定-o 后面的文件名,会默认生成文件名为a.out。
预处理
main.c——>main.i
编译
main.i——>main.s
汇编
main.s——>main.o
链接
main.o——>main.exe
手动链接过程
当我们手动链接时,除了使用main.o与sum.o之外,还要使用如下文件——
其中的crt指的是 c time runtime 的缩写。链接就是将这些文件重定向文件打包成可执行文件。
具体文件如下——
这里的ld指的是链接器,-static表示采用静态链接的方式。 通过这条命令我们就可以执行手动链接过程。
关于每个crt具体实现的功能,可以参考《程序员的自我修养》一书。
执行./prog的过程
最后的文件可以通过shell控制台的这样的命令执行。
具体是shell通过调用操作系统中的加载器函数loader来实现的,加载器将可执行文件prog中代码和数据复制到内存中,然后将CPU的控制权转移到prog文件的开头,随后程序prog开始执行。
以上就是从程序从源文件到可执行文件的整个过程
可重定位目标文件
也叫目标文件,也叫可重定位定位文件。
所有代码都在main.c中。main中有局部static变量a、b。
源代码文件编译成可重定位文件,但是不进行链接—— gcc -c
这里的wc -c命令是计数可重定向文件中的字节数,这里是1896个。
接下来看看这里面这里的1896B的文件到底是什么样的数据。
组成
每一个可重定向文件大致可以分为三个部分——
分别是ELF header,不同的section,以及描述这些section信息的表。
其中,ELF是可执行可链接文件的首字母缩写。
查看ELF header 的内容 readelf -h main.o
魔数
首先,文件开头的16个字节被称为ELF文件的魔数,通俗来讲,魔术就是用来确认文件类型的。
操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确就拒绝加载。
type
这一行信息(REL Relocatable file)可以看出是可重定位文件,除此之外还有两种文件,分别是可执行文件和共享文件。
Size of this header
64B=0x40,则ELF文件头就是64字节,可以看出section在elf文件中的起始位置就是0x40
除比之外,ELF-heacer中还给中了header table的信息 ,他是用来描述不同的section属性的表。
这样就可以计算出ELF 和header的全部大小为:1896。恰好与我们用wc -c计算出来的字节数一致。
关于ELF header更多内容,感兴趣的同学可以查看《程序员的自我修养》一书
查看ELF section 的内容 readelf -S main.o
readelf -S main.o
readelf -S main.o 其中的 -S 选项表示打印整个表信息。
除了表中的第一项,其他每一个表项都对应着一个section。
我们可以看见整个ELF中包含12个section。
.text section
注意这里的.text的起始地址是0x40,就是说明在ELF header之后紧跟着的就是.text.
这个section中存放的是已经编译好的机器代码。对于查看已经编译好的机器代码,我们需要使用反汇编工具objdump将机器代码转换成汇编代码。
如下:
其中左边的四位数字代表的地址,右边的部分代表的是具体的机器指令。
.data section
.data section是用来存放已初始化的全局变量和静态变量的值。例如,程序中我们将全局变量count初始化为10,静态变量a初始化为1
.bss section
未初始化的和被初始化为0的全局变量和静态变量会存放在.bss section中。
注意一点:局部变量既不在data中,也不在bss中,而是在栈帧中。
实际上.bss section并不占据实际的空间,它仅仅只是一个占位符,区分已初始化和未初始化的变量是为了节省空间。当程序运行时,会在内存中分配这些变量,并把初始值设为0。
.rodata section
.data section之后是.rodata section,这里的ro是read only的缩写。
这个section就是用来存放只读数据的,例如例如,printf语句中的格式串和switch语句中的跳转表就是存在这个区域内。
其他重要的section
我们可以将符号看作是链接中的粘合剂,整个链接过程正是因为有符号才能正常进行.
接下来重点查看.symtab (symbol table)的内容
.symtab (symbol table)
先查看整个符号表名称:readelf -s main.o
接下来,我们看一下符号表中的符号与源程序之间存在什么样的关系
Common与.bss 之间的关系很微妙
这是因为局部变量在运行时栈中被管理,链接器对此类符号并不感兴趣。
链接器中的三种符号
对于每一个可重定位目标文件,都有一个符号表,这个符号表中包含该模块定义和引用的符号信息。在链接器的上下文中,有三种不同的符号——
全局符号
第一种是由该模块定义,同时能被其他模块引用的全局符号。例如main.c中定义的函数func以及全局变量count和value。
外部符号
第二种是被其他模块定义,同时被该模块引用的全局符号。这叫做外部符号。
局部符号
第三种是只能被该模块定义和引用的局部符号
C语言中,static的含义
任何带有static属性声明的全局变量或者函数都是模块私有的
强符号弱符号
编译器将符号按强弱等级分强弱两个等级。
在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱。接下来,汇编器把这个强弱信息隐含的编码在符号表中。
rule1 强强同名
链接器不允许有多个同名的强符号一起出现。会报错。
如下的x
rule2 强弱同名
不会报错,只会生成警告。
rule3 弱弱同名
x都是未初始化的,那么它们都属于弱符号。
-Werror选项,这个选项会把所有的警告都变为错误。
链接器如何使用printf类的静态库
Linking with Static Libraries
在linux系统中,静态库以一种称为archive的特殊文件格式存放在磁盘上,以.a结尾
archive文件是一组可重定位目标文件的集合
反汇编后看到lib.a中的文件集合
查看printf.o的内容
我们可将libc.a解压在当前目录之下。统计得出共1690个文件。
构建静态库并形成可执行文件的过程
源文件分别如下:
先将两文件编译成可重定向文件。
使用ar命令完成静态库的生成libvector.a
演示静态库的用法
注意这里的vector.h里面包含了静态库的引用。
main中引用了 vector,其中头文件vector.h中定义了libvector.a中的函数原型。
注意这里的链接过程,-static代表是静态库链接。
当链接器运行时,它确定main.o中引用了addvec.o中定义的addvec符号,所以链接器就从libvector.a中复制addvec.o到可执行文件,因为程序中没有引用multvec.o中定义的符号,所以链接器就不会将这个模块复制到可执行文件。除此之外,链接器还会从libc.a中复制printf.o模块以及其他C runtime所需的模块
链接器如何引用符号解析的?
我们看一下链接器是如何使用静态库来解析引用的
链接器从左到右按照命令行中出现的顺序来扫描可重定位文件和静态库文件,由于编译器驱动程序总是会把libc.a传给链接器,所以不写也可以。
三种集合
在扫描过程中,链接器一共维护了3个集合——
E:
在链接器扫描的过程中发现了可重定位目标文件就会放到这个集合中。
在链接即将完成的时候,这个集合中的文件最终会被合并起来形成可执行文件。
U:
第二个是集合U,链接器会把引用了但是尚未定义的符号放在这个集合里。
D:
第三个是集合D,它用来存放输入文件中已定义的符号。
链接刚开始时,这三个集合均为空。对于命令行上的每一个文件f,链接器都会判断f是一个目标文件还是静态库文件。如果f是一个目标文件,那么链接器会把f添加到集合E中。同时修改集合U和D来反映f中的符号定义与引用。
如下:
目标文件处理
连接器在读取到main.o时,判断这是目标文件,就会将main.o放入集合E中,通过main.o的符号表可以看到这个目标文件中存在两个不在当前模块中定义的符号,分别是printf和addvec。
接下来将这两者放入集合U中。此时链接器假设他们在其他模块中被定义,所以并不会报错。
除此之外,main.o中已经定义的全局符号x,y,z以及main会放到集合D中。
静态库处理
当文件main.o处理完成之后继续处理下一个文件,libvector.a是一个静态库文件,那么链接器就尝试在这个静态库文件中寻找集合U中未解析的符号。例如静态库文件中libvect.a存在两个成员,分别为addvect.o和multvect.o。
当链接器发现成员addvect.o中存在未定义的符号addvec的定义,此时就把addvect.o加到集合E中,然后将集合U中的符号addvect删除。
如果addvect.o中还定义了其他的符号,还要添加到集合D中,所以addcnt也要被添加到集合D中。
链接器处理完成员addvect.o之后,还要处理multivec.o;
对于静态库文件中的所有成员目标文件都要依次进行上述处理过程,直到集合U和集合D不在发生变化。此时任何不包含在集合E中的文件都被简单的丢弃。
对于这个例子,multivec.o被丢弃,addvec.o被保留。
最后链接器还要扫描liba.c文件,printf.o会加载到集合E中。 集合U中的符号printf被删除。上述操作执行整之后,如果集合U是空的,链接器会合并集合E中的文件来生成可执行文件。
如果集合U是非空的,那就说明了程序使用了未定义的符号,会发生链接错误,链接器就会输出错误并终止。
重定位
在上一期的视频中,我们介绍了符号解析的相关内容。当链接器把代码中的符号引用和对应的符号定义关联起来之后,链接器就可以确定要将哪些目标文件进行合并了。同时,链接器也获得了这些目标文件的代码节和数据节的大小信息,接下来开始进行重定位的操作。
可执行文件
详细介绍
可执行文件中定义了一个.init section,里面定义了一个程序的入口函数——_init
程序的初始化代码会利用这个函数进行初始化 .关于.text、rodata以及.data section与可重定位目标文件中的节是类似的.不过这些节已经被重定位到最终的运行时内存地址上。因此可执行文件中不再需要rel section。以上就是可执行文件的大致情况。
程序运行时,可执行文件中的代码段和数据段需要加载到内存中执行。不过还有一部分不会加载到内存中执行。如符号表、调试信息等。具体如下所示——
可执行程序prog的程序头部表,也就是段头部表的内容,描述了代码段、数据段与内存的映射关系。
程序加载到执行的过程
所有的程序都是通过调用函数execve函数来调用加载器,接下来加载器将可执行文件中的数据与代码从磁盘复制到内存。然后调转到程序的入口来执行该程序。
这个将程序从磁盘复制到内存并运行的过程叫做加载。
每一个linux程序都有一个运行时内存镜像,具体如下:
实际上由于数据段有地址对齐的要求,所以代码段和数据段之间是有间隙的。
同时,为了防止程序受到攻击,在分配栈、共享库以及堆的运行时地址时,链接器还会用到地址空间随机化的策略,所以每次程序运行时这些区域之间的地址都会改变。不过相对位置不变。
当加载器运行时,为程序创建如图所示的内存镜像。
根据程序头部表的内容,加载器将可执行文件的section复制到内存相应的位置。接下来加载器跳转到程序的入口处,也就是_start()函数的地址。这个start函数在系统目标文件ctrl.o中定义,对于所有的C程序都是一样的。
接下来函数 start调用系统启动函数libc_start_main(),这个函数位于libc.so中,它的作用是初始化执行环境。然后调用main函数。
接下来开始执行可执行文件中的main函数。当程序prog执行完毕后,函数main的返回值还是由libc.so中的这个函数来处理。并且在程序需要之时将控制权交还给操作系统。
这只是大致的粗略内容,因为想要理解程序的加载是如何工作的,必须理解进程、线程、虚拟内存、内存映射等知识,这些内容之后再了解。
动态链接共享库
简介
静态库与所有的软件一样,需要定期维护和更新。如果软件开发人员想要使用一个库的最新版本。然将要获取最新共享库后,将更新后的静态库与他们编写的程序重新进行链接。
这样应用程序才能用到静态库的最新版本。
林外一个问题是几乎每个进程都会用到标准输入输出函数,如printf、scanf函数等,这些函数代码会复制到每个进程的代码段中。那么一个对于运行了成百上千的进程而言,这会导致内存空间的极大浪费。为了解决静态库的缺陷,操作系统提供了一种共享库的技术。
共享库是一种可重定位目标文件,在Linux系统中通常用.so的后缀表示。windows系统中使用了大量的共享库,是以DLL结尾的。
共享库在运行或加载时,可以被加载到任意的内存地址,还能和一个也能行中的程序链接起来, 这个过程称为“动态链接”具体是由动态链接器来执行的。
创建共享库
这里的-share是指示编译器创建共享库的。-fpic是告诉编译器生成与位置无关的代码,这样共享库才能被加载到任意地址执行。这条命令执行完毕后就可以构造得到名为libvector.so的共享库。
接下来可以使用这个共享库来构造可执行程序prog2。具体命令如下:
与静态库的链接命令相比,虽然只是将libvector.a变成了libvector.so,但是libvector.so中的数据并没有真正的复制到可执行文件prog2中。这个操作知识复制了一些重定位表和一些重定位信息。
当可执行程序prog2被加载运行时,加载器会发现可执行文件prog2中存在一个名为.interp的section。这个section中包含了动态链接器的路径名。
实际上这个动态链接器本身也是一个共享目标文件(ld-linux.so)。加下来加载器会将动态链接起加载到内存中运行。然后由动态链接器执行重定位代码和数据的工作。
动态链接的作用
许多Windows应用的开发者常常利用共享库来进行软件版本的更新
构建高性能web服务器
💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖
热门专栏推荐
🌈🌈计算机科学入门系列 关注走一波💕💕
🌈🌈CSAPP深入理解计算机原理 关注走一波💕💕
🌈🌈微服务项目之黑马头条 关注走一波💕💕
🌈🌈redis深度项目之黑马点评 关注走一波💕💕
🌈🌈Java面试八股文系列专栏 关注走一波💕💕
🌈🌈算法leetcode+剑指offer 关注走一波💕💕
总栏
🌈🌈JAVA后端技术栈 关注走一波💕💕
🌈🌈JAVA面试八股文 关注走一波💕💕
🌈🌈JAVA项目(含源码深度剖析) 关注走一波💕💕
🌈🌈计算机四件套 关注走一波💕💕
🌈🌈算法 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总 关注走一波💕💕
🌈🌈考试复习资料 关注走一波💕💕
🌈🌈C/C++技术栈 关注走一波💕💕
🌈🌈GO技术栈 关注走一波💕💕
分栏
🌈🌈JAVA后端技术栈
🌈🌈spring 关注走一波💕💕
🌈🌈redis 关注走一波💕💕
🌈🌈MySQL 关注走一波💕💕
🌈🌈mybatis 关注走一波💕💕
🌈🌈mybatisplus 关注走一波💕💕
🌈🌈MQ 关注走一波💕💕
🌈🌈微服务 关注走一波💕💕
🌈🌈设计模式 关注走一波💕💕
🌈🌈分布式锁 关注走一波💕💕
🌈🌈JAVA八股文JAVA面试八股文(redis、MySQL、框架、微服务、MQ、JVM、设计模式、并发编程、JAVA集合、常见技术场景) 关注走一波💕💕
🌈🌈JAVA项目(含源码深度剖析)
🌈🌈黑马头条(微服务) 关注走一波💕💕
🌈🌈黑马点评(redis) 关注走一波💕💕
🌈🌈计算机四件套
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机网络 关注走一波💕💕
🌈🌈数据结构与算法 关注走一波💕💕
🌈🌈算法
🌈🌈leetcode 关注走一波💕💕
🌈🌈剑指offer 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总
🌈🌈CSAPP笔记 关注走一波💕💕
🌈🌈计算机科学速成课 关注走一波💕💕
🌈🌈CS自学指南 关注走一波💕💕
🌈🌈读书笔记与每日记录 关注走一波💕💕
🌈🌈考试复习资料 关注走一波💕💕
🌈🌈C/C++技术栈 关注走一波💕💕
🌈🌈GO技术栈 关注走一波💕💕
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤收藏✅ 评论💬,大佬三连必回哦!thanks!!!
📚愿大家都能学有所得,功不唐捐!