计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2021年6月
本文以程序hello.c为线索,以其预处理、编译、汇编、链接、执行的过程为例,从P2P,O2O的角度系统地介绍了一个C语言程序如何从源程序变成可执行程序,到最终被CPU运行的整个过程。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 25 -
6.3 Hello的fork进程创建过程... - 25 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 29 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 29 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 30 -
7.5 三级Cache支持下的物理内存访问... - 31 -
7.6 hello进程fork时的内存映射... - 32 -
7.7 hello进程execve时的内存映射... - 32 -
第1章 概述
1.1 Hello简介
P2P:首先在vim,记事本等编辑器下编辑好hello.c源文件,然后经过cpp预处理可得到修改了的源程序hello.i,经过cc1编译可得到汇编程序hello.s,经过as汇编可得到可重定位目标程序hello.o,最后经ld链接可得到可执行目标程序hello。在shell中输入运行该程序的命令后,shell会通过fork创建一个子进程,至此hello就从一个program变成了一个process,即P2P。
O2O:fork一个子进程后,子进程便有了独立的虚拟地址空间,然后调用execve加载并运行hello程序,加载完成后处理器便开始执行该程序的指令,期间将需要的指令或数据载入物理内存,CPU为运行着的hello程序分配时间片执行逻辑控制流,运行结束后,父进程shell负责回收终止的hello子进程,之后操作系统内核会从系统中删除hello的所有痕迹,即O2O。
1.2 环境与工具
硬件环境:Intel Core i5-9300H CPU @ 2.40GHz;8G RAM;512G SSD
软件环境:Microsoft Windows 10 家庭中文版;Vmware 11;Ubuntu 18.04.5 LTS
开发和调试工具:Visual Studio Code;vim;gcc;gdb;objdump;readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件名字 | 文件作用 |
hello.i | hello.c经cpp预处理得到的修改了的源文件 |
hello.s | hello.i经过cc1编译得到的汇编文件 |
hello.o | hello.s经过as汇编得到的可重定位目标文件 |
hello | hello.o经ld链接得到的可执行目标文件 |
1.4 本章小结
本章主要介绍了hello的P2P和O2O过程,列出了大作业所用到的硬件、软件环境,开发调试工具,列举出了生成的中间结果文件的名字及作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。如将引用的库、头文件等插入到程序文本中,得到一个新的C程序,通常以 .i作为文件扩展名。预处理的内容包括:宏定义,文件包含,条件编译、布局控制等。
预处理的作用:
1、文件包含:将引用的文件插入源程序文本中。如#include。
2、条件编译:进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含。如#if,#ifndef,#ifdef,#endif,#undef等。
3、布局控制:为编译程序提供非常规的控制流信息。如#pragma。
4、宏替换:这是最常见的用法,它可以定义符号常量、函数功能、重新命名、字符串的拼接等各种功能。如#define。
2.2在Ubuntu下预处理的命令
linux> gcc -E ./hello.c -o ./hello.i
图2-2-1预处理过程
2.3 Hello的预处理结果解析
打开预处理生成的hello.i文件可以看到,预处理后的文件有3110行,相比于预处理之前29行的源程序文件多出了很多内容。预处理器将预处理指令#include 替换为了系统头文件stdio,h中的内容,同理也将unistd.h,stdlib.h系统头文件里的内容插入到了源程序文本中。
图2-3-1预处理结果
2.4 本章小结
本章主要介绍了预处理的概念及作用,在linux下对hello.c源文件进行了预处理操作,并对预处理的结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译即编译器(ccl)将文本文件hello.i翻译成文本文件hello.s的过程,得到的hello.s文件包含一个汇编语言程序。
编译的作用:编译器首先检查代码的规范性、是否有语法错误等,并确定代码实际要做的工作,在检查无误后,将高级语言程序翻译成汇编语言程序。除此基本功能外,编译器还具有目标程序优化等功能。
3.2 在Ubuntu下编译的命令
命令:linux> gcc -S ./hello.i -o ./hello.s
图3-2-1编译过程
3.3 Hello的编译结果解析
3.3.1 对各数据类型的处理
1.全局变量
hello.c中定义了一个int型全局变量sleepsecs,并赋初值为2.5,由于sleepsecs是int类型,所以只保留了整数部分2,如图3-3-1-1.
图3-3-1-1全局变量sleepsecs的信息
2.局部变量
局部变量一般存储在栈中或寄存器中,hello.c源文件的main函数里定义了一个局部变量int i,查看hello.s文件可以看到,i在栈中-4(%rbp)的地方,如图3-3-1-2。
图3-3-1-2局部变量i的位置
3.字符串
程序中共出现了两个字符串,均为printf函数的格式控制字符串,如图3-3-3.
图3-3-1-3程序中的字符串
3.3.2 赋值操作
hello.c中有一条赋值语句i=0,赋值在汇编语言中通过mov指令实现,如图3-3-2-1,根据操作数类型的不同,可分为
movb:传送字节
movw:传送字(2个字节)
movl:传送双字(4个字节)
movq:传送四字(8个字节)
图3-3-2-1汇编中的赋值语句
3.3.3类型转换
hello.c中出现了隐式类型转换,即给int型全局变量sleepsecs赋的初值为2.5,但由于sleepsecs是int类型,所以只保留了整数部分2。
图3-3-3全局变量sleepsecs的值
3.3.4 算术操作
hello.c中出现的算术操作为for循环中的i++,汇编中用addl指令实现,如图3-3-4。
图3-3-4 i++的汇编指令
3.3.5 关系操作
1.if语句中出现的argc!=3,在汇编中用cmpl指令实现,该指令计算argc-3的值,并根据结果设置条件码,后续根据条件码中的零标志ZF判断argc和0是否相等,并决定是否跳转。
图3-3-5-1 关系操作!=的汇编指令
2.for循环的循环条件i<10,在汇编中用cmpl指令实现,此处计算i-9的值,并根据结果设置标志位,后续根据符号标志SF和零标志ZF判断i是否<=9,并决定是否跳转。
图3-3-5-2 关系操作<的汇编指令
3.3.6 数组/指针
main函数的第二个参数argv[]是一个字符串数组,数组内容为指向字符类型的指针。由汇编代码可以看出argv的首地址存放在栈中的-32(%rbp)处
图3-3-6-1 argv首地址在栈中的位置
图3-3-6-2中箭头所表示的指令为对argv[2]的访问,方框所标注的内容为对argv[1]的访问,均是通过取出数组首地址,再加上相应的偏移量来访问数组中的特定元素。
图3-3-6-2 对argv的访问
3.3.7 控制转移
1.if (argc != 3),在汇编中用cmpl指令实现,该指令计算argc-3的值,并根据结果设置条件码,后续根据条件码中的零标志ZF判断argc和0是否相等,并决定是否跳转。
2. for(i=0;i<10;i++),i被赋初值0后,程序无条件跳转到.L3,.L3中先用cmpl指令计算i-9的值,并根据结果设置标志位,若i<=9,则跳转到.L4执行循环体,.L4结束时将i++并执行.L3,以此规则重复循环。
图3-3-7-1 控制转移之if/else
图3-3-7-2 控制转移之for循环
3.3.8 函数操作
main函数的调用过程:
1.传递控制:系统启动函数__libc_start_main使用call指令,将返回地址(即下一条指令的地址)入栈,然后跳转到main 函数的起始地址。hello.c源程序中的return 0,对应汇编中将%eax 设置为0,然后使用ret指令返回。
2.参数传递:第一个参数argc保存在%rdi中,第二个参数argv保存在%rsi中,若有更多参数,依次保存在%rdx,%rcx,%r8,%r9中,若有更多参数则保存在栈中。
3.栈帧的分配和释放:将%rbp 作为栈底指针,栈顶指针%rsp下移,为被调用者开辟栈帧。程序结束时,调用leave 指令,恢复栈空间为调用之前的状态,然后通过ret返回。
3.4 本章小结
本章介绍了编译的概念及作用,在linux下实际执行了编译操作,并结合生成的汇编文件hello.s,说明了编译器处理hello.c中出现的C语言的各个数据类型(全局变量、局部变量、字符串)以及各类操作(赋值、类型转换、算术、关系、数组/指针、控制转移和函数操作)的流程和方法。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编是指汇编器(as)将.s文件翻译成机器语言指令,并把这些指令打包成可重定位目标程序的格式,并将结果保存在.o文件中的过程。其中.o文件是一个二进制文件,包含程序代码和数据的机器指令编码。
汇编的作用:将.s文件中的汇编指令翻译成对应的二进制机器指令,汇编生成的可重定位目标文件可以与其他可重定位目标文件链接,形成一个可执行目标文件。
4.2 在Ubuntu下汇编的命令
linux> gcc -c ./hello.s -o ./hello.o
图4-2-1汇编过程
4.3 可重定位目标elf格式
1. ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位的、可执行或共享的)、机器类型(如X86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4-3-1 ELF头的格式
2. 节头部表(section header table):描述了各节的名称、类型、地址、偏移量、大小、对齐要求等信息
图4-3-2 节头部表
3. 重定位节.rela.text:一个.text 节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。节中的8个重定位条目分别是对.L0、puts 函数、exit 函数、.L1、printf 函数、 sleepsecs、sleep函数、getchar 函数重定位信息的描述。其中各项的含义如下:
偏移量Offset:是需要被修改的引用的节偏移。
符号名称Symbol:标识被修改引用应该指向的符号。
类型Type:告知连接器如何修改新的引用。
加数Addend:一个有符号常数,一些类型的重定位需使用它对被修改引用的值做偏移调整。
图4-3-3 重定位节.rela.text
4. 符号表.symtab:存放在程序中定义和引用的函数和全局变量的信息。链接器进行重定位需要引用的符号都在其中声明。各项含义如下:
name:字符串表中的字节偏移,指向符号的以null结尾的字符串名称。
value:对于可重定位目标模块,是距定义目标的节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。
size:目标的大小(以字节为单位)。
type:通常要么是数据要么是函数。
Bind:字段表明符号是本地的还是全局的。
图4-3-4 符号表
4.4 Hello.o的结果解析
1. 汇编语言与机器语言是一一对应的关系,即一条汇编指令对应一条机器指令,因为汇编指令本质上就是机器语言的助记符。
2. 对全局变量sleepsecs的访问,hello.s中使用sleepsecs(%rip) ,hello.o的反汇编中使用0x0(%rip),默认值为0,重定位后将更新为sleepsecs的实际值。
3. 分支转移:hello.s中直接跳转的跳转目标是用.L2,.L3,.L4等标号表示的,而hello.o的反汇编中跳转目标main+偏移量来表示的。
4. 函数调用:hello.s中call指令后跟的是函数名称,hello.o的反汇编中call指令的目标地址是main+偏移量(定位到call的下一条指令),汇编器会在.rela.text 节中为其添加重定位条目,待链接时确定函数的运行时地址,然后更新call指令的编码。
图4-4-1 hello.o的反汇编
4.5 本章小结
本章介绍了汇编的概念及作用,在linux下实际执行了汇编操作,并对生成的可重定位目标文件hello.o的ELF格式进行了详细分析,着重介绍了ELF头、节头部表、重定位节、符号表,最后将hello.o的反汇编结果与之前生成的hello.s文件进行对比,更清晰地理解了汇编的工作原理。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序自动执行的,链接器使得分离编译成为可能。
5.2 在Ubuntu下链接的命令
linux> ld -o hello hello.o -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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/ x86_64-linux-gnu/crtn.o
图5-2-1 执行链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
1. ELF头:描述了文件的总体格式,相比于hello.o可重定位目标文件,hello的类型变成了EXEC(可执行文件),入口点地址也已确定。
图5-3-1 ELF头
2. 程序头部表(program header table):为链接器提供运行时的加载内容和提供动态链接的信息,每一个条目包含各段在目标文件中的偏移、在虚拟地址空间中的位置、目标文件中的段大小、内存中的段大小、运行时访问权限和对齐方式。
图5-3-2 程序头部表
3. 节头部表(Section Headers):共有25个节头,描述了各个节的名称、类型、地址、相对于文件开始的偏移量、大小、对其要求等信息。各节已经被重定位到它们最终的运行时内存地址。
图5-3-3节头部表
5.4 hello的虚拟地址空间
由图5-3-2程序头部表可以看出,代码段从虚拟地址空间的0x400000处开始,用edb加载hello,可以看出程序确实从0x400000处开始,第一部分为ELF头。
图5-4-1 edb查看代码段
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
1. hello的反汇编代码有确定的运行时地址,说明已经完成了重定位,而hello.o反汇编代码中涉及到运行时地址的地方均标记为0,如图5-5-1。
图5-5-1 hello反汇编中确定的运行时地址
2. hello的反汇编代码增加了.plt,.init,.fini节。与hello.o链接的库函数的代码都已经插入到了程序中,如图5-5-2。
图5-5-2 hello反汇编中增加的部分
重定位过程分析:
1. 重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,然后连接器将运行时地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。这一步完成后,程序中每条指令和全局变量都有了唯一的运行时内存地址。
2. 重定位节中的符号引用:在这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构,hello.o的重定位条目如图5-5-3。
偏移量Offset:是需要被修改的引用的节偏移。
符号名称Symbol:标识被修改引用应该指向的符号。
类型Type:告知连接器如何修改新的引用。
加数Addend:一个有符号常数,一些类型的重定位需使用它对被修改引用的值做偏移调整。
图5-5-3 hello.o的重定位条目
以全局变量sleepsecs的重定位为例:
图5-5-4 sleepsecs的重定位条目
ADDR(main)= 0x400500
ADDR(sleepsecs)= 0x601040
offset = 0x60
addend = -4
refaddr= ADDR(main)+offset=0x400500+0x60=0x400560
*refptr=ADDR(sleepsecs)+r.addend-refaddr=0x601040+(-0x4)-0x400560
=(unsigned)0x200ADC
图5-5-5 hello反汇编中重定位后对sleepsecs的引用
5.6 hello的执行流程
子程序地址 子程序名称
0x0000000000400488 _init
0x00000000004004b0 puts@plt
0x00000000004004c0 printf@plt
0x00000000004004d0 getchar@plt
0x00000000004004e0 exit@plt
0x00000000004004f0 sleep@plt
0x0000000000400500 main
0x0000000000400590 _start
0x00000000004005c0 _dl_relocate_static_pie
0x00000000004005d0 __libc_csu_init
0x0000000000400640 __libc_csu_fini
0x0000000000400644 _fini
5.7 Hello的动态链接分析
假设程序调用了共享库里的函数,编译器无法其运行时地址,为了能使得代码段里对数据及函数的引用与具体地址无关,链接器采用延迟绑定的策略。延迟绑定是通过全局偏移量表(GOT)和过程链接表(PLT)之间的交行实现的。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,PLT是代码段的一部分。
图5-7-1 GOT的信息
5.8 本章小结
本章主要介绍了链接的概念及作用,并在linux下实际进行了链接操作,并对生成的hello可执行文件的ELF格式、虚拟地址空间、重定位过程、执行流程、动态链接等方面进行了详细分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程就是一个运行中的程序的实例。
进程的作用:进程向我们提供一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序地指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序,其基本功能是解释并运行用户的指令。
处理流程:
1. shell读取用户由键盘输入的命令行。
2. shell解析命令,获取命令行参数,将各参数保存在argv中。
3. 检查第一个命令行参数是否是一个内置命令,若是,调用相应处理程序,否则调用fork()创建子进程,在子进程中,根据argv中的的参数,调用execve执行指定程序。
4. 若用户未要求后台运行,则shell等待作业终止后将其回收。否则将进程转入后台运行,开始等待用户输入下一个命令。
6.3 Hello的fork进程创建过程
当用户在shell中输入./hello 1190202306 宁天弛,shell就会通过调用fork()函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码段、数据段、共享库以及用户栈。子进程还获得与父进程打开文件描述符相同的一份副本,父进程和子进程的不同在于他们的PID不同。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
6.4 Hello的execve过程
fork 创建子进程之后,子进程调用execve 函数在当前进程的上下文中加载并运行一个新程序hello。新程序会覆盖覆盖当前进程的代码、数据、栈,但拥有和当前进程相同的PID,并继承已打开的文件描述符和信号上下文。execve函数加载并运行可执行目标文件hello,创建一组新的代码、数据、堆和栈段,设置PC 指向_start 的地址,调用main函数,并将控制传递给新程序的主函数,同时传递参数列表argv和环境变量envp。如果出现错误,如hello文件不存在,execve会返回到调用程序,否则execve调用一次从不返回。
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
进程调度:当内核选择一个新的进程运行时,称内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换包含:
1. 保存当前进程的上下文
2. 恢复某个先前被抢占的进程被保存的上下文
3. 将控制传递给这个新恢复的进程。
hello程序开始时运行在用户模式,在调用 sleep 之后转入内核模式,内核处理休眠请求,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当休眠结束时定时器发送一个中断信号,此时进入内核状态执行中断处理程序,将hello进程从等待队列中移出并重新加入到运行队列,之后hello进程就可以继续进行自己的逻辑控制流。
图6-5 上下文切换(图源百度)
6.6 hello的异常与信号处理
6.6.1 异常
1. 中断:程序运行过程中随时可能有来自外部I/O设备的信号引起的中断。
2. 陷阱:使用了sleep,exit等系统调用。当用户程序调用sleep函数时,会执行一个syscall指令,将控制转移给内核,内核将运行陷阱处理程序,解析参数,调用sleep函数,调用后返回到用户进程中引起异常的下一条指令。
3. 程序执行时可能存在缺页故障。
6.6.2 信号处理
1. 程序正常执行
图6-6-1 程序正常执行
2. Ctrl-Z
在程序执行过程中,用户键入Ctrl+Z,内核会发送一个SIGTSTP信号到前台进程组中的每个进程,子进程hello被停止(挂起),成为后台挂起进程。同时父进程shell收到SIGTSTP信号后,调用信号处理程序,打印提示信息,并开始等待用户输入下一条命令。输入ps命令,可以看到hello在进程列表中。输入fg 1将后台hello程序变更到前台继续运行,hello子进程继续从被停止的位置执行,打印完10条信息后,读入用户输入的任意字符,然后进程终止。此时再次使用ps命令,发现hello已不在进程列表中。
图6-6-2 用户键入Ctrl-Z
3. Ctrl-C
用户通过键盘输入Ctrl-C会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。按下Ctrl-C后使用ps命令,发现进程列表中没有hello,表明hello已被终止。
图6-6-3 用户键入Ctrl-C
4. 不停乱按
在程序运行中乱按不会影响程序正常运行,输入会被缓存到stdin,当程序运行getchar函数的时候,会读取一个以’\n’结尾的字符串作为输入。
图6-6-4 乱按
6.7本章小结
本章介绍了进程的概念及作用,shell的作用及其处理流程,并分析了hello的fork进程创建过程、execve过程和进程执行过程,最后根据不同情况分析了hello运行过程中的异常和信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在有地址变换功能的计算机中,访存指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存中的实际有效地址,即物理地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
虚拟地址:即线性地址。
物理地址:物理地址用于内存芯片级的单元寻址,CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在保护模式下,段描述符占8个字节,无法直接存放在段寄存器中(段寄存器只有2字节)。X86的设计是段描述符存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。一个逻辑地址由两部份组成,段标识符: 段内偏移量。
逻辑地址到线性地址的变换方法:
1. 给定一个完整的逻辑地址[段选择符:段内偏移地址],首先根据T1的值,确定当前要转换是GDT中的段,还是LDT中的段,再依据对应寄存器,得到其地址和大小。
2. 根据段选择符中前13位,在数组中查找到相应的段描述符,获得基地址。
3. 将基地址加上偏移量,就得到要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址,即虚拟地址,虚拟内存被分割成称为多个虚拟页(VP),类似地,物理内存被分割为物理页(PP)。系统使用页表将虚拟页映射到物理页,每一个页表条目由有效位和一个n位的地址字段组成。如果设置有效位说明该页已缓存到物理内存,否则未缓存。有效位为0且地址字段不为空时指向一个虚拟页在磁盘上的起始地址。
CPU通过MMU将虚拟地址翻译成物理地址,通过VPN找到对应的页表条目,如果已缓存则命中,否则不命中,发生缺页故障,需要操作系统内核与硬件合作处理。此时MMU会选择一个牺牲页,用将产生缺页的虚拟页替换牺牲页,并更新页表,然后重新执行地址翻译。
图7-3-1 基于页表的地址翻译
图7-3-2 地址翻译用到的符号
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。为了降低不命中带来的巨大时间开销,在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相连度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。
四级页表:VPN被解释成从低位到高位的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则该表项存储的是PPN。在上述过程中,只要有一级页表条目的有效位为0,下一级页表就不存在,对子页表的访问将产生缺页故障,需要从磁盘载入内存。
图7-4-1 四级页表的原理
7.5 三级Cache支持下的物理内存访问
L1 Cache是8路64 组相联。块大小为 64字节。因为共64组,所以需要 6位 CI作为组索引,因为块大小为64字节所以需要 6位CO表示数据块内偏移,因为PA 共52位,所以 CT 共40位。7.4中我们已经将VA转换为PA,,使用CI进行组索引,将CT与组内的8个块的标记分别进行比较,如果匹配成功 且块的有效位为1,则命中,根据数据块内偏移CO取出数据返回给CPU。如果不命中,就去下一 级缓存中查询数据,以此类推,直到主存。
图7-5-1 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
fork函数为新进程创建各种数据结构,并给它分配一个唯一的PID。为了给新的hello进程创建虚拟内存,它创建了当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构都标记为私有的写时复制。fork在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
fork创建hello子进程后,在子进程中调用execve函数,加载并运行可执行程序hello,主要步骤如下:
1. 删除已存在的用户区域,也就是将shell与hello都有的区域结构删除。
2. 然后映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构,均为私有的、写时复制的。映射共享区域,将一些动态链接库映射到hello的虚拟地址空间。
3. 设置PC,使之指向hello程序的代码入口。
经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。
图7-7-1 execve的内存映射
7.8 缺页故障与缺页中断处理
当指令引用一个虚拟地址,通过查找页表发现,该虚拟地址对应的物理地址所在的物理页不在内存中,需要从磁盘调入,即发生了缺页故障。
当发生缺页故障时,控制转移到处理程序,处理程序从磁盘加载相应的页面,然后将控制转移给引起缺页故障的指令。接着指令再次执行,相应的物理页面已被加载到内存中,页面命中。
图7-8-1 缺页中断处理
7.9动态存储分配管理
动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
显式分配器的实现:
1. 隐式空闲链表:空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
图7-9-1 隐式空闲链表
2. 显式空闲链表:显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如在每个空闲块中,都包含一个前驱与一个后继指针。
图7-9-2 显式空闲链表的块结构
3. 分离空闲链表:分配器维护一个空闲链表数组,每个空闲链表和一
个大小类关联,链表是显式或隐式的。
图7-9-3 分离空闲链表
7.10本章小结
本章首先介绍了逻辑地址、线性地址、虚拟地址、物理地址的概念及其关系,接着分析了逻辑地址到线性地址的转化、线性地址到物理地址的转化,TLB与四级页表支持下的VA到PA的变换以及三级Cache支持下的物理内存访问。最后介绍了hello进程fork、execve时的内存映射,缺页故障及其处理以及动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
1. 打开和关闭文件
打开文件:进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的,int open(char *filename, int flags, mode_t mode),其中open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
关闭文件:进程通过调用close 函数关闭一个打开的文件。int close(int fd)。
2. 读写文件
应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
3. 读取文件元数据
应用程序可以通过调用stat和fstat函数,检索到关于文件的信息(元数据)。stat函数以一个文件名作为输入,并填写一个stat数据结构中的各个成员。Fstat函数是相似的,只不过是以文件描述符而不是文件名作输入。
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
8.3 printf的实现分析
printf参数中的…表示传递参数的个数不确定,arg是一个字符指针,表示…中的第一个参数,即输出的时候格式化串对应的值。
图8-3-1 printf的函数体
vsprintf 程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
图8-3-2 vsprintf函数
write(buf,i)函数接受buf与需要输出的参数个数,执行写操作,把buf中的i个元素的值输出。write函数中,先给寄存器传递参数,然后执行系统调用sys_call
图8-3-3 write函数的汇编代码
sys_call将字符串“Hello 1190202306 宁天弛”中每个字符对应的ASCII码值复制到显存中。字符显示驱动子程序根据ASCII找到字模库相应的字形,并将每一个点的RGB颜色信息写入到显示vram,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
图8-3-4 sys_call的实现
8.4 getchar的实现分析
用户输入的字符被存放在键盘缓冲区中,当用户键入回车之后,getchar开始从stdin流中每次读入一个字符,getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符输出到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:当用户按键时,键盘接口会收到一个该按键的键盘扫描码,同时产生一个中断请求,中断请求运行键盘中断子程序,从键盘接口取得该按键的扫描码,将按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了linux的IO设备管理办法、unix IO接口及函数、printf的实现分析和getchar的实现分析。
(第8章1分)
结论
1、首先在vim,记事本等编辑器下编辑好hello.c源文件。
2、hello.c经过cpp预处理可得到hello.i,cpp进行了向源程序中插入包含的外部库、宏替换等操作。
3、cc1将hello.i编译成汇编程序hello.s。
4、hello.s经过as汇编可得到可重定位目标程序hello.o。
5、hello.o最后经ld链接可得到可执行目标程序hello。
6、在shell中输入运行hello的命令后,shell会通过fork创建一个子进程,至此hello就从一个program变成了一个process。
7、fork一个子进程后,hello进程便有了独立的虚拟地址空间。
8、调用execve加载并运行hello。
9、CPU为运行着的hello程序分配时间片执行逻辑控制流。
10、执行期间需要的指令或数据被从磁盘载入物理内存,还有可能被缓存在cache中。
11、运行结束后,父进程shell负责回收终止的hello子进程,之后操作系统内核会从系统中删除hello的所有痕迹。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
hello.i | hello.c经cpp预处理得到的修改了的源文件 |
hello.s | hello.i经过cc1编译得到的汇编文件 |
hello.o | hello.s经过as汇编得到的可重定位目标文件 |
hello | hello.o经ld链接得到的可执行目标文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant,David R.O'Hallaron.深入理解计算机系统[M].机械工业出版社:北京,2016.7:1.
[2] https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%
9D%80/3283849?fr=aladdin
[3] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)