摘 要
本文以hello的整个生命周期为线索,对这个过程中所涉及到的各种问题与操作进行研究与探讨。通过合理运用这个学期在计算机系统课程上所学习的知识,分析研究hello程序在Linux系统下的P2P和020过程。并在各种分析工具的帮助下,探究Linux框架下整个程序的生命历程,从而使对计算机系统知识的学习更加深入。
关键词:hello;编译;汇编;进程;计算机系统;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章
概述… - 4 -
1.1 Hello简介… - 4 -
1.2 环境与工具… - 4 -
1.3 中间结果… - 4 -
1.4 本章小结… - 4 -
第2章
预处理… - 5 -
2.1 预处理的概念与作用… - 5 -
2.2在Ubuntu下预处理的命令… - 5 -
2.3 Hello的预处理结果解析… - 5 -
2.4 本章小结… - 5 -
第3章
编译… - 6 -
3.1 编译的概念与作用… - 6 -
3.2 在Ubuntu下编译的命令… - 6 -
3.3 Hello的编译结果解析… - 6 -
3.4 本章小结… - 6 -
第4章
汇编… - 7 -
4.1 汇编的概念与作用… - 7 -
4.2 在Ubuntu下汇编的命令… - 7 -
4.3 可重定位目标elf格式… - 7 -
4.4 Hello.o的结果解析… - 7 -
4.5 本章小结… - 7 -
第5章
链接… - 8 -
5.1 链接的概念与作用… - 8 -
5.2 在Ubuntu下链接的命令… - 8 -
5.3 可执行目标文件hello的格式… - 8 -
5.4 hello的虚拟地址空间… - 8 -
5.5 链接的重定位过程分析… - 8 -
5.6 hello的执行流程… - 8 -
5.7 Hello的动态链接分析… - 8 -
5.8 本章小结… - 9 -
第6章
hello进程管理… - 10 -
6.1 进程的概念与作用… - 10 -
6.2 简述壳Shell-bash的作用与处理流程… - 10 -
6.3 Hello的fork进程创建过程… - 10 -
6.4 Hello的execve过程… - 10 -
6.5 Hello的进程执行… - 10 -
6.6 hello的异常与信号处理… - 10 -
6.7本章小结… - 10 -
第7章
hello的存储管理… - 11 -
7.1 hello的存储器地址空间… - 11 -
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 -
7.9动态存储分配管理… - 11 -
7.10本章小结… - 12 -
第8章
hello的IO管理… - 13 -
8.1 Linux的IO设备管理方法… - 13 -
8.2 简述Unix IO接口及其函数… - 13 -
8.3 printf的实现分析… - 13 -
8.4 getchar的实现分析… - 13 -
8.5本章小结… - 13 -
结论… - 14 -
附件… - 15 -
参考文献… - 16 -
第1章 概述
1.1 Hello简介
(1)P2P
Hello作为一个程序的生命历程起源于某个程序员通过某个IDE创建并编写的一个名为hello.c的文本文件。该源文件在预处理器,编译器,汇编器和链接器的作用下,经过一系列的步骤(如下图)——首先经预处理器生成了修改后的源程序hello.i,然后由编译器生成了汇编语言程序hello.s,再通过汇编器生成可重定位的目标程序hello.o。最后再由链接器将hello.o与调用的库中的其他可重定位的目标程序链接之后就生成了可执行目标程序hello。
图1.1
(2)020
用户通过shell执行可执行目标程序hello,并且同时为该程序映射出虚拟内存。当该程序的进程开始运行时,系统会为其分配并且载入物理内存。在进程运行过程中,shell会调用fork函数,生成一个子进程,并在这个子进程中调用execve函数加载hello程序。然后程序会跳转到_start地址,最终调用hello的main函数。当hello的进程结束时,shell会将其的内存空间回收。
1.2 环境与工具
硬件环境:Intel®
Core™ i5-8300H CPU;16.00GB RAM; 512GSSD
软件环境:Vmware
Workstation 15 Player;Ubuntu 16.04 LTS 64位;Windows 10 64位;
开发工具:Dev-C++ ;Visual Studio Code;GCC;objdump;EDB;readelf;hexedit;vim;Ld;
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件
文件作用
hello1.i
预处理之后的文本文件
hello.s
编译之后的汇编文件
hello.o
汇编之后的可重定位目标执行
hello
连接之后生成的可执行目标文件
asm.txt
对hello.o的反汇编文件
asm1.txt
对hello的反汇编文件
1.4 本章小结
在本章中我对hello的P2P与020过程进行了分析,并罗列了进行本次大作业的软硬件环境、所使用的工具与各中间结果文件,为下一步的实践做了准备。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
1.概念
所谓预处理是指进行编译的第一遍扫描之前所做的工作,是C语言中的一个重要功能,由预处理器负责完成。C语言提供了多种预处理功能,如宏定义,文件包含,条件编译等。
2.作用
1.根据宏定义将程序中的对应的宏进行替换。
2.读取系统调用的库文件中的内容,并把它直接插入到程序中去。
3.针对#ifdef等进行条件编译。
2.2在Ubuntu下预处理的命令
对hello预处理命令为:gcc -E
hello.c -o hello1.i
在Linux系统中执行:
图2.1
发现文件夹中出现一个新文件hello1.i
图2.2
2.3 Hello的预处理结果解析
使用文本编辑器打开文件hello1.i
图2.3
可以看到在hello1.i文件中在原本的main函数之前添加了大量代码,有三千余行之多,这与原本hello.c文件中的二十余行代码形成了鲜明的对比。这也说明了从源文件到可执行目标文件所进行的转换过程的复杂性。
对helllo1.i文件进行初步的浏览,可以发现一些有明显目的性的代码段:
在下图中的代码段指明了所使用的各个运行库在计算机中的位置
图2.4
而下图中的代码段则声明了部分可能使用到的函数
图2.5
2.4 本章小结
本章节对c语言在编译前的预处理过程进行了简单的介绍,说明了预处理过程的概念和作用,并且对预处理过程进行了演示,同时举例说明了预处理的结果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:所谓编译即将预处理好的高级语言程序文本翻译成能执行相同操作的汇编语言的过程。
作用:因为计算机底层无法理解高级语言,所以需要一个编译器将高级语言转换成更接近机器语言的汇编语言,从而实现从高级语言转换成计算机可执行的二进制机器语言的过程。
3.2 在Ubuntu下编译的命令
对hello1.i进行编译的命令为:gcc -S -o hello.s hello1.i
图3.1
执行后发现文件夹中出现一个新文件hello.s
图3.2
使用文本编辑器打开hello.s文件,下图即为生成的汇编程序(不完全)
图3.3
3.3 Hello的编译结果解析
3.3.1对各数据类型的处理
整数:
1.sleepescs: hello的源程序定义了一个名为sleepescs的全局变量,并将其存放在.data节中(.data存放已经初始化的全局和静态变量)。如下图,全局变量sleepescs在.data段中,对齐方式设置为4、类型设置为object、大小为4字节、数据定义为long,其值为2。
图3.4
2.i:hello的源程序在main函数中定义了一个名为i的局部变量,在本程序中,变量i被保存在寄存器之中,大小为4个字节。
图3.5
argc:hello的源程序为main函数制订的名为argc的形式参数
图3.6
4.立即数:没有保存到堆栈或者寄存器而直接写入汇编程序中的数据,以符号$开头
图3.7
字符串:
1.“Usage: Hello 学号 姓名!\n”:该字符串的格式为UTF-8,声明在.LC0段,保存在.rodata中。
2.“Hello %s %s\n”:声明在.LC1段,同样保存在.rodata中。
图3.8
数组:hello的源程序定义了两个数组类型argv[1]和argv[2],均声明在.rodata只读数据段中。当我们在shell中输入参数后,argv存储该参数,且数组中一个元素大小为8字节。
图3.9 栈帧中对argv参数的调用操作
3.3.2 对于赋值操作的处理
程序中一共有两处赋值操作:
1.int sleepsecs=2.5 :由之前的定义可知,sleepsecs为全局变量,所以直接在.data节中将变量sleepsecs声明为值为2的长整型。
2.i=0:由之前的定义可知,i为局部变量,所以可在main函数的汇编代码中执行语句movl $0, -4(%rbp),从而将立即数0赋给局部变量i。
3.3.3 类型转换操作
hello.c程序中存在隐式的类型转换,具体原因为全局变量sleepsecs原本被声明为整型数据,但在程序中它被赋值为一个浮点数2.5,因此会产生隐式的类型转换,进行向零舍入的操作,使得hello.s中的变量sleepsecs被赋值为2 。
图3.10 hello.c中对变量sleepsecs的定义及赋值
图3.11 hello.s中sleepsecs的实际赋值
3.3.4 对于控制转移的处理
程序hello.c中的一共有两种类型的控制转移:
if条件判断语句的控制转移:该处控制转移的具体实现如下图
图3.12
由图中汇编代码可得,该控制转移的判断主要由语句cmpl $3,-20(%rbp)与je .L2来实现。即使用cmpl指令来比较argv和3,同时设置条件码,使用je判断ZF标志位,如果标志位为0,说明argv == 3,则不执行if中的代码直接跳转到.L2,否则顺序执行下一条语句。
控制for循环终止的控制转移: 该处控制转移的具体实现如下图:
图3.13
变量i为该for循环的索引。由helllo.c代码可知,在该for循环中变量i需迭代10次。程序控制流的逻辑为:首先无条件跳转到位于循环体.L4段之后的比较代码,使用cmpl指令进行比较,当i<=9时,则执行.L4段中的 for循环体。一旦i>9,则循环结束。
3.3.5
对于算术操作的处理
程序hello.c中的一共有三处算术操作,其中后两处为同一类型,在此将其归结为一种:
i++:此处算术操作在汇编代码中的实现如下:
图3.14
使用addl指令使局部变量i自增一,从而实现一个程序计数器的功能。
leaq操作:此处算术操作在汇编代码中的实现如下:
图3.15
leaq指令可以将存储在.rodate节中的printf语句的格式串传递给寄存器%rdi作为调用printf函数的参数,从而实现函数的正常功能。
3.3.6
对于关系运算的处理
源程序hello.c中一共有两处关系运算:
argc!=3:hello.s中使用如下的指令来进行此关系运算:
图3.16
通过使用cmpl命令来计算argc-3并且设置条件码ZF,为下一步利用条件码进行跳转作准备。
i<10:hello.s中使用如下的指令来进行此关系运算:
图3.17
同样使用cmpl命令来计算i-9并且设置条件码ZF,为下一步利用条件码进行跳转做准备。
3.3.7
对于函数的处理
1.main函数
参数:argc和argv,对应的寄存器分别为%rdi和%rsi。
函数调用:系统启用函数__libc_start_main通过使用call语句来调用main函数,call指令将下一条指令的地址dest压入栈中,然后跳转至main函数执行。
函数返回:return
0为main函数的正常返回指令,即将寄存器%eax设置为0之后返回,如下图。
图3.18
2.printf函数
参数:argv[1]和argc[2]的地址或字符串参数首地址
函数调用:在if条件判断执行体与for循环执行体之中被调用。
函数返回:无返回
参数:传递参数寄存器%edi值为1
函数调用:通过指令call 调用exit函数
函数返回:无返回
参数:传递参数寄存器%edi值为sleepsecs
函数调用:通过指令call 调用sleep函数
函数返回:无返回
参数:无参数
函数调用:通过指令call 调用getchar函数
函数返回:无返回
3.4 本章小结
在本章中我们主要了解了编译的概念与作用,介绍了一些与编译相关的指令。 同时本章也对汇编文件hello.s进行了部分分析,分析了其数据类型及相关操作、算数操作、关系操作、函数操作和控制转移操作。在本章的学习中我对C语言编译后生成的汇编语言有了更深的了解,也为下一步的学习做好了准备。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:所谓汇编即将汇编文件hello.s翻译成机器语言指令,并且将这些指令转换成一种叫做可重定位目标程序的格式,然后将保存结果保存在目标文件hello.o中。
作用:将汇编语言翻译成对应的机器语言从而使机器可以执行该段代码。
4.2 在Ubuntu下汇编的命令
对hello.s文件进行汇编操作的命令:gcc -c -m64 -no-pie
-fno-PIC hello.s -o hello.o
在linux系统中执行以上命令,如下图:
图4.1
文件夹中出现了新文件hello.o
图4.2
4.3 可重定位目标elf格式
输入命令如下:readelf -a hello.o,输出的结果如下图:
作用:大体描述ELF文件各主要信息的段
图4.3 ELF头
作用:说明了hello.o文件中出现的各个节的大小、类型、位置等信息
图4.4 节头
作用:重定位节中包括的信息首先为.text节中进行重定位所需要的信息。整个重定位信息位于hello.o二进制文件中偏移量为0x340的位置上。其中每个重定位信息给予了相对首地址的偏移量,通过info提供了重定位信息。hello中需要进行重定位的信息有puts,exit,printf,sleepsecs,sleep,getchar
以及rodata。
图4.5 重定位节
4.4 Hello.o的结果解析
图4.6 将hello.o反汇编的内容
图4.7 hello.s的主要内容
与hello.s相比较,可以发现hello.o在反汇编之后不仅多了一些跳转位置的解释和注释,而且文件的左侧还增加了运行时的机器指令的位置,和每一行汇编语句所对应的机器指令。机器语言完全由0/1构成,在这里显示的时候为十六进制表示。
4.4.1函数调用
在hello.s中,调用一个函数被表示成call指令+函数名,但是在hello.o反汇编文件中,调用一个函数被表示成call+一个具体的地址位置。
4.4.2分支跳转
在hello.s中跳转的目标位置都是用.L3/.L4等标号来表示的,而在hello.o反汇编文件之中,跳转的目标被具体的地址位置所代替。
4.5 本章小结
在本章中我们具体描述了编译器将hello.s的汇编指令翻译成hello.o的机器指令的方式,通过readelf查看了hello.o的ELF文件,以反汇编的方式查看了hello.o反汇编的内容,同时针对hello.s与hello.o的不同做了分析,从而对从汇编指令到机器指令的变换有了更加深入的了解。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。目标文件是包括机器码和链接器可用信息的程序模块。
作用:解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。链接器还要完成程序中各目标文件的地址空间的组织工作。
图 5.1
5.2 在Ubuntu下链接的命令
ld的链接命令: 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
在linux系统中执行如下:
图5.2
文件夹中出现新文件hello
图5.3
5.3 可执行目标文件hello的格式
在终端中执行命令readelf -a hello
作用:大体描述ELF文件各主要信息的段
图5.4
作用: 节头给出了各节的基本信息,其中地址对应了这一节的信息在虚拟内存中的存储位置,偏移量对应了每一节相对于0x400000的偏移地址。信息的后两位对应类型,而前六位对应在symtab节中的ndx值。
图5.5
5.4 hello的虚拟地址空间
Data
Dump中虚拟地址各段的信息
图5.6
从上图我们可以得知程序从可执行文件中加载的信息从0x400000处开始存储,存储位置的范围为0x400000—0x400ff0。由ELF文件中节头的信息就可以知道虚拟内存中相应位置存储的信息。
图5.7
在ELF文件里的程序头中我们可以知道可执行文件的8个部分中每一个部分所对应的偏移地址,虚拟地址位置,物理地址位置,文件大小,存储大小,标志和对齐方式。系统需要程序头来创建进程映像,因此用来构造进程映像的目标文件必须具有程序头。
5.5 链接的重定位过程分析
使用命令objdump -d -r hello >
asm1.txt 生成反汇编代码。将反汇编代码文件asm1.txt与之前的反汇编代码文件asm.txt相对比。有以下发现:
1.文件asm1.txt中的汇编程序添加了许多动态链接库中的函数,程序原先调用的库函数都被添加至程序的代码中
图5.8
跳转地址由原先的相对偏移地址,变成了该函数或者语句所在的虚拟内存地址。
图5.9
5.6 hello的执行流程
数据载入:
_dl_start
_dl_init
执行:
__stat
_cax_atexit
_new_exitfn
_libc_start_main
_libc_csu_init
运行:
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
Exit
5.7 Hello的动态链接分析
首先找到GOT表地址:
图5.10
通过edb调试,发现在dl_init前后
图5.11
变为了
图5.12
即dl_init调用之后,0x6008b8和0x6008c0处的数据分别改变为00007f1ee350f168和00007f1ee32ff870,其中GOT[1]指向重定位表(.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址,GOT[2]指向动态链接器ld-linux.so运行时地址。
5.8 本章小结
本章介绍了链接的概念和作用,分析了hello的ELF格式,虚拟地址空间的分配,重定位和执行过程还有动态链接的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正常运行所需要的状态组成的。每个状态包括存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
作用:进程提供给应用程序关键抽象:1.独立的逻辑控制流 2.私有的地址空间:
6.2 简述壳Shell-bash的作用与处理流程
作用:在操作系统中提供了一个用户与系统内核进行交互的界面,使得用户可以通过这个界面访问操作系统内核的服务。
处理流程:
1.从终端读入用户输入的命令
2.分析输入内容,获得输入参数
3.如果是内置命令则立即执行,否则调用相应的程序为其分配子进程并运行
4.在程序运行期间,shell需要接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
父进程通过调用fork()函数创建一个子进程,新创建的子进程的虚拟地址空间是与父进程相同的一份副本,包括代码、数据段、共享库、堆以及用户栈。父进程和新创建的子进程之间最大的区别在于他们拥有不同的PID。
fork()函数的特点是在执行时被调用一次,但是却返回两次,一次是返回到父进程,一次是返回到新创建的子进程。
hello的fork进程创建过程如下图:
图6.1
6.4 Hello的execve过程
execve函数的作用是在当前进程的上下文中加载并运行一个新的程序。但是与fork函数不同,execve函数直接在当前的进程中删除现有的虚拟内存段,并穿插一组新的代码、数据、堆和用户栈的段,再将栈和堆初始化为0,代码段与数据段初始化为可执行文件中的内容,最后将PC指向_start的地址。在CPU开始引用被映射的虚拟页的时候,内核才会将需要用到的数据从磁盘中放入内存中。
图6.2 相应的系统映像
6.5 Hello的进程执行
hello的进程在运行到sleep函数之前,一直保持顺序执行的状态。当运行到sleep函数之后,进程收到挂起信号,就会挂起当前进程切换进入其他进程。在2s后sleep函数的调用截止,当前运行的进程又收到一个恢复信号,此时再次发生上下文切换,返回hello程序继续运行,同时恢复寄存器,堆栈的状态。
图6.3 hello的进程执行
6.6 hello的异常与信号处理
1.当程序正常运行:
图6.4
2.在程序正常运行过程中键入回车,除出现空行外与正常运行情况一致
图6.5
3.在程序正常运行过程中按下Ctrl+Z,进程会收到SIGSTP信号,之后进程挂起不再运行。使用ps命令可以发现此时hello进程只是被挂起而并没有被终止和回收。
图6.6
在按下Ctrl+Z后使用jobs命令,查看当前的关键命令
图6.7
在按下Ctrl+Z后使用pstree命令,把各个进程用树状图的方式显示出来
图6.8
在按下Ctrl+Z后使用fg命令,使后台挂起的进程继续运行
图6.9
在按下Ctrl+Z后使用kill命令
图6.10
在程序正常运行过程中按下Ctrl+C,进程会收到SIGINT信号,结合ps命令可知此时进程立即被终止并回收。
图6.11
6.7本章小结
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间的重要意义。讲述了shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程、hello进程如何在内核和前端中反复跳跃运行的。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:即源程序里的地址,或源代码经过编译以后编译器生成的地址,又或者是相对于当前段的偏移地址。
物理地址:计算机系统的主存中的每一个字节所对应的唯一的地址。
虚拟地址:虚拟地址就是逻辑地址,又叫虚地址。
线性地址:所谓线性地址实际上和虚拟地址一样,是经过段机制转化之后用于描述程序分页信息的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
所谓段式管理就是直接将逻辑地址转换为物理地址,即CPU不支持分页机制的情况。其地址由段号与段内偏移地址组成。段选择符大小为16位,段描述符为8字节。GDT、LDT分别为全局描述表和局部描述表。段描述符则存放GDT或LDT中,而段首地址存放在段描述符中。
图7.1 段选择符
段选择符由3个字段组成,分别是:请求特权级RPL(Requested
Privilege Level);表指示标志TI(Table Index);索引值(Index)。TI说明段描述符的位置,而Index表示在这个描述符表中的偏移量。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是从虚拟地址变换到物理地址的主要方式。形式上来说,地址翻译过程是虚拟地址空间中的元素和物理地址空间中的元素之间的一一映射。
CPU芯片上有一个专门的硬件叫做内存管理单元(MMU),这个硬件的功能就是动态地将虚拟地址翻译成物理地址。N位的虚拟地址包含两个部分,一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE(页表条目)。接下来在对应的PTE中获得PPN(物理页号),将PPN与VPO串联起来,就得到了相应的物理地址。同时因为物理和虚拟页面都是P字节的所以物理页面偏移和VPO是相同的。具体过程如下图:
图7.2
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:即翻译后备缓冲器,它是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。在翻译地址的过程中,需要先在TLB中查询是否命中,如果命中则可直接获取物理地址。
多级页表:使用多级页表的目的是为了压缩页表。一级页表中的每个PTE负责映射到虚拟地址空间中的一个片,这里每一个片都是由1024个连续的页面组成,即二级页表。再用这些二级页表的PTE覆盖整个空间。两级页表层次结构如下图所示:
图7.3
TLB与四级页表支持下的地址翻译过程为:先将虚拟地址的VPN分为TLB标记和TLB索引。检查是否TLB命中,如果命中则直接取出物理地址,否则就把虚拟地址划分为四个VPN和一个VPO。VPN(i)对应了第i级页表的索引,通过这些索引最后对应一个固定的PPN,然后将这个PPN与VPO结合得到新的物理地址,并把这个物理地址的信息存入TLB缓存。
7.5 三级Cache支持下的物理内存访问
在此以L1 d-cache的介绍为例,L2和L3同理
L1 Cache是8路64组相联。块大小为64B。因此其CO和CI都是6位,CT是40位。根据物理地址,首先使用CI组索引,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中取出被请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
7.6 hello进程fork时的内存映射
父进程调用fork函数之后,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给子进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork函数由子进程返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个后来进行写操作时,写时复制机制就会创建新的页面。
7.7 hello进程execve时的内存映射
exceve函数加载和执行程序hello的步骤为:首先删除已存在的用户区域。然后为hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。再映射共享区域。比如hello程序与标准C库libc.so链接,这些对象都是动态链接到hello,然后存储在用户虚拟地址空间中的共享区域内。最后设置程序计数器(PC)。
7.8 缺页故障与缺页中断处理
DRAM缓存的不命中被称为缺页。DRAM缓存的不命中触发一个缺页故障,缺页故障调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果该牺牲页已经做了更改,那么内核会将它复制回磁盘,否则不会进行复制即写回,然后将牺牲页从DRAM中出去,更新该页的位置放入待取的页面。然后CPU重新执行造成缺页故障的命令此时将可以正常运行。
7.9动态存储分配管理
在程序运行时应使用动态内存分配器给引用程序分配内存,动态内存分配器的维护着一个进程的虚拟内存(堆)。分配器将堆视为一组不同大小的块的集合来进行维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留以供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
普通堆的组织结构:
图7.4
边界标记的堆的组织结构:
图7.5
显式空闲链表是更好的空闲块组织结构——即在每个块的头部后加上前驱和后继标记,将空闲块当作链表一样串接起来:
图7.6
7.10本章小结
在本章中我们了解了储存器的地址空间,学习了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程调用fork函数和execve函数时的内存映射的内容。描述了系统应对缺页异常的措施,最后还了解了malloc的内存分配管理机制。
计算机系统的内存管理的确有着丰富而复杂的内容,多种分配管理机制无不体现出设计者们的精妙构思。本章的内容使我对现代计算机系统的认识更加广泛与深入,这些前人智慧的结晶着实需要我们的敬畏。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
文件类型:
1.普通文件(regular
file):包含任意数据的文件。
2.目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件,即我们所说的文件夹。
3.套接字(socket):用来与另一个进程进行跨网络通信的文件
4.命名通道
5.符号链接
6.字符和块设备
设备管理:unix io接口
2.读取和写入文件
3.改变当前文件的位置
8.2 简述Unix
IO接口及其函数
open()函数:这个函数回打开一个已经存在的文件或者创建一个新的文件,可以添加参数只读,只写和可读可写。
close()函数:这个函数关闭一个已经打开的文件。
read()函数:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值为0表示EOF。否则返回值表示的是实际传送的字节数量。
write()函数:从内存buf位置复制至多n个字节到描述符fd的当前文件位置。
dup2()函数:复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,就会在复制oldfd之前关闭newfd。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
printf函数代码如下:
int printf(const char *fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
观察代码可以发现,printf函数中调用了vsprintf函数和write函数。
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函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数是将buf中的i个元素写到终端的函数。
所以printf函数的实现过程为:
1.从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
2.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
3.显示芯片按照刷新频率逐行读取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,读入BUFSIZE长度的字节到buf,然后返回buf的首地址。对于n =
0的情况还有特殊处理。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
在本章节中我们学习了linux系统中的的I/O设备管理机制,了解了开、关、读、写、转移文件等Unix IO接口及其函数,还简单分析了printf函数和getchar函数的实现方法以及操作过程。
虽然printf函数和getchar函数经常被使用,但是这些函数的具体实现却常常被大多数人所忽略。本章对这个方面进行的研究与探讨使我对这些“老朋友”有了新的认识。
(第8章1分)
结论
临近结束,让我们来回顾一下hello的一生:
1.预处理器将hello.c源代码经过初步的修改变成了hello.i文件。
2.编译器处理hello.i文件使之成为汇编代码并保存在hello.s文件中。
3.汇编器将hello.s文件处理成了可重定位的目标程序,也就是hello.o文件,这个时候,我们的程序离可以运行就只差一步了。
4.链接器将我们的hello.o与外部文件进行链接,终于我们得到了可以跑起来的hello文件了。
5.当我们在shell中运行hello程序时,内核会为我们分配好运行程序所需要的堆、用户栈、虚拟内存等一系列信息。使我们的hello程序能够正常的运行。
6.从外部对hello程序进行操控只需要在键盘上给一个相应的信号,hello程序就会按照我们的指令来执行。
7.在hello需要访问磁盘中的信息的时候,MMU会将程序中使用的虚拟内存地址通过页表映射成物理地址。
8.当hello执行结束,shell父进程回收子进程,内核删除为这个进程创建的所有数据结构,hello也就结束了它的一生。
在完成大作业的过程中,我对整个学期所学习的计算机系统的知识进行了系统地回顾与思考,对系统中程序的运行过程有了一个新的整体上的认识。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello1.i:预处理之后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行
hello:连接之后生成的可执行目标文件
asm.txt:
对hello.o的反汇编文件
asm1.txt:
对hello的反汇编文件
(附件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.
(参考文献0分,缺失 -1分)