经过这学期计算机系统的学习,我可以说是从一无所知的门外汉真正迈入了计算机专业的大门,这学期的课程让我对计算机的组成原理和操作系统有了初步的认识和理解。当然《深入理解计算机系统》这本书我还没有真正读透,希望我以后还能坚持读下去,继续学下去。
下面的大作业算是对自己本学期所学内容,付出的时间、精力的一份总结吧,当然如有错误,欢迎大家指正。
摘 要
本文通过计算机系统的术语从头介绍了预处理,编译,汇编,链接,hello的进程管理……等过程,描述了一个c语言程序从创建到结束的历程,此过程也是这学期课程的展开顺序,通过本文,可以对这学期所学知识进行描述与总结。
关键词:CSAPP;计算机系统;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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P过程:
作为用户的我们首先需要按照语法规则编写hello.c,当我们想要运行hello.c文件时,hello.c文件得首先经过预处理,编译,汇编,链接4个阶段从hello.c文本文件转变为可执行文件,这样我们的机器才能运行这个文件。
以linux系统为例,当我们在终端运行可执行文件时,shell解析这段命令,使用fork为我们的hello创建子进程,至此完成了从program到progress的过程。
020过程:
当程序完成p2p过程之后,就进入了020过程
在此过程中, shell 为我们的程序调用 execve函数,execve函数启动加载器为该进程分配独立的虚拟内存空间,程序运行在物理内存中,CPU 为其分配时间片执行指令,调用系统I/O,printf函数显示相应的功能。当程序运行结束后,shell 父进程负责回收 hello 进程。
至此,020过程也告一段落。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
环境:Ubuntu19.04,windows10
工具:gedit,edb,code::blocks17.12
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i hello.c文件经过预处理的结果,仍是文本文件。
hello.s hello.i文件经过编译得到的结果。
hello.o hello.s经过汇编得到的可重定位目标文件。
hello hello经过连接后得到的可执行文件。
readelf1 对hello.o使用readelf的结果
readelf2 对hello使用readelf的结果
disass1 对hello.o反汇编的结果
disass2 对hello反汇编的结果
1.4 本章小结
本章根据hello的自白,用计算机系统的相关术语解释并描绘了p2p,020两个过程。介绍了本次大作业的一些基本信息。
第2章 预处理
2.1 预处理的概念与作用
预处理是指编译系统(预处理器,编译器,汇编器和连接器),读取源程序,并把它翻译成一个可执行目标文件的整个过程的第一步,由预处理器完成。
预处理阶段(cpp)根据以字符‘#’开头的命令,比如文件包含–#include <文件名>,宏定义—#define PI 3.1415926或者是我们目前不太常用的条件编译—#ifdef 的用法,来修改原始的c程序,得到的文件依旧是文本文件,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
gcc -m64 –no-pie –fno-PIC –E hello.c –o hello.i
图2.1–预处理的命令
2.3 Hello的预处理结果解析
图2.2–hello.i文件的内容
图2.3—同上
通过上面两张图我们可以看出,预处理阶段把.c变成.i文件的过程中,保留我们在.c文件中代码的同时,根据我们引用的头文件,为该文本文件添加了很多相关头文件的内容,包括各种变量的定义,函数的声明,结构体的声明等等,原来.c文件中的内容也包含在其中。经过预处理阶段以后,原来短短几行的.c文件被扩充到了很多.
2.4 本章小结
预处理是一份.c源程序到转化为最终的可执行文件中的第一步,也是非常重要的一步,有了预处理我们才能通过包含头文件来调用很多系统函数,这对我们的编程非常有用。
第3章 编译
3.1 编译的概念与作用
在编译这个阶段中,gcc编译器首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器将文本文件hello.i翻译成文本文件hello.s,这份文本文件包含了一个汇编语言程序。这是一种比较低级的机器语言指令,汇编语言为高级语言的编译器提供了相同的输出语言。
3.2 在Ubuntu下编译的命令
gcc -m64 –no-pie –fno-PIC –S hello.i –o hello.s
图3.1–编译的命令
3.3 Hello的编译结果解析
3.3.1
首先我们可以观察到在对应的.s文件中,在汇编语句中间还出现了很多如.cfi_endproc ,.cfi_startproc, .cfi_def_cfa_register 6等等类似这些的命令,这些命令前面都有个关键字.cfi ,它是Call Frame infromation的意思,如图:
图3.2–hello.s中的.cfi命令
其中,.cfi_startproc 用在每个函数的开始,用于初始化一些内部数据结构
.cfi_endproc 在函数结束的时候使用与.cfi_startproc相配套使用
这是据常见的类似命令,还有一些命令的定义如下:
.cfi_def_cfa_register
6
.cfi_def_cfa_register
register
.cfi_def_cfa_register modifies a rule for computing CFA.
From now on register will be used instead of the old one. Offset remains the
same.
这条语句就规定了对寄存器的一些说明。
(详细解释见http://sourceware.org/binutils/docs/as/CFI-directives.html,这里提供了很多cfi指令的详细信息)
3.3.2
局部变量:
在本例中,hello.c中定义了一个整型的局部变量i
图3.3—hello.s中main函数的分析
在编译过程中,局部变量被储存在堆栈中,在稍后在for循环中我们可以看到对局部变量i赋初值为0。
图3.4
.L2部分对应for循环为i赋值的语句,i=0。这里可以看到整型变量占4个字节,储存在堆栈中。
3.3.3
for循环:
图3.5—hello.s中for循环的分析
在这里我们可以看到条件判断还是用cmp语句来执行,然后根据标志寄存器来查看是否满足循环继续的条件。若满足条件则跳转到.L4执行for循环的内部指令:
(.L4是for循环内部语句对应的部分)
图3.6
3.3.4
main函数的参数argc,argv
main函数的两个参数均储存在栈中。
图3.7-hello.s中main函数参数的分析
在x86-64下,参数由寄存器传递,我们可以看到argc存放在%rbp-20的位置。argv存放在%rbp-32的位置。
3.3.5
调用函数
在hello.c内部除main函数外,我们还调用了exit,printf,atoi,getchar等系统的库函数。
在hello.s文件中,函数调用是使用call语句直接实现的,而对应参数的传递是借助寄存器如%rdi,%rsi等寄存器实现的。
图3.2-图3.6均对函数调用有所体现,这里就不再重复赘述了。
3.3.6
分支结构:
C语言实现:
图3.8
—hello.s中分支结构的实现
汇编语言实现:
图3.9
分支结构-在本例中为if语句,常采用比较的方法来判断是否满足对应的条件。
若不满足条件则使用jmp及其变形如je,jne,jl等语句,借助标志寄存器的值进行比较和跳转;否则直接执行接下来的语句。
3.3.7
只读字符串
在hello.c中的printf语句中存在只读字符串 “用法: Hello 学号 姓名 秒数!”
图3.10(可以看到puts的参数存放在.LC0中)
图3.11—hello.s中对字符串的处理
由上方图片我们可以看到只读字符串在hello.s文件中以.LC0的形式存储。
同理,另一个在printf中出现的只读字符串存储在.LC1中。(见下图)
图3.12–同3.11
3.4 本章小结
本章主要介绍了编译器在将hello.i文件转化为hello.s文件的过程中,对hello.c中的变量,数据,操作进行了怎样的处理,使之变成了更底层的汇编语言。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将我们之前生成的hello.s汇编文件翻译成机器语言指令,并把这些指令打包成可重定位目标文件的格式,并将结果保存在hello.o中。.o文件时一种二进制文件。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图4.1-汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
hello.o是可重定位目标文件,其elf格式如下:
图4.2–hello.o的ELF格式
使用readelf -a hello.o可以得到elf表的大致信息:
图4.3
重定位项目分析:
图4.4
rel.text包含.text 节中需要进行重定 位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置
我们可以先来看一下elf文件的重定位条目的格式:
typedef struct{
long offset;
long type:32;
long symbol32;
long attend;
}Elf64_Rela
(见教材P479)
其中:
offset(偏移量):是需要进行重定向的代码在.text 或.data 节中的偏移位置,8个字节。
attend(加数):是计算重定位位置的辅助信息。
info(信息):包括 symbol 和 type 两部分, 其中 symbol 占前 4 个字节, type 占后 4 个字节,symbol 代表重定位到的目标在.symtab(符号表)中的偏移量,type 代表重定位的类型。
name(符号名称):指重定位到的目标的名称。
4.4 Hello.o的结果解析
使用objdump -d –r hello.o的命令:
图4.5
使用objdump –d
–r hello.o得到的结果(部分)见上图
上述由hello.o得到的反汇编代码与第三章我们得到的hello.s进行对比我们可以看到有以下不同:
(1)
首先我们能看到在只读数据的使用上,反汇编得到的结果是先用$0x0代替,因为将来需要重定位,我们目前无法判断其将来的具体地址,在代替的同时,反汇编代码还在这行代码下方标注了一些重定位信息。
而经过在第三章的分析我们可以看到在hello.s中,我们是使用.LC0,.LC1来代替汇编代码中只读变量的出现。
(2)
在函数调用时:
在hello.s中我们可以看到,在调用函数时,.s文件是直接用 call 函数名
这样的方式工作。
而在hello.o得到的反汇编代码中我们可以观察到对函数的调用在call之后用的是对应命令在main函数中的偏移量而不是具体的函数名,其中的原因与上方提到的对只读数据的引用所作的处理相同。
4.5 本章小结
当程序进行到汇编阶段时,我们的hello.c终于由文本文件变成了二进制文件,离最终运行更近了一步。汇编器将我们之前得到的hello.s转化成了机器语言,我们更接近了程序实现的本质,我们所用的高级程序语言实现的种种功能终将转化一系列的0,1字符串。
第5章 链接
5.1 链接的概念与作用
链接是指像hello.o这样的可重定位文件与别的可执行文件或像C语言函数库这样的静态库按照一定的格式结合起来生成可执行目标文件的过程。
链接替我们程序员减少了很大的工作量,我们可以直接调用库函数而不用去亲自构造,编写对应函数的具体实现。链接还可以让我们的程序有更好的模块化设计,而且能做到允许每个模块分开编译,这在我们构造大型程序时是非常有用的。
图5.1
5.3 可执行目标文件hello的格式
图5.2
可以看到可执行文件的elf文件与可重定位文件的elf文件非常相似。
使用readelf获取可执行文件每节的信息:
图5.3,图5.4----hello中各段的信息
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图5.5
可以看到由于使用了-fno-PIC选项,所以虚拟地址是从0x00400000开始的
在图5.3中我们可以看到.text节从0x401090开始:
对应地址的内容由edb查看可知:
图5.6
与通过对hello可执行文件反汇编得到的结果进行对比:
图5.7
我们可以看到反汇编结果中.text节的机器代码(31 ed 49 89 d1……)与edb中使用Data Dump查看对应地址的内容完全一致。(见图5.6,图5.7)
可执行文件hello中的其他节也可以通过与.text节类似的处理方法在反汇编中查看,得到的结果与edb中查看其对应地址处内存中的内容之间进行比较,结果应该相同。这里就不重复赘述了。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
(1)
第一个不同之处是在汇编这一章,我们经过objdump –d –r hello.o的命令只会得到text节的反汇编
图5.8
而在经过链接之后的hello中,除了text节以外还有很多其他的部分:
图5.9
(2)
在经过连接之后,hello.o中标注了重定位信息的部分均被具体的地址,数据所代替:
比如说在hello.o得到的反汇编中有这样的信息
图5.10
而在hello的相同部分则对应下图:
图5.11
可以看到图5.9中的寻址模式在经过链接的可执行文件中变成了具体的函数和数据。
(3)
除了我们在hello.c文件中完成的部份外,hello在反汇编之后多出了很多我们没有实现的函数:
图512–图5.14
(4)
在hello.o的反汇编中,函数地址从0开始,以main函数为例,函数中语句的地址都是相对main函数起始位置的偏移量,而可执行文件hello对应的语句有了对应虚拟内存的地址。
此变化从上部分的图5.10和5.11的比较过程就能看出,这里不在赘述。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址
图5.15
使用edb打开可执行文件hello时,程序所在的位置。
接下来会调用地址为0x7f6c22e07030的函数。
图5.16
第二个调用0x7f6c22e159e0处的函数.。
图5.17
第三个调用<libc-2.29.so!_libc_start_main>
之后就进入main函数,按照hello.c的步骤运行程序。
在main函数结束之后,使用同样的方法一步步跟踪edb中调用的函数可以得到在main函数结束之后,hello还调用了如下的函数:
ld-2.29.so!_dl_runtime_resolve_xsave
ld-2.29.so!_dl_fixup
ld-2.29.so!_dl_lookup_symbol_x
libc-2.29.so!exit
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明
对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所 以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代
码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表 PLT+全局偏移量 表 GOT 实现函数的动态链接,GOT 中存放函数的目标地址,PLT 中存放跳转到 GOT 中目标地址的代码逻辑
图5.18
通过之前使用readelf时得到的可执行文件hello各节的相关信息,我们可以得到.got和.got.plt的地址。
在Data Dump中查看对应地址的内容:
图5.19
可以看到在_dl_init之前,对应地址处内容为空
而在Data Dump之后,对应地址内容不再为空,而是根据延迟绑定的策略调整对应的内容。
比如程序调用了共享库中的函数printf,对应的会进入printf函数对应的plt表中,找到对应的条目,通过GOT表进行间接跳转,将printf的ID压入栈中,跳转到PLT[0]。由PLT[0]通过GOT间接地把动态链接器的一个参数压入栈中,再简介跳转进动态链接器,由动态链接器使用两个栈条目来确定puts的运行时位置。
5.8 本章小结
链接是完成hello的一生中向可执行文件转换的最后一步,这一步涉及很多概念和过程,要搞懂它并不容易。
第6章 hello进程管理
6.1 进程的概念与作用
进程可以被定义为执行中程序的一个实例,系统的每个程序都运行在某个进程的上下文中。
通过进程这种概念,操作系统为我们提供了我们当前运行程序独占处理器和内存的感觉。
6.2 简述壳Shell-bash的作用与处理流程
shell是操作系统内核常驻内存的部分,其可以被看作一个由C语言编写的终端程序。
shell从命令行接收到一行命令。
shell分析该行输入的命令,如果是系统内置命令就直接执行;
如果该命令不是内置命令,就是用fork创建子进程来执行该命令。
shell还会区分该命令是前台的指令还是后台的指令,如果是后台命令,shell在创建子进程之后直接返回;如果该命令是前台的,shell就需要调用waitpid等待该进程结束。
shell还负责接受来自键盘的一些信息,比如我们按下ctrl+c,ctr+z…这些都是shell的处理对象。
6.3 Hello的fork进程创建过程
当我们在终端运行之前得到的可执行文件,使用./hello的命令,shell会对输入的命令行进行解析,因为 hello 不是一个内置的shell 命令所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件 hello,之后终端程序首先会调用 fork 函数创建一个新的运行的子进程。
6.4 Hello的execve过程
子进程调用 execve 函数在当前进程的上下文中加载并运行一个新程序即 hello 程序。加载器删除子进程现有的虚拟内存段, 并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后设置PC指向程序入口点,完成对可执行程序的execve加载过程。
6.5 Hello的进程执行
在介绍hello的进程之前我们需要先回忆一些概念:
逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行以下过程 1)保存以前进程的上下文 2)恢复新恢复进程被保存的上下文,3)将控制传 递给这个新恢复的进程 ,来完成上下文切换。
再回忆一些与进程有关的基本概念之后,我们来简单看 hello sleep 进程调度的过程:
当调用 sleep 之前,如果hello 程序的时间片没有结束,hello进程不会被抢占则顺序执行。
如果hello进程的时间片在调用sleep函数之前就已经结束,则需要进行上下文切换。
开始时由于我们再shell中运行hello,所以hello 初始运行在用户模式,在 hello 进程调用 sleep 之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换并将当前进程的控制权交给其他进程.
当定时器计数到我们作为参数传入的暂停秒数之后会发送一个中断信号,此时进入内核状态, 内核处理信号,将 hello 进程从等待队列中移出重新加入到运行队列,恢复之前保存的hello的上下文,hello 进程就可以继续进行自己的控制逻辑流了。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs
pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
图6.1
在运行hello过程中乱按键盘时会在终端正常显示。
图6.2
按回车的效果就是正常换行。
图6.3
按Ctrl+c的效果是hello会终止。当按下 ctrl-c 之 后,shell 父进程收到 SIGINT 信号,信号处理函数的逻辑是结束 hello,并回收 hello 进程。
图6.4
按下Ctrl+Z时hello会终止,
图6.5
在图6.4所示步骤之后,按下ps可以看到hello进程未被回收,只是被挂起。
图6.6
使用jobs命令可以看到hello的状态是停止状态。
图6.7
可以看到使用fg命令之后,hello进程又重新开始在前台运行。
图6.8
使用pstree后的情况。
图6.9
使用kill命令杀死子进程。
6.7 本章小结
在本章中,阐明了进程的定义与作用,介绍了 Shell 的一般处理流程,调用 fork 创建新进程,调用 execve 执行 hello,hello 的进程执行,hello 的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:CPU 通过地址总线的寻址,找到真实的物理内存对应地址。 CPU 对内存的访问是通过连接着 CPU 和北桥芯片的前端总线来完成的。在前端总线上 传输的内存地址都是物理内存地址
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符 (在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏 移部分)组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合 形式。分页机制中线性地址作为输入
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段功能在实模式和保护模式下有所不同。
实模式,即不设防,也就是说逻辑地址=线性地址=实际的物理地址。段寄存 器存放真实段基址,同时给出 32 位地址偏移量,则可以访问真实物理内存。
保护模式下,线性地址还需要经过分页机制才能够得到物理地址,线性地 址也需要逻辑地址通过段机制来得到。段寄存器无法放下 32 位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。描述符表中的一个 条目描述一个段。
图7.1–段描述符的构造。
图7.2—段选择符的构造。
7.3 Hello的线性地址到物理地址的变换-页式管理
这部分是csapp这本书讲述的重点:
线性地址(书里的虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制 完成。而分页机制是对虚拟地址内存空间进行分页。而linux系统组织虚拟内存的结构如下:
图7.3
系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传输的单元,虚拟地址分为虚拟页号 VPN 和虚拟页偏移量 VPO,根据位数限制分析可以确定 VPN 和 VPO 分别占多少位是多少。
通过页表基址 寄存器 PTBR+VPN 在页表中获得条目 PTE,一条 PTE 中包含有效位、权限信息、 物理页号。
如果有效位是 0+NULL 则代表没有在虚拟内存空间中分配该内存。
如果是有效位 0+非 NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中。
如果有效位是 1 则代表该内存已经缓存在了物理内存中,可以得到其物 理页号 PPN,与虚拟页偏移量共同构成物理地址 PA。
图7.4可以反应对应的地址翻译。
7.4 TLB与四级页表支持下的VA到PA的变换
在使用TLB和四级页表的情况下
VA可分为以下几个部分:
VPN,VPO,TLBI,TLBT;
PA可以分为一下几个部分:
PPN,PPN,CO,CI,CT
根据当前当前使用页的大小我们可以确定VPO的位数,再根据虚拟地址的位数,我们就能得到VPN的位数。
首先根据TLB的结构确定TLBI的位数,结合以求得的VPN就能得到对应的TLBT。根据TLBI和TLBT我们就能找到对应的TLB存储块,块中的内容就是PPN,又PPO和VPO相等。所以将得到的PPN和PPO组合起来就得到了PA。
如果再TLB中找不到VA对应的PTE,就得利用多级页表。每级页表中的每个PTE都是记载下级页表的基址,这样一层层寻找,直到第四层页表中PTE存放对应PPN,和快表一样,我们只要结合PPN和PPO就能得到PA
图7.5
7.5 三级Cache支持下的物理内存访问
得到物理地址,我们先要在一级cache中寻找对应的块:
这里需要说明:PA分为CO,CI,CT三部分。
CO由块大小决定,CI由cache的构造—有多少组决定,而剩下的CT就是标记。
根据CI找到对应的组,在根据CT看是否命中。
如果在一级cache中命中PA对应的块,就根据CO将要找的数据传给cpu。
如果在一级cache中找不到对应的数据,就得根据相同的地址去二级,三级甚至主存中寻找数据。
在二,三级cache中找到数据还需要在一级cache中根据具体情况选择是否要进行驱逐和替换。
7.6 hello进程fork时的内存映射
当shell调用fork创建hello进程时,内核为hello进程创建各种数据结构,将某个唯一的PID分配给它。
同时,为了给hello进程创建其唯一的虚拟内存,内核会复制当前进程的mm_struct,区域结构和页表的原样副本。内核将两个进程中的每个页面都标记为只读,,两个进程中的区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
加载并运行 hello 需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射到 hello 文件中的.text 和.data 节,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在可执行文件 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3.设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
图7.6
7.8 缺页故障与缺页中断处理
我们通常将DRAM缓存不命中称为缺页,即cpu在执行hello进程时引用了某个虚拟地址,通过TLB和多级页表,找到了对应的物理地址,结果mmu发现该物理页号对应的有效位为0,即该条目还未被缓存,此时会触发一个缺页异常,缺页异常会调用内核中的缺页异常处理子程序,内核会将对应磁盘上对应地址的内容加载进内存,同时更新之前的PTE。
之后,处理程序会返回到触发缺页异常的那条指令重新执行,而此时之前要获取的数据已经在内存中了,因此程序会正常运行,缺页异常处理结束。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存管理一般由动态内存分配器维护,分配器有两种风格,显示和隐式,两者的区别在于前者要求应用显式的释放任何已分配块,而后者则自动检测一个已分配块何时不再被程序使用。不过两者都要求显式地分配空闲块。
动态内存要处理好空闲块与已分配块的区分,空闲块的管理等问题。所以通过将一些信息如该块的大小,该块是否已经被分配等等嵌入到块中。通过这种处理得到的结构被称为隐式空闲链表。
由于隐式空闲链表的某些缺点,动态内存分配器还通过显式空闲链表这种结构来管理空闲块。显式空闲链表在隐式的基础上增加了指向前驱节点和后继节点的指针,当然,其前驱和后记也是空闲块。
图7.7–基于边界检查的显示空闲链表
内存分配器在malloc时需要寻找合适的空闲块,这时就有不同的策略。可以有首次适配,即从头开始搜索,选择第一次遇到的合适的块;下一次适配,略有不同,这种方法从上一次查询结束的地方开始搜索,而不是每次都从头开始搜索。
除此之外,对空闲块的搜索还包括最佳适配,还可以配合显式空闲链表实现LIFO(后进先出)的策略。
在内存分配器释放已分配块的同时,还要根据不同的空闲块的相对位置确定空闲块之间要如何合并。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管理,介绍了VA 到PA 的变换、物理内存访问,还介绍了hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态 存储分配管理。
第8章 hello的IO管理
8.1 Linux的I/O设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix I/O接口及其函数
Unix I/O接口:
1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4) 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF.
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.
先看printf函数的实现:
图8.1
可以看到在printf中调用了vsprintf函数:
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
之后在 printf 中调用系统函数 write(buf,i)将长度为 i 的 buf 输出.
而在write函数中,通过INT_VECTOR_SYS_CALL来通过系统调用 syscall。
syscall 将字符串中的字节“Hello 1180300227 孔德文”也就是我们在运行可执行文件hello时输入的参数。参数从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的 ASCII 码。
接下来字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
这一章介绍了UNIX系统下I/O接口的操作,对应的函数。
根据大作业中已经为我们完成的部分和提供的网址,我们能跟清楚地了解到UNIX是如何管理运行其I/O接口来为hello进程服务的。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
要完整地完成hello地一生要经历以下几个步骤:
1.编写对应的程序:
首先,我们要用高级编程语言(此处以C语言为例)完成对相应功能地实现,形成对应的.C文件。
2.预处理:
将hello.c中用到的所有外部的库(通过引用头文件实现)展开合并到一个 hello.i
文件中。
3.编译:
完成对hello.i的进一步处理,使之变为更接近机器语言的hello.s形式。
4.汇编:
将hello.s文件中的汇编语言语句转化为机器语言指令,并将这些指令合并成hello.o的二进制文件。
5.链接:
将 hello.o 与可重定位目标文件和动态链接库链接成为可执行目标程序 hello。
6.运行:
在linux系统下,在终端中输入./hello的指令(参数视不同的学号和姓名而定)。
7.创建子进程:
shell 判断hello并非内置命令,进程调用 fork 为其创建子进程
8.运行程序:
shell 调用 execve,execve 调用启动加载器,加载器为 hello 创建新的运行空间,最终调用 hello 的 main 函数。
9.执行指令:
CPU 为其分配时间片,在一个时间片中,hello 享有 CPU 资源,顺序执行自己的控制逻辑流
10.对异常和信号进行处理:
包括运行过程中代码段,数据段,堆栈段的缺页异常,除此之外如果运行途中键入
ctr-c ,ctr-z 则调用 shell 的信号处理函数分别停止、挂起。
11.结束:
shell 父进程回收子进程,内核删除为这个进程创建的所有数据结构。
对计算机系统的设计与实现的话,可以说是本学期我花时间,也是最让我头疼的部分,特别是csapp配套的一些实验与其HIT版本,我觉得实现困难的原因是这是我们第一次自己动手接触这些偏向硬件和底层原理的知识和操作。
以往我们都是直接接触一些IDE的用法即可,在学这门课之前,我对linux系统可以说是完全不了解,所以在课程刚开始的时候有些陌生。
尽管本学期课程的内容很多也让人经常焦头烂额,但我确实通过这门课学到了很多知识,希望以后能经常温习,不要让这学期付出的精力和时间白费吧
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c ----------- C语言源程序。
hello.i -----------hello.c经过预处理的结果。
hello.s ----------- hello.i经过编译的结果。
hello.o -------- hello.o经过汇编的结果。
hello ------------ hello.o经过链接的结果。
readelf1 ------- 对hello.o使用readelf的结果,分析重定位条目,各节内容和相关信息
readelf2 ---------- 对hello使用readelf的结果,与hello.o进行对比。
disass1 -----------对hello.o反汇编的结果,分析重定位作用和相关信息。
disass2 ---------对hello反汇编的结果,与hello.o进行对比。
参考文献
为完成本次大作业你翻阅的书籍与网站等
1.深入理解计算机系统第三版
2.https://www.cnblogs.com/pianist/p/3315801.html(介绍printf的实现和用法)
3.http://sourceware.org/binutils/docs/as/CFI-directives.html(介绍编译中用到的.cfi命令)