ICS2022大作业报告

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

题目 程序人生-Hello’s P2P
专业 计算机学院
学号 120L020408
班级 2003004
学 生 曾敏航
指 导 教 师 史先俊

计算机科学与技术学院
2022年5月
摘 要
本文主要阐述hello程序在Linux系统的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程;并结合课本的知识详细阐述我们的计算机系统是如何对hello进行进程管理、存储管理和I/O管理,通过对hello一生周期的探索,让我们对计算机系统有更深的了解。
关键词:Hello程序;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O ;

目 录

第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简介
“P2P”意为Program to process,其中program 是执行任务的命令。为创建可执行程序, 需要经过 cpp 预处理、 ccl 编译、 as 汇编和 ld 链接的步骤 。 Process是执行模块, hello.c转换为Hello, 实现process。
"020(zero to zero)”意为将程序从无到有,最终再到无的所有过程。shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果在这里插入图片描述

1.4 本章小结
本章主要介绍了hello程序的性质,何为P2P,020;介绍了编译运行的环境和工具;大致列出了hello程序从C程序到可执行目标文件的大致中间过程
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号,FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
命令: gcc hello.c –E –o hello.i
在这里插入图片描述

2.3 Hello的预处理结果解析
经过预处理之后,hello.c转化为一个3060行的hello.i文件,打开该文件可以发现,文件的内容增加,且仍为可以阅读的C语言程序文本文件。对原文件进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。
在这里插入图片描述
原hello.c文件:
在这里插入图片描述
2.4 本章小结
本章介绍了预处理相关概念以及其所进行的一些操作,如宏展开、头文件的引入等等
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译(compilation , compile)的概念:
1、利用编译程序从源语言编写的源程序产生目标程序的过程。
2、用编译程序产生目标程序的动作。
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译的作用:
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析
在这里插入图片描述
file:声明源文件
.text:声明代码节
.rodata:声明只读代码段
.align:声明数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型

3.3.1数据
1.字符串:
程序中有两个字符串LC0与LC1,都存放在只读区域,作为之后printf函数的参数
在这里插入图片描述

2.局部变量i
Main函数定义了一个局部变量i作为循环控制,在编译过程中i会被放入栈中,在此程序中i被放入栈上-4(%rbp)的位置
在这里插入图片描述
3.main函数
参数 argc 作为用户传给main的参数,也被放到了栈中
在这里插入图片描述
4.立即数immediate
hello.c函数中的第二个参数是一个数组,数组中的每个元素都是一个指向字符类型的指针;数组起始位置位于栈-32(%rbp)的位置,在for循环中被调用参数传给printf函数
在这里插入图片描述
3.3.2全局函数
hello.c中定义了一个全局函数int main(int argc,char *argv[]),其中两个参数分别为一个整型数和一个数组,经过编译之后,main函数使用的字符串常量也被存放在数据区

3.3.3赋值操作
在程序中第一了局部变量i并且对其赋值,使用的是mov指令实现,而mov指令又可以根据数据的类型有几种不同的后缀如:
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节

3.3.4算数操作
hello.c中用到的算数操作有i++,还有其余的算出操作汇编代码如下:
在这里插入图片描述
3.3.5关系操作
(1)argc!=4 是一条条件判断语句中的条件判断,使用了汇编语言中的cmp操作。同时这条cmp指令还设置了条件码,根据条件码来判断是否需要跳转
在这里插入图片描述
(2)i<8 在for循环中作为循环判断条件,在汇编语言中同样也是使用了cmp指令,设置条件码作为jle指令跳转的条件
在这里插入图片描述
3.3.6控制转移指令
在汇编语言中首先设置条件码,作为控制转移的条件,在hello.c中用到了如下指令:
(1) argc!=4 判断argc是否等于4;不等于4则执行if语句,否则就不执行if语句,如图
在这里插入图片描述
(2) i<8 作为for循环中的循环判断条件,在每次循环时先判断i是否小于8,若满足则执行循环体中的内容,如图;其中L3为循环判断部分,跳转到L4为循环体内部

在这里插入图片描述
3.3.7函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
1.传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
2.传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向 P中返回一个值。
3.分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.C涉及的函数操作有:
main函数中printf,exit,sleep ,getchar函数
main函数的参数是argc和数组argv[ ];两次printf函数的参数恰好是那两个字符串
exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。

3.3.8类型转换
在hello.c中涉及的类型转换有:atoi(argc[ ]),将字符串类型转换为整型
其他类型的转换还有int float double short char等
3.4 本章小结
本章主要介绍了编译阶段中编译器如何处理各种数据和操作,以及C语言中各种类型数据和操作对应的汇编代码,通过学习该部分的内容我们可以在C语言和汇编语言之间进行转换

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
把汇编语言翻译成机器语言的过程称为汇编
汇编的作用:
汇编程序(AS)把hello转换成机器语言指令,将这些指令打包成一种叫做可伸缩目标程序的格式,并将结果保存到二进制目标文件Hello.o.。
4.2 在Ubuntu下汇编的命令
指令:gcc hello.s –c –o hello.o
在这里插入图片描述
4.3 可重定位目标elf格式
(1)ELF Header: 指令readelf -h hello.o
ELF Header以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息
在这里插入图片描述
(2)Section Headers:指令 readelf -S hello.o
Section Headers意为节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
在这里插入图片描述
(3)查看符号表.symtab:命令readelf -s hello.o
.symtab:存放程序中定义和引用的函数和全局变量的信息。Num是符号的编号;name是符号名称;对于可重定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址;size是目标的大小;type是类型,为数据或是函数;Bind字段表明符号是本地的还是全局的
在这里插入图片描述
(4)重定位节.rela.text:命令readelf -r hello.o
重定位节.rela.text定义:
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置
重定位节.rela.text中各项符号的信息:
Offset偏移量:需要被修改的引用节的偏移Info:包括Info信息和type类型两个部分,symbol在前面四个字节,type在后面四个字节;
Info信息:表示被修改引用应该指向的符号
Type类型:告知连接器应该如何修改新的应用
Name符号名称:重定位到的目标名称
Adden加数:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整
在这里插入图片描述
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
运行指令所得反汇编代码如下:
在这里插入图片描述
hello.s截图如下:
在这里插入图片描述
通过反汇编代码与hello.s比较,发现汇编语言的指令与hello.s中的代码大同小异,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码;机器语言程序是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。

1.分支转移:反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
2.函数调用:在hello.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章中对hello.s文件进行了汇编,生成了hello.o可重新定位目标文件,并且分析了可重新定位文件的ELF Header、Section Headers、符号表等,最后还对hello.s与反汇编代码进行了比较,得到了汇编语言到机器语言一一映射的关系
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程。这个文件可被加载(拷贝)到内存中并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的
链接的作用:
使得分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是把它分解成更小、更好管理的模块。可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,我们只要单独地编译它,并将它重新链接到应用上,而不用编译其他文件。
5.2 在Ubuntu下链接的命令
在这里插入图片描述

5.3 可执行目标文件hello的格式
(1)ELF Header: 指令readelf -h hello
hello与hello.o的ELF Header有两个不同之处:Type类型显示EXEC表明hello是一个可执行目标文件,Number of section headers也从原来14变成了27
在这里插入图片描述
(2)Section Headers:指令 readelf -S hello
Section Headers 对 hello中所有的节信息进行了声明,其中包括大小Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小);其中 Address 是程序被载入到虚拟地址的起始地址
在这里插入图片描述
(3)查看符号表.symtab:命令readelf -s hello
在这里插入图片描述
(4)重定位节.rela.text:命令readelf -r hello
在这里插入图片描述
5.4 hello的虚拟地址空间
通过查看edb,看出hello的虚拟地址空间开始于0x400000
在这里插入图片描述
5.5 链接的重定位过程分析
hello.o的反汇编代码如下:
在这里插入图片描述在这里插入图片描述
hello的反汇编代码如下:命令objdump -d -r hello
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
hello与hello.o主要有以下的不同:
1.链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。
2.增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。
3.函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。hello的反汇编代码中导入了puts、printf、atoi、getchar、sleep等在主程序中使用过的函数。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata和sleepsecs等全局变量的访问,是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

根据hello和hello.o的不同,分析出链接的过程为:链接就是链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段按照一定规则累积在一起,比如规则:解决符号依赖,库依赖关系,并生成可执行文件。

5.6 hello的执行流程
hello调用与跳转的各个子程序名或程序地址如下:
0x401000 _init
0x401020 .plt
0x401090 puts@plt
0x4010a0 printf@plt
0x4010b0 getchar@plt
0x4010c0 atoi@plt
0x4010d0 exit@plt
0x4010e0 sleep@plt
0x4010f0 _start
0x401120 _dl_relocate_static_pie
0x401125 main
0x4011c0 _libc_csu_init
0x401230 _libc_csu_fini
0x401238 _fini

5.7 Hello的动态链接分析
在对hello的readelf分析中得知,.got表的地址为0x0000000000403ff0
通过edb中对Data Dump窗口跳转,定位到GOT表处
得到do_init前的GOT表:
在这里插入图片描述
用edb执行do_init后的GOT表:
在这里插入图片描述

在开始edb的调试后,初始的地址0x00403ff0全为0。对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行地址,所以需要添加重定位记录,等待动态链接器的处理,为避免运行时修改调用的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。
5.8 本章小结
本章主要理解了Ubuntu下链接的过程,链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。通过查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,更好地掌握了链接尤其是重定位的过程,但是我们知道链接并不止于此,hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中,至此hello终于诞生了

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。

进程的作用:进程为用户提供了以下假象:
1.我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
2.处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)
其基本功能是解释并运行用户的指令,重复如下处理过程:
1.终端进程读取用户由键盘输入的命令行
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork( )创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回
6.3 Hello的fork进程创建过程
进程,包括代码、数据和分配给进程的资源。fork函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(int argc , char **argv , char *envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
在这里插入图片描述

6.5 Hello的进程执行
进程提供给应用程序的抽象:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占地使用处理器
2.一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用CPU内存。
hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:
①逻辑控制流::一系列程序计数器 PC的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
②并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
⑤用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
⑥CPU上下文:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
⑦上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文
3)将控制传递给这个新恢复的进程,来完成上下文切换
现在我们再来看一下hello进程执行,再进程调用execve函数之后,由上面分析可知,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.txt和.data节分配虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出hello 120L020408 曾敏航,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。如图:
在这里插入图片描述
6.6 hello的异常与信号处理
异常和信号异常可以分为四类:中断、陷阱、故障、终止
在这里插入图片描述

具体来说有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出
在这里插入图片描述
下面演示程序运行时的各种可能情况:
(1)运行程序时键盘无输入;程序运行完之后进程被回收,键入回车结束在这里插入图片描述
(2)运行过程中键入ctrl+c;父进程收到SIGINT信号,种植hello进程并回收
在这里插入图片描述
(3)运行过程中乱按键盘发现乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入
在这里插入图片描述
(4)运行过程中键入ctrl+z,父进程收到SIGTSTP信号,将hello进程挂起,再执行ps/jobs/pstree等命令
ps命令:列出当前系统中包括僵死进程的所有进程
在这里插入图片描述
jobs命令:列出当前shell环境中已经启动的任务状态
在这里插入图片描述
pstree命令:以树状图显示进程之间的关系
在这里插入图片描述
6.7本章小结
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用execve函数执行hello,hello的进程执行,以及hello的异常与信号处理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:
逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:
一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成:段标识符、 段内偏移量。
段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器
在这里插入图片描述
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成:Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
在这里插入图片描述
将逻辑地址转换为相应的线性地址的一般步骤(如图):
给定一个完整的逻辑地址[段选择符:段内偏移地址],
1.看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了
2.拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了
3.把Base + offset,就是要转换的线性地址了

在这里插入图片描述
7.3 Hello的线性地址到物理地址的变换-页式管理
由课本知识点可知,线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。

使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。

虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小时相同的。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页

每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在次级的起始地址。
在这里插入图片描述
将虚拟地址转换为物理地址的一般步骤:
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令

在这里插入图片描述

7.4 TLB与四级页表支持下的VA到PA的变换
前提:虚拟地址空间 48 位,物理地址空间 52 位,页表大小4KB,4级页表。TLB 4路16组相联。CR3 指向第一级页表的起始位置(上下文一部分)。解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以VPO 12位;因为 TLB 共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。

CPU 产生虚拟地址VA,VA 传送给MMU,MMU使用前36位 VPN作为 TLB(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向TLB中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误
![在这里插入图片描述](https://img-blog.csdnimg.cn/c80ba558a4a04534860041a41856883a.png
7.5 三级Cache支持下的物理内存访问
讨论Cashe1的物理内存访问,Cashe2,Cashe3原理相同。

由于L1Cashe有64组,所以组索引位s为6,每组有8个高速缓存行,由于每个块的大小为64B,所以块偏移为为6,因此标记位为52-6-6=40位。
因此L1Cashe的物理访存大致过程如下:
1.组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组
2.行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
3.字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可
4.如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换
在这里插入图片描述
7.6 hello进程fork时的内存映射
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。Fork函数为新进程创建虚拟内存。创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。在新进程中返回时,新进程拥有与调用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.8 缺页故障与缺页中断处理
缺页故障:
当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成
在这里插入图片描述
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器(如malloc)获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器的类型包括显式分配器和隐式分配器。前者要求应用显式地释放任何已分配的块,后者在检测到已分配块不再被程序所使用时,就释放这个块。
动态内存管理的策略包括首次适配、下一次适配和最佳适配。首次适配会从头开始搜索空闲链表,选择第一个合适的空闲块。搜索时间与总块数(包括已分配和空闲块)成线性关系。会在靠近链表起始处留下小空闲块的“碎片”。下一次适配和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快,避免重复扫描那些无用块。最佳适配会查询链表,选择一个最好的空闲块,满足适配,且剩余最少空闲空间。它可以保证碎片最小,提高内存利用率。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:

设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息
2.Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。 3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置k
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置 k 开始,然后将 k增加到k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k开始,然后更新k
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去

Unix I/O 函数:
1.int open(char* filename,int flags,mode_t mode),进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访 问这个文件,mode参数指定了新文件的访问权限位
2.int close(fd),fd是需要关闭的文件的描述符,close返回操作结果
3.ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为fd的当前文件位置赋值最多 n个字节到内存位置buf。返回值-1 表示一个错误,0 表示EOF,否则返回值表示实际传送的字节数量
4.ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置 buf复制至多n个字节到描述符为 fd 的当前文件位置
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
结论
hello所经历的过程:
1.hello被I/O设备编写,以文件的方式储存在主存中。
2.hello.c被预处理hello.i文件
3.hello.i被编译为hello.s汇编文件
4.hello.s被汇编成可重定位目标文件hello.o
5.链接器将hello.o和外部文件链接成可执行文件hello
6.在shell输入命令后,通过exceve加载并运行hello
7.在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流
8.hello的VA通过TLB和页表翻译为PA
9.三级cache 支持下的hello物理地址访问
10.hello在运行过程中会有异常和信号等
11.printf会调用malloc通过动态内存分配器申请堆中的内存
12.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构

感悟:
本实验贯穿了几乎CSAPP全书,让我对全书的结构有了一一个更加具体的认知,同时也认识到了自己在学习本书时还存在诸多漏洞,之后的学习任务还任重道远

附件
hello.c :hello源代码

hello.i :预处理后的文本文件

hello.s :hello.i编译后的汇编文件

hello.o :hello.s汇编后的可重定位目标文件

hello_objdump :hello的反汇编代码

hello.o_objdump :hello.o的反汇编代码

hello :链接后的可执行文件

参考文献
[1]逻辑地址到线性地址的转换
https://blog.csdn.net/xuwq2015/article/details/48572421
[2] LINUX 逻辑地址、线性地址、物理地址和虚拟地址转换
https://www.cnblogs.com/zengkefu/p/5452792.html
[3] 深入了解计算机系统(第三版)2016 Bryant,R.E. 机械工业出版社
[4] printf 函数实现的深入剖析
https://blog.csdn.net/zhengqijun_/article/details/72454714
[5] 百度百科 getchar计算机语言函数
https://baike.baidu.com/item/getchar/919709?fr=aladdin

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值