操作系统知识点梳理

文章目录

      • 一、编译和链接
      • 二、ELF格式文件
          • 1、ELF头
          • 2、.text
          • 3、.rodata
          • 4、.data
          • 5、.bss
          • 6、.symtab
          • 7、.rel.text
          • 8、.rel.data
          • 9、.strtab
          • 10、节头部表(段表)
          • 11、其他有必要说的段
      • 三、静态链接的过程
          • 1、符号解析
            • a、概念
            • b、静态链接库的好处
            • c、gcc静态链接过程详解
          • 2、重定位
            • a、重定位节和符号定义
          • 2、重定位节中的符号引用
      • 四、虚拟内存
          • 1、前提概念
          • 2、虚拟内存运行的步骤
      • 五、可执行文件装载过程
      • 六、动态链接库
          • 1、动态链接库的好处
          • 2、动态库链接的示意图
          • 3、动态链接库中的关键技术
          • 4、动态库的调用方法
      • 七、程序运行时的内存
          • 1、虚拟内存布局
            • a、虚拟内存布局图
            • a、虚拟内存分区
          • 2、栈
            • a、栈的特性
            • b、函数栈帧
            • c、函数返回值的传递
          • 3、堆
            • a、堆的性质
            • b、linux中内存获取方法
            • c、对分配方法
      • 八、系统调用
          • a、系统调用
          • b、系统调用的过程
          • c、上下文切换

一、编译和链接

由于在之前linux学习和CSAPP学习中,已经对这部分有了很多了解,具体可以看我的相关博客,这里再简要的用流程图来总结一下
操作系统知识点梳理_第1张图片

二、ELF格式文件

ELF格式文件主要分为以下四类

  • 可重定位文件,包括汇编得到的二进制文件以及静态链接库文件。(Linux的.o文件,.a文件,windows的.obj文件,lib文件)
  • 可执行文件(Linux的a.out这类文件以及windows的.exe文件)
  • 共享目标文件,包括动态链接库文件(Linux的.so文件,以及windows中的dll文件)
  • 核心转储文件(Linux中的core dump文件)

其中目标文件和可执行文件的格式几乎是一样的,下面就着重介绍一下目标文件的格式,以CSAPP中的图来说明:
操作系统知识点梳理_第2张图片
下面挑选重点段来介绍。

1、ELF头

其中主要存储了ELF文件的一些版本和相关信息,其中最主要的信息是入口地址、程序头入口及程序长度、段表(节头部表)的位置和长度

2、.text

代码段,存储的就是程序二进制代码,这里来解释一下用objdump得到的代码段信息的含义:
操作系统知识点梳理_第3张图片
其中指令中的-s代表输出将所有段内容以十六进制方式打印出来(这里只截取了代码段的内容,其他段省略),-d表示将所包含的代码段用再用反汇编显示出来(所以这部分不是真正代码段中的内容)
真正代码段中第一列是地址偏移量,中间四列是以十六进制显示出来的内容。最后一列是用ACSII码形式。

3、.rodata

只读数据段,存放的是只读变量(比如const修饰的变量)和字符串常量(有些编译器会把字符串常量放到下面的数据段中)。

4、.data

数据段,存放已经初始化的全局变量和静态变量。
注意:局部变量不在.rodata,.data,.bss中,是被保存在运行的栈中

5、.bss

未初始化段,存放未初始化的全局变量和静态变量。
.bss段中的变量并不占用实际存储空间,所以减少了磁盘空间,仅仅是一个占位符,用objdump得到的就是如下的情况,占4字节。
在这里插入图片描述
但是到了加载执行的过程中会开辟相应的内存给未初始化的变量。

6、.symtab

符号表,存储的是在程序中用到的各个符号的名字以及符号值,这里的符号值对于变量和函数来说就是它们的地址。符号表是链接过程中最重要的段,尤其是符号表中的全局符号,是链接过程的主要处理对象。

7、.rel.text

就是针对.text段的重定位表,其中是对代码段中运用到的外部全局函数地址重定位信息。

8、.rel.data

针对.data段的重定位表,其中是对全局外部变量的地址重定位信息。

9、.strtab

字符串表,这里主要存储段名以及变量名的字符串。因为字符串的长度往往是不定的,所以先集中存储起来,然后用偏移地址来表示字符串,如下所示:
操作系统知识点梳理_第4张图片
这样在ELF文件中只需给一个数字下标就可以得到字符串,符号表中的符号名都是这样表示的。

10、节头部表(段表)

这里存储的是各段的信息,比如各段的段名(这里也是偏移量),段长度,在文件中的偏移,读写权限等等。所以想要遍历每一段,需要通过段表才能遍历。
注意:解析ELF表的一般过程:先解析ELF头,可以得到段表和字符串表中段名的位置,从而可以解析整个ELF文件。

11、其他有必要说的段

.init:该段保存的是可执行的指令,构成进程初始化代码,在一个程序开始运行,并在main函数调用之前,会执行.init中的代码。
.fini:该段保存着进程终止代码指令,当main函数正常退出时,会执行这个段中的代码。
这两个段对于全局的类对象,很有用,全局类对象在main执行之前就进行构造函数,在main结束之后,再执行析构函数,所以构造和析构函数都放在这两个段中。

三、静态链接的过程

静态链接步骤是紧跟着汇编步骤之后执行的。
整个静态链接的过程按照CSAPP的说法分为两部分,第一是符号解析,第二是重定位。

1、符号解析
a、概念

符号解析就是将所有的可重定位文件中符号表中的符号找到唯一对应的地址(偏移量)。首先讲解一下符号表的结构:
操作系统知识点梳理_第5张图片
符号解析主要完成的是,对于内部符号(在该可重定向目标文件中定义的符号),查看是否唯一(如果不唯一,需要链接器按规则解析多重定义的符号)对于外部符号(与内部符号相反,在符号表中是UND的就是外部符号),就是要在其他的可重定向文件或者静态库中找到对应的符号定义。

b、静态链接库的好处

在静态链接过程中,如果有一个库里面有多个可重定位文件,但是在使用这个库时只需要其中某几个文件中的函数即可,这时候有两种选择:

  • 一种就是你将库中所有文件都链接进来,这样方便,但是可执行文件的体积就很大,因为所有的可重定位文件都要加入进来。第
  • 另一种就是把用到的可重定位文件链接进来,可是这样需要人为筛选,比较麻烦。

这时候静态库的出现就完美结合了这两者的优点。通俗点说,将库中所有的文件打包成静态库,连接器就会帮你自动去找到所用到可重定位文件,然后拿出来进行链接。
所以静态库其实是可重定位文件的一个集合形式,所以认为静态库也是可重定位文件
以一个例子来说明:
操作系统知识点梳理_第6张图片

c、gcc静态链接过程详解

介绍gcc编译器的静态链接过程,gcc静态链接是根据gcc编译命令中从左到右的顺序来解析可重定位文件与静态库的
下面截取了CSAPP中的原话:
在这里插入图片描述

2、重定位

当完成符号解析以后,可执行文件中所有的内容都可以确定下来了,这时就可以输出可执行文件了。重定位由两步组成:重定位节和符号定义,重定位节中的符号引用。

a、重定位节和符号定义

就是将符号解析得到的E集合中的可重定位文件合并成一个可执行文件。这里涉及两个地址的分配。一个是多个可重定位文件中的内容分配到可执行文件中的地址上(在磁盘空间中,方法是相同的段放在一起),另一个是将可执行文件中各个段内容映射到虚拟地址上(在虚拟内存空间)。
举例如下:
操作系统知识点梳理_第7张图片
其中每一个符号地址的确定方法(外部符号除外)就是通过偏移量来确定的,之前的符号地址都是给的从段头开始的偏移量,现在只要用虚拟地址加上偏移量即可。

2、重定位节中的符号引用

之前讲了非外部地址是通过偏移量来确定在虚拟空间中的地址的,那么重定位节中的符号引用就是确定外部符号的地址。在可重定位文件中,外部符号一般是以一个临时的假地址来作为外部符号的地址。
重定位的步骤是:

  • 根据重定位表,找到需要重定位的代码以及变量。重定位表记录了在段的那个位置需要进行重定位,比如.rel.data就是记录数据段中那些偏移位置需要重定位。.rel.text就是定义在代码段的哪些位置需要重定位。举例如下:
    操作系统知识点梳理_第8张图片
  • 之前通过符号解析已经得到了重定位的符号在虚拟内存的地址,根据这个符号表进行指令修正(有绝对寻址修正,相对寻址修正等方法)

四、虚拟内存

在得到可执行文件以后,下面就是加载运行可执行文件了。由于可执行文件中的各种地址已经被转换成在虚拟内存中的地址了,所以首先要建立虚拟内存,并且将可执行文件载入到虚拟内存中去,才可以对应使用。那么这里先介绍一下虚拟内存

1、前提概念
  • 无论是虚拟内存还是物理内存,最小转换单位都是,也就是想要将虚拟内存加载到物理内存中去,最少要加载一页。
  • 虚拟内存并不是真正存在的,虚拟内存只是将磁盘空间和内存空间有机结合的一种方式,让人看起来好像磁盘和内存空间是在一起的。也可以说虚拟内存是磁盘空间与内存之间的桥梁。虚拟内存唯一占用物理空间的地方就是页表,页表是存储在物理内存中或者高速缓存中的,记录了虚拟页和物理页之间的对应关系。
    以图来说明:
    操作系统知识点梳理_第9张图片
    页表中每一行代表一个对应关系,有效位表明这个虚拟页是否被加载到内存中去,后面的二进制分为如果设置了有效位,那么就是物理页的起始地址,如果没有设置有效位,就是虚拟页在磁盘上的起始地址
2、虚拟内存运行的步骤

几个概念

  • 页命中:所使用的虚拟地址被缓存在内存中
  • 缺页:页命中的反义词,所使用的虚拟地址没有被缓存在内存中
    以CPU执行指令的过程为例来讲解虚拟内存机制运行的全过程
    操作系统知识点梳理_第10张图片

五、可执行文件装载过程

可执行文件装载的过程其实就是进程创建的过程,进程创建的过程,整个过程主要做三件事:
操作系统知识点梳理_第11张图片
用图片的方式来展现:
操作系统知识点梳理_第12张图片
注释

  • 步骤一其实就是创建页表,页表就是虚拟内存和物理内存的映射数据结构
  • 步骤二就是建立task_struct结构体(进程控制块PCB),其中vm_area_struct结构体按照Segment来记录了虚拟空间中的某一片区域示意图如下:
    操作系统知识点梳理_第13张图片
  • ELF文件中Segment和Section的区别,Segment其实是一个或者多个Section合成的。之前所说的数据段,代码段等都属于Section。具体可以看书164页

六、动态链接库

1、动态链接库的好处

动态链接库是针对静态链接库的缺点来做补充的,所以先讲一下静态链接库的缺点(其实也是可重定位文件的缺点):

  • 浪费磁盘空间,如果有多个可执行文件都调用了libex.a中的一个目标文件a.o,那么就会有a.o的多份拷贝在这多个可执行文件中,浪费了磁盘空间
  • 浪费内存,如果上述的多个可执行文件被同时执行,那么就会有有多个a.o被加载到虚拟内存中,甚至物理内存中,浪费内存空间
  • 每次更改程序,都要加入静态库重新编译。

所以动态库的好处如下:

  • 可执行文件中不会复制动态库中的内容(依靠装载时重定位技术)
  • 在内存中只会有动态库的一份拷贝(依靠的是位置无关代码)
  • 编译时,如果动态库编译和源文件编译是区分开来的,也就是如果动态库没有改变,就不需要再编译动态库。

但是动态库也有缺点,那就是运行速度没有静态库快,因为在加载阶段,动态库需要消耗额外的时间进行符号查找,重定位工作,一般比静态库程序运行性能减少1%~5%。

2、动态库链接的示意图

操作系统知识点梳理_第14张图片
我们可以看到在静态链接时,不需要动态库所有信息,只需要少部分信息即可。
注意:动态链接器也是一个动态库。

3、动态链接库中的关键技术
  • 地址无关代码,就是在装载时重定位,这时不一定能确定动态库的具体位置,所以对于可执行文件中动态链接库中的符号,只是重定位成动态库的相对位置,而不是绝对位置(个人理解
  • 延迟绑定,延迟绑定是为了加快动态库的加载速度,就是对于使用到的动态库中的函数,等到第一次使用再进行符号查找,重定位工作。
4、动态库的调用方法
  • 在Linux中
    • 静态库文件是.a文件,动态库文件是.so文件
    • 显式调用:调用dlopen(),dlsym(), dlerror(),dlclose()这四个函数
    • 隐式调用:输入指令时加入动态库的库路径,以及在程序中加入动态库的头文件
  • 在Windows中
    • 静态库文件是.lib文件,动态库文件是**.lib文件和.dll文件**,其中lib文件并不关键,是记录了动态库的一些相关信息,只有在隐式调用是才需要用到,而dll文件才是真正的动态库文件。
    • 显式调用:只需要调用dll文件即可,使用LoadLibrary(), GetProcAddress(), FreeLibrary()三个函数
    • 隐式调用:头文件、lib文件和dll缺一不可,lib文件可以通过#gragma comment(lib, “xxx”)语句在程序中添加,也可以通过配置VS的项目来添加,dll暂时知道的项目的库目录必须包含动态库才可以。

windows可以参考此篇博客:https://blog.csdn.net/liangyanghui/article/details/77981848

七、程序运行时的内存

1、虚拟内存布局
a、虚拟内存布局图

当一个可执行文件(暂时不考虑动态库)加载结束以后,虚拟内存的布局如下:
需要声明的是,这张图其实网上出现很多,但是这个虚拟内存布局究竟是如何得到的呢?
其实可以理解为这是将页表映射的内容完全罗列下来得到的。其中有的部分在内存中,比如与进程相关的数据结构,有的在磁盘上,比如代码、未初始化,已初始化数据,这些都在ELF文件中,还有一些根本不存在,比如标蓝色的区域,这些是分给堆栈,但并未被分配的区域,这些区域在实际中是不存在的。
操作系统知识点梳理_第15张图片

a、虚拟内存分区

虚拟内存的分区有多种分法,这里选用最常用的一种:
分为内核区,栈区,堆区,全局静态区,文字常量区,代码区和保留区

  • 内核区,存放内核代码数据以及进程相关数据结构
  • 栈区,一般存放函数体的局部变量、函数调用期间的所有参数压栈、函数的返回值
  • 堆区,用户申请的内存区域
  • 全局/静态区,存放全局变量、静态类型的变量
  • 文字常量区,存放常量和字符串
  • 代码区,存放程序代码
  • 保留区,是不可以使用的区域,因为极小的地址就被丢弃了。
    其中全局静态区,文字常量区以及代码区就是由可执行文件中相对应的段在程序加载进来以后就确定了,而栈区和堆区是在程序运行过程中不断变化调整的区域
    以一幅图来说明全局静态区,文字常量区以及代码区和可执行文件中段的对应关系:
    操作系统知识点梳理_第16张图片
2、栈
a、栈的特性

内存中的栈仍然具有先进后出的特性,栈总是按虚拟地址向下生长

b、函数栈帧

栈最重要的作用就是在程序运行过程中,保存正在执行函数所需要维护的信息,包括如下:

  • 函数返回地址和参数。
  • 临时变量,包括函数的非静态局部变量以及编译器自动生成的临时变量
  • 保存调用该函数的上下文,比如调用前的外部函数地址等等

函数栈帧区域是依靠ebp和esp两个寄存器来限定的。esp始终指向栈顶,也就是当前调用函数栈帧的顶部,ebp始终指向当前调用函数栈帧的底部。所以这两个寄存器中间的区域,就是当前执行函数的栈帧。
要想了解具体ebp和esp是怎样工作的,可以参考我的博客https://blog.csdn.net/qq_34489443/article/details/93158460

c、函数返回值的传递

如果是4字节以内,用exa寄存器来传递
如果是5~8字节,用exa和edx两个寄存器来传递
如果大于8字节,以一个例子来说明

以一个例子来解释
操作系统知识点梳理_第17张图片
操作系统知识点梳理_第18张图片

3、堆
a、堆的性质

内存中的堆在存储上没有太多局限,是按虚拟地址向上生长的。

b、linux中内存获取方法

我们想要在已经成型的虚拟内存中去使用空闲的内存,可以用malloc或者new去申请,但是这属于C++封装过的函数,最底层的函数应该是系统调用,不同的系统不一样,这里着重介绍Linux系统。
linux系统中有两种内存获取方式:brk()和mmap()
brk():这个函数属于动态分配内存,也是堆的分配方法,也就是在运行中分配内存,这个函数其实就是调整brk指针的位置,brk指针指向的是堆顶,增加堆顶相当于扩大堆。
mmap():将磁盘上的空间映射到虚拟内存中堆和栈的中间那部分。

c、对分配方法

内存空间的管理方法,有空闲链表法等,空闲链表法还分隐式显式等。就不展开细讲了。

八、系统调用

a、系统调用

系统调用就是操作系统提供的函数接口。
系统调用的缺点:
操作系统知识点梳理_第19张图片
针对这些缺点,出现了运行库,举例来说Linux中read函数是系统调用,用来读取文件,但是C语言运行库中是fread,fread函数在所有系统下都可以使用,而在Linux系统下,fread其实就是对read系统调用的封装,所以运行库有如下好处:
操作系统知识点梳理_第20张图片

b、系统调用的过程

系统调用不像普通的函数,直接运行就可以了,系统调用需要执行特殊的步骤。在《程序员自我修养》中说系统调用是通过中断来执行的,但是在CSAPP中是依靠陷阱来执行的。我查阅了很多资料,最后觉得其实是说法的不一致,陷阱还有一种说法是软中断,与此相对的还有硬中断。所以在《程序员自我修养》中,系统调用是软中断实现的。
比较系统中四种异常:
操作系统知识点梳理_第21张图片

  • 中断指的是硬中断,硬中断是依靠硬件产生的,所以对于CPU或者进程来说,总是被动的。
  • 陷阱指的就是软中断,软中断是依靠软件产生,是主动发生的,系统调用就是依靠陷阱产生。
  • 故障是由错误情况引起的,如果严重会变成终止。
  • 终止就是执行abort函数。

所以以下做一个统一:以下说的中断就是软中断,硬中断会单独指出。
系统调用的过程:
操作系统知识点梳理_第22张图片

c、上下文切换

上下文:内核重新启动一个闲置进程所需的信息,包括程序计数器,用户栈,内核栈和各种内核数据结构等。
上下文切换就是内核将当前进程保存起来,执行其他进程,注意用户模式切换成内核模式也需要上下文切换
上下文切换的时机:

  • 系统调用需要上下文切换,也就是软中断(陷阱)会产生上下文切换,用户态切换成内核态最后再切换回用户态
  • 进程阻塞或者sleep会先执行其他进程,产生上下文切换。
  • 硬中断也会产生上下文切换,比如所有系统都会产生周期性定时器中断机制,避免一个进程运行太久,会硬中断切换成其他进程执行,产生上下文切换。

上下文切换的步骤:

  • 保存当前进程的上下文
  • 恢复先前被抢占或者内核态的上下文
  • 将控制传递给新恢复的进程
    注意:Linux中用户态和内核态使用的是不同的栈,所以在切换用户态的时候,就需要切换栈,切换栈其实就是将原来使用的栈寄存器中的地址SS和BSP保存起来,置换成另外一个。(不同的进程上下文切换也是这样)

你可能感兴趣的:(CSAPP)