计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数据科学与大数据
学 号 2022113332
班 级 2203501
学 生 朱达
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
本论文跟踪并分析了一个hello.c源文件被创建、预处理、编译、汇编、链接为可执行程序,再到程序被执行而创建为进程、进程在操作系统上运行、最终被杀死的过程,研究了计算机为了编译并运行hello.c在操作系统、处理器、内存、I/O设备等层面进行交互的过程。
关键词:计算机系统;P2P;020;编译系统;异常控制流。
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
-
- Hello简介
- P2P (Program to Process,从程序到进程)
- Hello简介
P2P指Hello.c从源程序到进程的过程。
Hello.c经过预处理器的编译预处理,得到预编译文件Hello.i;Hello.i又经过编译转换为目标文件Hello.o;Hello.o又经过汇编器翻译为机器语言命令汇编文件Hello.s;最后经过链接器链接得到可执行文件Hello;Hello经过运行产生进程。
图1-1,编译系统
-
-
- 020 (zero to zero,从0到0)
-
020指可执行文件Hello产生的进程从进入内存到从内存被回收的过程。
子进程由父进程fork()产生成为父进程的副本,随着execve函数的执行,可执行目标文件hello被加载并运行,新程序由此开始;随着进程的进行,进程最终由于某种原因终止而变为僵尸进程,最后又父进程或者init进程回收,至此进程最终消失,即“从0到0”。
1.2 环境与工具
1.2.1 硬件环境
x64 CPU;1.60GHz;16G RAM;1.5THD Disk。
1.2.2 软件环境
Windows11 64位
1.2.3开发工具
VMware Workstation 17; Ubuntu 22.04 LTS Desktop;
Gcc; gdb; VScode remote;
1.3 中间结果
hello 的C源文件 | |
hello.i | hello.c经过预处理后的预编译文本文件 |
hello1.i | hello.c经过加上-P选项的预处理屏蔽垃圾内容后的预编译文本文件 |
hello.s | hello.i经过编译得到的汇编语言文本文件 |
hello.o | hello.s经过汇编得到的机器语言二进制文件(可重定位目标文件) |
hello | hello.o经过链接得到的机器语言二进制文件(可执行目标文件) |
hello_objdump.txt | hello经过反汇编得到的机器语言与对应反汇编语言的文本文件 |
gdb.txt | gdb中用于在hello每个函数上加断点的辅助文件 |
breakpoints.txt | 记录gdb中所有断点 |
1.4 本章小结
本章在总体上介绍了hello程序的一生,主要从p2p和020过程开始总体上分析hello程序的一生。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor)对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
预处理作用:
1. 进行宏替换,例如替换掉#define所定义的宏。
2. 处理文件包含,将#include中包含的文件的内容插入到程序文本中
3. 去掉C语言源程序中所有的注释。
4. 按照宏定义,使用#if 、#elif 、#else等,进行有选择的操作。
5. 添加行号信息则是方便程序员进行调试操作
2.2在Ubuntu下预处理的命令
对示例.c文件进行预处理:使用gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
预处理后,hello.c被扩展了许多行,其中一部分描述了运行库在计算机内的位置,一部分对可能用到的函数进行了申明。解析了文件的引用、定义了大量的数据别名。hello.c开始引用了三个头文件stdio.h,unistd.h,和stdlib.h,预处理器将他们替换为系统文件中的内容
2.4 本章小结
预处理是编译系统进行的第一步程序:首先进行条件编译;若无问题则将源文件进行修改,替换掉其中的目标头文件和定义的宏。预处理提供了编译前处理的手段,和利用外部系统或其他头文件。
本章通过对于hello.c进行预处理,获得hello.i文件,分析了预处理后的文件的内容以及其作用,对结果进行了相应的解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译是指将高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序的过程。即编译器将文件hello.i翻译成汇编语言程序hello.s的过程。
编译的作用:编译将高级计算机语言所写作的源代码程序翻译为汇编语言程序,在此过程中,会进行以扫描、源代码优化、词法分析、语法分析、语义分析来生成汇编语言程序。
3.2 在Ubuntu下编译的命令
使用编译命令gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据
3.3.1.1 字符串
这对应hello.c里printf对应的两个字符串,一个常量一个格式化字符串
3.3.1.2 整数
比较过程的立即数5:
整型变量i:(局部变量),可以观察到这是存储在栈上的,每次调用通过栈指针访问(图中高亮区域):
3.3.1.3 数组
argv[]
main 函数中访问数组元素argv[1],argv[2]时,按照起始地址 argv 大小 8字节计算数据地址取数据,hello.s 中,引用两次(%rax)取出其值。
3.3.2 运算
3.3.2.1算术操作:
i++,如下图所示:
3.3.2.2关系操作:
argc!=5
i<10
3.3.2.3数组操作:
汇编代码中数组通过首地址加上偏移量取argv中的字符串的地址,argv数组中的内容存储在了栈中,可以取出对应的字符串的地址,并分别放到%rsi和%rdx中。
3.3.2.4控制转移:
if:
编译器使用跳转指令实现对于 if 判断,首先比较 argv 和5,用减法实现并设置条件码,使用 je 判断 ZF 标志位,如果为0,说明argv==5,则不执行if 中的代码直接跳转到.L2顺序执行下一条语句,否则执行 if 中的代码。
for:
通过计数变量 i 和跳转语句实现循环10次。首先使用 cmpl 进行比较,如果 i<9,则跳入.L4 for 循环体执行,否则说明循环结束,执行 for 之后的代码。
3.3.3函数操作:
main函数的参数argc和argv分别存储在%edi,%rsi中,并在一开始首先分别保存到了-20(%rbp),-32(%rbp)的位置。
下列为调用函数:
printf:调用printf函数进行打印操作
atoi:调用atoi函数进行类型转化
put:调用put函数进行输出
exit:调用exit函数进行程序终止
getchar:调用getchar函数进行读取字符
sleep:调用sleep函数进行延迟
图中高光即是汇编调用出处(部分在屏幕外):
函数返回:一般函数返回前会恢复被调用者保存的寄存器的值、恢复旧的帧指针%rbp,最后跳转到原来的控制流的地址。通常以ret指令结尾。
3.4 本章小结
本章是对汇编代码的深入理解,主要学习了数据操作、算数运算、逻辑运算、流程控制、过程调用等等知识,并对结果进行了相应的解析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编,指汇编器(Assembler)把汇编语言翻译成机器语言的过程中,将汇编语言文本文件转变为二进制可重定位目标文件。可重定位文件可以经过重定位和链接与其他可重定位文件合并,创建可执行目标文件。
汇编过程的作用包括:
1.将高级语言转换为低级语言:汇编过程将 C 语言代码转换为汇编语言代码,以便与特定的机器架构对应。汇编语言使用助记符代替机器指令,更接近底层硬件。
2.生成目标文件:编译器将汇编语言代码转换为目标文件。目标文件包含了计算机可以直接执行的机器指令,但它还没有经过最终的链接过程。
3.转换和优化:在汇编过程中,编译器可以对代码进行优化,消除冗余操作、简化表达式、调整指令顺序等,以提高代码的性能和效率。
4.处理模块化和库链接:在汇编过程中,编译器将不同的源码文件编译成独立的目标文件,并在必要时处理模块化和库链接,以生成最终的可执行文件。
5.生成调试信息:汇编过程还可以生成调试信息,这些信息用于调试和跟踪程序的执行。调试信息包括符号表、行号信息和源代码映射等,使得在调试器中可以准确地定位代码的位置和变量的值。
总之,C 语言编译过程中的汇编过程将 C 代码转换为汇编语言代码,并进行优化和转换,以生成目标文件。这个阶段是将高级语言转化为低级语言,为最终的机器码生成和可执行文件生成做准备。
4.2 在Ubuntu下汇编的命令
使用命令gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
hello.o的ELF格式:
4.3.1 ELF头
使用readelf -h hello.o 查看ELF头如图:
按照格式,我们逐行分析ELF内容:
ELF 头: 指名ELF文件头开始。 |
Magic用来指名该文件是一个 ELF 目标文件。第一个字节7f是个固定的数;后面的 3 个字节正是 E, L, F 三个字母的 ASCII 形式。 |
表示文件类型,ELF64指这个文件是64位的ELF格式。 |
表示文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。 |
当前 ELF 文件头版本号,这里版本号为1 。 |
OS/ABI,指出操作系统类型,ABI是Application Binary Interface 的缩写。 |
ABI 版本号,当前为0 。 |
表示文件类型。ELF 文件有 3 种类型,一种是如上所示的Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library) 。 |
机器平台类型。 |
当前目标文件的版本号。 |
程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零。 |
程序头起点位置,与 11 行同理,这个目标文件没有程序头。 |
节头开始处,这里 1240 是十进制。 |
是一个与处理器相关联的标志,x86 平台上该处为0。 |
ELF 文件头的字节数。 |
程序头大小,因为这个不是可执行程序,故此处大小为0。 |
程序头数量,同理于第 16 行。 |
节头的大小,这里每个节头大小为64个字节。 |
一共有多少个节头,这里是14个,与Section Headers中的数量一致。 |
节头字符串表索引号。 |
4.3.2 节头部表
使用 readelf -S(-W) hello.o,查看节头部表
文件的节头部表描述了目标文件中的不同节的类型、地址、大小、偏移等信息,以及可以对各部分进行的操作权限。对象文件中的重定位条目,会构成一个个单独的节。重定位条目所构成的节(如.rela.text)需要和另外两个节产生关联:符号表节以及受影响地址单元所在的节。
4.3.3 符号表
使用readelf -s hello.o查看符号表
符号表存放在程序中定义和引用的函数和全局变量的信息,表示了重定位的所有符号,用于之后的符号解析和重定位。
4.3.4 .rela.XXX
使用 readelf -r hello.o查看重定位信息
我们可以看到.rela.xxx中的重定位信息和.symtab中的信息对应,但.rela.xxx的来源不只有一个节。重定位节中包含了.text 节中需要进行重定位的信息,我们可以发现需要重定位的函数有: .rodata, puts, exits, printf, atoi, sleep, getchar。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编
对照:
1. .s汇编代码会有一些不知意义的伪代码,标识一些信息。
2.分支转移:.s汇编代码是跳到.L2位置,这个是段名称,而反汇编代码跳转指令的操作数使用的不是段名称。
3.全局变量访问:.s文件中访问时用段名加%rip,反汇编时用0+%rip。
4.函数调用:在.s 汇编代码中,函数调用之后直接跟着函数名称,比如call exit,而在反汇编程序中,hello.c 中调用的函数是共享库中的函数
4.5 本章小结
汇编过程中汇编器将汇编代码翻译为机器指令,生成可重定位目标文件,但该文件仍然不能运行,其中可能有外部声明的变量或函数,需要与其他文件一起生成可执行的文件。可重定位目标文件按照ELF格式组织,方便我们后续与其他可重定位目标文件进行链接,生成可执行目标文件。
(第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 /usr/lib/x86_64-linux-gnu/crtn.o /usr/lib/x86_64-linux-gnu/libc.so hello.o
5.3 可执行目标文件hello的格式
分析hello的ELF格式
5.3.1 ELF头
hello的elf格式和hello.o的elf格式相类似。其中可以很明显的发现在Type一栏变成了EXEC,即可执行的。
第11行,入口点地址被设置为0x4010f0 |
第12行,程序头起点,设置为64 |
第13行,节头开始处,设置为14208 |
第15行,ELF头文件字节数设置为64 |
第16行,程序头大小设置为56 |
第17行,程序头数量被改为12 |
第18行,节头的大小,这里每个节头大小为64个字 |
第19行,节头增加了13个 |
第20行,节头字符串表索引号,同样增加了13个 |
5.3.2 程序头表
使用readelf -l hello查看程序头表(最下边一部分在屏幕外)
可执行文件的连续的片(chunk)被映射到连续的内存段(segment),程序头部表描述了这种映射关系:
(1)上边的程序头描述了段的目标文件中的偏移(Offset),内存地址(VA/PA),目标文件中的段大小(filesiz),内存中的段大小(memsiz),访问权限(Flags)(R:可读/W:可写/E:可执行),对齐要求(Align)。
(2)下边的段节标注了对应索引号的段所映射的节。
索引号为02的程序头LOAD,对应映射的节有:
.interp , .note.gnu.property , .note.ABI.tag , .hash , .gnu.hash , .dynsym , .dynstr , .gnu.version_r , .rela.dyn , .rela.plt 。
5.3.3 节头部表
使用 readelf -S hello 查看节头部表
显然增加了一些动态链接有关的节
5.3.4 重定位信息
使用readelf -r hello 查看重定位信息
同样多出一个节.rela.dyn,与动态链接有关,连接器在动态链接时在共享库中找到函数定义等待加载/运行时进行动态链接,其他符号在静态链接时完成重定位,不再存在于重定位信息中。
在静态链接中有专门用来重定位的重定位表.rela.text和.rela.plt,动态链接也有这类表,分别为.rela.data和.rela.plt。
5.3.5 符号表
使用readelf -s hello 查看符号表
hello与hello.o的符号表不同的是,多了一个节.dynsym。
.dynsym是动态符号表。.symtab节中除了来自.dynsym的多出来符号,还有一些符号,他们来自其他的可重定位目标文件。
5.4 hello的虚拟地址空间
使用edb加载hello,可以得出hello的虚拟地址空间从0x401000开始,到0x401ff0结束。接下来可以分析具体内容:其中PHDR保存的是程序头表。INTERP保存了程序执行前需要调用的解释器。LOAD记录程序目标代码和常量信息;DYNAMIC 储存了动态链接器所使用的信息。
5.5 链接的重定位过程分析
用命令行objdump -d -r hello > hello.txt得到反汇编文件。
- hello的反汇编代码有确定的虚拟地址,表明完成了重定位;hello.o的反汇编代码的虚拟地址是0,表明未完成重定位。
- hello的反汇编代码中有很多节和函数的汇编代码。
- hello的反汇编地址从0开始;hello.o的反汇编地址不从0开始。
重定位过程:
对hello而言,链接器把hello中的符号定义都与一个虚拟内存位置关相关联,重定位了这些节,并在之后对符号的引用中把它们指向重定位后的地址。hello中每条指令都对应了一个虚拟地址,而且对每个函数,全局变量也都它关联到了一个虚拟地址,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地址来进行,从而执行这些指令。
5.6 hello的执行流程
ld-2.27.so!_dl_init |
加载hello hello!_start |
libc-2.27.so!__libc_start_main |
-libc-2.27.so!__cxa_atexit |
-libc-2.27.so!__libc_csu_init |
Hello初始化 hello!_init |
libc-2.27.so!_setjmp |
-libc-2.27.so!_sigsetjmp |
–libc-2.27.so!__sigjmp_save |
调用main函数(运行) hello!main |
调用打印函数 hello!puts@plt |
调用退出函数 hello!exit@plt |
ld-2.27.so!_dl_runtime_resolve_xsave |
-ld-2.27.so!_dl_fixup |
–ld-2.27.so!_dl_lookup_symbol_x |
退出程序 libc-2.27.so!exit |
5.7 Hello的动态链接分析
动态链接:假设程序调用一个有共享库定义的函数。编译器无法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法时为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU通过一种叫做延迟绑定的技术来将地址的绑定推迟到第一次调用该过程。
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。GNU 编译系统使用了延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。在got中存放函数目标地址,plt使用got中地址跳转到目标函数,在加载时,动态链接器会重定位got中的每个条目,使得它包含目标的正确的绝对地址。
执行前:
执行后:
5.8 本章小结
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行,其在软件开发中扮演着重要角色,因为它使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是指在操作系统中正在运行的一个程序的实例。一个进程具有独立的内存空间、寄存器集合和上下文信息,可以执行特定的任务。进程是操作系统进行资源分配和调度的基本单位。
进程作用包括:
- 资源管理:操作系统通过进程来管理和分配计算机系统中的资源,如内存、CPU、文件和设备等。每个进程在运行时具有独立的资源空间,操作系统通过进程控制块(PCB)来维护进程的资源信息。
- 并发执行:操作系统可以同时运行多个进程,通过分时或并行的方式实现多个任务并发执行。进程切换和调度机制使不同进程可以在一段时间内共享处理器,并且给用户提供执行多个任务的体验。
- 进程间通信:进程之间可以通过进程间通信(IPC)机制进行数据传输和信息交换。常见的IPC方式包括管道、消息队列、共享内存等,这些机制使得进程能够相互协作、共享数据和完成复杂的任务。
4.保护与隔离:每个进程有自己独立的内存空间,使得进程之间的数据和代码可以被隔离和保护。这样可以提高程序的安全性和稳定性,一个进程的错误或崩溃不会影响其他进程的运行。
5.进程的创建和销毁:操作系统提供了创建和销毁进程的机制。通过进程的创建,可以产生新的进程以执行特定任务;通过进程的销毁,释放资源并终止进程的执行。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Shell为用户提供命令行界面,使用户可以在这个界面中输入shell命令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的命令行,求值步骤则解析命令行,并运行程序。完成后重复上述步骤,直到用户退出shell。
处理流程:Shell等待用户输入指令。在用于输入指令后,从终端读取该命令并进行解析。若该命令为shell的内置命令,则立即执行该命令,否则,shell会通过fork创建一个子进程,通过execve加载并运行该可运行目标文件,用waitpid命令等待执行结束并对其进行回收,从内核中将其删除。若将该文件转为后台运行,则shell返回到循环的顶部,等待下一条命令。完成上述过程后,shell重复上述过程,直到用户退出shell。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同,子进程会获得与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆共享库以及用户栈。
Fork创建过程如下:
1.原始进程(父进程)调用fork()系统调用。
2.操作系统内核接收到fork()系统调用请求后,复制原始进程的所有信息,包括代码、数据、堆栈、打开的文件描述符等,并为子进程分配新的进程ID。
3.在子进程中,fork()函数返回值为0,表示这是子进程。在父进程中,fork()函数返回值为新创建的子进程的进程ID。
4.子进程和父进程都从fork()函数返回后,继续执行下面的代码。它们从fork()之后的那一行开始执行。
5.父进程和子进程是独立执行的,它们拥有各自的内存空间和寄存器值。父进程可以通过fork()函数的返回值获得子进程的进程ID,从而对子进程进行控制。
6.Hello程序的运行结果将在父进程和子进程中分别输出。
下图是一个fork工作的示例图:
6.4 Hello的execve过程
execve()是一个系统调用,用于执行一个新的程序。在Hello的fork进程创建过程中,父进程可以通过execve()系统调用来加载并执行一个新的程序。以下是Hello的execve过程:
1.父进程调用execve()系统调用,提供要执行的新程序的路径名和参数列表。
2.操作系统内核接收到execve()系统调用后,会启动加载并执行新程序的过程。
3.内核根据新程序的路径名找到可执行文件,并为新程序分配内存空间。
4.内核将新程序的代码、数据和资源加载到分配的内存空间,并设置相应的执行环境。
5.内核根据参数列表,将参数传递给新程序。
6.新程序从加载的内存空间的入口点开始执行,覆盖了原始进程的代码和数据。
7.Hello程序的运行结果将作为新程序的输出。
6.5 Hello的进程执行
上下文:上下文通常包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构等。
进程时间片:时间片又称为“量子”是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
进程调度的过程:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
用户态与核心态转化:进程之间的上下文切换是在kernel里完成的,完成后,又退出内核模式,进入到用户模式,去执行切换后的进程,这就是调度的原理。具体如下图所示:
6.6 hello的异常与信号处理
信号:SIGINT、SIGTSTP、SIGCONT、SIGKILL
进程收到一个SIGINT信号(Ctrl+C):
进程收到一个SIGTSTP信号(Ctrl+Z)
此时进程依然存在:
Jobs:
Pstree:在进程树中,我们可以找到bash和hello进程,由于是ssh连接,所以在sshd进程下可以找到。
进程收到一个SIGCONT信号:进程继续执行。
进程收到一个SIGKILL信号:进程被杀死(这里重新启动了一个进程,PID有所变化)
6.7本章小结
本章总结了hello进程的执行过程,展示了hello进程的执行以及hello的异常和信号处理。通过这章的学习,了解到了进程的基本概念、运行原理、进程的用户模式和内核模式、上下文切换、两个重要函数(fork和execve)、四大异常(终端、陷阱、故障、终止)、进程的并发执行等等。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是由程序产生的由段选择符和段内偏移地址组成的地址。在hello中即为通过偏移相对寻址的例如前面的0x12(%rip)寻找静态提示字符串,又如利用指针进行寻址等。
线性地址是一种中间地址,在地址空间由连续的整数表示,是逻辑地址到物理地址变换之间的中间层。在各段中,逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。
虚拟地址:是程序访问存储器使用的逻辑地址。使用虚拟地址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送至内存先转换成适当的物理地址。在linux中,虚拟地址数值等于线性地址
物理地址:物理地址就是内存单元的绝对地址,比如你有一个4G的内存条插在电脑上,物理地址0x0000就表示内存条的第一个存储单元,0x0010就表示内存条的第17个存储单元,不管CPU内部怎么处理地址,最终访问的都是物理地址。在CPU实模式下“段基址+段内偏移地址”就是物理地址,CPU可以使用此地址直接访问内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址表示为[段标识符:段内偏移量],段标识符是一个16位长的字段(段选择符)。根据段选择符的T1选择,当前要转换段位是GDT还是LDT,再根据寄存器,得到其地址。拿出段选择符的前13位,查找到对应的段描述符,得到Base。将基地址和逻辑地址相加,获得线性地址。如下图所示:
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址用VA来表示。处理虚拟地址即处理线性地址。VA分为虚拟页号(VPN)与虚拟页偏移量(VPO)。CPU取出VPN,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址和物理页偏移量(PPO)组成的物理地址。在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。具体如下图所示:
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(快速转换缓冲器)是一种缓存结构,用于加速虚拟地址到物理地址的转换过程。在Hello程序中,TLB可以缓存最近的虚拟地址到物理地址的映射关系,以便更快地进行地址变换。
在四级页表的支持下,虚拟地址被分为四个级别的索引,包括页目录表、页中间目录表、页目录和页表。每个级别的索引都会查找相应的页表项,直到找到最终的物理页帧号。
当地址转换过程中,TLB能够缓存最近的映射信息,极大地提高了地址转换的效率。如果TLB中没有找到对应的映射信息,那么就需要显式地访问页表来完成地址转换。
7.5 三级Cache支持下的物理内存访问
因为Cache的速度大大快于主存,所以三级Cache的支持可以使得程序的运行速度大大提高。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的pid。为了给这个新进程创建虚拟内存,它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork从新进程返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。具体情况如下图所示:
7.7 hello进程execve时的内存映射
execve()系统调用会将指定的可执行文件加载到当前进程的内存空间,并运行这个可执行文件。在这个过程中,原进程的内存空间会被释放,而指定的可执行文件所需的内存空间会被加载到新的地址空间中。
在Hello程序中,可执行文件被加载到程序的代码段和数据段中。程序的堆和栈也会根据可执行文件的需要进行重新分配和映射。
7.8 缺页故障与缺页中断处理
指令引用一个相应的虚拟地址,而与该地址相应的物理页面不在内存中,会触发缺页异常。缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,即存放在PP3的VP4。若VP4已经被修改了,那么内核就会将它复制回磁盘,否则直接修改。接下来,内存从磁盘复制VP3到内存中的PP3,更新PTE3,然后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会将导致缺页的虚拟地址重新发送到地址翻译硬件。此时,VP3已经缓存在主存中,可以正常处理,而不会触发缺页。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(如图7-9)。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
隐式空闲链表:
这样的一种结构,主要是由三部分组成:头部、有效载荷、填充(可选);
头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据
简单的放置策略:
1> 首次适配:从头搜索,遇到第一个合适的块就停止;
2> 下次适配:从头搜索,遇到下一个合适的块停止;
3> 最佳适配:全部搜索,选择合适的块停止。
分割空闲块:适配到合适的空闲块,分配器将空闲块分割成两个部分,一个是分配块,一个是新的空闲块。
增加堆的空间:通过调用sbrk函数,申请额外的存储器空间,插入到空闲链表中 。
合并:
(1)合并时间:立即合并和推迟合并。
立即合并:在每次一个块被释放时,就合并所有的相邻块
推迟合并:直到某个分配请求失败时,扫描整个堆,合并所有的空闲块。
(2)合并:(4种情况)
a.当前块前后的块都为已分配块:不需要合并
b.当前块后面的块为空闲块:用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。
c.当前块前面的块为空闲块:用当前块和前面块的大小的和来更新前面块 的头部和当前块的脚部。
d.当前块的前后块都为空闲块:用三个块大小的和来更新前面块的头部和 后面块的脚部。
其中,查询前面块的块大小时可以通过脚部来查,查询后面块的块大小时 可以通过头部来查。
7.10本章小结
本章主要介绍了hello的存储器地址空间,并介绍了逻辑地址、线性地址、虚拟地址、物理地址等概念。分析了段式管理是如何完成从逻辑地址到线性地址(虚拟地址)的变换的。分析了页式管理是如何完成线性地址到物理地址的变换。分析了TLB与四级页表支持下的VA到PA的变换。介绍了三级Cache支持下的物理内存访问的流程。分析了hello进程fork与execve时的内存映射。介绍了缺页故障和缺页中断的处理。分析了动态存储分配管理。虚拟内存使得程序的调用更加私密,也使得物理空间得到了最大利用。也使得每个进程都有一个自己的虚拟空间,让进程运行的更加效率。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I /O设备都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。
设备管理:unix io接口
IO是在主存和外部设备之间复制数据的过程。输入操作是从IO设备复制数据到主存,而输出操作时从主存复制数据到IO设备。所有的IO色号被被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为UnixI/O。
8.2 简述Unix IO接口及其函数
Unix IO接口是一组系统调用函数,用于进行文件和设备的输入输出操作。这些函数提供了对文件描述符的操作,包括打开文件、读取文件内容、写入文件内容和关闭文件等功能。
常用的Unix IO函数包括:
open():打开一个文件,返回一个文件描述符。
read():从文件或设备中读取数据。
write():向文件或设备写入数据。
close():关闭文件。
lseek():设置文件指针的位置。
ioctl():对设备进行控制操作。
这些函数可以使用文件描述符来指定要操作的文件或设备。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
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;
}
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);
}
write函数:将buf中i个元素写到终端
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
可以找到INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
可以发现它是要调用sys_call这个函数:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
接着执行字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。主要有以下3个步骤:
1.将ASCII码转换为二字节码存储在VRAM中
2. 将VRAM中的二字节码转换为字形码存储在ROM中
3. 显示控制过程
8.4 getchar的实现分析
getchar的实现是用一个宏实现的
#define getchar() getc(stdin)
getchar()有一个int类型的返回值。当程序调用getchar()时,程序等着用户按键。用户输入的字符被放在键盘缓冲区中,直到用户按下回车为止。当用户键入回车后,getchar()开始从stdin中每次读入一个字符。如果用户在按下回车之前输入了不止一个字符,那么其他字符会保留在键盘缓冲区中,等待后续的函数调用读取。
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;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。当键盘上的一个按键按下时,键盘会发送一个中断信号给CPU,与此同时,键盘会在指定端口(0x60) 输出一个数值,这个数值对应按键的扫描码叫通码,当按键弹起时,键盘又给端口输出一个数值,这个数值叫断码,这样计算机知道我们何时按下何时按下、松开,是否一直按着按键。
中断处理子程序把键盘中断通码和断码数值转换为按键编码缓存到键盘缓冲区,然后把控制器交换给原来任务,若没有遇到回车键,继续等待用户输入,重复上述过程。
8.5本章小结
应用程序现实中需要利用操作系统提供I/O函数来与外部设备传输数据,例如我们的输入输出,需要利用到一系列I/O函数,以及其他辅助函数来帮我们达到输入和输出的目的。
(第8章1分)
结论
hello的一生包括程序(Program)和进程(Progress)两个阶段:
- Program:hello在编译系统内经过五个阶段,将高级语言的程序逐步翻译为机器能读懂得机器语言:从hello.c被预处理到hello.i,hello.i被编译到hello.s,hello.s被汇编到hello.o,hello.o被链接到hello。
- Progress:可执行目标文件hello在服务于软硬件交互的操作系统上运行,操作系统对其程序抽象为进程,好像系统上只有这个程序在进行,系统利用异常控制流控制进程的运行,利用虚拟内存实现数据到物理内存的映射,提供接口实现与I/O设备以及其他程序通信,使程序在系统上能够自如地走完自己的一生。
通过本次大作业我意识到了计算机系统设计的挑战。随着技术的不断更新和演进,如何保持系统的稳定性和安全性、如何满足用户日益增长的需求,都成为了摆在设计者面前的难题。而且,由于这一领域的专业性,需要不断学习和掌握新的知识和技能,才能跟上时代的步伐。回首这段时间的学习和实践,我深感计算机系统设计不仅仅是一门技术,更是一种艺术。它需要设计者具备全面的知识和技能,同时还需要具备创新思维和敏锐的洞察力。我相信,随着技术的不断发展,计算机系统设计将会发挥更加重要的作用,为人类创造更加美好的未来。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c | hello 的C源文件 |
hello.i | hello.c经过预处理后的预编译文本文件 |
hello.s | hello.i经过编译得到的汇编语言文本文件 |
hello.o | hello.s经过汇编得到的机器语言二进制文件(可重定位目标文件) |
hello | hello.o经过链接得到的机器语言二进制文件(可执行目标文件) |
(附件0分,缺失 -1分)
参考文献
[1] https://blog.csdn.net/xiaosaizi/article/details/105669070
[2] https://blog.csdn.net/huoyahuoya/article/details/53083424
[3] https://www.cnblogs.com/mlgjb/p/8241718.html
(参考文献0分,缺失 -1分)