计算机系统大作业——程序人生-Hello’s P2P

目 录

第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:From Program to Process。Hello程序的生命周期从一个高级C语言程序开始,在预处理阶段,预处理器读取hello.c源程序中的头文件内容,并把它直接插入程序文本中,得到hello.i即修改后的源程序文本文件。在编译阶段,编译器(ccl)将文本文件hello.i翻译成由汇编语言构成的hello.s文件,称之为汇编程序。在汇编阶段,汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在hello.o中。这时,源程序已经转变为二进制文件,然后在连接阶段,链接器(ld)将hello程序中调用的函数(这里如printf函数)所在的可重定位目标程序文件与hello.o链接到一起,成为hello文件,这时hello已经从一个源程序被处理成了一个可执行目标文件,被加载入内存,由系统执行。在linux系统的shell(终端)输入./hello,shell认定他不是内核程序,是一个可执行文件,为其fork产生进程,然后hello就成为了一个进程。

图1-1 编译系统
020:From 0 to 0。刚开始程序不在内存空间中的时候,可以看作为0,当在shell中使用execve加载并执行该程序时,操作系统为程序分配一部分虚拟空间,将程序加载到虚拟空间所映射的物理内存空间中,然后执行目标程序。在进程终止后,shell回收hello进程,操作系统会释放进程的虚拟空间、相关数据结构,可以看作回归于0。
1.2 环境与工具
ubuntu64位 vmware虚拟机 edb gcc codeblocks gedit等
1.3 中间结果

中间结果文件 文件作用
hello.i 预处理得到的源程序文本文件
hello.s 编译得到的ASLL汇编语言文本文件
hello.o 汇编得到的二进制可重定位文件
hello.asm hello.o反汇编文件
hello.elf hello.o的ELF文件
hello 链接得到的可执行文件

1.4 本章小结
本章对hello的P2P,020的过程作了简要介绍,列出了运行的环境及使用的工具以及中间结果,是后面章节的整体概述。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
作用:预编译的主要作用如下:
1、将源文件中以”include”格式包含的文件复制到编译的源文件中。
2、用实际值替换用“#define”定义的字符串。
3、根据“#if”后面的条件决定需要编译的代码。
4、删除注释。
2.2在Ubuntu下预处理的命令
gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

图2-1 预处理命令
2.3 Hello的预处理结果解析
Hello.i文件相比于Hello.c文件内容大幅增多,但仍未C语言源程序,其中上述所说的#include头文件中包含的库已经复制到了文件中。

图2-2 预处理结果文件
2.4 本章小结
本章介绍了预处理的相关概念及其作用,通过在Ubuntu中对hello.c的预处理加深了对预处理过程的理解。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器将.i文本文件翻译成由汇编语言程序构成的.s文本文件的过程。
作用:进行词法分析、语法分析、目标代码的生成,检查无误后生成汇编语言。汇编语言为不同的编译器提供了通用的输出语言。

3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

图3-1 编译命令
3.3 Hello的编译结果解析
3.3.1指令内容及作用

图3-2 编译结果文件
.file 声明源文件
.text 声明数据段
.section .rodata 只读数据,rodata节
.string 声明一个字符串
.align 声明指令或数据的存放地址的对齐方式
.global 声明全局变量
.type 用来指定是函数类型或是对象类型
3.3.2 数据
全局变量:main
根据.globl main可知,main为该段的全局变量。由.type main, @function可知,main的类型为function。
局部变量:int i,int argc

图3-3 .L2

图3-4 argc参数传递
i在main函数中定义,被压入main的栈帧。argc作为参数传递给main,同样被压入main的栈帧。
指针数组变量:char *argv[]

图3-5 argv参数传递
argv[0]存放在%rbp-32中,argv[1]存放在%rbp-24,argv[2]存放在%rbp-16,argv[3]存放在%rbp-8中。
字符串常量:“用法: Hello 学号 姓名 秒数!\n” “Hello %s %s\n”

图3-6 字符串常量
此外,还有一些在汇编语言里能体现的立即数,但只在代码段里有意义,所以不作讨论。
3.3.3 操作

图3-6 汇编代码
赋值:通过mov类指令完成赋值操作,包括movb(一个字节)、movw(单字)、movl(双字)、movq(四字)。这里传送int型和地址,所以只用到了movl和movq。
运算:此段代码中只在for循环中用到了加操作,而一些其他的数据处理汇编语言都是选择用addq实现的,如计算指针数组元素地址等。
关系操作:这里用到了cmpl来处理关系操作。在条件语句argc != 3中的条件判断和for循环语句中判断循环条件时,使用了cmpl指令。
控制转移:这里出现的控制转移指令包括:je、jle、jmp。它们分别的作用是:je:通过cmp,如果!=4,跳转到.L2,执行printf;jmp:先将0赋值给i,跳转到.L3;jle:比较i与7,如果得到的结果是<=7,跳转到.L4。
函数操作:这里涉及的函数操作有:main、printf、exit、sleep、getchar、puts等函数,相应地使用了call来传递控制,使用赋值、关系操作等语句传递数据,ret从函数返回。main函数需要使用movl、movq传递参数,而将字符串传入%edi后才调用printf。对sleep、getchar等的调用同理。

图3-7函数操作(1)

图3-7函数操作(2)

图3-7函数操作(3)

3.4 本章小结
本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及C语言中各种类型和操作所对应的的汇编代码。这一阶段将.i文件处理成.s文件,使我们能够通过汇编语言代码了解机器处理C语言代码的流程。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在.o文件中的过程。
作用:把一个源程序通过五个阶段翻译成目标程序的工作过程:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

图4-1 汇编命令及结果文件
4.3 可重定位目标elf格式
readelf -h hello.o
获得ELF文件头信息。数据存储方式:补码,小端存储。ELF header 的大小:64字节。Section header的大小:64字节。字符串表索引节头是13。

图4-2 文件头信息
readelf -S hello.o
得到节头部表,这里包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。因为还没有链接,所以现在虚拟地址都是0,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小。

图4-3 节头部表

readelf -s hello.o
获取符号表信息.symtab,这里存储着程序中定义和引用的函数和全局变量的信息。name是符号名称,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

图4-4 符号表信息
readelf -r hello.o
获取重定位信息,这部分信息展现了文件中在链接阶段需要重定位的部分。有函数,有全局变量,代码,要输出的字符串等等的定位信息。

图4-5 重定位信息

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

图4-6 汇编与编译结果文件对比
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
很明显的区别:已经为汇编语言编写了相应的机器指令代码;消去了.L段的分别,而变成了具体的相对地址。
机器语言程序由二进制的机器指令序列集合构成,而机器指令由操作码和操作数组成。反汇编的代码相比hello.s,出现了机器代码,机器可以识别这些机器代码完成操作。这里的每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言。
分支转移:反汇编的跳转指令不再使用.L
的段名称,而是使用相对于main的地址,因为段名称是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后会变成确定的地址。

图4-7 分支转移
函数调用:call后面不再跟着要调用的函数名称,call的目标是当前下一条指令的地址。这是因为 hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0,然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

图4-8 函数调用
4.5 本章小结
本章对可重定位文件的ELF头、节头部表、符号表和可重定位节进行分析了解,比较了hello.s和汇编过程得到的hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系,从而加深了对汇编过程的理解。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念与作用:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。
5.2 在Ubuntu下链接的命令
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

图5-1 链接命令
5.3 可执行目标文件hello的格式
readelf -h hello
ELF文件头信息表示文件类型已经转变为可执行文件,很多标志如Size of program headers等也发生了变化。

图5-2 文件头信息
readelf -S hello

图5-3 节头部表信息
readelf -s hello
链接后符号表信息大幅增加,链接了许多共享库中的内容。

图5-4 符号表信息
readelf -r hello
重定位节信息

图5-5 重定位节信息
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
hello段地址如下:

图5-6
链接共享库的地址如下:

图5-7
堆栈等其他运行组件的地址如下:

图5-8
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
很明显的不同:hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程,此外,hello反汇编的代码中多了很多的节以及很多函数的汇编代码。

图5-9

图5-10
hello链接的过程:
链接时,同libc.so动态链接共享库构成联系,共享库中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,确定了.text与.plt节的相对距离,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt。
5.6 hello的执行流程
使用edb单步调试执行
_dl_start
_dl_init
__stat
_cax_atexit
_new_exitfn
_libc_start_main
_libc_csu_init
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
Exit

子程序名 地址
ld-3.31.so!_dl_start 7efbff4d8ea0
ld-3.31.so!_dl_init 7efbff4e7630
Hello!_start 400500
libc-3.31.so!_libc_start_main 7efbff100ab0
Hello!puts@plt 4004b0
Hello!exit@plt 4004e0

5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
根据hello ELF文件可知,GOT起始表位置为0x404000

图5-11
进入edb查看,发现GOT表内容由全0改变,可知连接成功,GOT表发生更改。在dl_init调用之后, GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址,GOT[2]指向动态链接器ld-linux.so运行时地址。

图5-12
5.8 本章小结
本章介绍了链接的概念和作用,分析了hello的ELF格式,虚拟地址空间的分配,重定位和执行过程还有动态链接的过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个正在执行的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
作用:进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell是个交互型应用及程序,代表用户运行其他命令,基本作用是解释并运行用户的指令。
处理流程:
(1)读取用户命令行输入
(2)分析命令行字符串,获取命令行参数,构造传递给execvedargv向量
(3)检查第一个命令行参数是否是一个内置的shell命令
(4)如果不是,用fork创建子程序
(5)子进程中,进行步骤(2)获得参数,调用exceve()执行制定程序
(6)命令行末尾没有&,代表前台作业,shell使用waitpid等待作业终止后返回
(7)命令行末尾有&,代表后台作业,shell返回
6.3 Hello的fork进程创建过程
在运行可执行程序hello时,shell将输入的命令解析,当shell检测到./hello不是一个内置命令的时候,就默认这是当前目录下的一个可执行文件,将hello视为父进程,并利用fork创建一个子进程给hello执行自己的程序。子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本,包括代码和数据段、堆、共享库以及用户栈,但是得到与父进程不同的PID。在hello执行期间,父进程等待hello结束,利用wait函数进行回收。

图6-1 fork进程与hello
6.4 Hello的execve过程
子进程调用Execve加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以与fork调用一次返回两次不一样,execve调用一次并且从不返回。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段(这些内容被放到磁盘里)。新的栈和堆段被初始化为零,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程的时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文。2)恢复新恢复进程被保存的上下文。3)将控制传递给这个新恢复的进程 。来完成上下文切换。
hello在用户模式执行完printf之后,调用sleep进入内核模式,内核什么都不做等待sleep结束,hello从运行队列进入等待队列,由内核统一调度进行上下文切换,安排别的进程运行。sleep结束后,进行上下文切换,定时器发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程继续进行控制逻辑流。

图6-2
6.6 hello的异常与信号处理
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。
执行过程中键盘按Ctrl+C,hello程序终止。

图6-3
执行过程中键盘按Ctrl+Z,hello程序终止。但hello进程并没有回收,而是运行在后台下。

图6-4
执行过程中键盘乱按,只是将屏幕的输入缓存到stdin,当getchar的时候读出一个换行符结尾的字串,其他字串会当做shell命令行输入

图6-5

6.7本章小结
本章分析了hello的进程管理过程,包括fork创建子进程,execve加载hello,hello在用户模式和内核模式之间转换运行,可能遇到的异常与异常控制流的处理机制。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是程序运行由CPU产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有唯一的物理地址。虚拟地址通过页表的方式通过MMU映射到物理地址中。
虚拟地址:虚拟地址就是线性地址。
线性地址:分段机制下CPU寻址是二维的地址即,段地址:偏移地址,CPU不可能认识二维地址,因此需要转化成一维地址即,段地址×16+偏移地址,这样得到的地址便是线性地址(在未开启分页机制的情况下也是物理地址)。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理(segmentation),是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
逻辑地址到线性地址:
线性地址是由逻辑地址加上隐含的DS 数据段的基地址而得到。也就是说,逻辑地址就是在DS中的偏移量。线性地址的基本组成方式是段号+段内偏移地址。
首先给定一个完整的逻辑地址[段选择符:段内偏移地址],接下来:
1.看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。
2.拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。
3.把基地址Base+Offset,就是要转换的下一个阶段的地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
在这个转换中要用到翻译后备缓冲器(TLB),首先我们先将线性地址分为VPN(虚拟页号)+VPO(虚拟页偏移)的形式,然后再将VPN拆分成TLBT(TLB标记)+TLBI(TLB索引)然后去TLB缓存里找所对应的PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到PPN之后,将其与VPO组合变为PPN+VPO就是生成的物理地址了。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB可以理解为页表的一个小的,虚拟寻址的高速缓存(类似cache是内存的一个缓存),用于组选择和行匹配的索引和标记字段是从虚拟地址的页号中提取出来的,如下图所示,如果TLB有2t组,TLB索引(TLBI)由VPN的t个低位组成的,TLB标记(TLBG)是由VPN中剩余的位组成的。对于TLBT和TLBI,如果在TLB中找到对应的PPN,则直接使用PPN+VPO。但是,如果缺少页面,则必须转到页面表进行查找。每个条目的范围越小,地址就越准确。在四个地址级别之后,就会找到合适的PPN并将其连接到VPO。

图7-1 VA结构
具体过程如下:CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN与VPO组合成PA。如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

7.5 三级Cache支持下的物理内存访问
根据物理地址VA,使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO(后六位)取出数据返回。如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。

图7-2 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
当shell调用fork函数时,内核为新进程创建一个不同的数据结构,并为其分配一个pid。要为进程分配虚拟内存,必须创建mm u struct(一个描述进程总虚拟内存的内存描述符),区域结构和页表的原样副本,并将这两个进程的每个页面都标记为只读。
7.7 hello进程execve时的内存映射
execve加载并运行hello:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve设置当前进程上下文的程序计数器,使之指向代码区域的入口点
7.8 缺页故障与缺页中断处理
处理缺页要求硬件和操作系统内核协作完成。处理器生成一个虚拟地址,并把它传送给MMU;MMU生成PTE地址,并从高速缓存/主存请求得到它;高速缓存/主存向MMU返回PTE;PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序;缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘;缺页处理程序页面调人新的页面,并更新内存中的PTE;缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将地址重新发送给MMU。因为虚拟页面现在已经缓存在物理内存中,所以会命中,主存将所请求字返回给处理器。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicit allocator),要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
显式分配器必须在严格的约束条件下工作,约束有:必须处理任意请求序列;立即响应请求;只使用堆;对齐块;不修改已分配的块。分配器的编写应该实现:吞吐率最大化;内存使用率最大化(两者相互冲突)。
在分配器的具体实现中,主要有以下几种实现方法:
(1)隐式空闲链表
隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块,要求对空闲链表进行搜索,该搜索时间与堆中已分配块和空闲块的总数呈线性关系。

图7-3 隐式空闲链表
(2)带边界标记的隐式空闲链表
这种方式可以允许在常数时间进行对前面块的合并,并且它对许多不同类型的分配器和空闲链表组织都是通用的。然而它也存在一个潜在的缺陷。它要求每个块都保持一个头部和一个脚部,在应用程序操作许多个小块时,会产生显著的内存开销。

图7-4 带边界标记的隐式空闲链表
(3)显式空闲链表
堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。这样一来,会使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。显式空闲链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在地提高了内部碎片的程度。

图7-5 显示空闲链表
7.10本章小结
本章主要介绍Hello的存储地址空间、Intel的段管理模式、Hello的页面管理、VA到PA的转换、物理内存访问、缺页处理方法、动态内存分配。hello运行的过程涉及到了地址变换,TLB,页表等多种操作,还要处理缺页的异常,还调用了动态内存分配,利用堆和显示分配器来完成自己的printf功能。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
IO指的是输入输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。简而言之,从硬盘中读写数据或者从网络上收发数据,都属于IO行为。
设备的模型化:文件
1.普通文件:包含任意数据的文件
2.目录:包含一组链接的文件,每个链接都将一个文件名映射到一个文件
3.套接字(socket):用来与另一个进程进行跨网络通信的文件
4.命名通道、符号链接、字符和块设备
设备管理:unix io接口:主要功能是打开和关闭文件、读取和写入文件以及更改当前文件的位置。
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)关闭文件
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数
(1)打开文件
函数原型:int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。flags参数指明了进程打算如何访问这个文件,mode参数则指定了新文件的访问权限位。
(2)关闭文件
函数原型:int close(int fd);
关闭描述符为fd的文件,关闭一个已关闭的描述符会出错。
(3)读和写文件
函数原型:
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。返回值:成功则返回写的字节数,出错则为-1。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
1.printf函数的实现

图8-1 printf函数
printf函数中,定义了一个字符指针va_list类型的arg,这个函数调用了vsprintf。

图8-2 vsprintf函数
函数主要实现的功能:格式化。接受确定输出格式的字符串fmt,勇哥是字符串对个数变化参数进行格式化,产生格式化输出。
2.系统函数write:反汇编write函数,发现要调用INT_VECTOR_SYS_CALL,他通过系统调用sys_call这个函数。再来看sys_call这个函数,通过分析,知道这个函数的主要功能是显示格式化的字符串,将要输出的字符串从总线复制到显卡的显存里面
3.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
4.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。我们要显示的“hello 1190301610 王家琪”就被打印输出到了显示器上。
8.4 getchar的实现分析
1.运行getchar函数的时候,用户模式转变为内核模式,输入时,显示屏上面有显示并进入缓存。按回车,通知内核完成,在切换为用户模式。
2.异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
3.getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO的基本概念和管理机制,介绍了打开和关闭读写文件的接口及相关函数,分析了printf和getChar函数的实现细节
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。

  1. hello.c经过预处理,生成hello.i文本文件,加入头文件。
  2. hello.i经过编译,生成了hello.s的汇编语言文本文件。
  3. hello.s经过汇编,生成了hello.o的二进制文件,一种可重定位目标文件。
  4. hello.o经过链接,生成了可执行文件hello,这个可以直接被计算机执行。Bash进程调用fork函数创建一个子进程,子进程execute可执行文件hello。
  5. hello的文件一开始是放在磁盘里面的,执行过程中通过将虚拟地址映射到物理地址,通过不断地访问内存执行hello可执行文件。
  6. hello可能调用I/O函数,这部分与linuxI/O系统函数密切相关
  7. 最后hello执行完,被父进程回收,内核回收清除他的信息
    你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
    首先是扩展了我的认知,从前只知道写代码,却不知道小小的计算机里居然蕴藏着这么多的结构、组织,一个程序的执行居然经过了这么多复杂的映射、处理、变换过程。我明白了C语言源代码是怎么一步一步成功set、debug、run的。这也使我对从前毫不在意的一些代码编写方式、编译环境配置更加重视了起来。
    再者,觉得CSAPP越学越多。直到所有步骤都学完了,才对可执行程序的流程彻底有所了解。不然总是一块懂,一块不懂,觉得前后联系不上,最后才明白计算机系统的系统性有多强烈。
    (结论0分,缺失 -1分,根据内容酌情加分)

附件
中间结果文件 文件作用
hello.i 预处理得到的源程序文本文件
hello.s 编译得到的ASLL汇编语言文本文件
hello.o 汇编得到的二进制可重定位文件
hello.asm hello.o反汇编文件
hello.elf hello.o的ELF文件
hello 链接得到的可执行文件

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值