csapp作业

摘 要
本文通过跟踪hello程序的一生,对计算机系统课程所学进行了简要梳理,对程序被创建,到在系统上运行,到输出信息,对程序的历程进行了详尽的分析,加深了对计算机系统的理解。

关键词:hello程序;linux;计算机系统;

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

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 7 -
2.3 HELLO的预处理结果解析 - 8 -
2.4 本章小结 - 16 -
第3章 编译 - 17 -
3.1 编译的概念与作用 - 17 -
3.2 在UBUNTU下编译的命令 - 18 -
3.3 HELLO的编译结果解析 - 18 -
3.4 本章小结 - 25 -
第4章 汇编 - 26 -
4.1 汇编的概念与作用 - 26 -
4.2 在UBUNTU下汇编的命令 - 26 -
4.3 可重定位目标ELF格式 - 26 -
4.4 HELLO.O的结果解析 - 30 -
4.5 本章小结 - 33 -
第5章 链接 - 34 -
5.1 链接的概念与作用 - 34 -
5.2 在UBUNTU下链接的命令 - 34 -
5.3 可执行目标文件HELLO的格式 - 35 -
5.4 HELLO的虚拟地址空间 - 41 -
5.5 链接的重定位过程分析 - 41 -
5.6 HELLO的执行流程 - 44 -
5.7 HELLO的动态链接分析 - 44 -
5.8 本章小结 - 44 -
第6章 HELLO进程管理 - 45 -
6.1 进程的概念与作用 - 45 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 45 -
6.3 HELLO的FORK进程创建过程 - 45 -
6.4 HELLO的EXECVE过程 - 46 -
6.5 HELLO的进程执行 - 47 -
6.6 HELLO的异常与信号处理 - 48 -
6.7本章小结 - 49 -
第7章 HELLO的存储管理 - 50 -
7.1 HELLO的存储器地址空间 - 50 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 50 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 51 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 52 -
7.5 三级CACHE支持下的物理内存访问 - 53 -
7.6 HELLO进程FORK时的内存映射 - 54 -
7.7 HELLO进程EXECVE时的内存映射 - 54 -
7.8 缺页故障与缺页中断处理 - 54 -
7.9动态存储分配管理 - 54 -
7.10本章小结 - 55 -
第8章 HELLO的IO管理 - 59 -
8.1 LINUX的IO设备管理方法 - 59 -
8.2 简述UNIX IO接口及其函数 - 59 -
8.3 PRINTF的实现分析 - 59 -
8.4 GETCHAR的实现分析 - 60 -
8.5本章小结 - 60 -
结论 - 60 -
附件 - 61 -
参考文献 - 62 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:
通过编写代码得到hello.c程序,在linux的Ubuntu的操作系统下,调用C预处理器ccl得到hello.i,然后调用汇编器as得到可重定位目标文件hello.o,然后通过链接器ld得到可执行目标文件hello在shell中键入./hello启动程序,系统为hellofork子进程,hello就变成了进程。
O2O:
OS的进程管理调用fork函数产生子进程,调用execve函数,并进行虚拟内存映射,并为运行的hello分配时间片以执行取指译码流水线等操作;OS的存储管理以及MMU解决VA到PA的转换,cache、TLB、页表等加速访问过程,IO管理与信号处理综合软硬件对信号等进行处理;程序结束时,shell回收hello进程,内核将其所有痕迹从系统中清除。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件工具:
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件工具:
Windows10 64 位;Vmware14.1.3;Ubuntu 18.04.1 LTS 64 位
开发与调试工具:
gcc,edb,readelf,gedit,objdump
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i 预处理得到的文件
hello.s 编译得到的文件
hello.o 汇编得到的文件
hello.asm hello.o反汇编得到的文本文件
hello.elf hello.o的elf文件
hello 链接得到的可执行目标文件
hello.asm2 hello反汇编得到文本文件

1.4 本章小结
本章介绍了hello.c的P2P和O2O,然后介绍了本次实验的环境和工具,最后展示的所有的中间文件。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能, 它由预处理程序负责完成。当对一个源文件进行编译时, 系统把自动引用预处理程序对源程序中的预处理部分作处理, 处理完毕自动进入对源程序的编译。
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
预处理的作用:
预处理指令包括条件编译、源文件包含、宏替换、行控制、抛错、杂注和空指令。
1、 条件编译
条件编译的功能是根据条件有选择的保留或者放弃源文件中的内容。常见的条件包含#if、#ifdef、#ifndef指令开始,以#endif结束。用#undef定义的标识符取消定义。
2、 源文件包含
源文件包含指令的功能是搜索指定的文件,并将它的内容包含进来,放在当前所在的位置,源文件包含有两种,包含系统文件和用户自定义文件。
#include <stdio.h>
让C编译器去系统目录下查找相关文件
#include”test.c”
3、 宏替换
宏的作用是把一个标识符指定为其他一些成为替换列表的预处理记号,当这些标识符出现在后面的文本中时,将用对应的预处理记号把它替换掉,宏的本质是替换。
宏的定义分为两种方式:有参和无参。
无参数的宏(对象式宏定义)
#difine MAX 100
有参数的宏(函数式定义)
#define MAX(a,b) ((a)>(b)?(a):(b))
4、 行控制
行控制指令以”#”和”line”引导,后面是行号和可选的字面串。它用于改变预定义宏”LINE”的值,如果后面的字面串存在,则改变”FILE”的值。
5、 抛错
抛错指令是以”#”和”error”引导,抛错指令用于在预处理期间发出一个诊断信息,在停止转换。抛错是人为的动作。
6、 杂注
杂注指令用于向C实现传递额外的信息(编译选项),对程序的某些方面进行控制。
杂注指令以”#”开始,跟着”pragma”,后面是其他预处理记号,即所谓的选项。下面这个杂注用于指示C实现将结构成员对齐到字节边界。

pragma pack(1)

7、 空指令
空指令只有一个”#”,自称一行,空指令的使用没有效果。
2.2在Ubuntu下预处理的命令
预处理命令:
gcc -E hello.c -o hello.i

gcc -E hello.c -o hello.i

hello.i
2.3 Hello的预处理结果解析
查看hello.i文件,可以看到hello.i的篇幅较hello.c有了大幅度增加,有3000+行。这是因为在预处理的过程中,实现了条件编译、源文件包含、宏替换等工作。
对于源文件包含:
hello.c文件中的#include <stdio.h>在hello.i中进行了如图2.3.1和2.3.2所示的展开。

图2.3.1

图2.3.2
hello.c文件中的#include <unistd.h>在hello.i中进行了如图2.3.3和2.3.4所示的展开。

图2.3.3

图2.3.4
hello.c文件中的#include <stdlib.h>在hello.i中进行了如图2.3.5和2.3.6所示的展开。

图2.3.5

图2.3.6
对于宏替换和条件编译,在系统头文件中,如图2.3.7:

图2.3.7
对于程序主体:
预处理删除了我们的注释,其他并无差别,如图2.3.8

图2.3.8
2.4 本章小结

(以下格式自行编排,编辑时删除)
本章主要介绍了预处理的概念和作用、Ubuntu下预处理的指令和Hello的预处理解析,分析了预处理的具体过程。预处理主要由预处理器完成,进行文件包含、条件编译、宏替换等工作。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:
1、利用编译程序从源语言编写的源程序产生目标程序的过程。
2、用编译程序产生目标程序的动作。编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
编译程序把一个源程序翻译成目标程序的工作过程分为四个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化。
编译的作用:
一、词法分析
词法分析是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。执行词法分析的程序称为词法分析程序或扫描器。
二、语法分析
编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。编译程序的语法规则可用上下文无关文法来刻画。
三、中间代码
中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。中间语言有多种形式,常见的有逆波兰记号、四元式、三元式和树。
四、代码优化
代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。
有两类优化:一类是对语法分析后的中间代码进行优化,它不依赖于具体的计算机;另一类是在生成目标代码时进行的,它在很大程度上依赖于具体的计算机。对于前一类优化,根据它所涉及的程序范围可分为局部优化、循环优化和全局优化三个不同的级别。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

(以下格式自行编排,编辑时删除)

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。

3.3.1hello.s文件内容
.file 源文件
.text 代码段
.globl 全局变量
.data 数据段
.align 存放地址的对齐方式
.type 声明是函数类型还是对象类型
.size 大小
.long long类型
.section.rodata rodata节
.string string类型
3.3.2数据
hello.c中数据包括:整型、数组、字符
3.3.2.1整型
int sleepsecs=2.5
在hello.s中,sleepsecs是已经初始化的全局变量,在.data节中,4字节对齐,是object类型,大小是4字节,从int变成了long类型,值是2,如图3.2.2.1.1

图3.2.2.1.1
int argc
main函数的第一个参数,指明argv[]数组中的数量。
保存在%edi(第一个参数)中并传递给%rbp-20。
如下图3.2.2.1.2。

图3.2.2.1.2
int i
局部变量保存在栈中,i保存在了-4(%rbp)中,如图3.2.2.1.3

图3.2.2.1.3
3.3.2.2数组
char *argv[]
main函数的第二个参数,是指针数组,保存着终端输入的命令行。
argv[0] 可执行目标文件的名字
argv[1] 命令行参数“学号”
argv[2] 命令行参数“姓名”
保存在%rsi中,并被传递给%rbp-32。
argv[1],argv[2]分别保存在%rsi、%rdx中。如图3.2.2.2.1。

图3.2.2.2.1
3.3.2.3字符
“Usage: Hello 学号 姓名!\n”
“Hello %s %s\n”
在.rodata节中,声明两个string类型的字符串。如下图3.2.2.3.1。

图3.2.2.3.1
3.3.4赋值
int sleepsecs=2.5
在.rodata节中声明值为2的long类型数据,如图3.2.3.1

图3.2.3.1
i=0
通过立即数赋值,把0赋给%rbp-4,如图3.2.3.2

图3.2.3.2
i++
通过add指令,把%rbp-4加一,如图3.2.3.3

图3.2.3.3
3.3.4类型转换
int sleepsecs = 2.5
将浮点数转换为int整数2(double转换int),向偶数舍入,2.5舍入到2,如图3.3.4.1

图3.3.4.1
3.3.5算术操作
i++
如3.3.4中的i++部分
3.3.6关系操作
i<0
计算-4(%rbp)-9,设置条件码,随后jle根据这些条件码,进行跳转。如图3.3.6.1

图3.3.6.1
argv!=3
计算-20(%rbp)-3,设置条件码,随后用je根据这些条件码,进行跳转。如图3.3.6.2

图3.3.6.2
3.3.7数组/指针/结构操作
如3.3.2.2
3.3.8控制转移

计算-20(%rbp)-3,设置条件码,随后用je根据这些条件码,跳转到.L2段,如图3.3.8.1。

图3.3.8.1

计算-4(%rbp)-9,设置条件码,随后jle根据这些条件码,跳转到.L4段,如图3.3.8.2

图3.3.8.2
3.3.9函数操作
3.3.9.1main函数
第一个参数是argv,指明argv[]数组中非空指针的数量。由%edi保存并传递给%rbp-20。
第二个参数是argc[],argv[]是一个指针数组,每个指针都指向一个参数字符串。argv[0]是可执行目标文件的名字,argv[1]、argv[2]指向命令行参数“学号”、“姓名”。

返回值在%eax中,设置为0。

3.3.9.2printf函数
用call调用

printf函数被优化成为puts函数,首先将%rdi赋值给字符串“Usage:Hello 学号 姓名!\n”的首地址,然后调用puts函数,如图3.3.9.2.1。

图3.3.9.2.1

设置%rsi为argv[1],%rdx为argv[2],然后调用printf函数,如图3.3.9.2.2。

图3.3.9.2.2
3.3.9.3sleep函数
用call调用
参数是%edi,如图3.3.9.3.1

图3.3.9.3.1
3.3.9.4getchar函数
用call调用
如图3.3.9.4.1

图3.3.9.4.1
3.3.9.5exit函数
用call调用
参数是%edi,如图3.3.9.3.1

图3.3.9.3.1
3.4 本章小结
本章介绍了编译的概念和作用,Ubuntu下编译的指令,最后对hello.s文件中的数据(整型、字符、数组)和操作(赋值、类型转换、算术、数组、函数)进行了分析。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的概念:
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
汇编的作用:
将高级语言转化为机器可直接识别执行的代码文件。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o

gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF格式的可执行目标文件的各类信息:
ELF头:字大小、字节序、文件类型、机器类型、节头表位置、条目大小、数量等。
程序头表:页面大小、虚拟地址内存段、段大小
.text:已编译程序的代码
.rodata:只读数据:printf的格式串、开关语句跳转表……
.data:已初始化全局和静态变量
.bss:未初始化静态变量、初始化为0的全局和静态变量
.symtab:函数和全局/静态变量名,节名称和位置
.rel.txt:可重定位代码
.rel.data:可重定位数据
.debug:调试符号表:符号调试的信息
节头表:每个节的在文件中的偏移量、大小等

4.3.1读取hello.o
readelf -a hello.o > hello.elf,用文本文件输出信息。

readelf -a hello.o >. hello.elf
4.3.2ELF头
ELF头以一个16字节的序列开始,这个序列描述了系统的字的大小和字节顺序。剩下的部分包括帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小,程序头的大小、目标文件的类型、机器类型,节头部表的文件偏移,节头部表中条目的大小和数量,如下图。

ELF头
4.3.3节头部表
包括节名称、类型、地址、偏移量、大小等。

节头部表
4.3.4重定位节
包括偏移量、信息、类型、符号值、符名称和加数。
.rela.text中包含8条重定位信息,分别对应第一个字符串、puts函数、exit函数、第二个字符串、printf函数、sleepsecs、sleep函数、getchar函数。
针对重定位分析,以第一个字符串为例:
r.offset = 0x1c,r.sympol=.rodata,r.type=R_X86_X64_PC32,r.addend=-4(如下图所示)
首先,连接器计算处引用的运行时的地址
refaddr = ADDR(s) +r.offset = ADDR(s) + 0x18。
然后,更新该引用,使得它在运行时指向真正的内容
*refptr = (unsigned)(ADDR(r.sympol) + r.addend - refaddr) = (unsigned) (ADDR(r.sympol) + (-4) - refaddr)

最后,在得到的可执行目标文件中,我们便可以得到正确的引用地址,即完成对第一个重定位条目的重定位计算。

重定位节
4.3.5符号表
Name:字符中表中的字节偏移,指向符号的以null结尾的字符串名字。
Value:距定义目标的节的起始位置的偏移。对可执行目标文件来说,该值是一个据对运行时的地址。
Size:目标的大小。
Type:数据或者函数。
Bind:本地或者全局字段。

符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
objdump –d -r hello.o > hello.asm:

4.4.1对照分析
4.4.1.1分支转移
hello.s:
使用段名称进行跳转,如图4.4.1.1.1

图4.4.1.1.1
hello.o:
使用地址进行跳转,如图4.4.1.1.2

图4.4.1.1.2
4.4.1.2函数调用
hello.s:
call+函数名称,如图4.4.1.2.1

图4.4.1.2.1
hello.o:
call+下一条指令,因为此时地址还不确定,所以call指令将相对地址设置为0,然后在.rela.text节为其添加重定位条目,等待下一步链接,如图4.4.1.2.2。

图4.4.1.2.2
4.4.1.3全局变量
hello.s:
段地址+%rip,如图4.4.1.3.1

图4.4.1.3.1
hello.o:
0+%rip,也需要重定位,在.rela.text节为其添加重定位条目,如图4.4.1.3.2

图4.4.1.3.2
4.4.2机器语言构成
4.4.2.1机器语言构成
机器语言是二进制机器指令的集合。基本格式:操作码字段和地址码字段,其中操作码指明指令的功能,地址码指明操作数或者操作数的地址。
4.4.2.2与汇编语言的映射关系
一一对应,如图4.4.2.2.1

图4.4.2.2.1
4.4.2.3操作数
对于立即数,反汇编是十六进制,hello.s是十进制。

对于寄存器,二者相同。
对于内存引用,反汇编基址加偏移量寻址,hello.s是伪指令。

4.5 本章小结
本章对汇编的概念和作用、Ubuntu下汇编的命令进行了介绍。还对可重定位目标elf格式进行了重点介绍,还对hello.o的反汇编文件和hello.s进行了对比。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
链接的概念:
链接是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载(复制)到内存并执行。
链接的作用:
链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
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

链接生成hello可执行目标文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
输入命令readelf -a hello
5.3.1ELF头

ELF头
5.3.2节头部表,声明各节的大小、地址、偏移量等。

节头部表
5.3.3程序头

程序头表
5.3.4段节

段节
5.3.5Dynamic section

Dynamic section
5.3.6重定位节

重定位节
5.3.7符号表

符号表
5.3.8others

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.4.1在edb中找到并加载hello可执行文件
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
objdump -d -r hello > hello.asm2

5.5.1分析hello与hello.o的不同
5.5.1.1hello.asm2比hello.asm多了很多文件节,如图5.5.1.1.1和5.5.1.1.2

图5.5.1.1.1

图5.5.1.1.2
5.5.1.2hello.asm是相对偏移地址,hello.asm2是虚拟地址,如图5.5.1.2.1和图5.5.1.2.2

图5.5.1.2.1

图5.5.1.2.2
5.5.1.3hello.asm2增加了很多共享库函数,如图5.5.1.3.1

图5.5.1.3.1
5.5.2链接的重定位过程说明
合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位,根据.rel_data和.rel_text节中保存的重定位信息修改.text节和.data节中对每个符号的引用。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
该章介绍了链接的概念和作用、在Ubuntu下链接的命令行,对helllo的elf格式进行了分析,对hello和hello.o的反汇编文件进行了对比,对hello的执行流程和动态链接进行了分析。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
一个执行中程序是实例,系统中的每个程序都运行在某个进程的上下文中。
进程的作用:
在现代系统上运行一个程序时,我们会得到一个假象,我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash的作用:
Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作。而NU组织发现sh是比较好用的又进一步开发Borne Again Shell,简称bash,它是Linux系统中默认的shell程序。
shell-bash的处理流程:
1、解析输入的命令行
2、如果是内置命令行,解释命令
3、如果是可执行文件,在一个新的紫禁城的上下文中execve并运行文件。
4、运行过程中,shell处理异常。
5、运行终止后,shell回收子进程。
6.3 Hello的fork进程创建过程
fork函数:父进程调用创建子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚
拟地址空间相同的一份副本,包括代码和数据段、堆、共享库及用户栈。子进程
还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程打开的任何文件,父进程和创建的子进程最大的区别在于它们有不同的 PID。fork 函数被调用一次会返回两次,一次是在父进程中,一次是在新创建的子进程中。在父进程中,fork返回子进程的 PID,在子进程中fork返回0
使用ctrz+Z挂起子进程,用ps查看进程信息,如图6.3.1。

图6.3.1
6.4 Hello的execve过程

fork创建完子进程后,execve函数在子进程中加载hello,调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数main,当main开始执行时,用户栈的组织结构如图6.4.1所示。让我们从栈底往栈顶看,首先是参数和环境字符串,然后是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量environ指向这些指针中的第一个envp[0]。之后是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。栈顶是系统启动函数libc_start_main的栈帧。

图6.4.1
hello子进程通过execve系统调用加载器。
加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。
通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。
最后加载器跳到_start地址,它最终调用hello的main 函数。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程上下文信息:内核为每个进程维持一个上下文。上下文包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等。
进程调度的过程(如图6.5.1):
开始时,运行hello程序时处于用户模式,直到调用sleep让hello进入内核模式,内核模式中的陷阱处理程序让调用进程休眠。
进程休眠时间很长,内核进行hello进程到进程B的上下文切换,切换的前一部分,内核代表进程 hello 在内核模式下执行指令,然后在后半部分,代表进程 B
执行指令。在切换之后,内核代表进程 B 在用户模式下执行指令。之后,进程B在用户模式下运行,直到hello进程休眠结束,执行一个从进程B到进程hello的上下文切换,将控制返回给进程 hello 中紧随系统调用 sleep 之后的那条指令。进程 hello 继续运行,直到下一次异常发生,其他 9 次调用 sleep 以此类推。直到进程终止被 shell回收。

图6.5.1
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常的分类:中断、陷阱、故障、终止。
hello执行过程中会出现陷阱和终止。
随便按键盘,回车。

键入Ctrl+C,内核会发送一个 SIGINT 信号给前台进程 组中的每个进程,终止前台作业。然后 shell 会回收该进程。

键入 Ctrl+Z,会发送一个 SIGTSTP 信号到前台进程组中的每个进程,挂起前台作业,即挂起进程 hello。使用 ps 命令,发现进程还在。

使用 jobs 命令查看前台进程

使用 pstree 查看 hello 进程所在位置

使用 fg 命令,让 hello 程序在前台运行

使用 kill 命令杀死 hello进程,shell 回收该进程

6.7本章小结
本章介绍了进程的概念及作用、shell的作用及处理流程、execve执行、进程执行以及异常与信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:
程序hello产生的和段相关的偏移地址部分。
线性地址:
是逻辑地址到物理地址变换之间的中间层。程序hello的代码产生逻辑地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再变换产生一个物理地址。若没有,那么线性地址就是物理地址。
虚拟地址:
就是hello的逻辑地址。
物理地址:
目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终地址。如果启用了分页基址,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下:逻辑地址CS:EA到物理地址CS*16+EA
保护模式下:以段描述符作为下表,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。
段选择符各字段含义
15 14 32 10
索引 TI RPL

高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置
TI=0,选择全局描述符表(GDT)
TI=1,选择局部描述符表(LDT)
RPL=00,为第0级,位于最高级的内核态,
RPL=11,为第3级,位于最低级的用户态,第0级高于第3级。
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址

7.3 Hello的线性地址到物理地址的变换-页式管理
假设系统仅用一个单独的页表进行翻译。CPU 中的一个控制寄存器,页表基址寄存器指向当前页表。48 位的虚拟地址包括12 位的虚拟页偏移量 VPO和36 位的虚拟页号 VPN。MMU利用 VPN 选择适当的页表条目 PTE。例如,VPN0 选择 PTE0,VPN1 选择 PTE1,以此类推。将页表条目中物理页号 PPN(40 位)和虚拟地址中的 VPO(12 位)串联起来,就得到相应的物理地址(52 位)。如图7.3.1

图7.3.1
实际上,我们采取四级页表结构。如图7.3.2。48 位虚拟地址被翻译成了 52 位物理地址。每一个页表有 512 项,每一个页表项有 8个字节,每个页表都是 4KB 大小。一级页表每个页表项映射 512GB 区域,一级页表总计映射 256TB区域;二级页表有 512个,每个页表映射 512GB 区域;三级页表有 256K个,每
个页表映射 1GB 区域;四级页表有 128MB 个,每个页表映射 2MB 区域。四级页表每个页表项映射 4KB 区域

图7.3.2
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保留着一个由单个 PTE 组成的块。在 Core i7 中,TLB 索引有 4 位,TLB 标记有 32 位。故 TLB 有 16 组,并且每组 4个条目。
VA到PA转换(如图7.4.1):
1、CPU产生一个48位的虚拟地址
2、MMU从虚拟地址得到36位的VPN,并检查PTE。TLE从VPN中抽取TLB索引和TLB标记,TLB索引选择一个组,TLB标记匹配标记位,找到匹配。命中将PPN返回给MMU.
3、如果TLB不命中,MMU需要从主存中得到PTE。36位VPN被划分4个片,每个片被作用到一个页表的偏移量。CR3控制寄存器指向第一级页表L1 PTE的起始位置。VPN1提供一个指向L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个L2 PTE的偏移量,以此类推,直到找到L4中的PTE。
4、如果得到PTE是无效的,就产生一个缺页,内核调入页面,再次运行刚才的指令。
5、最后MMU得到PPN和VPO,形成物理地址。

图7.4.1
7.5 三级Cache支持下的物理内存访问
1、得到物理地址VA后,首先使用物理地址的CI进行组索引,对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回。
2、若没有相匹配或者标志位为0,就miss。那么cache向下一级cache,这里向二级cache或者三级cache中查找,然后写入cache
3、再更新cache的之后,判断是否有空闲块。如果有空闲块,就写入,没有就驱逐(LRU策略)。

7.6 hello进程fork时的内存映射
当fork被当前进程调用时,内核为新进程创建数据结构,分配PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构(vm_area_struct)和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存区域,成为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配块显式地保留为供程序使用。空闲块可用来分配。空闲块保持空闲,直到他显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器:要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及java之类的高级语言就依赖垃圾收集来释放已分配的块。
对于带边界标签的隐式空闲链表分配器来说,一个块是由一个字的头部、有效载荷以及可能的一些额外的填充组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的,如下图。

如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零。因此,我只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低为(已分配位)来指明这个块是已分配的还是空闲的。
例如,假设我们有一个已分配的块,大小为24(0x18)字节。那么它的头部是0x00000018|0x1=0x00000019
类似地,一个块大小为40(0x28)字节的空闲块有如下头部:
0x00000028|0x0=0x00000028
头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充,其大小可以是任意的。
假设块的格式如图9-35所示,我们可以将堆组织为一个连续的已分配块和空闲块的序列,如图9-36所示。

我们称这种结构为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着。分配块可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。注意,我们需要某种特殊标记的结束块,在这个示例中,就是设置了已分配位而大小为零的终止头部。
隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块,要求对空闲链表进行搜索,该搜索所需时间与堆中已分配块和空闲块的总数呈线性关系。
显式空闲链表:因为程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,如图9-48所示。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于我所选择的空闲链表中块的排序策略。
一种方法是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处。使用后进先出的顺序和首次适配的放置策略,分配器会先检查出最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。
另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都会小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比后进先出排序的首次适配有更高的内存利用率,接近最佳适配的利用率。

一般而言,显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部,这就导致了更大的最小快大小,也潜在地提高了内部碎片的程度。
7.10本章小结
本章介绍了hello的内存管理。分析了如何把虚拟地址翻译成物理地址,如何根据物理地址访问cache和内存,还介绍了动态内存分配。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的 I/O 设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O。(以下格式自行编排,编辑时删除)
8.2 简述Unix IO接口及其函数
接口
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.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了IO设备管理方法、Unix的接口及函数,对printf函数和getchar函数的实现做了分析。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello所经历的过程:
hello.c经过预编译,变成hello.i文本文件。
hello.i经过编译,变成hello.s汇编文件。
hello.s经过汇编,变成hello.o可重定位目标文件。
hello.o经过链接,变成hello可执行文件。
fork函数创建子进程,execve函数加载运行。
hello会调用函数,这些与IO有关。
hello最后被shell父进程回收。
感悟:
在学习计算机系统的过程中,对创造这些的人感到无限的敬佩和敬仰,让我觉得这些我连理解都觉得困难的东西,有人创造出来了,不仅是行业的伟大贡献,更是对于人类历史的伟大贡献。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 预处理得到的文件
hello.s 编译得到的文件
hello.o 汇编得到的文件
hello.asm hello.o反汇编得到的文本文件
hello.elf hello.o的elf文件
hello 链接得到的可执行目标文件
hello.asm2 hello反汇编得到文本文件

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深藏功名丿小志 程序预处理阶段,在做什么?[电子文献]: https://zhuanlan.zhihu.com/p/72515788:2019.07.06
[2] 陈英 陈朔鹰.编译原理.清华大学出版社,2009年(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值