CSAPP 程序人生——Hello‘s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 未来技术学院

学   号 2022110785

班   级 22wl022

学 生 闫靖宸    

指 导 教 师 刘宏伟   

计算机科学与技术学院

2024年5月

摘 要

HelloWorld是每一个程序员梦开始的地方,而这篇文章就跟踪采访了Hello的程序人生。Hello从最开始的C语言源代码,会先经过他人生的第一步——预处理;接着会继续变化,从一个青涩的.i文件变化成更能让机器理解的.s汇编文件;随着Hello的一步一步成长,他会经过汇编、链接等一系列的动作处理,变成一个可执行文件。这也标志着它即将迈入机生的一个新阶段。

在下一个阶段中,它会和操作系统进行交谈,操作系统像它的伯乐一样,给他开辟进程,提供虚拟内存和独立的地址空间;给它划分时间片、逻辑控制流来让它操作系统上畅游,最后随着进程的结束,停止这短暂而辉煌的机生。

本文从一个hello.c源代码开始,跟随hello的脚步,详细说明Hello成长生涯的每一步变化。

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

目 录

第1章 概述- 4 -

1.1 Hello简介- 4 -

1.2 环境与工具- 4 -

1.3 中间结果- 4 -

1.4 本章小结- 4 -

第2章 预处理- 5 -

2.1 预处理的概念与作用- 5 -

2.2在Ubuntu下预处理的命令- 5 -

2.3 Hello的预处理结果解析- 5 -

2.4 本章小结- 5 -

第3章 编译- 6 -

3.1 编译的概念与作用- 6 -

3.2 在Ubuntu下编译的命令- 6 -

3.3 Hello的编译结果解析- 6 -

3.4 本章小结- 6 -

第4章 汇编- 7 -

4.1 汇编的概念与作用- 7 -

4.2 在Ubuntu下汇编的命令- 7 -

4.3 可重定位目标elf格式- 7 -

4.4 Hello.o的结果解析- 7 -

4.5 本章小结- 7 -

第5章 链接- 8 -

5.1 链接的概念与作用- 8 -

5.2 在Ubuntu下链接的命令- 8 -

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

5.4 hello的虚拟地址空间- 8 -

5.5 链接的重定位过程分析- 8 -

5.6 hello的执行流程- 8 -

5.7 Hello的动态链接分析- 8 -

5.8 本章小结- 9 -

第6章 hello进程管理- 10 -

6.1 进程的概念与作用- 10 -

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

6.3 Hello的fork进程创建过程- 10 -

6.4 Hello的execve过程- 10 -

6.5 Hello的进程执行- 10 -

6.6 hello的异常与信号处理- 10 -

6.7本章小结- 10 -

第7章 hello的存储管理- 11 -

7.1 hello的存储器地址空间- 11 -

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

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

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

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

7.6 hello进程fork时的内存映射- 11 -

7.7 hello进程execve时的内存映射- 11 -

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

7.9动态存储分配管理- 11 -

7.10本章小结- 12 -

第8章 hello的IO管理- 13 -

8.1 Linux的IO设备管理方法- 13 -

8.2 简述Unix IO接口及其函数- 13 -

8.3 printf的实现分析- 13 -

8.4 getchar的实现分析- 13 -

8.5本章小结- 13 -

结论- 14 -

附件- 15 -

参考文献- 16 -

第1章 概述

1.1 Hello简介

HelloWorld想必是每一个程序员的启蒙小怪,当我们打开VSCode,输入一行行代码,按下编译运行后,我们便完成了第一次代码编写,向世界问好。这个看似非常简单的程序实则是早先第一个实现的P2P。

P2P: From Program to Progress,从项程序到进程。这个看似简单的过程需要经过预处理、编译、汇编、链接等一系列的复杂动作才可以生成一个可执行目标文件。在运行时,我们打开Shell,等待我们输入指令。通过输入./hello,使Shell创建新的进程用来执行hello。 操作系统会使用fork产生子进程,然后通过execve将其加载,不断进行访存、内存申请等操作。最后,在程序结束返回后,由父进程或祖先进程进行回收,程序结束。

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

这两个部分便是Hello从无到有,从始到终的白描。

1.2 环境与工具

硬件环境:

处理器 AMD Ryzen 7 6800H with Radeon Graphics 3.20 GHz

机带 RAM 16.0 GB

系统类型 64 位操作系统, 基于 x64 的处理器

GPU:NVIDIA GeForce RTX 3060 Laptop GPU

软件环境:

Windows 11

Vmware17上的Ubuntu(64位)虚拟机

开发工具:

vim文本编辑器

Visual Studio

EDB

gcc编译器

调试工具:

GDB

1.3 中间结果

hello.c储存hello程序源代码

hello.i源代码经过预处理产生的文件(包含头文件等工作)

hello.shello程序对应的汇编语言文件

hello.o可重定位目标文件

hello_o.shello.o的反汇编语言文件

hello.elfhello.o的ELF文件格式

hello二进制可执行文件

hello.elf可执行文件的ELF文件格式

hello.s可执行文件的汇编语言文件

1.4 本章小结

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

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理顾名思义预先处理,是指在程序代码被翻译为目标代码的过程中,生成二进制文件之前的过程。这个过程一般包括包含头文件等工作。

预处理的作用:

预处理为编译做准备工作,主要进行代码的文本替换工作,它会根据预处理指令来修改源代码。在源代码中,以#开头的代码段即为预处理工作的对象。有以下几个功能:

头文件包含:

例如 #include ,即为包含标准输入输出头文件。

条件编译指令:

相当于一个选择装置,可以让程序员通过定义不同的宏(宏定义)来决定对哪些代码进行处理,而那些代码要被忽略。以下为一些条件编译指令简要介绍:

#if如果判断条件为真,则编译下面的代码

#ifdef判断是否宏定义,若是,则编译下面的代码

#ifndef判断是否宏定义,妥否,则编译下面的代码

#elifelse语句,若前置条件判断为假此条为真,则编译下面的代码

#endif结束一段if…else的条件编译指令判断

特殊符号处理:预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。

2.2在Ubuntu下预处理的命令

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

gcc hello.c -E -o hello.i

2.3 Hello的预处理结果解析

.i文件可以作为一个文本文档被打开

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

2.4 本章小结

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

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译是利用编译程序从源语言编写的源程序产生目标程序的过程,也是用编译程序产生目标程序的动作。也就是说编译器会将通过预处理产生的.o文件翻译成一个后缀为.s的汇编语言文件,编译就是从高级程序设计语言到机器能理解的汇编语言的转换过程。

编译的功能:

其实从概念中也可以直接提炼出,编译的功能就是产生汇编语言文件,并交给机器。除此之外,编译器还有一些其他功能,例如语法检查等

3.2 在Ubuntu下编译的命令

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

gcc hello.i -S -o hello.s

3.3 Hello的编译结果解析

打开hello.s文件,乍一看是一堆看不懂的东西,实际这就是汇编代码,接下来将对hello.s中出现的汇编指令进行介绍。

3.3.1工作伪指令

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

.file声明源文件(此处为hello.c)

.text声明代码节

.section文件代码段

.rodataRead-only只读文件

.align数据指令地址对齐方式(此处为8对齐)

.string声明字符串(此处声明了LC0和LC1)

.globl声明全局变量

.type声明变量类型(此处声明为函数类型)

3.3.2数据格式和寄存器结构

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

char字节b1

short字w2

int双字l4

long四字q8

char *四字q8

float单精度s4

double双精度l8

3.3.3数据

立即数:

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

寄存器变量:

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

字符串:

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

3.3.4数据传送指令

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

MOV\qquadS,DD←S传送

movb传送字节

movw传送字

movl传送双字

movq传送四字

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

3.3.5压入和弹出栈数据

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

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

pushq SR[%rsp]←R[%rsp] - 8;

M[R[%rsp]]←S将四字压入栈

popq DD←M[R[%rsp]];

R[%rsp]←R[%rsp] + 8将四字弹出栈

3.3.6算术操作

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

INC D D←D - 1 减1

DEC D D← -D 取负

NEG D D← ~D 取补

NOT D D←D + 1 加1

ADD S, D D←D + S 加

SUB S, D D←D - S减

IMUL S, D D←D * S乘

$16, %rax

3.3.7逻辑操作

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

.LC1(%rip), %rdi

3.3.8条件控制

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

$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语言程序的大致样貌。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

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

汇编的作用: 它的作用也很明晰了,就是完成从汇编语言文件到可重定位目标文件的转化过程。

4.2 在Ubuntu下汇编的命令

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

gcc hello.s -c -o hello.o

4.3 可重定位目标elf格式

4.3.1ELF头

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

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

4.3.2SEACTION头

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

夹在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查看符号表,结果如下

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

4.3.4可重定位段信息

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

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

4.4 Hello.o的结果解析

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

看到hello.o的反汇编文件我们很是熟悉,它使用的汇编代码和hello.s汇编文件的汇编代码是一样的,但是在这反汇编文件的字里行间中,也混杂着一些我们相对陌生的面孔,也就是机器代码。

这些机器代码是二进制机器指令的集合,每一条机器代码都对应一条机器指令,到这儿才是机器真正能识别的语言。每一条汇编语言都可以用机器二进制数据来表示,汇编语言中的操作码和操作数以一种相当于映射的方式和机器语言进行对应,从而让机器能够真正理解代码的含义并且执行相应的功能。机器代码与汇编代码不同的地方在于:

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

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

4.5 本章小结

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

第5章 链接

5.1 链接的概念与作用

连接的概念:

链接是将各种代码和数据片段和搜集并组成成为一个但以文件的过程,这个文件可被夹在到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被记载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

连接的作用:

将程序调用的各种静态链接库和动态连接库整合到一起,完善重定位目录,使之成为一个可运行的程序。

5.2 在Ubuntu下链接的命令

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

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

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

5.3.2Section头

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

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

5.3.3符号表

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

可以发现经过链接之后符号表的符号数量陡增,说明经过连接之后引入了许多其他库函数的符号,一并加入到了符号表中。

5.4 hello的虚拟地址空间

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

在edb中打开可执行文件hello

5.5 链接的重定位过程分析

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

可以观察到hello的反汇编代码与hello.s的反汇编代码在结构和语法上是基本完全相同的,只不过hello的反汇编代码多了非常多的内容。

链接过程:链接器根据hello.o文件中的重定位条目中不同的重定位元素类型,从而得到不同的偏移量。通过重定位条目的地址和节偏移计算出重定位后的地址。

有两种基本的重定位类型:重定位一个使用32位PC相对地址的引用和重定位一个使用32位绝对地址的引用。得到地址后根据hello.o文件对地址进行修改,即可完成重定位。之后链接hello所需的其他库函数。

5.6 hello的执行流程

hello程序先调用_init进行初始化,然后执行_start,再到main,之后依次执行_printf、_exit、_atoi、_sleep、_getchar,最后退出。

0000000000401000 <_init>

00000000004010f0 <_start>

0000000000401125

0000000000401090

00000000004010a0

00000000004010d0

00000000004010c0

00000000004010e0

00000000004010b0

5.7 Hello的动态链接分析

动态共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,兵和一个程序链接起来,这个过程就是动态链接。

把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

.plt:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

.got:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

5.8 本章小结

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

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

进程的作用:

在运行一个进程时,我们的这个程序好像是系统当中唯一一个运行的程序,进程的作用就是提供给程序两个关键的抽象。一分别是独立的逻辑控制流和私有的地址空间。

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

首先首先我们需要了解一下这个Shell是什么:

Shell是命令语言解释器。他的本质是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核。它在操作系统的最外层,负责直接与用户进行对话,把用户的输入解释给操作系统,并处理各种各样的操作系统的输出结果,输出到屏幕反馈给用户。例如我们经常使用的Windows下的cmd命令行和Bash以及Linux中的Shell。

Shell工作时的处理流程如下:

当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

6.3 Hello的fork进程创建过程

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

6.4 Hello的execve过程

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

删除已存在的用户区域

映射私有区:为 hello 的代码、数据、.bss 和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的

映射共享区:比如 hello 程序与共享库 libc.so 链接

设置 PC:exceve() 做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点

execve() 在调用成功的情况下不会返回,只有当出现错误时,例如找不到需要执行的程序时,execve() 才会返回到调用程序

6.5 Hello的进程执行

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

6.5.1逻辑控制流

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

6.5.2并发流与时间片

两个流如果在执行的时间上有所重叠,那么我们就说这两个流是并发流,每个流执行一部分的时间就叫做时间分片。

6.5.3内核模式和用户模式

用户模式:

在用户模式中,进程不允许执行特权指令,例如发起一个I/O操作等,更重要的是不允许直接引用地址空间中内核区内的代码和数据。如果在用户模式下进行这样的非法命令执行,会引发致命的保护故障。

内核模式:

在内核模式下,进程的指令执行相对没有限制,这有点类似于在Linux操作系统中,是否使用sudo(SuperUser do)作为指令的前缀一样。而在内核模式下运行的进程相当于获得了超级管理员的许可。

6.5.4上下文切换

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

6.5.5Hello的执行

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

6.6 hello的异常与信号处理

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

中断来自I/O设备的信号异步总是返回到下一条指令

陷阱有意的异常同步总是返回到下一条指令

故障潜在可恢复的错误同步可能返回到当前指令

终止不可恢复的错误同步不会返回

中断:

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

陷阱:

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

故障:

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

终止:

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

6.2.2信号

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

1SIGHUP 终止 终端线挂断

2 SIGINT 终止 来自键盘的中断

3 SIGQUIT 终止 来自键盘的退出

4 SIGILL 终止 非法指令

5 SIGTRAP 终止并转储内存 跟踪陷阱

6 SIGABRT 终止并转储内存 来自abort函数的终止信号

7 SIGBUS 终止 总线错误

8 SIGFPE 终止并转储内存 浮点异常

9 SIGKILL 终止 杀死程序

10 SIGUSR1 终止 用户定义的信号 1

11 SIGSEGV 终止并转储内存 无效的内存引用(段故障)

12 SIGUSR2 终止 用户定义的信号2

13 SIGPIPE 终止 向一个没有读用户的管道做写操作

14 SIGALRM 终止 来自alarm 函数的定时器信号

15 SIGTERM 终止 软件终止信号

16 SIGSTKFLT 终止 协处理器上的栈故障

17 SIGCHLD 忽略 一个子进程停止或者终止

18 SIGCONT 忽略 继续进程如果该进程停止

19 SIGSTOP 停止直到下一个 SIGCONT2 不是来自终端的停止信号

20 SIGTSTP 停止直到下一个 SIGCONT 来自终端的停止信号

21 SIGTTIN 停止直到下一个SIGCONT 后台进程从终端读

22 SIGTTOU 停止直到下一个 SIGCONT 后台进程向终端写

23 SIGURG 忽略 套接字上的紧急情况

24 SIGXCPU 终止 CPU时间限制超出

25 SIGXFSZ 终止 文件大小限制超出

26 SIGVTALRM 终止 虚拟定时器期满

27 SIGPROF 终止 剖析定时器期满

28 SIGWINCH 忽略 窗口大小变化

29 SIGIO 终止 在某个描述符上可执行 I/O 操作

30 SIGPWR 终止 电源故障

6.7本章小结

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

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

在CPU中当然不是仅仅只有hello一个进程,而是许多进程共享CPU和主存资源。那么为了使内存管理更加高效,操作系统提出了一种十分重要的抽象,即虚拟内存。它有几处有点:1)可以有效使用主存;2)可以简化内存管理;3)提供独立的地址空间。下面介绍几个概念

逻辑地址:逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元、存储单元、网络主机的地址。逻辑地址往往不同于物理地址,通过地址翻译器或映射函数可以把逻辑地址转化为物理地址。

线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。保护模式下,hello 运行在虚拟地址空间中,它访问存储器所用的逻辑地址。

物理地址:它是在地址总线上,以电子形式存在的,使得数据总线可以访问 主存的某个特定存储单元的内存地址。

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

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

使用段选择符中的偏移值在GDT或LDT表中定位相应的段描述符。

利用段选择符检验段的访问权限和范围,以确保该段可访问。

把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。

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

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

未分配:虚拟内存中未分配的页

未缓存:已经分配但是还没有被缓存到物理内存中的页

已缓存:分配后缓存到物理页块中的页

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

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

7.4.1TLB加速地址翻译

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

7.4.2四级页表支持下缓存

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

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

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

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

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

删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。

映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。

映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

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

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

7.8.2缺页故障处理

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

7.9动态存储分配管理

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

7.9.1堆

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

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

7.9.2隐式空闲链表

首先了解几个概念:

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

下一次适配 (Next fit): 和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快: 避免重复扫描那些无用块。一些研究表明,下一次适配的内存利用率要比首次适配低得多。

最佳适配 (Best fit): 查询链表,选择一个最好的空闲块;适配,剩余最少空闲空间。保证碎片最小——提高内存利用率,通常运行速度会慢于首次适配。

在隐式空闲链表工作时,如果分配块比空闲块小,可以把空闲块分为两部分,一部分用来承装分配块,这样可以减少空闲部分无法使用而造成的浪费。隐式链表采用边界标记的方法进行双向合并。脚部与头部是相同的,均为 4 个字节,用来存储块的大小,以及表明这个块是已分配还是空闲块。同时定位头部和尾部,是为了能够以常数时间来进行块的合并。无论是与下一块还是与上一块合并,都可以通过他们的头部或尾部得知块大小,从而定位整个块,避免了从头遍历链表。但与此同时也显著的增加了额外的内存开销。他会根据每一个内存块的脚部边界标记来选择合并方式

7.10本章小结

本章介绍了Hello和操作系统之间的交流方式。介绍了计算机系统中非常重要的一个概念:虚拟内存,以及关于它的内容。介绍了Hello是如何经过地址翻译从而找到最终的物理地址。阐释了TLB加速地址翻译、多级缓存以及动态内存管理相关的要点。

(第7章 2分)

结论

(结论0分,缺失 -1分,根据内容酌情加分)

hello所经历的过程:

1. 编写hello.c源码(C语言文本文件);

2. 预处理:预处理器(cpp)将hello.c预处理为hello.i(C语言文本文件);

3. 编译:编译器(ccl)将hello.i编译成hello.s(汇编语言文本文件);

4. 汇编:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,将结果保存在文件hello.o(二进制文件)中;

5. 链接:链接器(ld)将hello中使用的库函数文件合并到hello.o文件中构成可执行目标文件hello;

6. shell为hello创建进程;

7. shell运行hello;

8. 运行指令,读取内存,处理信号和异常;

9. 结束运行,shell回收hello进程;

通过对hello程序在计算机上的“程序人生”的梳理,对于程序在计算机系统上实现的底层原理有了系统而深入的了解,有助于未来进行更好地进行程序编写与调试。

附件

(附件0分,缺失 -1分)

1.hello.i:cpp预处理原始的hello.c得到的修改了的源程序。主要作用是将头文件直接插入程序文本中,并且根据#define进行字符串的简单替换,得到新的预处理过的文本文件。(文本文件)

2.hello.s:编译器将预处理过的hello.i文件编译成汇编文件。(文本文件)

3.hello.o:汇编器将hello.s汇编文件转化为使用机器语言的可重定位目标文件(二进制文件)。

4.hello:链接器将C函数库中相应的函数与hello.o可重定位目标文件链接,生成的可执行目标文件(二进制文件)。

5.elf1.txt:hello.o使用readelf产生的ELF文件。

6.o.txt:hello.o的反汇编文件。用于和hello.s进行比较分析。

7.elf2.txt:hello使用readelf产生的ELF文件。有的中间产物的文件名,并予以说明起作用。

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

(参考文献0分,缺失 -1分)

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值