Hello的一生

Hello的一生

关键词:Hello,预处理,编译,汇编,链接,进程管理,存储管理,IO管理。

摘要:
我历史长河中一个个菜鸟与我擦肩而过,只有CS知道我的生、我的死,我的坎坷,“只有 CS 知道……我曾经……来…………过……”————未来一首关于Hello的歌曲绕梁千日不绝 !!
这篇论文,就将详尽的分析hello程序从.c到执行完毕这个过程中的每一步,包括:预处理,编译,汇编,链接,进程管理,存储管理,IO管理几个部分。hello程序虽然简单,虽然只是我们最开始的程序,但是它也具有着代表性,几乎所有的程序的运行都与hello相似,或者说建立在hello运行的方式之上,理解hello的处理过程,有助于我们深入理解计算机系统的组成部分,理解计算机执行一个进程的过程中都进行了什么操作。
通过这个大作业,我又一遍复习了计算机系统这门学科,并更加深入地理解了计算机组成原理,懂得了计算机执行一个进程的过程中具体都进行了什么操作。这个大作业使我受益匪浅。

目 录

第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的缩写。
O2O是Zero to Zero的缩写。
接下来就简略的介绍一下这两个过程。

P2P:

首先我们在各种编译器中写下hello的代码,就完成了hello.c,接下来我们需要运行它,也就是将program变成process。以下是这个过程:
1.可执行文件的生成:
在这里插入图片描述
图1.1.1 hello.c生成hello的过程
hello.c经过预处理、编译、汇编、连接,会生成可执行目标程序hello。
2.运行可执行文件:
在bash(壳)中,输入./hello,shell逐一读取文字,并存至内存,然后OS(进程管理)为其fork,新建子进程。
之后为其调用execve,将可执行文件装入内核的linux_binprm结构体。进程调用exec时,该进程执行的程序完全被替换,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段。
之后为其mmap,使进程之间通过映射同一个普通文件实现共享内存。hello被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问。
最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。然后程序从内存读取指令字节,然后再从寄存器读入最多两个数,然后在执行阶段算术/逻辑单元要么执行指令指明的操作,计算内存引用的有效地址要么增加或者减少栈指针。然后在流水线化的系统中,待执行的程序被分解成几个阶段,每个阶段完成指令执行的一部分。最后变成一个Process运行在内存中。
最后为其分时间片。时间片通常很短(在Linux上为5ms-800ms),用来运行hello这个进程。

O2O:
为了有效使用主存,简化内存管理,独立地址空间,我们使用了虚拟地址。为了实现从虚拟地址到物理地址的快速翻译,我们采用了:TLB、4级页表、3级cache、Pagefile等多个方法为其加速。
在进程结束之后,OS、Bash对其进行回收,shell又回到调用hello之前的状态,称其从Zero回到Zero。

1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
VMware Workstation Pro
codeblocks
Windows 10
Ubuntu 64位
gredit
edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
如图1.1,我们可以看到所有中间结果文件。
hello.i(修改了的源程序):
预处理阶段实现的功能主要有三个:1.加载头文件2.进行宏替换3.条件编译
hello.s(汇编程序):包含汇编语言程序。
hello.o(可重定位目标程序):将汇编语言翻译成机器语言指令,并将指令打包成一种叫做可重定位目标程序的格式。

1.4 本章小结
简述了Hello的一生。
一个程序被编写出来到生成可执行文件,然后可执行文件执行,结束后系统回收,完成其一生。这个过程有着普适性,大部分程序都会经历以上几个步骤。

第2章 预处理
2.1 预处理的概念与作用
概念:
预处理pre-treatment,是指在进行最后加工完善以前进行的准备过程
即在编译之前进行的处理。 C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3.条件编译。预处理命令以符号“#”开头。

作用:
言预处理程序的作用是根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。
理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。

2.2在Ubuntu下预处理的命令

在这里插入图片描述
图2.2.1 gcc编译生成hello.i

2.3 Hello的预处理结果解析在这里插入图片描述
图2.3.1 hello.i内容
在这里插入图片描述
图2.3.2 hello.c与hello.i的比较
通过指令gedit hello.i我们可以打开hello.i查看其中的内容。
由ANSI标准的定义,该预处理程序应该处理以下指令:
#if
#ifdef
#ifndef
#else #elif
#endif
#define
#undef
#line
#error
#pragma
#include
显然,上述所有的12个预处理指令都以符号#开始,每条预处理指令必须独占一行。我们都可以从hello.i中寻找到。
更主要的是:
#include<stdio.h>、#include<unistd.h>、#include<stdlib.h>等头文件包含的文件被插入到该预编译指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。
通过对比,我们可以看到.i文件中删除了.c中的注释,并且插入了.c文件的头文件。
2.4 本章小结
本章详细介绍了hello预处理的阶段。由于hello.i的介绍在网上介绍的资料太少,只能结合自己的分析,尽量的写出这些:
.cw文件头文件也会又外部文件,还有一些宏定义和注释、一些条件编译和完善程序文本文件操作都需要预处理来实现。
预处理可以使程序在后边的操作中不受阻碍,是整个过程中非常重要的一步。

第3章 编译
3.1 编译的概念与作用

编译程序的作用是将高级语言源程序翻译成目标程序。
编译程序(Compiler,compiling program)也称为编译器,是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的翻译程序。编译程序属于采用生成性实现途径实现的翻译程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。编译出的目标程序通常还要经历运行阶段,以便在运行程序的支持下运行,加工初始数据,算出所需的计算结果。

3.2 在Ubuntu下编译的命令

在这里插入图片描述
3.2.1 gcc编译生成hello.s
3.3 Hello的编译结果解析
3.3.1数据:
变量int sleepsecs:
在这里插入图片描述
3.3.1.1 int sleepsecs的处理
3.3.2 赋值:
sleepsecs = 2.5:
在这里插入图片描述
3.3.2.1 sleepsecs = 2.5的处理
i = 0:
在这里插入图片描述
3.3.2.2 i = 0的处理
3.3.3 算术操作:
i++:
在这里插入图片描述
3.3.3.1 i++的处理

3.3.4 类型转换:
前边赋值sleepsecs = 2.5就包括了一个类型转换。因为sleepsecs是int,2.5为浮点型。

3.3.5 比较:
i<10:
在这里插入图片描述
3.3.5.1 i<10的处理
argc != 3:
在这里插入图片描述
3.3.5.2 argc != 3的处理

3.3.6 数组/指针/结构体:

在这里插入图片描述
3.3.6.1 数组的处理
3.3.7 控制转移:
if(argc != 3):
在这里插入图片描述
3.3.7.2 控制转移的处理(a)
for中的循环判断:
在这里插入图片描述
3.3.7.1 控制转移的处理(b)

3.3.8 函数操作:
第一个printf:
在这里插入图片描述
3.3.8.1 第一个printf处理
第二个printf:
在这里插入图片描述
3.3.8.2 第二个printf处理
sleep:
在这里插入图片描述
3.3.8.3 sleep的处理

3.4 本章小结
汇编语言是直接面向处理器的程序设计语言。每种处理器都有着自己可以识别的一套指令,称为指令集。处理器执行指令时,根据不同的指令采取不同的动作,完成不同的功能。
汇编语言另一个特点是其操作的对象不是具体的数据,而是寄存器或者储存器。
再者,汇编语言指令是及其指令的一种符号表示,不同类型的CPU有不同的及其指令系统,也就有不同的汇编语言。
汇编语言是各种编程语言中与硬件关系最密切、最直接的一种,在空间和时间上的效率也最高。
本章介绍了汇编操作的具体概念、作用,以及对其结果进行了解析。

第4章 汇编
4.1 汇编的概念与作用

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

作用:汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,可以根据对照表一一对应进行翻译。
4.2 在Ubuntu下汇编的命令
在这里插入图片描述
图4.2.1 汇编命令

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1.ELF Header:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。

在这里插入图片描述
图4.3.1 ELF头信息

    1. Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

在这里插入图片描述
图4.3.2节头信息
3. 重定位节.rela.text ,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。如图4.4.3,图中8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、sleepsecs、sleep函数、getchar函数进行重定位声明。
其中信息的前四位是symbol中的偏移量,后四位是type中的偏移量。
在这里插入图片描述
图4.3.3 重定义节的信息
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
在这里插入图片描述
4.4.1 hello.o反汇编和hello.s的比较
1.全局变量访问:
对于hello.s,全局变量的访问方式为:段名称+%rip,而对于hello.o的反汇编为0+%rip,因为rodata的数据地址是在运行时确定的,故也需要重定位,所以现在是0,并添加了重定位条目。
2.分支转移:
对于hello.s,跳转时是跳转到某个段名称,而再hello.o的反汇编中,已经变成了真正的地址。
3.函数调用:
对于hello.s,调用时直接跟着函数名称,而对于hello.o的反汇编,因为函数是要link的时候才能生成地址的,所以现在的call后的相对地址都设置为0,即call的目标地址为下一条语句的地址。

4.5 本章小结
本章介绍了hello从hello.s到hello.o的汇编过程,通过查看hello.o的elf格式和使用objdump得到反汇编代码与hello.s进行比较的方式,间接了解到从汇编语言映射到机器语言汇编器的过程中都进行了什么操作,以及其两者的异同。

第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程。链接器使得分离编译成为可能。链接操作最重要的步骤就是将函数库中相应的代码组合到目标文件中。
5.2 在Ubuntu下链接的命令
在这里插入图片描述
图5.2.1 链接命令
5.3 可执行目标文件hello的格式
对readelf中节头部分进行分析:
在ELF格式文件中,
节头对hello中所有的节信息进行了声明,其中包括大小以及在程序中的偏移量,因此根据节头中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中地址是程序被载入到虚拟地址的起始地址。

在这里插入图片描述
图5.3.1 hello节头信息
在这里插入图片描述
图5.3.2 承接上图
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在这里插入图片描述
图5.4.1 edb加载后与5.3对比说明
5.5 链接的重定位过程分析
hello相对于hello.o有如下不同:
1.hello相对hello.o多了很多的节类似于.init,.plt等
2.hello.o中的相对偏移地址到了hello中变成了虚拟内存地址
3.hello中相对hello.o增加了许多的外部链接来的函数。
4.hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。

在这里插入图片描述
图5.5.1 hello和hello.o的一些差异
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。
然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。
然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。
当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
5.6 hello的执行流程

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

程序名称 程序地址
ld-2.27.so!_start 0x7fd7:8dfda090
ld-2.27.so!_dl_start 0x7fd7:8dfdaea0
ld-2.27.so!_dl_start_user 0x7fd7:8dfda09b
ld-2.27.so!_dl_init 0x7fd7:8dfe9630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fd7:8dc09ab0
-libc-2.27.so!__cxa_atexit 0x7fd7:8dc2b430
-libc-2.27.so!__libc_csu_init 0x4005c0
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fd7:8dc26c10
-libc-2.27.so!_sigsetjmp 0x7fd7:8dc28e2b
–libc-2.27.so!__sigjmp_save 0x7fd7:8dc2db30
hello!main 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0
*hello!printf@plt –
*hello!sleep@plt –
*hello!getchar@plt –
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fd7:8dc324d0
-ld-2.27.so!_dl_fixup 0x7fd7:8dc34520
–ld-2.27.so!_dl_lookup_symbol_x 0x7fd7:8dd24dc0
libc-2.27.so!exit 0x7fd7:8dd32fd0
图5.6.1 执行流程

5.7 Hello的动态链接分析
这里注明:因为每次运行程序,ld的地址都不同,所以截图中地址和5.6.1中的地址会稍有区别。

在这里插入图片描述
图5.7.1 调用dl_init之前的全局偏移表
在这里插入图片描述
图5.7.2 调用dl_init之后的全局偏移表
在这里插入图片描述
图5.7.3 查找方法
具体这里进行了什么操作,我们可以看下图,主要看变化对比,GOT[3]从指向下一条指令变成了具体的地址,这儿就是发生变化的原因:

在这里插入图片描述
图5.7.3 变化分析
在这里插入图片描述
图5.7.4 接上图
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
在这里插入图片描述
图6.2.1 shell-bash的处理流程
作用:
Shell是一个程序,提供了一个界面,用户通过这个界面访问操作系统内核的服务。

6.3 Hello的fork进程创建过程
在这里插入图片描述
图6.3.1 fork进程创建
在终端Terminal中键入 ./hello 1170300110 lizhenyu,运行的终端程序会读入这个字符串,然后对这个字符串进行解析,因为hello不是一个内置的命令所以解析之后终端程序判断执行hello,之后终端程序首先会调用fork函数创建一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,父进程与子进程之间最大的区别在于它们拥有不同的PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

6.4 Hello的execve过程
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。
新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。
最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
在这里插入图片描述
图6.4.1 创建的系统映像
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
开始时由6.4内容已经知道:加载器设置PC指向_start地址,_start最终调用hello中的main函数。
通过逻辑控制流,在不同的进程间切换。分配给某个进程的时间就叫做进程的时间片。上下文信息即重新启动一个被抢夺的进程的条件。用户态中,程序不允许执行一些特权功能,而核心态中可以,它们之间需要某些条件才能切换。
对hello进行简单的分析:
在这里插入图片描述
图6.5.1 对hello进程执行的简单分析

6.6 hello的异常与信号处理

在这里插入图片描述
图6.6.1 正常执行

在这里插入图片描述
图6.6.2 ctrl+c

在这里插入图片描述
图6.6.3 ctrl+z

在这里插入图片描述
图6.6.4 ctrl+z后ps

在这里插入图片描述
图6.6.5 ctrl+z后jobs
在这里插入图片描述
图6.6.6 ctrl+z后pstree
在这里插入图片描述
图6.6.7 ctrl+z后fg并停止
在这里插入图片描述
图6.6.8 ctrl+z后kill再ps
在这里插入图片描述
图6.6.9 执行中乱按

6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址是程序代码经过编译之后出现在汇编程序中的地址,由选择符和偏移量组成。
线性地址是逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的形式。
虚拟地址:其实就是这里的线性地址。
在这里插入图片描述
图7.1.1 逻辑地址、线性地址、物理地址的关系
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由16位字段组成,前13位为索引号。
索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前13位,再这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。
段描述符中的base字段,描述了段开始的线性地址,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由T1字段决定使用哪个。
以下是具体的转化步骤:

  1. 给定一个完整的逻辑地址。
  2. 看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小。
  3. 取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址。
  4. 线性地址等于基地址加偏移。

7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分以固定长度为单位的组,成为页。
一个机器会有很多个页,为了索引到这些不同的页,我们可以得到一个大叔组,称为也目录,页目录中的每一个项都是一个指向不同页的地址。
物理页与内存页一一对应。
为了减少空间的占用,引入了一个二级管理模式的机器来组织分类单元。
如图:
在这里插入图片描述
图7.3.1 二级管理模式
转化步骤:

  1. 从cr3中去除目录地址,操作系统再调度进程的时候,把这个地址装入对应寄存器。
  2. 根据线性地址前十位,在数组中,找到对应索引项,因为引入二级管理,所以页目录中的项,不再是页的地址,而是一个页表的地址,页的地址再这个页表中。
  3. 根据线性地址中间的十位,再页表中找到页的起始地址(基址+偏移)。
  4. 将页的起始地址与线性地址中的后12位相加,得到物理地址。

在这里插入图片描述
7.4 TLB与四级页表支持下的VA到PA的变换

图7.4.1 页表翻译
如图给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的VPN划分为4个9位的片,每个片对应一个页表的偏移量。CR3寄存器存有L1页表的物理地址。VPN1提供一个到L1PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2PET的偏移量,以此类推。
通过四级页表,我们可以查找到PPN,与VPO组合成PA,或者说VPO直接对应了PPO,它们组成PA并且向TLB中添加条目。

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

CPU发出一个虚拟地址再TLB里搜索,如果命中,直接发送到L1cache里,如果没有命中,就现在也表里加载到之后再发送过去,到了L1中,寻找物理地址又要检测是否命中,如果没有命中,就向L2/L3中查找。这就用到了CPU高速缓存,这种机制加上TLB可以是的机器再翻译地址的时候性能得以充分发挥。

在这里插入图片描述
图7.5.1 三级cache访存(a)
在这里插入图片描述
图7.5.2 三级cache访存(b)

7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。并且创建hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
execve函数执行了以下几个操作:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
在这里插入图片描述
图7.7.1 execve加载

7.8 缺页故障与缺页中断处理
在虚拟内存中,DRAM缓存不命中称为缺页。
在这里插入图片描述
图7.8.1 缺页异常
如图,引用VP3时发现其不再DRAM中,除法缺页异常,调用异常处理程序,选择VP4作为牺牲页,修改VP4的页表条目。
在这里插入图片描述
图7.8.2 故障处理
如上图,进行故障处理。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。

配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它被应用所分配。一个已分配的块保持已分配状态,直到它被释放。

基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有三种分别是:首次适配,下一次适配和最佳适配。首次适配是从开始处往后搜索,下一次适配是从上一次适配发生处开始搜索,最佳适配依次检查所有块,性能要比首次适配和下一次适配都要高。

策略:分为隐式空闲链表和显示空闲链表。任何实际的分配器都需要一些数据结构,允许他来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。
隐式空闲链表:通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。
在这里插入图片描述
图7.9.1 隐式空闲链表
显示空闲链表:因为根据定义,程序不需要一个空闲块地主题,所以实现这个数据结构地指针可以存放在这些空闲块的主体里。
在这里插入图片描述
图7.9.2 显示空闲链表

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。详尽的介绍了与hello的存储管理有关的内容。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口

所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
Unix I/O接口:

1.打开文件。一个应用程序通过要求内核打开相应的文件,通知它想要访间一个I/O 设备。内核返回一个小的非负整数,它在后续对此文件的所有操作中标识这个文件。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。

Unix I/O函数:

1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:

int open(char *filename, int flags, mode_t mode);

open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。

返回:若成功则为新文件描述符,若出错为-1。

2.进程通过调用close 函数关闭一个打开的文件。

int close(int fd);

返回:若成功则为0, 若出错则为-1。

3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。

ssize_t read(int fd, void *buf, size_t n);

read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。

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

ssize_t write(int fd, const void *buf, size_t n);

write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。

返回:若成功则为写的字节数,若出错则为-1。

8.3 printf的实现分析
已经给出,不作赘述:
https://www.cnblogs.com/pianist/p/3315801.html
从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.c的诞生。
  2. 预处理,初步处理hello.c将外部库合并到hello.i文件中。
  3. 编译,将hello.i编译成hello.s
  4. 汇编,将hello.s汇编成hello.o
  5. 链接,将hello.o与可重定位目标文件以及动态链接库链接称为可执行程序hello
  6. 运行,在shell输入./hello 1170300110 lzy运行
  7. 创建子进程,shell调用fork
  8. 运行程序,shell调用execve
  9. 执行指令,CPU为hello分配时间片,hello在一个时间片中执行自己的逻辑控制流。
  10. 访问内存,MMU将虚拟内存映射成物理地址
  11. 动态内存申请,malloc
  12. 信号,如果遇到ctrl+c或ctrl+z,则分别停止、挂起
  13. 结束,shell父进程回收子进程。

真实的听到了Hello之歌,并流下了赶ddl的泪水。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
hello.i:预处理生成的文件。

hello.s:编译生成的文件。

hello.o:汇编生成的可重定位目标文件。

hello:链接生成的可执行文件。

hello.txt:存有hello.o的反汇编内容。

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://blog.csdn.net/qq_44242536/article/details/85231624.
HIT 2018 CS:APP Hello的一生 大作业 小明难亡
[2] https://blog.csdn.net/hahalidaxin/article/details/85144974
HIT CSAPP 2018 大作业 程序人生 Hello’s P2P hahalidaxin
[3] 深入理解计算机系统。
[4] http://www.baidu.com
百度一下

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值