由于在之前linux学习和CSAPP学习中,已经对这部分有了很多了解,具体可以看我的相关博客,这里再简要的用流程图来总结一下
ELF格式文件主要分为以下四类
其中目标文件和可执行文件的格式几乎是一样的,下面就着重介绍一下目标文件的格式,以CSAPP中的图来说明:
下面挑选重点段来介绍。
其中主要存储了ELF文件的一些版本和相关信息,其中最主要的信息是入口地址、程序头入口及程序长度、段表(节头部表)的位置和长度
代码段,存储的就是程序二进制代码,这里来解释一下用objdump得到的代码段信息的含义:
其中指令中的-s代表输出将所有段内容以十六进制方式打印出来(这里只截取了代码段的内容,其他段省略),-d表示将所包含的代码段用再用反汇编显示出来(所以这部分不是真正代码段中的内容)
真正代码段中第一列是地址偏移量,中间四列是以十六进制显示出来的内容。最后一列是用ACSII码形式。
只读数据段,存放的是只读变量(比如const修饰的变量)和字符串常量(有些编译器会把字符串常量放到下面的数据段中)。
数据段,存放已经初始化的全局变量和静态变量。
注意:局部变量不在.rodata,.data,.bss中,是被保存在运行的栈中
未初始化段,存放未初始化的全局变量和静态变量。
.bss段中的变量并不占用实际存储空间,所以减少了磁盘空间,仅仅是一个占位符,用objdump得到的就是如下的情况,占4字节。
但是到了加载执行的过程中会开辟相应的内存给未初始化的变量。
符号表,存储的是在程序中用到的各个符号的名字以及符号值,这里的符号值对于变量和函数来说就是它们的地址。符号表是链接过程中最重要的段,尤其是符号表中的全局符号,是链接过程的主要处理对象。
就是针对.text段的重定位表,其中是对代码段中运用到的外部全局函数地址重定位信息。
针对.data段的重定位表,其中是对全局外部变量的地址重定位信息。
字符串表,这里主要存储段名以及变量名的字符串。因为字符串的长度往往是不定的,所以先集中存储起来,然后用偏移地址来表示字符串,如下所示:
这样在ELF文件中只需给一个数字下标就可以得到字符串,符号表中的符号名都是这样表示的。
这里存储的是各段的信息,比如各段的段名(这里也是偏移量),段长度,在文件中的偏移,读写权限等等。所以想要遍历每一段,需要通过段表才能遍历。
注意:解析ELF表的一般过程:先解析ELF头,可以得到段表和字符串表中段名的位置,从而可以解析整个ELF文件。
.init:该段保存的是可执行的指令,构成进程初始化代码,在一个程序开始运行,并在main函数调用之前,会执行.init中的代码。
.fini:该段保存着进程终止代码指令,当main函数正常退出时,会执行这个段中的代码。
这两个段对于全局的类对象,很有用,全局类对象在main执行之前就进行构造函数,在main结束之后,再执行析构函数,所以构造和析构函数都放在这两个段中。
静态链接步骤是紧跟着汇编步骤之后执行的。
整个静态链接的过程按照CSAPP的说法分为两部分,第一是符号解析,第二是重定位。
符号解析就是将所有的可重定位文件中符号表中的符号找到唯一对应的地址(偏移量)。首先讲解一下符号表的结构:
符号解析主要完成的是,对于内部符号(在该可重定向目标文件中定义的符号),查看是否唯一(如果不唯一,需要链接器按规则解析多重定义的符号)对于外部符号(与内部符号相反,在符号表中是UND的就是外部符号),就是要在其他的可重定向文件或者静态库中找到对应的符号定义。
在静态链接过程中,如果有一个库里面有多个可重定位文件,但是在使用这个库时只需要其中某几个文件中的函数即可,这时候有两种选择:
这时候静态库的出现就完美结合了这两者的优点。通俗点说,将库中所有的文件打包成静态库,连接器就会帮你自动去找到所用到可重定位文件,然后拿出来进行链接。
所以静态库其实是可重定位文件的一个集合形式,所以认为静态库也是可重定位文件
以一个例子来说明:
介绍gcc编译器的静态链接过程,gcc静态链接是根据gcc编译命令中从左到右的顺序来解析可重定位文件与静态库的。
下面截取了CSAPP中的原话:
当完成符号解析以后,可执行文件中所有的内容都可以确定下来了,这时就可以输出可执行文件了。重定位由两步组成:重定位节和符号定义,重定位节中的符号引用。
就是将符号解析得到的E集合中的可重定位文件合并成一个可执行文件。这里涉及两个地址的分配。一个是多个可重定位文件中的内容分配到可执行文件中的地址上(在磁盘空间中,方法是相同的段放在一起),另一个是将可执行文件中各个段内容映射到虚拟地址上(在虚拟内存空间)。
举例如下:
其中每一个符号地址的确定方法(外部符号除外)就是通过偏移量来确定的,之前的符号地址都是给的从段头开始的偏移量,现在只要用虚拟地址加上偏移量即可。
之前讲了非外部地址是通过偏移量来确定在虚拟空间中的地址的,那么重定位节中的符号引用就是确定外部符号的地址。在可重定位文件中,外部符号一般是以一个临时的假地址来作为外部符号的地址。
重定位的步骤是:
在得到可执行文件以后,下面就是加载运行可执行文件了。由于可执行文件中的各种地址已经被转换成在虚拟内存中的地址了,所以首先要建立虚拟内存,并且将可执行文件载入到虚拟内存中去,才可以对应使用。那么这里先介绍一下虚拟内存
几个概念
可执行文件装载的过程其实就是进程创建的过程,进程创建的过程,整个过程主要做三件事:
用图片的方式来展现:
注释:
动态链接库是针对静态链接库的缺点来做补充的,所以先讲一下静态链接库的缺点(其实也是可重定位文件的缺点):
所以动态库的好处如下:
但是动态库也有缺点,那就是运行速度没有静态库快,因为在加载阶段,动态库需要消耗额外的时间进行符号查找,重定位工作,一般比静态库程序运行性能减少1%~5%。
我们可以看到在静态链接时,不需要动态库所有信息,只需要少部分信息即可。
注意:动态链接器也是一个动态库。
windows可以参考此篇博客:https://blog.csdn.net/liangyanghui/article/details/77981848
当一个可执行文件(暂时不考虑动态库)加载结束以后,虚拟内存的布局如下:
需要声明的是,这张图其实网上出现很多,但是这个虚拟内存布局究竟是如何得到的呢?
其实可以理解为这是将页表映射的内容完全罗列下来得到的。其中有的部分在内存中,比如与进程相关的数据结构,有的在磁盘上,比如代码、未初始化,已初始化数据,这些都在ELF文件中,还有一些根本不存在,比如标蓝色的区域,这些是分给堆栈,但并未被分配的区域,这些区域在实际中是不存在的。
虚拟内存的分区有多种分法,这里选用最常用的一种:
分为内核区,栈区,堆区,全局静态区,文字常量区,代码区和保留区
内存中的栈仍然具有先进后出的特性,栈总是按虚拟地址向下生长的
栈最重要的作用就是在程序运行过程中,保存正在执行函数所需要维护的信息,包括如下:
函数栈帧区域是依靠ebp和esp两个寄存器来限定的。esp始终指向栈顶,也就是当前调用函数栈帧的顶部,ebp始终指向当前调用函数栈帧的底部。所以这两个寄存器中间的区域,就是当前执行函数的栈帧。
要想了解具体ebp和esp是怎样工作的,可以参考我的博客https://blog.csdn.net/qq_34489443/article/details/93158460
如果是4字节以内,用exa寄存器来传递
如果是5~8字节,用exa和edx两个寄存器来传递
如果大于8字节,以一个例子来说明
内存中的堆在存储上没有太多局限,是按虚拟地址向上生长的。
我们想要在已经成型的虚拟内存中去使用空闲的内存,可以用malloc或者new去申请,但是这属于C++封装过的函数,最底层的函数应该是系统调用,不同的系统不一样,这里着重介绍Linux系统。
linux系统中有两种内存获取方式:brk()和mmap()
brk():这个函数属于动态分配内存,也是堆的分配方法,也就是在运行中分配内存,这个函数其实就是调整brk指针的位置,brk指针指向的是堆顶,增加堆顶相当于扩大堆。
mmap():将磁盘上的空间映射到虚拟内存中堆和栈的中间那部分。
内存空间的管理方法,有空闲链表法等,空闲链表法还分隐式显式等。就不展开细讲了。
系统调用就是操作系统提供的函数接口。
系统调用的缺点:
针对这些缺点,出现了运行库,举例来说Linux中read函数是系统调用,用来读取文件,但是C语言运行库中是fread,fread函数在所有系统下都可以使用,而在Linux系统下,fread其实就是对read系统调用的封装,所以运行库有如下好处:
系统调用不像普通的函数,直接运行就可以了,系统调用需要执行特殊的步骤。在《程序员自我修养》中说系统调用是通过中断来执行的,但是在CSAPP中是依靠陷阱来执行的。我查阅了很多资料,最后觉得其实是说法的不一致,陷阱还有一种说法是软中断,与此相对的还有硬中断。所以在《程序员自我修养》中,系统调用是软中断实现的。
比较系统中四种异常:
所以以下做一个统一:以下说的中断就是软中断,硬中断会单独指出。
系统调用的过程:
上下文:内核重新启动一个闲置进程所需的信息,包括程序计数器,用户栈,内核栈和各种内核数据结构等。
上下文切换就是内核将当前进程保存起来,执行其他进程,注意用户模式切换成内核模式也需要上下文切换。
上下文切换的时机:
上下文切换的步骤: