计算机系统
大作业
计算机科学与技术学院
2021年5月
本文通过hello程序从无到有再从有到无的历程,介绍了程序从编写到翻译成机器可识别语言再到机器具体运行程序时的行为以及计算机系统中的软硬件配合。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;io管理;
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理.......................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问............................................................. - 11 -
7.6 hello进程fork时的内存映射..................................................................... - 11 -
7.7 hello进程execve时的内存映射................................................................. - 11 -
7.8 缺页故障与缺页中断处理.............................................................................. - 11 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
1.11 From Program to Process
Hello.c通过编辑器录入内容,成为一个程序也即Program,这个程序经过预处理、编译、汇编、链接等过程后,成为一个二进制可执行文件.out。通过shell去和系统内核交互,进程管理(OS)给hello创建(fork)一个新进程,然后在这个新进程中,调用execve函数将hello可执行文件加载到内存中并运行成为进程(Process)。
1.12 2 From Zero-0 to Zero-0
Execve函数为hello分配虚拟内存,建立内存映射并载入物理内存,操作系统为其划分时间片,然后进入内存代码段也即main函数代码开始执行。执行过程中操作系统调节cpu与内存之间的配合,cpu流水线读取指令并进行逻辑运算然后写回,此过程中还有多级缓存配合。程序运行结束后产生中断,shell负责回收hello的进程,其占用的内存段也会被删除。
1.2 环境与工具
图1 环境与工具
开发与调试工具: Vscode、Visualstudio
1.3 中间结果
Hello.i | Hello.c预处理后的文本文件 |
Hello.s | Hello.i编译后的汇编文件 |
Hello.o | Hello.s汇编之后的可重定位目标文件 |
Hello | 链接之后的可执行目标文件 |
Hello.out | Hello反汇编之后的可重定位文件 |
Hello.txt | Hello的反汇编文件 |
Helloo.txt | Hello.o的反汇编文件 |
1.4 本章小结
本章概括性的叙述一个源代码文件hello.c的p2p,020的过程,对后面的内容有提纲挈领的作用,还列出了实验的软硬件环境以及用到的文件,大致介绍了整篇论文的内容。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理一半指程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程,主要包括宏定义、条件编译、库文件和删除注释等,将原始的c程序变为一个完整的文本文件。
预处理的作用:
(1)#define 宏替换,将目标字符串换为自定义的字符串。
(2)#ifdef 检查是否有重复定义。
(3)#inlcude 将所提到的库文件与c程序链接在一起形成完整的文件。
(4)// 删除注释中提到的内容。
(5)识别一些预定义的特殊符号,将其替换预定义指向的特殊内容。
2.2在Ubuntu下预处理的命令
在Ubuntu下用gcc预处理hello.c程序的命令是:gcc hello.c -E -o hello.i
其中-E选项的作用是使编译过程在预处理后停止
图2 执行预处理命令
2.3 Hello的预处理结果解析
图3 预处理后的文件
可以看到,预处理后的文件是一个可阅读的文本文件,相比于源c程序,预处理后的文件多了很多内容,这些内容就是我们上面所提到过的库文件和宏展开等。也可以观察到预处理只是基于文本的改动,并没有再进行深入的处理。
2.4 本章小结
本章主要介绍了预处理的概念及作用,Ubuntu下用gcc预处理c程序的命令,展示了预处理在实际程序处理中的行为及结果,并基于结果进行了分析。
3.1 编译的概念与作用
编译的概念:
利用编译程序从源程序(hello.i)产生目标程序(hello.s)的过程,这个过程中hello从以高级语言编写的文本文件变成了用汇编语言或机器语言表示的文件。是一个由面向人的高级语言转换成面向机器的低级语言的过程。
编译的作用:
编译程序会对源程序进行词法分析(对字符组成的单词进行处理,将作为长字符串的源程序打断为单词符号串的中间程序)、语法分析(分析单词符号串中表达式、幅值、循环等语法单位是否符合语法规则),将其表示为中间代码,然后进行代码优化,保证在程序等价变化的原则下生成更有效的代码,最后变换中间代码为目标代码,完成编译过程。
3.2 在Ubuntu下编译的命令
Unbuntu下的编译命令:gcc -S hello.i -o hello.s
图4 编译命令
3.3 Hello的编译结果解析
Hello编译结果如下:
图5 hello的编译结果
图6 源c程序部分代码
3.1.1 数据
(1)常量
立即数4、0、8等,会直接体现在代码中,如argc!=4这句语句中的4,直接体现在编译后文件中24行cmpl $4, -20(%rbp)中。
字符串会被存储进只读数据段.LC0中。
(2)变量
Main函数中有一个局部循环变量i,我们可以在.L2中找到movl $0, -4(%rbp),这就是i=0语句,可以看到它被存放在堆栈-4(%rbp)中。
而程序中没有全局变量和静态变量,故不体现。
(3)表达式
表达式涵盖过于宽泛,不做单独介绍。
(4)类型
表现在申请堆栈空间不同。
(5)宏
没有体现。
3.1.2 赋值
汇编程序中赋值一般通过mov指令实现,指令的后缀取决于操作数据的字节大小,movb:一个字节;movw:两个字节;movl:四个字节;movq:
源代码中有I = 0赋值操作,体现在movl $0, -4(%rbp)。
3.1.3 算数操作
源代码中有i++赋值操作,体现在
图7 i++汇编代码
前面已经知道-4(%rbq)存储的是i变量,addl指令就是把i加1。
3.1.4 关系操作
源代码中argc!=4为关系操作,在汇编代码中为
图8 关系操作汇编代码
通过cmpl指令比较立即数4和变量argc,并通过设置标志位记录比较结果。
3.1.5 数组、指针操作
图9 数组操作汇编代码
通过首地址加偏移量的方式访问数组元素,可以观察到首地址是-32(%rbp),分别将首地址加8和加16获取argc[1]和argc[2]的值,并且通过寄存器%rax和%rdx存储起来等待调用。
3.1.6 控制转移
源代码中两处控制转移if、for,分别在汇编代码中体现为
图10 if控制转移汇编代码
图11 for控制转移汇编代码
两处都是将变量与立即数比较并设置条件码,然后分别调用jle和je执行条件跳转,跳转后的地址分别为.L2和.L4。
3.1.7 函数操作
有六处函数调用,分别为printf(),exit(),printf(),sleep(),atoi(),getchar()。
图12 printf函数调用和atoi函数调用
图13 getchar函数调用
可以看到函数的调用是通过call指令加函数名,参数则是通过寄存器(%rdi,%rsi依次启用)传递,一般参数的赋值都紧挨着调用且在调用之前,例如,第一个printf函数的调用参数通过%rdi传递,而exit函数通过%edi(实际上也是%rdi,只不过使用位数不同)传递。
3.4 本章小结
本章介绍了编译的概念和作用,如何在unbuntu下用gcc编译文件,展示了编译后文件内容和编译时源代码与汇编语言的对应规则和编译行为。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编器(as)将编译后的文件(.s)翻译成机器语言二进制文件(.o)。
汇编的作用:
将汇编语言翻译成机器语言。
4.2 在Ubuntu下汇编的命令
Unbuntu下用gcc汇编的命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o
图14 汇编命令
4.3 可重定位目标elf格式
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。
使用readelf -a命令读取hello.o的ELF格式。
4.3.1 ELF头
图15 ELF头
ELF头以16字节的Magic开始,描述了系统的字的大小和字节顺序,后面包含的信息可以帮助链接器语法分析和解释目标文件的信息。包括ELF头大小,目标文件类型,机器类型,节头部表的文件偏移和节头部表中条目的大小和数量。
4.3.2 节头部表
图16 节头部表
通过目标文件中的节头表,我们就可以轻松地定位文件中所有的节。节头表通过数组实现,每个数组项包含一个节的信息。各个节构成了程序头表中定义的各段的内容。
我们可以看到一共列出来14个节:
节 | 大小 | 类型 | 旗标 | 偏移量 |
.text | 0x92 | PROGBITS | AX | 0x40 |
.rela.text | 0xc0 | RELA | I | 0x388 |
.data | 0x00 | PROGBITS | WA | 0xd2 |
.bss | 0x00 | NOBITS | WA | 0xd2 |
.rodata | 0x33 | PROGBITS | A | 0xd8 |
.comment | 0x2c | PROGBITS | MS | 0x10b |
.note.GNU-stack | 0x00 | PROGBITS | 0x137 | |
.note.gnu.propert | 0x20 | NOTE | A | 0x138 |
.eh_frame | 0x38 | PROGBITS | A | 0x158 |
.rela.en_frame | 0x18 | RELA | I | 0x448 |
.symtab | 0x1b0 | SYMTAB | 0x190 | |
.strtab | 0x48 | STRTAB | 0x340 | |
.shstrtab | 0x74 | STRTAB | 0x460 |
4.4.3 重定位节
图17 重定位节
重定位是连接符号引用与符号定义的过程。 例如,程序调用函数时,关联的调用指令必须在执行时将控制权转移到正确的目标地址。 可重定位文件必须包含说明如何修改其节内容的信息。
可以看到重定位节包含以下信息:偏移量、信息、类型、符号值、符号名称和加数。
Hello.s中,重定位节.rela.text一共描述了8个重定位条目,重定位节.rela/eh_frame描述了1个重定位条目。
4.4.4 符号表
图18 符号表
符号表中存放定义和引用全局变量的信息。Value项为地址偏移量,size项为目标大小,type项为目标数据类型,bind项表示该符号为局部符号还是全局符号,name项为符号名。
4.4 Hello.o的结果解析
objdump -d -r hello.o反汇编hello.o的结果为:
图19 hello.o的反汇编结果
与第3章的 hello.s进行对照分析后发现,汇编程序较反汇编程序多了只读代码段等预处理内容,而反汇编程序相比于汇编程序多了机器码,且二者具体汇编语言有细微不同,但实际上这些不同都只是表现形式不同,是无关紧要的,实际二者本质上,也即代码逻辑上是一样的。
机器语言由操作码和操作数构成,是纯粹的二进制代码,而汇编语言则是由助记符、操作数等构成,cpu不能直接识别并执行,但汇编语言可以翻译为机器语言,
代码逻辑相同可能是多对多,但是直接翻译可以做到一一映射。
机器语言中分支调用是直接跳转地址,而汇编语言中是用段名称。函数调用汇编语言中利用函数名,而在反汇编程序中call调用的是下一条指令。
4.5 本章小结
本章介绍了汇编的概念与作用,unbuntu下利用gcc汇编命令,对hello.s进行了汇编,并对生成的hello.o文件的ELF头,节头部表、可重定位表和符号表进行了分析。比较hello的汇编程序和反汇编程序之间的异同,辨析了机器语言和汇编语言之间的关系。
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是将各种代码和数据片段收集并组合为一个单一文件的过程,所得的文件可以被加载到内存并执行。
链接的作用:
将原本分散的数据片段链接起来变成一个完整的可执行文件。
5.2 在Ubuntu下链接的命令
Unbuntu下用命令: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进行链接
图20 链接命令
5.3 可执行目标文件hello的格式
5.3.1 ELF头
图21 ELF头
可以看到相比于hello.o的ELF头,类型由可重定位文件变为了可执行文件。且节数目也有所增加。
5.3.2 节头部表
图22 节头部表
包含了hello中节的信息,与hello.o的节头部表相比只是多了一些节。
5.3.3 程序头
图23 程序头表
Hello.s中无此表,程序头表定义了可执行文件在装入内存后的段内存布局,每个程序头对应进程的一个内存段。在hello程序头中,描述了12个不同的程序段,并且给出了段的偏移地址、虚拟内存地址和物理内存地址。
5.3.4 段节
图24 段节表
5.3.5 动态链接部分
图25 动态链接表
给出了程序中动态链接部分,还给出了动态链接部分的标记、类型、名称、值。
5.3.6 重定位节
图26 重定位节
同hello.o类似,重定位节.rela.dyn包含两个条目,重定位节.rela.plt包含六个条目。
5.3.7 符号表
图27 符号表
原理与作用皆与上文类似,所以在此只截取一部分留作参考。
5.4 hello的虚拟地址空间
图28 程序开始处
图29 程序结束处
通过edb加载hello可以看到,本进程虚拟地址空间开始于0x401000,结束于0x401ff0。
根据5.3中节表头偏移地址和读出的程序开始地址,可以找到各节在虚拟地址空间的位置。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
图30 hello的反汇编
objdump -d -r hello对hello进行反汇编,并将结果输出至hello.txt中,然后比较hello和hello.o的区别。
其区别主要有以下几种:
- 可以看到两个文件起始内容就不一样,hello.o反汇编代码中以main函数开始,而hello的反汇编代码中以节.init开始,这个节是在链接过程中引入的,与此同时,链接中还引入了很多其它数据,所以hello的反汇编内容比起hello.c要长的多。
- 还有一个很大的不同是,hello.o的反汇编代码中,可以观察到地址从0x0开始,而hello的反汇编程序中,第一个节.init的起始地址就在0x401000了,这是我们前面看到过的虚拟内存地址。
- Main函数中涉及重定位的代码被修改。我们回顾hello.o反汇编代码中的机器码可以知道,机器码调用一些重定向函数时,留下的并不是函数的实际地址,而是提示修改的信息,我们可以观察hello.o的重定位条目。
下面介绍重定位过程:hello.o的重定位文件
图31 hello的重定位节
观察后可以发现,重定位节中有两种类型,分别为R_x86_64_PC_32和R_x86_64_PLT32,查阅资料可以知道:
图32 资料
图33 资料
我们以printf重定位为例,它的类型是R_x86_64_PLT32,要求静态链接生成<printf@plt>函数,并根据"L(<printf@plt>函数地址)+A(-4)-P",即<printf@plt>相对于下一条指令的相对地址,修改hello.o镜像中0x5e偏移处的值.
其中
图34 <printf@plt>
重定位提示处链接前
图35 重定位提示链接前
而在链接后
图36 重定位提示部分链接后
其地址0x4010a0刚好是计算值。其它函数调用也类似,这就是hello.c的重定位过程。
5.6 hello的执行流程
图37 edb单步调试hello
通过单步调试,记录其执行流程以函数调用顺序表示为:
<_init>
<puts@plt>
<printf@plt>
<getchar@plt>
<atoi@plt>
<exit@plt>
<sleep@plt>
<_start>
<main>
<libc_csu_init>
<libc_csu_fini>
<exit>
5.7 Hello的动态链接分析
在程序中动态链接是通过延迟绑定来实现的,延迟绑定的实现依赖全局偏移量表GOT和过程连接表PLT实现。GOT是数据段的一部分,PLT是代码段的一部分。
在节头目表中查看与动态链接有关的节地址:
图38 动态链接有关节地址
可以看到从0x404000地址开始.
Init之前
图39 执行init之前
Init之后
图40 执行init之后
5.8 本章小结
本章主要介绍了链接的概念与作用,在ubuntu下执行链接的命令,并通过readelf分析可执行目标文件的hello格式,再用edb运行hello程序分析其虚拟地址空间。然后利用反汇编代码和读出的节头表对hello程序链接的重定位过程进行分析。利用edb单步执行操作获取hello的执行流程,最后使用edb的内存查询功能观察在动态链接前后程序内存的改变。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是操作系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的。
进程的作用:
进程可以同其它进程在宏观上近似可以同步运行,造成一起在运行的假象,多进程也可以更好的利用cpu的多核性能。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:
1. 可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。
2. shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能接收下面的输入。在异步模式,命令运行的同时,shell就可接收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。
3. shell提供了少量的内置命令,以便自身功能更加完备和高效。
4. shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。
5. shell强大的的交互性除了可编程,还体现在作业控制,命令行编辑,历史命令,和别名等方面。
Shell的处理流程:
1. 读取从键盘输⼊的命令。
2. 判断命令是否正确,且将命令⾏的参数改造为系统调⽤execve() 内部处理所要求的形式。
3. 终端进程调⽤fork() 来创建⼦进程,⾃⾝则⽤系统调⽤wait() 来等待⼦进程完成。
4. 当⼦进程运⾏时,它调⽤execve() 根据命令的名字指定的⽂件到⽬录中查找可⾏性⽂件,调⼊内存并执⾏这个命令。
5. 如果命令⾏末尾有后台命令符号& 终端进程不执⾏等待系统调⽤,⽽是⽴即发提⽰符,让⽤户输⼊下⼀条命令;如果命令末尾没有&,则终端进程要⼀直等待。当⼦进程完成处理后,向⽗进程报告,此时终端进程被唤醒,做完必要的判别⼯作后,再发提⽰符
6.3 Hello的fork进程创建过程
父进程shell通过调用fork()函数来创建一个新运行的子进程。
当我们从终端发送执行命令时,内核产生中断来为我们执行fork()命令创建子进程,通过返回值判别出所在进程时子进程时,进行下一步操作。
6.4 Hello的execve过程
通过fork()创建子进程并判断处在子进程中时,子进程会调用execve()函数来在当前上下文进程中(如shell)加载并运行hello程序,此时execve将清空该进程接下来的命令和空间,并为hello程序分配空间完成加载,且将下一条指令指向hello程序的入口地址来运行程序,同时还会将父进程的运行环境、共享库也映射给hello一份。
6.5 Hello的进程执行
6.5.1 进程上下文信息
进程上下文是指,在一个程序执行时,当其要切换到另一个进程时,cpu的所有寄存器中的值、进程的状态以及堆栈上的内容都被保存下来,以便再次执行该进程。
假设当我们用shell进程执行Hello程序时,会先将shell中使用到的数据于保存的信息保存下来,才会去正式执行Hello程序。
6.5.2 进程时间片
内核运行多个进程时,虽然我们观察到的是同时运行,实际上是内核控制cpu不断切换任务,这其中时间不连续的片状形态就是进程时间片,所以Hello进程是和Shell不断在cpu中切换运行的,通过时间片来调度。
6.5.3 用户模式和内核模式
用户模式权限远远低于内核模式,大部分进程只有在响应中断切换进程时才会进入内核态,当我们发出要执行Hello命令时,系统会产生中断,中断处理程序进入内核模式,判断我们要执行新程序,此时在内核模式下才有权限分配各种资源,在程序交接结束后,内核模式会退出到用户模式。
6.6 hello的异常与信号处理
异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。异常就是控制流中的突变,用来响应处理器状态中的某些变化。
异常可以分为四种:中断、陷阱、故障、终止。
如果我们在hello过程中只是乱按而没有触发到其它问题的话,那我们只会遇到中断和陷阱两种异常。
6.6.1 中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的。
- ctrl+Z
图42 ctrl+Z和乱按
乱按后ctrl+Z退出,可以看到乱按并没有任何影响。而ctrl+Z产生了中断信号。
Ps:
图43 ps命令运行结果
Jobs:
图44 jobs命令运行结果
Pstree:
图45 pstree命令执行结果
输出太多这里只截取一部分
Fg:
图46 fg命令执行结果
Kill:
图47 kill发送杀死进程信号
- ctrl+C
图48 ctrl+C执行结果
Ps一下
图49 ps命令执行结果
可以看到程序已经完全退出了
6.6.2 陷阱
陷阱是有意识的异常,是执行一条指令的结果。上面的中断不是程序执行指令导致的,而是外部I/O设备的信号导致的。这就是区别。陷阱最重要的用途就是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
程序中的陷阱有:exit函数、sleep函数。它们都会产生异常信号,并且根据自定义行为做中断处理。
6.7本章小结
本章主要介绍了hello进程中的进程管理,包括进程创建、加载、执行到终止的全部过程。还有fork()和execve()函数的调用。最后介绍了内核管理中的信号和异常处理,并且实际实验了进程运行过程中对不同异常做出的反应,用ps、pstree、kill、fg等命令观察结果。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由程序产生的段内偏移地址,一般与虚拟地址不做区分。
线性地址:虚拟地址到物理地址变换的中间层,是cpu可寻址的内存空间中的地址。程序代码会产生逻辑地址,加上相应段机制就成为线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。
虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。这2部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。
物理地址:指内存中物理单元的集合,他是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。
在hello程序中,hello.o程序未链接前就是采用的虚拟地址,而在用edb运行hello程序时,则是可以观察到在hellp程序运行中的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
首先,检查段选择器的T1=0还是1,即检查段选择器中的Ti字段,以确定段描述符保存在哪个描述符表中,并知道要转换的段是GDT中的段(在这种情况下,分段单元从gdtr寄存器中获取GDT的线性基址)还是LDT中的段(在这种情况下,分段单元从LDTR寄存器中获取GDT的线性基址),然后根据相应的寄存器,获取其地址和大小。由于段描述符的长度为8字节,因此它在GDT或LDT中的相对地址是通过将段选择器的最高13位的值乘以8来获得的。取出段选择器的前13位,您可以在该数组中找到相应的段描述符。这样,就可以得到offset,即偏移地址。最后,base+offset是要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
从cr3寄存器中读取页目录所在物理页面的基址并从线性地址中获取页目录项的索引,相加后得到页目录项的物理地址。首次访存得到pgd_t目录项然后取出物理页基址,这就是上级页目录的物理地址,从线性地址中读取上级页目录的索引,相加即为上级页目录的物理地址。按此操作获得页中间目录的物理地址、页表项的物理地址和物理页的基地址。最后读取物理页内偏移量与物理页相加得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE(Page Table Entry,页表项)组成的块。如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。
页表级数越少,虚拟地址到物理地址的映射会很快,但是需要管理的页表项会很多,能支持的地址空间也有限。相反页表级数越多,需要的存储的页表数据就会越少,而且能支持到比较大的地址空间,但是虚拟地址到物理地址的映射就会越慢。
虚拟地址 = 高10位在页目录表中的偏移量(页目录项pde)+中间10位在页表中的偏移量(页表项pte)+物理页的偏移。
TLB与四级页表的支持可以加快VA到PA的转换。
7.5 三级Cache支持下的物理内存访问
引入 Cache 的理论基础是程序局部性原理,包括时间局部性和空间局部性。时间局部性原理即最近被CPU访问的数据,短期内CPU 还要访问(时间);空间局部性即被CPU访问的数据附近的数据,CPU短期内还要访问(空间)。因此如果将刚刚访问过的数据缓存在一个速度比主存快得多的存储中,那下次访问时,可以直接从这个存储中取,其速度可以得到数量级的提高。
三级cache可以跟结合大小和数据命中率指定较好的策略,使程序整体运行效率更高。
7.6 hello进程fork时的内存映射
当前进程调用fork函数时,内核为新进程创建各种数据结构,并为其分配唯一的PID。为了为这个新进程创建虚拟内存,它创建当前进程的mm\ustruct、region结构和page表的原始副本。它将两个进程中的每个页面标记为只读,并将两个进程中的每个区域结构标记为写时专用拷贝。
当fork在新进程中返回时,新进程的虚拟内存与调用fork时的现有内存完全相同。当两个进程中的任何一个稍后写入时,写时拷贝机制会创建一个新页面,因此每个进程都会保留私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行可执行对象文件a.out中包含的程序,并用a.out程序有效地替换当前程序。加载和运行a.out需要以下步骤:
删除现有用户区域:删除当前进程虚拟地址的用户部分中的现有区域结构。
绘制私有区域:为程序代码、数据、BSS和堆栈区域创建新的区域结构。所有这些新领域都是私有的,并且是在写的时候复制的。代码和数据区域映射到a.out文件文本和数据区域中的字段。BSS请求以匿名方式映射到二进制区域。
映射共享区域:如果a.out程序链接到共享对象(或目标),如标准C库libc So,则这些对象将动态链接到程序,然后映射到用户虚拟地址空间中的共享区域。
设置程序计数器(PC)。execve所做的最后一件事是将当前进程上下文中的程序计数器设置为指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:指的是当软件试图访问已映射在虚拟地址空间中,但是并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。
缺页中断处理:
- 保护程序运行状态;
- 分析中断原因;
- 转入缺页中断处理程序进行处理;
- 回复程序运行状态,继续运行程序。
7.9动态存储分配管理
动态存储分配,即指在目标程序或操作系统运行阶段动态地为源程序中的量分配存储空间,动态存储分配包括栈式或堆两种分配方式。
最佳置换(Optimal, OPT):
置换以后不再被访问,或者在将来最迟才会被访问的页面,缺页中断率最低。但是该算法需要依据以后各页的使用情况,而当一个进程还未运行完成时,很难估计哪一个页面是以后不再使用或在最长时间以后才会用到的页面。所以该算法是不能实现的。但该算法仍然有意义,作为衡量其他算法优劣的一个标准。
先进先出置换算法(First In First Out, FIFO)
置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。但是该算法会淘汰经常访问的页面,不适应进程实际运行的规律,目前已经很少使用。
最近最久未使用置换算法(Least Recently Used, LRU)
置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
最近最久未使用置换算法(Least Recently Used, LRU)
置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
LFU (Least Frequently Used)(最少使用页面置换算法)
该置换算法选择在之前时期使用最少的页面作为淘汰页。
7.10本章小结
本章主要介绍了hello的存储器地址空间,分别有逻辑地址、线性地址、虚拟地址、物理地址。然后分别介绍了intel逻辑地址到线性地址变换-段式管理和线性地址到物理地址的变换-页式管理。并且介绍了TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问。还介绍了fork()函数的内存映射以及execve进程的内存映射。最后介绍了缺页故障与缺页中断处理和动态存储的分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备有着不同的驱动程序,用户通过操作文件的方式操作设备,而落实到操作系统上则是调用适当的驱动程序接口。
设备管理:unix io接口
分时执行任务时为了合理利用系统设备,所有设备都应由操作系统按一定原则统一管理。进程要进行IO操作时需要向操作系统申请权限。
8.2 简述Unix IO接口及其函数
对于内核而言所有打开的文件都是通过文件描述符引用。
读或写一个文件时使用open或creat返回文件描述符标识该文件,将其作为参数传递给read或write。
UNIX系统shell将标准输入输出与文件描述符关联。
UNIX函数:open、read、write、lseek、close。
8.3 printf的实现分析
Vsprintf接受三个参数char *buf,const char *fmt,va_list ards。
图50 vsprintf函数原型
其先申请字符指针p、字符数组tmp和va_list型数据p_next_arg。
首先循环判断输出格式,然后用条件控制转移,如果输出的是x%类型,用itoa函数将整数转换为字符串,然后strcpy进tmp中,最后返回字符串长度。如果是其它类型则直接返回长度。
Write(buf,i)的实现,从产生中断信号开始,当调用write函数时,会先产生一个中断信号,父进程保存运行环境和数据,然后进入内核模式处理中断,然后利用文件描述符向显示器输入字符。UINX设备管理系统接受到命令,执行显示器驱动程序利用字模库译码要显示的字符,转换成vram。最后显示器按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了UNIX系统的IO管理方式,分别有设备的模型化和IO接口。然后介绍了printf函数从io接口到显示器电路显示的全部过程。
结论
Hello程序从一个文本文件的编辑开始,经预处理完成宏替换、宏展开等操作,成为一个可以进行编译的文本文件,再经编译过程将源文件翻译成汇编语言,再经汇编过程进一步翻译成机器语言,经过这两步源文件已经从人类易读的高级语言变成了二进制代码。可是这还不够,还有经过一个链接过程将程序与运行所需要的文件链接在一起才会变成一个可以直接复制在内存中运行的可执行文件。
此时,程序方面的准备工作已经完毕,我们转向操作系统一方。操作系统的内核控制着整个机器资源的调用,而操作系统通过shell向内核发起申请请求系统资源。当我们在一个进程中要执行hello时,父进程首先会fork一个子进程,作为hello程序运行的进程,并且将父进程运行的环境和共享库等资源给fork出的子进程也复制一份,然后当进程通过fork的返回值判断自己所处的是子进程时,会调用execve函数来将hello加载到内存中并执行。当然在hello执行之前,还会保存父进程运行中的寄存器等信息,以便恢复运行状态,这个就是进程的上下文。父进程与子进程同时占用cpu资源,都以不连续的时间片运行,这是进程调度中的时间片管理。
而在存储管理中,MMU支持下的VA到PA、TLB、4级页表、3级cache、pagefile等等结构都是为了加速程序运行而生,在它们的支持下程序可以以更有效的方式运行。除此之外,外接键盘、显示器等也不是生来就有,它们本身有着自己的电路结构和实现原理,并被操作系统通过驱动程序调度,而用户在使用这些外接设备时无法直接使用驱动程序,而是用操作系统IO接口的方式调用。
一个简单的hello程序却经历了如此复杂的过程,不实践一遍很难直观感受到其中之美。整个流程的步骤粗看好似复杂繁琐,实际上每一步都有明确的分工和不可缺少的作用,每一步对程序运行的作用都可以直观感受的到。我在进行整个实践的过程中时常为前人的智慧所折服,繁多的结构之间却是和谐统一,整个程序执行下来有条不紊,并且给了我们上层调用以极大的自由空间。
单着眼于计算机系统的震撼架构并不是实验全部的内容,计算机与外接IO设备的交互同样令人惊叹,cpu的实现我才疏学浅无法直观感受,但是显示器、键盘和鼠标等外设设备的调用让我真切的感受到了计算机系统的魅力。
我想如此复杂却和谐的设计也不是一蹴而就,而是经过长久的努力改进而得,我希望未来能够深入理解计算机系统,更好的了解乃至优化程序的一生。
附件
Hello.i | Hello.c预处理后的文本文件 |
Hello.s | Hello.i编译后的汇编文件 |
Hello.o | Hello.s汇编之后的可重定位目标文件 |
Hello | 链接之后的可执行目标文件 |
Hello.out | Hello反汇编之后的可重定位文件 |
Hello.txt | Hello的反汇编文件 |
Helloo.txt | Hello.o的反汇编文件 |
参考文献
[1] https://www.coder.work/article/165699.
[2] 深入理解计算机系统--异常 - 思过崖 - 博客园 (cnblogs.com)
[3] (35条消息) 虚拟地址、逻辑地址、线性地址、物理地址的区别_种向日葵的小仙女的博客-CSDN博客_逻辑地址和虚拟地址.
[4] 段页式访存——逻辑地址到线性地址的转换 - 简书 (jianshu.com)
[5] (35条消息) 浅析线性地址到物理地址的转换_weixin_30908649的博客-CSDN博客
[6] MMU段式映射(VA -> PA)过程分析 - 王楼小子 - 博客园 (cnblogs.com)
[7] Linux四级页表及其原理 - 简书 (jianshu.com)
[8] (35条消息) CPU 与 Memory 内存之间的三级缓存的实现原理_Shy_tom的博客-CSDN博客_cpu和memory
[9] (35条消息) 第2章 Cache和内存_ruchuer的博客-CSDN博客_cache和内存
[10] (35条消息) 256-Linux虚拟内存映射和fork的写时拷贝_-林泽宇的博客-CSDN博客
[11] Linux 虚拟内存系统, 内存映射, fork, execve, malloc, free 动态内存分配与释放 | 编程之禅 (wangjunstf.github.io)
[13] 深入理解【缺页中断】及FIFO、LRU、OPT这三种置换算法 - 云+社区 - 腾讯云 (tencent.com)
[15] (35条消息) Linux内核:IO设备的抽象管理方式_墨篙和小奶猫的博客-CSDN博客