计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2021110983
班 级 2103201班
学 生 胡嘉怡
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文运用计算机系统知识,以hello.c文件为例,讲述了一个程序由创建到运行结束的过程,主要包括预处理、编译、汇编、链接、进程管理、存储管理、IO管理,掌握计算机的信息表示及处理、程序的机器级表示、处理器体系结构、存储器层次结构、链接过程、异常控制流、虚拟内存等知识。
关键词:预处理,编译,汇编,链接;存储管理;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
Hello程序的生命周期如下解释:
-
-
- P2P(From Program to Process):
-
如图1-1,最初程序员通过编译器创建并保存文本文件hello.c,之后在编译器驱动程序作用下,Hello经历了
- 使用预处理器cpp进行预处理,修改原始的C文件,得到hello.i;
- 使用编译器ccl编译将文本文件翻译成为hello.s;
- 使用汇编器as将hello.s翻译成机器指令,并将指令打包成为可重定向目标程序并保存于目标文件hello.o中;
- 使用链接器ld将调用的标准C库中的函数与hello.o文件进行合并,得到可执行目标文件hello;
该文件可被加载到内存中,有系统执行。在运行阶段,在壳(shell)中输入命令(./hello)后,操作系统(OS)为其调用fork,创建一个新的子进程,这样hello就是实现了从程序到进程的转变,该过程就是P2P。
图1-1 hello的编译过程
-
-
- 020(From Zero-0 to Zero-0)
-
- 在shell进程中输入程序名称;
- 在操作系统进程管理下,父进程shell通过fork函数产生子进程,通过execve函数加载并运行程序,将进程映射到虚拟内存,shell执行指令将hello目标文件中的代码和数据从磁盘复制到主存;
- 处理器就开始执行程序的main程序中的机器语言指令,将要打印的字符串中的字节从主存复制到寄存器文件,再从寄存器文件复制到显示设备最终显示到屏幕上;
- 运行结束后,shell 父进程回收 hello进程,内核清理相关资源占用,进程最终消亡。
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
-
-
- 软件环境
-
Windows11 64位;VirtualBox 6.1;Ubuntu 18.04 LTS 64位
-
-
- 开发工具
-
Visual Studio 2022 64位;CodeBlocks 64位;gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件名字 | 作用 |
hello.c | 编写的hello.c代码文件 |
hello.i | 预处理生成的文件 |
hello.s | 编译生成的文件 |
hello.o | 汇编生成的文件 |
hello | 从hello.c一步到位 |
obj.txt | hello.o的反汇编代码 |
1.4 本章小结
本章对Hello程序的生命周期进行了介绍,对P2P,020的意义与过程进行了介绍
简单讲述了实验环境与中间结果文件及其作用,
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1 概念
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.1.2 作用
- 预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,将用#include形式声明的文件复制到新的程序中;
- 预处理器将程序中间出现的所有的宏名称全部用宏定义当中的字符串进行代换(#define定义的字符串);
- 根据#if后面的条件去编译相应的程序部分;
- 删除所有的注释。
2.2在Ubuntu下预处理的命令
2.2.1 预处理的命令
cpp main.c > hello.i或者gcc -E main.c -o hello.i
2.2.2预处理过程
图2-1 预处理过程截图
2.3 Hello的预处理结果解析
- 如图2-2,经过预处理后,文件长度大大增加;
- 如图2-3,显示了cpp处理#include指令的证据;
- 如图2-4,在替换掉注释后,main函数部分并不会出现任何改变;
- 如图2-5,引入外部变量和符号。
图2-2文件长度对比
图2-3 头文件引用
图2-4 main函数内容
图2-5 外部变量和符号引入
2.4 本章小结
本章运用命令对文件进行了预处理,展示了详细的预处理过程以及对预处理之后得到的文件进行分析比对。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1 概念
编译器(cc1)将拥有高级语言的文本文件(hello.i)翻译为能被计算机直接运行的汇编语言的文本文件(hello.s),其中每条语句都以一种文本格式描述了一条低级机器语言指令。
3.1.2 作用
- 将预处理后得到的预处理文件(如hello.i)进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件;
- 经过编译后,得到的汇编代码文件(如,hello.S)还是一个可读的文本文件。将源程序转换为更易于机器理解的汇编语言代码,为后续汇编语言程序铺垫;
- 对代码进行合理的安全的优化,以提高程序性能以及编译器的运行效率。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
3.2.1 编译的命令
输入/usr/lib/gcc/x86_64-linux-gnu/9/cc1 hello.i -o hello.s或gcc -S hello.c -o hello.s
3.2.2 编译过程
图3-1 编译过程
3.3 Hello的编译结果解析
图3-2 hello.s文件中的内容
3.3.1 标记符号部分
.flie 声明源文件”hello.c”
.text 代码节
.section.rodata 只读代码段
.align 声明对指令或数据的存放地址
.string 字符串的声明
.global 全局变量
.type 声明符号类型
如图3-3
图3-3 标记符号部分
3.3.2 赋值
利用mov传送传送指令。move后缀含义b(传送一字节),w(传送两字节),l(传送四个字节),q(传送八个字节),如图3-4所示,可知赋值i为立即数0;
图3-4 赋值部分
3.3.3 算术运算
由add来完成算术运算,后缀含义同上,b(传送一字节),w(传送两字节),l(传送四个字节),q(传送八个字节),如图3-5所示
图3-5 算术运算
3.3.4 类型转换
在程序中用到了C标准库的atoi函数,把argv[3]中的字符串内容转换为一个整型数,如图3-6所示
图3-6 类型转换
3.3.5 关系操作
图3-7 关系操作(1)
如图3-7所示,比较argc和4的大小,
图3-8 关系操作(2)
如图3-8,比较i和4的大小,
3.3.6 数组
图3-9 for 循环的实现
如图3-9,在for循环内需要对argv数组进行访问;
表示argv[2]的地址
表示argv[1]的地址
表示argv[3]的地址
3.3.7 控制转移
图3-7 关系操作(1)
如图3-7所示,比较argc和4的大小,如果相等,则跳转到.L2;
图3-8 关系操作(2)
如图3-8,比较i和4的大小,如果小于等于,则跳转到.L4
3.3.8 函数调用
本程序调用两个C标准库的函数printf,atoi和一个包含在unistd.h中的sleep函数;
如下图3-10,printf的传入参数有三个,%rdi是在.L1节的格式串,%rsi是argv[1]的地址,%rdx是argv[2]的地址;atoi传入的参数%rdi是argv[3]的值;sleep传入的参数是atoi的返回值。
图3-10 函数调用
3.4 本章小结
本章对文件进行编译得到hello.s,对其进行汇编操作,并对得到的文件内容进行一点的分类、解释与分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
-
-
- 概念
-
汇编是指汇编器(as)将hello.s翻译为机器语言,并产生可重定位目标文件(hello.o)的过程。hello.o是一个二进制文件,它将hello.s中的用文本表述的机器指令大体上一对一地翻译为由0、1组成的机器指令。
-
-
- 作用
-
将汇编语言文本文件转换为机器代码的二进制文件,汇编的结果是一个可重定位目标文件(如hello.o)其中包含的是不可读的二进制代码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
图4-1 汇编过程
如图4-2所示
图4-2 典型的可重定位目标文件
-
-
- ELF中各节的解析
-
- ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下部分如图23所示,包含了帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型(如可重定位、可执行或者共享的),机器类型(如 X86-64 AMD),节头部表的文件偏移,以及节头部表中条目的大小和数量。不同的节位置和大小都是由节头部表描述的。
- .text节
已编译的机器代码
- rodata
只读数据,比如printf语句中的格式串和开关语句的跳转表,立即数
- .data
已初始化的全局和静态C变量。
- .bss
未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际空间,它仅仅是一个占位符。运行时,在内存中分配这些变量,初始值为0。
- .symtab
一个符号表,存放在程序中定义和引用的函数和全局变量信息。
注意:与编译器中的符号表不同,该符号表不含局部变量的条目。
- .rel.text(可重定位代码)
一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般来说。任何调用外部函数或者引用全局变量的指令都需要修改,调用本地函数的指令则不需要修改。
- .rel.data
被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
- .debug
一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件
- .line
原始C源程序中的行号和.text节中机器指令之间的映射。
- .strtab
一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的接名字。字符串表就是以null结尾的字符串的序列。
-
-
- Hello中各节信息
-
- ELF头
如图4-3所示,包括ELF头的大小,目标文件的类型(如可重定位、可执行或者共享的),机器类型(如 X86-64 AMD),节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4-3 ELF头
- hello.o各节
如图4-4所示,详细标识了每个节的名称、类型、地址、偏移量、大小、读取权限、对齐方式
图4-4 hello.o中各节信息
- hello.o符号表
如图4-5所示
图4-5 hello.o符号表
- hello.o可重定位节
如图4-6所示,在ELF表中有两个.rel节,分别是.rela.text和.rela.eh_frame。内容有偏移量、信息、类型、符号值、符号名称等等。
图4-6 可重定位节
4.4.1 反汇编过程
用指令objdump -d -r hello.o > obj.txt生成反汇编代码的文本文件来分析hello.o的反汇编。结果如图4-7所示
图4-7 输入指令得到的结果
图4-8 反汇编结果
4.4.2 机器语言的构成
机器语言由计算机直接识别的二进制代码构成,不同型号的计算机其机器语言是不相通的,按着一种计算机的机器指令编制的程序,不能在另一种计算机上执行。器语言全为0、1序列表示数据和指令。机器语言与汇编语言是一一对应的关系,一条机器语言对应一条汇编指令。
4.4.3 hello.o反汇编和hello.s的对比
图3-2 hello.s文件中的内容
由图4-8和图3-2对比易得,汇编代码与反汇编代码几乎一致
其中有以下几点不同:
1)操作数进制不同:汇编语言中的操作数是十进制数,而反汇编结果中的是十六进制数;
2)分支转移:汇编语言的分支跳转后跟的是段的名字,而由于以“. ”开头的行是指导汇编器和链接器工作的伪指令,在反汇编代码中没有这些,而且hello.o是可重定位文件,因此其跳转的时候用的是其要跳转的目的地址;
3)函数调用:汇编代码的函数调用时后面跟着函数名字,而反汇编代码调用函数时后面跟着的也是相对于main函数的偏移地址;
4)重定位条目:反汇编代码采用重定向的方式进行跳转,机器代码在此处留下一个地址以供链接时重定向。或者采用PC相对寻址,或者直接寻址,根据地址的更新和寻址的计算,实现跳转和调用。
4.5 本章小结
本章主要讲述了汇编的概念与作用,并进行了实际操作,并分析对比了汇编后得到的文件进行反汇编与汇编语言文件的对比。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.5.1 链接的概念
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行干编译时(compiletime),也就是在源代码被翻译成机器代码时;也可以执行干加载时(loadtime),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(runtime),也就是由应用程序来执行。
5.5.2 链接的作用
链接可以将各种代码和数据片段手机并组合策划归纳成一个可以加载到内存并执行的单一文件。它使得分离编译成为可能,可以将一个大型的应用程序分解为更小,更好管理的模块,便于独立修改和编译,链接让程序员能够利用共享库,通过动态链接为程序提供动态的内容。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
5.2.1链接的命令
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.2.2 连接结果
图5-1 链接过程
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
5.3.1 hello的ELF文件格式
- 只读代码段
- ELF头:字段e_entry给出执行程序时第一条指令的地址
- 程序头表:结构数组
- .init节:用于定义_init函数,该函数用来进行可执行目标文件开始执行的初始化工作
- .text节:编译后的代码部分
- .rodata节:只读数据
- 读写数据段
- .data节:已初始化的全局和静态C变量
- .bss节:未初始化的全局和静态C变量
- 无需装入到存储空间的信息
- .symtab节:符号表,存放在程序中定义和引用的函数和全局变量的信息
- .debug节:一个调试符号表,条目是程序中定义的局部变量和类型定义
- .strtab节:一个字符串表,内容包括.symtab和.debug节中的符号表,以及节头部中的节名字
- .line节:原始C源程序中的行号和.text节中机器指令之间的映射
- 节头表:每个节的节名、偏移和大小
5.3.2 hello中ELF各节
- ELF头
图5-2 hello的ELF头
- 各节
图5-3 各节信息
连接后增加了许多节
- .hash/gun.hash:对应的符号哈希
- .dynsym段:动态符号表,存储与动态链接相关的导入导出符号,不包括模块内部的符号;
- .dynstr段:存储.dynsym段符号对应的符号名;
- .dynamic段:保存动态链接所需要的基本信息,存储动态链接会用到的所有表的位置信息;
- .gotplt:全局偏移表-过程链接表
- .got:.got节保存了全局偏移表。
5.3.2 符号表
图5-4符号表
5.3.3 程序头
图5-5 程序头
5.3.4 段节
图5-6 段节
5.3.5 动态节
图5-7 动态节
5.3.6 重定位节
图5-8 可重定位节
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
- 使用edb打开hello从DataDump窗口观察hello加载到虚拟内存的情况,如图5-9,观察到起始的虚拟辞职为00000000:00401000,与hello的ELF文件中节头部表中的init节虚拟地址相同
图5-9 hello的Data Dump
- .init节起始地址0x401000,大小为0x1b
2.plt节起始地址为0x401020,大小为0x70
3.text节起始地址0x401090,大小0x145
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.5.1 hello和hello.o的不同
- 如下所示,hello比hello.o多出了init、plt、fini这几节
- init:包含程序初始化时需要的代码
图5-10 init节
- plt:节也称为 过程链接表(Procedure Linkage Table) , 其包含了动态链接器调用从共享库导入的函数所必需的相关代码 。 由于.plt 节保存了代码,所以节类型为 SHT_PROGBITS 。
图5-11 plt节
- fini:包含进程终止时要执行的指令代码少了.rel.text 和.rel.data 节等重定位信息节。多了一个程序头表也叫作段头表
图5-12 fini节
- 重定位:将多个代码段和数据段分别合并为一个完整的代码段和数据段,计算每一个定义 的符号在虚拟地址空间的绝对地址而不是相对偏移量,将可执行文件中的符号引用处修改为重定位后的地址信息。如图5-13所示
图5-13 重定位地址
5.5.2 链接的过程
- 符号解析:链接器将每个符号引用与一个确定的符号定义关联起来。
在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:
规则1:不允许有多个同名的强符号;
规则2:如果有一个强符号和多个弱符号同名,那么选择强符号;
规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
- 重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
- 重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节
- 重定位节中的符号引用:在该步中,链接器修改代码节和数据节中对每个符号的引用,使它们指向正确的运行地址
5.5.3 hello的重定位
hello主要依靠重定位条目进行修改。主要采用两种方式R_x86_64_PC32和R_x86_64_32
- R_x86_64_PC32: 重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。
- R_x86_64_32: 重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
例如:调用atoi函数
可重定位条目如图5-13所示
可知r.offset=0x71 r.symbol=atoi raddend=-4
由hello反汇编代码可知ADDR(s)=0x401125 ADDR(atoi)=0x4010c0
refaddr=ADDR(s)+r.offset=0x401125+0x71=0x401196
*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)
=(unsigned)(0x4010c0-4-0x401125)
=ffff ff9b
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
图5-14 执行过程
5.7 Hello的动态链接分析
动态链接是在程序加载时完成连接任务,将动态库的文本和数据重定位到某个段,重定位符号引用,最后动态链接器将控制传递给应用程序。
程序调用由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU 编译系统使用了延迟绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
在dl_init调用之前,编译器产生对调用函数的直接PC相对引用,并增加一个重定位,让链接器在构造共享模块时解析它。PLT[1]调用系统启动函数初始化执行环境,调用main并处理其返回值,每个GOT条目都对应一条PLT条目,初始时,每个GOT条目都指向对应的PLT条目的第二条指令。
由elf头表易得got.plt位置为:
图5-15 在dl_init之前
而运行之后,可以发现该位置发生了变化,如图5-16:
图5-16 在dl_init之后
这样就了解了动态链接的过程,由此可以看出,动态链接器在程序加载的时候解析调用的共享库函数,并使用PLT和GOT来实现函数的动态链接,在解析过程中,GOT中用来存放函数目标地址,PLT使用GOT中存储的地址跳到目标函数。
5.8 本章小结
本章详细介绍了链接的相关知识,并且细致的对其内容进行了分析比对,深入了解了重定位,动态链接以及过程中虚拟地址的分配情况。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 概念
进程就是一个执行中程序的实例。是计算机中的程序关于某数据集合 上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
6.1.2 作用
提供给应用程序两个关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;
一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 shell-bash作用
shell 最重要的功能是命令解释。shell 是一个命令解释器。用户提交了一个命令后,shell 首先判断它是否为内置命令,如果是就通过 shell 内部的解释器将其解 释为系统功能调用并转交给内核执行;若是外部命令或使用程序就试图在硬盘中 查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。
6.2.2 处理流程
- 从终端读入输入的命令,将输入字符串切分获得所有的参数。
- 对用户输入的命令进行解析,判断命令是否为内置命令,如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。
- 判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。其他的输入信号有其对应的信号处理。
6.3 Hello的fork进程创建过程
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。fork被调用一次,返回两次:一次是在调用进程(公进程)中,一次是在新创建的子进程中,在父进程中,fork返回子进程的PID。在子进程中,fork返回0。
6.4 Hello的execve过程
在这个子进程创建出来后,子进程会去调用execve()来加载可执行文件到当前进程,这样,子进程原来的用户区域将会被删除,然后通过虚拟内存机制将可执行文件hello中的各个段映射到对应的代码段、数据段等地址空间,这样就加载了hello的新的用户区域。然后,execve会加载hello用到的共享库(比如上面提到过的ld-2.31.so),也是通过虚拟内存映射的方式。最后,子进程的程序将直接跳转到hello的入口点,进行hello的执行。
6.5 Hello的进程执行
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。
当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
比如,hello初始运行在用户模式,调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,定时器到时会发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程继续运行。
图6-1 进程切换
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 常见异常
- 中断
- 原因:来自I/O设备的信号
- 异步/同步:异步
- 返回行为:总是返回到下一条指令
- 陷阱
- 原因:有意的异常
- 异步/同步:同步
- 返回行为:总是返回到下一条指令
- 故障
- 原因:潜在可恢复的异常
- 异步/同步:同步
- 返回行为:可能返回到当前指令
- 终止
- 原因:不可恢复的错误
- 异步/同步:同步
- 返回行为:不会返回
6.6.2 hello执行过程中异常
- 正常运行
- ctrl+z
在hello在前台运行的时候,按下ctrl+z会向其发送SIGTSTP信号,这个进程就会暂时挂起
- 回车+乱按
乱输入字符会输出,回车会进行保存在进程结束后输出
- fg
会恢复前台运行
5.ps
- jobs
- pstree
- Kill
6.7本章小结
本章,我们了解了hello运行的一些机制和一些常见异常的认识以及一些常见异常会出现的问题和异常处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
机器语言指令中出现的内存地址都是逻辑地址,需要转换成线性地址,再经过MMU(内存管理单元)转换成物理地址才能够被访问到。
7.1.1 逻辑地址
包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
7.1.2 线性地址
Linux中逻辑地址等于线性地址。因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x0 开始,这样线性地址 = 逻辑地址 + 0x0,也就是说逻辑地址等于线性地址了。
7.1.3 虚拟地址
虚拟地址将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。同时,它为每个进程提供了一致的地址空间,从而简化了内存管理。最后,它保护了每个进程的地址空间不被其他进程破坏。
7.1.4 物理地址
而物理地址则是对应于主存的真实地址,是能够用来直接在主存上进行寻址的地址。由于在系统运行时,主存被不同的进程不断使用,分区情况很复杂,所以如果要用物理地址直接访问的话,地址的处理会相当麻烦。
7.2 Intel逻辑地址到线性地址的变换-段式管理
对于一个以“段:偏移地址”形式给出的逻辑地址,CPU将会通过其中的16位段选择子定位到GDT/LDT中的段描述符,通过这个段描述符得到段的基址,与段内偏移地址相加得到的64位整数就是线性地址。这就是CPU的段式管理机制,其中,段的划分,也就是GDT和LDT都是由操作系统内核控制的。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存机制使用页表的数据结构进行页式管理,它把线性地址空间和物理地址空间划分成大小相同的页,然后通过建立线性地址空间的页同物理地址空间中的页的映射,实现线性地址到物理地址的转化。如图7-1,过程为:MMU利用虚拟页号(VPN)找到对应的物理页号(PPN),然后将找到的PPN与由虚拟页偏移量(VPO)得到物理页偏移量(PPO)组合就构成了实际的物理地址。
图7-1 变换过程
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。如图7-2所示用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。
图7-2 TLB组成
图 7-3 展示了当 TLB 命中时(通常情况)所包括的步骤。这里的关键点是,所有的地址翻译步骤都是在芯片上的 MMU 中执行的,因此非常快。
第1步:CPU 产生一个虚拟地址。
第2步和第3步:MMU从TLB 中取出相应的PTE
第4步:MMU将这个虚地址翻译成一个物理地址,并且将它发送到高速缓存/主存
第5步:高速缓存/主存将所请求的数据字返回给 CPU。
当TLB不命中时,MMU 必须从 L1 缓存中取出相应的 PTE,如图7-4 所示。新取出的 PTE 存放在 TLB 中,可能会覆盖一个已经存在的条目。
图7-3 命中
图7-4 不命中
7.5 三级Cache支持下的物理内存访问
L1cache 有64组,八路组相连,每块64字节。所以块偏移CO是 位, 组索引 CI 是6位,剩下的 40 位为标记 CT。现有物理地址52位,低6位是 CO,CO的左边高 6 位是CI,剩余的是CT。根据组索引CI,定位到 L1cache 中的某一组,遍历这一组中的每一行,如果某一行的有效位为1且标记位等于CT,则命中,根据块偏移CO取出数据。如果未命中,则向下一级 cache 寻找数据。更新 cache 时,首先判断是否有空闲块。如果有,则写入这个块,否则根据替换算法驱逐一个块后再写入。
7.6 hello进程fork时的内存映射
当shell使用fork创建子进程时,内核为新的子进程创建各种数据结构,并分配给子进程一个唯一的PID,为了给它创建虚拟内存空间,内核创建了当前进程的mm_struct、区域结构和页表的原样副本,将两个进程的页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。这样,在新进程里,最开始的时候它的虚拟内存和原进程的虚拟内存映射相同,但当这两个进程中的任意一个进行写操作时,写时复制机制就会创建新页面,这样两个进程的地址空间就在逻辑上私有了。
7.7 hello进程execve时的内存映射
execve函数在进程中加载并运行hello需要以下几个步骤∶
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello文件中的.text 和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。图9-31概括了私有区域的不同映射。
- 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc. so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障
就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件[4]。
7.8.2 缺页中断处理
如果出现缺页故障,那么此时控制权会交给相应的异常处理程序,该程序会根据一定策略淘汰物理内存中的某个页,然后再从硬盘中调出所需新的所需的页,并更新PTE,并返回到原来的进程,再次执行当前指令。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
在hello程序中使用的printf,而printf会使用由动态内存分配器动态内存分配机制。动态内存分配器维护进程虚拟地址空间中的的堆区域,它将堆视作一组不同大小的块的集合来维护,每个块是一段连续的虚拟内存碎片,要么是已分配的,要么是空闲的。空闲块保持空闲直至被应用程序分配,以已分配块保持已分配状态直至被释放。
分配器需要一些数据结构维护堆块来区分块边界以及区分已分配块和空闲块,这些可以被标识在块的头部,那么分配器可以将堆组织为一个连续的已分配块和未分配块的序列(称为隐式空闲链表),如下图所示:
这样的话,通过隐式空闲链表,分配器可以通过对于链表的操作以完成在堆上放置已分配的块、分割空闲块、获取额外内存、合并空闲块等操作。于是应用程序就可以动态地在堆上分配额外内存空间了。
7.10本章小结
本章主要介绍了hello四类地址的概念以及相互转变的步骤与操作,其次,也了解了cache的寻址,fork、execve函数的内存映像,缺页故障及管理以及动态内存分配等相关理论。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
8.1.2 设备管理:unix io接口
这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口
- 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
- Linux创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
- 改变当前的文件位置。对于每个打开的文件内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
- 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处没有明确的“EOF符号”。
类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2 函数
- open()函数
open函数将 filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件ORDONLY:只读;OWRONLY:只写;ORDWR:可读可写
mode参数指定了新文件的访问权限位。
- close()
- read()
读文件从当前文件位置复制字节到内存位置,然后更新文件位置,返回值表示的是实际传送的字节数量。
- write()
写文件从内存复制字节当前文件位置,然后更新文件位置,返回值表示的是从内存向文件fd实际传送的字节数量nbytes<0表明发生错误。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
vsprintf代码如下:
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 (p - buf);
}
- 从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
- 字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
- 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
int getchar(void)
{
static char [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;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简述了IO设备管理方法、IO设备接口机器函数。更着重的讲述了 printf,getchar 的实现和工作过程。
(第8章1分)
结论
hello的一生
- 程序员在编译器中编写hello的代码,将之存储为hello.c文件
- 预处理:经由cpp,变为hello.i;
- 编译:经由cc1,变为hello.s;
- 汇编:经由as,变为hello.o;
- 链接:经由ld,变为hello可执行程序;
- 创建进程:在命令行输入执行指令,shell调用fork创建子进程;
- 加载程序:Execve加载并运行hello;
- 运行:CPU,顺序执行hello的逻辑控制流中的指令,响应其需求。
- 异常:在运行过程中,OS同时接受和检测异常信号,并调用相关的异常处理程序,对各种异常信号进行处理。
- 程序结束后,父进程回收hello,内核清除hello所占资源,hello短暂而又不平凡的一生到此结束。
感悟
hello作为我们在接触计算机时敲入的第一个代码,又在此时作为一个反例来反应一个程序历经底层硬件到预处理阶段,编译阶段,汇编阶段,加载进程,链接器,体现着程序每一步的发展进程,也体现着计算机的魅力,通过对hello的分析我们看到了计算机背后的神奇,鼓励着我们进一步学习了解计算机
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件名字 | 作用 |
hello.c | 编写的hello.c代码文件 |
hello.i | 预处理生成的文件 |
hello.s | 编译生成的文件 |
hello.o | 汇编生成的文件 |
hello | 从hello.c一步到位 |
obj.txt | hello.o的反汇编代码 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E,Brynant, David R. O’Hallaron. 深入理解计算机系统(原书第三版). 北京:机械工业出版社,2016.
[2] Stallings.计算机组成与体系结构:性能设计(原书第8版). 北京:机械工业出版社,2011.
(参考文献0分,缺失 -1分)