计算机科学与技术学院
2021年5月
本文介绍并详细分析了hello程序的完整生命周期:预处理,编译,汇编,链接,在shell中运行,在进程中管理,接受I/O管理,终止回收。通过hello的一生,很好地对计算机系统的各部分知识进行了一个较为完整的回顾和梳理。
关键词:P2P;计算机系统;程序的声明周期;进程;虚拟内存;I/O;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式..................................................................... - 18 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 26 -
6.3 Hello的fork进程创建过程........................................................................ - 26 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 33 -
7.3 Hello的线性地址到物理地址的变换-页式管理......................................... - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 35 -
7.5 三级Cache支持下的物理内存访问............................................................. - 36 -
7.6 hello进程fork时的内存映射..................................................................... - 36 -
7.7 hello进程execve时的内存映射................................................................. - 36 -
7.8 缺页故障与缺页中断处理.............................................................................. - 37 -
8.2 简述Unix IO接口及其函数.......................................................................... - 40 -
第1章 概述
1.1 Hello简介
P2P:在Linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。
020: shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 17.02 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果
中间结果文件 | 文件作用 |
hello.i | 预处理得到的文本文件 |
hello.s | 编译后得到的文本文件 |
hello.o | 汇编得到的可重定位目标文件 |
hello.asm | hello反汇编得到的文件 |
hello | ld得到可执行目标文件 |
1.4 本章小结
本章对hello进行了简单的介绍,分析了其P2P和020的过程,列出了本次任务的环境和工具,并且阐明了任务过程中出现的中间产物及其作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(CPP)根据以字符#开头的命令(宏定义、条件编译等),修改原始的C程序,并将其引用的所有库进行展开合并,生成以.i结尾的文本文件。
作用:
- 删除注释。
- 用实际值代替用#define定义的字符串。
- 将源文件中用#include形式声明的文件复制到新的程序中。
- 条件编译,根据#if后面的条件决定要进行编译的代码。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图表 1 对hello.c文件预处理
2.3 Hello的预处理结果解析
图表 2 hello.i中main函数的位置
打开hello.i文件后发现,文件的内容明显增加,有3000多行,但是仍为可以阅读的C语言文本文件。预处理器对源文件中的宏进行了宏展开,将系统头文件中的内容直接插入到了程序文本中,同时也对#define相应的符号进行了替换。
2.4 本章小结
通过本章了解了c语言在编译前的预处理过程,明白了预处理的概念及作用,学会了GCC下预处理对应的指令,同时解析了预处理文本文件内容,对预处理的结果有了一定的了解,更好的了解了预处理的过程。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。它是以高级程序设计语言书写的源程序作为输入,以汇编语言或者机器语言表示的目标程序作为输出,这个过程称为编译。
作用:可以在转化时对程序的实现进行优化。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图表 3 对hello.i文件进行编译
3.3.1 汇编指令介绍
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.2数据
1,字符串
程序中有两个字符串,位于只读数据段,如下图所示:
这两个字符串作为printf函数的参数
2,局部变量
main函数声明的局部变量i,编译器在进行编译的时候会将其放在堆栈中,本程序中它被放在了栈上-4(%rbp)的位置。
3,main函数的参数argc
用户传递给main函数的参数argc,同样被放到了堆栈,本程序它被放在了-20(%rbp)的位置
4,main函数的参数argv数组
数组中的每一个元素都是一个指向字符类型的指针,数组的起始地址存放于栈中-32(%rbp)的位置。
分别获取了argv[2],argv[1],argv[3]的 地址,并将其地址处的内容存储到相应的寄存器中,用于printf函数和atoi函数的参数传递。
3.33操作
1,赋值操作
本程序的赋值操作,主要是指对i的赋值,其通过mov指令来进行实现。
2,算术操作
这里主要指i++,通过add指令即可实现。
数据算术操作汇编指令 | |
指令 | 效果 |
leaq S,D | D=&S |
INC D | D+=1 |
DEC D | D-=1 |
NEG D | D=-D |
ADD S,D | D=D+S |
SUB S,D | D=D-S |
3,关系操作
本程序中有两处进行了关系的判断。
<1>if条件判断:argc!=4, 进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
<2>for循环的条件判断:i<8,等同于比较i是否小于等于7,被编译为cmpl $7,-4(%rbp)
4,控制转移操作
本程序主要是指if条件分支引起的跳转以及for循环分支引起的跳转。它们主要通过关系操作cmpl进行比较设置条件码,之后根据条件码进行对应的跳转。
5,函数调用操作
在函数调用前,设置用于参数传递的寄存器的值,之后通过call指令进行函数的调用,本程序主要有printf函数的调用,sleep函数的调用,以及atoi函数的调用
3.4 本章小结
通过本章了解了c语言的编译过程,明白了编译的概念及作用,学会了GCC下编译操作对应的指令,同时解析了hello的编译结果,对编译的过程处理有了一定的了解,同时对程序的机器级表示方法更加熟练。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编的概念是指的将汇编语言程序翻译成机器指令,并将这些指令打包成一种叫做可重定位目标程序,并将这个结果保留.o目标文件中。
作用:
1,实现了文本文件到二进制文件的转化。
2,将汇编指令转换成一条条机器可以直接读取分析的机器指令。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
1,查看ELF头
ELF Headers:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。
2,查看节部头表
Section Headers:通过此可知道此elf文件中共有13个节,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,各节的读写权限等。
命令:readelf -S hello.o
3,查看符号表
.symtab:存放程序中定义和引用的函数和全局变量的信息。
Ndx:
ABS表示不该被重定位、UND表示未定义(在这个地方被引用,但是在其他地方进行定义)、COM表示未初始化数据(未初始化的全局变量)
Bind:
绑定属性:全局符号、局部符号
Type:
符号类型:函数、数据、源文件、节、未知
Value:
.o.文件中是偏移量
命令:readelf -s hello.o
4,查询重定位条目
重定位条目:描述了需要进行重定位的各种信息,包括需要进行重定位符号的位置、重定位的方式、名字。
命令:readelf -r hello.o
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
机器语言:是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成。
机器语言与汇编语言的映射关系:每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系。
不同之处:
数据访问:在汇编语言中,访问rodata(printf中的字符串),使用段名称+%rip,而在机器语言对应的反汇编程序中为0+%rip。因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
分支转移: 在汇编语言中,分支转移命令是由助记符来标识,通过用符号.L2等助记符,跳转到相应的位置。而在机器语言反汇编程序中,分支转移命令是直接跳转入目的地址。
函数调用:在汇编语言中,函数调用之后直接跟着函数名称。而在机器语言的反汇编程序中,call的目标地址是下一条指令的地址。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
通过将hello.s汇编指令转换成hello.o机器指令,通过readelf查看hello.o的ELF、反汇编的方式查看hello.o反汇编的内容,比较其与hello.s之间的差别,以及学习汇编指令和机器指令之间的映射关系,更深刻地理解了汇编语言到机器语言实现地转变,和这过程中为链接做出的准备(设置重定位条目等)。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:
当程序调用函数库(如标准C库)中的一个函数printf,printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个函数必须通过链接器(ld)将这个文件合并到hello.o程序中,结果得到hello文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。另外,链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。
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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
1,通过查询hello文件的ELF头信息,我们可以知道hello文件类型为EXEC,即是一个可执行目标文件,文件中共有27个节。
2,通过readelf -S hello 我们可以知道文件中各个段的基本信息,从Size获取各个段的大小,从Address可以获得各个段的起始地址即为程序被载入虚拟地址后各段的起始地址,从offset可以获得各个段在程序中的偏移量。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据5.3的节头部表中显示的各节的地址,通过edb可以查询各个节的信息,例如:.rodata节,虚拟地址开始于0x402000,大小为0x3b
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
对比发现:
1,我们发现hello.asm比hello.o的反汇编代码多出了许多文件节。比如.init节和.plt节(hello.o反汇编代码只有.text节)
2,hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。
查询hello.o的重定位条目:
结合分析可得hello重定位的过程:
1,重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
2,重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
3.重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。
链接的重定位过程说明:要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位(确定地址),修改.text节和.data节中对每个符号的引用(地址),而这些需要用到在.rel_data和.rel_text节中保存的重定位信息。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
libc-2.27.so!exit
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GOT中地址跳转到目标函数。
GOT表位置在调用_init之前0x404008后的16个字节均为0
调用_init函数后,从地址0x4008处,由原来的00 00 00 00 00 00 变为90 91 fb 2f 5d 7f;由原来的00 00 00 00 00 00变为20 2a fa 2f 5d 7f由于小端的缘故,则这两处的地址应该是0x7f 5d 2f fb 91 90 ,0x7f 5d 2f fa 2a 20
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数
5.8 本章小结
本章介绍了链接的概念及作用,在Ubuntu下链接的命令行,并对hello的elf格式进行了详细的分析对比,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,在最后对hello进行了动态链接分析,使得对hello的链接过程有了一个深刻的理解和体会。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例。每个进程都有它自己的地址空间,一般情况下包括代码段、数据段、和堆栈段。
作用:进程主要为用户提供了下列两个假象
(1)一个独立的逻辑流,提供程序独占使用处理器的假象。
(2)一个私有的虚拟地址空间,提供程序独占使用整个系统内存的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是命令行界面,是系统内核的一层壳,作用是用来保护内核同时传递入与计算机交互的信息.它只是系统的一个工具,我们可以使用它来操作计算机。6.3 Hello的fork进程创建过程。
1,从终端读入输入的命令。
2,Shell对用户输入命令进行解析,判断是否为内置命令。
3,如果是内置命令则立即调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。
4,判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。
5,shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
当在shell中运行hello后,父进程通过调用fork函数创建一个新的运行的子进程。其函数原型为pid_t fork(void);其调用一次返回两次,子进程返回0,父进程返回子进程的ID。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的ID。父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
进程图:
-
- Hello的execve过程
1,hello子进程通过execve系统调用启动加载器。
2,加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。新的栈和堆段被初始化为0。
3,通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件中的内容。
4,最后加载器跳到_start地址,它最终调用hello的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
内存映像如下:
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
多任务也叫做多时间片。
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。
当内核调度一个新的进程的运行的时,内核就会抢占当前进程,通过使用一种上下文切换的较为高层的形式异常控制流将控制转移到新的进程。具体如下:内核首先保存当前进程的上下文,之后恢复之前被抢占的进程保存的上下文,将控制传递给这个恢复的进程。
用户态核心态转换:进程hello初始运行在用户模式中,直到它通过执行系统调用函数sleep或者exit时便陷入到内内核。内核中的处理程序完成对系统函数的调用。之后,执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。
6.6 hello的异常与信号处理
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
可能产生的信号有:SIGINT、SIGKILL、SIGSEGV、SIALARM、SIGCHLD
按下Ctrl-C: 在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
按下Ctrl-Z,并运行ps: 输入ctrl-z,内核会发送SIGSTP。SIGSTP默认挂起前台hello作业,但 hello进程并没有回收,而是运行在后台下,通过ps指令可以对其进行查看。
Ctrl-z后运行fg:
Ctrl-z后运行jobs:
Ctrl-z后运行pstree:
Ctrl-Z后按下kill 命令:内核会发送SIGKILL信号给我们指定的pid(hello程序),结果是杀死了hello程序。
终端随意输入:
6.7本章小结
通过对hello的创建、加载和终止,使得对hello执行过程中产生信号和信号的处理过程有了更多的认识,加深了对异常的理解。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o)。
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
虚拟地址也就是线性地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下:逻辑地址CS:EA到物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。
段选择符各字段含义
15 14 | 32 | 10 |
索引 | TI | RPL |
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位
于最低级的用户态,第0级高于第3级。
高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分以固定长度为单位的组,称为页(page)。例如一个32位的机器,线性地址最大可以为4G,用4KB来划分的话整个地址就被划分为2^20个页,这个数组称为页目录,目录中的每个目录项,就是对应页的地址;另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
1、分页单元中,页目录是唯一的,它的地址放在CPU的CR3寄存器中,是进行地址转换的开始点。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到CR3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从CR3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址;
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是MMU中的一个关于PTE的小的缓存,有了TLB后,VPN又分为了TLB标记(TLBT)和TLB索引(TLBI),TLB的机制与全相联的cache的机制相同,如果TLB有T = 2s个组,那么TLB索引(TLBI)是由VPN的s个最低位组成的,TLB标记(TLBT)是由VPN中剩余的位组成。
引入多级页表后,VPN被划分成了多个区域,例如使用k级页表,VPN被划分成了k个VPN,每个VPNi都是一个到第i级页表的索引,第k个VPN中存储着VPN对应的PPN。
CPU产生一个VA,MMU在根据VPN在TLB中搜索PTE,若命中,MMU取出相应的PTE,根据PTE将VA翻译成PA;若没命中,则通过多级页表查询PTE是否在页中,若在页中,找到对应的PIE,MMU将VA翻译成PA,若没有在页中,则进行缺页处理
7.5 三级Cache支持下的物理内存访问
1,得到了物理地址VA,首先使用物理地址的CI进行组索引(每组8路),对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回。
2,若没找到相匹配的或者标志位为0,则miss。那么cache向下一级cache,这里是二级cache甚至三级cache中寻找查询数据。然后逐级写入cache。
3,在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位为0),则写入;若不存在,则进行驱逐一个块(LRU策略)。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的,写时复制的。代码和数据区域被映射为hello文件中的.test和.data区。bss区域是请求二进制0的,映射到匿名文件,其大小包含在a.out当中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域hello 程序与共享对象 libc.so 链接,libc.s是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器:execve做的最后一件事情就是设置当前进程上下文中的程序计数器,指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:当CPU想要读取虚拟内存中的某个数据,而这一片数据恰好存放在主存当中时,就称为页命中。相对的,如果DRAM缓存不命中,则称之为缺页。如果CPU尝试读取一片内存而这片内存并没有缓存在主存当中时,就会触发一个缺页异常。
缺页故障处理:发生缺页故障时,处出发缺页异常处理程序,缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。缺页处理程序调入新的页面,并更新内存中的PTE,缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经缓存在物理内存中,所以就会命中。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
1,隐式空闲链表:
空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
(1)放置策略:首次适配、下一次适配、最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
(2)合并策略:立即合并、推迟合并。立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。
2,显式空闲链表:
每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。
空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。
3,分离的空闲链表
维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。
7.10本章小结
通过对段式和页式存储,页表的存储管理,虚拟地址物理地址的转换,进程的加载时的内存映射,缺页故障和处理,动态内存分配等一系列关于进程存储问题的讨论,对程序运行时OS对内存的相关管理机制以及进程运行的实现有了一定的理解。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件 ,设备管理:unix io接口
一个linux文件就是一个m个字节的序列:B0 , B1 , … , Bk , … , Bm-1所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
2.linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_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.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
函数:
1,open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1
2,close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错
3,read()函数
功能描述: 从文件读取数据。
所需头文件: #include
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
4,write()函数
功能描述: 向文件写入数据。
所需头文件: #include
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)
5,lseek()函数
功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include ,#include
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1。
8.3 printf的实现分析
- static int printf(const char *fmt, ...)
- {
- va_list args;
- int i;
- va_start(args, fmt);
- write(1,printbuf,i=vsprintf(printbuf, fmt, args));
- va_end(args);
- return i;
- }
首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
再看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:
- mov eax, _NR_write
- mov ebx, [esp + 4]
- mov ecx, [esp + 8]
- int INT_VECTOR_SYS_CALL
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall
发现反汇编语句中的int 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
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取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函数将整个缓冲区都读到了buf中,并用静态变量n来保存缓冲区的长度,当n为0,即buf长度为0时,getchar进行调用read函数,否则直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
通过本章,了解了linux的I/O设备管理机制,开、关、读、写、转移文件的接口及相关函数,以及printf和getchar函数的实现方法以及操作过程。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
1,预处理器cpp将c文件的宏展开为i文件。
2,编辑器将i文件编译成汇编语言的s文件。
3,汇编器将汇编语言翻译成二进制机器语言,生成二进制可重定位目标程序,o文件。
4,链接器将所引用的目标文件符号解析,重定位后完全链接成可执行的目标文件hello。
5,在shell中输入命令行指令,运行hello程序。
6,如果不是内置指令,shell fork一个子进程。
7,对子进程用命令行分析的参数execve加载,mmap映射虚拟内存。改变PC到_start,最后开始执行main函数
8,hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
10,return或exit后,hello子进程终止被shell 父进程回收,内核 回收为其创建的所有信息。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
看似简单常见的计算机,每一部分原来都有着精巧的设计和机制,都凝结着前人无数的智慧结晶,就连不起眼的hello程序背后都有着如此复杂精细的过程,这更加深了它在我心中的神秘感,吸引我对其进行进一步的探索,同时也使我进一步认识到知识是无涯的,我们在日后的学习研究中,应该注重知识的精度而非广度,才能有所成就。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
中间结果文件 | 文件作用 |
hello.i | 预处理得到的文本文件 |
hello.s | 编译后得到的文本文件 |
hello.o | 汇编得到的可重定位目标文件 |
hello.asm | hello反汇编得到的文件 |
hello | ld得到可执行目标文件 |
(附件0分,缺失 -1分)
参考文献
[1] 计算机系统PPT
[2] Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737
[3] ORACLE.链接程序和库指南. https://docs.oracle.com/cd/E38902_01/html/E38861/chapter6-54839.html#gentextid-15180