目录
- 摘 要
- 第1章 概述
- 1.1 Hello简介
- 1.2 环境与工具
- 1.3 中间结果
- 1.4 本章小结
- 第2章 预处理
- 2.1 预处理的概念与作用
- 2.2在Ubuntu下预处理的命令
- 2.3 Hello的预处理结果解析
- 2.4 本章小结
- 第3章 编译
- 3.1 编译的概念与作用
- 3.2 在Ubuntu下编译的命令
- 3.3 Hello的编译结果解析
- 3.3.1 数据
- 3.3.2 赋值
- 3.3.3 类型转换
- 3.3.4 算术操作
- 3.3.5 关系操作
- 3.3.6 数组
- 3.3.7 控制转移
- 3.3.8 函数操作
- 3.4 本章小结
- 第4章 汇编
- 4.1 汇编的概念与作用
- 4.2 在Ubuntu下汇编的命令
- 4.3 可重定位目标elf格式
- 4.3.1 ELF头
- 4.3.2 节头表
- 4.3.3 符号表
- 4.3.4 重定位节
- 4.5 本章小结
- 第5章 链接
- 5.1 链接的概念与作用
- 5.2 在Ubuntu下链接的命令
- 5.3 可执行目标文件hello的格式
- 5.3.1 ELF头
- 5.3.2 节头表
- 5.3.3 符号表
- 5.3.4 重定位节
- 5.3.5 段头表
- 5.4 hello的虚拟地址空间
- 5.5 链接的重定位过程分析
- 5.6 hello的执行流程
- 5.7 Hello的动态链接分析
- 5.8 本章小结
- 第6章 hello进程管理
- 6.1 进程的概念与作用
- 6.2 简述壳Shell-bash的作用与处理流程
- 6.3 Hello的fork进程创建过程
- 6.4 Hello的execve过程
- 6.5 Hello的进程执行
- 6.6 hello的异常与信号处理
- 6.7本章小结
- 第7章 hello的存储管理
- 7.1 hello的存储器地址空间
- 7.2 Intel逻辑地址到线性地址的变换-段式管理
- 7.3 Hello的线性地址到物理地址的变换-页式管理
- 7.4 TLB与四级页表支持下的VA到PA的变换
- 7.5 三级Cache支持下的物理内存访问
- 7.6 hello进程fork时的内存映射
- 7.7 hello进程execve时的内存映射
- 7.8 缺页故障与缺页中断处理
- 7.9动态存储分配管理
- 7.10本章小结
- 第8章 hello的IO管理
- 8.1 Linux的IO设备管理方法
- 8.2 简述Unix IO接口及其函数
- 8.3 printf的实现分析
- 8.4 getchar的实现分析
- 8.5本章小结
- 结论
- 附件
- 参考文献
摘 要
本文主要阐述hello程序的生命周期,介绍hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。熟悉程序的进程管理、存储管理和IO管理。通过对hello一生周期的探索,让我们对计算机系统有更深的了解。
关键词:编译,链接,进程管理,存储管理,IO管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
第1章 概述
1.1 Hello简介
P2P:hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入./hello+学号+姓名+秒数,shell为其fork产生一个子进程,然后hello便从程序(program)变为了进程(process)。
020:加载器execve将程序加载到新创建的进程中,映射虚拟内存,进入程序入口后程序开始载入物理内存,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,释放hello的内存并删除进程上下文。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟64位以上。
开发与调试工具:gdb,gcc,gedit,edb,readelf,objdump。
1.3 中间结果
1.4 本章小结
本章主要介绍了hello.c程序P2P,020的过程。列出了本次实验所需的环境,工具。还介绍了过程中所生成的中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第6行的#include命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
预处理的作用:
1、将源文件中以”include”格式包含的文件复制到编译的源文件中。
2、用实际值替换用“#define”定义的字符串。
3、根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图2-1 Ubuntu输入命令
图2-2 生成的hello.i文件
2.3 Hello的预处理结果解析
图2-3 hello.i文件内容
打开文件后,发现文件的内容增加,最后是main函数,与源程序没有变化。前面的三个头文件#include ,#include ,#include 被预处理器插入到程序文本中,增加了很多宏定义。
2.4 本章小结
本章介绍了预处理的概念和作用,在Linux下生成了预处理文件,并对该文件进行分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
编译的作用:编译C是为了提高程序的运行效率,把对用户友好的语言文本编译成对机器友好的特定指令直接执行,而不是执行时一条一条通过解释器解析执行,很大地提高了执行的效率。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
图3-1 Ubuntu输入命令
图3-2 生成的hello.s文件
3.3 Hello的编译结果解析
3.3.1 数据
- 字符串
图3-3 字符串
程序中有两个字符串,如上文所示,都存在.rodata中,并且作为printf函数的参数。
- 局部变量i
main函数定义了一个局部变量i,初值为0,存放在栈上-4(%rbp)的位置上。
图3-6 局部变量i
3.3.2 赋值
图3-7 赋值操作
上图截取了hello.s中的一个赋值操作,将1赋给edi,主要通过mov指令来实现。
3.3.3 类型转换
程序中调用atoi函数,atoi(argv[3]),将argv[3]这个字符串转为int型。
3.3.4 算术操作
加法操作:将rax加8,并将结果存到rax中。
图3-8 加法操作
减法操作:将栈指针减去32.
图3-9 减法操作
自增操作:i的自增运算
图3-10 自增操作
3.3.5 关系操作
1.判断argc != 4,该语句用来判断argc是否与4相等,比较完成后设置条件码,以便做后续操作。
图3-11 argc != 4
2.判断i <= 7,该语句用来判断i是否小于等于7,比较完成后设置条件码,以便做后续操作。
图3-12 i <= 7
3.3.6 数组
hello.c中的数组是main函 数的第二个参数char *argv[],作为函数的第二个参数 ,argv[]开始被保存在栈中-32 (%rbp)的位置,然后argv[1],argv[2]作为参数传给printf函数。
图3-13 数组argv[]
红色箭头指的是数组argv[]存放的地方:-32(%rbp)。
蓝色箭头指的是argv[1],argv[2]作为参数传给printf函数。
3.3.7 控制转移
1.if
判断argc是否为4,如果是4,则跳转到L2处的指令,否则继续执行下一条指令。
图3-14 if判断
2.while
判断i是否小于等于7,如果成立,则跳转到L4处的指令,否则继续执行下一条指令。
图3-15 while循环
3.3.8 函数操作
hello程序中包含着对puts,exit,printf,atoi,sleep,getchar函数的调用。
- main
参数传递:第一个参数argc存在%edi中,第二个参数argv[]存在%rsi中。
图3-16 main函数的参数
返回值:0
图3-17 main函数的返回值0
2. puts
参数传递:一个字符串
图3-18 puts函数参数
3. exit
参数传递:1
图3-19 exit函数参数
4. printf
参数传递:第一个参数.LC1(字符串)存在%rdi中,第二个参数argv[1]存在%rsi中,三个参数argv[2]存在%rdx中
图3-20 printf函数参数
5.atoi
参数传递:参数argv[3]存在%rdi中
图3-21 atoi函数参数
6.sleep
参数传递:将atoi函数的返回值作为参数存在%rdi中。
图3-22 sleep函数参数
7.getchar
无参数,返回值是int类型。
3.4 本章小结
本章介绍了编译的概念与作用,对hello.c文件进行了编译,生成了hello.s文件。对hello.s文件从数据,算术操作,关系操作与控制转移,函数操作等方面进行了分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
汇编的作用:汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码。
4.2 在Ubuntu下汇编的命令
命令:gcc –c hello.s –o hello.o
图4-1 Ubuntu下的命令
图4-2 生成的hello.o文件
4.3 可重定位目标elf格式
4.3.1 ELF头
用命令readelf -h hello.o查看。
图4-3 ELF头
ELF头以一个16字节的序列(Magic)开始,Magic描述了生成该文件的系统的字的大小和字节顺序。
由上图可以看出,hello.o为64位文件;数据采用补码表示,小端存储;文件类型为REL(可重定位文件);程序的入口地址为0x0,节头表的起始位置为1152;文件中共有13节。
4.3.2 节头表
用命令readelf -S hello.o查看。
图4-4 节头表
该部分包含了各节头的名称,类型,地址,偏移量。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。每个节有不同的读写权限。
4.3.3 符号表
用命令readelf -s hello.o查看。
图4-5 符号表
符号表.symtab包含了程序中定义和使用的各种符号,包含函数名称,变量的名称,类型,大小。Bind字段表示符号是本地的还是局部的。
4.3.4 重定位节
用命令readelf -r hello.o查看。
图4-6 重定位节
重定位节.rela.text包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.text节中包含很多字段,包括偏移量,信息,类型,符号值,符号名称+加数。
4.4 Hello.o的结果解析
用objdump -d -r hello.o命令查看。
图4-7 反汇编代码
与汇编指令进行对比可以发现:
1.二者没有太大的差别。只是反汇编代码中不仅有汇编代码,还有其对应的机器语言代码。
2.机器语言中的数据采用小端存储的二进制形式表示,而在汇编语言中采用的是顺序十六进制形式表示。
3.反汇编指令后面没有对字大小的表示,但ret后面加上了q。
4.分支转移:反汇编代码中用的不是hello.s中的符号来代表位置,如.L1等。而是用的逻辑地址表示。
5.函数调用:在.s 文件中,直接call函数名称,而在反汇编代码中,call下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,还未链接,对于这些未知地址的函数调用,将其call指令后的地址设置为全0,最后在链接时才填入正确的地址。
4.5 本章小结
本章介绍了汇编的概念与作用,生成了二进制可重定位目标文件,查看并分析了ELF文件中的内容,最后将反汇编文件与汇编程序进行对比,发现二者的相同与不同点。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。
链接的作用:链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
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-1Ubuntu下的命令
图5-2 生成的hello文件
5.3 可执行目标文件hello的格式
5.3.1 ELF头
用命令readelf -h hello查看。
图5-3 ELF头
ELF头以一个16字节的序列(Magic)开始,Magic描述了生成该文件的系统的字的大小和字节顺序。
由上图可以看出,hello为64位文件;数据采用补码表示,小端存储;文件类型为EXEC(可执行文件);程序的入口地址为0x401090,节头表的起始位置为14120;文件中共有25节。
5.3.2 节头表
用命令readelf -S hello查看。
图5-4 节头表
hello中节头表的条目数多于hello.o中节头表的条目数。每一节都有了实际地址,而不是像在hello.o中那样地址值全为0。这说明重定位工作已完成。同时多出的节是为了能够实现动态链接,如.interp这一节包含动态链接器的路径名,动态链接器通过执行一系列重定位工作完成链接任务。dynamic段保存了动态链接器所需要的基本信息。
5.3.3 符号表
用命令readelf -s hello查看。
图5-5 符号表
符号表包含了程序中定义和使用的各种符号,包含函数名称,变量的名称,类型,大小。Bind字段表示符号是本地的还是局部的。
5.3.4 重定位节
用命令readelf -r hello查看。
图5-6 重定位节
节中包含很多字段,包括偏移量,信息,类型,符号值,符号名称+加数。
5.3.5 段头表
用命令readelf -l hello查看。
图5-7 段头表
段头表描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从段头表中可以看到根据可执行目标文件的内容初始化为两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。
5.4 hello的虚拟地址空间
使用edb加载hello。
1.程序从0x401000开始,对应节头表.init地址。
图5-8 程序起始地址
2.程序的入口地址0x401090,对应节头表.text地址。
图5-9 程序入口地址
3.节头表中.interp地址为0x400270
图5-10 .interp地址
4.节头表中.got地址为0x403ff0
图5-11 .got地址
5.5 链接的重定位过程分析
使用objdump -d -r hello,查看hello的反汇编代码。
图5-12 生成的hello的反汇编代码
通过比较hello和hello.o的反汇编代码,可以得到如下不同:
1.hello.o的反汇编代码的地址从0开始,而hello的反汇编代码从0x401000开始。这说明hello.o还未实现重定位的过程,每个符号还没有确定的地址,而hello已经实现了重定位,每个符号都有其确定的地址。
2.发现hello的反汇编代码比hello.o的反汇编代码多了许多节头。比如.init节,.plt节。
图5-13 .init节和.plt节
其中.init节是程序初始化需要执行的代码,.plt是动态链接的过程链接表。
3.hello的反汇编代码中增加了许多外部链接的共享库函数,如puts@plt,printf@plt,getchar@plt等函数。
图5-14 链接的共享库函数
4.对于跳转,返回指令的地址hello中已经有了明确的数,而hello.o中的地址位置全为0。由此可以看出链接是会为地址不确定的符号分配一个确定的地址,而在该符号的引用处也将地址改为确定值。
hello重定位的过程:
(1)重定位节和符号定义
在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中的每条指令和全局变量都有唯一运行时内存地址了。
(2)重定位节中的符号引用
这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
(3)重定位条目
在汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text节中,已初始化数据的重定位条目放在.rel.data中。
(4)重定位过程
PC相对寻址的重定位代码如下:
图5-15 PC相对寻址的重定位代码
1.以printf函数为例由图5-15的公式可知:ADDR(s) =0x401040refaddr = ADDR(s)+r.offset=(0x401040+(-4)-0x40111b)=(unsigned)(0xffffff21)。
图5-16 hello的反汇编代码
5.6 hello的执行流程
函数名称 函数地址
ld-2.29.so!_dl_start 0x7ffff7a34de0
ld-2.29.so!_dl_init 0x7ffff7dee900
hello!_start 0x401080
libc-2.29.so!_libc_start_main 0x7ffffcd425fa0
libc-2.29.so!_cxa_atexit 0x7ffffcd445ae2
hello!_libc_csu_init 0x401150
libc-2.29.so!_stejump 0x7ffffcd442de6
libc-2.29.so!exit 0x7ffffcd4458e2
表5-1 函数及函数地址
5.7 Hello的动态链接分析
PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。
GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。
1.在elf文件中找到.got的地址为:0x403ff0
图5-17 .got地址
2.调用dl_init前
图5-18 调用dl_init前的.got表
3.调用dl_init之后
图5-19 调用dl_init后的.got表
5.8 本章小结
本章介绍了链接的概念与作用,对比了hello.o和hello的不同,查看了hello的虚拟地址空间,分析了其重定位原理与流程,执行流程与动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个正在运行的程序的实例。
进程的作用:进程为用户提供了以下假象:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:shell是一个交互型应用级程序,代表用户运行其他程序。这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
Shell-bash的处理流程:
1.shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) , , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有符号的变量进行替换。6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用符号的变量进行替换。 6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用符号的变量进行替换。6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ” />
图6-1 进程的上下文切换
当hello调用getchar的时候,实际执行输入流是系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。
6.6 hello的异常与信号处理
1.hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误。
在发生异常时,产生信号,如下图所示:
图6-2 Linux信号
2.键盘上各种操作导致的异常
(1)不停乱按
图6-3 不停乱按
如果乱按的过程中包括回车,那么乱按的内容将会在该程序结束之后作为命令行输入。
(2)Ctrl-Z
图6-4 Ctrl-Z
在键盘下按下Ctrl-Z之后,会导致内核发送一个SIGTSTP信号到前台进程组的每个进程,默认情况下结果是停止(挂起)前台作业。
(3)Ctrl-C
图6-5 Ctrl-C
在键盘上按下Ctrl-C之后,会导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下结果是终止前台作业。
(4)ps
图6-6 ps命令
(5)jobs
图6-7 jobs命令
(6)pstree
图6-8 pstree命令
(7)fg
图6-9 fg命令
(8)kill
图6-10 kill命令
6.7本章小结
本章介绍了进程的概念与作用,说明了shell的作用与处理流程,介绍了hello程序的进程创建,加载程序,各种异常与信号处理的相关内容。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是个索引号,后面3位包含一些硬件细节 。
线性地址:也叫虚拟地址(virtual address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址:等同于线性地址。
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
图7-1 虚拟地址
在hello的反汇编文件中,红色方框标注的就是虚拟地址,虚拟地址经过翻译得到物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1.段式管理的数据结构
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。
进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。
在系统中为每个进程建立一张段映射表,如图:
图7-2 段映射表
系统段表:系统所有占用段(已经分配的段)。
空闲段表:内存中所有空闲段,可以结合到系统段表中。
2.段式管理的地址变换
图7-3 段式管理的地址变换
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址(见图4—5)。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
1.数据结构
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。为了完成上述的功能,—个页式系统中,一般要采用如下的数据结构。
进程页表:完成逻辑页号(本进程的地址空间)到物理页面号(实际内存空间,也叫块号)的映射。
每个进程有一个页表,描述该进程占用的物理页面及逻辑排列顺序,如图:
图7-4 页表
物理页面表:整个系统有一个物理页面表,描述物理内存空间的分配使用状况,其数据结构可采用位示图和空闲页链表。
对于位示图法,即如果该页面已被分配,则对应比特位置1,否置0.
图7-5 页面表
请求表:整个系统有一个请求表,描述系统内各个进程页表的位置和大小,用于地址转换也可以结合到各进程的PCB(进程控制块)里。如图:
图7-6 请求表
2.页式管理地址变换
在页式系统中,指令所给出的地址分为两部分:逻辑页号和页内地址。
原理:CPU中的内存管理单元(MMU)按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址。
逻辑页号,页内偏移地址->查进程页表,得物理页号->物理地址:
图7-7 页式管理的地址变换
上述过程通常由处理器的硬件直接完成,不需要软件参与。通常,操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存:
第一次用来查找页表将操作数的 逻辑地址变换为物理地址;
第二次完成真正的读写操作。
这样做时间上耗费严重。为缩短查找时间,可以将页表从内存装入CPU内部的关联存储器(例如,快表) 中,实现按内容查找。此时的地址变换过程是:在CPU给出有效地址后,由地址变换机构自动将页号送人快表,并将此页号与快表中的所有页号进行比较,而且这 种比较是同时进行的。若其中有与此相匹配的页号,表示要访问的页的页表项在快表中。于是可直接读出该页所对应的物理页号,这样就无需访问内存中的页表。由于关联存储器的访问速度比内存的访问速度快得多。
7.4 TLB与四级页表支持下的VA到PA的变换
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。
图7-8 一个两级页表的层次结构
多级页表的使用从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。第二,只有一级页表才需要总是在主存中。虚拟内存系统可以在需要时创建、页面调入或调出二级页表。
7.5 三级Cache支持下的物理内存访问
在从TLB或者页表中得到物理地址后,根据物理地址从cache中寻找。到了L1里面以后,寻找物理地址要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,其中可能会发生块的替换等其它操作。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text 和.data区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域, hello 程序与共享对象 libc.so链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
图7-9 加载器是如何映射用户地址空间的区域的
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
缺页中断处理:假设MMU在试图翻译某个虚拟地址A时触发了一个缺页,这个异常会导致控制转移到内核的缺页处理程序,执行以下步骤:
(1)虚拟地址A是合法的吗?换句话说就是A在某个区域结构定义的区域内吗?如果这个指令不合法,那么缺页处理程序就会触发一个段错误,进而终止这个进程。
(2)进程是否有读、写或者执行这个区域内页面的权限?例如这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,终止进程。
(3)如果缺页是由于对合法的虚拟地址进行合法的操作造成的,内核会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会缺页中断了。
图7-10 Linux缺页处理
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护一个变量brk,指向堆的顶部。分配器将堆视作一组不同大小的块的集合,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。
分配器有两种基本风格。两种风格都是要求显示的释放分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
隐式空闲链表:
一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。
图7-11 一个简单的堆块的格式
放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
当分配器找到一个匹配的空闲块时,通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
当分配器找不到合适的空闲块一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。在每个空闲块中,都包含一个前驱和后继的指针。
图7-12 使用双向空闲链表的堆块的格式
7.10本章小结
本章介绍了hello的存储器的地址空间,介绍了四种地址空间的含义和相互转换过程。阐述了四级页表的地址翻译和三级cache的物理内存访问原理。介绍了fork时的内存映射,execve时的内存映射,缺页故障与缺页中断处理,动态存储分配的内容。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的接口,称为Unix I/O,这使得所有的输入和输出能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。 (3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
Unix I/O 函数:
open函数:进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。
close函数:进程通过调用close函数来关闭一个打开的文件。
应用程序通过调用read和write函数来分别进行输入和输出。
调用stat和fstat函数检索到关于文件的信息(元数据)。
应用程序可以用readdir系列函数来读取目录的内容。
8.3 printf的实现分析
(1)首先来看printf的函数体
图8-1 printf的函数体
(2)printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数将buf中的i个元素写到终端。
(4)从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
(5)字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
(6)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(1)首先来看getchar的函数体
图8-2 getchar的函数体
(2)异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
(3)getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
hello程序首先从hello.c的源代码文件开始,依次经过:
(1)预处理:对hello.c进行预处理,生成hello.i文件
(2)编译:将预处理完的hello.i文件通过一系列词法分析、语法分析和优化之后生成hello.s汇编文件
(3)汇编:将hello.s文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中
(4)链接:与动态库链接,生成可执行文件hello
(5)创建进程:在shell命令行输入./hello,shell解释命令并调用fork函数创建一个子进程。
(6)加载:加载器调用execve函数,在当前进程(新创建的子进程)的上下文中运行hello程序。
(7)上下文切换:hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。
(8)动态申请内存:当hello程序执行printf函数是,会调用malloc向动态内存分配器申请堆中的内存。
(9)异常处理:如果产生缺页异常,则缺页处理程序选择合适的牺牲页替换,并重新加载相应命令。
(10)终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
通过本次实验,全面了解了hello的整个生命进程,让我感悟到计算机系统设计的巧妙与精密。抽象的概念在计算机里的意义重大,文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象,进程是对一个正在运行的程序的抽象,虚拟机是对整个计算机的抽象。
对于一个计算机专业的学生来说,系统全面的了解计算机系统,了解底层知识,对于我们编写程序,优化程序具有很大的帮助,帮助我们提高专业素养。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名称 文件作用
hello.c 源程序
hello.i 预处理之后的文本文件
hello.s 编译后的汇编文件
hello.o 汇编之后的可重定位目标文件
hello 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.
[7] https://blog.csdn.net/macrossdzh/article/details
[8] https://blog.csdn.net/hit_shaoqi/article/details
(参考文献0分,缺失 -1分)