计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
学 号 *******
班 级 *******
学 生 yui
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文通过跟踪hello程序的生命周期来开始对系统的概括:从它被程序员创建开始,到在系统上运行,输出简单的信息,然后终止。了解了在linux下hello.c是如何一步步变为hello可执行程序,也了解了指令系统,内存存储体系结构,操作系统管理硬件,进程线程虚拟内存,系统之间的网络通信和并发并行
关键词:汇编;计算机系统;存储
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.1.1 P2P
hello程序的生命周期是从一个高级C语言程序开始的。为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。源程序(文本)hello.c经过预处理器(cpp)变为修改了的源程序(文本)hello.i,后通过编译器(cc1)变为汇编程序(文本)hello.s,再通过编译器(as)生成可重定位目标程序(二进制)hello.o,并和共享链接库如printf.o经过链接器(ld)最后生成可执行目标程序(二进制)hello程序。
在shell中输入./hello后,程序通过fork()函数创建新的进程,再使用execve加载程序,映射虚拟内存,即mmap。此时,程序从程序program变为进程process,完成了P2P过程
1.1.2 020
020即From Zero-0 to Zero-0。在shell中通过调用fork函数创建子进程后通过execve进行虚拟内存的映射,然后分配物理内存,并在代码段中执行程序,实现打印printf等操作。在此过程中,内存管理器和CPU在L1,L2,L3高速缓存和TLB多级页表取数据并提升运行速度。程序运行完成后,shell会回收子进程,内核最后删除有关的数据结构,从系统中清除。最后实现了020。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
CPU:Intel Core i7 12700H
GPU:Intel Iris Xe Graphics ox1B50
1.2.2 软件环境
Windows11 家庭版;VMware 17;Ubuntu 20.04
1.2.3 调试工具
Visual Studio2022;codeblocks 64位;gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:源程序
hello.i:预处理后文件
hello.s:编译后的汇编文件
hello.o:可重定位目标程序
hello.out:可执行目标程序
hello_o.elf:hello.o的ELF格式
hello_o.txt:hello.o的反汇编代码
hello.elf:hello的ELF格式
hello.txt:hello的反汇编代码
1.4 本章小结
1.本章主要介绍了hello程序的p2p过程和020过程
2.介绍了硬件环境、软件环境和开发工具
3.描述了中间文件的名字和作用
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:#define宏定义和常量标识符替换成相应的代码和值
#include检查文件包含,预处理的指令等
还会删除程序中的注释和多余的空白字符
2.2在Ubuntu下预处理的命令
图1
2.3 Hello的预处理结果解析
图2
图3
找到main函数,通过比较发现两边函数几乎一样,唯一的区别是.i文件中将所有#include的内容和注释部分的内容删除掉了,这与前面分析预处理的作用相一致。继续寻找发现.i文件将include内容进行了引用,如stdio.h,如图
图4
在经过预处理后,代码的长度大大的增加,达到了3060行
2.4 本章小结
1.预处理可以初步翻译源文件,进行宏替换,文件包含,删除注释等操作
2.熟悉了Ubuntu下预处理的命令
3.解析了hello.c预处理的结果,并和前面的理解进行对比和验证
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义。
编译的作用:通过语法分析和词法分析,在确认所有的指令都符合语法规则后,将预处理程序翻译成等价的中间代码表示或汇编代码
3.2 在Ubuntu下编译的命令
图5
3.3 Hello的编译结果解析
3.3.1 数据
1.数字常量
*图6
对应于C语言
图7
*图8
对应于C语言
图9
中间的判断
2.字符串常量
图10
存在.rodata段中对应于C语言
图11
图12
后通过leaq传送地址指令通过puts函数打印出来
3.局部变量
通过观察C语言可知
图13
如图int argc, char *argv[], int i均为局部变量,在hello.s中寻找局部变量,如图所示
图14
发现将参数argc存入-20(%rbp)的栈位置,将*arg[]存入-32(%rbp)的栈位置,在.L4 中也可以发现,如图
图15
地址为八个字节这里分别对应ragv[1], argv[2], argv[3]。
图16
int i存在-4(%rbp)的栈位置
3.3.2 赋值
hello.c中的赋值操作为
图17
对应hello.s中为
图18
可以看到为movl操作进行赋值,mov指令根据操作数字节大小分为movb:一个字节、movw:“字”、movl:“双字”、movq:“四字”
3.3.3 类型转换
hello.c中的类型转换操作为
图19
将字符型变量转换为整型,对应hello.s中程序为
图20
通过call调用atoi函数将argv[]转换为整型,并通过movl操作存放在%eax 中,如图21
3.3.4算术操作
hello.c中唯一的算术操作为
图22
中的i++,对应于hello.s中为
图23
由前文知int i被存在-4(%rbp)的栈位置,因此这里是对i的++,符合分析。
3.3.5 关系操作
C文件中关系操作有两处,一处为
图24
判断argc是否等于4,对应S文件中为
图25
另一处为
图26
中的i<8,对应S文件中为
图27
发现关系操作多使用cmpl操作
3.3.6数组/指针/结构操作
C文件中的数组操作为
图28
可以看到分别访问了argv[1], argv[2], argv[3],对应S文件中的操作为
图29
首先数组的首地址在-32(%rbp)的栈位置,于是printf中的argv[1], argv[2]对应于基地址+8和+16。后sleep函数对应于基地址+24,符合分析
3.3.7 控制转移
S文件中有关跳转的指令均为控制转移
图30
为条件跳转,判断argc和4是否相等,相等则跳转.L2,否则顺序执行;
图31
为无条件跳转,当执行到这个语句后直接根据要求跳转;
图32
为循环中的条件跳转
3.3.8 函数操作
函数调用:函数的调用通过call语句来实现,如
图33
图34
图35
图36
分别对应于C语言中调用printf函数,sleep函数,exit函数和getchar函数,同时主函数main函数自动执行。
参数传递:在其中main函数传递参数为int argc,char *argv[],printf函数传递参数为argv[1], argv[2],exit函数传递参数为1,sleep函数传递参数为atoi(argv[3]),getchar函数无传递参数。
函数返回:main函数通过ret返回了0,其他的函数的返回值保存在%rax寄存器中。
3.4 本章小结
1.了解并介绍了编译的概念和作用
2.熟悉了在Ubuntu下编译的指令
3.在S文件中找到了C语言的数据与操作的编译表示,进一步熟悉了汇编指令
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中
汇编的作用:把汇编语言翻译成机器指令,这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
图37
4.3 可重定位目标elf格式
可重定位目标elf格式为
图38
生成elf格式文件并打开文件进行分析
图39
ELF头:
图40
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头部表:
图41
节头部表是描述目标文件的节,描述了目标文件当中不同的节的类型、地址、大小、偏移等信息。
重定位节:
图42
重定位节是链接时链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并为一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。本程序需要被重定位的是printf、puts、exit、sleep、getchar和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息
符号表:
图43
符号表包含模块m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:1.由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量。2.由其他模块定义并被模块m引用的全局符号。这些符号被称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。3.只被模块m定义和引用的局部符号。它对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其它模块引用。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
图44
分支转移:在S文件中的跳转为如图
图45
而反汇编后的文件跳转为
图46
由分析知S文件中用的是段名称,如图中为.L3,而在反汇编代码中发现跳转为80<main+0x80>发现为一个相对的地址而不是助记符。
函数调用:S文件中的函数调用为
图47
而同样的语句在反汇编中代码为
图48
发现在可重定位文件中call后不是函数的具体名称,而是一条重定位条目指引的信息。而汇编文件中直接加的是相对地址
数字进制:S文件中
图49
如图所示立即数等均为十进制,而在反汇编代码中图50
可知均变为16进制
4.5 本章小结
1.了解并介绍了汇编的概念和作用
2.熟悉了在Ubuntu下反汇编的指令
3.了解了elf文件各个部分的作用如ELF头、节头部表、符号表和可重定位节等
4.对比了hello.o和hello.s之间的差距,分析了汇编语言和机器语言的对应关系
(以下格式自行编排,编辑时删除)
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。在现代系统中,链接是由叫做链接器的程序自动执行的
链接的作用:链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把他分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
图51
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF头:
图52
ELF头效果和汇编时基本一样,唯一的不同是hello文件ELF头中type变为EXEC格式,原来为REL格式
节头部表:和原来几乎一样
图53
经比较,重定位节和符号表也相同
图54
图55
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb加载hello,观察到虚拟地址空间如图,datadump从0x400000开始,最初是ELF表,和前ELF相同,包含了此段的相关信息
图56
根据节头部表可以知道个各节所在的位置,如.text节在偏移量为0x40的地方,即0x400040,如图
图57
又如.data节在偏移量为d2的位置,即0x4000d2,由于本程序没有全局和静态C变量,因此此节所存数据为0,验证了结论
图58
在symbols中可以查询到各段的起始位置,如.interp开始于0x4002e0,对应虚拟地址如图
图59
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
图60
通过指令反汇编代码并打开
通过对比发现不同点如下:
1.代码的行数增加了许多,由原来的53行变为了192行
图61
图62
2.hello_o.txt中只有main程序的代码,但是hello.txt中包含了所有此程序需要用到的代码,包括引用的函数。原来调用的C标准库中的代码都被插入了代码中,并且经过了链接之后给每个函数都分配了各自的虚拟地址,如图
图63
图64
3.hello_o.txt中发现所有的地址均为相对地址,是相对于main函数的地址,而在hello.txt中可以发现已经给各个代码分配了虚拟地址,如图
图65
为hello_o.txt
图66
为hello.txt
同时也发现hello_o.txt中对于函数的调用原来也是使用的偏移地址的方法如上图中2f<main+0x2f>,而在hello.txt中调用的函数位置为虚拟地址0x4010d0不再使用相对位置。跳转指令也是同样的原理,如图
图67
为hello_o.txt
图68
为hello.txt
因此可以发现链接的过程中,原代码的每个节中有很多函数,链接将这些函数链接到了程序中,程序各个节变得更加完整,跳转的地址也具有参考性
重定位:一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时就可以进行重定位流程,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。最后,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。代码的重定位条目放在.rel.txt中。在程序中则体现为由原来的相对地址变为了分配的唯一的虚拟地址。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
流程:1.开始执行:_start, _libc_start_main
2.执行mian:_main, _printf, _exit, _sleep, _getchar
3.退出:exit
图69
根据edb可以看到_start在0x4010f0的位置
图70
_init在0x401000
图71
_libc_start_main在0x7f08d65aafc0
图72
puts在0x401030
图73
printf在0x401040
图74
getchar在0x401050
图75
atoi在0x401060
图76
exit在0x401070
图77
sleep在0x401080
图78
plt+0x70在0x401090
图79
main在0x401125
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。之前他们相互独立,只有在程序运行时才将它们链接在一起形成完整的程序。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任何位置,因此使用了延迟绑定的方法,所以需要在程序运行后再观察got表的变化。
由hello的elf文件可知,got表是0x404000为首地址,大小为0x48的表
查询gdb可以发现,在运行dl_init之前,为如图所示
图80
在运行后变为
图81
由观察可以发现0x404008及之后的16个字节由原来的全为0发生了改变,说明.got.plt已经发生了变化
5.8 本章小结
1.了解并介绍了链接的概念和作用
2.熟悉了在Ubuntu下链接的指令
3.了解了elf文件各个部分的作用如ELF头、节头部表、符号表和可重定位节等并和hello.o的进行比较
4.在edb中查找了hello的虚拟空间地址,并分析了运行程序前的链接过程和整个程序的执行流程
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个执行中程序的实例
进程的作用:系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一个交互型应用级程序,代表用户运行其他程序。如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等。Shell是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。
处理流程:1.从终端读入输入的命令2.将输入字符串切分获得所有的参数3.如果是内置命令则立即执行4.否则调用相应的程序执行5.shell 应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
输入如图所示命令,首先shell对输入的命令进行解析,在输入以后会为程序创建一个进程(使用fork()函数),由于是子进程,因此与父进程共用一个虚拟地址空间,但是进程PID并不相同
图82
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。他会覆盖当前进程的地址空间,但并没有创建一个新进程。execve函数调用一次且从从不返回。如果成功,则不返回,如果错误,则返回-1。
运行hello程序时,虚拟空间创建新的代码、数据、堆和栈段,将可执行代码和数据从磁盘复制到内存中,映射共享区域,最后跳转到第一条指令开始执行。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
运行hello程序时,shell会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。进程是轮流使用处理器的。每个进程执行他的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。一个进程执行它的控制流的一部分的每一时间段叫做时间片。
运行hello代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法就是诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。当他返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。(如hello中的sleep函数和getchar函数就是如此过程)
内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1.正常运行
图83
2.按下回车
图84
运行时回车对程序运行没有影响,但是运行完循环后会自动输入回车,因此程序直接终止,且出现多次回车的情况
3. Ctrl-Z
图85
停止进程,发送信号SIGSTP,父进程接收到信号并进行处理
图86
使用ps指令发现进程并没有终止
图87
输入fg后又会继续进行被挂起的进程
图88
输入jobs可以观察到hello程序当前的状态
图89
输入pstree可以查看当前进程树,找到hello程序发现是在terminal的bash下的
图90
使用kill发送信号9(SIGKILL)给进程15770,负的PID会导致信号被发送到进程组PID中的每个进程,终止以后再ps发现已经没有hello进程
4. Ctrl-C
图91
Ctrl-C发送信号2(SIGINT),是来自键盘的中断,默认行为是终止进程,因此程序直接退出并被回收
5.不停乱按
图92
6.7本章小结
1.了解了进程的概念与作用
2.熟悉了shell-bash的作用和处理流程
3.了解了fork函数如何创建进程,execve函数如何执行函数
4.了解了多种信号的处理和异常中断
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:逻辑地址是指段基址和段偏移量构成的相对寻址方式
hello程序的反汇编代码中地址为逻辑地址,是一种相对寻址方式
线性地址:如果地址空间中的整数是连续的,那么我们说他是一个线性地址空间
hello程序的反汇编代码中由偏移可知地址均为线性连续的,是线性地址
虚拟地址:是一种虚拟的地址,是由CPU生成的用来访问主存的中间地址,由MMU硬件将虚拟地址翻译成物理地址
hello程序中代码段0x400000就是虚拟地址,需要映射到物理地址中
物理地址:计算机系统的主存被组织成一个由M个字节大小的单元组成的数组。这里对应的主存的地址就是唯一的物理地址
hello程序中MMU将虚拟地址翻译成物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
段描述符占8个字节定义如下。通过段描述符,我们能够得到如下信息:段的基址,由B31-B24/B23-B16/B15-B0构成,一共32位,基址可以是4GB空间中任意地址;段的长度,由L19-L16/L15-L0构成,一共20位。如果G位为0,表示段的长度单位为字节,则段的最大长度是1M,如果G位为1,表示段的长度单位为4kb,则段的最大长度为1M*4K=4G。假定我们把段的基地址设置为0,而将段的长度设置为4G,这样便构成了一个从0地址开始,覆盖整个4G空间的段。访存指令中给出的“逻辑地址”,就是放到地址总线上的“物理地址”,这有别于“段基址加偏移”构成的“层次式”地址。段的类型,代码段还是数据段,可读还是可写
图93
十六位段寄存器的称之为段选择符,其定义如下
图94
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
图95
首先通过页表基址寄存器找到应该查询的页表,然后通过虚拟地址查询虚拟页号,接着查询有效位是否有效,若有效,则可以查到对应的物理页号,即可以找到物理地址中的基址。虚拟页偏移量与物理页偏移量完全相同,通过物理地址的基址和偏移量,即可以实现虚拟地址到物理地址的转化。若无效则触发缺页中断,从低一级的缓存中查找并映射到这一级中。
7.4 TLB与四级页表支持下的VA到PA的变换
图96
1.CPU产生一个虚拟地址
2.MMU从TLB中取出相应的PTE
3.MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
4.高速缓存/主存将所请求的数据字返回给CPU
图97
在从高速缓存/主存中取数时,使用多级页表进行检索。每一级页表的大小相等,但是高一级的页表对应了低一级页表的首地址。如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。只有一级页表才需要总是在主存中;虚拟内存可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力;只有最常使用的二级页表才需要缓存在主存中
7.5 三级Cache支持下的物理内存访问
图98
如果未命中则需要访问物理内存。MMU从主存中取出相应的PTE,它通过将来自PTE的PPN和来自虚拟地址的VPO连接起来,这就形成了物理地址。接下来,MMU发送物理地址给缓存,缓存从物理地址中抽取缓存偏移CO、缓存组索引CI以及缓存标记CT。因为组中的标记与CT相匹配,所以缓存检测到一个命中,读出在偏移量CO处的数据字节,并将它返回给MMU,随后MMU将它传递回CPU
总的内存访问如图
图99
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。他将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念
7.7 hello进程execve时的内存映射
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2. 映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data 区。bss 区域是二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器(PC)。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
图100
DRAM缓存不命中即称为缺页,如图所示,需要返回磁盘中找到需要的数据并修改页表。这时会找到一个牺牲页替换这个页表
图101
如图所示修改后重新执行刚刚触发中断的程序,此时会命中,程序正常进行。
图102
Linux缺页处理如图所示,原理同前文相同
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
基本方法与策略:只有在malloc函数调用时才分配内存。需要多大的内存就分配多大的空间。在分配的过程中,分配的块可能会受到字节对齐的影响
Malloc函数具体如图
图103
Malloc函数可以通过使用mmap和munmap函数,显式地分配和释放堆内存。
之所以选用动态内存分配,是因为经常直到程序实际运行时,才知道某些数据结构的大小
7.10本章小结
本章主要介绍了hello的存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
设备的模型化:所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
函数:
1.int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件。
2.int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
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的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从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代码:
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);
}
观察代码发现函数作用是返回需要打印的字符串长度
Write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
观察函数发现是给寄存器赋值,然后用到INT_VECTOR_SYS_CALL表明需要调用sys_call函数
sys_call函数:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
观察代码发现函数的作用是将ASCII码字模库存在vram中,实现字符串显示
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘输入产生中断,进入中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。然后通过getchar函数(出错则返回-1),getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回,这其中一个一个读,当输入多个字符时需要等待读取保留在键盘缓冲区的字符串,全部读完后等待用户按键,然后继续读取
8.5本章小结
本章主要学习了Linux的IO设备管理方法,了解了IO接口及其函数,对printf和getchar函数的实现进行了分析
(第8章1分)
结论
首先通过编程写下hello.c文件,通过预处理器(cpp)变成了hello.i,再经过编译器(cc1)变成了hello.s,再经过汇编器(as)生成hello.o,最后通过链接器(ld)生成了可执行目标程序hello
在运行程序的过程中,shell通过fork创建子进程,通过execve加载程序,实现了P2P
运行程序后开始执行指令,根据时间片进行运行,每个进程执行它的流的一部分。在执行过程中会不断访问内存。MMU翻译CPU产生的虚拟内存映射到物理内存当中。在申请内存的过程中也有动态分配的内存
在运行一些程序时,进程会受到中断信号的影响,此时程序会执行中断服务子程序运行中断程序运行以后返回继续执行
程序运行完毕以后会对这个进程进行终止,后面这个进程的父进程会回收僵死子进程,从此以后hello程序的运行就完成了
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c:源程序
hello.i:预处理后文件
hello.s:编译后的汇编文件
hello.o:可重定位目标程序
hello.out:可执行目标程序
hello_o.elf:hello.o的ELF格式
hello_o.txt:hello.o的反汇编代码
hello.elf:hello的ELF格式
hello.txt:hello的反汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://blog.csdn.net/sy06106/article/details/118274118
[2] 《深入理解计算机系统》
[3] https://blog.csdn.net/LWuoa/article/details/124845263?spm=1001.2014.3001.5501
[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
(参考文献0分,缺失 -1分)