HIT计算机系统大作业-程序人生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 *******
班   级 ******
学 生 *******    
指 导 教 师 郑贵滨

计算机科学与技术学院
2021年6月
摘 要
本文对hello程序的P2P和020过程进行分析,解释了hello从创建开始到多次处理最后到回收的过程,在这些分析中,我们可以更好的理解一个程序的生命周期以及计算机的底层实现的原理。

关键词:预处理、编译、链接、进程、信号

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

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
4.1 汇编的概念与作用 - 15 -
4.2 在UBUNTU下汇编的命令 - 15 -
4.3 可重定位目标ELF格式 - 15 -
4.4 HELLO.O的结果解析 - 18 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在UBUNTU下链接的命令 - 21 -
5.3 可执行目标文件HELLO的格式 - 22 -
5.4 HELLO的虚拟地址空间 - 24 -
5.5 链接的重定位过程分析 - 25 -
5.6 HELLO的执行流程 - 26 -
5.7 HELLO的动态链接分析 - 27 -
5.8 本章小结 - 28 -
第6章 HELLO进程管理 - 29 -
6.1 进程的概念与作用 - 29 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 29 -
6.3 HELLO的FORK进程创建过程 - 29 -
6.4 HELLO的EXECVE过程 - 30 -
6.5 HELLO的进程执行 - 30 -
6.6 HELLO的异常与信号处理 - 31 -
6.7本章小结 - 36 -
第7章 HELLO的存储管理 - 37 -
7.1 HELLO的存储器地址空间 - 37 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 37 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 37 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 39 -
7.5 三级CACHE支持下的物理内存访问 - 40 -
7.6 HELLO进程FORK时的内存映射 - 41 -
7.7 HELLO进程EXECVE时的内存映射 - 41 -
7.8 缺页故障与缺页中断处理 - 42 -
7.9动态存储分配管理 - 43 -
7.10本章小结 - 43 -
第8章 HELLO的IO管理 - 45 -
8.1 LINUX的IO设备管理方法 - 45 -
8.2 简述UNIX IO接口及其函数 - 45 -
8.3 PRINTF的实现分析 - 46 -
8.4 GETCHAR的实现分析 - 48 -
8.5本章小结 - 48 -
结论 - 49 -
附件 - 50 -
参考文献 - 51 -

第1章 概述
1.1 Hello简介
P2P:From Program to Process
首先用C语言编写出hello.c,之后在编译系统中hello.c先经过预处理器(cpp)读取系统头文件内容,并把它直接插入程序文本中,预处理得到hello.i,再经过编译器(ccl)把hello.i翻译成文本格式的汇编程序hello.s,之后汇编器(as)将hello.s翻译成机器语言指令,并把它们打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制文件hello.o中,最后再通过链接器(ld)合并所需文件,得到可执行文件hello文件。这样就得到了Program。之后在shell中运行hello,就会创建一个hello的新的进程。这样就得到了Process。
020:From Zero to Zero
操作系统使用shell通过execve执行hello文件,映射虚拟内存,之后程序载入内存,执行指令,执行完之后,hello进程被shell父进程回收,内核删除与之相关的数据结构,所有关于hello的信息又都没有了。
1.2 环境与工具
硬件环境:i7-8750H CPU;2.21GHz;8G RAM;256GHD Disk;
软件环境:Windows10 64位;Vmware 10;Ubuntu 18.04 LTS 64位;
开发工具:Visual Studio 2019 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc;
1.3 中间结果
hello.i:预处理后的文本文件
hello.s:编译后的汇编文件
hello.o:汇编后的可重定位文件
hello:链接后的可执行文件
objhello.txt:hello.o的反汇编文件
1.4 本章小结
本章概括了hello程序从编写到执行的过程。介绍了所需的环境和工具。列出来了hello文件整个过程中的中间结果文件。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,比如#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。其他常见的预处理指令还有#define(定义宏)、#if、#ifdef、#endif等。
作用:得到了另外一个C程序,通常是以.i作为文件扩展名。使编译器在进行编译之前对源代码做某些转换,将头文件中的代码插入到程序代码中、通过宏对符号进行替换等。它一般被用来使源代码在不同的执行环境中被方便的修改或者编译。可以让目标程序变小,提高运行速度

2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

经过预处理后,生成了.i文件。可以观察到,代码已经有3066行,预处理实现了对代码的重新处理,对头文件里面的宏进行了宏展开,分析头文件里的内容可以看到里面有声明函数,定义结构体等内容。从3049行之后才开始我们一开始写的代码。

2.4 本章小结
本章介绍了预处理器对.c文件进行预处理的过程,生成了.i文件。完成了准备工作的开始的一部分。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:编译把高级语言变成了2进制文件,编译程序把一个源程序翻译成目标程序的工作过程经过了如下的阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,会给出提示信息。得到的汇编语言代码在之后的过程中可供编译器进行生成机器代码、链接等操作。

3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 代码中的汇编指令
汇编指令 含义
.file 声明源文件
.text 以下是代码段
.data 以下是数据段
.globl 声明一个全局变量
.align 声明对指令或者数据存放地址进行对齐的方式
.type 指定函数类型或对象类型
.size 声明大小
.long 声明一个long类型
.string 声明一个string类型
.section.rodata 以下是rodata节

3.3.2数据类型

程序中有两个字符串
第一个printf输出格式化参数,其中编码为utf8,汉字在UTF-8中有三个字节
第二个printf传入的输出格式参数

全局变量sleepsecs被设置为值为2的long类型
argc作为第一个参数传入

int i 编译器把i存在了栈上的-4(%rbp)中,i占据了栈中4B的大小空间。

立即数 整型数据的一部分还会以$+数据的形式直接出现在汇编代码上

数组argv[]为char类型单个数组元素的大小为8个字节起始地址为argv, argv[1],argv[2]是传入的两个参数,它们的值为getchar()在函数运行时获取的两个%s。
3.3.3 赋值

程序只在上图的这两个地方赋值了。又因为sleepsecs是全局变量,所以直接在.data中声明它是值为2的long类型,在这个地方要注意类型转换,sleepsecs是int类型,但给它的值是2.5,程序会自动转换类型,把2.5改为2,在进行编译的时候程序改变数值和位模式的原则是:值会向零舍入。而int的值直接用movl赋值就行。
3.3.4 关系操作

文中一共有两个判断,第一处是判断i是否小于10;第二处是判断argc是否等于3。
3.3.5 控制转移
同样见3.3.4中的两个图,左图中如果i=9,就跳到L4执行其中的代码,右图中如果argc不等于3就执行程序段中的代码,如果标志位ZF等于1的话就会跳转到L2.
3.3.6 函数操作
C 语言中,子程序的作用是由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,以减少重复编写程序段的工作量。
函数包括如下内容:
(1) 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参
与表达式的运算。这种方式要求函数是有返回值的。
(2) 函数语句:函数调用的一般形式加上分号即构成函数语句。
(3) 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。
调用函数的动作如下:
(1) 传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起
始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
(2) 传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中
返回一个值。
(3) 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回
前,又必须释放这些空间。
程序中的函数有以下五个:

  1. main函数

main函数被call调用,之后call会将下一条指令的地址压入栈中再跳到main函数,参数argc和argv被%edi和%rsi存储,再用%rbp记录栈桢的底用来分配和释放内存。
2. printf函数

第一次printf将%rdi设置为“Usage: Hello 学号 姓名! \n”字符串的首地址, 因为只有一个字符串参数,所以 call puts@PLT
第二次printf设置%rdi 为“Hello %s %s\n” 的首地址,第二次printf使用call printf@PLT。
3. exit函数

将1赋给%edi,call exit@PLT 退出。exit(1)表示异常退出,在退出前可以给出一些提示信息,或在调试程序中察看出错原因。
4. sleep函数

程序把sleepsecs赋值给%edi,在sleepsecs时间过后继续进行,call指令将下一条指令压栈,之后跳转到atoi函数。
5. getchar函数

读取标准输入流的一个字符,并返回输入字符的ASCII码或者EOF表示输入有误。

3.4 本章小结
本章系统阐述了使用Ubuntu下的编译指令可以使编译器将 hello.i 翻译成 hello.s 的操作,从汇编指令、数据类型、赋值、关系操作、控制转移、函数操作等方面针对 hello.s 中各部分做出了详细的说明。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指把汇编语言书写的程序翻译成与之等价的机器语言程序,汇编把.s文件转化成立.o文件。
作用:汇编器(as)将 hello.s 翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件 hello.o 中
4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

先用realelf -a hello.o > hello.elf 生成其elf格式,查看elf文件并分析
① ELF头:它以一个16字节的序列开始,这个序列描述了生成该系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
② 节头部表:不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。节头部表记录了各节名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐信息。

③ 重定位节
.rela.text:一个.text 节中位置的列表,当链接器把这个目标文件和其他文件组合时,其中的重定位信息分别是对.L0,puts,exit,.L1,printf,sleepsecs,sleep,getchar进行重定位声明。
rela.eh_frame :记录了. text 的信息
其中,Offset是需要被修改的引用的字节偏移(在代码节或数据节的偏移),Info指示了重定位目标在.symtab中的偏移量和重定位类型,Type表示不同的重定位类型,例如图中的R_X86_64_PC32就表示重定位一个使用32位PC相对地址的引用。.Sym.Name表示被修改引用应该指向的符号,Append用于一些类型的重定位要使用它对被修改引用的值做偏移调整。

④ 符号表:它存放在程序中定义和引用的函数和全局变量的信息

4.4 Hello.o的结果解析
机器语言程序的是二进制的机器指令序列集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数组成。汇编语言是以人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的较为通俗的比较容易理解的语言。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。机器语言与汇编语言具有一一对应的映射关系,一条机器语言程序对应一条汇编语言语句,但不同平台之间不可直接移植。
使用objdump -d -r hello.o > hello.obj 可以获得反汇编代码。

对比hello.s和反汇编代码,可以总结为以下几个方面的差别
① 操作数的进制:hello.s中是十进制;hello.obj中是十六进制
② 分支转移:hello.s中在跳转语句之后是.L2和.L3等段名称,hello.obj中是相对偏移的地址。
③ 函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行的地址,因此在.rela.text节中为其添加了重定位条目。
④ 对栈的使用:.s文件中对栈的使用有一定的浪费,反汇编代码的栈空间利用率较高。
⑤ 全局变量的访问:在hello.s中,对全局变量的访问是“段名称+%rip”,在hello.obj中是“0+%rip”

4.5 本章小结
本章介绍了hello.s到hello.o的汇编过程,查看了hello.o的elf格式,并对hello.s
和hello.o的区别进行了分析。还使用了objdump查看了反汇编的代码,在本章中,可以加深我们对汇编语言和机器语言的转化的理解

(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。在早期计算机系统中,链接是手动执行的,在现代系统中,链接是由叫做链接器的程序自动执行的。

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

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

该文件是可执行目标文件,有27个节,比未链接前的14个字节的大小扩大了

2.节头表

在节头表中,我们可以查看到各种节的具体信息,大小、偏移量、其中地址是程序被载入到虚拟地址的起始地址。

3.符号表

目标文件的符号表包含定位和重定位程序的符号定义和符号引用所需的信息。符号表索引是此数组的下标。索引 0 指定表中的第一项并用作未定义的符号索引。动态符号表 (.dynsym) 用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。其中 .symtab 段只保存函数名和变量名等基本的符号的地址和长度等信息。

4.程序头表

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。

这里我们发现,从程序虚拟地址的开始处是0x400000,右边对应的有ELF标识

代码段开始于0x401000的地方。

只读数据段开始于0x402000的地方。

与上面的程序头表相对应

5.5 链接的重定位过程分析

(以下格式自行编排,编辑时删除)
对hello进行反汇编得到hello.objdump文件

与hello.o的反汇编文件hello.obj进行对比:

  1. hello.obj中一开始就是.text,而在hello中添加了.init节,先是.init,而后才是.text,而且在hello.objdump中链接加入了在hello.c中用到的一些函数,比如exit、prinf等。
  2. hello.obj中跳转和函数调用的地址都是用了重定位条目,而在hello.objdump中是具体的虚拟地址,没有重定位条目。
  3. hello.obj中的相对偏移地址变成了hello.objdump中的虚拟内存地址。而hello.obj文件中对于.rodata的访问,是$0x0,是因为它的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

链接就是链接器ld将各个目标文件组装在一起,把.o文件中的各个函数段按照诸如解决符号依赖,库依赖关系,并生成可执行文件等规则组合在一起。

重定位过程

  1. 链接器在完成符号解析以后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。
  2. 合并输入模块,并为每个符号分配运行时的地址。首先是重定位节和符号定义,链接器将所有输入到hello.objdump中相同类型的节合并为同一类型的新的聚合节。
  3. 链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。
  4. 符号引用,链接器会修改hello.objdump中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址

5.6 hello的执行流程
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
libc-2.27.so!__cxa_atexit
hello!__libc_csu_init
libc-2.27.so!_setjmp
hello!_main
if内容(
hello!puts@plt
hello!exit@plt
)
hello!printf@plt
hello!sleep@plt
hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave
ld-2.27.so!_exit

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析

查看节头表我们可以知道.got.plt从0x404000开始,在0x404040结束。包含共享函数的共享块在运行时会根据函数运行的地址加载到任何地方。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。接下来我们用edb观察在dl_init的前后got.plt节发生的变化。
发生之前:

发生之后

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程,比较了链接前后的反汇编文件的不同,使用了edb对我们的程序进行细致的分析,提升了对链接这部分
内容的理解。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程(Process)是指计算机中已运行的程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。进程是程序真正运行的实例,若干进程可能与同一个程序相关,且每个进程皆可以同步或异步的方式独立运行。
作用:进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell字面理解就是个“壳”,它一个应用程序,是操作系统(内核)与用户之间的桥梁,为它们提供可以进行交互的界面,充当命令解释器的作用,将用户输入的命令翻译给系统执行。
处理流程:

  1. shell打印命令行提示符,等待用户输入
  2. 读取用户的输入
  3. 解析用户输入的命令
  4. 分析输入内容,如果第一个参数是一个内置的shell命令名,立即执行对应的操作,如果第一个参数是可执行目标文件,会创建子进程去执行程序
  5. 在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应
  6. 如果用户要求在后台运行该程序,那么shell返回到循环的顶部,等待下一个命令的执行
  7. shell会等待作业终止并回收
  8. 下一轮迭代开始

6.3 Hello的fork进程创建过程
父进程通过fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(包括代码、数据段、堆、共享库以及用户栈),子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父子进程最大的区别在于它们有不同的PID。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。

6.4 Hello的execve过程
int execve (const char *filename , const char *argv[] , const char *envp[]);

exceve函数在当前进程的上下文中加载并运行一个新程序。
exceve函数加载并运行可执行目标文件fielname,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,exceve才会返回到调用程序。exceve调用一次且从不返回。
在exceve加载了filename后,它调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数,其中的具体过程是将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,将控制传递给新程序的主函数
6.5 Hello的进程执行
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
处理器用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围,从而进行用户模式和内核模式的转变。没有设置模式位时,进程运行在用户模式中,它必须通过系统调用接口才可间接访问内核代码和数据;而设置模式位时,它运行在内核模式中,可以执行指令集中的任何指令,访问系统内存的任何位置。运行应用程序代码的进程初始时是在用户模式中的,进程从用户态切换到内核态的唯一方法就是通过诸如中断、故障、或者陷入系统调用这样的异常。异常发生时,控制传递到异常处理程序,处理器将模式由用户模式转变到内核模式,返回至应用程序代码时,又从内核模式转变到用户模式。
内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
在进程执行的某种时刻,内核可以决定抢占当前进程,并重新开始一个先前已经被抢占了的进程。这种决策就称为调度。内核调度一个新的进程运行后,它就抢占当前进程,并通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文3)将控制转移给这个新恢复的进程

6.6 hello的异常与信号处理
Linux信号如下

程序执行过程中可能出现四类异常:中断、陷阱、故障和终止。①中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。②陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。③故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。④终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
hello执行过程中会出现中断、终止的异常,中断会产生SIGTSTP信号,会停止直到下一个SIGCONT,终止会产生SIGINT信号,会终止。

  1. 正常运行

程序会正常运行,打印学号和姓名十次,之后按下回车,进程会结束。

  1. 不停乱按(包括回车)

不停的乱按并不会影响程序的输出,这是因为程序在执行时会阻塞外来操作产生的信号,到执行完之后,程序才会读取这些字符,当读到回车时,程序会自动结束。
3. Ctrl-Z

这样操作会引发中断异常,会发送SIGSTP信号到父进程,之后会运行信号处理程序,将程序挂起。
4. Ctrl-C

这同样也会导致中断异常,会使内核产生信号SIGINT,之后会运行信号处理程序,父进程会向子进程发送SIGKILL杀死子进程并回收。
5. Ctrl-Z后加各种命令
(1) ps

打印出来各进程的PID,看到挂起的进程。
(2) jobs

打印出来被挂起的进程组的JID。
(3) pstree

(4) fg

fg 1可以把之前挂在后台的程序调到前台来继续执行。

(5) kill

由ps我们可以查看到具体的进程号,使用“kill -9 -进程号”可以发送SIGKILL给进程从而杀死进程。
6.7本章小结
本章首先介绍了进程这个重要的概念,程序会使用fork和execve创建新的进程,还介绍了hello的进程管理,以及对shell中hello的运行,执行,异常和信号处理做出了详细的说明。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址: CPU生成的地址,包含在机器语言里用来指定一个操作数或者一条指令的地址。每一个逻辑地址都是由一个段和偏移量组成的,偏移量对于hello说就是hello.o里的相对偏移地址。
线性地址、虚拟地址:两者是同样的概念,是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址(虚拟地址)。
物理地址 :用于内存芯片级的单元寻址,这是内存单元的真正地址,与处理器和CPU连接的地址总线相对应,地址翻译会将hello的一个虚拟地址转化为物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理方式就是直接将逻辑地址转换成物理地址,一个逻辑地址由段选择符和段内偏移量组成。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
在段式管理的具体过程中:

  1. 看段选择描述符中的 T1 字段是 0 还是 1,可以知道当前要转换的是 GDT(全局段描述符)中的段,还是 LDT(局部段描述符)中的段,再根据指定的相应的寄存器,就可以得到其地址和大小,我们就有了一个数组了。
  2. 根据段选择符中的前 13 位,可以在这个数组中查找到对应的段描述符,
    这样就可以看到基地址。
    3.把基地址 Base+Offset,就是要转换的下一个阶段的物理地址。
    7.3 Hello的线性地址到物理地址的变换-页式管理
    Intel处理器从线性地址到物理地址的变换通过页式管理实现。虚拟页存在未分配、缓存、未缓存三状态。缓存的页会映射到物理页
    CPU 中的页式内存管理单元负责把线性地址转换为物理地址,把内存物理空间划分成大小相等的若干区域,一个区域称为一块。把逻辑地址空间划分为大小相等的若干页,页大小与块大小相等。通过页机制以及页表索引,可设置二级页表结构,第一级的页表是用于存放页表的基地址的页目录;第二级用于存放物理内存中页框的基地址。
    页表将虚拟页映射到物理页,页表条目由有效位和一个n位的地址字段组成。如果设置有效位说明该页已缓存,否则未缓存,在地址字段不为空的情况下指向虚拟页在磁盘上的起始地址。
    从虚拟地址到物理地址的翻译通过MMU(内存管理单元),它通过虚拟地址索引到对应的PTE,如果已缓存则命中,否则不命中,称为缺页。发生缺页时,MMU会选择一个牺牲页,在物理内存将之前缺页的虚拟内存对应的数据复制到它的位置,并更新页表,然后重新触发虚拟地址翻译事件。
    通过页表,MMU可以实现从虚拟地址到物理地址的映射。

CPU中的页表基址寄存器指向当前页表,n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个n- p位的虚拟页号(VPN)。
MMU利用VPN选择适当的PTE,然后将页表条目中的物理页号(PPN)与虚拟地址中的VPO串联起来,得到相应的物理地址。PPO和VPO是相同
当页面命中时,CPU硬件执行的步骤

(1) 处理器生成一个虚拟地址,并把它传送给MMU
(2) MMU生成PTE地址,并从高速缓存/主存请求得到它
(3) 高速缓存/主存向MMU返回PTE
(4) MMU构造物理地址,并把它传送给高速缓存/主存
(5) 高速缓存/主存返回所请求的数据字给处理器
页面命中完全由硬件处理,处理缺页要求硬件操作系统与内核合作完成。
7.4 TLB与四级页表支持下的VA到PA的变换

如图的页表翻译。首先四级页表中,每个四级页表进程都有他自己私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的pte全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中,采用多级页表是对内存的极大节约。只有经常使用的页表才会保存在内存中。第一级页表相当于第二级页表的索引,第二级页表相当于第三级页表的索引,第三级相当于第四级的索引,第四级页表映射到物理地址。
多级页表的不同之处就如上所述,其他过程与一级页表完全一样。对虚拟地址进行划分,假设共有48位,前36位为VPN,则根据VPN访问页表,根据页表判断相应地址的数据是否缓存,若缓存,可直接从页表中读出PPN,则VPO与PPN组成一个完整的物理地址,接下来便可访存。

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

如图是三级cache的缓存层次结构

物理地址的结构包括组索引位CI(倒数7-12位),使用它进行组索引,找到对应的组之后。假设我们的cache采用8路的块,匹配标记位CT(前40位)如果匹配成功且寻找到的块的有效位valid上的标志的值为1,则命中,根据数据偏移量CO(后6位)取出需要的数据然后进行返回。

如果没有数据被匹配成功或者匹配成功但是标志位是 1,这些都是不命中的情况,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存),并且将查找到的数据加载到cache里面。这时候我们面临一个替换谁的问题,一般我们会选择使用一种常见的简单替换策略,查询得到的数据之后,如果我们映射得到的组内已经有很多个空闲块,则直接在组内放置;否则组内都已经是有效块,产生了冲突,则我们会采用最近最少最少使用(lfu)的策略,然后确定将哪一个块替换作为牺牲块。
7.6 hello进程fork时的内存映射
当fork函数被新进程调用时,内核会为hello创建子进程,同时为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前代码共享进程的上下文中加载并自动运行一个新的代码共享程序,它可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但是它并没有自动创建一个新的代码共享进程。新的运行程序仍然在堆栈中拥有相同的区域pid。之后为新运行程序的用户共享代码、数据、bss和所有堆栈的区域结构创建新的共享区域和结构,这一步叫通过链接映射到新的私有代码共享区域,所有这些新的代码共享区域都可能是在运行时私有的、写时复制的。
它首先映射到一个共享的区域,hello这个程序与当前共享的对象libc.so链接,它可能是首先动态通过链接映射到这个代码共享程序上下文中的,然后再通过映射链接到用户虚拟地址和部分空间区域中的另一个共享代码区域内。为了设置一个新的程序计数器,execve函数要做的最后一件要做的事情就是自动设置当前代码共享进程上下文的一个程序计数器,使之成为指向所有代码共享区域的一个入口点(即_start函数)。

7.8 缺页故障与缺页中断处理
在指令请求一个虚拟地址时,MMU中查找页表,如果对于的物理地址没有存在主存内部,以至于我们必须要从磁盘中读出数据,这就是缺页故障(中断)。
在发生缺页中断之后,系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。具体的操作过程如下:
第1步:CPU生成一个虚拟地址,并把它传送给MMU.
第2步: 地址管理单元生成PTE地址,并从高速缓存/主存请求得到它.
第3步:高速缓存/主存向MMU返回PTE.
第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序.
第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘.
第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE.
第7步: 缺页处理程序返回地址到原来的缺页处理进程,再次对主存执行一些可能导致缺页的处理指令,cpu,然后将返回地址重新再次发送给处理程序mmu.因为程序中虚拟的页面现在已经完全缓存在了物理的虚拟内存中,所以处理程序会再次命中,主存将所请求字符串的返回地址发送给虚拟内存的处理器。

7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器(如malloc)获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器的类型包括显式分配器和隐式分配器。前者要求应用显式地释放任何已分配的块,例如C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.后者要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。在检测到已分配块不再被程序所使用时,就释放这个块。
动态内存管理的策略包括首次适配、下一次适配和最佳适配。首次适配会从头开始搜索空闲链表,选择第一个合适的空闲块。搜索时间与总块数(包括已分配和空闲块)成线性关系。会在靠近链表起始处留下小空闲块的"碎片"。下一次适配和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快,避免重复扫描那些无用块。最佳适配会查询链表,选择一个最好的空闲块,满足适配,且剩余最少空闲空间。它可以保证碎片最小,提高内存利用率。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,虚拟地址到物理地址的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。理解这些缓存方面的知识有助于我们能够编写出对高速缓存更加友好的程序代码以及一些对高速缓存的优化解决手段,加速我们的程序运行。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列,所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口:

  1. 打开文件。
    一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备.内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件.内核记录有关这个打开文件的所有信息.应用程序只需记住这个描述符.
  2. 描述符。
    Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) .头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值.
  3. 改变当前的文件位置。
    对于每个打开的文件,内核保持着一个文件位置k, 初始为0.这个文件位置是从文件开头起始的字节偏移量.应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K .
  4. 读写文件。
    一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n .给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件.在文件结尾处并没有明确的“EOF 符号” .类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k .
  5. 关闭文件。
    当应用完成了对文件的访问之后,它就通知内核关闭这个文件.作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中.无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.

Unix I/O 函数:

  1. int open(char* filename,int flags,mode_t mode) 。open 函数将 filename(文件名,含后缀)转换为一个文件描述符(C 中表现为指针),并且返回描述符数字。
  2. int close(fd)。fd 是需要关闭的文件的描述符(C 中表现为指针),close 返回操作结果。
  3. ssize_t read(int fd, void *buf, size_t n)。read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf .返回值-1表示一个错误,而返回值0 表示EOF.否则,返回值表示的是实际传送的字节数量。
  4. ssize_t write(int fd, const void *buf, size_t n)。
    write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置.图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出.返回:若成功则为写的字节数,若出错则为-1。
    8.3 printf的实现分析
    printf函数代码:
    int printf(const char fmt, …)
    {
    int i;
    char buf[256];
    va_list arg = (va_list)((char)(&fmt) + 4);
    i = vsprintf(buf, fmt, arg);
    write(buf, i);
    return i;
    }

vsprintsf函数如下:
int vsprintf(char *buf, const char fmt, va_list args)
{
char
p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != ‘%’) {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case ‘x’:
itoa(tmp, ((int)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case ‘s’:
break;
default:
break;
}
}
return (p - buf);
}

它的作用是将所有的参数内容格式化后存进buf,最后返回要打印出来的字符串的长度。
下一步就是调用write

write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
(int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。)
sys_call的实现:
sys_call:
call save//保存之前中断的进程的状态
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
(syscall将字符串中字节的ASCII码从寄存器复制到显存)
于是可以直到printf函数执行过程如下:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
8.4 getchar的实现分析
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(–n>=0)?(unsigned char)*bb++:EOF;
}
在用户敲击或者按键盘上面的按钮的时候,键盘接口获得一个键盘扫描码,这样会在此时同时产生一个中断的请求,这会调用键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区的内部。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回这个字符串。Getchar的大概思想是读取字符串的第一个字符之后再进行返回操作。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章通过介绍linux下IO设备的管理方法、hello中包含的函数所对应的unix I/O,大致了解了I/O接口及其工作方式,还有常见的printf函数和pritgetchar函数,同时也了解到了硬件设备的使用和管理的技术方法。
(第8章1分)
结论
hello所经历的过程:
① 使用C语言编写hello.c。
② 对hello.c预处理得到hello.i文件
③ 利用编译器把hello.i编译成hello.s汇编文件
④ 汇编器把hello.s翻译成二进制机器语言,可重定位目标文件hello.o
⑤ 链接器将hello.o和外部文件链接成可执行文件hello
⑥ 在shell输入命令后,父进程调用 fork 函数为 hello 创建子进程,然后在子程序中通过exceve加载并运行hello
⑦ 在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流
⑧ MMU 将程序中使用的虚拟内存地址,通过页表映射成物理地址。
⑨ hello在运行过程中会有异常和信号,如Ctrl+C,Ctrl+V等
⑩ printf会调用malloc通过动态内存分配器申请堆中的内存
⑪ shell父进程回收hello子进程,内核删除为hello创建的所有数据结构

深切感悟:hello程序从一开始的编写到最终的实现回收,其中经历了很多复杂的进程,需要很多详细的步骤,任何东西只要经过钻研,你都会发现的它的精妙和伟大。软件和硬件的交互发展促进了程序的的发展,一个小小的程序如今也可以有大大的世界。我们不也是这样吗,我们每个人也很渺小,但是我们每个人也有一个庞大的人生,HITer,在路上!
、(结论0分,缺失 -1分,根据内容酌情加分)

附件
hello.c :hello的c语言源代码
hello.i :经过预处理后的文本文件
hello.s :编译后的汇编文件
hello.o :汇编后的可重定位目标文件
hello.elf:hello.o的elf文件
hello.obj:hello.o的反汇编结果文件
hello :helllo.o与预编译文件链接后的可执行文件
hello.objdump:hello的反汇编文件
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 预处理_百度百科.https://baike.baidu.com/item/预处理/7833652?fr=aladdin
[2] 编译_百度百科.https://baike.baidu.com/item/编译/1258343?fr=aladdin
[3] atoi_百度百科.https://baike.baidu.com/item/atoi/10931331?fr=aladdin
[4] 汇编程序_百度百科.https://baike.baidu.com/item/汇编程序/298210?fromtitle=%E6%B1%87%E7%BC%96&fromid=627224&fr=aladdin
[5] 进程_百度百科.https://baike.baidu.com/item/进程
[6] shell(计算机壳层)_百度百科.
https://baike.baidu.com/item/shell/99702?fr=aladdin
[7]https://blog.csdn.net/WZJwzj123456/article/details/84201047?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242
[8] 深入理解计算机系统(课本)

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

猜你喜欢

转载自blog.csdn.net/m0_50906780/article/details/118182464