计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1190200718
班 级 1903009
学 生 赖思颖
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本文以hello.c文件为例,实验与理论相结合,阐述了一个程序经过预处理、编译、汇编、链接再到运行中间经过的全过程,同时也分析了这一过程背后计算机系统的进程管理、存储管理及IO管理。
关键词:计算机系统;编译原理;内存管理;虚拟内存
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P(From Program to Process):即hello.c变成进程的过程。程序员用键盘输入代码得到hello.c文件,接着hello.c程序的经过预处理器、汇编器、编译器、链接器的一系列处理,产生hello可执行文件,然后在shell中键入启动命令后,shell为其fork,产生子进程。
020(From Zero-0 to Zero-0):即hello可执行文件运行的全过程。shell调用execve函数,在新的子进程中加载并运行hello,在hello运行的过程中,CPU需要为hello分配内存、时间片,使得hello看似独享CPU资源。系统的进程管理帮助hello切换上下文、shell的信号处理程序使得hello在运行过程中可以处理各种信号,当程序员主动地按下Ctrl+Z或者hello运行到return 0时,hello所在进程将被杀死,shell会回收它的僵死进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz 2.40 GHz;16.0 GB;256G SSD+1T HDD
软件环境:Windows 10 64位;VirtualBox;Ubuntu 19.04 64位
开发与调试工具:gcc,gdb,readelf,hexedit,visual studio code
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c——原文件
hello.i——预处理之后的文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标文件
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf,包含hello.o的各节信息
hello1.elf——hello的elf,包含hello的各节信息
objhello——hello.o的反汇编文件,汇编器翻译后的汇编代码
objhello1——hello的反汇编文件,链接器链接后的汇编代码
1.4 本章小结
本章简单介绍了hello的P2P,020过程,并列出了本次实验的环境与工具及中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)对源程序中以字符#开头的预处理命令(宏)进行处理的过程。
作用:1.将源程序中所有#include声明的头文件的内容复制到程序中。
2.定义和替换源程序中使用#define定义的符号。
3.确定代码的部分内容是否应该根据一些条件编译指令进行编译。
4.删除源程序中的注释。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图2.2 ubuntu下预处理命令
2.3 Hello的预处理结果解析
打开hello.i文件,可以发现该文件共有3060行。在文本的结尾部分可以看到hello.c中的源代码。可以发现原本程序中的中文注释并没有出现。
图2.3.1 hello.i文件中的源代码部分
在该文件开头部分可以看到对头文件的处理。这里截取了对stdio.h头文件的处理。预处理器(cpp)首先到默认的环境变量下寻找stdio.h,打开文件/usr/include/stdio.h,然后发现其中stdio.h中也引用了其他的头文件,于是再打开对应的文件......直到把所有引用过的头文件都展开。同样地,预处理时也会将宏定义展开。
图2.3.2 hello.i文件中,对引用的头文件stdio.h的处理
图2.3.3 stdio.h文件中,对头文件的引用
2.4 本章小结
本章主要介绍了预处理的概念及作用,并结合hello.c预处理后得到的hello.i文件,对预处理过程结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码或汇编代码。
作用:1.确认所有的指令都符合语法规则。
- 根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树,最后转化为汇编代码。
3.对代码进行优化,比如处理一些可以在编译时期确定的值(比如一些计算)、选择合适的寻址方式、删除出多余的指令等。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3.2 Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1常量
if语句中的常量4在编译文件中以立即数形式出现。
3.3.2变量
变量i存放于-4(%rbp)中。
main函数传入参数argv由寄存器%edi转为存放于栈中,即-20(%rbp)中,argc数组的头指针也是如此,由寄存器%rsi转为存放于栈中,即存放于-32(%rbp)。
3.3.3字符串
源文件输出的字符串"用法: Hello 学号 姓名 秒数! "所对应的编码如下。
而另一个输出的字符串"Hello %s %s "则在编译文件中如下表示。
3.3.4赋值
if语句对i的赋值即i=0,编译器处理后如下所示,使用mov指令,由于i为int类型,因此使用的是movl,前文提到i存放于-4(%rbp)中。
3.3.5类型转换
仅一处,sleep(atoi(argv[3]));处将char*类型的arg[3]强制转化为了整型。似乎没有特别的操作,直接赋值了。
3.3.6算术操作
循环过程中每次循环的i++。使用addl指令实现。
3.3.7关系操作
条件语句中argv!=4的比较。使用了cmpl指令和je指令(相等则跳转)。
循环过程中每次循环i<8的比较。使用cmpl指令和jle指令(小于等于则跳转),并且由于i是int类型,于把i<8变成了i<=7来处理。
3.3.8数组操作
对数组argv中元素的引用。
首先是argv[1]和argv[2],由于char*类型即指针类型的大小位8字节(64位环境下)它们分别存储于argv数组头指针+8和+16的位置。而argv数组头指针头指针存储于-32(%rsp)。所以首先使用movq指令取出argv数组首地址到%rax,%rax+16就是argv[2]的地址,(%rax+16)就是argv[2]的值,同理可得到argv[1]的值为(%rax+8)。
3.3.9控制转移
if语句。首先使用cmpl指令比较argc与4,若不等,je指令会使得程序跳转到.L2处,否则继续进行。
for循环语句。首先mov指令给i赋初值0,然后跳转到.L3部分。L3部分先比较7与i,通过jle语句可知,若i<=7,程序跳转到.L4继续,否则调用函数getchar。
L4部分,除了执行了sleep(atoi(argv[3]))(后面会分析),就是最后一个addl指令,将i自增了1。
3.3.10函数操作
Sleep函数与atoi函数
首先atoi函数的传入参数为argv[3],在截取的代码部分,argv[3]开始存储于%rax,中,由于寄存器%rdi为函数的第一个参数,所以将%rax赋值给%rdi,而sleep函数的传入参数为atoi函数的返回值,返回值由寄存器%rax存储,所以当atoi返回时,要将%rax再次赋值给%rdi,再用call指令调用sleep函数。
main函数
传入参数为argv和argc,它们在传入时分别存储与寄存器%rdi和%rsi中
最后的返回值为0,所以在最后使用mov指令将0赋给了%eax,然后使用leave,ret指令退出main函数。
printf函数
输出"用法: Hello 学号 姓名 秒数! "使用的是puts,传入参数为字符串的首地址。
输出printf("Hello %s %s ",argv[1],argv[2]);时,传入参数有argv[1],argv[2]及字符串"Hello %s %s "。
exit函数
传入参数为1,使用call指令调用。
getchar函数
没有传入参数,返回值在这段程序中没有使用到,使用call指令调用
3.4 本章小结
本章主要介绍了编译的概念及作用,并且对照hello.c中源代码和hello.i编译生成的hello.s中相应的汇编代码,详细分析了hello.s中汇编代码的含义。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器as将.s文件翻译成机器指令,把这些指令打包成一中叫做可重定位目标程序格式,并将结果保存在目标文件中。
作用:将编译器产生的汇编语言进一步翻译为计算机可以理解的机器语言,生成.o文件。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图4.2 Ubuntu下汇编的命令
4.3 可重定位目标elf格式
使用readelf -a -W hello.o > hello.elf命令,生成并导出hello.o文件的elf文件,该文件由以下几个部分组成:
elf头:
elf头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括elf头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图 4.3.1 elf头
节头部表:
节头部表包含了文件中出现的各个节的类型、地址、偏移量、大小等信息。
图 4.3.2 节头部表
重定位节:
重定位,即编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
对于本程序,重定位的信息有,.rodata中的模式串,例如串"用法: Hello 学号 姓名 秒数! ");还有各个函数,例如puts,exit函数。
图 4.3.3 重定位节
符号表:
每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。
图 4.3.4 符号表
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > objhello
机器语言的构成:
机器语言指不经翻译即可为机器直接理解和接受的程序语言或指令代码。一条指令就是机器语言的一个语句,机器语言指令是一种二进制代码,由操作码和操作数两部分组成。操作码规定了指令的操作,是指令中的关键字,不能缺省。操作数表示该指令的操作对象。
与汇编语言的映射关系:
1.操作数:机器语言中的操作数使用16进制表示,而汇编语言数使用10进制表示。
2.分支转移:机器语言中的分支跳转语句例如je 2f<main+0x2f>,这后边的2f<main+0x2f>是一个地址。而汇编语言的分支跳转语句例如je .L2,这后边的.L2是一个段的名称。
3.函数调用:机器语言中的函数调用例如callq 25<main+0x25>,这后边的25<main+0x25>是要跳转到的地址。而汇编语言中的函数调用例如call puts@PLT,这后边的puts@PLT是要跳转到的函数的名称。
图 4.4 hello.o的反汇编代码
4.5 本章小结
本章主要介绍了汇编的概念及作用,分析了hello对应的可重定位目标elf格式的内容与结构。并对比hello.s汇编得到的hello.o文件和hello.s文件,分析了机器语言与汇编语言的区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。在现代系统中,链接是由较做链接器的程序自动执行的。
链接的作用:链接器使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
命令:
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.2 Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
命令:readelf -a -W hello> hello1.elf
节头部表:
其中,Address表示各段的起始地址,Off表示偏移量,Size表示大小。
图5.3.1 hello.elf中的节头部表
5.4 hello的虚拟地址空间
用edb查看程序hello,发现程序在地址0x400000中被载入。
图5.4.1 edb查看hello
使用edb的内置插件SymbolViewer查看各个段的位置。对照5.3,不难发现与5.3中各个段的Address一致。
图5.4.2 SymbolViewer查看各个段的位置
根据地址可以找到对应段的信息,比如0x4002e0是.interp的信息
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令objdump -d -r hello > objhello1
hello与hello.o的不同:
1.链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2.函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了绝对地址,例如,调用函数put,hello.o中为callq 25 <main+0x25>,而hello中为callq 401090 <puts@plt>。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
3.地址访问:
hello.o中的访问地址使用的是相对偏移地址,hello则用的是绝对地址。例如,同样是main函数中的跳转,hello.o中的为je 2f <main+0x2f>,hello中则为je 401154 <main+0x2f>。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
链接的过程:
对比hello和hello.o的不同,可知链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,这些文件中的各个函数段按照一定规则和顺序排列在一个结果文件中。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start
libc-2.31.so!__libc_start_main
libc-2.31.so!__cxa_atexit
libc-2.31.so!__libc_csu_init
libc-2.31.so!_setjmp
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!sleep@plt
hello!getchar@plt
ld-2.31.so!_dl_runtime_resolve_xsave
ld-2.31.so!_dl_fixup
ld-2.31.so!_dl_lookup_symbol_x
libc-2.31.so!exit
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
由elf文件可知_init的地址为0x401000,查询相应地址可得内容如下。
图5.7 edb查找地址0x401000
5.8 本章小结
本章主要介绍了链接的概念及作用,对比hello与hello.o文件,分析了hello的虚拟地址空间、重定位过程、执行流程和动态链接的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
进程的作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它接收用户命令,然后调用相应的应用程序。
处理流程:
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.Shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:
- 内建的命令
- shell函数(由用户自己定义的)
- 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
6.3 Hello的fork进程创建过程
fork函数创建子进程。子进程与父进程近似,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,即当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间的区别在于它们拥有不同的PID。
6.4 Hello的execve过程
execve函数的作用就是在当前进程的上下文中加载并运行一个新的程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文由程序正确运行所需的状态组成。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
在进行程序调度时,系统保存当前进程的上下文,载入目标进程的上下文,并将控制传递到目标程序。这些操作都在内核模式下被执行。
进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。对于一个运行在这些进程之一的上下文中的程序,它看上去就像是在独占地使用处理器。
处理器通过某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式中的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。当没有设置模式位时,进程就运行在用户模式,用户模式中的进程不允许执行特权指令,也不允许直接引用地址空间内核区内的代码和数据。
图6.5 上下文切换图示
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常可分成如下四类:
图6.6.1 异常的类别
执行过程中产生的信号如下表:
图6.6.2 Linux信号
处理方式见上表中返回行为。
接着是在程序运行过程中按键盘的部分
1.回车
如图所示,回车键并不影响程序的运行。
图6.6.3 程序运行时按下回车
2.Ctrl-Z
如图所示,程序会收到SIGINT信号,进程暂停。
图6.6.4 程序运行时按下Ctrl-Z
3.Ctrl-C
如图所示,程序接收到SIGSTOP信号,进程终止。
图6.6.5 程序运行时按下Ctrl-C
4.Ctrl-z后运行ps
使用ps可以查看进程和对应的PID。
图6.6.6 程序运行时按下Ctrl-z后运行ps
5.Ctrl-z后运行ps
使用jobs可以查看到被停止的hello进程。
图6.6.7 程序运行时按下Ctrl-z后运行ps
6.Ctrl-z后运行pstree
可以看到hello在其中的一个分支上(已用红笔标出)
图6.6.8 程序运行时按下Ctrl-z后运行pstree
7.Ctrl-z后运行fg
运行fg后hello再次开始运行。
图6.6.9 程序运行时按下Ctrl-z后运行fg
8.Ctrl-z后运行kill
运行kill -s 94003后可以发现PID为4003的进程,即hello被杀死。
图6.6.10 程序运行时按下Ctrl-z后运行kill
6.7本章小结
本章介绍了进程的概念与作用,阐述了shell的作用和及其处理流程,fork进程的创建过程和execve过程,最后分析了hello的执行过程和过程中出现的异常及其处理方式。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。
线性地址:跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码产生逻辑地址,它加上相应段的基地址就生成了一个线性地址。
虚拟地址:我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
物理地址:物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。索引号,或者直接理解成数组下标,是段描述符的索引。段描述符具体地址描述了一个段,很多个段描述符,就组成了段描述符表,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页。类似地,物理内存被分配为物理页,大小与。将虚拟页映射到物理页的是一个存放在物理内存中的数据结构页表。
图7.3.1 物理内存与虚拟内存的转换图示
MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。
图7.3.2 物理地址与虚拟地址的转换图示
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。TLE是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLE通常有高度的相联度。TLB分为索引(TLBI)与标记(TLBT)两个部分。MMU在读取PTE时会直接通过TLB读取,如果不命中,将会从缓存中取出相应的PTE存放在TLB中。
图7.4.1 TLE的构成图示
多级页表,即使用层次结构的页表。对于每一级页表,将所有的PTE分成若干块,每一块都映射着下一级链表中的同一个PTE。对于k级页表,虚拟地址被分成k个VPN和1个VPO,每个VPNi都是一个到第i级页表的索引。
下图给出了Corei7MMU如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推。
图7.4.1 四级页表支持下的地址转化的运行过程
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,MMU按照上述操作给出物理地址PA。PA分为三段,即CT(标记位),CS(组号),CO(偏移量)。根据CS找到缓存组,在缓存组中根据有效位和CT进行匹配。如果命中就根据块偏移访问数据,如果不命中,就到下一级缓存继续匹配,当命中时,根据替换策略更新各级缓存的相应缓存块。
7.6 hello进程fork时的内存映射
(以下格式自行编排,编辑时删除)当fork函数,它将创建新进程。内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。子进程与父进程近似,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,即当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
图7.6 一个私有的写时复制对象
7.7 hello进程execve时的内存映射
1.在bash中的进程中执行了如下的execve调用:execve(“hello”,NULL,NULL);
2.execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
下面是加载并运行hello的几个步骤:
3.删除已存在的用户区域。
4.映射私有区域
5.映射共享区域
6.设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
DRAM缓存不命中称之为缺页。具体地说,指令引用一个虚拟地址,如果在MMU中查找页表时发现,该地址相对应的物理地址并未缓存(有效位为0),即触发了一个缺页故障。
当发生缺页故障时,将调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,若其被修改过,还会将其复制回磁盘,最后用指令引用的虚拟地址对应的物理地址更新该页。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件。由于此时这个虚拟地址已经缓存,所以这条指令正常处理。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
图7.9 一个Linux进程的虚拟内存
7.10本章小结
本章介绍了hello的存储地址空间,两种不同的地址变化段式管理、页式管理,以及TLB与四级页表支持下的VA到PA的变换过程和三级Cache支持下的物理内存访问。还描述了hello进程fork、execve时的内存映射以及缺页故障的处理流程和动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O,这使得所有的输入输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。内核返回一个小的非负整数,即描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_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 IO函数:
1.int open(char* filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件。flag参数也可以是一个或者更多位掩码的或,为写提供一些额外的指示,mode参数指定了新文件的访问权限位。
2.int close(int fd);
关闭一个打开的文件,成功返回0,出错返回-1,关闭一个已关闭的描述符会出错。
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);
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
首先查看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;
}
发现其中调用了函数vsprintf,于是查看vsprintf函数的代码
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);
}
分析可知,printf函数先调用vsprintf,vsprintf函数按照格式fmt,将args的内容格式化并将结果存入buf中,然后返回buf的长度。printf函数再调用write,write函数从buf复制i个字节到描述符fd的当前文件位置。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar有一个int型的返回值。当程序调用getchar时。程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键.
8.5本章小结
本章介绍了Linux中I/O设备的管理方法,UnixI/O接口和函数,对printf和getchar函数进行了实现分析。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
从hello.c被程序员编写完成,到它最终被回收,一共经历了如下过程:
1.hello.c经过预处理生成hello.i文件
2.hello.i经过编译生成hello.s汇编文件
3.hello.s经过汇编生成二进制可重定位目标文件hello.o
4.hello.o经过链接生成了可执行文件hello
5.bash进程调用fork函数,生成一个新进程。execve函数加载运行当前进程的上下文中加载并运行新程序hello
6.当CPU需要访问hello时,通过MMU将虚拟地址转化为对应的物理地址。
7.在运行过程中hello调用了printf函数和getchar函数,这些函数的实现与unix I/O密切相关。
感悟:在本次完成大作业的过程中我复习了整个计算机系统课程的内容。我感觉我对计算机系统的了解不够深入,对很多东西的理解浮于表面,需要在未来进行进一步的学习。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c——原文件
hello.i——预处理之后的文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标文件
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf,包含hello.o的各节信息
hello1.elf——hello的elf,包含hello的各节信息
objhello——hello.o的反汇编文件,汇编器翻译后的汇编代码
objhello1——hello的反汇编文件,链接器链接后的汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant,David O'Hallaron.深入理解计算机系统(原书第3版).
机械工业出版社,2016-11
[2]百度百科-机器语言
[3]Shell命令行处理流程 https://blog.51cto.com/evillinux/1192072
[4]Windows内存管理https://www.2cto.com/os/201107/95812.html
[5]逻辑地址(段式)-> 线性地址或者虚拟地址(页式) -> 物理地址(页框) 解释 深入浅出https://blog.csdn.net/dellme99/article/details/30456849
[6]printf函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html
[7]getchar()函数https://www.cnblogs.com/develop-me/p/5675766.html
(参考文献0分,缺失 -1分)