计算机系统大作业

摘 要
本文基于深入理解计算机系统(第三版),通过hello.c经过预处理器,编译器,汇编器,链接器,在bash里的进程管理,存储管理,以及IO管理,最终在终端显示打印的过程,向读者详细地解释程序在软硬件上的传输过程,使读者能真正地深入理解计算机系统。

关键词:程序的结构和执行;在系统上运行程序;软硬件的结合;

(摘要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 本章小结 - 6 -
第3章 编译 - 7 -
3.1 编译的概念与作用 - 7 -
3.2 在Ubuntu下编译的命令 - 7 -
3.3 Hello的编译结果解析 - 7 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
4.1 汇编的概念与作用 - 15 -
4.2 在Ubuntu下汇编的命令 - 15 -
4.3 可重定位目标elf格式 - 15 -
4.4 Hello.o的结果解析 - 16 -
4.5 本章小结 - 21 -
第5章 链接 - 22 -
5.1 链接的概念与作用 - 22 -
5.2 在Ubuntu下链接的命令 - 22 -
5.3 可执行目标文件hello的格式 - 22 -
5.4 hello的虚拟地址空间 - 28 -
5.5 链接的重定位过程分析 - 29 -
5.6 hello的执行流程 - 33 -
5.7 Hello的动态链接分析 - 34 -
5.8 本章小结 - 35 -
第6章 hello进程管理 - 36 -
6.1 进程的概念与作用 - 36 -
6.2 简述壳Shell-bash的作用与处理流程 - 36 -
6.3 Hello的fork进程创建过程 - 36 -
6.4 Hello的execve过程 - 37 -
6.5 Hello的进程执行 - 38 -
6.6 hello的异常与信号处理 - 39 -
6.7本章小结 - 42 -
第7章 hello的存储管理 - 43 -
7.1 hello的存储器地址空间 - 43 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 43 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 44 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 44 -
7.5 三级Cache支持下的物理内存访问 - 45 -
7.6 hello进程fork时的内存映射 - 46 -
7.7 hello进程execve时的内存映射 - 46 -
7.8 缺页故障与缺页中断处理 - 47 -
7.9动态存储分配管理 - 48 -
7.10本章小结 - 50 -
第8章 hello的IO管理 - 52 -
8.1 Linux的IO设备管理方法 - 52 -
8.2 简述Unix IO接口及其函数 - 52 -
8.3 printf的实现分析 - 52 -
8.4 getchar的实现分析 - 55 -
8.5本章小结 - 55 -
结论 - 55 -
附件 - 57 -
参考文献 - 58 -

第1章 概述
1.1 Hello简介
P2P的过程:hello.c(源程序)经过预处理器(cpp)生成hello.i(修改了的源程序),hello.i再经过编译器(ccl)生成hello.s(汇编程序),hello.s再经过汇编器(as)生成hello.o(可重定位目标程序),hello.o再经过链接器(ld)生成hello(可执行目标程序),在shell中输入命令后,shell为其fork子进程。
在这里插入图片描述
图1.1编译系统
O2O的过程:shell通过execve函数加载程序,程序在运行的过程中,通过内存管理机制对逻辑地址,物理地址,虚拟地址进行相应的访问,从而进行数据交换。通过CPU为其分配的时间片决定了进程的执行时间,在相应的硬件逻辑信号的协同下,程序运行结束时,shell父进程对子进程hello进行回收,内核对数据进行更新,并将结果在终端显示。

1.2 环境与工具
软件环境:VMware Workstation Pro,Ubuntu 64位,Windows10 64位
硬件环境:Intel® Core™ i7-8750H @2.20GHz CPU 8G RAM
开发与调试工具:vim,Sublime Text,gcc,edb,as,ld,readelf
1.3 中间结果
名称 作用
hello 生成的可执行程序
hello.c 源程序
hello.elf hello的ELF格式
hello.i hello.c经预处理后的文本文件
hello.o hello.s经汇编后的可重定位目标程序
hello.objdump hello的反汇编代码
hello.s hello.i经编译后的汇编文件
helloo.elf hello.o的ELF格式
helloo.objdump hello.o的反汇编代码
1.4 本章小结
本章主要解释了hello的P2P,O2O的过程,以及完成此论文所用到的环境与工具,和中间结果。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器根据以字符#开头的命令,修改原始的C程序,将读到的内容直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:
在这里插入图片描述
图2.1 hello.c的#include
以所给的hello程序为例,预处理器会读取系统头文件stdio.h,unistd.h,stdlib.h的内容,并将它们直接插入程序文本中。得到了一个另一个C程序,以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令
预处理所用命令:gcc -E hello.c -o hello.i

图2.2使用gcc命令对hello.c进行预处理

在这里插入图片描述
图2.3 hello.i的生成
2.3 Hello的预处理结果解析
用vim打开hello.i发现main函数此时在第3029行。经过分析可以发现,三个头文件的源码出现在了main函数之前,所以main函数前多了这么多代码。
在这里插入图片描述
图2.4 用vim打开hello.i
预处理将#include申明的全部写入了hello.i中

2.4 本章小结
本章主要讲解了预处理的概念与作用,以hello.c经过预处理生成hello.i的过程为例,讲解了hello.c中#include所申明的被处理到了hello.i中的过程。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器将文本文件hello.i翻译成文本文件hello.s的过程。
作用:将计算机的高级语言等价翻译成汇编语言。

3.2 在Ubuntu下编译的命令
编译所用命令:gcc -S hello.i -o hello.s

图3.1 用gcc对hello.i进行编译

在这里插入图片描述
图3.2 hello.s的生成

3.3 Hello的编译结果解析

在这里插入图片描述
图3.3 hello.s的内容
3.3.1数据
3.3.1.1常量
在这里插入图片描述
图3.4 hello.c中的字符串常量(如红框所示)
在这里插入图片描述
图3.5 hello.s中的字符串常量
程序中有两字符串:
①“用法: Hello 学号 姓名 秒数!\n”,由图3.5可以看出,字符串编 码格式为UTF-8,一个汉字占3个字节。
②“Hello %s %s\n”
这两个字符串都在.rodata中申明。

3.3.1.2变量
在这里插入图片描述
图3.6 hello.c中的变量(如红框所示)

在这里插入图片描述
图3.7 hello.s中的argc和argv
argc和argv作为传入的第一个和第二个参数分别存放在edi和rsi中。

在这里插入图片描述
图3.8 hello.s中的i
i为局部变量,初值为0。

3.3.2赋值
在这里插入图片描述
图3.9 hello.s中的赋值操作(如红框所示)

在这里插入图片描述
图3.10 hello.s中变量i的赋值操作

3.3.3类型转换
在这里插入图片描述
图3.11 hello.s中的类型转换(如红框所示)

在这里插入图片描述
图3.12 hello.s中的类型转换
调用函数atoi,将argv[3]转化成数字。

3.3.4算数操作
在这里插入图片描述
图3.13 hello.s中的算数操作(如红框所示)

在这里插入图片描述
图3.14 hello.s中对i的算数操作(i++)

3.3.5关系操作
在这里插入图片描述
图3.15 hello.s中的关系操作(如红框所示)

在这里插入图片描述
图3.16 hello.s中对argc的关系操作(判断argv!=4)

在这里插入图片描述
图3.17 hello.s中对i的关系操作(判断i<8)

3.3.6数组操作
在这里插入图片描述
图3.18 hello.s中的数组操作(如红框所示)

3.3.7控制转移
在这里插入图片描述
图3.19 hello.s中的控制转移if(argc!=4)
比较立即数4与-20(%rbp),如果相等就跳转至L2。

在这里插入图片描述
图3.20 hello.s中的控制转移for(i=0;i<8;i++)
比较立即数7与-4(%rbp),如果7<=-4(%rbp),则跳转至L4。

3.3.8函数操作
在这里插入图片描述
图3.21 hello.s中的函数操作

在这里插入图片描述
图3.22 hello.s中main函数的函数操作

在这里插入图片描述
图3.23 hello.s中第一次printf函数的函数操作

在这里插入图片描述
图3.24 hello.s中exit函数的函数操作

在这里插入图片描述
图3.25 hello.s中第二次printf函数的函数操作

在这里插入图片描述
图3.26 hello.s中的atoi函数的函数操作

在这里插入图片描述
图3.27 hello.s中的sleep函数的函数操作

在这里插入图片描述
图3.28 hello.s中的getchar函数的函数操作

3.4 本章小结
本章解释了编译的概念和作用,以及linux下相应的命令行。并从数据、赋值、类型转换、算数操作、关系操作、数组操作、控制转移、函数操作八个方面分析了hello.i的编译结果。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件。
作用:将汇编指令转化成机器可以分析的机器指令。
4.2 在Ubuntu下汇编的命令
汇编所用命令:gcc -c hello.s -o hello.o

图4.1 用gcc对hello.s进行汇编

在这里插入图片描述
图4.2 hello.o的生成
4.3 可重定位目标elf格式
使用readelf -a hello.o > helloo.elf 指令获得hello.o文件的ELF格式
在这里插入图片描述
图4.3 用readelf指令获得ELF格式
在这里插入图片描述
图4.4 helloo.elf的生成

4.3.1 ELF头
在这里插入图片描述
图4.5 ELF头
以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。

4.3.2节头
在这里插入图片描述
在这里插入图片描述
图4.6 节头
在节头中,描述了不同节的名称、大小、类型、全体大小、地址、旗标、链接、信息、偏移量和对齐。最后还有对旗标做出解释。

4.4.3重定位节
在这里插入图片描述
图4.7 重定位节
第一部分是.rela.text的重定位节。内容包含了偏移量、信息、类型、符号值、符号名称+加数。
第二部分是.rela.eh_frame的重定位节。内容包含了偏移量、信息、类型、符号值、符号名称+加数。
在这个hello.o里面需要被重定位的有puts,exit,printf,atoi,sleep,getchar和.rodata里的两个元素。
在这里插入图片描述
4.4.4符号表

图4.8 符号表
符号表的内容包括了数字、值、大小、种类、Bind、Vis、Ndx和名字。

4.4 Hello.o的结果解析
输入命令objdump -d -r hello.o > helloo.objdump进行分析

图4.9 用objdump进行分析

在这里插入图片描述
图4.10 helloo.objdump的生成

在这里插入图片描述
图4.11 helloo.objdump的内容

在这里插入图片描述
图4.12 hello.s的内容
总体而言两者差距不大,helloo.objdump每一行多了机器指令运行的地址,以及经过汇编后得到的机器指令

4.4.1数据的不同
在这里插入图片描述
图4.13 hello.s中的加法操作

在这里插入图片描述
图4.14 helloo.objdump中的加法操作
hello.s中以十进制形式对立即数表示,而helloo.objdump中以十六进制对立即数表示。

4.4.2控制转移的不同
在这里插入图片描述
图4.15 hello.s中的控制转移

在这里插入图片描述
图4.16 helloo.objdump中的控制转移
hello.s中的控制转移是利用助记符.L2实现跳转,而helloo.objdump中的控制转移是利用目的地址寻址(2b)进行跳转。

4.4.3函数调用
在这里插入图片描述
图4.17 hello.s中的函数调用

在这里插入图片描述
图4.18 helloo.objdump中的函数调用
hello.s中的函数调用是通过函数名(puts)进行调用,而helloo.objdump中的函数调用是利用目的地址寻址(21)进行调用。

4.5 本章小结
本章解释了汇编的概念与作用,给出了查看汇编的指令。从ELF头、节头、重定位节和符号表四方面分析了可重定位目标ELF格式。并且从数据、控制转移、函数调用三个方面分析了hello.s与helloo.objdump的差异。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:将可重定位目标程序hello.o经过链接得到可执行目标程序hello。
作用:合并所有所需用到的.o文件,将其合并为可执行目标程序hello。
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.1 用ld链接生成hello

在这里插入图片描述
图5.2 hello的生成
5.3 可执行目标文件hello的格式
使用readelf -a hello > hello.elf 命令生成hello程序的ELF格式文件。

图5.3 使用readelf生成hello.elf

在这里插入图片描述
图5.4 hello.elf的生成

5.3.1 ELF头
在这里插入图片描述
图5.5 hello.elf ELF头
以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。

5.3.2 节头

在这里插入图片描述
在这里插入图片描述
图5.6 节头
在节头中,描述了不同节的名称、大小、类型、全体大小、地址、旗标、链接、信息、偏移量和对齐。最后还有对旗标做出解释。
在这里插入图片描述
5.3.3程序头

图5.7 程序头

5.3.4段节
在这里插入图片描述
图5.8 段节

5.3.5动态节
在这里插入图片描述
图5.9 动态节

5.3.6重定位节

在这里插入图片描述
图5.10 重定位节
第一部分是.rela.dyn的重定位节。内容包含了偏移量、信息、类型、符号值、符号名称+加数。
第二部分是.rela.plt的重定位节。内容包含了偏移量、信息、类型、符号值、符号名称+加数。

5.3.7符号表
在这里插入图片描述
在这里插入图片描述
图5.11 符号表
5.3.8桶列表长度直方图
在这里插入图片描述
图5.12 桶列表长度直方图

5.3.9版本符号
在这里插入图片描述
图5.13 版本符号

5.3.10版本需求
在这里插入图片描述
图5.14 版本需求

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
在这里插入图片描述
图5.15 打开界面

在这里插入图片描述
图5.16 edb中虚拟地址情况
从可执行文件中加载的信息从0x400000处开始存放,其中每个部分(Type、PHDR、INTERP、LOAD、DYNAMIC、NOTE GUN_STACK、GNU_RELRO)所占的地址区间与程序头的一致,如下图。
在这里插入图片描述
图5.17 程序头

5.5 链接的重定位过程分析
输入命令:objdump -d -r hello > hello.objdump

图5.18 用objdump命令

在这里插入图片描述
图5.19 hello.objdump的生成

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图5.20 hello.objdump的内容
与helloo.objdump相比,hello.objdump多了:Disassembly of section .init
Disassembly of section .plt
Disassembly of section .text部分中多了0000000000400500 <_start>0000000000400530 <_dl_relocate_static_pie>00000000004005c0 <__libc_csu_init>0000000000400630 <__libc_csu_fini>
Disassembly of section .fini

5.5.1控制转移的不同
在这里插入图片描述
图5.21 helloo.objdump的控制转移

在这里插入图片描述
图5.22 hello.objump的控制转移
helloo.objump利用地址偏移量(2b)进行跳转,而hello.objdump利用直接地址(4010ec)进行跳转。

5.5.2函数操作的不同
在这里插入图片描述
图5.23 helloo.objdump的函数操作

在这里插入图片描述
图5.24 hello.objdump的函数操作
helloo.objdump是利用地址偏移量(21)进行函数调用,而hello.objdump利用直接地址(401030)进行函数调用。

5.6 hello的执行流程
过程 函数名称
main函数执行前 _dl_start
_dl_init
main函数运行时 _main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
main函数执行后 _libc_csu_init
_libc_csu_fini
_fini
在这里插入图片描述
图5.25 main函数运行时的函数

5.7 Hello的动态链接分析
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用got中地址跳转到目标函数。
由节头表可知.got.plt起始位置是0x404000
在这里插入图片描述
图5.26 调用_start之前
可以发现在调用_start之前0x404008后的16个字节均为0。
在这里插入图片描述
图5.27 调用_start之后
调用_start之后发生改变,0x404008后两个8个字节分别变为0x7fbfc1091190、0x7fbfc107c200, 其中GOT [1](对应0x7fbfc1091190、)包含动态链接器在解析函数地址时会使用的信息。GOT [2](对应0x7fbfc107c200)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数。

5.8 本章小结
本章解释了链接的概念和作用,并给出了linux下链接的命令行。并从ELF头、节头、动态节、重定位节、符号表、桶列表长度直方图、版本符号、版本需求是个方面分析了hello文件。并且从控制转移、函数操作两个角度比较了helloo.objdump和hello.objdump的差异。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例。
作用:进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
①读取用户的输入。
②分析输入内容,获得输入参数。
③如果是内核命令则直接执行,否则调用相应的程序执行命令。
④在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应。

6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
父进程和新创建的子进程之间最大的区别在于它们有不同的PID。fork函数调用一次,返回两次,一次是在父进程中,返回子进程的PID,一次是在子进程中,返回0。因为子进程的PID总非0,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。父进程和子进程是并发运行的,也就是说在不同的机器上,运行的先后顺序不一样。
在这里插入图片描述
图6.1 fork进程创建实例
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数的功能是加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。进程的地址空间如图所示。
在这里插入图片描述
图6.2 进程地址空间
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新打开一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。一共分为下面三步:①保存当前进程的上下文。②恢复某个先前被抢占的进程被保存的上下文。③将控制传递给这个新恢复的进程。
每次发生定时器中断时,内核就能判定当前进程已经运行了足够长的时间,并切换到另一个新进程。当发生上下文切换时,处理器将模式从用户模式转换到内核模式,切换完毕后,会从内核模式转换回用户模式。
在这里插入图片描述
图6.3 进程上下文的切换
6.6 hello的异常与信号处理
在这里插入图片描述
图6.4 异常的类别
在这里插入图片描述
图6.5 运行过程产生的Linux信号

图6.6 正常运行时

图6.7 乱按(不按回车)
可以发现乱按且不按回车时并不会影响进程执行。

图6.8 乱按后按回车
发现乱按后按了回车,getchar会读入回车符,并将乱按的字符作为命令行输入,故报错:无法找到此命令。

图6.9 运行时按ctrl+c
按了ctrl+c时,父进程收到SIGINT信号,终止并回收hello进程。

图6.10 运行时按ctrl+z
按了ctrl+z时,父进程收到SIGTSTP信号,将当前hello进程挂起。

图6.10 ctrl+z后输入ps
ps命令列出当前系统中的进程。

图6.11 输入jobs
jobs命令列出当前已启动的任务状态。

图6.12 输入pstree
pstree以树状结构显示进程间的关系。

图6.13 输入fg

6.7本章小结
本章讲解了进程的概念与作用,介绍了shell的定义和作用,以及用fork创建新进程,用execve执行进程,以及hello具体的执行与异常、信号处理。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。是hello.o中的相对偏移地址。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址。是hello中的虚拟内存地址。
物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。Hello中虚拟地址所对应的是物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理
最初8086处理器的寄存器是16位的,为了能够访问更多的地址空间但不改变寄存器和指令的位宽,所以引入段寄存器,8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
分段功能一共分为两种模式:①实模式 ②保护模式。
实模式,即不设防,也就是说逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
在保护模式下,线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。段寄存器无法放下32位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。描述符表中的一个条目描述一个段,构造如下:

在这里插入图片描述
图7.1 段描述符
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符在段描述符表中根据Index选择目标描述符条目,从目标描述符中提取出目标段的基地址,最后加上偏移量共同构成线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理
在这个转换中要用到翻译后备缓冲器(TLB),首先我们先将线性地址分为VPN(虚拟页号)+VPO(虚拟页偏移)的形式,然后再将VPN拆分成TLBT(TLB标记)+TLBI(TLB索引)然后去TLB缓存里找所对应的PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到PPN之后,将其与VPO组合变为PPN+VPO就是生成的物理地址了。
在这里插入图片描述
图7.2 使用页表的地址翻译

7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。CR3指向第一级页表的起始位置。
由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。
如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。
如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
在这里插入图片描述
图7.3 TLB与4级页表下Core i7的地址翻译情况

7.5 三级Cache支持下的物理内存访问
使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突,则采用最近最少使用策略LFU进行替换。
在这里插入图片描述
图7.4 物理内存的访问
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有地址空间的抽象概念.
7.7 hello进程execve时的内存映射
加载并运行hello有以下几个步骤:
①删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out 文件中的. text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
③映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的人口点。
下一次调度这个进程时,它将从这个人口点开始执行。Linux 将根据需要换人代码和数据页面。
在这里插入图片描述
图7.5 加载器是如何映射用户地址空间的区域的

7.8 缺页故障与缺页中断处理
①段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)。
②非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
③如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。
在这里插入图片描述
图7.6 故障处理流程
7.9动态存储分配管理
printf函数会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
①显式分配器:要求应用显式地释放任何已分配的块。
②隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

7.9.1隐式空闲链表堆中内存块的组织结构
在这里插入图片描述
图7.7 一个简单的堆块的格式

在这里插入图片描述
图7.8 使用边界标记的堆块的格式

7.9.2放置已分配的块
放置策略:
①首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块。
②下一次适配:从上一次查询结束的地方开始。
③最佳适配:检查每个空闲块,选择适合所需请求大小的最小空闲块。

7.9.3分割空闲块
一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,那就是分配这个空闲块中多少空间。一个选择是用整个空闲块。虽然这种方式简单而快捷,但是主要的缺点就是它会造成内部碎片。如果放置策略趋向于产生好的匹配,那么额外的内部碎片也是可以接受的。
然而,如果匹配不太好,那么分配器通常会选择将这个空闲块分割为两部分。第一部分变成分配块,而剩下的变成一个新的空闲块。下图展示了分配器如何分割8个字的空闲块,来满足一个应用的对堆内存3个字的请求。

在这里插入图片描述
图7.9分割一个空闲块

7.9.4合并空闲块
当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻。这些邻接的空闲块可能引起一种现象,叫做假碎片,就是有许多可用的空闲块被切割成为小的、无法使用的空闲块。比如,下图展示了释放分配的块后得到的结果。结果是两个相邻的空闲块,每一个的有效载荷都为3个字。因此,接下来一个对4字有效载荷的请求就会失败,即使两个空闲块的合计大小足够大,可以满足这个请求。
在这里插入图片描述
图7.10 合并空闲块
合并策略:
①立即合并:在每次一个块被释放时,就合并所有的相邻块。
②推迟合并:等到某个稍晚的时候再合并空闲块。

7.9.5显示空闲链表的基本原理
根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
在这里插入图片描述
图7.11 使用双向空闲链表的堆块格式
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。维护链表有两种方法,一种方法是用后进先出(LIFO)的顺序,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序,其中链表中每个块的地址都小于它后继的地址。
7.10本章小结
本章详细描述了hello的存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、 hello进程execve时的内存映射、缺页故障与缺页中断处理以及动态存储分配管理的内容。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
③ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
④ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析
在这里插入图片描述
图8.1 printf函数的实现
arg获得第二个不定长参数,即输出的时候格式化串对应的值。

在这里插入图片描述
图8.2 vsprintf函数的实现
vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

在这里插入图片描述
图8.3 write函数的实现
将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

在这里插入图片描述
图8.4 sys_call函数的实现
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是索要打印的字符便显示在了屏幕上。

8.4 getchar的实现分析
在这里插入图片描述
图8.5 getchar函数的实现
getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章详细描述了Linux的I/O设备管理方法,Unix I/O接口及其函数,printf和getchar函数的实现分析。

(第8章1分)
结论
事到如今,Hello终于结束了它的一生。虽出生平凡,但它绽放自己生命的过程,着实令人感到振奋。生命的激昂迸发后,也让我们回顾它一生的历程。
①每一个程序猿将代码输入至hello.c(源程序)
②hello.c经过预处理器生成了hello.i(修改了的源程序)
③hello.i经过编译器生成了hello.s(汇编程序)
④hello.s经过汇编器生成了hello.o(可重定位目标程序)
⑤hello.o经过链接器与其他.o文件和动态链接库生产力hello(可执行目标程序)
至此,hello已从懵懂的孩童成为了真正的男子汉。
⑥程序猿输入命令./hello运行程序
⑦shell进程调用fork创建子进程
⑧shell调用execve加载进程
⑨hello通过复杂的硬件逻辑结构进行访存、申请,以及信号的传递
至此,hello已迈入耄耋之年,静静等待着生命的终章。
⑩hello运行结束后被回收

这几个月的学习,有关计算机系统的知识从无到有,越学越觉得知识的深渊在暗中凝视着我,知识的厚度让我起了敬畏之心。课程结束了,hello也最终被回收了,但是我的学习之路却才刚刚开始…

(结论0分,缺失 -1分,根据内容酌情加分)

附件
名称 作用
hello 生成的可执行程序
hello.c 源程序
hello.elf hello的ELF格式
hello.i hello.c经预处理后的文本文件
hello.o hello.s经汇编后的可重定位目标程序
hello.objdump hello的反汇编代码
hello.s hello.i经编译后的汇编文件
helloo.elf hello.o的ELF格式
helloo.objdump hello.o的反汇编代码

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E. Bryant & David R. O’Hallaron. 深入理解计算机系统[M]. 北京:机械工业出版社,2019:1-613.
[2] linux下gcc命令详解
https://xuexue.blog.csdn.net/article/details/86704156
[3] printf函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[4] 线性地址的解释
https://baike.baidu.com/item/线性地址
[5] 内存地址转换与分段
https://blog.csdn.net/drshenlei/article/details/4261909
[6] ELF 构造
https://www.cs.stevens.edu/~jschauma/631/elf.html

(参考文献0分,缺失 -1分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值