2019哈工大计算机系统大作业-程序人生
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1181910201
班 级 1903002
学 生 李金宣
指 导 教 师 郑贵滨
主要是懒得粘图片了,哪位学弟学妹如果以后点进来想参考,直接看链接吧
https://www.kdocs.cn/l/cdo93qR0QlI9
[金山文档] CS大作业论文.docx
计算机科学与技术学院
2021年6月
摘 要
本文通过追踪hello小程序在Linux系统的一生,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件,并由操作系统进行进程管理、存储管理和I/O管理的全过程。以此将CSAPP课程中的内容进行全面地总结和梳理,加深对计算机系统的理解。
关键词:Hello程序;Ubuntu;linux;预处理;编译;汇编,链接;进程;shell;存储;虚拟内存;I/O
目 录
第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 本章小结 - 5 -
第3章 编译- 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编- 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接- 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理- 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理- 11 -
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 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理- 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论- 14 -
附件- 15 -
参考文献- 16 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
(1). P2P简介
Linux环境下,hello.c经过cpp的预处理得到中间文件hello.i;然后经ccl编译后得到汇编语言文件hello.s;在经由as汇编后得到可重定位目标文件hello.o;最后由ld链接后得到可执行目标文件hello。用户在 shell键入./hello启动程序,shell调用fork函数产生子进程,hello便成为了进程。
(2). O2O 简介
Shell为此子进程调用execve,并进行虚拟内存映射,CPU接着为运行的hello分配时间片以执行逻辑控制流;OS的储存管理以及MMU解决VA到PA的转换,cache、TLB、页表等加速访问过程,IO管理与信号处理综合软硬件对信号等进行处理;运行程序结束后,shell回收hello进程,内核将相关痕迹删除。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,gdb,vim,edb,readelf,HexEdi,objdump,ldd等
1.3 中间结果
|
中间结果文件 |
文件作用 |
|
hello.i |
hello.c预处理得到的文本文件 |
|
hello.s |
hello.i编译后的汇编文件 |
|
hello.o |
hello.s汇编得到的可重定位目标文件 |
|
hello |
链接得到的可执行目标文件 |
1.4 本章小结
本章简要介绍了hello.c的P2P与O2O,然后对本次实验环境、实验工具、中间结果进行初步列举,最后大致地简介了hello程序从hello.c到可执行目标文件hello的历程。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念
预处理是在编译之前进行的处理,一般指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程,预处理中会展开以#起始的行,试图解释为预处理指令。
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
预处理之后,28行的hello.c文件变成了3065行的文本文件。因为预处理器在预处理过程中实现了头文件的展开,宏替换和去注释并作条件编译。头文件里有大量的宏定义和条件编译语句存在,预处理阶段需要对这些语句进行相应的宏替换和条件编译处理。
预处理删除了原来的程序主体段的注释信息,预处理删除了我们的注释信息(图2)除了注释部分以及头文件部分,预编译文件与源文件差别不大。
图1 头文件展开部分截图
图2 程序主体段(删去了注释信息)
2.4 本章小结
本章主要介绍了预处理的相关概念和应用功能。分析hello.c预处理得到hello.i文本文件的过程,发现了预处理阶段的行为:头文件的展开、宏替换、去掉注释、条件编译。
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译就是将源语言经过词法分析、语法分析、语义分析以及经过一系列优化后生成汇编代码的过程。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。
编译的作用:
将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤。除了基本作用之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 hello.s文件
|
内容 |
含义 |
|
.file |
声明源文件 |
|
.text |
代码节 |
|
.global |
声明一个全局变量 |
|
.section .rodata |
只读代码段 |
|
.align |
指令或者数据的存放地址进行对齐的方式 |
|
.type |
声明符号是数据类型或函数类型 |
3.3.2 数据类型
- 全局变量
hello.s中的全局变量sleepsecs,初始化语句为int sleepsecs=2.5。.data 节存放已经初始化的全局和静态C变量,编译器将sleepsecs在.text代码段中声明为全局变量;然后在.data 段中,设置对齐方式(.align)为4字节对齐,设置类型(.type)为对象,设置大小(.size)为4字节,设置为long类型(.long),其值为2。如下图所示
- 字符串
程序中有两个字符串,”Usage: Hello 学号 姓名!\n”,与终端键入的储存在argc[]为地址的数组中的”Hello %s %s\n”这两个字符串都在只读数据段中,
这两个字符串是printf函数的参数。
- 局部变量
hello.s中的局部变量i,初始化语句为int i,局部变量运行时被保存在栈或是寄存器里。在hello.s文件中,编译器将i存储在栈空间-4(%rbp)中
- 数组
数组char *argv[]main函数的第二个形式参数,来源于终端输入的数据,argv是存放char指针的数组。argv数组中一个元素大小为8个字节,我们可以看到在hello.s中2次指令movq (%rax), %rdx与movq (%rax), %rax,是为了解析终端输入的命令参数。
图1.int argc[]数组传入函数 图2.int argc[]数组内容使用
- 立即数
直接在汇编代码中以$常数形式存在
3.3.3 赋值操作
源程序中的赋值操作int sleepsecs=2.5; i=0;i++;
- int sleepsecs = 2.5。sleepsecs为全局变量,直接在.data节中将sleepsecs 声明为值为2的long类型数据(隐式转换,编译器缺省)。
- i=0。在hello.s文件中通过汇编语句movl $0, -4(%rbp)将立即数赋值给局部变量int i。因为局部变量是int型,4个字节,因此使用字母l。如下图
- i++。在hello.s文件中是通过语句addl $1, -4(%rbp)实现,因为-4(%rbp)存储i的值,因此通过addl使每次循环+1。如下图
3.3.4 算术操作与逻辑操作
|
指令 |
效果 |
描述 |
|
1eaq S,D |
D←&S |
加载有效地址 |
|
INC D DEC D NEG D NOT D |
D←D十1 D←D-1 D← -D D←~D |
加1 减l 取负 取补 |
|
ADD s,D |
D←D+s |
加 |
|
SUB S,D |
D←D-S |
减 |
|
IMUL S,D |
D←D*S |
乘 |
|
XOR S,D |
D←D^S |
异或 |
|
OR S,D |
D←D|S |
或 |
|
AND S,D |
D←D&S |
与 |
|
SAL k,D |
D←D<<k |
左移 |
|
SHL k,D |
D←D<<k |
左移 |
|
SAR k,D |
D←D>>k |
算术右移 |
|
SHR k,D |
D←D<<k |
逻辑右移 |
在源程序中的例子:
subq $32, %rsp。对栈指针进行减法操作,开辟新的栈空间,大小为32字节。
如下图
3.3.5 关系操作与控制转移操作
- cmpl $3, -20(%rbp);配合跳转语句je .L2,对应源程序argc!=3的条件判断,同时这条cmpl的指令还有设置条件码的作用,根据条件码来判断是否需要跳转到分支中。
图1.汇编指令的条件判断 图2.对应的源程序C语句
- i<8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。
图1.汇编指令的条件判断 图2.对应的源程序C语句
3.3.6 函数操作
函数是过程的一种形式,而过程是软件中一种重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。
当调用一个过程时,除了要把控制传递给它并在过程返回时再传递回来之 外, 过程调用还可能包括把数据作为参数传递,而从过程返回还有可能包括返 回一个值。
源程序的函数有main函数,printf函数(第一处被优化为puts函数), sleep函数,getchar函数和exit函数。main函数的参数是argc和argv;两 次printf函数的参数为两个字符串。exit参数是1,sleep函数参数是atoi (argv[3])。函数的返回值存储在%eax寄存器中。
分析main函数的例子:
main函数的参数是argc和argv,main函数被调用即call才能执行(被系统启动函数__libc_start_main调用)。call指令将下一条指令的地址压栈,然后跳转到main 函数,完成对main函数的调用。程序结束时,调用leave指令恢复栈空间为调用之前的状态,然后 ret 返回。
如下图所示:
图为main传递形参argc和argv
图为main函数返回
3.3.7 类型转换
源程序中用到的类型转换有int sleepsecs=2.5(隐式类型转换,将浮点数2.5转化为int整数2)
3.4 本章小结
本章主要讲述了编译的概念与作用,编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
概念与作用
汇编就是将汇编语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
|
ELF头 |
节 |
|
.text |
|
|
.rodata |
|
|
.data |
|
|
.bss |
|
|
.symtab |
|
|
.rel.text |
|
|
.rel.data |
|
|
.debug |
|
|
.line |
|
|
.strtab |
|
|
节头部表 |
描述目标文件的节 |
1.读取ELF头
readelf -h hello.o
如图1:
图1.ElF头
ELF头以16字节的序列 Magic开始,Magic描述系统字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
2.读取节头部表
readelf -S hello.o
如图2:
图2.节头部表
节头部表(Section Headers)包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,代码可执行,但是不可写;数据段和只读数据段都不可执行,且只读数据段也不可写。
3.查看符号表
readelf -s hello.o
如图3
图3.符号表
符号表(.symtab)存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
4.重定位节
readelf -r --relocs hello.o
如图4
图4.重定位节
重定位节(.rela.text): .text 节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把目标文件和其他文件组合时,需要修改这些位置。
重定位节中各项符号的信息:
偏移量:需要被修改的引用节的偏移
信息:包括符号和类型两个部分,符号在前面四个字节,类型在后面四个字节
符号值:标识被修改引用应该指向的符号,
类型:重定位的类型
加数:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整
符号名称:重定向到的目标的名称。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
分析:
hello.o的反汇编代码与hello.s文件总体大致相同,有小部分区别,反汇编代码所显示的不仅仅是汇编代码,还有机器指令码。
- 分支转移:hello.s文件中分支转移是使用段名称进行跳转,而hello.o文件中分支转移是通过地址进行跳转的
- 函数调用:hello.s文件中,函数调用call后跟的是函数名称;而在hello.o文件中,call后跟的是下一条指令。因为这些函数都是共享库函数,地址是不确定的,因此call指令将相对地址设置为全0,然后在.rela.text节中为其添加重定位条目,等待链接的进一步确定。
- 全局变量:hello.s文件中,全局变量是通过语句:段地址+%rip完成的;对于hello.o的反汇编来说,则是:0+%rip,因为.rodata节中的数据是在运行时确定的,也需要重定位,现在填0占位,并为其在.rela.text节中添加重定位条目。
图1.反汇编代码
图2.hello.s
4.5 本章小结
本章结合hello.s与hello.o文件,介绍了汇编的概念与作用,对可重定位目标文件ELF进行了详细的分析。对比并分析了hello.s和hello.o反汇编代码的异同。本章对汇编的过程进行了详细的分析。
(第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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
1.ELF头
hello与hello.o的ELF头大致相同,不同之处在于hello的类型为EXEC可执行文件,表明hello是一个可执行目标文件,有25个字节。
readelf -h hello
图1.ElF头
2.节头部表(Section Headers)
节头部表是描述目标文件的节,各节的基本信息均在其中进行了声明,包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息等。
readelf -S hello
图2.节头部表
3.重定位节
图3.重定位节
4.符号表
图4.符号表
5.4 hello的虚拟地址空间
观察edb的Data Dump窗口。窗口显示虚拟地址由0x401000开始,到0x401ff0结束
图1.虚拟地址由0x401000开始
图2.虚拟地址由0x401ff0结束
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
- hello反汇编的节中是虚拟地址,完成了重定位(如图1),而hello.o反汇编代码节中的是相对偏移地址,未完成重定位的过程。(如图2)
图1.hello的反汇编代码(main函数部分)
图2.hello.o的反汇编代码
- hello的反汇编代码中多了许多文件节,如.init节与.plt节,而hello.o的反汇编代码中只有.text节
图3.hello反汇编代码的.init节与.plt节
3.hello_asm中增加了许多外部链接的共享库函数。如puts@plt,printf@plt等
图4.hello反汇编代码的外部链接库函数
链接的重定位的过程:
重定位节和符号定义链接器将相同类型的节合并,生成ELF节。链接器将运行时的内存地址分配给生成的节,此时程序中每条指令和全局变量都有唯一的运行时地址。要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位,修改.text节和.data节中对每个符号的引用,需要用到在.rel_data和.rel_text节中保存的重定位信息。
5.6 hello的执行流程
在edb中找加载hello可执行文件,列出子程序名及地址:
|
子程序名 |
程序地址 |
|
ld -2.33.so!_dl_start |
0x7f641a388df0 |
|
ld-2.33.so!_dl_init |
0x7f641a398c10 |
|
hello!_start |
0x4010b0 |
|
libc-2.33so!__libc_start_main |
0x7f6fe58bd550 |
|
hello!printf@plt |
0x4010c0 |
|
hello!sleep@plt |
0x4010f0 |
|
hello!getchar@plt |
0x4010d0 |
|
libc-2.33.so!exit |
0x7f6fe58b40d0 |
5.7 Hello的动态链接分
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。
dl_init函数调用前后GOT信息变化截图,经动态链接,GOT条目已经改变:
图1.GOT起始表位置
图2.edb执行init之前的内容
图3.edb执行init之后的内容
5.8 本章小结
本章介绍了链接的概念及作用,对hello的elf格式进行了详细的分析,介绍了hello的虚拟地址,分析了hello的重定位过程、执行流程、动态链接过程,详细阐述了hello.o链接成为一个可执行目标文件的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情 况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数 据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。
在现代计算机中,进程为用户提供了以下假象:程序好像是系统中当前运行的唯一程序一样,程序好像是独占的使用处理器和内存,处理器好像是无间断的执行程序中的指令程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork( )创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
1.逻辑控制流:
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
2.用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
3.上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。初始时,控制流再hello内,处于用户模式。调用系统函数sleep后,进入内核态,此时间片停止。2s后,发送中断信号,转回用户模式,继续执行指令。
4.调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
5.用户模式与内核模式转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
异常和信号异常可以分为四类:中断、陷阱、故障、终止
|
类别 |
原因 |
异步/同步 |
返回行为 |
|
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
|
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
|
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
|
终止 |
不可恢复的错误 |
同步 |
不会返回 |
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。
正常运行:
按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是2681;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。
Ctrl+C:进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,hello已经被彻底结束。
中途乱按:将屏幕的输入缓存到缓冲区,乱码被认为是命令。
Kill命令:挂起的进程被终止,在ps中无法查到到其PID。
6.7本章小结
本章阐述了进程的概念与作用,Shell的一般处理流程,分析了hello进程的执行过程,创建、加载和终止,以及hello的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址
逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o)。
2.线性地址
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
3.虚拟地址
有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
4.物理地址
物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下:逻辑地址CS:EA到物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。
段选择符各字段含义:
|
15 14 |
32 |
10 |
|
索引 |
TI |
RPL |
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级。高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置,被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
图1.页式管理流程图
7.4 TLB与四级页表支持下的VA到PA的变换
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
图2.使用页表的地址翻译
7.5 三级Cache支持下的物理内存访问
Cashe的物理访存大致过程如下:
1.组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组
2.行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
3.字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU
4.不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
图3.Core i7地址翻译情况
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,加载并运行 hello 需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。
3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的。
处理流程:
- 处理器生成一个虚拟地址,并将它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
- 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
- 缺页处理程序页面调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
图4.缺页中断处理
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
1.显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
隐式空闲链表:
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
显示空闲链表:
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了hello存储器的地址空间;虚拟地址到物理地址的转换;cache的物理内存访问;进程 fork、execve 时的内存映射、缺页故障与缺页中断处理;动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化
文件(所有的I/O设备都被模型化为文件,甚至内核也被映射为文件)
设备管理
Unix IO接口
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等
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.打开和关闭文件。
打开文件函数原型:int open(char* filename,int flags,mode_t mode)
返回值:若成功则为新文件描述符,否则返回-1;
flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode:指定新文件的访问权限位。
关闭文件函数原型:int close(fd)
返回值:成功返回0,否则为-1
2.读和写文件
读文件函数原型:ssize_t read(int fd,void *buf,size_t n)
返回值:成功则返回读的字节数,若EOF则为0,出错为-1
描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf
写文件函数原型:ssize_t wirte(int fd,const void *buf,size_t n)
返回值:成功则返回写的字节数,出错则为-1
描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置
8.3 printf的实现分析
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
return i;
}
vsprintf函数(在printf函数内部调用),vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
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
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
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
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
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;
}
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,Unix I/O 接口及其函数,printf 函数和 getchar 函数的工作过程。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello程序的一生:
- 我们作为程序员,使用高级语言C语言,编写出了C程序hello.c。
- hello.c经过预处理器cpp预处理,扩展得到hello.i文本文件。
- hello.i经过编译器ccl编译,得到hello.s汇编文件。
- hello.s经过汇编器as汇编,得到可重定位目标文件hello.o。
- hello.o与可重定位目标文件、动态链接库,经链接器ld链接生成可执 行文件hello。
- bash进程调用fork函数,生成子进程;并由execve函数加载运行当前 进程的上下文中加载并运行新程序hello。
- execve 调用启动加载器,加映射虚拟内存,进入程序入口后,程序载 入物理内存进入 main 函数。
- hello在运行时会调用一些函数,比如printf函数,这些函数与linux I/O 的设备模拟化密切相关。
- hello最终被shell父进程回收,内核会收回为其创建的所有信息。
感悟:
经过一学期的学习,结合深入理解计算机系统(CSAPP)这本经典教材与CMU的课程实验,以及HIT的老师们的悉心讲解。对现代计算机系统的整体框架和底层原理有了大致的认识和深刻的理解,从计算机内信息的存储,再到处理器体系结构,基本的汇编语言,存储器结构,链接,异常控制,虚拟内存,系统级I/O。
从宏观的角度,对现代计算机系统的整体认识,对以后的学习和工作无疑有莫大的帮助,对继续在计算机科学领域深耕也打下了坚实的基础。而从现实的角度,本门课程也从一个程序员的角度为以后的程序编写提供了保障,例如编写缓存友好,CPU友好的代码。
作为哈工大计院的学生,不仅仅应该停留在写代码的表面,更应该对这样计算机系统的底层知识有更深入的理解,而这门课程正是为我们以后成为一个优秀的程序员打下基础,培养我们的能力。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
|
中间结果文件 |
文件作用 |
|
hello.i |
hello.c预处理得到的文本文件 |
|
hello.s |
hello.i编译后的汇编文件 |
|
hello.o |
hello.s汇编得到的可重定位目标文件 |
|
hello |
链接得到的可执行目标文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018-1-737
[2] http://www.kuqin.com/language/20090806/66164.html
[3] https://blog.csdn.net/u012491514/article/details/24590467
[4] https://www.cnblogs.com/pianist/p/3315801.html
[5] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)

浙公网安备 33010602011771号