计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2022112165
班 级 2203201
学 生 王佳宁
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
本文运用计算机系统所学知识,基于《深入理解计算机系统》,以hello.c程序从诞生到结束的过程为例,论述其从hello.c经过预处理、编译、汇编、链接等一系列操作生成可执行文件hello,再对程序的异常处理,进程分配,存储管理,IO管理进行分析,串联了计算机系统所学知识,加深了对计算机系统的理解。
关键词:计算机系统;预处理;汇编;链接;异常控制流;存储管理;IO管理
目 录
第1章 概述
1.1 Hello简介
P2P:from Program to Process即从程序到进程。我们利用高级语言C语言编写hello.c源程序,经过cpp预处理形成hello.i,再经过ccl编译形成汇编语言程序hello.s,然后经过as转换为机器语言指令,形成可重定位目标程序hello.o,最后通过ld与库函数链接并符号解析与重定位,形成可执行目标文件hello。接下来计算机便可以运行这个hello文件了,在计算机的bash(shell)中,os为其创建子进程(fork),这样hello在计算机系统中就有了自己的进程,hello在该进程中运行。
020:from Zero-0 to Zero-0,即从运行到结束,初始时内存中没有hello文件相关的内容,即开始为0,通过fork产生hello子进程后,通过execve进行加载,OS为其开辟一块虚拟内存,并将程序加载到虚拟内存映射到的物理内存中。执行过程中,虚拟内存为进程提供独立的空间;存储结构一层一层逐层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O设备通过描述符与接口实现了hello的输入输出。Hello在多个方面的配合下完成执行。当程序执行完,OS会回收该程序,hell回收hello进程,删除hello的所有痕迹,释放运行中占用的内存空间,此时又变回了0。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;16G RAM;512GHD Disk
软件环境:Windows 10 64位;Vmware16;Ubuntu 20.04
开发工具:Visual Studio;codeblocks;vi/vim/gedit+gcc
1.3 中间结果
文件名 | 功能 |
hello.c | 源代码 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello | 链接后的可执行文件 |
hello.elf | 用readelf读取hello.o的ELF格式信息 |
hello.asm | 反汇编hello.o的反汇编文件 |
hello2.elf | 由hello可执行文件生成的.elf文件 |
hello2.asm | 反汇编hello可执行文件得到的反汇编文件 |
。
1.4 本章小结
本章主要介绍了hello的P2P和020过程,从总体上简述了hello的一生,介绍了环境与工具,以及hello生成的中间结果。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
C语言编译器预处理步骤是指程序开始运行时,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序的过程。结果会得到扩展名为.i的文件。
2.1.2 预处理的作用
(1)进行宏替换,将宏定义中的目标字符替换为我们所定义的字符。
(2)头文件展开,对文件包含命令#include,引入对应头文件,将头文件的内容(.h)插入到命令所在位置,把源文件直接插入到程序文本中,提高编程效率。
(3)条件编译,如#if,#ifdef,#else等
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
1 . 开头为hello.c涉及到的所有头文件信息
- 然后是类型定义相关信息
- Include的头文件主体内容经过预处理的宏展开包括函数声明和结构体的定义
- 程序源码
2.4 本章小结
在本章我们进行对hello.c的预处理生成hello.i,观察到了预处理器进行宏展开、头文件展开、条件编译等处理,并删除注释的过程,并分析了hello.i的组成。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是C语言源程序经过预处理后,编译器(ccl)将预处理文件hello.i翻译成汇编语言文件hello.s。
3.1.2 编译的作用
- 在编译过程中检查代码的规范以及是否存在语法错误,如果有错误的话就会报错。
- 生成汇编代码:将程序翻译成汇编语言,从而在下一阶段可以让汇编器翻译成机器语言指令。
- 编译器会对程序进行一部分有限的优化,生成效率更高的目标代码。
3.2 在Ubuntu下编译的命令
编译命令为gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 汇编代码展示
3.3.2 结构分析
内容 | 含义 |
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.data | 存放已经初始化的全局和静态C 变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long | long类型 |
.string | string类型 |
3.3.3 数据类型及操作分析
(1) 常量
编译时对常量进行编码,并将其存储在只读代码区的 .rodata节,在程序运行 时会直接通过寻址找到常量。
如:“Usage: Hello 学号 姓名 秒数!”
- 局部变量
局部变量被分布在栈上。例如int i,将栈指针减去4,为局部变量i分配了四 个字节的空间,并对其进行赋值操作。
- 全局变量,静态变量
初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节
- 数组/指针
程序中涉及到的数组操作为对argv数组进行取元素,操作为首先获取数组的首地址,再根据需要的第几个元素进行索引乘以数据大小加上首地址来获得该元素的地址,再对改地址进行访问得到元素的值。
- 算术运算
如add,sub,Inc,dec,imul,idiv等算术运算操作
本程序中运用算术运算的例子如下:
addl $1, -4(%rbp)将指针存储的数据加上立即数之后再存入到指针,实现i++的操作。Subq $32, %rsp,将栈指针减去32,来分配栈空间。addq$16, %rax将%rax的数据加上立即数16获取数组的元素地址。
- 比较和跳转操作(关系操作和控制转移)
通过CMP指令进行比较,计算两个值相减大小,根据结果设置条件码,跳转指令根据CMP指令返回的条件码进行跳转。
例如,在程序中检查argc是否不等于5。在hello.s中,使用cmpl $5,-20(%rbp),比较 argc与4的大小并设置条件码,为下一步je利用条件码进行跳转作准备。
- 赋值操作
赋值操作,使用MOV指令,根据不同的数据类型选择不同指令movb、movw、movl、movq等。例:为i初始化赋值为0。
- 函数操作
hello.c中包括main函数,printf函数,sleep函数,getchar函数,exit函数。
最初,内核shell会获取命令行参数和环境变量的地址,并执行main函数。在 main函数内,需要调用其他函数,并为这些被调用的函数分配栈空间。调用 函数时需要使用栈,首先将返回地址压入栈中,然后将程序计数器(PC)设置 为被调用函数的起始地址进行调用。当函数执行完毕返回时,从栈中弹出返回 地址,并将程序计数器设置为这个返回地址。函数正常返回后,通过leave指 令恢复栈空间。
在hello.s使用call指令调用函数,调用函数有:puts,exit,printf,sleep,getchar
3.4 本章小结
本章介绍了编译的概念和作用,生成了hello.s文件,并对汇编代码进行了详细的分析,介绍了汇编代码操作,包括对数据、算术操作、关系操作、控制转移、数组操作、赋值操作,函数操作的处理。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指汇编器(as)将.s文件翻译成机器语言指令的过程,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。
4.1.2 汇编的作用
将汇编指令转换成机器可以直接读取分析的机器指令,把这些指令打包成可重定位目标程序的格式,生成hello.o文件,用于后续的链接。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
在经过汇编之后,会产生可重定位目标文件,其在Unix和x86_64 linux上称为ELF(Executable and Linkable Format)。在汇编器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而在ELF文件的头部,还有ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置。
首先我们利用readelf -a hello.o > hello.elf指令获得hello.o文件的 ELF 格式
4.3.1 ELF头
可重定位文件的ELF(Executable and Linkable Format)头包含了描述文件结构和布局的关键信息。从ELF头我们可以看到hello.elf的基本信息,其描述了生成该文件的系统的字的大小和字节顺序,包含操作系统版本, ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
4.3.2 节头
节头包含各节名称及大小、类型及全体大小、地址及旗标、链接、信息和偏移量及对齐信息。
4.3.3 重定位节
rela.text包含需要重定位的信息,当链接器链接.o文件时,会根据重定位节的信息计算正确的地址,重定位.rela.text中的信息。一般,调用外部函数或者引用全局变量的指令都需要修改。有两种类型R_X86_64_PC32( PC相对地址的引用)和R_X86_64_32(绝对地址的引用)。
4.3.4 符号表
symtab 是一个符号表,符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
4.4 Hello.o的结果解析
使用指令objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
4.4.1 反汇编代码
与hello.s进行比较
每行代码末尾指令基本是相同的。
不同点有以下几点:
- 包含内容不同,反汇编文件中只有函数代码的相关内容,而hello.s中包含.type .size .align以及.rodata只读数据段等信息。
- Hello.s是由汇编语言组成的,而反汇编得到的不仅有汇编代码,还有机器语言代码,机器可识别的纯二进制编码。
- 分支转移的时候hello.s中为形如L1,L2这样的段名称,而hello.asm跳转的目标为具体的地址。
- 函数调用形式不同,在hello.s文件中,call之后直接跟着函数名称,hello.asm中,call 的目标地址是当前指令的下一条指令。
4.4.4 机器语言的构成以及与汇编语言的映射关系
汇编器的主要任务就是将汇编语言代码翻译成机器语言代码,确保每条汇编指令被正确地转换为对应的机器指令。机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。机器语言使用绝对地址和绝对操作码。不同的计算机都有各自的机器语言,即指令系统。从使用的角度看,机器语言是最低级的语言。汇编语言和机器语言一般是一一对应的,汇编语言是机器语言的符号表示方式。这种一一映射关系使得汇编语言程序员可以精确控制计算机的行为,同时也让机器语言的复杂细节变得相对容易管理。
4.5 本章小结
本章讨论了汇编阶段将汇编代码hello.s翻译成机器语言指令hello,o的过程,将汇编语言一一对应到机器语言。于是我们便将hello.s汇编得到hello.o可重定位目标文件。分析了hello.o与hello.s之前的相同与不同之处,了解了机器语言的构成以及其与汇编语言的映射关系。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。链接使得分离编译成为可能。我们可以独立的修改和编译我们需要修改的小的模块,而不必将全部的程序都重新编译一次,简化了维护和管理。
5.2 在Ubuntu下链接的命令
链接命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello
5.3 可执行目标文件hello的格式
输入命令 readelf -a hello > hello1.elf 生成 hello 程序的 ELF 格式文件
5.3.1 ELF头
其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等。
5.3.2 节头
记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。
5.3.3 程序头
一个结构数组,描述了系统准备程序执行所需的段或其他信息。
5.3.4 Dynamic section
Dynamic section(动态段)是一个特别重要的部分,它描述了与动态链接有关的信息。这些信息对动态链接器(dynamic linker)至关重要,以便在运行时加载和链接共享库。
5.3.5 重定位节
在ELF表中有两个.rel节,分别是.rela.text和.rela.eh_frame。内容有偏移量、信息、类型、符号值、符号名称等等。在重定位节中可以看到符号名称有.rodata, puts, exit, printf, atoi, sleep, getchar
5.3.6 符号表
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,根据计算机系统的特性,程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。通过ELF可知,程序从0x00400000到0x00400fff,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。与5.3对照发现edb显示的各段虚拟地址空间与5.3中的一致。
5.5 链接的重定位过程分析
在Shell中使用命令objdump -d -r hello > hello2.asm生成反汇编文件hello2.asm,与第四章中生成的hello.o.asm文件进行比较
5.5.1 重定位概念及过程
链接器在完成符号解析以后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后开始进行重定位,合并输入模块,并为每个符号分配运行时的地址。在 hello 到 hello.o 中,首先是重定位节和符号定义,链接器将所有输入到 hello 中相同类型的节合并为同一类型的新的聚合节。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。这一步依赖于.o文件的重定位条目,链接器依据重定位条目中指示的重定位方式(重定位PC相对引用、重定位绝对引用等等)修改引用。
5.5.2 hello与hello.o的不同
(1)函数调用指令call的参数发生变化,hello中调用为call+函数名,hello.o中为call+相对偏移地址。
(2)链接后hello中增加了库中的外部函数。
(3)跳转指令参数发生变化,有了具体地址(虚拟地址)。
(4)hello.o中是相对偏移地址,hello为虚拟内存地址。
(5)hello.o将lea后的操作数置为0,并添加重定位条目。
(6)在hello增加了.init节
5.6 hello的执行流程
<ld-2.31.so!_dl_start>
<ld-2.31.so!_dl_init>
<hello!_start>
<libc-2.31.so!__libc_start_main>
<hello!main>
<hello!printf@plt>
<libc-2.31.so! printf >
<hello!atoi@plt>
<libc-2.31.so! atoi >
<libc-2.31.so! strtoq >
<hello!sleep@plt>
<libc-2.31.so! sleep >
<libc-2.31.so! nanosleep >
<libc-2.31.so! clock_nanosleep >
<libc-2.31.so! _IO_file_xsputn>
<hello!getchar@plt>
<libc-2.31.so!getchar>
5.7 Hello的动态链接分析
使用指令readelf -a hello > elf_hello.txt查看hello的elf
动态链接的基本思想为创建可执行文件时,静态执行一些链接,在程序加载时,动态完成链接过程。共享库是一个目标模块,在加载或运行时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程被称为动态链接,是由一个叫做动态链接器的程序来执行的。hello中的printf、sleep、atoi等都是通过动态链接与源程序建立关系的。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
调用前:.got表位置在调用dl_init之前0x403ff0后的16个字节均为0
调用_start后.got发生了变化,存入了地址
这样就改变了GOT条目,完成了动态链接。
5.8 本章小结
本章主要介绍了链接的概念与作用,通过对比hello与hello.o ,能更好地理解链接与重定位的相关过程,以及分析了ELF文件各部分的含义、hello的执行过程、动态链接过程等。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程提供给应用程序两个关键抽象(假象):一个独立的逻辑控制流、一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互型的应用级程序,它代表用户运行其他程序。Shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
Shell不断读入用户输入的命令行,字符串的第一个单词是一个可执行程序,或者是 shell 的内置命令。如果第一个单词是内置命令,shell 会立即在当前进程中执行。否则,shell 会新建一个子进程,然后再子进程中执行程序。作业为新建的子进程。作业可由 Unix 管道连接的多个子进程组成。否则shell认为这个参数是一个可执行目标文件的名字,它会在一个新的子进程的上下文中加载并运行这个文件。如果最后一个参数是&,shell不会等待这个命令完成(在后台执行),否则表示在前台执行,shell会等待它完成。之后shell会使用waitpid函数等待作业终止并开始下一轮迭代。Unix shell 支持作业控制的概念,允许用户在前台和后台之间来回移动作业,并更改进程的状态(运行,停止或终止)。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的、处于运行状态的子进程。调用fork函数后,子进程返回0,父进程返回子进程的PID;新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。但是子进程有不同于父进程的PID。fork函数被调用一次,返回两次。
Fork具体处理hello文件过程为带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,和打开的文件相同的一份副本。子进程有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
shell fork一个子进程后,execve函数在当前进程的上下文中加载并运行一个新程序(hello)。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。然后,映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的;加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
6.5.1 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.2 上下文信息
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
6.5.3 用户模式和内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
6.5.4 执行过程
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策叫做调度,是由内核中称为调度器的代码处理的。内核调度了一个新的进程运行后,它就抢占当前进程,并使用上下文切换机制将控制转移到新的进程,上下文切换(1)保存当前进程的上下文(2)恢复某个先前被抢占的进程被保存的上下文(3)将控制传递给这个新恢复的进程;上下文切换机制建立低层的异常机制上,需要在内核模式下进行。当设置了模式位时,进程就运行在内核模式中,表示它可以执行指令集中的任何指令并且可以访问系统中的任何内存位置。进程从用户模式变为内核模式的唯一方法是通过中断、故障或陷入系统调用这样的异常。当某个异常发生时,系统进入内核模式,内核可以执行从某个进程A到进程B的上下文切换,在切换的第一部分,内核代表进程A在内核模式下执行指令,在某一时刻开始代表进程B在内核模式下执行指令。切换完成后,内核代表进程B在用户模式下执行指令。这样就实现了多任务。
6.6 hello的异常与信号处理
异常可以分为四类:中断,陷阱,故障和终止。
(1)中断异常是异步发生的,是来自处理器外部的I/O设备的信号的结果。当前指令完成执行后,处理器注意到中断引脚的电压变高,就从系统总线来读取异常号,然后调用适当的中断处理程序。当处理程序返回后,它就将控制返回给下一条指令。按下ctrl-c时内核向shell发送SIGINT信号,按下ctrl-z时内核向shell发送SIGTSTP信号,两种情况下控制都会转移到对应的信号处理程序。在SIGINT信号处理程序中,shell向前台进程组发送信号,使前台进程组的所有成员终止;在SIGTSTP信号处理程序中,使前台暂时挂起。
(2)陷阱是有意的异常,是执行一条指令的结果。应用程序执行一次系统调用,然后把控制传递给处理程序,陷阱处理程序运行后,返回到syscall之后的指令。
(3)故障由错误情况引起,故障发生时处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
(4)终止是不可恢复的致命错误造成的结果。终止处理程序从不将控制返回给应用程序,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
以下给出程序运行过程中按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令的运行结果:
可以看到程序运行过程中可以乱按
可以看到在按下ctrl-c后进程终止,这时使用ps命令就看不到hello进程了。
按下ctrl-z后进程是被暂时挂起,并没有终止,使用ps、jobs等还可以看到它。
执行kill指令
执行pstree指令
执行fg指令返回前台
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章主要讨论了进程和shell的概念与作用,进程的创建和执行过程,对异常和信号的处理以及相关函数,和进程相关的指令,并且以hello为例子进行了分析。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址指由程序产生的和段相关的偏移地址部分。hello.c经过汇编生成的偏移地址为逻辑地址。
(2)线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
(3)虚拟地址为CPU通过生成虚拟地址访问主存。有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
(4)物理地址放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在x86架构的处理器中,逻辑地址到线性地址的变换是通过段式管理实现的。在段式管理中,内存被划分成多个段,每个段有一个基地址和一个长度。当处理器产生一个逻辑地址时,这个逻辑地址包括两部分:段选择子(Segment Selector)和偏移量(Offset)。段选择子用于选择一个段,偏移量表示相对于选定段的位置。
具体的逻辑地址到线性地址的转换过程如下:
(1)处理器使用段选择子从全局描述符表(Global Descriptor Table,GDT)或本地描述符表(Local Descriptor Table,LDT)中找到对应的段描述符。
(2)段描述符包含了段的基地址和长度等信息。处理器使用段描述符中的基地址和偏移量进行线性地址的计算。
(3)线性地址 = 段基址 + 偏移量
在这个过程中,处理器会检查访问权限、段的有效性等信息,以确定是否允许对该线性地址的访问。
7.3 Hello的线性地址到物理地址的变换-页式管理
在计算机系统中,将线性地址转换为物理地址的过程通常是通过页式管理来实现的。页式管理是一种虚拟内存管理机制,它将物理内存和逻辑内存分割成固定大小的页,同时将逻辑地址和物理地址也分割成相同大小的页。在页式管理中,线性地址(由程序生成的地址)被划分为两部分:页号和页内偏移量。同样,物理地址也被划分为页号和页内偏移量。转换过程如下:
(1)程序产生一个线性地址(例如,Hello程序中的某个变量的地址)。
(2)将这个线性地址分为页号和页内偏移量。
(3)系统查找页表,将页号映射到对应的物理页框号。
(4)将物理页框号和页内偏移量组合成物理地址。
页表是一个数据结构,用于记录每个页号对应的物理页框号。在实际应用中,通常会使用多级页表来管理大量的页表项,以提高查找效率。
7.4 TLB与四级页表支持下的VA到PA的变换
地址管理单元(MMU)实现了虚拟内存到物理内存的转换,处理器将需要翻译的虚拟地址交给MMU。MMU首先在TLB中寻找对应的页表条目,以便转换虚拟地址。一个n位的虚拟地址包含两部分:一个p位的虚拟页面偏移(VPO)和一个n-p位的虚拟页号(VPN)。MMU利用VPN选择适当的页表条目,例如选择PTE 0来解析VPN 0。通过查询PTE的信息,可以确定虚拟页的状态,即未分配、未缓存或已缓存。若虚拟页已缓存,则直接将页表条目的物理页号与虚拟地址的VPO组合即可得到相应的物理地址。这时VPO和PPO是相同的。如果虚拟页未缓存,会引发缺页故障,触发缺页处理子程序将磁盘上的虚拟页重新加载到内存中,然后再执行导致缺页的指令。
在现代计算机系统中,由于地址空间较大,如果仅使用单个巨大的页表进行地址翻译,将需要大量内存空间。为了节省空间,现代计算机系统采用层次结构的页表来压缩页表大小。Core i7MMU使用了四级页表将虚拟地址翻译成物理地址。36位VPN被划分为四个9位的片段,每个片段用作一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供一个到L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2 PTE的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
在一个三级Cache支持下的系统中,物理内存访问过程通常会涉及到多级缓存和内存层次结构。以下是在这样的系统中典型的物理内存访问过程:
(1)CPU访问内存:
当CPU需要访问内存中的数据时,首先会检查最近使用的数据是否在CPU的一级缓存(L1 Cache)中。如果数据在L1 Cache中,则可以直接从缓存中读取,无需访问其他存储层次。
(2)L1 Cache未命中:
如果数据未在L1 Cache中命中,CPU将继续在更大容量的二级缓存(L2 Cache)中寻找数据。如果数据在L2 Cache中命中,则可以从L2 Cache中读取数据并返回给CPU。
(3)L2 Cache未命中:
如果数据未在L2 Cache中命中,CPU将继续尝试在更大容量的三级缓存(L3 Cache)中寻找数据。如果数据在L3 Cache中命中,则可以从L3 Cache中读取数据并返回给CPU。
(4)L3 Cache未命中:
如果数据未在L3 Cache中命中,CPU将发出对内存的读取请求。此时,会通过内存管理单元(MMU)将虚拟地址翻译成物理地址,并将该物理地址传递给内存控制器。
(5)访问主存:
内存控制器根据物理地址,访问主存中的相应位置,将数据读取到缓存中,并返回给CPU。
整个过程中,多级缓存的存在可以减少对主存的访问频率,提高数据访问速度和系统性能。三级缓存的引入进一步扩展了缓存层次结构,可以提供更大的容量和更高的缓存命中率,减少对主存的访问次数,从而加速数据访问过程
7.6 hello进程fork时的内存映射
当fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
新进程执行execve加载可执行文件hello,先将当前进程虚拟内存空间中的用户部分的已存在的区域结构删除,再为新程序hello的代码、数据、bss段和栈段区域创建新的区域结构,这些新的区域都是私有且写时复制的。代码和数据区域被映射为hello可执行文件中的.text和.data节,bss区域请求二进制0故映射到匿名文件,栈和堆被初始化为空。然后execve会将hello链接的动态链接库(共享对象)映射到虚拟地址空间的共享区域内。最终跳转到hello的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种异常,是可恢复的。DRAM缓存不命中的情况称为缺页。当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。一个页面就是虚拟内存的一个连续的块。缺页异常调用内核中的缺页异常处理程序,缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格,显式分配器和隐式分配器,显式分配器要求应用显示的释放任何已分配的块,隐式分配器要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。
隐式空闲链表:带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
显式空闲链表:将空闲块组织为某种形式的显示数据结构,因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。在显式空闲链表中可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了程序存储管理,分析了逻辑地址、线性地址、虚拟地址、物理地址的区别和联系以及它们的转换。分析了实现虚拟地址向物理地址映射的机制和hello进程fork、execve时的内存映射以及缺页和动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(1)设备的模型化:文件
在Linux系统中,设备被抽象为文件,这是Linux设备管理的一个重要特点。Linux将所有设备都视为文件,并通过文件系统提供对这些设备的访问。这种文件模型化的设计使得对设备的管理更加统一和简单,用户可以像操作普通文件一样来读写设备。
(2)设备管理:unix io接口
在Linux系统中,设备管理主要通过Unix I/O接口来实现。Unix I/O接口提供了一组标准的系统调用函数来进行设备的读写操作,以及对设备的控制和管理。
8.2 简述Unix IO接口及其函数
Unix I/O接口是Unix和类Unix操作系统中用于进行输入输出操作的一组标准接口,它提供了一系列函数来进行文件和设备的读写、控制和管理。
以下是Unix I/O接口中常用的函数:
1. int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
2. int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3. ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4. ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
5. off_t lseek(int fd, off_t offset,int whence),lseek函数将fd文件描述符所指定的文件移动offset字节长度的距离,若成功,返回位移的长度,若失败,返回-1。
8.3 printf的实现分析
printf函数代码如下:
- printf开辟一块输出缓冲区
- 用vsprintf在输出缓冲区中生成要输出的字符串
- 通过write将这个字符串输出到屏幕上
- write会通过syscall陷阱跳到内核,内核的显示驱动程序会通过这些字符串及其字体生成要显示的像素数据,将它们传到屏幕上对应区域的显示vram中
- 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar是stdio.h中的库函数,作用是从stdin流中读入一个字符,当程序调用getchar时,程序等待用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车(回车也在缓冲区中)。当用户键入回车之后,getchar开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar时,确实需要人工的输入,但是如果输入了多个字符,以后的getchar再执行时就会直接从缓冲区中读取了。
异步异常-键盘中断的处理:当用户按键时触发键盘终端,操作系统将控制转移到键盘中断处理子程序,中断处理程序执行,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,显示在用户输入的终端内。当中断处理程序执行完毕后,返回到下一条指令运行。
8.5本章小结
本章主要介绍了Linux的IO设备的基本概念和管理机制,Unix IO接口以及相关函数,以及printf和getchar函数的实现方法与操作过程。
结论
hello经历的历程:
- C语言程序的编写,hello.c
- 预处理,将头文件的内容插入hello.c得到hello.i文件
- hello.i经过编译器的编译,得到hello.s文件
- 汇编器将汇编代码文件hello.s转为二进制形式的可重定位目标文件hello.o
- 将hello.o以及其他可重定位目标文件和动态链接库链接成为可执行目标文件hello
- 输入运行指令:./hello 2022112165 王佳宁 18946314868 3
- OS调用fork函数为hello创建一个子进程,调用execve函数加载运行hello,加载映射虚拟内存,在当前进程的上下文中加载运行hello。
- 程序的运行中伴随着对存储、地址的操作,在hello中的地址为虚拟地址,要将虚拟地址翻译映射到物理地址,才能对该地址进行操作;
- hello运行时执行着各种函数
- 进程在运行过程中会捕获异常处理信号,若有Ctrl+Z或Ctrl+C等信号,hello进程捕获这些信号,并执行相应的异常处理。
- hello运行结束,最终被父进程或者init回收。
计算机系统的完美运行需要系统各个部分的合理协调和共同工作,在这次大作业中,通过一个小小的程序HELLO,让我具体地看到了一个程序从诞生,到运行,到结束的全过程,更好地理解了计算机系统原理,深入了解了计算机中数据的存储方式,汇编指令的含义,CPU的运行逻辑,程序的优化方法,存储器的层次结构,进程以及系统级IO等内容,通过计算机系统课程的学习,对于我对其他计算机课程的学习也提供了很大的帮助。
附件
文件名 | 功能 |
hello.c | 源代码 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello | 链接后的可执行文件 |
hello.elf | 用readelf读取hello.o的ELF格式信息 |
hello.asm | 反汇编hello.o的反汇编文件 |
hello1.elf | 由可执行文件hello生成的.elf文件 |
hello1.asm | 反汇编hello可执行文件得到的反汇编文件 |
参考文献
- Randal E.Bryant,David R.O’Hallaron. 深入理解计算机系统
- https://www.cnblogs.com/pianist/p/3315801.html