计算机系统
大作业
计算机科学与技术学院
2023年5月
本大作业从hello.c开始介绍,完整地介绍了一个程序的整个生命周期。包含了预处理、编译、汇编、链接、进程管理、存储管理等内容。是对计算机系统的很好的总结。
关键词:计算机系统;hello程序;Linux;链接
目 录
第1章 概述
(0.5分)
1.1 Hello简介
P2P过程:
首先,程序员编写代码,形成hello.c文件。
然后,驱动程序运行C预处理器,它将C的源程序hello.c翻译成一个ASCII码的中间文件hello.i。
接下来,驱动程序运行C编译器,它将hello.i翻译成一个ASCII汇编语言文件hello.s。
然后,驱动程序运行汇编器,它将hello.s翻译成一个可重定位目标文件hello.o。
之后,驱动程序运行链接器程序ld,将hello.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件hello。
在运行hello程序时,首先在shell中输入./hello启动程序,之后shell调用fork函数来创建一个新的子进程,通过execve函数调用启动加载器,使用mmap函数创建新的内存区域。
O2O过程:
shell调用execve函数加载和执行hello程序,CPU为运行的hello分配时间片以执行逻辑控制流。同时计算机通过TLB、4级页表、3级Cache,Pagefile等加速hello程序的运行。程序运行结束后,shell回收hello进程,内核删除相关痕迹。
1.2 环境与工具
硬件环境:
CPU:AMD Ryzen 5 5600H 内存:16GB 硬盘:Samsung SSD 980 1TB
软件环境:
Windows10 64位;Vmware 17;Ubuntu 20.04 64位
开发与调试工具:
Microsoft Visual Studio Community 2022 (64 位) - Current
版本 17.1.6
CodeBlocks 64位;vi/vim/gedit+gcc;edb1.3.0
CLion 2022.2.3
1.3 中间结果
中间结果文件 | 作用 |
hello.i | hello.c预处理得到的文本文件 |
hello.s | hello.i编译后的汇编文件 |
hello.o | hello.s汇编得到的可重定位目标文件 |
hello | 链接得到的可执行目标文件 |
1.txt | hello.o的反汇编代码,分析hello.o |
2.txt | hello的反汇编代码,分析重定位过程 |
hello.elf | hello的ELF格式,分析重定位过程 |
hello1.elf | hello.o的ELF格式,分析链接过程 |
表一
1.4 本章小结
本章介绍了hello的P2P、O2O过程,列出了所需的软硬件环境和开发与调试工具。列出了生成的中间结果文件的名字并给出了这些文件的作用。
第2章 预处理
(0.5分)
2.1 预处理的概念与作用
预处理的概念:
预处理是在编译之前对源文件提前进行的处理。预处理过程扫描源代码,对其进行初步转换,产生新的源代码提供给编译器,预处理过程先于编译器对源代码进行处理。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
预处理的作用:
1.将源文件中以“include”格式包含的文件复制到编译的源文件中。
2.用实际值替换用“#define”定义的字符串。
3.删除所有的注释“//”和“/* */”。
4.根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
命令为:>cpp hello.c hello.i
运行截图如图一:
图一
2.3 Hello的预处理结果解析
对比图二(预处理前)和图三(预处理后文件末尾),一个明显的变化就是代码行数变多了。hello.c只有23行代码,而hello.i有3060行代码。多出的代码主要来源是对原文件中的宏进行了宏展开,头文件中的内容被加入此文件中。观察发现,原来的代码在hello.i的末段。
图二
图三
2.4 本章小结
本章主要介绍了linux环境下有关预处理的内容,介绍了预处理的概念和作用,
演示了在Ubuntu下预处理的命令,分析了Hello的预处理结果。
第3章 编译
(2分)
3.1 编译的概念与作用
编译的概念:
编译的过程是进行词法分析、语法分析、语义分析以及优化后产生相应的汇编代码文件。整个编译过程,就是将高级语言翻译为机器语言的过程:通常将其分为5步,包括词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
编译的作用:
把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编语言或机器语言书写的目标程序。
3.2 在Ubuntu下编译的命令
命令为:gcc -S hello.i -o hello.s
运行截图为图四:
图四
3.3 Hello的编译结果解析
3.3.1数据
argc:传入的参数,位置在栈中,位于%rbp-20。
局部变量:局部变量运行时被保存在栈或是寄存器里,作为函数中的局部变量i被储存在栈中,栈地址:%rbp-4
立即数:储存在.data段中。
数组操作:argv[]:传入的数组,位置在栈中,argv的地址位于%rbp-32,利用argv的地址加i*8,就能得到argv[i]。
表达式:存在代码段的.rodata中
3.3.2赋值
把局部变量i赋值为0的操作如下:
.L2:
movl $0, -4(%rbp)
3.3.3类型转换
调用atoi函数将字符型argv[3]转换为整型数:
call atoi@PLT
3.3.4算数操作
编译器将i++编译为:
addl $1, -4(%rbp)
3.3.5关系操作
编译器将i<8与跳转编译为:
.L3:
cmpl $7, -4(%rbp)
jle .L4
编译器将argc!=4编译为:
cmpl $4, -20(%rbp)
je .L2
3.3.6控制转移
编译器将if(argc!=4)编译为:
cmpl $4, -20(%rbp)
je .L2
编译器将for循环里的控制转移编译为:
cmpl $7, -4(%rbp)
jle .L4
3.3.7函数操作
编译器将printf函数调用编译为:
call printf@PLT
编译器将atoi函数调用编译为:
call atoi@PLT
3.4 本章小结
本章介绍了编译的概念与作用、编译时需要输入的命令行。通过对比源程序和汇编语言程序,简要说明了编译器如何处理 C 语言的各种数据类型和操作,包括数据分配、类型转换、赋值、算术操作、关系操作、数组操作、控制转移和函数操作等。
第4章 汇编
(2分)
4.1 汇编的概念与作用
汇编的概念:
汇编是指从 .s 到 .o 即将汇编语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
汇编的作用:
汇编将汇编语言转化为机器可直接识别执行的代码文件。
4.2 在Ubuntu下汇编的命令
汇编的指令为:gcc -c hello.s -o hello.o
汇编结果如图五:
图五
4.3 可重定位目标elf格式
readelf命令:readelf -a hello.o > hello.elf
运行截图见图六:
图六
4.3.1ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table )的文件偏移,以及节头部表中条目的大小和数量。在hello.elf中ELF头如图七:
图七
4.3.2节头部表
不同节的位置和大小是由节头部表描述的。hello.elf中节头部表如图八所示。
图八
4.3.3符号表
符号表存放在程序中定义和引用的函数和全局变量的信息。hello.elf中符号表如图九所示。
图九
4.3.4重定位节
重定位节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件
组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的
指令都需要修改,另一方面,调用本地函数的指令不需修改。可执行目标文件中并不需要重定位信息。hello.elf中重定位节如图十所示。
图十
4.4 Hello.o的结果解析
生成反汇编文件的命令:objdump -d -r hello.o >1.txt
运行后生成了1.txt文件,如图十一、图十二:
图十一
图十二
与第3章的hello.s对照分析可发现以下不同:在操作数进制方面,hello.s运用十进制,而反汇编的运用十六进制。在函数调用方面,hello.s中的函数调用是call函数名,而反汇编的是call重定位条目指引的相对地址。在跳转分支方面,hello.s的分支跳转是通过跳转到段名称的形式来表示,而反汇编的代码使用跳转到主函数+段内偏移量(相对位置)来表示。
4.5 本章小结
本章介绍了汇编的概念与作用,结合hello.s、hello.o与反汇编产生的文件,分析了它们之间的不同。还查看了hello.o的elf格式,分析了可重定位文件ELF的结构和各个组成部分,以及它们的内容和功能。
第5章 链接
(1分)
5.1 链接的概念与作用
链接的概念:
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。链接是由叫链接器(linker)的程序自动执行的。
链接的作用:
链接把程序的各个部分联合成一个文件。
5.2 在Ubuntu下链接的命令
链接命令为:>ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello
链接的结果,产生了hello可执行目标文件,如图十三:
图十三
5.3 可执行目标文件hello的格式
生成hello的命令:readelf -a hello > hello1.elf
运行结果如图十四:
图十四
5.3.1ELF头
hello与hello.o的ELF头大致相同,不同之处在于hello的类型为EXEC可执行文件,表明hello是一个可执行目标文件,有27个节头。具体见图十五。
图十五
5.3.2节头
节头是描述目标文件的节,各节的基本信息均在其中进行了声明,包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息等。共有29个节。具体信息见图十六。
图十六
5.3.3程序头表
程序头表见图十七。
图十七
5.3.4重定位节
重定位节具体信息见图十八。
图十八
5.3.5动态节
动态节具体信息见图十九。
图十九
5.3.6符号表
符号表具体信息见图二十。
图二十
5.4 hello的虚拟地址空间
使用edb加载hello的命令:./edb
使用edb打开hello从DataDump窗口观察hello加载到虚拟地址的情况,查
看各段信息。如图二十一。
图二十一
比如.dynsym节,在hello1.elf中是这样的:
图二十二
根据虚拟地址,可以在edb中找到对应的信息:
图二十三
再如.text节
图二十四
根据虚拟地址,可以在edb中找到对应的信息:
图二十五
5.5 链接的重定位过程分析
不同之处:hello相对hello.o而言,多了很多函数,如图二十六中的.init和.plt等。
图二十六
图二十七
在地址方面,hello.o为相对偏移地址,hello为虚拟内存地址。如下两张图所示。
图二十八
图二十九
并且hello含有外部链接得到的函数。
重定位由两步组成:
(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同
一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模
块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全
局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
重定位过程地址计算方法如下:
foreach section s t
foreach relocation entry r {
refptr = s + r.offset; /* ptr to reference to be relocated */
/* Relocate a PC-relative reference */
if (r.type = R _X86_ 64_ _PC32) {
refaddr = ADDR(s) + r.offset; /* ref's run-time address */
*refptr = (unsigned) (ADDR(r .symbol) + r.addend - refaddr) ;
}
/* Relocate an absolute reference */
if (r.type = R X86_ 64. 32)
*refptr = (unsigned) (ADDR(r . symbol) + r. addend) ;
}
5.6 hello的执行流程
开始时调用:_ start,执行时调用:_ main、printf、exit、sleep、
getchar,退出时调用: _exit。
调用与跳转的各个子程序名或程序地址见表二:
子程序名 | 程序地址 |
_start | 00000000004010f0 |
puts@plt | 0000000000401090 |
printf@plt | 00000000004010a0 |
getchar@plt | 00000000004010b0 |
atoi@plt | 00000000004010c0 |
exit@plt | 00000000004010d0 |
sleep@plt | 00000000004010e0 |
_main | 00000000004011d6 |
表二
5.7 Hello的动态链接分析
GOT表的起始位置为0x404000。GOT表位置在调用dl_ init 之前0x404008后的16个字节均为0,如图三十所示。
图三十
调用dl_ init 之后如图三十一所示。
图三十一
由此可知,.got.plt条目发生了变化。
5.8 本章小结
本章介绍了链接的概念和作用,给出了在Ubuntu下链接的命令,分析了可执行目标文件hello的格式和hello的虚拟地址空间。介绍了hello链接的重定位过程和动态链接过程。
第6章 hello进程管理
(1分)
6.1 进程的概念与作用
进程的概念:进程是一个正在运行的程序或命令的实例。系统中的每个程序都在某个进程的上下文中运行。上下文由程序正确运行所必需的状态组成。这种状态包括存储在内存中的代码和程序数据、堆栈、通用寄存器的内容、程序计数器、环境变量和文件描述符的集合。
进程的功能:进程提供给应用程序两种关键抽象,一种是独立的逻辑控制流,它提供一个假像,好像程序独占地使用处理器;一种是私有地址空间,它提供一个假象,好像程序独占地使用内存系统。这样做可以提高CPU执行效率,减少因程序等待造成的CPU空闲和其他计算机软硬件资源的浪费。
6.2 简述壳Shell-bash的作用与处理流程
Shell是运行在终端中的文本互动程序,bash(GNU Bourne-Again Shell)是最常用的一种shell。是当前大多数Linux发行版的默认Shell。
Shell相当于是一个翻译,把我们在计算机上的操作或我们的命令,翻译为计算机可识别的二进制命令,传递给内核,以便调用计算机硬件执行相关的操作;同时,计算机执行完命令后,再通过Shell翻译成自然语言,呈现在我们面前。
Shell的处理过程一般是这样的:首先从终端读入输入的命令,并将输入字符串分割获取参数,如果是内置命令则立即执行,如果不是内置命令则调用对应的程序并运行。Shell还可以接受键盘输入的信号比如ctrl+c,ctrl+z等,并对这些信号进行处理。
6.3 Hello的fork进程创建过程
在shell中输入执行hello的命令后,父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork函数是有趣的(也常常令人迷惑),因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的PID。在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello, 且带参数列表argv和环境变量列表envp。与fork一次调用返回两次不同,execve调用一次并从不返回。
execve函数会先删除当前进程虚拟地址的用户部分中的已存在的区域结构。
之后为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello.out文件中的.text和.data 区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello.out中。栈
和堆区域也是请求二进制零的,初始长度为零。之后映射共享区域。.最后设置程序计数器(PC),使之指向代码区域的入口点。Linux 将根据需要换入代码和数据页面。
6.5 Hello的进程执行
操作系统提供的抽象有:
(1)逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计
数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
(2)用户模式和内核模式。处理器通常使用某个控制寄存器中的一个模式位来
提供这种功能。该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。
(3)上下文切换。操作系统内核使用一种称为上下文切换的较高层形式的异常
控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新
启动一个被抢占的进程所需状态。它由一些对象的值组成,包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
(4)时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因
此,多任务也叫作时间分片。
hello程序执行过程中,在进程调用exeeve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf 函数输出信息,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
异常可分为四类:中断、陷阱、故障和终止。异常的属性见表三。
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
表三
6.7本章小结
本章介绍了进程的概念和作用,简要介绍了壳的作用的处理流程,回顾了fork函数和execve函数的运行过程。最后分析了在hello执行过程中会出现不同异常的种类以及异常与信号的处理。
第7章 hello的存储管理
( 2分)
7.1 hello的存储器地址空间
1.逻辑地址
逻辑地址是程序内部使用的地址,也就是段内偏移量。编译器编译程序时,会为程序生成代码段和数据段,然后将所有代码放到代码段中,将所有数据放到数据段中。最后程序中的每句代码和每条数据都会有自己的逻辑地址。
2.线性地址
线性地址是逻辑地址到物理地址转换的中间层。程序代码经编译后会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。若启用了分页机制,则线性地址会再此转换产生一个物理地址。若没有启用分页机制,则线性地址就是物理地址。
3.虚拟地址
有时也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
4.物理地址
物理地址是内存中的一个位置。它是出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终地址。物理地址可以由32位或者64位无符号整数表示,地址引脚的数量决定了物理地址的寻址范围。从地址的取值符合线性关系来看,物理地址也算是一种线性地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理具体内容如下:
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后面3位包含一些硬件细节。索引号指向“段描述符”,段描述符具体描述了一个段。这样,多个段描述符就组成了一个数组,称为“段描述符表”。因此,可以通过段标识符的前13位,在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。
给定一个逻辑地址[段选择符:段内偏移地址],转换过程如下:
首先根据段选择符判断当前要转换的是GDT中的段还是LDT中的段,再根据相应寄存器得到其地址和大小。这样就得到了一个数组。拿出段选择符中前13位,在这个数组中查找到对应的段描述符,这样就知道了Base,即基地址。Base + offset就是要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:TLB是利用VPN的位进行虚拟寻址的。因为TLB有4个组,所以VPN 的低2位就作为组索引(TLBI)。VPN中剩下的高6位作为标记(TLBT), 用来区别
可能映射到同一个TLB组的不同的VPN
页表:这个页表是一个单级设计,一共有28 =256个页表条目(PTE)。然而,我们只对这些条目中的开头 16 个感兴趣。为了方便,我们用索引它的VPN来标识每个PTE; 但是要记住这些VPN并不是页表的一部分,也不储存在内存中。另外,注意每个无效 PTE 的 PPN 都用一个破折号来表示,以加强一个概念:无论刚好这里存储的是什么位值,都是没有任何意义的。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。
7.5 三级Cache支持下的物理内存访问
高速缓存:直接映射的缓存是通过物理地址中的字段来寻址的。因为每个块都是4字节,所以物理地址的低2位作为块偏移(CO)。因为有16组,所以接下来的4位就用来表示组索引(CI)。剩下的6位作为标记(CT)。
具体过程如下:
CPU发送一条虚拟地址,随后 MMU按照上述操作获得了物理地址 PA。根据cache大小组数的要求,将 PA分为 CT,CS,CO。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及 CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给 CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。
7.6 hello进程fork时的内存映射
当 fork 函数被hello进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件hello.out中的程序,加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text 和.data 区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。3.映射共享区域, hello程序与共享对象 libc.so 链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault)。图四十三展示了在缺页之前我们的示例页表的状态。CPU引用了VP 3中的一个字,VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果 VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。
图四十三
接下来,内核从磁盘复制VP 3到内存中的PP 3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。图四十四展示了在缺页之后我们的示例页表的状态。
图四十四
7.9本章小结
本章主要学习了hello进程的存储器空间地址,简要介绍了intel的段式管理和页式管理、TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问。结合hello进程分析了fork与execve的内存映射。最后介绍了缺页故障与缺页中断处理。
结论
(0分,必要项,如缺失扣1分,根据内容酌情加分)
hello经历的过程如下:
1.我们使用C语言,编写出了C程序hello.c。
2.hello.c经过预处理器预处理,扩展得到hello.i文本文件。
3.hello.i经过编译器编译,得到hello.s汇编文件。
4.hello.s经过汇编器汇编,得到可重定位目标文件hello.o。
5.hello.o与可重定位目标文件、动态链接库,经链接器ld链接生成可执行文件hello。
6.shell调用fork函数来创建一个新的子进程,通过execve函数调用启动加载器,使用mmap函数创建新的内存区域。
7.程序运行结束后,shell回收hello进程,内核删除相关痕迹。
感悟:
这门课到此就彻底结束了,这次大作业的形式也很有意思。我见证了一个程序的诞生、成长、衰老与消失。感觉课时压缩还是有些可惜的,以后还需要把后面的程序间的交互与通信再自学一下,感觉后面的三章:系统级I/O、网络编程和并发编程都值得再好好学习。
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件 | 作用 |
hello.i | hello.c预处理得到的文本文件 |
hello.s | hello.i编译后的汇编文件 |
hello.o | hello.s汇编得到的可重定位目标文件 |
hello | 链接得到的可执行目标文件 |
1.txt | hello.o的反汇编代码,分析hello.o |
2.txt | hello的反汇编代码,分析重定位过程 |
hello.elf | hello的ELF格式,分析重定位过程 |
hello1.elf | hello.o的ELF格式,分析链接过程 |
表四
参考文献
[1] Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018-1-737