资讯详情

哈工大计算机系统大作业-Hello的程序人生

快速链接目录

  • 摘要
  • 第一章 概述
    • 1.1Hello简介
    • 1.2环境与工具
    • 1.3中间结果
    • 1.4本章小结
  • 第二章 预处理
    • 2.预处理的概念和功能
    • Ubuntu下面的预处理命令
    • 2.3Hello预处理结果分析
    • 2.4本章小结
  • 第三章 编译
    • 3.编译的概念和功能
    • 3.2Ubuntu下面编译的命令
    • 3.3Hello分析编译结果
      • 3.3.1工作伪指令
      • 3.3.2数据格式和寄存器结构
      • 3.3.3数据
      • 3.3.4数据传输指令
      • 3.3.5压入和弹出栈数据
      • 3.3.6算术操作
      • 3.3.7逻辑操作
      • 3.3.8条件控制
      • 3.3.9跳转语句
      • 3.3.10函数调用
    • 3.4 本章小结
  • 第四章 汇编
    • 4.汇编的概念和作用
    • 4.2在Ubuntu下面汇编的命令
    • 4.可重定位目标ELF格式
      • 4.3.1ELF头
      • 4.3.2Section头
      • 4.3.3符号表
      • 4.3.可重定位段信息
    • 4.4Hello.o的结果解析
    • 4.4本章小结
  • 第五章 链接
    • 5.链接的概念和功能
    • 5.2在Ubuntu下链接命令
    • 5.3可执行目标文件hello的ELF格式
      • 5.3.1ELF头
      • 5.3.2Section头
      • 5.3.3符号表
      • 5.3.可重定位段信息
    • 5.4hello虚拟地址空间
    • 5.5连接的重定位过程分析
    • 5.6Hello的执行流程
    • 5.7Hello动态链接分析
    • 5.8本章小结
  • 第六章 hello进程管理
    • 6.过程的概念和作用
    • 6.2简述壳Shell-bash作用及处理过程
    • 6.3Hello的fork创建过程的过程
    • 6.4Hello的execve过程
    • 6.5Hello的进程执行
      • 6.5.1逻辑控制流
      • 6.5.并与时间片发流
      • 6.5.3核心模式和用户模式
      • 6.5.4上下文切换
      • 6.5.5Hello的执行
    • 6.6hello异常和信号处理
      • 6.6.1异常
      • 6.2.2信号
    • 6.7本章小结
  • 第七章 hello的存储管理
    • 7.1hello存储地址空间
    • 7.2Intel逻辑地址到线性地址的变换-段式管理
    • 7.3Hello从线性地址到物理地址的转换-页面管理
    • 7.4TLB支持四级页面VA到PA的变换
      • 7.4.1TLB加快地址翻译
      • 7.4.24级页面支持缓存
    • 7.5三级Cache支持的物理内存访问
    • 7.6 hello进程fork时间内存映射
    • 7.7hello进程execve时间内存映射
    • 7.缺页故障和缺页中断处理
      • 7.8.1缺页故障
      • 7.8.2缺页故障处理
    • 7.9动态存储分配管理
      • 7.9.1堆
      • 7.9.2隐式空闲链表
      • 7.9.2显式空闲链表
    • 7.10本章小结
  • 第八章 hello的IO管理
    • 8.1Linux的IO设备管理方法
    • 8.2 简述Unix IO接口及其函数
      • 8.2.1函数open()和opennat()
      • 8.2.2creat()函数
      • 8.3.3lseek()函数
      • 8.3.4read()函数
      • 8.3.5write()函数
    • 8.3 printf的实现分析
    • 8.4 getchar的实现分析
    • 8.5本章小结
  • 结论
  • 附件

摘要

HelloWorld这是每个程序员梦想开始的地方,本文跟踪并采访了它Hello程序生活。Hello从最初的C语言源代码开始,它将首先通过人生的第一步预处理;然后它将继续改变,从一个绿色的.i文件变成更能让机器理解的文件.s随Hello的一步一步成长,他会经过汇编、链接等一系列的动作处理,变成一个可执行文件。这也标志着它即将进入机生的新阶段。 在下一阶段,它将与操作系统交谈,像伯乐一样,为他打开过程,提供虚拟内存和独立的地址空间;划分时间片和逻辑控制流,让操作系统游泳,最后停止这个短暂而辉煌的机器。 本文从一个hello.c开始跟随源代码hello详细说明脚步Hello成长生涯的每一步变化。

预处理,编译,汇编,链接,进程管理,虚拟地址,存储管理,IO操作

第一章 概述


本章描述的Hello在Hello死前,它脑海中的跑马灯


1.1Hello简介

HelloWorld当我们打开它时,它一定是每个程序员的启蒙怪物VSCode,输入一行代码并按下编译操作后,我们完成了第一次编写代码并向世界问好。这个看似简单的程序实际上是第一个实现的P2P。 全称为From Program to Progress,从程序到过程。这个看似简单的过程需要一系列复杂的动作,如预处理、编译、汇编和链接来生成可执行的目标文件。我们在运行时打开它Shell,等待我们输入指令。通过输入./hello,使Shell创建新的进程用来执行hello。 将使用操作系统fork通过execve加载,不断进行访存、内存申请等操作。最后,程序结束后,由父进程或祖先进程回收,程序结束。 图1 hello的编译运行过程

图1 hello编译操作过程

020:全称为Fro 0 to 0,从无到终。Hello的出生是由操作系统进行存储管理、地址翻译、内存访问,通过按需页面调度来开始这段生命。父进程或祖先进程的回收也标志着它生命的结束。 这两个部分便是Hello从无到有,从始到终的白描。

1.2环境与工具

\qquad 处理器:Intel CORE i7 10th GEN \qquad 系统类型:X64 CPU; 2GHz; 16G RAM; 256G HD Disk \qquad Windows10家庭和学生版 \qquad VMware Workstation pro2022 \qquad Ubuntu20.04 \qquad Visual Stdio 2019; ClodeBlocks; gedit+gcc;VSCode

1.3中间结果

编写这篇文章生成的中间结果文件以及文件内容如下表格所示

文件名称 作用
hello.c 储存hello程序源代码
hello.i 源代码经过预处理产生的文件(包含头文件等工作)
hello.s hello程序对应的汇编语言文件
hello.o 可重定位目标文件
hello_o.s hello.o的反汇编语言文件
hello.elf hello.o的ELF文件格式
hello 二进制可执行文件
hello.elf 可执行文件的ELF文件格式
hello.s 可执行文件的汇编语言文件

1.4本章小结

本章简述了Hello程序的一生,概括了从P2P到020的整个过程,可以发现整个计算机系统的学习和Hello的生命轨迹基本重合一致。本章还简要说明了实验的软、硬件环境以及编译处理工具,是整体文章的布局脉络。

第二章 预处理


本章是介绍的是Hello出生后走的第一个里程碑事件,前进一小步


2.1预处理的概念和作用

预处理顾名思义预先处理,是指在程序代码被翻译为目标代码的过程中,生成二进制文件之前的过程。这个过程一般包括包含头文件等工作。 预处理为编译做准备工作,主要进行代码的文本替换工作,它会根据预处理指令来修改源代码。在源代码中,以#开头的代码段即为预处理工作的对象。有以下几个功能:

  • 头文件包含:例如 #include <stdio.h>,即为包含标准输入输出头文件。
  • 条件编译指令:相当于一个选择装置,可以让程序员通过定义不同的宏(宏定义)来决定对哪些代码进行处理,而那些代码要被忽略。以下为一些条件编译指令简要介绍:
指令名称 功能
#if 如果判断条件为真,则编译下面的代码
#ifdef 判断是否宏定义,若是,则编译下面的代码
#ifndef 判断是否宏定义,妥否,则编译下面的代码
#elif else语句,若前置条件判断为假此条为真,则编译下面的代码
#endif 结束一段if…else的条件编译指令判断
  • 特殊符号处理:预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。

Ubuntu下的预处理命令

Linux系统中使用如下指令进行预处理工作

gcc hello.c -E -o hello.i        ##-E为预处理 -o为对命令产生文件命名

图1 .i文件生成

2.3Hello的预处理结果分析

.i文件可以作为一个文本文档被打开,我选择使用Vim编辑器查看这个生成文件

图2 .i文件内容

可以看到.i文件相比于.c源文件多了的内容,乍一看迷迷糊糊,但仔细阅读还是可以分辨出这些。对源文件中定义的宏进行了展开,将头文件中的内容包含到这个文件中。例如上图中getsubopt、getloadavg等函数的定义,以及一些结构体类型的声明。

2.4本章小结

这一章主要介绍了hello.c程序预处理方面的内容,包括预处理的概念和作用,以及进行了hello.c文件的预处理和结果展示。预处理作为编译运行的第一步是非常重要的一部分,查看.i文件会让我们更加直观的感受到预处理前后源文件的变化。

第三章 编译


本章可以说,因为接触了新的语言,这种语言对Hello的机生也是重要一步。


3.1编译的概念和作用

编译是利用编译程序从源语言编写的源程序产生目标程序的过程,也是用编译程序产生目标程序的动作。也就是说编译器会将通过预处理产生的.o文件翻译成一个后缀为.s的汇编语言文件,编译就是从高级程序设计语言到机器能理解的汇编语言的转换过程。 其实从概念中也可以直接提炼出,编译的功能就是产生汇编语言文件,并交给机器。除此之外,编译器还有一些其他功能,例如语法检查等。

3.2Ubuntu下编译的命令

Linux中使用如下指令进行编译

gcc hello.i -S -o hello.s        ##-S为编译,-o为命令产生的文件命名

图1 hello.s文件

3.3Hello的编译结果分分析

同样,使用Vim文本编辑器打开hello.s文件,乍一看是一堆看不懂的东西,实际这就是,接下来将对hello.s中出现的汇编指令进行介绍。

图2 hello.s文件内容

3.3.1工作伪指令

我们阅读hello.s文件,发现第一部分的汇编代码有一部分是以.作为开头的代码段。这些代码段是指导汇编器和连接器工作的伪指令。这段代码对我们来说没有什么意义,通常可以忽略这些代码,但对汇编器和连接器缺是十分重要的。这些指导伪代码的含义如下表

图3 伪代码段
伪指令 含义
.file 声明源文件(此处为hello.c)
.text 声明代码节
.section 文件代码段
.rodata Read-only只读文件
.align 数据指令地址对齐方式(此处为8对齐)
.string 声明字符串(此处声明了LC0和LC1)
.globl 声明全局变量
.type 声明变量类型(此处声明为函数类型)

3.3.2数据格式和寄存器结构

在解析下面的汇编代码之前,我们需要先了解数据存储的格式以及寄存器的存储结构,Intel数据类型令16bytes为字,21bytes为双字,各种数据类型的大小一级寄存器的结构如下所示:

变量类型 Intel数据类型 汇编代码后缀 大小(字节)
char 字节 b 1
short w 2
int 双字 l 4
long 四字 q 8
char * 四字 q 8
float 单精度 s 4
double 双精度 l 8

图4 寄存器结构

3.3.3数据

立即数在汇编代码中的呈现形式最为简单易辨认。即数顾名思义,是直接显式使用的一类数据,在汇编代码中通常以$美元符号作为标识。例如下图中的例子,表示的含义是比较(cmp compare)寄存器中的值和4,根据结果设置条件码。

在汇编代码中,指令后面出现过许许多多的形如-32(%rbp)形式的代码声明,其实这些就是寄存器存储的变量,通过特定的寻址方式进行引用。例如下图中的例子,表示的就是将寄存器%edi中存储的值,加载到以现在栈指针%rbp指向的位置基础上,减去32所对应的地址中去。类似的加载使用的例子在这里面不胜枚举,就不一一赘述了。

.LC0和.LC1作为两个字符串变量被声明。而在.LC0中出现的\347\224等是因为中文字符没有对应的ASCII码值无法直接显式显示,所以这样的字符方式显示。而且这两个字符串都在.rodata下面,所以是只读数据。随后有两句leaq指令,这个指令为加载有效地址,相当于转移操作。

图5 字符串数据

3.3.4数据传送指令

数据传送指令无疑是整个程序运行过程中使用的最频繁的指令。汇编代码中数据移动使用MOV类,这些指令把数据从源位置复制到目的位置,不做任何变化。MOV类中最常用的有四条指令:movb、movw、movl、movq,这些指令执行相同的操作,区别在于他们所移动的数据大小不同,如下表所示。指令的最后一个限制字符必须和寄存器所对应的数据大小保持一致。

指令 效果 描述
MOV \qquad S,D D←S 传送
movb 传送字节
movw 传送字
movl 传送双字
movq 传送四字

例如下图中第一句汇编代码,表示意思为将寄存器%rax中存储的值传送到寄存器%rdx中。

图6 数据传送指令

除此之外,还有一些指令会先将数据进行零扩展或者符号扩展之后再进行传送。典型实例就是MOVZ(零扩展)和MOVS(符号扩展),因为比价少见并且在hello.s中么有相应的体现,就不展开说明了。

3.3.5压入和弹出栈数据

压入数据使用指令pushq,弹出数据使用指令popq,他理解起来其实可以看做一个由两句指令组成的结合体。我们拿popq指令作为例子来说明。

popq %rax 等价于 addq $8 %rsp + movq %rbp, (%rsp)

指令 效果 描述
pushq S R[%rsp]←R[%rsp] - 8;M[R[%rsp]]←S 将四字压入栈
popq D D←M[R[%rsp]];R[%rsp]←R[%rsp] + 8 将四字弹出栈

3.3.6算术操作

算术运算也是十分常用的一些指令类,同样的,每种算术运算指令的末尾也有b、w、l、q(例如addb)来限制数据的大小。

指令 效果 描述
INC DDEC DNEG DNOT D D←D + 1 D←D - 1D← -DD← ~D 加1减1取负取补
ADD S, DSUB S, DIMUL S, D D←D + SD←D - SD←D * S 加减乘
addq				$16, %rax        ##加法操作

3.3.7逻辑操作

逻辑操作常见的有两类,一类是加载有效地址,一类是位移操作。加载有效地址指令类似于MOV类指令,它的作用是将有效地址写入到目的操作数,相当于C语言中大家所熟知的取址操作,可以为以后的内存引用产生指针。位移操作顾名思义就是将二进制数进行整体左移或者右移。

leaq				.LC1(%rip), %rdi        ##加载有效地址

3.3.8条件控制

汇编语言中,一些指令会改变条件码,例如cmpl指令和setl指令。这种指令一般不会单独使用,会根据比较结果进行后续操作。例如下图,将寄存器中存储的值和立即数4进行比较,设置条件码,然后进行跳转或者其他操作。

cmpl				$4, -20(%rbp)        ##比较,设置条件码

3.3.9跳转语句

跳转指令会根据条件码当前的值来进行相应的跳转。比较常见的是直接跳转,在hello.s中也有体现,如下图所示。cmpl指令判断寄存器中的值和立即数4的大小关系,设置条件码,再进行je。je的含义是jump if equal,也就是说,如果此时的条件码所表示含义为相等,则会跳转到相应的.L2指令行。因此,跳转指令用来实现条件分支。

3.3.10函数调用

call指令用来进行函数的调用。如下图所示的示例,call调用了getchar函数。它会先将函数的返回地址压入运行时栈中,之后跳转到相应的函数代码段进行执行。执行结束通过ret指令返回。

3.4 本章小结

本章对汇编指令做了以下简单的介绍,以及查看了Hello的机器级实现。经过简单的思考,我们便可以发现这些汇编指令和C语言代码语句之间的对应关系。同时,根据一个程序的汇编代码我们也可以翻译出相应的C语言程序的大致样貌。

第四章 汇编


本章介绍了Hello是怎么让大家更能理解它,以及如何找到小伙伴的一些事


4.1汇编的概念与作用

汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。也就是说,汇编器会把输入的汇编指令文件重新打包成可重定位目标文件,并将结果保存成.o文件。它是一个二进制文件,包含程序的指令编码。 它的作用也很明晰了,就是完成从汇编语言文件到可重定位目标文件的转化过程。

4.2在Ubuntu下汇编的命令

Linux系统中使用如下命令进行汇编

gcc hello.s -c -o hello.o        ##-c为汇编,-o为命令产生的文件命名

图1 hello.o文件产生

4.3可重定位目标ELF格式

4.3.1ELF头

.o文件为目标文件,相当于Windows中的.obj后缀文件,因此直接使用Vim或者其他文本编辑器查看会出现一大堆乱码。那么我们选择查看可重定位目标文件的elf形式,使用命令readelf -h hello.o查看ELF头,结果如下

图2 ELF头信息

ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了生成文件的系统的字的大小和字节顺序。ELF头剩下部分的信息包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型和机器类型等。例如上图中,Data表示了系统采用小端法,文件类型Type为REL(可重定位文件),节头数量Number of section headers为13个等信息。

4.3.2Section头

使用命令readelf -S hello.o查看节头,结果如下

图3 Section头信息

夹在ELF头和节头部表之间的都为节,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。各部分含义如下:

名称 包含内容含义
.text 已编译程序的机器代码
.rodata 只读数据
.data 已初始化的全局和静态C变量
.bss 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量
.symtab 一个符号表,存放一些在程序中定义和引用的函数和全局变量的信息
.rel.text 一个.tex节中位置的列表
.rel.data 被模块引用或定义的所有全局变量的重定位信息
.debug 一个调试符号表
.line 原始C源程序中的行号和.text节中机器指令之间的映射
.strtab 一个字符串表(包括.symtab和.debug节中的符号表)

4.3.3符号表

使用命令readelf -s hello.o查看符号表,结果如下

图4 符号表信息

这其中,Num为某个符号的编号,Name是符号的名称。Size表示他是一个位于.text节中偏移量为0处的146字节函数。Bind表示这个符号是本地的还是全局的,由上图可知main函数名称这个符号变量是本地的。

4.3.4可重定位段信息

使用readelf -r hello.o查看可重定位段信息,结果如下

图5 可重定位段信息

offset是需要被修改的引用的节偏移,Sym.标识被修改引用应该指向的符号。Type告知连接器如何修改新的引用,Addend是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。

4.4Hello.o的结果解析

使用objdumo -d -r hello.o命令对hello.o可重定位文件进行反汇编,得到的反汇编结果如下图

图6 hello.o的反汇编结果

看到hello.o的反汇编文件我们很是熟悉,它使用的汇编代码和hello.s汇编文件的汇编代码是一样的,但是在这反汇编文件的字里行间中,也混杂着一些我们相对陌生的面孔,也就是。 这些机器代码是二进制机器指令的集合,每一条机器代码都对应一条机器指令,到这儿才是机器真正能识别的语言。机器代码与汇编代码不同的地方在于:

  1. 分支跳转方面:汇编语言中的分支跳转语句使用的是标识符(例如je .L2)来决定跳转到哪里,而机器语言中经过翻译则直接使用对应的地址来实现跳转。

  2. 函数调用方面:在汇编语言.s文件中,函数调用直接写上函数名。而在.o反汇编文件中,call目标地址是当前指令的下一条指令地址。这是因为hello.c中调用的函数都是共享库中的函数,需要等待链接之后才能确定响应函数的地址。因此,机器语言中,对于这种不确定地址的调用,会先将下一条指令的相对地址设置为0,然后再.rela.text节中为其添加重定位条目,等待链接时确定地址。

4.4本章小结

本章介绍了Hello从hello.s到hello.o的过程。这一节中对hello.o的ELF头,Section头以及符号表进行了分析,可以看到Hello的跟深处的信息。本节还对hello.o的反汇编文件进行了解析,比较了相对于hello.s文件.o文件是怎么让机器更加理解的。

第五章 链接


本站介绍Hello先生是如何结实与他志同道合的狐朋狗友的


5.1链接的概念与作用

链接是将各种代码和数据片段和搜集并组成成为一个但以文件的过程,这个文件可被夹在到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被记载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。 将程序调用的各种静态链接库和动态连接库整合到一起,完善重定位目录,使之成为一个可运行的程序。

5.2在Ubuntu下链接的命令

Linux系统hello.c使用如下指令进行链接

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

图1 hello文件产生

5.3可执行目标文件hello的ELF格式

同上一节一样,我们分别查看hello文件的ELF头,节头部表,符号表。

5.3.1ELF头

使用命令readeld -h hello查看hello的ELF头

图2 ELF头

可以看到文件的Type发生了变化,从REL变成了EXEC(Executable file可执行文件),节头部数量也发生了变化,变为了27个。

5.3.2Section头

使用命令readeld -S hello查看节头部表信息

图3 节头部表信息

节头部表对hello中所有信息进行了声明,包括了大小(Size)、偏移量(Offset)、起始地址(Address)以及数据对齐方式(Align)等信息。根据始地址和大小,我们就可以计算节头部表中的每个节所在的区域。

5.3.3符号表

使用命令readelf -s hello查看符号表信息

图4 符号表信息

可以发现经过链接之后符号表的符号,说明经过连接之后引入了许多,一并加入到了符号表中。

5.3.4可重定位段信息

使用命令readeld -r hello查看可重定位段信息

图5 可重定位段信息

5.4hello虚拟地址空间

在edb中打开可执行文件hello,可以看到hello虚拟地址空间的起始地址为0x401000,结束地址为0x401ff0。

图6 hello起止虚拟地址

根据5.3.2节里面的Section头部表,我们可以找到对应的节的其实空间对应位置,例如.init初始化节,起始位置地址为0x401000在edb中有其对应位置

图7 .init和其对应

5.5连接的重定位过程分析

使用命令objdump -d -r hello查看hello可执行文件的反汇编条目

图8 hello反汇编(截取)

可以观察到hello的反汇编代码与hello.s的返沪编代码在结构和语法上是完全相同的,只不过hello的反汇编代码多了非常多的内容,我们通过比较不同来看一下区别:

  1. hello反汇编代码中函数调用时不再仅仅储存call当前指令的下一条指令,而是已经完成了重定位,调用的相应函数已经有对应的明确的虚拟地址空间
  2. hello反汇编代码中肉眼可见的多了很多节,这些陌生的节都是经过链接之后加入进来的。例如.init节就是程序初始化需要执行的代码所在的节,.dynamic节是存放被ld.so调用过的 动态链接信息的节等等。

重定位的过程分为

  1. 在这一步中,连接器将所有相同类型的节合并成为同一类型的新的聚合节。例如,来自所有输入模块的.data节全部被合并成一个节,这个节成为输出的可执行目标文件的.data节。 2.** 重定位节中的符号引用:**在这一步中,连接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。要执行这一步,连接器依赖于可重定位条目,及5.3节中分析的那些数据。

5.6Hello的执行流程

程序名称 程序地址
_start 0x4010f0
_libc_start_main 0x7ffff7de2f90
__GI___cxa_atexit 0x7ffff7e05de0
__new_exitfn 0x7ffff7e05b80
__libc_csu_init 0x4011c0
_init 0x401000
_sigsetjump 0x7ffff7e01bb0
main 0x401125
do_lookup_x 0x7ffff7fda4c9
dl_runtime_resolve_xsavec 0x7ffff7fe7bc0
_dl_fixup 0x7ffff7fe00c0
_dl_lookup_symbol_x 0x7ffff7fdb0d0
check_match 0x7ffff7fda318
strcmp 0x7ffff7fee600

5.7Hello的动态链接分析

动态共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,兵和一个程序链接起来,这个过程就是动态链接。 把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。 PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。 GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。 hello在动态连接器加载前后的重定位是不一样的,在加载之后才进行重定位

图9 重定位后.init

5.8本章小结

本章介绍了连接的过程。解释了程序是如何进行重定位的操作,把相同类型的数据放在同一个节的过程,同时也说明了链接的工作原理。

第六章 hello进程管理


本章讲述的是Hello已经可以独当一面,独自在CPU中游走了


6.1进程的概念与作用

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在运行一个进程时,我们的这个程序好像是系统当中唯一一个运行的程序,进程的作用就是提供给程序两个关键的抽象。一分别是独立的逻辑控制流和私有的地址空间。

6.2简述壳Shell-bash的作用与处理流程

Shell是命令语言解释器。他的本质是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核。它在操作系统的最外层,负责直接与用户进行对话,把用户的输入解释给操作系统,并处理各种各样的操作系统的输出结果,输出到屏幕反馈给用户。例如我们经常使用的Windows下的cmd命令行和Bash以及Linux中的Shell。 当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

6.3Hello的fork进程创建过程

在Linux系统中,用户可以通过 ./ 指令来执行一个可执行目标文件。在程序运行时,Shell就会创建一个新的进程,并且新创建的进程更新上下文,在这个新建进程的上下文中便可以运行这个可执行目标文件。fork()函数拥有一个int型的返回值。子进程中fork返回0,在父进程中fork返回子进程的Pid。新创建的进程与父进程几乎相同但有细微的差别。子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈),并且紫禁城拥有与父进程不同的Pid。

图1 进程的地址空间

6.4Hello的execve过程

当Hellol的进程被创建之后,他会调用execve函数加载并调用程序。exevce函数在被调用时会在当前进程的上下文中加载并运行一个新程序。它被调用一次从不返回,执行过程如下:

  1. 删除已存在的用户区域
  2. 映射私有区:为 hello 的代码、数据、.bss 和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的
  3. 映射共享区:比如 hello 程序与共享库 libc.so 链接
  4. 设置 PC:exceve() 做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点
  5. execve() 在调用成功的情况下不会返回,只有当出现错误时,例如找不到需要执行的程序时,execve() 才会返回到调用程序

6.5Hello的进程执行

Hello在执行过程中涉及到几个十分重要的概念,如果不提前阐述就无法很好地理解Hello在运行中的状态。

6.5.1逻辑控制流

逻辑控制流是进程给运行程序的第一个假象,它让程序看起来独占整个CPU在运行,但实际上的情况当然不会是这样的,如下图。这三个进程的运行时间不是连续的,也就是说每个进程会交替的轮流使用处理器来进行处理。每个进程执行它的流的一部分,之后可能就会被抢占,如果被抢占了的话就会被挂起进行其他流的处理。

图2 逻辑控制流

6.5.2并发流与时间片

两个流如果在执行的时间上有所重叠,那么我们就说这两个流是并发流,每个流执行一部分的时间就叫做时间分片。例如上图中,我们可以说进程A和进程B并发,进程A也和进程C并发,但是进程B和进程C就不是并发的。

6.5.3内核模式和用户模式

在用户模式中,进程不允许执行特权指令,例如发起一个I/O操作等,更重要的是不允许直接引用地址空间中内核区内的代码和数据。如果在用户模式下进行这样的非法命令执行,会引发致命的保护故障。 在内核模式下,进程的指令执行相对没有限制,这有点类似于在Linux操作系统中,是否使用sudo(SuperUser do)作为指令的前缀一样。而在内核模式下运行的进程相当于获得了超级管理员的许可。

6.5.4上下文切换

进程在运行时会依赖一些信息和数据,包括通用目的寄存器、浮点寄存器等的状态,这些进程运行时的依赖信息成为进程的上下文。而在进程进行的某些时刻,操作系统内核可以决定抢占当前的进程,并重新开始一个新的或者之前被抢占过的进程,这一过程成为调度。而抢占进程前后由于进程发生改变依赖信息也变得不同,这个过程就是上下文切换。

6.5.5Hello的执行

从Shell执行hello程序时,会先处于用户模式运行。在运行过程中,由内核不断进行上下文切换,配合调度器,与其他进程交替运行。如果在运行过程中收到了信号,那么就会陷入到内核中进入内核模式运行信号处理程序,之后再进行返回。

6.6hello的异常与信号处理

6.6.1异常

异常可以分为四类:中断、陷阱、故障和终止,各类异常产生原因和一些行为总结成下表:

类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

在进程运行的过程中,我们施加一些I/O输入,比如说敲键盘,就会触发中断。系统会陷入内核,调用中断处理程序,然后返回。

图3 中断

陷阱和系统调用是一码事,用户模式无法进行的内核程序,便通过引发一个陷阱,陷入内核模式下再执行相应的系统调用。

图4 陷阱

常见的故障就是缺页故障。在hello中如果我们使用的虚拟地址相对应的虚拟页面不在内存中,就会发生此类缺页故障。故障是可能会被修复的,例如缺页故障触发的故障处理程序,会按需调动页面,再返回到原指令位置重新执行。但对于无法恢复的故障则直接报错退出。

图5 故障

如果遇到一个硬件错误,那对于幼小的hello来说是相当致命的,导致结果就是触发致命错误,终止hello的运行。

图6 终止

6.2.2信号

信号可以被理解为一条小消息,他通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件,它提供了一种机制,通知用户进程发生了这些异常。Linux中信号有如下种类:

图7 Linux信号

当hello在运行时我们在键盘上按下Ctrl-z,这个操作就会向进程发送SIGSTP信号,进入信号处理程序。可以从后台工作信息看到hello被暂时挂起,进程号为3。如果想继续这个进程可以使用命令fg %3

输入Ctrl-z进程就会被终止。

6.7本章小结

本章介绍了Hello进程如何运行,以及进程相关一些知识和概念。更加清晰地了解了进程对于程序运行所提供的重要假象:逻辑控制流和私有地址空间。

第七章 hello的存储管理


在这一章中,Hello怎么和它的伯乐——操作系统进行交互交流


7.1hello的存储器地址空间

在CPU中当然不是仅仅只有hello一个进程,而是许多进程共享CPU和主存资源。那么为了使内存管理更加高效,操作系统提出了一种十分重要的抽象,即虚拟内存。它有几处有点:下面介绍几个概念 逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元、存储单元、网络主机的地址。逻辑地址往往不同于物理地址,通过地址翻译器或映射函数可以把逻辑地址转化为物理地址。 线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。 CPU启动保护模式后,程序运行在虚拟地址空间中。保护模式下,hello 运行在虚拟地址空间中,它访问存储器所用的逻辑地址。 它是在地址总线上,以电子形式存在的,使得数据总线可以访问 主存的某个特定存储单元的内存地址。

7.2Intel逻辑地址到线性地址的变换-段式管理

Intel平台下,逻辑地址的格式为段标识符:段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。分段机制将逻辑地址转化为线性地址的步骤如下:

  1. 使用段选择符中的偏移值在GDT或LDT表中定位相应的段描述符。
  2. 利用段选择符检验段的访问权限和范围,以确保该段可访问。
  3. 把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。
    图1 段式管理

7.3Hello的线性地址到物理地址的变换-页式管理

概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组。如下图,虚拟内存被分为一些固定大小的块,这些块称为虚拟页块。这些页块根据不同的映射状态也被划分为三种状态:未分配、为缓存、已缓存。 虚拟内存中未分配的页 已经分配但是还没有被缓存到物理内存中的页 分配后缓存到物理页块中的页

图2 页式管理

7.4TLB与四级页表支持下的VA到PA的变换

页表是 PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地址字段,有效位表明该虚拟页是否被缓存在 DRAM 中。虚拟地址分为两个部分,虚拟页号(VPN)和虚拟页面偏移量(VPO)。其中VPN需要在PTE中查询对应,而VPO则直接对应物理地址偏移(PPO)。

图3 PTE寻页

7.4.1TLB加速地址翻译

既然要经常访问页表条目,不如直接将页表条目缓存到高速缓存中,这就是TLB的基本思想。TLB译为翻译后备缓冲器,也就是页表的缓存。TLB是一个具有较高相连度的缓存,如下图。根据VPN中的TLB索引找到缓存中相应的组,根据标记(tag)找到相应的缓存行,根据设置的有效位找到对应的位置。

图4 TLB加速地址翻译

7.4.2四级页表支持下缓存

下图为Core i7使用的四级页表地址翻译

图5 Core i7四级页表

同一级页表一样,若缓存页命中,则返回PPN,以VPO作为页便宜的到地址;若未命中,则经过四级页表查询,直到找到最终的PTE,查询,返回PPE。下图为4级页表目录格式:

图6 四级页表目录格式

7.5三级Cache支持下的物理内存访问

在寻找一个虚拟地址时,CPU会优先到TLB中寻找,查看VPN是否已经缓存。如果页命中的话,就直接获取PPN;如果没有命中的话就需要查询多级页表,得到物理地址PA,之后再对PA进行分解,将其分解为标记(CT)、组索引(CI)、块便宜(CO),之后再检测物理地址是否在下一级缓存中命中。若命中,则将PA对应的数据内容取出返回给CPU;若不命中,则重复上述操作,直到找到。过程图示如下:

图7 TLBcache地址翻译

7.6 hello进程fork时的内存映射

当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。通过 fork 创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。

7.7hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

  1. 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
  2. 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。
  3. 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

7.8缺页故障与缺页中断处理

7.8.1缺页故障

首先明确一下页命中的概念:虚拟内存中的一个字存在于物理内存中,即缓存命中。那不难理解,缺页故障就是虚拟内存中的字不在物理内存中,发生页不命中。例如下图中,对VP3的访问即发生缺页故障

图8 缺页故障

7.8.2缺页故障处理

如果发生了缺页故障,则触发缺页故障处理程序,这个程序会选择一个牺牲页,例如下图中的P4,将其在物理内存中删除,加入所需要访问的VP3。随后返回重新执行原指令,则页命中。这种策略称为

图9 缺页故障处理

7.9动态存储分配管理

首先明确动态存储分配管理的概念:在程序运行时程序员使用动态内存分配器,例如malloc获得虚拟内存。分配器将堆视为一组不同大小的 块(blocks)的集合来维护每个块要么是已分配的,要么是空闲的。

7.9.1堆

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。

图10 堆

在内存中的碎片和垃圾被回收之后,内存中就会有空余的空间被闲置出来。这些空间有时会比较小,但是积少成多,操作系统不知道怎么利用这些空间,就会造成很多的浪费。为了记录这些空闲块,采用隐式空闲链表和显式空闲链表的方法实现这一操作。

7.9.2隐式空闲链表

首先了解几个概念: 从头开始搜索空闲链表,选择第一个合适的空闲块: 搜索时间与总块数(包括已分配和空闲块)成线性关系。在靠近链表起始处留下小空闲块的“碎片”。

标签: 贴片电容cl05b102ko

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司