本文通过对hello程序的深入研究,阐述了在Linux系统下由编写代码到程序运行的总过程。研究过程中使用了多种工具并且运用了多种方法。,最终成功实现了全部过程。对本文的阅读能够加深对计算机系统的了解
关键词:Linux;程序生成;计算机系统;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
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)P2P过程
程序员用IDE等相关工具编写hello.c程序(文本文件);在Linux操作系统里,预处理器根据以字符#开始的命令修改hello.c得到另一个C程序hello.i(文本文件);编译器将hello.i翻译成文本文件hello.s(文本文件);汇编器翻译得到可重定位目标文件hello.o;经过链接(ld)生成hello(可执行目标程序)。程序员在Shell输入./hello执行此程序,hello最后变成了系统里的一个进程。
2)020过程
在Shell处理Hello过程中,shell会fork一个子进程,并在这个子进程中调用execve加载hello。然后程序会跳转到_start地址,最终调用hello的main函数。打印完hello后程序结束。最后shell回收此进程。
1.2 环境与工具
硬件环境:CPU:AMD Ryzen 5 4600U with Radeon Graphics;RAM:16.0GB
软件环境:Windows 10 64位;VirtualBox 6.1,Ubuntu20.04.2
开发工具:VS Code; GCC 5.4.0;objdump;EDB;readelf;
1.3 中间结果
文件名 | 作用 |
hello.c | 初始的hello源文件 |
hello.i | hello.c预处理后生成的文件 |
hello.s | hello.i编译后生成的文件 |
hello.o | hello.汇编后生成的文件 |
hello | 最终链接后得到的可执行文件 |
hello.objdump | hello.o的反汇编 |
hello2.objdump | hello的反汇编 |
hello.elf | hello的elf文件 |
hello_elf.txt | hello.o的elf文件 |
1.4 本章小结
介绍了P2P、020的概念,以及实验的软、硬件环境、开发工具、中间产物。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名
作用:根据源代码中的预处理指令修改源代码
2.2在Ubuntu下预处理的命令
命令为:gcc -E hello.c -o hello.
2.3 Hello的预处理结果解析
可以看到,hello.i相较于hello.c多出了3000多行。如定义所描述的,预处理是将stido.h等头文件进行展开,多出来的部分即为头文件中的内容,并且这些展开部分是按照原.c文件中的顺序展开的。
此文件中的引用规范如下:“#”符号后面的第一个数字代表源代码中的行号,这个数字后面是来自的文件,再后面的数字是标志,不同数字有不同含义。“1”、“2”、“3”、“4”分别表示文件开始、返回文件、以下文本来自系统头文件、文本被视为包含在隐式extern“C”块中。
对整篇内容观察后可得知,这个过程中还删除了.c文件中的注释与空白字符,并且有大量对结构的定义和对外部变量的引用。
2.4 本章小结
本章主要介绍了预处理的定义、作用,并且对预处理后得到的hello.i文件进行了解析。解析过程中对预处理的详细内容做了分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包括一个汇编语言程序。这个文件的每一条语句都以文本格式描述了一条低级机器语言指令。
作用:扫描(词法分析),语法分析,语义分析,源代码优化(中间语言生成),代码生成以及目标代码优化。
3.2 在Ubuntu下编译的命令
命令为:gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据
1、字符串
hello.c中给出了字符串为:“用法: Hello 学号 姓名 秒数!\n”、“Hello %s %s\n”,它们在hello.s中指出存放在.section的.rodata中,是只读数据段。可以看出字符串以UTF-8的格式进行编码,一个汉字占三个字节。
2、int型
代码中的整型变量只有局部变量i,它并没有被初始化,只有在运行时才对它分配空间。以下分别为初始化(i=0)、循环中的条件判断(i<8)、每次循环后的处理(i++)三种操作。
3、数组
代码中的数组为字符串数组argv。处理操作如下:
首先,将数组的首元素地址存入栈中:
然后,当访问这两个字符串时,采用寄存器寻址的方法:
3.3.2 赋值
代码中涉及到的赋值操作有:“i=0”、“i++”。对应代码如下图。指令后的“l”表示处理8字节。
3.3.3 类型转换
代码中不涉及隐式的类型转换。仅有的类型转换操作为:“atoi(argv[3])”,即将argv字符串数组中的内容转换为整数。对应的处理如下。取出argv[3]中的内容,将其存入寄存器rdi中。
3.3.4 算数操作
代码中涉及的算数操作为:“i++”。如下图:
此外,还有使用leaq计算.LC1段的地址,并存入到寄存器rdi中。
3.3.5 关系操作
代码中涉及的关系操作为:“i<8”、“argc!=4”。两处操作都使用了cmpl与jx,达到判断是否跳转的作用。
3.3.6 控制转移
代码中涉及到两处控制转移。
第一处为比较argc是否等于4,等于则跳转,不等于则执行对应语句。代码如下:
第二处为循环的条件判断。I小于8则继续执行循环中的语句,否则跳出循环。这一部分的判断在hello.s文件中优化为了<=7。代码如下:
3.3.7数组、指针操作
代码中有两处数组操作,为取出argv[1]和argv[2]的内容,使用了寄存器寻址的方法。代码如下:
3.3.8 函数操作
代码中涉及的函数有:main、printf、exit、atoi、sleep、getchar。
- main
传递控制:
系统启动函数调用,call指令将下一条指令的地址压栈,然后跳转到 main函数。
传递数据:
外部调用过程向main函数传递参数argc和argv,分别使用%edi和%rsi 存储,函数正常出口为return 0,将%eax设置0返回。
分配和释放内存:
在程序的最后,,用eax返回0,leave相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret 相当pop IP,将下一条要执行指令的地址设置为dest。
2.printf
传递数据:
第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。 第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rdx为argv[1],%rsi 为argv[2]。
控制传递:
第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf 使用call printf@PLT。
3.exit
传递数据:将%edi设置为1。
控制传递:call exit@PLT。
4.atoi
传递数据:将argv[3](字符串)通过%rdi传递给atoi函数。
控制传递:通过call atoi@PLT函数,进行函数调用。
函数返回:从atoi中返回。
5.sleep
传递数据:将atoi的返回值%eax通过%rdi传递给sleep函数
控制传递:调用了sleep函数,将控制传送。
函数返回:从sleep中返回。
6.getchar
控制传递:call gethcar@PLT
3.4 本章小结
本章主要介绍了编译的定义与作用,给出了生成hello.s的指令,并对生成的文件的内容做了分析,详细阐述了多种操作的具体汇编代码实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。
作用:汇编器是将汇编代码转变成可以执行的指令,生成 目标文件。
4.2 在Ubuntu下汇编的命令
命令为:gcc -o hello.s -o hello.o
4.3 可重定位目标elf格式
使用readeld命令查看hello.o的elf格式内容,指令为:readelf -a hello.o > hello_elf.txt。这可以将hello.o的elf格式内容存储到hello_elf.txt文件中。
内容分析:
- ELF头
ELF头(ELF header)位于文件的开始位置。 它的主要目的是定位文件的其他部分。 文件头主要包含以下字段:
1)ELF文件鉴定:一个字节数组用来确认文件是否是一个ELF文件,并且提供普通文件特征的信息;
2)文件类型:确定文件类型。 这个字段描述文件是一个重定位文件,或可执行文件,或...;
3)目标结构;
4)ELF文件格式的版本;
5)程序入口地址;
6)程序头表的文件偏移;
7)节头表的文件偏移;
8)ELF头(ELF header)的大小;
9)程序头表的表项大小;
10)其他字段...
2.节
所有的数据都存储在ELF文件的节(sections)中。 通过节头表中的索引(index)来确认节(sections)。 节头表表项包含以下字段:
1)节的名字;
2)节的类型;
3)节的属性;
4)内存地址;
5)文件中的偏移;
6)节的大小;
7)到其他节的链接;
8)各种各样的信息;
9)地址对齐;
10)这个表项的大小,如果有的话;
文件中给出的节如下:
1).text节:已编译程序的机器代码以编译的机器代码。
2).rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
3).data节:已初始化的静态和全局C变量。
4).bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量,在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
5).rodata节:存放只读数据。
6).comment节:包含版本控制信息。
7).symtab:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。
8).strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
9).shstrtab节:该区域包含节的名称。
3.重定位节详细信息
重定位是将EFL文件中的未定义符号关联到有效值的处理过程。在hello.o中,对printf,exit等函数的未定义的引用和全局变量(sleepsecs)替换为该进程的虚拟地址空间中机器代码所在的地址。文件中含有两个重定位节,分别为:“.rela.text”、“.rela.eh_frame”
1).rela.text
一个.text节中位置的列表,当链接器把这个目标文件和其它文件组合是,需要修改这些位置。共含有八条重定位信息,分别为:第一个使用到的字符串、puts函数、exit函数、第二个使用到的字符串、printf函数、atoi函数、sleep函数、getchar函数。
2).rela.eh.frame
4.符号表
符号表(.symtab)是用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。
4.4 Hello.o的结果解析
输入objdump -d -r hello.o > hello.objdump,将内容存放到hello.obidump中
打开文件,可以看到内容如下。与hello.s的内容做对比可以发现有如下不同:
- 分支转移
可以看到,在hello.s中,使用了助记符(.L2等)。每当执行跳转指令时使用助记符进行跳转。而在反汇编代码中,会直接使用确定的地址进行跳转。
2.函数调用
在hello.s中,进行函数调用操作时,会直接使用函数的名称。而在反汇编代码中,在call后使用的是对应的偏移量。
3.全局变量访问
在hello.s文件中,访问.rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中使用0+%rip,因为.rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
4.5 本章小结
本章给出了汇编的介绍与作用、生成hello.s的命令、对hello.s内容的介绍以及经反汇编后得到的代码与hello.s中代码的对比,进一步了解了汇编的过程。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
作用:
链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。
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的格式
使用命令readelf -a hello > hello.elf将内容存储到hello.elf文件中
结构分析:
- ELF头
Elf头中对文件类型、程序入口地址、头起点地址、各节的大小进行修改,得到了相应的节大小。
2.节头
节头给出了各节名称、类型、地址、偏移量等信息。此处的地址是指程序运行时的虚拟地址。
3.程序头
程序头相当于一个目录,描述了各节的分布,地址的数值范围。
4.Section to Segment mapping
5.Dynamic section
如果程序已经进行了动态链接,就会有这部分内容
6.重定位节
7.符号表
8.其他信息
5.4 hello的虚拟地址空间
使用edb加载hello。通过对Data Dump窗口的观察,可以得到hello程序的虚拟内存地址为从0x401000到0x402000,这之间的每个节与与5.3节中的每一个节头表的生命相对应。
5.5 链接的重定位过程分析
输入指令objdump -d -r hello > hello2.objdump,将内容存储在hello2.objdump中。打开这个文件查看其中的内容如下。
与之前得到的hello.objdump文件内容做对比,会发现如下不同:
- 增加了新的函数代码,如puts、printf等
- 增加了新的节的代码,如.init、.plt等
2.可以发现hello2.objdump中的地址是虚拟地址,而hello.objdump中为相对偏移地址,并且跳转和函数调用都使用虚拟地址。
Hello中的重定位:
1.关联符号定义
链接器将代码中的每个符号引用和一个符号定义关联起来。此时,链接器知道输入目标模块中的代码节和数据节的确切大小。
2.合并输入模块
链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。
3.符号引用
链接器修改hello中的代码段和数据段中对每一个符号的引用,使其指向正确的运行地址。
5.6 hello的执行流程
使用edb打开hello,并查看从加载hello到_start,到call main,以及程序终止的所有过程。
对应的程序地址与名称如下:
程序名称 | 程序地址 |
_init | 0x0000000000401000 |
puts@plt | 0x0000000000401090 |
printf@plt | 0x00000000004010a0 |
getchar@plt | 0x00000000004010b0 |
atoi@plt | 0x00000000004010c0 |
exit@plt | 0x00000000004010d0 |
sleep@plt | 0x00000000004010e0 |
_start | 0x00000000004010f0 |
_dl_relocate_static_pie | 0x0000000000401120 |
main | 0x0000000000401125 |
_libc_csu_init | 0x00000000004011c0 |
_libc_csu_fini | 0x0000000000401230 |
_fini | 0x0000000000401238 |
5.7 Hello的动态链接分析
因为编译器没有办法知道函数运行时的地址,需要链接器进行连接处理。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
5.8 本章小结
本章介绍了链接的定义与作用,生成了hello的elf文件格式与objdump后的文件内容,并对hello的执行过程进行分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的代码和数据,它的栈,通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。作用:
计算机通过为用户提供一种假象:一个是独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。另一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:
它接收用户输入的命令并把它送入内核去执行。
处理流程:
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:
A. 内建的命令
B. shell函数(由用户自己定义的)
C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令
6.3 Hello的fork进程创建过程
父进程通过fork函数创建一个新的运行的子进程。新的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,为执行hello程序加载器、删除子进程现有的虚拟内存段,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
上下文信息:
上下文是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存 器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据 结构等对象的值构成。
进程时间片:
是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
调度过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。在内核调度了一个新的进程运行后,它就抢断当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换的过程:保存当前进程的上下文;恢复某个先前被抢断的进程被保存的上下文;将控制权传递给这个新恢复的进程。
用户模式与核心态转换:
进程从用户态进入核心态的唯一方法是通过中断、故障或陷入系统调用这样的异常,当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式,处理程序运行在内核模式中,当它返回到应用程序代码时,处理器把模式从内核模式改回用户模式
6.6 hello的异常与信号处理
hello执行过程中会出现共计四种异常,分别为:故障和终止。
会产生的信号有:SIGINT、SIGSTP
- 正常运行
2.按空格
3.按Ctrl-C
可以看到hello进程中断。Shell收到了SIGINT信号,回收子进程hello。
4.按Ctrl-Z
可以看到进程hello被挂起,成为后台。Shell收到了SIGTSTP信号,停止执行程序。此时可以输入ps命令,可以看到hello还在进程列表中。
再输入kill命令,最终杀死hello进程
5.乱按
6.7本章小结
本章介绍了进程的定义与作用,分析了fork进程创建与execve函数的过程,并且通过实际操作进一步解析了hello程序的执行过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,由选择符和偏移量组成。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
线性地址:
逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。Intel 80386的线性地址空间容量为4G(32根地址总线寻址)。
虚拟地址:
CPU 启动保护模式后,程序运行在虚拟地址空间中。与物理地址相 似,虚拟内存被组织为一个存放在磁盘上的N 个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。
物理地址:
真实的物理内存对应地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存单元。每个段有三个参数定义:段基地址,指定段在线性地址空间中的开始地址。段偏移量:是虚拟地址空间中段内最大可用偏移地址。段属性:指定段的特性。如该段是否可读、可写或可作为一个程序执行,段的特权级等。在此基础上,处理器有两种寻址模式:实模式与保护模式。
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
虚拟地址被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),VPO用来在页表中寻找相应的对应页表条目PTE,然后读取页表中存储的PPN物理页号,作为物理地址的PPN,然后,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。
如果PTE的有效位为1,则页命中,符合上述步骤。
如果PTE的有效位为0,则页不命中,没有缓存到物理内存,引发一个缺页异常,调入新的页并写入PTE,然后回到刚才导致缺页的程序处重新调用。
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。
为了消除每次 CPU 产生一个虚拟地址,MMU 就查阅一个PTE带来的时间开销,许多系统都在MMU中包括了一个关于 PTE 的小的缓存,称为翻译后被缓冲器(TLB),TLB的速度快于L1 cache。
TLB通过虚拟地址VPN部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU在读取PTE时会直接通过TLB,如果不命中再从内存中将PTE复制到TLB。同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩 页表大小。core i7使用的是四级页表。
在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。
7.5 三级Cache支持下的物理内存访问
L1、L2、L3各级Cache的原理相同,只做L1 Cache的分析。L1 Cache是8路64组相连高速缓存。块大小64B。因为有64组,所以需要6 bit CI进行组寻址,共有8路,块大小为64B,所以需要6 bit CO表示数据偏移位置,因为VA共52 bit,所以CT共40 bit。
在上一步中已经获得了物理地址VA,使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)。如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
因为L1高速缓存是直接映射高速缓存,所以每一组只有一行。在寻找对应字节时,首先根据组索引确定要访问的组,然后看有效位是否为真,为0则缓存不命中。如果有效位为1,再看缓存中的标记位是否与物理地址给出的标记位匹配。
如果没有匹配成功或者匹配成功但是标志位是0,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下几个步骤:
1)删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2)映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3)映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障
当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的 物理地址不在内存中,因此必须从磁盘中取出时就会发生故障。
缺页中断处理
选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去, 换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页 的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向更高的地址。分配器将堆视为一组不同大小的块的集合来维护。
隐式空闲链表
头部一共四个字节,前三个字节存储的是块的大小,最后一个字节存储的是当前这个块是空闲块还是已分配的块,0代表空闲块,1代表已分配的块。中间的有效载荷就是用于存放已分配的块中的信息用的。最后的填充部分是为了地址对齐等一些要求用的。
隐式链表的结构就是根据地址从小到大进行连接的,如图7-11所示。其中的每一个元素表示的是一个空闲块或者一个分配块,由于空闲块会合并的特性,链表中的元素的连接一定是空闲块的分配块交替连接的。
显式空闲链表
显示结构在空闲块中增加了8个字节,分别保存当前空闲块的前驱空闲块的地址和后继空闲块的地址。显式的结构比隐式结构多维护了一个链表,就是空闲块的链表。这样做的好处就是在malloc的时候,隐式的方法是要遍历所有的块,包括空闲块了分配块。但是显式的结构只需要在空闲块中维护的链表检索就可以了,这样降低了在malloc时候的复杂度。
关于空闲块的维护方式一共有两种,一种是后进先出的方式,另一种是按照地址的方式。按照地址维护很好理解,与隐式的结构大致相同。后进先出的方式的思想是,当一个分配块被free之后,将这个块放到链表的最开头,这样在malloc的时候会首先看一下最后被free的块是否符合要求。这样的好处是释放一个块的时候比较高效,直接放在头部就可以。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址到物理地址的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件,包括普通文件(包含任意数据的文件)、目录(文件夹,包含一组链接的文件,每个链接都将一个文件名映射到一个文件)、套接字(用来与另一个进程进行跨网络通信的文件)、命名通道、符号链接以及字符和块设备。
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O设备管理方法。
8.2 简述Unix IO接口及其函数
1、打开文件
应用程序通过要求内核打开相应的文件。
2、shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3、改变文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个k是 从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当 前文件位置。
4、读/写文件
读操作是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将 k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。
写操作是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5、关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述 符池中。
8.3 printf的实现分析
printf函数代码如下:
vsprintf函数代码如下:
printf函数接受一个格式化命令,并按指定的匹配的参数格式化输出。vsprintf函数按照格式fmt 结合参数args 生成格式化之后的字符串,并返回字串的长度。write函数中,先给寄存器传了几个参数,然后通过系统调用sys_call。sys_call函数将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数代码如下:
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。
8.5本章小结
本章介绍了Unix I/O设备管理机制及方法,Unix I/O接口及函数。同时分析了printf和getchar函数,以理解其实现。
(第8章1分)
结论
一个看似简单的hello程序其实要比我们想象的要复杂的多。
具体步骤如下:
1.由程序员编写hello.c的代码。
2.预处理:预处理器扩展源代码插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏,合并为一个hello.i文件。
3.编译:编译器将hello.i编译成hello.s文件,包含汇编代码。
4.汇编:汇编器将hello.s可重定位为hello.o文件。
5.链接:链接器将hello.o与实现库函数的代码合并,产生最终的可执行代码文件。
6.带参数运行:在命令行输入:./hello 1190201715 叶扬帆 1
7.创建子进程:调用fork创建一个子进程。
8.运行:调用execve,execve调用启动加载器,加映射虚拟内存,进入hello程序入口后将程序载入物理内存,进入main函数执行hello。
9.执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行相应的控制逻辑流。
10.访问内存:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行相应的控制逻辑流。
11.申请内存:将虚拟内存地址通过页表映射成物理地址。
12.发出/接收信号:如果运行中键入Ctrl + C或Ctrl + Z,则调用shell的信号处理函数分别停止、挂起。
13.结束进程:exit,hello的父进程回收hello,内核也删除它的所有数据。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
hello.c | 初始的hello源文件 |
hello.i | hello.c预处理后生成的文件 |
hello.s | hello.i编译后生成的文件 |
hello.o | hello.汇编后生成的文件 |
hello | 最终链接后得到的可执行文件 |
hello.objdump | hello.o的反汇编 |
hello2.objdump | hello的反汇编 |
hello.elf | hello的elf文件 |
hello_elf.txt | hello.o的elf文件 |
(附件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分)