2021 HIT CSAPP大作业 程序人生-Hello’s P2P

目录

摘要

本论文对hello运行的一生进行了深入地分析,在Ubuntu下通过从hello文件的诞生再到加载hello的进程并回收这一连串的过程来梳理与回顾整个CSAPP课程的脉络与知识,并对这个过程进行进一步的分析去深入地理解计算机系统。
关键词:hello;文件;进程;计算机系统

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

第1章 概述

1.1 HELLO简介

Hello的P2P是一个从高级C语言程序开始到一个运行的进程的过程,由hello.c(源程序)经过预处理器(ccp)变成hello.i(修改了的源程序),再经过编译器(ccl)变成hello.s(汇编程序),再经过汇编器(as)生成hello.o(可重定位目标程序)最后由链接器(ld)生成hello(可执行目标程序)。经过这预处理、编译、汇编、链接四个步骤后,最后Linux系统再由shell加载运行hello程序,为它fork或是execve进程,完成从From Program to Process的P2P过程。

1.2 环境与工具

硬件环境:
X64 CPU;2GHz;2G RAM;256GHD Disk
软件环境:
Windows10 64 位;Vmware 11;Ubuntu 16.04 LTS 64 位
开发工具:
Visual Studio 2010 64位以上;GDB/OBJDUMP;DDD/EDB

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件 文件作用 使用时期
hello.c 本次大实验使用的程序 整个过程
hello.i 预处理后得到的文本文件 第二章-预处理
hello.s 编译后的文本文件 第三章-编译
hello.o 汇编后得到的可重定位目标文件(二进制) 第四章-汇编
helloo.asm 反汇编hello.o得到的文本文件 第四章-汇编
helloo.elf hello.o的elf文件 第四章-汇编
hello 链接后得到的可执行目标文件(二进制) 第五章-链接
hello.elf hello的elf文件 第五章-链接
hello.asm hello的反汇编文件 第五章-链接

1.4 本章小结

本章对整个实验的全部过程进行了一个基本的概述,介绍了其P2P,O2O的两个过程,同时展示出本次实验所需的环境和工具,列举出编写本论文生成的所有中间结果文件及其作用。
(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:编译之前进行的处理,在程序编译之前,预处理器根据以字符#开头的命令,修改原始的c程序。
预处理的作用:展开#起始行的内容,并直接插入程序文本中以便调用。
使用的预处理名称及意义如图2-1所示。

图2-1 预处理名称及意义
同时,预处理还会把代码中的注释去掉。

2.2在Ubuntu下预处理的命令

预处理的命令为gcc -o hello.i -E hello.c。
其中-E是对hello.c进行预处理命令,-o是对输入结果进行导入操作,这里即是将输入结果导入到hello.i。

在这里插入图片描述

图2-2 预处理命令

2.3Hello的预处理结果解析

预处理后生成文件结果如图2-3所示
在这里插入图片描述

图2-3 预处理后生成文件结果
hello.c中文本共28行,有注释,有头文件。
在这里插入图片描述

图2-4 hello.c文本
经过预处理后共3074行,其中删除了注释,并将头文件中的stdio.h unistd.h stdlib.h展开,将所有内容放入hello.i文本中。
在这里插入图片描述

图2-5 hello.i文本

2.4 本章小结

预处理阶段,预处理器将#后代码行展开,并删除注释,gcc预处理的命令为gcc -o hello.i -E hello.c。如此进行使得hello完成P2P的一步,为后续编译阶段打下基础。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将预处理后的代码进行词法语法分析语义检查及优化后翻译成汇编语言程序。
编译的作用:将不同的高级语言程序翻译成机器更好理解更通用的低级汇编语言文本。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。

3.2 在Ubuntu下编译的命令

编译的命令为gcc -o hello.s -S hello.i
其中-S是对hello.i进行编译命令,-o是对输入结果进行导入操作,这里即是将输入结果导入到hello.s。

在这里插入图片描述
图3-1 编译的命令

3.3 Hello的编译结果解析

3.3.1 编译后生成文件

编译后生成文件结果如图3-2所示
在这里插入图片描述
图3-2 编译后生成文件结果

3.3.2 数据处理解析

1.全局变量
在这里插入图片描述

图3-4全局变量在hello.c的初始化
只有一个全局变量sleepsecs,大小为4字节,要求也要4字节对齐。
在这里插入图片描述

图3-5全局变量sleepsecs
sleepsecs进行了初始化所以其位于.rodata节中,同时因赋值时sleepsecs时int型对于浮点数2.5发生隐式类型转换,故实际赋值为2。
在这里插入图片描述
图3-6全局变量sleepsecs

2.局部变量
局部变量存放在寄存器或栈中,此处存放在栈-4(%rbp)里。
在这里插入图片描述

图3-7局部变量在hello.c的赋值
在这里插入图片描述

图3-8 局部变量的赋值

3.形式参数

在这里插入图片描述

图3-9 hello.c定义的形式参数
前6个形式参数存放在寄存器rdi,rsi,rdx,rcx,r8,r9中,其后的参数放入栈中的参数构造区。
在根据图3-7中hello.c中对argv[1],argv[2]的调用以及argc!=3的表达式,定位到hello.s中的调用,得到argc,argv[]先是位于rdi,rsi中后放入栈中。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

图3-10 形式参数在hello.s中的调用

4.常量字符串
在这里插入图片描述

图3-11 hello.c中部分常量字符串
常量字符串分别位于 hello.s 的.LC0和.LC1中。

在这里插入图片描述

图3-12 hello.s中 的字符串常量
采用如图3-13的方式调用用lea指令加载具体存储地址到rdi寄存器中。
在这里插入图片描述

图3-13字符串常量的调用

3.3.3 操作符解析

1.赋值操作符
对于hello.c中的赋值操作符,均采用图3-8所示mov方式将其赋值给具体寄存器或内存位置,从而赋值给寄存器或内存位置对应参数,而对于mov后缀代码定义为赋值的字节大小。
而对于赋值过程中的类型转换及初值问题于3.3.2中全局变量已有解释。

在这里插入图片描述

图3-14 代码后缀定义
2.逗号操作符
逗号操作符在hello.c中当作顺序点用,对于图3-9用逗号隔开的形式参数,所位于栈中位置按一定规则排列,如图3-10所示,逗号前面的argc位于栈更低地址更高的位置,argv位于栈更高地址更低的位置。
3.算术操作符
对于i++操作在hello.s中的汇编代码如图3-16所示
在这里插入图片描述

图3-15 hello.c i++操作
在这里插入图片描述

图3-16 hello.s对应汇编代码
4.关系操作符
对于argc != 3 和 i < 10这样的比较操作,在hello.s中用cmp将具体变量与立即数进行比较。

在这里插入图片描述
在这里插入图片描述

图3-17 hello.s中的比较操作
5.数组操作
对于char* argv[]这样的数组引用,在hello.s中由图3-10得知其位于一段连续的栈中,从地址-32(%rbp)获得数组首地址后,分别加16和加8,获得argv[2],argv[1]。

3.3.4 控制转移解析

对于argv != 3就执行图3-11操作,以及对i < 10就继续循环的操作在hello.s中是通过cmp操作设置条件码然后根据条件采取对应jump操作到具体目标。
在这里插入图片描述

图3-18 hello.s设置条件码并跳转

3.3.5 函数操作解析

1.参数操作
对于函数参数的构建是前6个参数存放在寄存器rdi,rsi,rdx,rcx,r8,r9中,其后的参数放入栈中的参数构造区。
例如如图3-10main函数参数的传递,又如图3-19中寄存器rdi,rsi传递给print函数两个参数,rdi传递给sleep一个参数,rdi传递给exit一个函数

在这里插入图片描述

图3-19 hello.s中对print函数两个参数和sleep函数一个参数传递

2.函数调用及信息
如图3-19所示对于函数的调用在hello.s中采取call的汇编代码指令,包括图3-21亦是对getchar()函数调用。同时对于hello.c定义的主函数main还有关于其全局函数的信息解释。
在这里插入图片描述

图3-20 main函数的信息
在这里插入图片描述

图3-21 调用getchar函数
3.函数返回值
hello.s中的函数返回值均保存在寄存器rax中,如图3-21main函数返回0.
在这里插入图片描述

图3-22main函数返回值的信息

3.4 本章小结

阐述了编译过程的概念与作用,用gcc的命令实现了从预处理后文件到生成汇编代码程序,并深入地解析了hello.s中各部分数据操作的含义。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将汇编语言程序翻译成机器语言指令,把这些指令打包成可重定位目标程序。
汇编的作用:将汇编代码转换成计算机可识别的二进制文件。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

汇编的命令为gcc -o hello.o -c hello.s
其中-c是对hello.s进行汇编命令,-o是对输入结果进行导入操作,这里即是将输入结果导入到hello.o。
在这里插入图片描述

图4-1 汇编命令

4.3 可重定位目标elf格式

ELF格式如图4-2所示
在这里插入图片描述

图4-2 ELF格式
在这里插入图片描述

在这里插入图片描述

图4-3 ELF头与节头

4.3.1 ELF头

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

4.3.2 节头部表

节头部表则描述了各节的名称、大小、类型、地址、偏移量、对齐、读写属性等等信息。

4.3.3 重定位节

在这里插入图片描述

图4-4 重定位节信息
.rela.text是.text中重定位目标列表,链接器把这个目标文件和其他文件组合时,需要修改这些位置。同时下方每一行对应一个重定位条目,指出了每个符号的节偏移量,重定位类型,信息,符号值,对应的符号名称,和进行重定位时地址计算的加数。hello.o重定位条目包括以及只读数据节,各类函数,以及全局变量sleepsecs。
.rela.eh_frame则是eh_frame节的重定位信息。

4.3.4 符号表

在这里插入图片描述

图4-5 符号表信息
Value表示节中偏移量,Size表示大小,Type表示为函数还是数据,Bind表示是否为全局的,如sleepsecs为全局变量,大小为4字节

4.4 Hello.o的结果解析

在这里插入图片描述

图4-6 hello.o 反汇编结果
反汇编后与hello.s进行比较后发现hello.s中的立即数均为十进制,而反汇编后的为十六进制数,同时分支转移函数调用时反汇编call指令接的是具体值,汇编代码中直接接的函数名。同时反汇编中没有.L0.L1等目标位置,对于条件跳转直接接的地址值。
剩余部分机器语言二进制指令对应反汇编代码与hello.s中基本一致。

4.5 本章小结

介绍了汇编的概念和作用,采用gcc命令将hello.s汇编成hello.o,同时深入地分析了汇编后的ELF格式与信息,并且对hello.o进行了反汇编并于hello.s的汇编代码进行比较。
(第4章1分)

第5章 链接

5.1 链接的概念与作用

链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:把必要的系统目标文件组合起来生成一个可执行目标文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

Ubuntu下链接的命令为: ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/crt1.o /lib/x86_64-linux-gnu/crti.o /lib/x86_64-linux-gnu/libc.so /lib/x86_64-linux-gnu/crtn.o hello.o

图5-1 链接命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

5.3 可执行目标文件hello的格式

5.3.1 ELF头
对比图4-3我们可以发现,类型变成了可执行文件,节头增加至了27个。

					图5-2 ELF头

5.3.2 节头部表
对比图4-3我们可以发现,节头部表仍旧描述了各节的名称、大小、类型、地址、偏移量、对齐、读写属性等等信息。但增加了诸如.plt这般新的节。
这里给出部分常见节的解释
.init:程序初始化代码调用
.text:已编译的机器代码
.rodata:只读数据
.plt 过程链接表(Procedure Linkage Table),包含动态链接器调用从共享库导入的函数所必须的相关代码。
.data:已初始的全局和静态C变量
.got节保存全局偏移表。它和.plt节一起提供了对导入的共享库函数访问的入口。
.rela.dyn:动态重定位表
.rela.plt:.plt节的重定位条目
.gnu.hash:gnu的扩展符号hash表
.eh_frame:程序执行错误时的指令
.dynsym节保存共享库导入的动态符号信息。
.dynstr保存动态符号字符串表,存放一系列字符串,代表了符号的名称,以空字符作为终止符。
.gnu.version:符号版本
.gnu.version_r:符号引用版本
.bss:为初始化的全局和静态C变量及初始化为0的全局和静态变量
.symtab:符号表,存放程序中定义和引用的函数和全局变量的信息
.strtab:一个字符串表,其内容包括 .symtab 和 .debug节中的符号表,以及节头部中的节名字。

图5-3 节头部表
5.3.3 程序头
hello的程序头表是一个结构数组。每种结构都描述了系统准备程序执行所需的段或其他信息。目标文件段包含一个或多个节。
该程序头内共八个段,包含各段的类型,偏移量,读写属性,虚拟地址,物理地址,对齐要求,内存大小等信息。
其中程序包含八个段:
1.PHDR: 指定程序头表在文件及程序内存映像中的位置和大小。
2.INTERP: 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小。对于动态可执行文件,必须设置此类型。
3.LOAD: 指定可装入段,通过p_filesz和p_memsz进行描述。文件中的字节会映射到内存段的起始位置。
4.DYNAMIC: 指定动态链接信息。
5.NOTE: 指定辅助信息的位置和大小。
6.GNU_PROPERTY:
7.GNU_STACK: 权限标志,标志栈是否是可执行的。
8.GNU_RELRO: 指定在重定位结束之后那些内存区域是需要设置只读。

图5-4 程序头
5.3.4 段节与动态链接节信息
段节中包含各段的符号名字,同时动态链接节各共享库引入的节中的名称类型。

图5-5 段节和动态链接节
5.3.5 重定位节信息
重定位节信息与4.3.3相同,只是增加了两个新的重定位符号__libc_start_main@GLIBC_2.2.5和__gmon_start__。

图5-6 重定位节信息
5.3.6 符号表信息
符号表信息与4.3.4中相同,只是增加了许多新的符号。

图5-7 符号表信息

5.4 hello的虚拟地址空间

图5-8 edb查看虚拟地址空间
可以看到虚拟空间从0x400000开始与图5-4中的LOAD段对应,其中地址0x400040与PHDR段对应,其中地址0x4002e0与INTERP对应,地址0x400300与NOTE和GNU_PROPERTY段对应,地址0x403e50与GNU_RELRO和DYNAMIC段对应。

5.5 链接的重定位过程分析

图5-9 objdump-d -r hello
分析得到此时反汇编得到的程序包含链接函数的汇编代码,并且都取重定位后确定的地址为跳转目标,地址不再从0开始,不同区域有着不同的虚拟地址。
重定位常用方法为重定位PC相对引用和重定位PC绝对引用。
此处我们选取printf函数进行分析,得知main函数口的地址为0x401185结合图4-4printf的偏移量为0x5e,得到运行时地址为0x4011e3,同时得知printf函数的地址0x401090,0x401090 – 0x 4011e3 – 0x4(重定位时的加数)转换为小端序的计算结果为0xa8 fe ff ff与objdump printf函数结果相同。

					图5-10 objdump printf函数结果

5.6 hello的执行流程

其调用与跳转的各个子程序名如下:
_dl_start
_dl_init
_start
_libc_start_main
__libc_csu_init
_init
main
_GI_IO_puts
__GI_exit
_fini

				 图5-11 执行过程部分call from截图

5.7 Hello的动态链接分析

dl_init调用之前直接进入函数对应的PLT中,接着PLT指令通过对应的GOT指令进行间接跳转,由于每个GOT指令初始时都指向他对应的PLT条目的第二条指令,所以这个间接跳转只是简单的把控制传回PLT条目的下一条指令。接着把函数的ID入栈PLT跳转到PLT[0],PLT[0]再将动态链接器的一个参数入栈,然后间接跳转到动态链接器中。动态链接器依据两个栈条目确定函数的运行位置,重写对应的GOT条目,再把控制传给函数。
在dl_init调用之后,GOT表中存放的就是对应的函数的地址。
我们根据.got.plt地址发现存储的got基本为零。

图5-12 dl_init调用之前
调用后有新地址生成。

图5-13 dl_init调用之后

5.8 本章小结

本章介绍了链接的概念和作用,采用ld命令进行了动态链接,分析了链接可执行文件hello的ELF格式,节头部信息等等,并与hello.o文件对应比较,然后分析了hello的虚拟空间分布以及如何进行重定位,获取了hello从头到尾运行的子程序,最后对hello进行了动态链接分析。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:一个执行中程序的实例。
进程的作用:提供一种抽象使得我们的程序好像是系统当中当前唯一运行的程序一样。我们的程序就好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们的指令。最后我们程序中的代码和数据好像是系统内存中的唯一对象。

6.2 简述壳Shell-bash的作用与处理流程

shell的作用: 在交互方式下解释从命令行输入的命令,执行一系列读/求值步
骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并表用户运行程序。
shell 的处理流程:首先 shell 打印一个命令行提示符,等待用户在 stdin 上输入命令行,待输入命令行并回车后,解析这个以空格分隔的命令行参数,并构造最终会传递给 execve 的 argv 向量,同时若最后一个参数是’&’字符,表示应该在后台执行,否则应该在前台执行。解析完命令行后,开始检查第一个命令行参数是否是一个内置的 shell 命令。是则立即解释,否则 shell 创建一个子进程,并在子进程执行所请求程序,若是在后台运行则 shell 返回顶部,等待下一个命令行,否则 shell 等待作业终止再开始下一轮迭代。

6.3 Hello的fork进程创建过程

我们根据shell的处理流程,在当前目录下打开shell输入./hello 1190100612 冯梓峻运行,shell作为父进程,通过fork函数创建一个新的运行的子进程hello。Hello子进程几乎但不完全与父进程相同,hello进程得到与父进程用户级虚拟空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库、以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,Hello进程可以读写父进程中打开的任何文件。父进程和Hello进程最大的区别在于它们有不同的PID。

			        图6-1 hello的fork进程

6.4 Hello的execve过程

如果采用execve函数来加载hello进程的话,输入参数为当前hello所在目录文件名,且带上参数列表和当前环境变量列表,同时进行上下文切换,调用启动代码,将控制传递给新程序的主函数,并不返回。

图6-2 execve函数

						图6-3 上下文切换

6.5 Hello的进程执行

当我们执行hello进程的时候,首先因未设置模式位而运行在用户模式下,同时内核会该进程维护一个上下文,即内核重新新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
同时因为shell本身进程和其他进程与hello进程的运行时间有重叠,故他们是在并发的运行,每个进程都有它独立的逻辑控制流。
而此时我们考虑由不是hello的时间片到执行hello进程的时间片时,首先进行如图6-3的上下文切换,由内核的调度器来进行调度hello进程抢占当前进程,它首先保存当前进程的上下文,然后恢复hello被抢占所保存的上下文,最后将控制传递给这个新的上下文。
然后我们考虑hello进程的执行过程,在hello输出后,调用sleep函数使整个进程休眠2秒,此刻发生上下文转换,运行其他进程,两秒后hello重新抢占进程,如此运行9次。
随后遇见getchar()函数会读一个文件(read),触发一个陷阱,此时hello进程将控制传递给内核模式下的陷阱处理程序,处理程序再返回到hello进程的下一条指令,直至最后程序终止。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

  1. hello正常执行时
    Hello进程在执行中,遇见getchar()函数会读一个文件(read),触发一个陷阱,此时hello进程将控制传递给内核模式下的陷阱处理程序,处理程序再返回到hello进程的下一条指令。同时遇见sleep函数时,亦会触发陷阱,处理方法与上述相同。最后进程会执行exit指令也是一个陷阱终止进程,并向父进程发送一个SIGCHLD信号。
  2. Ctrl-Z
    Hello进程执行时遇见Ctrl-z后进程收到一个中断,同时收到一个SIGSTOP信号,整个进程停止。
    此时 ps jobs pstree fg kill各命令运行结果如下。

图6-4 ps与jobs命令

图6-5 jobs命令
fg命令发送一个SIGCONT信号给hello进程,使得hello进程在前台继续运行。

图6-6 fg命令
最后用kill命令发送一个SIGKILL信号杀死进程。

图6-7 kill命令
3. Ctrl-c命令
Ctrl-c发送一个SIGINT信号给hello进程,最终hello进程终止。

图6-8 ctrl-c命令

6.7本章小结

本章阐述了进程的概念与作用以及壳Shell-bash的作用与处理流程,同时深入地分析并理解了hello的fork和execve过程,同时理解了有关上下文信息、进程时间片,进程调度的过程,用户态与核心态转换等等诸多概念的理解过程,同时对hello进程的执行过程的异常和信号处理,以及外部造成的异常和信号处理有了更多的了解和认识。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,是CPU的段内偏移地址,由两个地址分量构成,一个为段基值,另一个为偏移量。
线性地址:是逻辑地址到物理地址变换之间的中间层,在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:指的就是线性地址。
物理地址:地址存储器中存储单元对应实际地址称物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

因CPU不支持分页机制,所以逻辑地址到线性地址的转换采用段式管理。
48位逻辑地址前16位为段选择符,段选择符又分为索引,TI,RPL,其中索引为描述符表的索引,TI是0则在GDT全局描述符表中寻找,为1则在LDT局部描述符表中寻找。RPL则是段的级别。通过索引找到段基址后与后32位段内偏移量相加就是线性地址值。

						图7-1段选择符

					   图7-2段式管理流程

7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址即虚拟地址,VM系统通过将虚拟内存分割位虚拟页,类似地,物理内存被分割为物理页,大小都为P字节,由于DRAM不命中昂贵的时间代价,虚拟页往往很大,于是我们采用页式管理来进行从线性地址到物理地址的变换。
虚拟地址VA分为p位的VPO(虚拟页面偏移量),和(n-p)位的VPN(虚拟页号),MMU(地址管理单元)利用VPN来选择适当的PTE。如,VPN0选择PTE0,VPN1选择PTE1,以此类推。然后将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意因为物理页面和虚拟页面都是P字节的,所以虚拟页偏移量和物理页偏移量都是相同的。

图7-3 页表的地址翻译

7.4 TLB与四级页表支持下的VA到PA的变换

TLB称为翻译后备缓冲器,是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB的组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号提取出来。如图T=2^t个组,索引则由VPN的t个最低位组成,而TLB标记由VPN剩下的位组成。TLB命中的步骤为,CPU产生虚拟地址MMU从TLB中取出对应PTE然后翻译成物理地址发送给高速缓存/主存,随后返回数据字给CPU,不命中时则必须从L1缓存中取出相应PTE。

			 图7-4 虚拟地址中用以访问TLB的组成成分

			图7-5 TLB命中和不命中操作图

而这里我们采用CORE i7地址翻译下运行的四级页表下VA到PA的变换:
我们可以发现,36位VPN被划分成了4个9位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。

			图7-6 如图为四级页表支持下VA到PA的变换

7.5 三级Cache支持下的物理内存访问

三级Cache支持下的物理内存访问。

			图7-7 三级Cache支持下的物理内存访问

当我们获取了物理地址PA后,PA分为CT(高速缓存标记),CI(高速缓存索引),CO(缓冲块内的字节偏移量)三部分。首先对于当前PA根据CI在L1cache查找索引然后根据高速缓存标记CT判断是否在组内存在该块,若存在该块且标记位为1则命中,再根据CO获取块偏移后结果,将该结果返回给CPU。若不命中则按照相同的查找策略按L2cache、L3cache、主存这样以此向下查找下去,找到相应块后,若上一层组内缓存有空闲块则将它写入空闲块中,否则则采用相应策略选取牺牲块用找到的块将其替换,替换到L1后返回结果。

7.6 hello进程fork时的内存映射

当shell先调用fork函数时,内核会为新进程创建如下组织的数据结构,同时分配给它一个唯一的PID。
这个数据结构首先是一个任务结构task_struct。任务结构中的一个条目指向mm_struct,它描述了虚拟内存的当前状态。两个字段分别是pgd和mmap,其中pgd指向第一级页表(页全局目录)的基址, 而mmp指向一个vm_area_structs(区域结构)的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,就将pgd存放在CR3控制寄存器中。
为了我们的目的,一个具体区域的区域结构包含下面的字段:
vm_start:指向这个区域的起始处。
vm_end:指向这个区域的结束处。
vm_prot:描述这个区域内包含的所有页的读写许可权限。
vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的(还描述了其他一些信息).
vm_next:指向链表中下一个区域结构。

图7-8 虚拟内存的组织
为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。将两个进程中的每个页面都标记为已读,并将两个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

在fork一个新进程后shell调用execve加载并运行hello可执行文件,按如下几个步骤进行,先是删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。然后映射私有区域。为hello程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和 data区。 bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

图7-9 加载器映射的地址空间
再然后是映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库1ibc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中共享区域内。
最后设置程序计数器(PC)。 execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

7.8 缺页故障与缺页中断处理

当MMU在试图翻译某个虚拟地址A时,触发一个缺页。这个异常导致控制转入到内核的缺页处理程序,处理程序随后就执行下面的步骤。
首先判断虚拟地址是否合法,即缺页处理程序搜索区域结构的链表将其与vm_start与vm_end作比较,不合法则触发段错误故障,从而终止进程。
然后判断进程是否有读写该页面的权限,如对只读页面进行写操作这样不合法的访问,触发一个保护异常,终止进程。
最后内核知道缺页是对合法的虚拟地址进行合法的操作造成的,那么它开始选择一个牺牲页面,若其被修改过,就交换出去,换入新的页面并更新页表。缺页处理程序返回则重新启动引起缺页指令,再次发送A到MMU后可正常翻译。

图7-10 缺页处理

7.9动态存储分配管理

Hello的Printf会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。
而对于分配器采用的数据结构,分为带边界标签的隐式空闲链表和带边界标签的显示空闲链表。
带边界标签的隐式空闲链表是由一个字的头部、有效载荷,以及有效载荷、可能的一些额外的填充组成。同时还在块的结尾处添加一个脚部(边界标记),其中脚部就是头部的一个副本。
而显式的链表则在隐式链表的基础下增添一个pred(前驱)和succ(后继)指针。
在确定了分配器的数据结构后,我们开始考虑内存管理的方法,首先对于放置分配块的策略,常见的策略有:首次适配、下一次适配、最佳适配。
首次适配是从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配则是从上一次查询结束地方开始搜索。最佳适配则是检查每一个空闲块,选择适合所需的空闲块。
找到匹配空闲块,则根据空间大小匹配选择整个空闲块还是分割空闲块,对于显示链表来说,分割后的空闲块采用后进先出的顺序或是按照地址顺序来维护链表。
若没有足够大的空闲块来放置,则向内核请求额外的堆内存,转化为一个大的空闲块来放置。
而当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻,则根据前后块的边界标记判断它是否是空闲块,是空闲块则将其合并为一个更大的空闲块。
至此动态内存管理的基本方法与策略介绍完毕。

7.10本章小结

本章首先介绍了逻辑地址、线性地址、虚拟地址、物理地址的概念,随后分析了从逻辑地址到线性地址的段式管理,再介绍了从线性地址到物理地址的页式管理,介绍了TLB对虚拟地址寻找物理地址的加速,同时介绍了TLB缓存查找不命中时,通过四级页表节约内存空间来进行从VA到PA的转换。随后介绍了hello进程fork时和execve时的内存映射,对翻译虚拟地址时的缺页故障和缺页故障处理程序进行了探讨,最后简述了动态内存管理的基本方法与策略。
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
文件的类型如下:
普通文件:包含任意数据,应用程序通常要区分文本文件和二进制文件,文本文件只含有ASCII或Unicode字符;二进制文件是所有其他的文件。对内核而言,这二者没有区别。
目录:目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。
套接字:是用来和另一个进程进行跨网络通信的文件。
设备管理:unix io接口
输入和输出以一种的统一且一致的方式来执行。

  1. 打开文件。应用程序通过要求内核打开相应文件,内核返回一个小的非负整数,称为描述符。
  2. 改变当前文件位置。对于每个打开的文件,内核保持一个位置k,初始为0。
  3. 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。
  4. 关闭文件。当应用完成对文件的访问,就通知内核关闭这个文件。

8.2 简述Unix IO接口及其函数

1.打开和关闭文件
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。出错则返回-1。Flags指明进程打算如何访问这个文件,mode参数指定新文件的访问权限位。
int close(int fd);
进程通过调用close关闭一个打开的文件。关闭一个已关闭的描述符会出错。
2.读和写文件
ssize_t read(int fd, void *buf, size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制之多n个字节到描述符fd的当前文件位置。
同样我们可以采用RIO包进行健壮的读取读写。
下为RIO无缓冲的输入输出函数。

图8-1 无缓冲的输入输出函数
Rio_readn函数从描述符fd的当前文件位置最多传送n个字节到内存位置usrbuf,rio_writen类似。当这两个函数被一个应用信号处理程序的返回中断,每个函数都会手动地重启read或write。
下为RIO带缓冲的输入函数

				 	图8-2 RIO带缓冲的输入函数

Rio_readlineb函数从文件rp读出下一个文本行,将它复制到内存位置usrbuf,并且用NULL字符来结束这个文本行。其最多都maxlen-1个字节,超过的文本行被截断,并用一个NULL字符结束。
Rio_reandnb函数从文件rp最多读n个字节到内存位置usrbuf。
3.读取文件元数据

				 图8-3 读取文件元数据函数

Stat函数以一个文件名作为输入,并填写数据到如图8-4所示的数据结构里,fstat函数类似,只是以文件描述符作为输入。

图8-4 stat数据结构
4.读取目录内容

					图8-5返回目录流指针函数

函数opendir以路径名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。

					图8-6 readdir函数
每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更多目录项则返回NULL。如果出错则返回NULL并设置error。
 
						图8-7 closedir函数
函数closedir关闭流并释放其所有的资源。

8.3 printf的实现分析

  1. 首先我们查看printf的源码

    			图8-8 printf源码 
    

发现定义了一个va_list类型 查看它的定义typedef char va_list发现是一个字符指针,(char)(&fmt) + 4) 表示的是…中的第一个参数。
2. 随后查看vsprintf函数的源码

图8-9 vsprintf源码
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
3. 反汇编查看write函数

图8-10 反汇编write函数
给几个寄存器传递了几个参数,然后一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
4. sys_call函数

图8-11 sys_call的实现
它的功能是显示格式化了的字符串。将要输出的字符串从总线复制到显卡的显存中。
5. 字符显示驱动子程序:
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
6. 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

				图8-10 getchar函数内容

当调用getchar函数时,getchar调用一个read系统函数,文件描述符为0,所以是从标准输入流中读入,因此触发一个异常,进入键盘中断处理子程序,进程控制转给终端输入,而当从键盘输入字符时,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,直到接受到回车键才返回。
随后控制返回给getchar函数,getchar函数返回内存位置的第一个元素。

8.5本章小结

本章是对hello中的I/O管理的总结,介绍了设备的模型化文件的概念,并介绍了IO的设备管理方法。随后对I/O打开和关闭、读写、读取元数据、目录等基本函数做了介绍。深入分析了hello中printf和getchar函数的I/O实现。
(第8章1分)

结论

一开始hello.c是一个程序文本文件,经过预处理器cpp读取头文件后修改生成了hello.i源程序,再经过编译器ccl翻译成汇编语言得到汇编程序hello.s,然后经过汇编器as翻译成机器语言程序hello.o,最后经过链接器ld链接生成可执行目标程序hello。然后运行shell并fork一个子进程,调用execve函数在当前进程的上下文加载并运行hello程序,该进程映射它的虚拟空间到文件,运行的过程当中伴随着虚拟地址到物理地址的转换,调用的函数与I/O设备紧密结合,当进程的一切终止时,被shell所回收,内核清除掉它的痕迹。
至此hello一生结束,计算机系统的课程也告一段落。
几百页书籍的翻看,成百上千小时的学习,最后凝结在hello这短短的一生,计算机系统就是这般,晦涩难懂到渐入佳境,书越读越薄,最后便是大道至简。
回顾学习,还是有许多地方没来的及深入学习,还有很多实验能够进一步优化和进步,还有很多细节没来的及分析。
有收获,也有遗憾,希望自己能在未来进一步的学习,真正做到计算机系统的融会贯通。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

中间结果文件 文件作用 使用时期
hello.c 本次大实验使用的程序 整个过程
hello.i 预处理后得到的文本文件 第二章-预处理
hello.s 编译后的文本文件 第三章-编译
hello.o 汇编后得到的可重定位目标文件(二进制) 第四章-汇编
helloo.asm 反汇编hello.o得到的文本文件 第四章-汇编
helloo.elf hello.o的elf文件 第四章-汇编
hello 链接后得到的可执行目标文件(二进制) 第五章-链接
hello.elf hello的elf文件 第五章-链接
hello.asm hello的反汇编文件 第五章-链接

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

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 链接程序和库指南[N/OL]
https://docs.oracle.com/cd/E38902_01/html/E38861/chapter6-83432
[2] 用gcc编译c语言程序以及其编译过程[N/OL]
https://blog.csdn.net/weixin_33755847/article/details/89697445
[3] Linux下 可视化 反汇编工具 EDB 基本操作知识[N/OL]
https://blog.csdn.net/hahalidaxin/article/details/84442132
[3] 通俗理解CPU中物理地址、逻辑地址、线性地址、虚拟地址、有效地址的区别[N/OL]
https://blog.csdn.net/mzjmzjmzjmzj/article/details/84713351
[4] printf 函数实现的深入剖析[N/OL]
https://www.cnblogs.com/pianist/p/3315801.html
[5]Randal E. Bryant, David R. O’Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737
(参考文献0分,缺失 -1分)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值