计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2022年5月
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:计算机系统;操作系统;hello程序;I/O;虚拟内存;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:
首先hello.c通过I/O设备存入主存。
Gcc编译器读取程序
预处理cpp变成hello.i
编译器变为hello.s
再通过汇编器as变为hello.o
组后通过链接器成为可执行文件hello
在shell里输入./hello
调用fork函数创建子进程
在通过execve调用加载器
用mmap创建新的内存区域,新的栈和堆被初始化零
再通过虚拟地址空间中的页映射到片
最后调用main函数
流水线化执行
变成process运行
O2O:
先执行上述操作
执行结束后
进程终止
直到进程被其父进程回收然后退出
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
1.2.3 开发工具
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c:源代码
hello.i:hello.c预处理生成的文本文件。
hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序。
hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello.elf:hello.o的ELF格式。
hello_o_asm.txt:hello.o反汇编生成的代码。
hello:经过hello.o链接生成的可执行目标文件。
hello_out.elf:hello的ELF格式。
hello_out_asm.txt:hello反汇编生成的代码。
1.4 本章小结
本章主要是漫游式了解hello在系统中的生命周期,同时对每个部分有一个大致的系统性了解和概括;最终列出了本次实验的平台与中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
(1)执行原文件包含。#include命令告诉预处理器(cpp)读取系统头文件,它直接插入到程序文本中。
(2)执行宏替换。宏是对一段重复文字的简短描写,例如#define MAX 2147483647在预处理中会把所有MAX替代为2147483647。
(3)条件编译。条件编译。 是根据实际宏定义(某类条件)进行代码静态编译的手段。可根据表达式的值或某个特定宏是否被定义来确认编译条件。例如, #ifdef可以判断某个宏是否被定义,若已定义,执行随后的语句。#endif是#if, #ifdef, #ifndef这些条件命令的结束标志。
2.2在Ubuntu下预处理的命令
表 1图2.2.1使用cpp命令生成hello.i文件
2.3 Hello的预处理结果解析
hello.i是在原本代码的基础上,将stdio.h的内容引入。
通过结果分析可以发现,预处理实现了在编译前对代码的初步处理,对源代码进行了某些转换。
另外,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了编译器预处理的相关概念。给出了预处理的概念,并简单描述了如何预处理,并展示了预处理后hello.i的样子。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
3.2 在Ubuntu下编译的命令
命令 :gcc -S hello.i -o hello.s
生成的hello.s
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
部分代码如下:
3.3.1数据类型
3.3.1.1 整型
此时在-4(%rbp)上的是局部变量int i
立即数的话 直接以$参与操作,如上述将0复制到-4(%rbp)
3.3.1.2 数组
字符串数组char *argv存放在rsi上
3.3.1.3 字符串
3.3.2 赋值操作
调用mov函数
既可以赋值直接数,也可以从寄存器里取
3.3.3 类型转换
参数放在%rdi后,调用atoi函数,将字符串化为整型
3.3.4 算术操作
对于i++,通过add进行增加
Sub减少
3.3.5 关系操作
通过cmp比较,并用je跳转。相等则跳转
3.3.6 控制转移
对于判断argc是否等于4中,如果argc不等于4则继续,否则就跳转到L2,形成if。
3.3.7 函数操作
main 函数:
a) 传递控制,main 函数因为被调用 call 才能执行(被系统启动函数 __libc_start_main 调用),call 指令将下一条指令的地址 dest 压栈, 然后跳转到 main 函数。
b) 传递数据,外部调用过程向 main 函数传递参数 argc 和 argv,分别使用%rdi 和%rsi 存储,函数正常出口为 return 0,将%eax 设置 0 返回。
c) 分配和释放内存,使用%rbp 记录栈帧的底,函数分配栈帧空间 在%rbp 之上,程序结束时,调用leave 指令,leave 相当于 mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后 ret 返回,ret 相当 pop IP,将下一条要执行指令的地址设置为 dest。
Exit函数
传递参数:1
控制传递:call调用
Printf函数
传递参数 设置rsi rdx
第一次因为只有一个字符串参数,所以call puts
第二梯次printf使用call printf@PLT
Atoi转换
如上所述
Getchar
控制传递 call getchar@PLT
3.4 本章小结
本章介绍了编译的概念与作用,在对hello.s的解析基础上,进一步读懂汇编语句,来了解汇编语句的:数据,表达式,赋值,类型转换,算术操作,关系操作,控制转移,以及函数操作等知识。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
概念
汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件(这里是hello.o)中。
作用
实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。
4.2 在Ubuntu下汇编的命令
命令: gcc -c hello.s -o hello.o
汇编结果:
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
生成.elf文件,以下为.elf文件的部分内容
.elf中的内容
- ELF头:以一个16位的Magic序列开始,描述生成该文件的字大小和字节顺序,剩下部分包括帮助链接器语法分析和解释目标文件的信息,还有节头部表中条目的大小和数量。
(2)节头部表:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
(3)重定位表:链接时通过重定位表修改位置信息,以下的分别对应L0、puts、exit、L1、printf、atoi、sleep、getchar的位置信息。
(4)符号表:存放程序中定义的全局变量和函数的信息。name记录目标名称,value记录符号地址,size记录目标大小,type记录目标类型,是函数还是数据,bind表示全局还是本地。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
代码:objdump -d -r hello.o > hello.asm
与上面的hello.s相比,有以下差别:
分支转移:汇编代码的分支转移是通过形如L0、L1等助记符的段名称进行转移,而在机器语言中不通过段名称转移,而是通过确切的地址,表示为主函数加段内偏移。
访问全局变量:
汇编代码可以看到汇编语言中%edi赋值时直接调用了.LC0这个全局变量。而汇编器在对.s文件进行汇编时会对每一个全局符号的引用产生一个重定位条目,机器语言,.LC0字符串存放在.rodata节中,并且是绝对地址的引用,汇编器对所有全局符号处理完后,链接器将对这些重定位条目指定的符号进行重定位,把这些符号(比如这里的字符串)的真正地址计算出来并生成最终的可执行文件。
函数调用:汇编代码的call后面直接跟函数的名称,而在反汇编代码中,call后面加由主函数加偏移量构成的下一条指令的地址,因为机器语言中调用的函数在共享库中,无法确定位置,所以相对位置为0,,在重定位表中为其设置偏移量,等待进一步确认。
进制:
Hello.s里立即数是十进制
而hello.o反汇编生成代码中立即数是16进制
一一映射关系:每一条汇编代码都可以用二进制机器指令表示,每一条机器指令由操作码和操作数构成,从而建立起一一对应关系。
4.5 本章小结
(以下格式自行编排,编辑时删除)
本章介绍了程序生成过程中编译器汇编的相关内容,汇编过程将汇编语言转换为机器代码,生成可重定位的目标文件,使机器能够直接处理与执行。
分析了可重定位文件的ELF头、节头部表、符号表和可重定位节。
比较了hello.s和hello.o反汇编代码的不同之处
(第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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello > hello1.elf
- ELF头:
type从REL变为EXEC
程序头起点 以及起始位置给出
节点数量从14变为27
2.重定位表
3.符号表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
由Data Dump可知从0x400000开始0x401ff0结束
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
【命令】objdump -d -r hello > odj_hello.s
加入了在hello.c中用到的库函数,如printf、getchar等函数
实现了调用函数时的重定位,因此在调用函数时调用的地址已经是函数确切的虚拟地址
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址。
5.6 hello的执行流程
5.7 Hello的动态链接分析
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
5.8 本章小结
在本章中主要介绍了链接的概念与作用。
并简单描述了hello.o是怎么链接成为一个可执行目标文件的过程。
介绍了hello.o的ELF格式和各个节的含义。
分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程.
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。
作用:进程为用户提供了以下假象:
1.我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
2.处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
处理流程:
(1)从终端读入命令行
(2)切分命令行字符串获得参数
(3)检查第一个命令行参数是否内置,是则执行,不是则fork创建子程序
(4)子程序中调用(2)获取参数,调用execve()执行
(5)前台作业等待作业终止后返回
(6)后台作业shell返回
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
在hello中,输入./hello 120L021319 LI 2
因为hello不是内置指令,所以分割后面字符串,作为参数,
用fork()创建子程序,并调用参数执行。
6.4 Hello的execve过程
当execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。如果没有出现错误,execve正常加载运行hello,调用后就不会返回。
1.删除已存在的用户区域:删除当前进程的虚拟地址的用户部分中的已存在的区域结构
2.映射私有区域:为hello的代码、数据、bss和栈区域创建新的数据结构。私有的、写时复制。
3.映射共享区域:将hello与共享对象动态链接,再映射到用户虚拟地址空间中的共享区域内
4.设置程序计数器(PC)设置hello上下文中的程序计数器
6.5 Hello的进程执行
6.5.1 逻辑控制流和时间片:
进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
6.5.2 用户模式和内核模式:
用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。
内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
6.5.3 上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.4 调度的过程:
在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
6.5.5 用户态与核心态转换:
为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。
核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
中断:来自I/O设备的信号,执行下一条指令
陷阱:有意的异常,执行下一条指令。
故障:可能修复的错误情况,如果能修正则返回命令重新执行,否则终止。
终止:不可恢复的错误,终止程序。
正常:
回车:
Ctrl+c:
Ctrl+z+ps:
Jobs:
Pstree:
6.7本章小结
本章了解了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。
线性地址:地址空间(address space) 是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space) 。就是hello里面的虚拟内存地址。
虚拟地址:CPU 通过生成一个虚拟地址(Virtual Address, VA) 。就是hello里面的虚拟内存地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成.
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。
虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小时相同的。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在磁盘的起始地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB,即转译后备缓冲器,是用于缩短虚拟寻址时间的小缓存,其每一行都保存着PTE块,这就使得如果请求的虚拟地址在TLB中存在,它将很快地返回匹配结果。
如果没有TLB,对线性地址的访问,就需要首先从PGD中获取PTE(第一次内存访问),在PTE中获取页框地址(第二次内存访问),最后访问物理地址,总共需要3次RAM的访问,远比一次访问要麻烦得多。
变换过程可以分成以下几步:
首先将CPU内核发送过来的32位VA[31:0]分成三段,前两段VA[31:20]和VA[19:12]作为两次查表的索引,第三段VA[11:0]作为页内的偏移,查表的步骤如下:
⑴从协处理器CP15的寄存器2(TTB寄存器,translation table base register)中取出保存在其中的第一级页表(translation table)的基地址,这个基地址指的是PA,也就是说页表是直接按照这个地址保存在物理内存中的。
⑵以TTB中的内容为基地址,以VA[31:20]为索引值在一级页表中查找出一项(2^12=4096项),这个页表项(也称为一个描述符,descriptor)保存着第二级页表(coarse page table)的基地址,这同样是物理地址,也就是说第二级页表也是直接按这个地址存储在物理内存中的。
⑶以VA[19:12]为索引值在第二级页表中查出一项(2^8=256),这个表项中就保存着物理页面的基地址,我们知道虚拟内存管理是以页为单位的,一个虚拟内存的页映射到一个物理内存的页框,从这里就可以得到印证,因为查表是以页为单位来查的。
⑷有了物理页面的基地址之后,加上VA[11:0]这个偏移量(2^12=4KB)就可以取出相应地址上的数据了。
这个过程称为Translation Table Walk,Walk这个词用得非常形象。从TTB走到一级页表,又走到二级页表,又走到物理页面,一次寻址其实是三次访问物理内存。注意这个“走”的过程完全是硬件做的,每次CPU寻址时MMU就自动完成以上四步,不需要编写指令指示MMU去做,前提是操作系统要维护页表项的正确性,每次分配内存时填写相应的页表项,每次释放内存时清除相应的页表项,在必要的时候分配或释放整个页表。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,在TLB和四级页表支持下变换成物理地址,将物理地址拆分成标记位、组索引、块偏移三部分。先在L1cache中通过组索引找到所在的组。把所在组中的所有行的标记位和物理地址中的标记位匹配,若匹配成功且行的有效位是1,则匹配成功。在相应行中通过物理地址的块偏移,即取出相应字节,将其返回CPU。若匹配不成功,向L2cache、L3cache、内存中继续匹配直到找到为止。匹配成功后,向上一级返回直至L1cache,若上一级中有空余位置就放到空余位置中,若没有,则驱逐一块内容,将目标块放到被驱逐的块位置上。
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve加载后,新的hello程序代替原先的程序,删除当前虚拟地址中用户部分已存在的区域结构。映射私有区域,为新程序的代码、数据、堆栈创建新的区域结构。映射共享区域,与动态链接到这个程序的共享程序链接,映射到虚拟地址的共享区域中,再设置程序计数器,指向代码入口处。
7.8 缺页故障与缺页中断处理
缺页概念:DRAM缓存不命中称为缺页,即虚拟内存中的字不在物理内存中。CPU引用了虚拟页的一个字,地址翻译硬件从内存中读取了该虚拟页对应的页表条目,从有效位推断出该页未被缓存,这样就触发了一个缺页异常,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,把要缓存的页缓存到牺牲页的位置。如果这个牺牲页被修改过,就把它交换出去。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU 就能正常翻译VA了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
1. 显式分配器:要求应用显式地释放任何已分配的块。
2. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
显式空闲链表管理:堆可以将空闲的块组织成一个双向空闲链表,每个空闲块中包含一个前驱指针和一个后继指针。此外,还需要一个边界标记,便于块合并。此时可以通过地址顺序或进出顺序管理链表,从而管理空闲空间。
隐式空闲链表管理:每个块由一个字的头部、有效载荷、和一些其他填充组成,空闲块通过头部的大小字段隐形的连接着,形成隐式空闲链表。遍历时分配器遍历所有块,从而遍历所有空闲块。有三种适配方法,第一种首次适配,从头遍历直到找到适合的空闲块。第二种下一次适配,从上次适配遍历过的块的下一个块开始遍历,减少无用情况。第三种最佳适配,遍历所有块找到最适合、剩余空闲空间最小的块。
7.10本章小结
简单了解了hello的存储器地址空间;
指明了逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理。
包含了TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问;
讲述了hello进程fork,execve时的内存映射,最后概括了动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口的几种操作:
1. 打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
2. shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
3. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
4. 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。
5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O 函数:
1. int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2. int close(int fd);
关闭一个打开的文件。
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的实现分析
(以下格式自行编排,编辑时删除)
[转]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;
}
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
我们来看一下这条语句:
va_list arg = (va_list)((char*)(&fmt) + 4);
(char*)(&fmt) + 4) 表示的是...中的第一个参数的地址
更直观的解释如下:
我们再来看一下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);
}
所以说:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
再看write函数:
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
再看一下syscall
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串就显示在了屏幕上。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
函数体如下:
int getchar(void)
{
static char buf[BUFSIZ];//缓冲区
static char* bb=buf;//指向缓冲区的第一个位置的指针
static int n=0;//静态变量记录个数
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;//并且指向它
}
return(--n>=0)?(unsigned char)*bb++:EOF;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
本章主要介绍了Linux的IO设备管理方法,Unix IO 接口以及函数;
最后简单分析了printf和getchar函数的实现。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
- 首先编写程序,诞生hello.c
- 进行预处理,变成hello.i
- 编译处理,变成hello.s
- 汇编处理,变成hello.o。此时变成了二进制文件
- 进行链接处理,生成可执行目标文件hello
- Shell上输入./hello运行hello程序
- 首先判断是否为内置,发现不是,当作程序执行
- 调用fork函数创建子进程
- 调用execve函数,将新创建的子进程的区域结构删除,然后将其映射到hello程序的虚拟内存,然后设置当前进程上下文中的程序计数器,使其指向hello程序的入口点。
- 执行,CPU分配时间片。
- 访问内存,通过虚拟内存访问物理内存
- 动态内存分配
- 接受程序异常信号与用户请求
- 父进程回收子进程,终止。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello虽小,五脏俱全。
它的底下是整个计算机系统的基础,理解该程序对我们日后的学习有着很好的帮助。
不过本次实验还是有一些不太懂的地方,回头再看看,希望能有些新的收获~
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello:链接得到的可执行文件
hello.c:源文件
hello.i:预处理后得到的文件
hello.o:汇编后得到的文件
hello.s:编译后得到的文件
hello.elf:hello.o的elf格式
hello1.elf:hello的elf格式
hello.asm:查看hello.o
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)