HIT-ICS大作业

2019年HIT-ICS大作业
程序人生人生-Hello‘s P2P
摘 要
本文在linux环境下,借助GDB,EDB,objdump,readelf等工具手段一步步实现hello.c程序的预处理,编译,汇编,链接。通过在shell里运行分析hello对加载,运行过程。本文依据计算机系统的知识对上述每一步骤都进行了详细的讲解。

关键词:Linux,hello,编译汇编,链接,进程,I/O管理;
第1章 概述
1.1 Hello简介
P2P过程:首先先有个hello.c的c程序文本,经过预处理->编译->汇编->链接四个步骤生成一个hello的二进制可执行文件,然后由shell新建一个进程,在那里执行。
020过程:shell执行hello,为其映射虚拟内存,开始执行hello的程序,然后在运行时发生缺页的时候载入物理内存,将其output显示到屏幕,然后hello进程结束,shell回收子进程。
1.2 环境与工具
1.2.1 硬件环境
2.8GHz Intel Core I7;16GB 2133MHz LPDDR3
1.2.2 软件环境
Ubuntu19.0.0 Vmware Fusion 11.0,macOS High Sierra(10.13.6)
1.2.3 开发工具
Codeblocks,Valgrind; gprof; GDB,edb,hexedit, xcode

1.3 中间结果
hello.i(hello.c预处理之后的程序文本)
hello.s(hello.i编译成汇编语言之后的程序文本)
hello.o(hello.s生成的二进制文件)
hello(可执行的hello二进制文件)
1.4 本章小结
本章介绍了hello,硬件环境,软件环境,开发工具和运行hello即调试开发过程中生成的产物。

第2章 预处理
2.1 预处理的概念与作用
2.1.1
预处理的概念:
预处理是对于c语言程序进行一个初步整理的过程,处理掉了所有的宏定义
删除“#define”并展开所定义的宏
– 处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等
– 插入头文件到“#include”处,可以递归方式进行处理
– 删除所有的注释“//”和“/* */”
– 添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
– 保留所有#pragma编译指令(编译器需要用)
2.1.2
预处理作用:
预处理可以在在将c程序转化为s的汇编程序之前对于宏定义处理,方便后续的代码转化,并且对于在汇编中无用的注释进行处理,删去无用部分对后续操作做准备。
2.2在Ubuntu下预处理的命令
在这里插入图片描述
2.3 Hello的预处理结果解析
HIT-ICS大作业_第1张图片
HIT-ICS大作业_第2张图片
对比两张图片可以发现预处理后代码因为插入头文件所以代码变长,添加了行号,而去注释被去除了。
2.4 本章小结
本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,对预处理过程进行演示。可以通过实验发现预处理是执行程序前非常重要的一步。
第3章 编译
3.1 编译的概念与作用
3.1.1
编译是编译器(ccl)将文本文件hello.i翻译成汇编语言文件hello.s的过程。
3.1.2
编译目的就是把预处理完的文件进行一系列词法分析,语法分析,语义,式其在接下来的操作中更容易被处理。
3.2 在Ubuntu下编译的命令
HIT-ICS大作业_第3张图片
HIT-ICS大作业_第4张图片
3.3 Hello的编译结果解析
3.3.1数据
3.3.1.1变量i
在这里插入图片描述
运行时出现在栈空间
3.3.2表达式
3.3.2.1赋值
在这里插入图片描述
运用movl通过把数放在一个位置上来进行赋值
3.3.2.2 控制转移
在这里插入图片描述
cmpl是一个比较语句。假设je address前面有一个语句cmpl x,y ,执行的操作是y-x,然后设置条件码。je判断的是ZF的值,如果ZF==1则表明y-x=0,则je条件达成,那么跳转到address位置,从address位置开始继续往下执行。否则的话则从je的下一条语句开始继续往下执行。
3.3.2.3循环
在这里插入图片描述
HIT-ICS大作业_第5张图片
通过jmp到不同部分如L3,L4完成循环,循环因子存在(%rbp)-4里
3.3.2.4算数操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过addq完成加法
3.3.2.5关系操作
在这里插入图片描述
i<8变成通过cmpl指令和7比较,若小于等于就继续循环
3.3.2.6数组/指针/结构操作
printf函数里面的一系列对指针和对数组的操作编译器编译为:
在这里插入图片描述
3.3.3函数调用
3.3.3.1printf的调用

在这里插入图片描述
3.3.3.2exit的调用

在这里插入图片描述
3.3.3.4atoi的调用
在这里插入图片描述
3.3.3.5sleep的调用
在这里插入图片描述
3.3.3.6getchar的调用
在这里插入图片描述
这些函数调用若需要参数都是通过rdi传参然后通过call指令将控制传递给被调函数
3.4 本章小结
本节主要介绍的是编译器通过编译由.i文件生成汇编语言的.s文件的过程,并通过解析局部变量在.s文件中的表示方法,以及各类c语言的基本语句的汇编表示,让读者更加理解高级语言的底层表示方法。汇编语言与编译过程的学习使我们真正理解计算机底层的一些执行方法。
第4章 汇编
4.1 汇编的概念与作用
4.1.1
汇编是把汇编语言翻译成机器语言的过程,并将这些指令打包成一种叫做可重定位目标程序,并将这个结果保留在(xxx.o)中。这里的xxx.o是二进制文件。汇编过程的作用是将汇编指令转换成一条条机器可以直接读取分析的机器指令。
4.1.2
通过汇编这个过程,把汇编代码转化成了计算机完全能够理解的机器代码,这个代码也是我们程序在计算机中表示。
4.2 在Ubuntu下汇编的命令
HIT-ICS大作业_第6张图片
4.3 可重定位目标elf格式
4.3.1 ELF头
ELF头首先以一个16字节的序列开始,这个序列描述了生成该文件的系统的 字的大小和字节顺序。剩下部分就如下图所示,列出了包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小(64字节),目标文件的类型(REL可重定位文件),机器类型(AMD X86-64),节头部表的文件偏移,以及节头部表中条目的大小和数量。
HIT-ICS大作业_第7张图片
4.3.2 节
HIT-ICS大作业_第8张图片
.text 节:存有编译完的代码
.rodata 节:存放只读数据,例如跳转表, …
.data 节:存放已初始化全局变量和静态变量
.bss 节:存放未初始化的全局变量和所有被初始化为0的全局变量和静态变量,这个块里的变量不占用空间,仅仅是一个占位符,“Block Started by Symbol” 符号开始的块,“Better Save Space” 更加节省空间
.symtab 节:即符号表,存放函数和静态变量名的节名称和位置
.rel.text 节:存放.text 节的可重定位信息,在可执行文件中需要修改的指令地址,需修改的指令.
.rel.data 节:存放.data 节的可重定位信息,在合并后的可执行文件中需要修改的指针数据的地址
.debug 节:存放为符号调试的信息 (gcc -g)
.strtab: 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
节头表Section header tabl:存放每个节的偏移量和大小
4.3.3 符号表
HIT-ICS大作业_第9张图片

每个可重定位目标模块都有一个符号表,它包含m的定义和引用的符号的信息。符号表是由汇编器构造,使用编译器输出到汇编语言.s文件中的符号。每个符号表是一个条目的数组,每个条目包含下面部分

HIT-ICS大作业_第10张图片
4.3.4 重定位节

在.rel.text里面有我们的重定位条目,这个条目能告诉链接器目标文件合并成可执行文件时如何修改引用。一个重定位条目包含以下信息:
HIT-ICS大作业_第11张图片
HIT-ICS大作业_第12张图片
符号名称说明这是哪个的重定位条目,偏移量是指被修改的引用的节偏移x
类型.这里有两种,R_X86_64_32意思是重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。R_X86_64_PC32意思是重定位时使用一个32位PC相对地址的引用。一个pc相对地址就是据程序计数器的当前运行值的偏移量。
4.4 Hello.o的结果解析
通过执行objdump -d -r hello.o 得到hello.o的反汇编代码,如下:
HIT-ICS大作业_第13张图片
4.4.1 二进制机器码
汇编代码的左侧这些数字代表的是机器语言的二进制代码,机器语言就是由一系列的二进制数组成的,一般包括操作码字段和字节码字段。在指令集中每个指令都有自己对应的机器代码。
4.4.2跳转
汇编代码中:
在这里插入图片描述
机器代码及反汇编结果中:
在这里插入图片描述
我们可以发现je后面的跳转对象由代码段L2变成了地址并且反汇编代码给出提示是main函数+0x2b偏移处。而且这个偏移值可以由74(je的机器码)后面的16进制数和下一条指令地址得到。
4.4.3重定向提示
在这里插入图片描述
在hello.o的反汇编代码中我们会看到很多函数调用和数据引用,包含这些函数和数据的指令需要链接后才能被正确执行,所以与汇编代码不同,机器代码为这些函数和数据留下了一些地址以便链接后重定向。此处就是用到了rodata节中的数据,链接后的地址是PC相对寻址方式的。
4.5 本章小结
本章简述了hello.s汇编指令被转换成hello.o机器指令的过程,通过readelf查看hello.o的ELF、利用odjdump反汇编的方式查看了hello.o反汇编的内容,比较其与hello.s之间的差别。学习了汇编指令映射到机器指令的具体方式和机器指令反汇编过来得到的编码的区别。 第5章 链接
5.1 链接的概念与作用
5.1.1概念:链接是将多个可重定位目标文件组合成为一个可执行目标文件的过程,这个文件可以被加载到内存并执行。
5.1.2作用:产生一个完全链接的,可以加载运行的可执行目标文件。
5.2 在Ubuntu下链接的命令
在这里插入图片描述
5.3 可执行目标文件hello的格式
5.3.1 ELF头
HIT-ICS大作业_第14张图片
5.3.2 节头
HIT-ICS大作业_第15张图片
HIT-ICS大作业_第16张图片
.text: 该节中包含程序的指令代码;
.init: 该节包含进程初始化时要执行的程序指令;当程序开始运行时,系统会在进程进入主函数之前先执行这一个节中的指令代码;
.fini: 该节中包含进程终止时要执行的指令代码;当程序退出时,系统会执行这个节中的指令代码;
.bss : 该节中包含目标文件中未初始化的全局变量;一般情况下,可执行程序在开始执行时,系统会把这一段内容清零;但是在运行期间的.bss段是由系统动态初始化而成的,目标文件中的.bss节中并不包含任何内容,其长度为0,所以它的节类型为SHT_NOBITS;
.data/.data1:这两个节用于存放程序中已被初始化过的全局变量;在目标文件中,它们是要占用实际的存储空间的,这一点与.bss节不同;
.rodata/.rodata1:这两个节中包含程序中的只读数据,在程序装载时,它们一般会被装入到进程地址空间中的那些只读的段中;
.dynamic: 该节中包含动态链接信息,并且可能有SHF_ALLOC和SHF_WRITE等属性;
.dynstr : 该节中包含用于动态链接的字符串,一般是那些与符号表相关的名字;
.dynsym : 该节中包含动态链接符号表;
.got : 该节中包含全局偏移表(global offset table);
.plt : 该节中包含函数链接表(function link table);
.hash : 该节中包含一张哈希表;
.interp : 该节中包含ELF文件解析器的路径名;如果该节被包含在某个可装载的段中,那么该节的属性中应设置SHF_ALLOC标志位,苟泽不设置此标志位;
.strtab : 该节用于存放字符串,主要是那些符号表项的名字;如果一个目标文件中有一个可装载的段,并且其中含有符号表,则该节的属性中应该有SHF_ALLOC属性;
.symtab : 该节用于存放符号表;如果一个目标文件中有一个可装载的段,并且其中含有符号表,则该节的属性中应该有SHF_ALLOC属性;
.shstrtab: 该节是节名字表,含有所有其它节的名字;
.comment: 该节中包含版本控制信息;
.line : 该节中包含调试信息,包括哪些调试符号的行号,为程序指令码与源文件的行号建立联系;
.note : 该节中包含注释;
.relX/.relaX:这两个节中包含重定位信息;如果该节被包含在某个可装载的段中,则该节的属性中应设置SHF_ALLOC标志位;X表示那些需要重定位的节的名字;比如:.text节的重定位节的名字将是.rel.text或.rela.text;
5.4.3 程序头
HIT-ICS大作业_第17张图片
由上图可以看出该程序头表列出了10个段,这些组成了最终在内存中执行的程序。
其还提供了各段在虚拟地址空间和物理地址空间的位置、大小、标志、访问授权和对齐方面的信息。还指定了一个类型来更精确的描述段。
PHDR保存程序头表。
INTERP指定在程序已经从可执行映射到内存之后,必须调用解释器。在这里解释器并不意味着二进制文件的内存必须由另一个程序解释。它指的是这样的一个程序:通过链接其他库,来满足未解决的引用。
通常/lib/ld-linux-so.2、/lib/ld-linux-ia-64.so.2等库,用于在虚拟地址空间中插入程序运行所需的动态库。对几乎所有的程序来说,可能c标准库都是必须映射的,还需要添加各种哭包括,GTK、数学库、libjpeg等。
LOAD表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等。
DYNAMIC段保存了其他动态链接器(即,INTERP中指定的解释器)使用的信息。
NOTE保存了专有信息。
虚拟地址空间的各个段,填充了来自ELF文件中特定段的数据。
5.4.4 符号表
HIT-ICS大作业_第18张图片
HIT-ICS大作业_第19张图片
与链接前的文件类型,只不过是符号表得到了扩展。
5.4 hello的虚拟地址空间
5.4.1.PDHR:起始位置 0x400040 大小:0x230 从 0x400040 到 0x400270
HIT-ICS大作业_第20张图片
5.4.2 .INTERP:起始位置 0x400270 大小:0x1c 从 0x400270 到 0x40028c
在这里插入图片描述
个段内保存的是动态链接库的位置,在后面edb也翻译出了这个语句对应的ASCII码。
5.4.3 .LOAD:起始位置 0x400000 大小:0x530 从 0x400000 到 0x400530
前面的截图加上
HIT-ICS大作业_第21张图片
5.4.4 .LOAD:起始位置 0x401000 大小:0x26d 从 0x401000 到 0x40126d
HIT-ICS大作业_第22张图片
5.4.5 .LOAD:起始位置 0x403e00 大小:0x260 从 0x403e00 到 0x404060
HIT-ICS大作业_第23张图片
5.4.6 .DYNAMIC:起始位置 0x403e10 大小:0x1e0 从 0x403e10 到 0x403ff0
HIT-ICS大作业_第24张图片
5.4.7 .NOTE:起始位置 0x40028c 大小:0x20 从 0x40028c 到 0x4002ac
在这里插入图片描述
5.4.8 .GNU_STACK:起始位置 0x0 大小:0x0
5.4.9 .GNU_STACK:起始位置 0x403e00 大小:0x200 从 0x403e00 到 0x404000
HIT-ICS大作业_第25张图片
5.5 链接的重定位过程分析

objdump -d -r hello
HIT-ICS大作业_第26张图片
HIT-ICS大作业_第27张图片
Hello比hello.o多出来很多节,如.init和.plt。其中plt节是在链接动态库的时候使用的,init节是为了程序初始化需要执行的代码
发现hello.o从.text节开始也就是0x0,而hello从.init节开始也就是0x401000。
而且hello多出来很多被调用的函数如printf、sleep、getchar、exit。在hello中调用函数都是使用call+,而在hello.o是直接使用call+<函数名> 的方法来直接调用的。
重定位的实现依靠.rodata这个段,这个段保留重定位所需的信息,叫做重定位条目。链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(如printf中的两个字符串,在hello.o的反汇编文件中对printf参数字符串的引用使用全 0 替代,在 hello 中则使用确定地址),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
5.6 hello的执行流程
程序名称 程序地址
ld-2.29.so!_dl_start 0x7f7955504030
ld-2.29.so!_dl_init 0x7f79555129e0
libc-2.29.so!__libc_start_main 0x7f795550d350
hello!printf@plt 0x00000000004011cb
hello!atoi@plt 0x00000000004011de
hello!sleep@plt 0x00000000004011e5
hello!getchar@plt 0x00000000004011f4
libc-2.29.so!exit 0x7f 7ae3297800
5.7 Hello的动态链接分析
GOT表:
概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。

PLT表:
过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目
当main()函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
ELF 文件对调用动态库 中的函数采用了所谓的"延迟绑定"(lazy binding)策略, 只有当该函数在其第一次被 调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地 方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态 库和可执行程序中的相应代码都不需要进行了。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
在这里插入图片描述
在dl_init调用之后,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
在dl_init调用之后,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
在这里插入图片描述
5.8 本章小结
本章阐述了链接的概念与作用。分析了 hello的ELF格式、 虚拟地址空间的分配、重定位过程、执行流程和动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1概念:进程是正在运行的程序的实例。
6.1.2作用:进程的概念为我们提供这样一种假象,就好像我们的程序是系统中当 前运行的唯一程序一样,我们的程序好像是独占地使用处理器和内存。
6.2 简述壳Shell-bash的作用与处理流程
Shell 是一个命令行解释器,以用户态方式运行的终端进程。为用户提供了一个向 Linux 内核发送请求以便运行程序的界面系统级程序,其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
在Shell中输入./hello 学号 姓名后,Shell通过调用fork 函数创建一个新的运行的子进程,也就是Hello程序。Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Sehll和Hello进程之间最大的区别在于它们有不同的PID。
进程图:
HIT-ICS大作业_第28张图片
6.4 Hello的execve过程
execve 函数在当前进程上下文加载并运行一个新程序。
在这里插入图片描述
execve 函数加载并运行可执行目标文件hello, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到hello, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。

6.5 Hello的进程执行
hello 在刚开始运行时内核为其保存一个上下文,进程在用户状态下运行。如果没有异常或中断信号的产生,hello 将继续正常地执行。如果有异常或系统中断,那么内核控制转移到内核模式并完成上下文切换,将控制传递给其他进程。当 hello 运行到 sleep时,hello 显式地请求休眠,并发生上下文切换,控制转移给另一个进程,此时计时器开始计时,当计时器到达argv[3]时,它会产生一个中断信号,中断当前正在进行的进程,进行上下文切换,恢复 hello 在休眠前的上下文信息,控制权回到 hello 继续执行。当循环结束后,hello 调用 getchar 函数,之前 hello 运行在用户模式下,在调用 getchar 时进入内核模式,内核中的陷阱处理程序请求来自I/O传输,并执行上下文切换,并把控制转移给其他进程。当完成键盘缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程切换回 hello 进程,然后 hello执行 return,进程终止。
6.6 hello的异常与信号处理
可能出现的异常种类:中断 故障(缺页故障,调用缺页异常处理函数即可) 可能出现的信号:SIGINT SIGSTP
正常执行:
HIT-ICS大作业_第29张图片
Ctr-Z: 当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理程序将hello进程挂起,放到后台:

  1. ps看到hello进程并没有被回收
  2. jobs找到hello的job号,
  3. fg 将hello进程调到前台执行
  4. kill杀死进程
    HIT-ICS大作业_第30张图片
    HIT-ICS大作业_第31张图片
    HIT-ICS大作业_第32张图片
    Ctr-C: 当按下ctrl-c之后,shell父进程收到SIGINT信号,由信号处理函数结束hello,并回收hello进程。
    在这里插入图片描述
    乱按
    HIT-ICS大作业_第33张图片
    6.7本章小结
    本章介绍了进程的概念和作用。讲述了shell的基本操作以及各种内核信号和命令,还介绍了shell是如何fork新建子进程、execve如何执行进程、异常控制流和信号机制。
    第7章 hello的存储管理
    7.1 hello的存储器地址空间
    逻辑地址:是指由cpu产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。
    线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。也是hello里面的虚拟内存地址。
    虚拟地址:hello里面的虚拟内存地址,包括 VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB 索引)、TLBT(TLB 标记)。例如main函数起始地址0x401172
    物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。是hello在运行时虚拟内存地址对应的物理地址。
    7.2 Intel逻辑地址到线性地址的变换-段式管理
    一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。索引号,这里可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
    首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了。
    7.3 Hello的线性地址到物理地址的变换-页式管理
    通过页表实现线性地址到物理地址到转换
    HIT-ICS大作业_第34张图片
    VPN是虚拟页号,VPO是虚拟页偏移量。VPO和PPO一样。同过页表的对应关系由一个VPN得到一个PPN。PPN加上PPO就得到了物理地址。
    7.4 TLB与四级页表支持下的VA到PA的变换
    虚拟地址被划分成 4 个 VPN 和 1 个 VPO。 每一个片被作用到一个页表的偏移量。VPN 1提供到L1 PTE偏移量,这个PTE包含L2页表的基地址。以此类推。第四级页表中的每个 PTE 包含某个物理页面的 PPN。将得到的 PPN 和虚拟地址中的 VPO 联起来,就得到相应的物理地址。
    HIT-ICS大作业_第35张图片
    7.5 三级Cache支持下的物理内存访问
    通过L1 TLB和VA得到PA,若L1 TLB缓存不命中则通过多级页表中找到目标页表然后选择牺牲块后替换。得到的PA传给L1 d-cache,通过先查CI即组号,再查CT即标志,和有效位确定物理块,然后通过CO得到目标数据。若是不命中则把PA传给下一级的cache重复过程,找到块后选择牺牲块再替换。若在主存里也不命中,则发生却页终端,执行异常处理子程序,把目标页放在主存里后,再取块。
    HIT-ICS大作业_第36张图片
    7.6 hello进程fork时的内存映射
    当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
    7.7 hello进程execve时的内存映射
    删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
    映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
    映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
    设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
    HIT-ICS大作业_第37张图片
    7.8 缺页故障与缺页中断处理
    下图展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。
    HIT-ICS大作业_第38张图片
    HIT-ICS大作业_第39张图片
    接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令。
    缺页处理程序不是直接就替换,它会经过一系列的步骤:
    1.虚拟地址是合法的吗?如果不合法,它就会触发一个段错误
    2.试图进行的内存访问是否合法?意思就是进程是否有读,写或者执行这个区域的权限
    若都合法则就选择一个页面牺牲然后换入新的页面并更新到页表。若不合法则将控制传给内核调用abort结束。
    HIT-ICS大作业_第40张图片
    7.9动态存储分配管理
    7.9.1动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
    分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
    1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
    2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
    7.9.2带边界标签的隐式空闲链表分配器原理
    对于带边界标签的隐式空闲链表分配器,一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
    头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。
    我们将对组织为一个连续的已分配块和空闲块的序列,这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。注意:此时我们需要某种特殊标记的结束块,可以是一个设置了已分配位而大小为零的终止头部。
    Knuth提出了一种边界标记技术,允许在常数时间内进行对前面快的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
    7.9.3显示空间链表的基本原理
    因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
    显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。
    使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
    一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
    7.9.4分离的空闲链表
    分离存储,是一种流行的减少分配时间的方法。一般思路是将所有可能的块大小分成一些等价类/大小类。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。有两种基本方法:
    1.简单分离存储:每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。如果链表非空:分配其中第一块的全部;如果链表为空:分配器向操作系统请求一个固定大小的额外存储器片,将这个片分成大小相等的块,并且连接起来成为新的空闲链表。优点:时间快,开销小;缺点:容易造成内部、外部碎片
    2.分离适配:每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。这种方法快速并且对存储器使用很有效率。
    3.伙伴系统——分离适配的特:其中每个大小类都是2的幂这样,给定地址和块的大小,很容易计算出它的伙伴的地址,也就是说:一个块的地址和它的伙伴的地址只有一位不同。优点:快速检索,快速合并。
    7.9.5 放置策略
    1.首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块
    2.下一次适配:从上一次搜索的结束位置开始搜索
    3.最佳适配:检索每个空闲块,选择适合所需请求大小的最小空闲块
    7.10本章小结
    本章主要介绍了程序的存储结构,通过段式管理在逻辑地址到虚拟地址,页式管理从虚拟地址到物理地址,缺页中断和缺页故障处理,内存映射,利用cache,TLB进行读取数据指令和动态内存分配。
    第8章 hello的IO管理
    8.1 Linux的IO设备管理方法
    设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
    8.2 简述Unix IO接口及其函数
    Unix I/O接口:
    1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
    2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
    3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
    4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
    5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
    Unix I/O函数:
    1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
    int open(char *filename, int flags, mode_t mode);
    open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。返回:若成功则为新文件描述符,若出错为-1。
    2.进程通过调用close 函数关闭一个打开的文件。
    int close(int fd);返回:若成功则为0, 若出错则为-1。
    3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
    ssize_t read(int fd, void *buf, size_t n);
    read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
    ssize_t write(int fd, const void *buf, size_t n);
    write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。返回:若成功则为写的字节数,若出错则为-1。
    8.3 printf的实现分析
    参考https://www.cnblogs.com/pianist/p/3315801.html
    printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。
    HIT-ICS大作业_第41张图片
    其调用了两个外部函数,一个是vsprintf,还有一个是write。vsprintf函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数是将buf中的i个元素写到终端的函数。
    Printf运行过程:
    从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
    字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
    显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
    8.4 getchar的实现分析
    getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
    异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
    getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
    8.5本章小结
    本章节讲述了linux的I/O设备管理机制,Unix IO接口及其函数,简单分析了printf和getchar函数的实现方法以及操作过程。
    结论
    用计算机系统的语言,逐条总结hello所经历的过程。
    Hello.c先被编写出来,然后通过汇编器(GNU)由cpp完成了预处理变成了hello.i,ccl完成了编译变成了hello.s,as完成了汇编变成了hello.o,ld完成了链接变成了hello。这样完全可执行文件hello就诞生了。
    在shell里面通过./hello加载并运行hello。Shell先fork子进程,在子进程里通过execve加载hello,完成内存映射,进入__start,运行hello。通过缺页中断与异常处理把hello的内容复制到内存上。开始执行一条条机器指令。如果中途收到信号,会进入信号处理子程序,或停止,挂起。最后hello运行结束,由shell回收hello。
    计算机系统是一个复杂的系统,想要全部搞清楚,弄明白需要很多的时间与精力。
    你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。随着对计算机的算力要求越来越大,并行与乱序必定会成为未来计算机的一大特点。
    附件
    文件 作用
    hello.i hello.c 预处理之后的文本文件
    hello.s hello.i 编译之后的文本文件
    hello.o hello.s 汇编之后的二进制文件
    hello hello.o 链接之后的二进制文件
    参考文献
    [1] Randal E.Bryant,David R.O’Hallaron.深入理解计算机系统(原书第 3 版)[M]. 机械工业出版社:北京,2018
    [2] http://bdxnote.blog.163.com/blog/static/844423520154502440229/
    [3] https://blog.csdn.net/ylcangel/article/details/18145155
    [4] https://blog.csdn.net/ylcangel/article/details/18145199
    [5] https://www.cnblogs.com/RogerLi/p/10226027.html
    [6] https://blog.csdn.net/weixin_42781851/article/details/89289049

你可能感兴趣的:(HIT-ICS大作业)