目 录
6.2 简述壳Shell-bash的作用与处理流程... - 29 -
6.3 Hello的fork进程创建过程... - 30 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 38 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 40 -
7.5 三级Cache支持下的物理内存访问... - 40 -
7.6 hello进程fork时的内存映射... - 41 -
7.7 hello进程execve时的内存映射... - 41 -
第1章 概述
1.1 Hello简介
Linux下,在shell中启动指令,执行fork()产生子进程,此时hello.c从Program(程序)变为Process(进程),便为hello的P2P。
而后在子进程中调用execve(),映射虚拟内存,而后程序开始时载入进物理内存,进入CPU处理,进入main函数执行目标代码,CPU为执行文件hello分配时间片,执行逻辑控制流,根据汇编语言指令执行取指、译码、执行、更新等操作。内存管理器和CPU在执行过程中通过L1、L2、L3三级缓存和TLB多级页表在物理内存中取的数据,通过I\O系统根据代码指令进行输出。当程序运行结束时回收进程,释放内存,删除与执行程序相关的数据结构,从一无所有,到完全结束程序,便为hello的020。
1.2 环境与工具
硬件环境:X64 CPU Intel Core i7;16G RAM;
软件环境:Windows10,Ubuntu
开发与调试工具:gedit、vim、gcc、edb、IDA Pro64、readelf、hexedit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i (预处理文件)
hello.s(编译得到的汇编代码)
hello.o(汇编得到的机器语言)
helloelf(hello.o可重定位目标文件)
hello_objdump(hello.o的反汇编)
hello(hello的可执行文件)
hello_elf(hello的可重定位目标文件)
1.4 本章小结
本章介绍了在本实验中P2P和O2O的特殊含义,介绍了实验的硬件环境,软件环境以及使用的开发调试工具,并列举了hello生成的中间结果文件及其作用。
第2章 预处理
2.1 预处理的概念与作用
概念:
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成 二进制代码 之前的过程。 典型地,由预处理器 (preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
作用:
C语言预处理程序的作用是根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。
预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
2.3.1预处理后的hello.i文件对宏定义进行了展开,调用了对应.h头文件,如下图:
(1)调用stdio.h
(2)调用unisted.h
(3)调用stdlib.h
2.3.2在调用stdio.h,stdlib.h和unisted.h库文件时,同时调用了未在源程序中直接调用的库文件,如start.h,cdefs.h,wordsize.h等库文件。
2.3.3预处理文件同时删除了所有注释,因此在hello.i文件中找不到任何源程序中的注释。
2.3.4在预处理文件中的main函数与hello.c文件中的main函数没有区别。
2.4 本章小结
本章对hello.c文件进行了预处理,得到了hello.i预处理文件,并对hello.i文件中的头文件调用,main函数实现等方面进行了解析。
第3章 编译
3.1 编译的概念与作用
概念:
编译器ccl将预处理文件翻译成汇编语言。
作用:
编译器做一些语法分析、词法分析、语义分析等,若检查无错误,便将高级程序设计语言(C语言)书写的程序转变为与其等价的汇编语言程序(可能依情况做一些优化),汇编语言是介于高级语言和机器语言之间的中间代码,它为不同高级语言的不同编译器提供了通用的输出语言,使他们产生的输出文件都是用的一样的输出语言。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1关键词
.file 声明源文件
.text 保存代码
.globl 声明全局变量
.data 数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.section 把代码划分成若干段
.long 声明long类型,其他等同
.type 用来指定是函数类型或是对象类型
.size 声明大小
3.3.2数据
(1)字符串
定义了两个字符串LC0和LC1。LC0字符串作为第一个printf的参数传入,LC1字符串作为第二个printf的参数传入。
(2)整型变量
1i做为循环体的判断标准,存储在-4(%rbp)中。
2argc作为main函数的参数,存储在-20(%rbp)中。
(3)数组
可以通过L4(循环部分)中输出函数前的两次取值找到argv[1],argv[2]的位置。其中,第三行的movq (%rax), %rdx为argv[2]。movq %rax, %rsi为argv[1]。
3.3.3赋值
将i的初值赋为0
3.3.4算数操作
在L4中有i++的算数操作。
3.3.5关系操作
1将argc与4进行比较。
2将i与7进行比较。
3.3.6控制转移
1将argc与4进行比较,若相等则跳转到.L2。
2将i与7进行比较,若i小于等于7,则跳转到.L4
3.3.7函数调用
1调用了printf函数,参数为%rdx和%rsi。
2调用了atoi函数,参数为%rdi。
3调用了sleep函数,参数为%edi
4调用了getchar函数。
3.4 本章小结
本章实现了从高级语言到汇编语言的转换,并且对转换后的汇编语言,从关键词,数据,数组,赋值操作,算数操作,关系操作,控制转移,函数调用等方面进行了解释和介绍。只有通过编译,高级语言,才能被之后的汇编器理解,从而汇编得出真正计算机可以识别的机器语言。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编的概念是指的将汇编语言程序翻译成机器指令,并将这些指令打包成一种叫做可重定位目标程序,并将这个结果保留.o目标文件中。
作用:
由汇编语言生成计算机能够直接识别的二进制数串,实现了二进制转化。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
(1)ELF可重定位目标文件中首先是ELF头,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
通过查询hello文件的ELF头信息,我们可以知道hello文件类型为REL,文件中共有14个节。
(2)节头部表,描述了不同节的位置和大小,目标文件中的每一个节都有一个固定大小的条目。NameSize表示各段名称和大小,Adress表示各段的起始地址,Offset表示偏移量大小。
(3)重定位项目,其中,Offset是需要被修改的引用的字节偏移(在代码节或数据节的偏移),Info指示了重定位目标在.symtab中的偏移量和重定位类型,Type表示不同的重定位类型。偏移量表示需要被修改的引用的节偏移。符号名称标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用。加数是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
(4)符号表,存放了程序中定义和引用的函数和全局变量的信息。符号表保存了查找程序符号、为符号赋值、重定位符号所需的全部信息。符号是表示每个ELF文件的一个重要部分,因为它保存了程序实现或使用的所有(全局)变量和函数。如果程序引用了一个自身代码未定义的符号,则称之为未定义的符号。此类引用必须在静态链接期间用其他目标模块或者库解决,或在加载期间通过动态链接。
4.4 Hello.o的结果解析
(1)操作数的进制不同
在第三章的hello.s中,操作数用的都是十进制,而在hello.o的反汇编中,操作数用的都是16进制。
(2)分支转移不同,在第三章的hello.s中,分支转移都是跳转到对应的分支段名称如L2,L4等分支。而在hello.o的反汇编中,分支转移都是跳转到一个地址(地址由计算得出),该地址有下一条汇编指令。
(3)函数调用不同,第三章中hello.s中的汇编语言在函数调用时,在call指令后用函数的名字表示对其调用,而hello.o的反汇编在call指令后加上下一条指令的地址来表示。观察机器语言,发现其中操作数都为0,即函数的相对地址为0,因为再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。
4.5 本章小结
在本章中,hello首次变成了计算机可以直接识别的机器语言(二进制01串),并且我们还通过反汇编得到了hello.o对应的汇编代码,并与hello.s进行了对比,这个过程中,体现了顺向编译和逆向反汇编的不同,也让我们更好地理解计算机时如何识别机器语言的。
第5章 链接
5.1 链接的概念与作用
概念:
链接 (linking)是将各种代码和数据片段收集并组合为一个单一文件的过程,所得到的文件可以被加载 (复制)到内存并执行。
作用:
把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件
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的格式
(1)ELF头,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
(2)节头目表,描述了不同节的位置和大小,目标文件中的每一个节都有一个固定大小的条目。
NameSize表示各段名称和大小
Adress表示各段的起始地址
Offset表示偏移量大小
(2)动态section
(3)Type指明描述的内存段的类型,或者如何解析该程序头的信息。如DYNAMIC表示该段描述的是动态链接库,NOTE表示该段描述一个以’\n’结尾的字符串,这个字符串中包含附加信息等。Offset指明该段内容的起始位置相对于文件开头的偏移量。VirtAddr指明该段中内容的起始位置在进程地址空间中的虚拟地址。PhysAddr指明该段中内容的其实位置在进程地址空间中的物理地址。对于目前大多数操作系统而言,这个字段大多数情况下不起作用。(由于无法预知物理地址)
5.4 hello的虚拟地址空间
.init如下
.got.plt如下
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
5.5 链接的重定位过程分析
objdump -d -r hello > hello.objdump得到hello的反汇编文件hello.objdump。
区别:
(1)hello.objdump中的跳转均为直接跳转到相应汇编指令的虚拟地址,而hello.o的反汇编hello_objdump中的跳转均为跳转到一个相对地址。
(2)hello.objdump中的代码都有确定的虚拟地址,也就是hello.objdump已经完成了重定位,而hello.o的反汇编中的代码虚拟地址均为0,未完成重定位。
(3)hello.objdump中增加了外部函数的,,并且增加了更多的节。
分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
重定位过程分析:
汇编器生成一个目标模块时从地址0开始生成代码和数据节 ,它并不知道数据和代码最终将放在内存的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。链接器在重定位步骤中,合并输入模块并将运行时地址赋给输入模块定义的每个节、符号。当这一步完成时,程序中的每条指令和全局变量才拥有唯一的运行时内存地址。
所以过程如下:首先,重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。包括hello.o在内的所有可重定位目标文件中的.data节被全部合并成一个节,这个节成为输出的可执行目标文件hello中的.data节。然后,连接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中每条指令和包括.rodata、sleepsecs等全局变量都有唯一的运行时内存地址了。最后,重定位节中的符号引用。链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。
5.6 hello的执行流程
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start
libc-2.31.so!__libc_start_main
-libc-2.31.so!__cxa_atexit
-libc-2.31.so!__libc_csu_init
hello!_init
libc-2.31.so!_setjmp
libc-2.31.so!exit
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。延迟绑定通过GOT和PLT实现,将过程地址的绑定推迟到下一次调用该过程时。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
调用init前的.got.plt:
调用后:
5.8 本章小结
本章介绍了链接的概念及作用,在Linux下链接的命令行,对hello的elf可重定向文件格式与hello.o的elf可重定向文件格式进行了详细的分析对比,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,在最后对hello进行了动态链接分析,使得对hello的链接过程有了一个深刻的理解和体会。
第6章 hello进程管理
6.1 进程的概念与作用
概念:在操作系统的角度来看进程是操作系统分配资源的最小单位。
简单来说进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进程并不局限于一段可执行程序代码(代码段)。通常进程还要包含其他资源,像打开的文件(即在Linux中对应的文件描述符),挂起的信号,内核内部数据,处理器的状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然包括用来存放全局变量的数据段等。
作用:
进程提供给应用两个关键抽象:一个独立的逻辑控制流,它提供一个假象,好像我们的程序在独占地使用处理器。一个私有的地址空间,它提供一个假象,好像我们的程序在独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
定义:在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。
功能:使用用户的缺省凭证和环境执行shell。
1.解释用户输入的命令,将他们传递给内核;
2.调用其他程序,给其他程序传递数据和参数,并且获取程序的处理结果。
3.在多个程序之间传递数据,把一个程序的输出作为另外一个程序的输入。
4. shell本身也可以被其他程序进行调用。
处理流程:
1,从终端读入输入的命令。
2,Shell对用户输入命令进行解析,判断是否为内置命令。
3,如果是内置命令则立即调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。
4,判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。
6.3 Hello的fork进程创建过程
当hello的父进程调用fork时,创建一个子进程,fork调用一次,返回两次。新创建的子进程几乎但不完全与父进程相同,在父进程中,fork返回值为父进程的pid,子进程中,fork返回值为0。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的ID。父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回调用程序。execve函数覆盖当前进程的代码、数据、栈,有相同的PID,继承已打开的文件描述符和信号上下文。
在execve加载了hello之后,它调用启动代码,启动代码设置栈并将控制传递给新程序的主函数。当main函数开始执行时,对用户的栈进行分析。从栈底到栈顶,首先是参数和环境字符串,紧随其后的是以null为结尾的指针数组,其中每一个指针都指向栈中一个环境变量字符串。全局变量erviron指向这些指针中的第一个envp[0]。紧随环境变量数组之后的是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。在栈顶的是系统启动函数的libc_start_main的栈帧。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。新的栈和堆段被初始化为0。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件中的内容。最后加载器跳到_start地址,它最终调用hello的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
操作系统内核使用一种称为上下文切换的较高曾测的异常控制流来实现多任务。内核为没有给进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器,浮点寄存器等。
处理器通常是用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。当设置了模式位时,进程就运行在内核模式中,一个在内核模式中运行的进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。反之若没有设置模式位,上述不能实现。
对于hello进程而言,进程hello初始运行在用户模式中,直到它通过执行系统调用函数sleep或者exit时便陷入到内内核。内核中的处理程序完成对系统函数的调用。之后,执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。
6.6 hello的异常与信号处理
异常:
中断,中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
陷阱,陷阱是有意的异常,是执行一条指令的结果。
故障,故障有错误情况引起,它可能能够被故障处理程序修正。
终止,终止是不可回复的致命错误造成的结果,通常是一些硬件错误。
6.6.1Ctrl+c:进程中止
6.6.2乱按:在乱按不包括回车键时,进程无法结束,在键入回车键时,进程结束。
6.6.3Ctrl+z:进程被暂时挂起,暂停
1ps:列出了当前系统中运行的进程,有如下3个,hello也在其中。
2jobs:查看当前终端放入后台的进程,只有hello一个
3pstree:列出当前的进程,以及它他们的树状结构。
4fg:把后台命令恢复到前台执行,于是又执行了一次。
5kill:结束了hello进程。
6.7本章小结
本章简单介绍了进程的概念与作用,简述了shell-bash的作用以及对hello的处理流程。然后分别分析了hello的fork和execve过程,让我们对hello进程的创建和运行有了一定的了解。之后简述了hello进程的模式切换。最后对hello进程运行过程中,可能产生的异常和信号进行分析,并针对不同外部指令进行观察。通过对hello的创建、加载和终止,使得对hello执行过程中产生信号和信号的处理过程有了更多的认识,加深了对异常的理解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:指由程序产生的与段相关的偏移地址部分。
线性地址:指的是虚拟地址到物理地址变换之间的中间层,是处理器可寻指的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。对于hello而言,程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
虚拟地址:指的是由程序产生的由段选择符和段内偏移地址两个部分组成的地址。对于hello而言,就是hello的虚拟地址。
物理地址:指的是现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理式直接将逻辑地址转换成为物理地址,即CPU不支持分页机制。
段式存储管理中以段为单位分配内存,每段分配一个连续的内存区,但各段之间不要求连续,内存的分配和回收类似于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包括两个部分:段号和段内位移。
在段式管理地址变换的过程中间,需要位运行的进程建立一个段表。
段表组成:段号、段长、存储权限、状态、起始地址、修改位、增补位
段的共享和保护机制:在段式系统中,分段的共享是通过两个作业的段表中相应表目都指向被共享部分的同一个物理副本来实现的。 因为段是一个完整的逻辑信息,所以可以共享,但是页不完整,难以实现共享。不能修改的过程称为纯过程或可重入过程。这样的过程和不能修改的数据是可以共享的,而可修改的程序和数据则不能共享。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分以固定长度为单位的组,称为页(page)。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。分页单元中,页目录是唯一的,它的地址放在CPU的CR3寄存器中,是进行地址转换的开始点。每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到CR3寄存器中,将别个的保存下来。每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从CR3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址;
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
LB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。
CPU产生虚拟地址VA,VA传送给虚拟地址MMU,MMU使用前32位VPN作为TLBT前32位+TLBI(后四位)TLB中进行相应的匹配,如果命中,那么将得到PPN,将其与VPO进行组合将会得到52位的物理地址PA
如果TLB没有命中,那么MMU向页表当中进行相应寻找,CR3确定第一级页表的起始地址。VPN1确定在确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。如果查询PTE的时候找不到,那么会引发缺页异常。
7.5 三级Cache支持下的物理内存访问
在访问内存的过程中,先对一级cache进行访问,如果一级cache不命中那么访问二级cache,如果二级cache不命中,那么访问三级cache,依然不命中,那么访问主存,如果主存缺页中断那么就访问硬盘。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。当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做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个人口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页故障(page fault)。
在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
1. 保护CPU现场
2. 分析中断原因
3. 转入缺页中断处理程序进行处理
4. 恢复CPU现场,继续执行
但是缺页中断时由于所要访问的页面不存在与内存时,有硬件所产生的一种特殊的中断,因此,与一般的中断存在区别: 在指令执行期间产生和处理缺页中断信号;一条指令在执行期间,可能产生多次缺页中断;缺页中断返回时,执行产生中断的那一条指令,而一般的中断返回时,执行下一条指令。
7.9动态存储分配管理
在动态分区分配存储管理方式的内存分配中,当有作业请求装入时,根据作业的需要内存空间的大小查询内存各个空闲区,从中找到一个大于或等于该作业大小的内存空闲区,然后按作业需求量划出一个分区装入该作业。作业执行完后,它占用的内存区被收回,称为空闲区,如果该空闲区的相邻分区也是空闲区,就要合并成为一个空闲区。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
程序通过malloc函数来从堆中分配块,malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。如果malloc遇到问题,那么它就返回NULL,并设置errno。malloc不初始化它返回的内存。
7.10本章小结
本章首先讨论了逻辑地址、线性地址、虚拟地址、物理地址的概念以及彼此间的联系,接着介绍了从逻辑地址到线性地址、从线性地址到物理地址的变换过程和原理。介绍了利用TLB和页表实现VA到PA的变换,以及利用三级Cache完成物理内存访问的过程。分析了hello在fork和execve时的内存映射。概括了缺页故障和缺页中断的处理流程。最后介绍了隐式空闲链表和显示空闲链表这两种动态存储分配管理的方法。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。
设备管理:这种将设备映射为文件的方式,允许linux内核引出一个低级的,简单的应用接口称为Unix I/O
8.2 简述Unix IO接口及其函数
IO接口:
(1)打开文件:内核返回一个文件描述符,该文件描述符是一个非负整数,当对这个文件进行读写的时候,将这个参数传递给read或write。linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1)和标准错误(描述符为2)。
(2)改变当前的文件位置:内核对于每个文件保持一个初始为0的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
(3)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。
(4)关闭文件:当应用访问文件完成时,关闭这个文件。
函数:
(1)int open(char* filename,int flags,mode_t mode):根据文件名返回一个描述符,flags为文件打开方式,mode为访问权限,若文件打开失败返回-1,若成功将返回最小的未用的文件描述符的值。
(2)int close(int fd):关闭指定文件描述符(fd)的文件,文件关闭成功返回0,关闭失败返回-1,若成功将返回最小的未用的文件描述符的值。
(3)read(int fd, void *buf, size_t nbytes) :fd为指定文件描述符,buf为读取文件数据缓冲区,nbytes为读取的字节数,若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数,若读取失败,返回-1。
(4)write(int fd, const void* buf, size_t ntyes):buf为写入内容的缓冲区,ntyes为期待写入的字节数,一般情况下返回值与ntypes相等,否则写入失败。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
printf的实现如下:
vsprintf接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,返回要打印的字符串长度,通过write函数将字符串数组buf中的i个内容写到终端。
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
syscall的功能是不断地打印字符,直到遇到’\0’为止。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar通过调用read函数来读取字符,第一个参数0表示从标准输入中读入,第二个参数为指向输入字符的指针,第三个参数为读入字符的个数。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错或遇到文件结尾则返回EOF。
8.5本章小结
在本章中,我们介绍了linux下IO设备的管理方法,简述了unix IO及其接口函数,对printf和getchar的实现进行了分析,让我们对相关内容有了更深入的理解。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello是每一个程序员最早接触的程序,然而这个简单的程序很快就被抛弃。究其历史,hello是编程语言的最终之作,是人类智慧结晶的简单体现。在计算机中,hello.c经历了预处理变成了预处理文件hello.i,hello.i再经历编译的过程得到了汇编代码文件hello.s,hello.s之后经历汇编得到hello.o汇编文件(一个计算机可以识别的01串文件),hello.o文件在链接之后成为了一个可执行文件hello。Hello运行之后,计算机会调用fork函数生成一个进程,在运行这个进程时,可以用信号来进行终止或暂停,最后结束运行之后,进程被回收,hello的一生就此结束。
我认为计算机系统是每一名程序员的必修课程,它让我们从较为底层的层面更深刻地理解计算机的运行方式,程序的开始,执行和结束。它让我对计算机这一人类智慧的结晶有了更深入的理解,更崇高的敬意。