计算机系统

大作业

题     目 程序人生-Hellos P2P

专       业 计算机类

学     号 120L022010

班   级 2003007

学       生 窦馨宇

指 导 教 师 吴锐

计算机科学与技术学院

2021年5月

摘  要

本篇论文将以hello程序为例,探讨一个c程序是如何从编写到处理成为可执行文件,再到加载到内存中成为一个进程,最后被内核命令回收进程的。从表面上对程序的理解将更加深入化到底层计算机系统。

关键词:预处理;编译;汇编;链接;进程;存储;I/O管理。                          

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:P2P的意思是process to process。hello.c预处理、编译、汇编、链接最终成为可执行目标程序hello,shell使用fork函数产生一个子进程,然后hello便从程序变为了进程。具体过程为:首先,源程序hello.c被预处理器预处理成修改了的源程序hello.i然后经编译器编译成汇编程序hello.s,接着hello.s经汇编器翻译成可重定位目标程序hello.o然后hello.o经链接器与其调用的库函数链接成为可执行目标程序hello。Hello运行过程中,shell使用fork函数为hello创建进程,于是hello从程序变成了进程。

020:hello成为进程之后,由shell进程调用execve函数运行,整个进程运行结束后,进程终止并被其父进程回收。内核删除相关结构后完成。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:x64CPU,RAM

软件环境:WINDOWS 10 64位,VirtueBox 20.04.4

调试工具:gcc gdb edb vim

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c源程序文件
hello.i修改了的源程序文件
hello.s汇编程序文件
hello.o可重定位目标程序文件(二进制)
hello可执行目标文件(二进制)

1.4 本章小结

本章旨在让我等学生了解阐述hello从一个.c文件通过预处理,编译,汇编,链接的过程把它变成一个可执行文件的过程,有助于深刻了解编译系统互相合作互相分工,在计算机内部井井有条的工作状态。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理器(CPP)根据以字符#开头的命令,修改原始的C程序。

预处理的作用:根据预处理指令修改源代码,例如#include<stdio.h>命令就是告诉预处理器读取系统头文件stdio.h中的内容,并把它直接插入程序文本中,就得到另一个C程序,通常以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

在Ubantu中预处理命令为:gcc -E hello.c -o hello.i

运行截图如下:

图2.2.1.预处理指令以及生成的hello.i文件

2.3 Hello的预处理结果解析

从上面图中可以看出,hello.c已经被扩展成为了hello.i,可以看到文件被扩展成了三千多行,内容大大增加,但是依然是可读的。源文件中的宏被展开,增加的文本文件应该是三个头文件中的源码,头文件的内容被直接插入文本文件中。

2.4 本章小结

本章让我探索了预处理的相关信息和应用,在虚拟机中进行实际操作并且看到了预处理之后的.i文件并对它进行了分析。更加了解了预处理阶段的程序变化过程。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包 含一个汇编语言程序。

编译的作用:进行词法分析和语法分析,将源程序翻译成目标程序,如果分析过程中没有语法错误就生成汇编语言。

3.2 在Ubuntu下编译的命令

命令应该是:gcc -S hello.i -o hello.s

截图如下:

图3.2.1.hello.i编译成hello.s的指令以及生成的hello.s文件

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

3.3.1 汇编文件指令

名称

作用

.file

声明源文件

.test

声明代码段

.section.rodata

只读数据段

.global

声明全局变量

.type

声明一个符号类型

.size

声明大小

.align

声明对指令或数据存放地址进行对齐的方式

3.3.2 数据

1.字符串

程序中存在两个字符串都是只读数据段:

图3.3.1.hello.s文件截图一

  1. 局部变量i

main函数声明了一个局部变量i,编译器进行编译的时候会把局部变量放在堆栈中,如图所示,局部变量i放在栈上-4(%rbp)的位置。

图3.3.2.hello.s文件截图二

  1. 参数argc

是用户传给main的参数,也被放在了堆栈中。

  1. 立即数

立即数体现在汇编代码中

例如:

图3.3.3.hello.s截图三

  1. 数组char *argv[]

这是main函数的第二个参数,数组中的每一个元素都是一个指向字符类型的指针,数组的起始地址存放在栈的-32(%rbp)位置,被两次调用传给printf。

图3.3.4.hello.s截图四

3.3.3算术操作

在hello.c中的算术操作是i++,对应的汇编语言如下:

图3.3.5.hello.s截图五

3.3.4赋值操作

Hello.c中赋值操作的实现是for循环中每次为i的赋值,对应到汇编代码中应该使用mov指令实现赋值操作。Mov指令根据操作数字节大小可以被分为movb,movw,movl,movq分别对应一个字节,两个字节,四个字节以及八个字节。

3.3.5关系操作

1.argc!=4,在源程序中,需要比较argc和4的大小,在编译中汇编代码如下:

图3.3.6.hello.s截图六

同时这条指令也有设置条件码的作用,根据条件码确定是否需要跳转。

2.i<=7.在源程序中需要比较i和7的大小关系,在编译中对应的汇编代码如下:

图3.3.7.hello.s截图七

这条指令也有设置条件码的作用,为利用下一条指令设置条件码预备跳转具有重大作用。

3.3.6控制转移指令

即根据之前设置好的条件码,然后根据条件码进行跳转操作

例如1:根据上面一行指令,如果argc等于4,就跳转。

图3.3.8.hello.s截图八

例2:根据上面一行指令,如果i<=7就跳转到.L4

图3.3.9.hello.s截图九

3.3.7函数操作

涉及的调用函数如下:

图3.3.10.hello.s截图十

运用call指令调用函数,调用时,函数需要的参数都已经被存储在寄存器或者栈中进行参数传递,函数中的返回值保存在%rax这个寄存器中。

3.4 本章小结

本章对翻译过程中的编译进行了具体例子的具体分析,了解了从修改了的文本文件hello.i编译成为汇编程序hello.s的过程,以及过程中汇编代码的分析。并且从C语言中数据类型,算术操作,赋值操作等多个操作方式对应到了汇编代码中,理解了编译器编译的机制,对从汇编语言进行反汇编的能力大大提升。更加深入了解了汇编代码。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

汇编的概念:汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成叫做可重定位目标程序的格式,并将结果保存在.o文件中。

汇编的作用:将汇编语言翻译成真正的机器语言,变为一个二进制文件,但是这个二进制文件在文本编辑器中打开将会看到一堆乱码。

4.2 在Ubuntu下汇编的命令

应截图,展示汇编过程!

命令为:gcc hello.s -c -o hello.o

过程截图:

图4.2.1.生成hello.o文件

上图为得到的hello.o

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

1)对hello.o的ELF的格式分析:

ELF头:描述文件总体格式,在后面细讲。

.txt:已编译程序的机器代码。

.rodata:只读数据。

.data:已初始化的全局和静态局部C变量。

.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。

.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.rel.text:一个.text节中位置的列表。

.rel.data:被模块引用或定义的所有全局变量的重定位信息。

.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件。

在Ubantu的Linux系统中生成Elf格式的命令为:即利用readelf命令来查看ELF,将其重定位为文本文件。

生成的指令为:readelf -a hello.o>hello.elf

截图如下:

图4.3.1.查看ELF文件

2)对重定位文本文件的分析:

Hello.elf文件内容以及分析:

1.ELF头

ELF头(ELF header)是以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头其余部分包含了帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可重定位、可执行、共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

一个典型的ELF可重定位目标文件包含下面几个节:

图4.3.2.ELF头部分代码截图

  1. 节头表

负责记录各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等。

图4.3.3.节头表部分代码截图

  1. 符号表

图4.3.4.符号表代码截图

符号表存放在程序中定义和引用的函数和全局变量的信息。.symtab符号表不包含局部变量的条目。

  1. 重定位节

图4.3.5.重定位节代码截图

重定位节保存的是.text节中需要被修正的信息(任何调用外部函数或者引用全局变量的指令都需要被修正),调用外部函数的指令和引用全局变量的指令需要重定位,调用局部函数的指令不需要重定位。

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

利用指令objdump -d -r hello.o对hello.o进行反汇编,截图如下:

图4.4.1.hello.o反汇编截图

而hello.s的表现为:

图4.4.2.hello.s截图

对照分析:

差别主要表现在以下几点:

1.指令前加上了十六进制表示,即机器语言,操作数的引用也不同,在反汇编中有关操作数是十六进制的,而汇编代码中则是十进制。

2.在汇编代码中调用函数时直接调用这个函的名称,而在反汇编文件中call之后是相对偏移量。

3.在汇编代码中分支跳转直接以.L0等助记符表示,但在反汇编代码中分支转移的表示为主函数+段内偏移量。

4.5 本章小结

本章要求我对程序的汇编过程加以分析,以实例出发,分析一个可重定位目标文件中的内容,包括他的组成和其他信息。并且还对hello.o的反汇编代码和hello.s作了比较,仔细寻找了它们之间的不同之处。对编译的过程更加了解熟悉。

(第4章1分)

5 链接

5.1 链接的概念与作用

注意:这儿的链接是指从 hello.o 到hello生成过程。

链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(或复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。

链接的作用:链接可以使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

在Ubantu下执行的命令以及生成的文件:

图5.2.1.执行指令并生成hello可执行文件

执行的命令为:

图5.2.2.执行的指令

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

  1. 使用命令生成ELF文件:

图5.3.1.生成hello的elf文件

  1. hello的ELF格式:

使用 ELF 文件格式的可执行文件的组成是这样的:一个 ELF 文件头,后面是一个程序头表,或者是一个节(即 section,用段指代 segment)头表,或两者都有。ELF 文件头总是位于文件中偏移量为 0 的位置。程序头表和节头表在文件中的偏移量则由 ELF 文件头定义。这两个表描述了整个 ELF 文件其余部分的细节。

3.各段的基本信息:

1.ELF头:可以看到节头大小为64字节,数量为27.

图5.3.2,ELF头

2.节头部表为:

图5.3.3,节头部表

可以看到可执行文件中经过重定位每个节的地址不再是0,而是根据自身大小加上对齐规则计算的偏移量。例如.hash的地址,计算方式是.note.ABI-tag的地址0x400320加上.note.ABI-tag的大小0x20,得到0x400340,再对.hash要求的8字节对齐进行调整,得到最终地址0x400340。

  1. 重定位节

图5.3.4.重定位节

重定位节偏移量与hello.o完全不同。

  1. 符号表

图5.3.5.符号表

可执行文件中多出了.dynym节。这里面存放的是通过动态链接解析出的符号,这里我们解析出的符号是程序引用的头文件中的函数。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

  1. 首先使用edb查看hello

图5.4.1.使用edb加载hello

  1. 观察Data Dump窗口,如下:

图5.4.2.edb打开的Data Dump窗口

可以看到虚拟地址从0x401000开始,到0x401ff0结束。并且根据5.3中的节头部表,可以通过edb找到各个节的信息。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

  1. 使用命令objdump -d -r hello > hello.txt。

图5.5.1.得到hello文件

  1. 生成的文件与hello.o的不同之处

图5.5.2.得到的hello文件

比较:首先hello.txt中比hello.o多了许多节。后者中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000。hello.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转指令也不再是相对偏移地址,而是虚拟内存地址。并且许多调用的函数都是外部链接的共享库函数。

hello的重定位过程:

重定位条目当编译器遇到对最终位置未知的目标引用时,它会生成一个重定位条目。

重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。

重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

5.6 hello的执行流程

hello在执行的过程中一共要执行三个大的过程,分别是载入、执行和退出。载入过程的作用是将程序初始化,等初始化完成后,程序才能够开始正常的执行。由于hello程序只有一个main函数,所以在程序执行的时候主要都是在main函数中。又因为main函数中调用了很多其它的库函数,所以可以看到,在main函数执行的过程中,会出现很多其他的函数。

程序名称

程序地址

ld-2.31.so!_dl_start

0x7f8e7cc34ed0

ld-2.31.so!_dl_init

0x7f8e7cc486a0

hello!_start

0x4010f0

libc-2.31.so!_libc_start_main

0x7ff 825425fc0

libc-2.31.so!_cxa_atexit

0x7ff 825448f60

hello!_libc_csu_int

0x4011c0

libc-2.31.so!_setjmp

0x7ff 82fdb2e00

libc-2.27.so!exit

0x7ff 82fdc3bd0

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

 可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

5.8 本章小结

本章介绍了程序的链接过程,这是程序生成可执行文件的最后一步。并且通过edb查看了hello的虚拟空间,比较了hello和hello.o之间的差别,对链接的过程更加熟悉。

(第5章1分)

6 hello进程管理

6.1 进程的概念与作用

进程的概念:在现代系统上运行一个程序的时候,我们会得到一个假象,就好像我们的程序是系统中当前唯一运行的程序一样。我们的程序好像独占的使用处理器和内存。处理器就好像是无间断地一条接一条执行我们程序中的指令。最后,程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正常运行所需的状态构成的,这个状态包括存放在内存中程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程的作用:我们会得到一个假象,就好像我们的程序是系统中当前唯一运行的程序一样。我们的程序好像独占的使用处理器和内存。处理器就好像是无间断地一条接一条执行我们程序中的指令。最后,程序中的代码和数据好像是系统内存中唯一的对象。

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

Shell-bash:Shell-Bash是一个用c语言编写的程序,它是用户使用Linux的桥梁,它既是一种命令语言,又是一种程序设计语言,它是一种应用程序。

功能:Shell-Bash应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。

处理流程: 1)从终端读入输入的命令。

2)将输入字符串切分获得所有的参数

3)如果是内置命令则立即执行

4)否则调用相应的程序执行

5)Shell-Bash 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

  1. 在终端中输入./hello 120L022010 窦馨宇 1 命令后,shell会处理该命令.
  2. 判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。
  3. 通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是虚拟地址独立、PID也不相同的一份副本。

6.4 Hello的execve过程

当使用fork函数创建了一个子进程之后,子进程需要调用exceve函数在当前子进程的上下文加载并运行hello程序,需要:

1.删除已存在的用户区域。

2.映射私有区域。所有这些区域结构都是私有的,写时复制的。栈和堆区域也是请求二进制零的,初始长度为零。

3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

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

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

上下文信息以及上下文切换: 上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。

进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

进程调度过程:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。

用户态与核心态转换:为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

Hello程序进程执行:程序开始时,内核将为其保存一个上下文,而当其调用函数sleep时,系统将陷入内核,而内核处理完成系统函数后,将执行上下文切换,将控制返回给hello程序中的下一条语句。从上到下依次执行,直到运行到返回值。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

(1)hello执行过程中可能出现的异常以及可能产生的信号:

1.中断:来自I/O设备的信号。比如输入CTRL -C或者CTRL-Z

2.陷阱:有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。

3.故障:是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

4.终止:是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

可能产生的信号有SIGINT,SIGSTP,SIGCONT,SIGWINCH等等。

  1. 各命令及运行结截屏:
  1. 正常运行,程序正常结束,被正常回收

图6.6.1.正常运行情况

  1. 运行过程中按下Ctrl-c,将会给进程发送SIGINT信号,程序将被终止回收

图6.6.2.输入Ctrl-c情况

  1. 运行过程中按下Ctrl-z,这将会给进程发送SIGSTP信号,hello程序将被挂起,用ps命令可以看到hello进程并没有回收,其进程号为649521,再使用jobs命令可以查看job的ID为1,状态为已停止,最后用fg 1命令,即可让其回到前台继续运行。直到程序结束并且被顺利回收。

图6.6.3输入ctrl-z情况

也可以在运行中尝试pstree命令查看所有进程的父子关系如下:

图6.6.4.对kill指令以及pstree指令的尝试使用

然后输入kill指令杀死进程,再用jobs查看就能看到进程已经被彻底回收了。

  1. 空格

图6.6.5.对空格的尝试使用

继续正常执行。

6.7本章小结

本章介绍了进程的概念以及相关应用。进一步充分了解了进程的创建与异常流控制。每一个标题都引领着去探索进程从创建到被回收之间的过程。其中包括异常处理程序对每个异常的处理。对进程管理的整个过程都有了更深的理解。

(第6章1分)

7 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为段标识符:段内偏移量。在hello反汇编代码中我们能够看到的就是逻辑地址。

线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。

虚拟地址:与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB索引)、TLBT(TLB标记)。

物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成。在前端总线上传输的内存地址都是物理内存地址。

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

在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。

一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。段标识符如下:

图7.2.1.段标识符的分块情况

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

线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。我们一般通过页表来获得虚拟地址到物理地址的映射。页表是一个关于页表条目PTE的数组。页表条目由有效位和物理页号组成。一个虚拟页只有如下三个状态:未分配的,缓存的以及未缓存在虚拟内存中的已分配页。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

TLB,(翻译后备缓冲器)是一个包含在MMU中的小缓存,其每一行都由一个PTE(页表条目)组成。TLB将一个n-p位VPN分为t位的组索引和n-t-p位的标记。 多级页表将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。

图7.4.1四级页表下VA到PA的转换

如左图所示。

从VA到PA变换过程:处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘——写回策略)。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。

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

Cache的访问并不复杂,对Cache的访问需要把一个物理地址分为标记、组索引、块偏移三个部分。首先通过组索引来找到我们的地址在Cache中所对应的组号,再通过标记和Cache的有效位来判断我们的内容是否在Cache中。若命中则通过块偏移读取我们要的数据,若不命中则从下一级Cache中寻找(下一级Cache不一定真的是Cache,比如对L3来说,它的下一级Cache就是主存)。

通过MMU将虚拟地址转化成物理地址后,计算机就通过提取中的组索引在L1中搜索组,再通过标记位匹配。如果匹配成功且有效位是1,则将块偏移指向的块中的内容交还给CPU,否则未命中,需要从下一级Cache中在重复上述操作。当我们找到内容后需要将内容写回我们的L1中,如果L1中没有空闲块,即有效位为0的块则需要牺牲一块内容,我们通常采用LRU算法来进行这一过程。对L2、L3的访问也是这样。

7.6 hello进程fork时的内存映射

在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。

7.7 hello进程execve时的内存映射

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

  1. 删除已存在的用户区域

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

2.映射私有区域

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

3.映射共享区域

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

4.设置程序计数器(PC)

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

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

动态内存管理的基本方法和策略:

7.9.1.动态分配器分类介绍

动态内存分配器动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器主要分为显式分配器和隐式分配器。

7.9.2.显示空间链表

显式空闲链表是将对组织成双向链表。在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。

维护链表方法:

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

另一种方法是按照地址顺序来维护链表,其中链表中的每一个块的地址都小于它后一个块的地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。

7.9.3.带边界标记的隐式空闲链表分配器

一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。

我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。

7.9.4.适配块策略

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

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

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

7.10本章小结

本章主要介绍了hello的存储地址空间,并且带领思路于地址空间的转换之中。还有段式管理,页表管理,TLB与四级页表支持下的VA到PA的变换,三级cache支持下的物理内存访问,hello进程fork和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理等内容。

我可以跟着走向去看程序内部是如何组织的,也看到了程序正确运行的方法。

(第7章 2分)

8 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的I/O设备都被模型化为文件(甚至是内核),而所有的输入和输出都被当作相应文件的读和写来完成。这种将设备映射为文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1.接口结构操作

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。

3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。

5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。

8.2.2.函数

1.打开文件:int open(char* filename,int flags,mode_t mode)

flag: O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)

mode: 指定新文件的访问权限位

返回值:成功则为文件描述符,失败则返回-1

2.关闭文件:int close(fd)

返回值:成功则返回0,失败则返回-1

3.读文件ssize_t read(int fd,void *buf,size_t n)

read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。

返回值:成功则返回读的字节数,出错则返回-1,EOF返回0

4.写文件ssize_t wirte(int fd,const void *buf,size_t n)

8.3 printf的实现分析

首先,printf的函数体如下:

图8.3.1.Printf代码截图

printf程序按照格式fmt结合参数arg生成格式化之后的字符串,并返回字串的长度。

vsprintf代码:

图8.3.2.Vsprintf代码截图

在printf中调用系统函数write(buf,i)将长度为i的buf输出。

write函数如下:

图8.3.3.write函数

在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALL代表通过系统调用sys_call。

Sys_call代码:

图8.3.4.sys_call代码

此函数将字符串从寄存器中通过总线复制到显卡的显存中去,显存中存储的是字符的ASCII码。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是屏幕上就会显示输入的hello 序号 姓名了。

8.4 getchar的实现分析

首先,getchar函数运行时,控制权会交给os,用户按键,输入的内容便会显示在屏幕上。按下回车键表示输入完成,这时控制权将被交还给程序。异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。然后getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5 本章小结

本章介绍了Unix中的I/O接口以及函数,并且以printf和getchar为例,分析了对他们的实现过程,对有关文件的操作更加了解了。

(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程:

Hello的经历看起来就是实现了一个简单的程序,但从计算机系统的角度来看就能看到它在计算机中一步步改变并且最终被执行的全过程。

1.首先从键盘用高级语言(C语言)编写得到hello.c程序

2.预处理:hello.c经过预处理得到hello.i

3.编译:hello.i经过编译得到汇编文件hello.s

4.汇编:hello.s经过汇编得到可重定位目标文件hello.o

5.链接:hello.o经过链接得到可执行文件hello

6.键盘输入:输入命令 ./hello 120L022010 窦馨宇 1 运行程序

7.创建子进程并加载程序:shell调用fork函数创建子进程,并调用execve函数加载运行hello程序

8.分配时间片:cpu将为其分配时间片,在自己的时间片里,hello顺序执行自己的逻辑控制流

9.访问内存:hello执行过程中,会访问内存,请求一个虚拟地址,再由MMU将其转化为物理地址,通过cache来访问

10.运行程序调用函数收到各种信号:运行过程中,同时也会调用一些函数,还会遇到各种各样的信号,shell为其准备了各种的信号处理程序

11.程序终止:最后hello程序结束,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

对计算机系统设计和实现的深切感悟,你的创新理念,如新的设计和实现方法:

计算机内部有序高效,并且团结协作,共同营造计算机快速处理程序的场面。设计计算机的人是伟大而拥有丰富设想和缜密逻辑的。他让整个计算机在处理数据时取长补短,达到最后“共同高效”的局面。比如说,由于CPU的处理速度比主存快得多,为了减少“供不应求”的现象,在CPU和主存之间增加了一级,二级,三级cache大大提高了CPU访问主存的速度。并且,计算机的制造者也巧妙地找到了操作的共性并为此发明了一套抽象方法。例如有了I/O抽象成对文件的操作,计算机设计者只要做好对文件的操作、外接设备的设计者也只要做好将自己的设备设计成支持文件操作的形式就可以完成各种各样的操作了。

这是非常伟大而神奇的发明。我在本次大作业中不断深入了解计算机运行程序的内部情况,就越来越叹服于前人伟大之处。我需要学习的地方还有很多很多。

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

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c 源程序

hello.i 预处理后文件

hello.s 编译后的汇编文件

hello.o 汇编后的可重定位目标执行文件

hello 链接后的可执行文件

hello.elf   hello.o的ELF格式

hello.txt  hello的反汇编

hello_elf2.txt  hello的ELF格式

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

参考文献

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

[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分)

哈尔滨工业大学 计算机系统大作业相关推荐

  1. 哈尔滨工业大学 计算机系统 大作业 22春

    计算机系统大作业 计算机科学与技术学院 2022年5月 摘 要 摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息.摘要应包括本论文的目的.主要内容.方法.成果及其 ...

  2. 哈尔滨工业大学计算机系统大作业2022春

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学   号 120L02**** 班   级 2003005 学       生 无敌飞龙 指 导 教 ...

  3. 哈尔滨工业大学计算机系统大作业--程序人生

    计算机系统   大作业 题     目  程序人生-Hello's P2P      专       业   计算机科学与技术        学    号        2021110xxx      ...

  4. 2023春 哈尔滨工业大学计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 物联网工程 学    号 2021111491 班    级 2137301 学       生 业知翰 指 ...

  5. 哈尔滨工业大学计算机系统大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 计算机科学与技术学院 2021年5月 摘  要 本文以hello程序从hello.c到进程各个阶段所 ...

  6. 哈尔滨工业大学计算机系统大作业

    计算机科学与技术学院 2021年5月 摘  要 本篇论文分析了程序hello的p2p,020过程,根据书籍<深入理解计算机系统>,对从hello程序的编译,到hello程序的运行的整体过程 ...

  7. 哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P

    目录 摘要 第1章 概述 1.1 Hello简介 1.1.1 P2P:From Program to Process 1.1.2 020:From Zero-0 to Zero-0 1.2 环境与工具 ...

  8. 程序人生-Hello’s P2P——哈尔滨工业大学计算机系统大作业

    摘  要 在这篇文章中,我们主要探究hello程序从几行源代码到真正变成一个程序,实现自身功能,最后被回收的过程.在这个过程中,我们按照课程知识体系的流程,一步步对hello进行分析阐述,分析程序在计 ...

  9. 哈尔滨工业大学计算机系统大作业-程序人生 Hello‘s P2P

    文章目录 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的作用及概念 2.2 在Ubuntu下预处理的命令 2.3 Hel ...

最新文章

  1. 【青少年编程】【Scratch】10 画笔模块
  2. circlize包可绘制的几个图形示例
  3. 1370亿参数、接近人类水平,谷歌重磅推出对话AI模型LaMDA
  4. 选择问题 and 字谜游戏问题
  5. Spring - @CompentScan全解
  6. python sys模块讲解_python模块之sys模块和序列化模块(实例讲解)
  7. Python爬虫框架Scrapy安装使用步骤
  8. java版电子商务spring cloud分布式微服务b2b2c社交电商(四)SpringBoot 整合JPA
  9. JPA JoinColumn vs mappedBy
  10. 如何对Javascript代码进行二次压缩(混淆)
  11. 5_Text Generation(文本生成)
  12. bzoj 1603: [Usaco2008 Oct]打谷机(拆点并查集)
  13. UPC 条形码的介绍及计算校验码
  14. 计算机一级网页制作教程视频教程,网页制作入门教程(一)
  15. 宏电7710与组态王通信配置
  16. VMware虚拟机全屏状态下如何快速切换回原主机界面
  17. Android 9.0 flash播放器播放swf源码讲解
  18. 正则表达式——同时验证座机号和手机号
  19. Neo4j ① <图论>图,节点,关系,属性<知识图谱和图库>图谱,图库,优势<基础>模块,应用场景,环境搭建,浏览器
  20. windows server 2008R2怎么设置活动但空闲的远程桌面服务会话时间的限制

热门文章

  1. 11度青春之《老男孩》
  2. 零知识证明之zk-snark 应用案例(零知识证明实战渔业监控【zksnark】)
  3. 极智Coding | 剖析 darknet load_weights 接口
  4. load_weights` requires h5py when loading weights from HDF5
  5. Censored! POJ - 1625(AC自动机 + dp +高精度模板)
  6. Galaxy数据统计平台(二)
  7. Word2010下划线不显示
  8. 前端开发面试题及答案整理
  9. 10个优秀的Golang开源库类,武装生产力
  10. LWR 局部加权线性回归算法