计算机系统
大作业
题 目 程序人生-Hello’s P2P
学 号 120Lxxxxxx
班 级 20xxxxx
学 生 赵xx
指 导 教 师 xx
计算机科学与技术学院
2021年5月
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
本文以c语言程序hello.c的生命周期为脉络,系统地简要介绍了从hello.c编写完成,进行预处理、编译、汇编、链接的步骤最终生成可执行目标文件hello的过程,以及hello在机器上运行时的进程管理、存储管理和IO管理的概念与方法简述,为读者展示了程序的生命历程以及计算机系统的工作流程,使读者能够构建一个清晰具体的计算机系统运作原理。
关键词:程序,生命周期,计算机系统,linux系统;
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.程序员进行编程,写出hello.c文件。
2.进行预处理生成hello.i文件;进行编译生成汇编语言文件hello.s;进行汇编生成可重定位目标文件hello.o;用连接器将hello.o与ld连接起来生成可执行目标文件hello。
3.在shell中运行hello,用fork为其创建进程并用execve执行程序,调用mmap将文件与虚拟内存相映射。
4.cpu会为hello分配时间片。根据指令集,cpu以流水线的形式执行hello。
5.OS和MMU会为hello程序设置从虚拟内存空间到物理内存的映射,硬件系统也会通过TLB,4级页表,3级cache等方式加快程序运行速度。当内核发现异常时会抛出信号并将运行权限转给信号处理程序,待处理完成后再转回hello。
6.运行结束后,shell会回收hello的进程,OS也将取回其使用的内存空间。
1.2 环境与工具
硬件:Intel(R) Core(TM) i7-4720HQ CPU @ 2.60GHz 2.60 GHz
软件:Windows 10 64位, VirtualBox 6.1.32, ubuntu20.04.4
开发环境与工具:Visual Studio 2017 64位,gcc,objdump,gdb,edb等
1.3 中间结果
hello.c:由程序员编写的源程序,使用c语言。
hello.i:hello.c通过预处理得到的文件,进行了一定的头文件展开和符号替换。
hello.s:编译形成的汇编语言文件。将代码编译为汇编语言模式,各部分分块加上汇编语言便于机器处理。
hello.o:通过汇编将hello.s汇编为机器码形式的可重定位目标文件hello.o。
hello:通过链接器将hello.o与ld链接起来得到可执行目标文件。
hello.elf:hello文件的elf格式。
asm.txt:hello的反汇编结果。
asmo.txt:hello.o的反汇编结果。
1.4 本章小结
第一章介绍了hello.c从编写出来到运行再到最后被回收的大致历程,并说明了本次大作业所使用到的软硬件和运行环境,以及一些调试工具。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理,是预处理器(cpp)根据以’#’开头的命令(如include,define)修改原始的C程序。比如#include<stdio.h>就是让cpp读取系统头文件stdio.h的内容并直接1插入到程序文本中。预处理会得到另一个c程序,一般以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
在终端中输入gcc –E hello.c –o hello.i即可生成预处理文件。
2.3 Hello的预处理结果解析
可以看到,预处理后的hello.i文件比hello.c多了许多行,预处理将hello.c中的#开头的命令扩展为具体的代码实现,并对#define命令的符号进行了替换。最后将hello.c不含#的命令原样复制到hello.i的结尾。
2.4 本章小结
本章主要介绍了预处理的概念与作用,并将hello.c在Ubuntu系统上运行gcc –E hello.c –o hello.i来生成了一个预处理文件hello.i,然后从hello.i中更加具体的了解了预处理对头文件等’#’号开头的命令的扩展功能。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
(以下格式自行编排,编辑时删除)
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
编译是指编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,其中包含一个汇编语言程序。每条语句都以汇编语言格式描述了一条低级机器语言指令。这种汇编语言的形式不仅有利于下一步机器对程序的理解和处理,同时也为不同的高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
输入编译指令gcc -S hello.i -o hello.s,生成hello.s文件。
3.3 Hello的编译结果解析
3.3.1伪指令解释
在文件开头由许多行以’,’开通的伪指令,这些命令用来指导汇编器和链接器工作。其中:
.file为源文件名
.text为代码段
.section .rodata为只读数据段
.align是声明对指令或者数据的存放地址进行对齐的方式
.string是对字符串进行声明,比如在hello.s中开头处对源文件中的两个printf函数的格式化输出字符串进行了声明
.global是声明一个全局变量
.type是声明一个符号是数据类型的还是函数类型的
.cfi告知汇编器生成相应的调试信息
.long定义一个长整型,并为它分配空间,占四字节
3.3.2数据
1.argc:是main的第一个参数,在.s文件中可以分析出运行main函数时储存在rbp-20的值所指向的位置,之前则储存在堆栈中
2.argv:是main的第二个参数,储存在rbp-32所指向的位置,argv是一个char型的指针数组,每一个元素为8位,一共四个元素。
3.int i:是main中的局部变量,用来作为for循环的循环变量。其值位与-4(%rbp)处,用于与立即数7做比较,小于等于7则跳转到.L4来继续循环。
4.立即数:在.s文件中以$n的形式表示值为n的立即数,代表源代码中直接写出的n,如1、2、3等。
5.字符串:源程序中在printf函数中由两个字符串,都在hello.s中以.string进行了声明,通过后续的汇编代码运行跳转来使用这些字符串。
3.3.3赋值
hello.c只在循环中为i赋值,出现在hello.s的语句:addl $1, -4(%rbp)来实现循环中的累加,而在开始循环时用movl $0, -4(%rbp)实现循环变量i的赋值为0。
3.3.4算术操作
hello.c中只有i进行了算数操作,在循环中每次加1,在汇编代码中使用addl $1, -4(%rbp)来实现循环中的累加。
3.3.4关系运算
hello.c中对argc与4、i与8进行了比较,在汇编代码中则以cmp指令来实现。判断argc!=4时 cmpl $4, -20(%rbp) je .L2来让两者相等时直接执行循环部分,跳过if后的语句;判断i<8时则执行cmpl $7, -4(%rbp) jle .L4来让i<=7时继续跳转回去执行。
3.3.5数组指针操作
hello.c中使用了argv,这是一个字符指针型数组,argv本身则是一个指针,指向该数组的首地址。Argv使用’[i]’来访问成员,这等价于*(argv+i),在汇编指令中则采用-32(%rbp)加上8的倍数的偏移量来取出各个元素值(取8的倍数则是因为char*类型是8位的)。
3.3.6控制转移
在hello.s中,汇编代码通过比较指令cmp和跳转指令jx来实现控制的转移,cmp后会比较源程序中所比较的两个值并根据结果修改条件码,然后根据所要判断的具体关系选择合适的jx来跳转到对应的代码区域继续执行。
3.3.6函数操作
hello.s中通过call指令调用main、printf等函数,调用时会将下一条指令压入栈中,然后将参数按从指定寄存器到栈的顺序存放(规定的参数寄存器满后剩余参数则存到栈中,hello.s中没有那么多参数),最后跳转到call中所写的位置。待到函数执行完毕后,会将栈复原,并执行ret跳转到之前存在栈中的那个call的下一条指令。
3.4 本章小结
本章解释了编译器生成hello.s文件时对于伪指令、数据、赋值、算术、关系、数组与指针、控制转移、函数操作等部分的处理方法,经过这些操作,生成的hello.s文件就是一个用汇编语言实现hello.c的功能的汇编语言文件。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,将这些指令打包成一种可重定位目标程序的格式,并将结果保存在目标文件hello.o中,这样的过程是程序的汇编,其中hello.o是一个二进制文件。
4.2 在Ubuntu下汇编的命令
运行指令gcc -c hello.s -o hello.o,将完成汇编并生成hello.o文件
4.3 可重定位目标elf格式
在linux终端中输入readelf -a hello.o > hello.elf命令,将会把hello.o的ELF格式输入到hello.elf文件中,然后在对其分析。
4.3.1ELF头
ELF头是在ELF文件开头以一个16字节的序列为起始的部分,这一序列描述了系统的字的大小和字节顺序。后面的部分则是用来帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件类型、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。(节头部表描述了不同节的位置和大小,目标文件的每个节都在其中有一个固定大小的条目)。
4.3.2节头表
节头表是包含各个节的信息的表格,包括节的名称、类型、地址、偏移量、大小、flags、链接、信息和对齐方式。
4.3.3重定位节
ELF格式文件中有一部分重定位条目的节,其中储存了重定位条目的信息,包括偏移量,符号,重定位类型和addend值等,用来提供给后续链接步骤中的链接器进行重定位操作。.rela.txt中存放全局变量和被调用函数的重定位条目,.rela.data存放初始化数据的重定位条目,.rela.eh_frame节则存放.eh_frame节的重定位条目。
4.3.4符号表
.symtab是符号表,包括了程序中定义和引用的函数与变量的信息。想要在ELF格式文件中获得符号表,不需要在编译时使用-g选项,每个ELF文件在.symtab中都有一个符号表,但与编译器中的符号表不同,ELF中的符号表不包括局部变量的信息。程序员可以用STRIP命令去掉.symtab。
4.4 hello.o的结果解析
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
1)条件判断
机器语言反汇编的条件判断使用的是立即数0x4与栈中存放的argc的值进行比对,相等则跳过,不等则执行。虽然汇编语言中的判断也是如此,但有两点不同,一是汇编语言中使用的立即数是十进制的4而非十六进制的0x4;二是汇编语言中跳转语句的目标位置使用的是段名称标识符,比如.L1、.L2等,而由于进行汇编后这些符号便被舍弃了,所以机器语言中的跳转目标使用的是以偏移量形式写出的地址,比如je 2f <main+0x2f>。
汇编语言 机器语言反汇编
2)函数调用
在汇编语言中,函数调用是形如call puts@PLT直接使用函数名来调用的,而在机器语言中则改为使用偏移量的表示方法,比如
中调用printf就是使用相对于main的偏移作为地址,并在命令后附上一个printf的重定位条目,用于后续链接时对printf的调用进行重定位。
3)立即数进制
如第一条所说,汇编代码中的立即数使用的是十进制,而机器语言中则被替换为了十六进制,这应该是因为机器难以直接利用十进制所致。
4.5 本章小结
本章介绍了生成hello的过程中汇编步骤的概念和作用,并通过分析ELF格式的结构和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.3 可执行目标文件hello的格式
运行指令readelf -a hello > hello.elf来生成elf文件。
1)ELF头
就像可重定位文件的ELF头一样,描述了文件的总体格式,此外它还包括了程序的入口点,也就是当程序运行时要执行的第一条指令的地址。
2)节头部表
节头部表包含了ELF文件中各个节的信息,包括名称、大小、偏移量等,具体如图:
3)程序头部表
比起hello.o的ELF文件,hello的ELF文件多出了一个Program Headers,即程序头部表,链接后的程序将原本的节分成了数个特性不同的段,如只读内存段(代码段)、读写内存段(数据段)等,这样在分配内存空间时,就可以将可执行文件的连续的片映射到连续的内存段,而程序头部表则描述了这种映射关系。其中包含了段的类型、偏移量、起始地址等信息。
4)节到段的对应
可执行文件ELF中加入了从节到段的对应,其中的信息表明了一个段中都包括了哪些节。
5)重定位节
ELF文件中仍然有着重定位节来储存重定位条目的信息。
6)符号表
可执行文件中的符号表部分存储着所有符号的信息,其中符号表.dynsym中的信息来自动态链接器。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb后的虚拟空间如下图
Init函数和fini函数对应两个特殊的节.init和.fini,它们会在main函数运行前和运行后执行,从节头表可知两者地址分别是0x401000和0x401238,这也恰好对应了虚拟地址空间的开头和结尾。而在ELF头中可知程序切入点main的地址为0x4010f0,在虚拟地址空间中0x4010f0则是两端风格的分界线,尽管结果是乱码,但还是可以推测出这是init与main的分界线。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello > asm.txt将反汇编结果输入到asm.txt中。
第一眼就可以发现,asm.txt比起asmo.txt,多出了许多形如<function>的节,这是经过链接后,在可执行文件中生成的外部函数。当运行到需要这些函数的时候,程序会根据call指令后经过重定位了的地址跳转到对应的.text节或.data节,这样机器就能一目了然地知道外部函数或全局变量的具体位置,不必再进行其他的操作或修改。
在hello.o中可以看到在调用了函数或全局变量后,下一行会衔接一条形如21: R_X86_64_PLT32 puts-0x4的语句,这就是对应符号的重定位条目,而重定位条目的具体信息则可以在hello.o的ELF文件中查看。在重定位时会经过链接器的重定位算法来根据重定位条目的信息计算重定位后的相对或绝对位置,并将结果写入机器码。经过反汇编后,就形成了汇编语言中调用函数或全局变量时的地址信息。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
执行顺序与地址如下:
_start于0x4010f0
__libc_start_main于0x7ffff7de3fc0
main于0x401125
_printf于0x4010a0
_sleep于0x4010e0
_getchar于0x4010b0
exit于0x4010d0
_fini于0x401238
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
动态链接是程序在运行时通过延迟绑定实现的,其中起着重要作用的就是PLT和GOT两部分分别为代码段和数据段。我们可以在节头表找到.got.plt的地址为0x404000,回到edb中进行运行:
运行前
运行后
可以看出原本为0的部分内容发生了更改,说明程序进行了动态链接,使用了GOT和PLT。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章介绍了hello.o经过链接生成hello的过程以及链接中程序所发生的变化,并通过运行与调试分析了链接过程中的重定向和连接后的具体执行情况等问题。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
(以下格式自行编排,编辑时删除)
进程是一个执行中的程序的实例,指程序的一次运行过程。进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因此进程具有动态含义。
进程提供了逻辑控制流和私有地址空间两个抽象,两者可以让程序员以为自己的程序在独占地使用处理器和存储器。进程的引入简化了程序员的编程和语言处理系统的处理,即简化了编程、编译、链接、共享和加载的整个过程。
6.2 简述壳Shell-bash的作用与处理流程
Shell作为linux系统中的一个交互型应用级程序,通过接受用户输入的命令来执行或调用相应的程序,实现用户与程序的交互。
Shell的处理流程如下:
1)用户输入指令,经过字符串处理函数后被分割和处理为相应的命令和参数,空指令会被跳过
2)检查是否有shell的内置命令,有则直接执行。
3)读取命令与参数并创建子进程,在子进程中加载并运行程序。
4)父进程等待子进程退出后回收子进程,然后父进程退出。
6.3 Hello的fork进程创建过程
输入“./hello 学号 姓名 时长”后,shell判断出这条命令是要运行hello程序,因此会调用fork函数创建子进程,并在fork()=0的条件下(就是子进程在shell程序中的代码部分)于子进程中继续进行hello程序。
新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,子进程获得与父进程任何打开文件描述符相同的副本,而最大的区别则是子进程有不同于父进程的PID。
6.4 Hello的execve过程
在fork产生的子进程中,shell会通过execve函数加载hello程序到此进程中。hello的数据将会覆盖覆盖当前进程的代码、数据、栈并对其进行初始化,进行内存映射,而保留相同的PID,继承已打开的文件描述符和信号上下文。最后将PC指向hello程序第一个执行的函数。
6.5 Hello的进程执行
每个进程都可以看做是一个逻辑控制流,进程每个执行它自己的一段时间就是一个时间片,即使是两个并发的逻辑控制流,在一个被另一个打断后也可以重新回到断点继续运行,尽管物理上进程不是重叠的,但是我们可以认为并发进程是并行运行的。
控制流通过上下文切换来转到另一个进程,这其中要借助内核。内核是内存中管理进程的操作系统代码块,进程的物理实体和支持进程运行的环境被称为进程的上下文,分为用户级上下文和系统级上下文,在上下文切换时,系统将原进程的寄存器上下文移动到系统级上下文的现场信息位置。通过内核模式作为中介,系统将用户模式下的控制流从一个进程转移到另一个进程,这样就避免了直接打断用户进程所导致的异常控制流。
而这样的设计给了用户对内核进行操作的机会,这是不允许的,因此设计了用户态和核心态,只有当遇到中断、故障或系统调用等的时候才会转到核心态,拥有内核的访问权限,其他时候则只出于用户态,以保证系统的安全与稳定。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1)正常运行
2)乱按与回车
此时程序仍然正常运行,只是手动的输入停留在了终端上面,对程序没有影响。
3)Ctrl-Z
输入Ctrl-Z后hello作为前台程序被挂起,但shell并没有回收它。而是将它放在了后台暂留,可以通过输入jobs来查看hello进程
此时运行pstree可以得到一个进程形成的树,展示了进程间的关系
此时可以输入ps来查看各个进程的pid,之后可以根据pid信息输入kill,从而向目标进程发送想要的信号。
我们可以再次运行hello程序,这次在Ctrl-Z后输入fg命令,就可以让停止的hello程序继续从刚才停止的位置运行。
6.7本章小结
本章解释了进程的概念和作用,并系统的讨论了从进程在shell中产生到执行以及异常与信号的处理机制与效果,并说明了进程在用户-内核体系下是如何实现近似独占处理器与存储器的以及如何通过上下文切换来安全稳定地切换当前控制流所执行的进程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1)逻辑地址:hello在编译后在汇编语言中出现的地址,通过一个标识符来确定一条指令的位置,并通过偏移量来确定同标识符下其他指令的位置。
2)线性地址:线性地址是逻辑地址通过段机制转化得来的地址,可以视作是虚拟地址的一种,同时也是逻辑地址到物理地址变换之间的中间层。通过段基址+段内偏移地址来找到对应的代码。
3)虚拟地址:虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组,通过页表的帮助以页为单位映射到物理内存上去。
4)物理地址:物理意义上的内存器件中的存储单元的地址,处理器通过物理地址来查找内存条上的数据位置,并对其进行操作。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和段内偏移量两部分构成,其中段标识符为16位,前13位是段描述符表的索引,通过这个索引可以找到一个段描述符,其中包含了描述一个段的所需信息,段描述符表分为2类,分别为全局段描述符表(GDT)和局局部段描述符表(LDT)。
在计算线性地址时,我们先查看段标识符的T1字段,为0说明要找GDT,为1则要找LDT,然后再段描述符表中找到对应的段描述符,得到其中的base值,即基地址,则此段中的所有指令的线性地址即为基地址加上段内偏移量,即为:base+offset.
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存与物理内存都以页为单位,一个页则是一部分内存块。当系统从虚拟地址去寻找物理地址时,则通过页表来进行中转。
在页式管理的系统中,虚拟地址由虚拟页号VPN和虚拟页偏移VPO两部分组成,物理地址则由物理页号PPN和物理页偏移PPO两部分组成。其中VPN又分成了TLBI(TLB索引)和TLBT(TLB标记)两部分,用于先行查找TLB快表中有无对应数据。
当开始地址变换时,内存管理单元MMU接受虚拟地址VA名据此前往TLB查找,若命中则说明cache中存有所需页表,于是cache返回给MMU以页表条目PTE,经过MMU翻译后得到物理地址PA;如果TLB未命中,则说明VA对应的PTE在页表中,MMU便生成页表条目地址PTEA发送到高速缓存或内存中,高速缓存或内存返回PTE,MMU再从PTE中得到物理地址PA。
在具体查找的时候,会现根据VPN中的TLBI部分搜索TLB中对应的组,再根据TLBT部分搜索组内对应的标识。如果TLB未命中的话,MMU就会根据VPN选择适当的PTE,然后查看对应PTE的有效位,如果为0则发生缺页,进行缺页的处理,反之则通过VPO和PPN来生成物理内存地址,其中PPN为物理内存页的位置,PPO则为物理地址偏移量(相对于页的),PPO与VPO是完全一致的。
7.4 TLB与四级页表支持下的VA到PA的变换
1)TLB
TLB即快表,名为翻译后备缓冲器,是页表的缓存,储存有一些经常使用的页表条目,实现虚拟页面向物理页面的映射,对于页面数很少的页表可以完全包含在TLB中。就像7.3中所讲一样,MMU在接收到VA后会先根据TLBI和TLBT的内容在TLB中寻找PTE,如果未命中的话再去页表中寻找,由于TLB较小且有较高相连度,因此要比直接寻找页表要快,从而加快VA到PA的转换。
2)四级页表
当虚拟地址太多时,一个页表的PTE数量已经无法满足虚拟地址的长度了,此时就要将虚拟地址的VPN分割为数个部分,前一个VPN的PTE对应着作为下一个VPN的寻找范围的页表,这样就能通过相连的页表来表示更长的虚拟地址,这就是多级页表的目的。
对于四级页表,就是将VPN分割为VPN1到VPN4,VPN1在第一级页表中找到PTE1,该PTE1指向数个二级页表中的一个,后续也同理寻找,直到VPN4对应的PTE4指向了一个物理内存,则VA到PA的变换结束。
7.5 三级Cache支持下的物理内存访问
在MMU获得PA后,将根据PA前往高速缓存中寻找数据。PA中有CT、CI和CO三个数据,先根据CI寻找L1中的对应组,然后比对每行的有效位、tag和CO以及CT是否相等。如果在L1中没有命中,则前往L2中进行相同操作,L3同理。在命中后上传数据时,如果目标块已经被占用了,就将目标块中的数据驱逐再传入数据。
7.6 hello进程fork时的内存映射
调用fork函数创建子进程后会为新进程创建虚拟内存。创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.,两个进程中的每个页面都标记为只读,并且两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),这样通过写时复制来保证每个进程都拥有一个独立的虚拟内存,且最大程度减少复制副本的量。随后的写操作通过写时复制机制创建新页。
7.7 hello进程execve时的内存映射
Execve加载并运行新程序的步骤如下:删除已存在的用户区域,创建新的区域结构;代码和初始化数据映射到.text和.data区(目标文件提供);.bss和栈映射到匿名文件;设置PC,指向代码区域的入口点;Linux根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
当发生缺页故障后(一般为PTE的有效位为0,表明该VA没有对应的物理缓存),内核就会中断目前的控制流并转到缺页异常处理程序上来,缺页异常处理程序会在页表中选择一个牺牲页,若牺牲页已经被修改过了就将其存至磁盘,然后取消该牺牲页与其物理地址之间的映射关系,再在发生缺页故障的页与原牺牲页的物理地址之间建立映射。此时原本的故障页已经缓存在内存中了,缺页故障解除,内核再继续运行原本的控制流,原本的故障页就可以正常命中了。
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。malloc函数是一种显式分配器,要求应用显式地释放任何已分配的块,除此之外还有一种隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块,隐式分配器一般用于一些语言的垃圾收集操作中。
每当调用函数malloc(n),系统就会将堆上n个连续的块分配给这次调用,并且调整为已分配状态。然而直接如此分配有许多问题,比如无法控制大小,还必须从空闲块开始分配以及需要块对齐等。
动态内存分配对此有显式空闲链表、隐式空闲链表和分离的空闲链表三种处理办法,前两者就是以不同的形式在块中记录块的长度或前后继信息,而malloc则是使用的第三种方式。
对于分离的空闲链表方式,分配器维护有一个空闲链表数组,每个大小类对应其中一个链表,当分配器需要一个大小为n的块时:搜索相应的空闲链表,使其满足m > n;如果找到了合适的块就拆分块,并将剩余部分插入到适当的可选列表中,如果找不到合适的块, 就搜索下一个更大的大小类的空闲链表直到找到为止。如果空闲链表中没有合适的块就向操作系统请求额外的堆内存 (使用sbrk()),然后从这个新的堆内存中分配出 n 字节,将剩余部分放置在适当的大小类中。
7.10本章小结
本章介绍了存储器的地址空间,从逻辑地址到物理地址的变换过程,以及从虚拟地址寻找物理地址的过程中经由MMU、TLB、Cache等许多部件的处理与应对过程。此外还解释了fork与execve的内存映射、缺页故障以及动态储存分配管理的过程。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
Linux将所有的I/O设备都映射成了文件的形式,经由这种处理Linux内核给出的一个简单、低级的应用接口,能够以统一且一致的方式执行 I/O操作,这就是unix io接口。
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件内核返回一个小的非负整数,作为描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
1)打开文件
调用open函数来打开文件,并返回一个小的描述符数字---- 文件描述符。如果返回的是-1则说明发生了错误。
2)关闭文件
调用close函数关闭一个打开的文件,如果关闭一个已经关闭的文件则会出错。
3)读文件
调用read函数将从当前文件位置复制字节到内存位置,然后更新文件位置。返回值表示的是实际传送的字节数量,类型为ssize_t,实际上是有符号整数。若返回值小于0则说明发生错误。但需注意发生不足值(Short counts) (实际返回值 < 函数参数中规定的传输长度 ) 是可能的,不是错误。
4)写文件
调用write函数来从内存复制字节到当前文件位置,然后更新文件位置,返回值表示的是从内存向文件实际传送的字节数量,若返回值<0同样表示发生错误,不足值也可能在write处出现。
5)设置位置
调用lseek函数可以显式地设置当前在文件中的位置。
8.3 printf的实现分析
首先是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按照输入格式组成arg,然后传到vsprintf函数中,而vsprintf函数原型如下:
根据其上下文分析,vsprintf函数是将传入的目标输出格式化为可输出的字符串,然后返回字符串的长度。
回到printf后调用write函数,输出长为i的buf。之后我们来看write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
其中ebx存有字符串的第一个地址,ecx存有字符串长度,之后来看INT_VECTOR_SYS_CALL:
init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
简单来说就是要通过系统来调用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
sys_call只实现一个功能:显示格式化了的字符串。它将通过总线将字符串中的字节从寄存器复制到显卡的显存。显存存储ASCII字符码,字符显示驱动子程序将控制从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
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;
}
可见,getchar调用了read函数读取键盘上的输入按键ascii码,直到读取到输入回车时,才将所读取的第一个字符返回,并将剩余的字符留在缓冲区。
异步异常-键盘中断的处理:当系统收到键盘输入产生的中断后会中断当前正在运行的进程,并转到键盘中断处理程序。键盘中断处理程序获得键盘输入的扫描码并将其转换为ascii码,然后保存到系统提供的键盘输入缓冲区中,然后键盘中断处理程序结束,处理程序返回到下一条指令。
8.5本章小结
本章描述了unix io接口及其相关函数的功能和用法,并分析了应用了这些函数的具体实例:printf函数与getchar函数。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.编写:程序员使用c语言写出hello.c程序源代码
2.预处理:修改hello.c中的所有#命令,生成hello.i文件
3.编译:将hello.i文件翻译为汇编语言实现的hello.s文件
4.汇编:将hello.s文件翻译为机器码的可重定位目标文件hello.o
5.链接:调用链接器由hello.o生成可执行目标文件hello
6.运行:在shell中输入./hello <学号> <姓名> <间隔时长>
7.创建进程:shell调用fork创建子进程
8.加载程序:shell调用execve将hello程序加载到进程中运行
9.内存空间:系统为hello分配存储空间,完成虚拟内存和内存映射,运行期间hello程序通过MMU翻译虚拟地址来访问并使用对应的物理内存空间
10.异常与信号:运行过程中发生异常或收到信号时,中断控制流并转移到对应处理程序上,完成后控制流回到原位置。
11回收进程:运行完毕后,父进程回收子进程。
经过这次大作业的编写和整个计算机系统的学习过程,我发现计算机系统的设计十分的精巧,从hello.c到hello文件的转换,到各个组件配合完成hello的运行,每个部分各司其职,不同组件互相独立,却又统一在hello的程序下相互影响。而生成hello的每一步骤,看似繁琐,但又必须经过一步步的转化才能形成最适合计算机处理的形式,从这两方面我都感觉到了计算机系统设计的巧妙之处。
列出所有的中间产物的文件名,并予以说明起作用。
hello.c:由程序员编写的源程序,使用c语言。作为程序的本体,是一切的开始。
hello.i:hello.c通过预处理得到的文件,进行了一定的头文件展开和符号替换。是在hello.c的基础上将其中一些比较省略的指令拓展为详细的信息。
hello.s:编译形成的汇编语言文件。将代码编译为汇编语言模式,各部分分块加上汇编语言便于机器处理。
hello.o:通过汇编将hello.s汇编为机器码形式的可重定位目标文件hello.o。便于下一步链接的进行。
hello:通过链接器将hello.o与ld链接起来得到可执行目标文件。
hello.elf:hello文件的elf格式,以人类的自然语言形式展示出了hello文件中的格式以及各个段、节的信息,用于分析链接过程的具体工作情况。
asm.txt:hello的反汇编结果,用于分析hello程序在机器中执行时的具体逻辑,而与高级语言编写的程序的逻辑相区分。
asmo.txt:hello.o的反汇编结果,用于分析hello程序在机器中执行时的具体逻辑,而与高级语言编写的程序的逻辑相区分,同时和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.