哈工大CSAPP大作业——程序人生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 人工智能(2+x)
学   号 202111****
班   级 21wl***
学 生 温**    
指 导 教 师 吴锐

计算机科学与技术学院
2023年5月

摘 要
每个程序员都知道,“Hello, world!” 是编程世界的基石。本文讲述了"Hello"这个词的一生经历。首先,我们编写了一个名为hello.c的源代码文件。然后,对它进行预处理,生成了一个叫做hello.i的文件。接下来,我们将它编译成汇编语言文件,命名为hello.s。再然后,我们将汇编文件转化为可重定位目标文件hello.o。接下来,我们使用链接器ld将hello.o和系统目标文件进行组合,创建了一个可执行目标文件,即hello。
当我们运行程序时,我们输入命令"./hello shell"。程序首先检查输入的命令是否是内置操作,如果不是,就开始调用fork函数创建一个新的进程。然后,使用execve函数将hello加载到内存中,CPU控制着程序的逻辑流程的运行,包括中断、上下文切换和异常的处理。最后,进程结束并由父进程进行回收,这标志着hello的"生命"的尽头。
通过这个过程,我们了解了hello在计算机中的生命周期,从源代码到可执行文件,再到进程的创建和结束。这个过程是程序员们在编程世界中经常经历的,也是他们构建软件的基础。

关键词:预处理;编译;汇编;链接;进程;存储;程序人生

目 录

第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简介
1.通过编辑器gcc编写hello程序建立.c文件,得到hello.c的源程序。
2.通过C预处理器(cpp)将其进行预处理生成hello.i文件(对include define的处理)。
3.通过C编译器(ccl)将其进行翻译生成汇编语言文件hello.s。
4.通过汇编器(as)将其翻译成一个可重定位目标文件hello.o。
5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。(如图所示)
6.通过shell输入./shell,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
7.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
8.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
1.2 环境与工具
1.硬件环境:X64 CPU;2.4GHz;
2.软件环境:Windows11 64位;Vmware 17;Ubuntu 17.1
3.工具:codeblocks;gdb;Objdump;vs2022
1.3 中间结果
文件名称 功能
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
1.4 本章小结
本章总体介绍了hello程序“一生”的过程,以及进行实验时的软硬件环境及开发与调试工具等基本信息。

第2章 预处理
2.1 预处理的概念与作用
1.预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.预处理作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件。

2.2在Ubuntu下预处理的命令
Linux中hello.c文件进行预处理的命令是:gcc -E hello.c
在这里插入图片描述

2.3 Hello的预处理结果解析
利用命令:gcc -E hello.c -o hello.i,可以生成hello.i文件,共有3000多行内容,仍为可以阅读的C语言程序文本文件。预处理的作用是对原程序中的宏进行了宏展开以及头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,如果代码中有#define命令还会对相应的符号进行替换。请添加图片描述

hello.i文件的开头的hello.c文件所涉及的头文件库的信息:
文件的最后是hello.c文件的主要内容:
在这里插入图片描述

2.4 本章小结
本章介绍了预处理的相关概念和作用,进行实际操作查看了hello.i文件,是对源程序进行补充和替换,预处理器确实对源代码进行了大量的展开操作,预处理后的结果仍然是合法的C语言源文件。

第3章 编译
3.1 编译的概念与作用
编译是将源语言编写的程序翻译成计算机可以识别的二进制语言的过程。它包括词法分析、语法分析、语义检查和中间代码生成、代码优化和目标代码生成五个阶段。编译程序的主要作用是让计算机能够理解源语言程序,从而编写出计算机能够运行的语言。与直译语言不同,编译程序不需要将程序一行一行翻译成目标语言。
3.2 在Ubuntu下编译的命令
在终端里输入命令gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64,得到编译结果hello.s。如下图所示:在这里插入图片描述

3.3 Hello的编译结果解析
3.3.1汇编初始部分
节名称 作用
.file 声明源文件
.text 代码节
.section.rodata 只读数据段
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
.size 声明大小
.string 声明一个字符串
.align 声明对指令或者数据的存放地址进行对齐的方式

3.3.2数据
①字符串
程序中有两个字符串,这两个字符串都在只读数据段中,分别如图所示:在这里插入图片描述

hello.c中的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,如图:

被两次调用找参数传给printf。这两个字符串作为printf函数的参数,如图:

②局部变量i
main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置,如图:

③参数argc
参数 argc 作为用户传给main的参数(编译器会根据用户传入的参数得出argc的大小,即传入参数的个数)。也是被放到了堆栈中。
④数组:char *argv[]
char *argv[]是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针(argv[0]一般表示输入命令行的可执行文件名)。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用传给printf。

⑤立即数
立即数直接体现在汇编代码中。

3.3.3全局函数
hello.c声明了一个全局函数int main(int argc,char *argv[]),汇编代码说明main函数是全局函数,如图所示:在这里插入图片描述

3.3.3赋值操作
hello.c中赋值操作是for循环中i=0;在汇编代码中使用mov指令实现,如图:在这里插入图片描述

mov指令根据操作数的字节大小分为:
movb:一个字节
movw:“字”
movl:“双字”
movq:“四字”

3.3.4算数操作
hello.c中的算术操作是i++,汇编语言addl $1, -4(%rbp),其他算数操作如图所示: 在这里插入图片描述

3.3.5关系操作
①hello.c中if(argc!=4);是条件判断语句,进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),在比较之后还设置了条件码,根据条件码判断是否需要跳转。如图:

②hello.c中i<9,作为判断循环条件指令被编译为cmpl $8,-4(%rbp),并设置条件码,为下一步 jle 利用条件码进行跳转做准备。如图:

3.3.6控制转移指令
汇编语言中先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:
①判断argc是否等于4,如果argc等于4,则不执行if语句,否则执行if语句,对应的汇编代码为:

②for循环中,每次判断i是否小于等于8来决定是否继续循环,对应的汇编代码为:在这里插入图片描述

3.3.7函数操作
调用函数时有以下操作:(假设函数A调用函数B)
① 传递控制:调用过程A的时候,程序计数器(%rip)必须设置为函数A代码的起始地址,然后在返回时,要把程序计数器(%rip)设置为callA指令后面那条指令的地址。
② 传递数据:函数A必须能够向函数B提供一个或多个参数,B必须能够向A中返回一个值。
③ 分配和释放内存:在开始时,B可能需要为局部变量分配空间(对于局部变量的空间是在堆栈中),而在返回前,又必须释放这些空间。
hello.c中的函数操作:
main函数:参数是int argc,char *argv[]
printf函数:参数是argv[1],argv[2]
exit函数:参数是1
sleep函数:参数是atoi(argv[3])
getchar函数:无参数
3.4 本章小结
本章主要介绍了编译器处理c语言程序的基本过程,函数从源代码变为等价的汇编代码,编译器分别从c语言的数据,类型转换,赋值语句,算术操作,逻辑/位操作,关系操作,控制转移与函数操作这几点进行分析,通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言,提高了反向工程的能力。

第4章 汇编
4.1 汇编的概念与作用
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。它是机器语言的一种,由助记符代替操作码,由地址符号或标号代替地址码。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,因此不同平台上的汇编语言可能有所不同。
汇编语言的作用:
(1)提供一种更接近计算机硬件的低级语言,用于实现更高效的程序。
(2)为程序员提供一种更直接的方式,以便更好地理解计算机的工作原理。
(3)可用于实现嵌入式系统、操作系统、驱动程序等复杂软件系统。
(4)汇编语言可以用来编写病毒、木马等恶意软件。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

命令:readelf -a hello.o > hello.elf
在这里插入图片描述

.elf文件中的内容:
(1)ELF头:
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。如图:
在这里插入图片描述

(2)节头:
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。如图:
在这里插入图片描述

(3)重定位节:
.rela.dyn保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用本地函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。如图:
在这里插入图片描述

.rel.plt保存了重定位表的信息,可以使用lazy的连接方式。如图:
在这里插入图片描述

(4)符号表:
.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。如图:
在这里插入图片描述

4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello1.txt
在这里插入图片描述

与hello.s的差异:
(1)分支转移:
hello.s:

hello1.txt:

反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址。在反汇编代码中,分支转移表示为函数地址+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(2)对函数的调用与重定位条目对应
hello.s :

hello1.txt :

在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在汇编文件中可以看到,call后面直接加的是文件名。
(3) 立即数变为16进制格式
hello.s :

hello1.txt :

在可重定位目标文件中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了汇编语言与机器语言的对应关系。

第5章 链接
5.1 链接的概念与作用
链接(Link)是电子计算机程序中各个模块之间传递参数和控制命令的机制,是计算机程序中各个模块之间的连接关系。在程序运行过程中,各个模块之间通过链接进行交互和协作,以完成特定的任务。
链接的概念可以应用于不同的领域。在计算机科学领域中,链接可以指代不同的文件、数据结构、函数等程序元素之间的连接关系。在互联网领域中,链接可以指代网页之间的连接关系,通过点击链接可以从一个网页跳转到另一个网页。在操作系统和网络协议领域中,链接可以指代设备和应用程序之间的连接关系,例如文件传输协议(FTP)中的链接。
链接的作用在于建立各个模块之间的联系,使得它们可以相互通信、共享数据和资源,以及实现复杂的计算任务。通过链接,程序中的各个模块可以独立地进行修改和升级,而不影响其他模块的运行。这使得程序的维护和更新变得更加容易。同时,链接也为用户提供了更加灵活和便捷的使用方式,可以通过点击链接来跳转页面、打开文件、执行操作等。
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的格式
命令:readelf -a hello > hello1.elf

(1)ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节。如图:
在这里插入图片描述

(2)节头:对 hello中所有的节信息进行了声明,包括大小和偏移量。如图:

在这里插入图片描述

(3)重定位节.rela.text:
在这里插入图片描述

(4)符号表.symtab:存放程序中定义和引用的函数和全局变量。
在这里插入图片描述

5.4 hello的虚拟地址空间
虚拟地址从0x401000开始, 到0x401ff0结束。
在这里插入图片描述在这里插入图片描述

5.5 链接的重定位过程分析
命令: objdump -d -r hello > hello2.txt
与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。如图:
可以看到整体的节数变多了,由于链接中加载了调用的printf.o等模块,使得程序整体的节数增加。同时虚拟内存的地址也稍有不同。相当于为其重新设定了内存地址。
在这里插入图片描述

重定位的过程分为两大步:
(1)重定位节和符号定义:在这一步中,连接器将所有相同类型的节合并成为同一类型的新的聚合节。例如,来自所有输入模块的.data节全部被合并成一个节,这个节成为输出的可执行目标文件的.data节。
(2)重定位节中的符号引用:在这一步中,连接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。要执行这一步,连接器依赖于可重定位条目,及5.3节中分析的那些数据。
5.6 hello的执行流程
(1)开始执行:_start、_libc_start_main
(2)执行main:_main、_printf、_exit、_sleep、_getchar
(3)退出:exit
程序名 程序地址
_start 0x400550
_libc_start_main 0x40057a
Main 0x400582
_printf 0x400500
_exit 0x400530
_sleep 0x400540
_getchar 0x400510

5.7 Hello的动态链接分析
动态链接的基本思想是将链接的过程推迟到运行时再执行。在程序运行时,需要加载的模块才会被动态地链接起来。因此,动态链接可以在运行时动态地组装程序,而不需要在编译时就确定所有模块的完整路径在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
延迟绑定是通过GOT和PLT实现的,根据hello ELF文件可知,GOT起始表位置为0x601000。
5.8 本章小结
本章主要介绍了链接的概念与作用,链接可分为符号定义和重定位,了解了可执行文件的ELF格式,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程,对链接有了更深的理解。

第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基础,是操作系统结构的基础。在计算机科学中,进程是指一个程序在计算机上的一次运行活动。它是一个抽象的概念,用于描述CPU资源分配的基本单位。进程是具有独立功能的程序关于某个数据集合的一次运行活动,它申请和拥有系统资源,是一个动态的概念。进程是一个活动的实体,不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
进程的作用在于:
(1)提高资源利用率:进程是系统进行资源分配和调度的基本单位,通过多进程并发执行多个任务,可以充分利用系统资源,提高资源利用率。
(2)实现任务的分离:进程实现了程序的分离,不同的进程之间相互独立,不会互相干扰和影响,有助于保护系统的稳定性和安全性。
(3)提供交互性:进程可以通过输入输出操作与用户进行交互,实现程序的调试和测试,以及提供更好的用户体验。
(3)支持并行处理:进程支持并发执行多个任务,可以实现并行处理,提高程序的执行效率和响应速度。
总之,进程是操作系统的重要概念,是系统进行资源管理和调度的基本单位,也是实现并发执行和并行处理的基础。
6.2 简述壳Shell-bash的作用与处理流程
壳Shell(也称为壳Bash)是一种命令行解释器,用于在Unix、Linux和其他操作系统上执行命令和脚本。它是操作系统中重要的用户界面,允许用户与计算机进行交互。
壳Shell的作用:
(1)提供用户与计算机交互的接口。用户可以在Shell中输入命令,并由Shell解释执行。
(2)执行脚本。Shell可以执行各种脚本文件,例如bash脚本、Python脚本、Perl脚本等。
(3)管理进程。Shell可以启动、停止和管理操作系统中的进程。
(4)访问文件系统。Shell提供了访问文件系统的命令,例如cd、ls、mkdir等。
执行系统管理任务。Shell可以执行各种系统管理任务,例如修改环境变量、安装软件等。

处理流程:
(1)打开Shell:用户打开终端,并输入Shell命令行界面(也称为Shell命令行解释器)。
(2)提示符:Shell显示一个提示符(也称为“$”),告诉用户输入命令的开始。
(3)用户输入命令:用户输入要执行的命令,例如cd、ls、mkdir等。
(4)接收命令:Shell接收用户输入的命令,并将其保存在内存中。
(5)解析命令:Shell解析用户输入的命令,并将其分解成更小的单元,例如单词、短语、参数等。
(6)查找命令:Shell在系统的命令路径中查找命令。
(7)执行命令:如果找到了命令,则Shell执行该命令并输出结果。如果没有找到命令,则Shell显示错误消息。
(8)退出Shell:当用户输入exit命令时,Shell退出并关闭终端。
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 2021112957温家杰后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,它被调用一次从不返回。
在 execve() 函数执行时,会先将当前进程的所有用户数据空间清空,并将新进程的映像加载到该空间中。然后,将新的 argv 和 envp 数组中的内容复制到新的用户数据空间中。最后,将新的用户数据空间的栈指针设置为新程序的栈指针,并跳转到新程序的入口点开始执行
6.5 Hello的进程执行
进程提供给应用程序的关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们程序独占的使用处理器。
一个私人的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
操作系统所提供的进程抽象:
(1)逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
(2)上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
(3)时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(4)用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
(5)上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 2021112957温家杰,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
在这里插入图片描述

6.6 hello的异常与信号处理
(1)异常可以分为4个类别:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。以下是这四个类别的异常的属性:
中断(interrupt):由硬件或软件引起的异常,例如键盘输入、时钟中断等。
陷阱(trap):由编译器或操作系统提供的一种机制,用于捕获特定类型的异常。
故障(fault):由于程序错误而引起的异常,例如除以零、数组越界等。
终止(abort):由程序员显式地抛出,用于在紧急情况下终止程序的执行。
这四种异常在程序中都有可能出现,并且需要程序员编写适当的代码来处理它们。处理异常可以提高程序的健壮性和可靠性,从而使程序能够更好地应对各种异常情况。、
(2)运行结果
1、正常运行
在这里插入图片描述

2、按下 Ctrl-z

ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用ps命令可以看到,hello进程并没有被回收。调用 fg 将其调到前台,如果已打印完,则进程被回收,如下图:
在这里插入图片描述在这里插入图片描述

3、按下Ctrl+c
在这里插入图片描述

![在这里插入图片描述](https://img-blog.csdnimg.cn/aed6154308e34f228ac5ab1a03bf7571.png#pic_center
在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程,如图所示:
在这里插入图片描述

4、不停乱按
无关输入被缓存到stdin,并随着printf指令被输出到结果。如图:
在这里插入图片描述

6.7本章小结
(本章介绍了进程的概念和作用、shell-bash的处理过程与作用并且着重分析了调用fork创建新进程,调用execve函数执行hello,hello的进程执行过程,以及hello在运行时遇到的异常与信号处理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3.虚拟地址:就是线性地址。
4.物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel平台下,逻辑地址的格式为 段标识符:段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。分段机制将逻辑地址转化为线性地址的步骤如下:

  1. 确定段基址(Segment Base Address):每个段都有一个对应的段基址,它是该段在内存中的起始地址。
  2. 计算偏移量(Offset):逻辑地址中的偏移量指相对于当前段基址的地址。通过逻辑地址中对应的偏移量和段基址可以计算出实际的内存地址。
  3. 将段基址和偏移量相加:将段基址和偏移量相加,得到线性地址。
    7.3 Hello的线性地址到物理地址的变换-页式管理
    线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成。
    分页机制将虚拟地址转换为物理地址的步骤如下:
  4. 确定页表起始地址(Page Table Start Address):页表是用于将虚拟地址转换为物理地址的表格。操作系统会将页表的起始地址加载到内存中。
  5. 确定页表项(Page Table Entry,PTE):在页表中,每个条目都称为一个页表项。每个页表项都包含一个物理地址和一个访问权限位。
  6. 确定虚拟地址中的页号(Page Number):虚拟地址由两部分组成:页号和页内偏移量。页号用于在页表中查找对应的页表项。
  7. 在页表中查找相应的页表项:通过页号,操作系统可以在页表中查找对应的页表项。
  8. 检查该页表项是否包含有效的物理地址:在访问虚拟地址之前,操作系统必须确保该虚拟地址对应的是一个有效的物理地址。
  9. 检查该页表项的访问权限位:如果该页表项包含一个有效的物理地址,操作系统还需要检查该页表项的访问权限位,以确定是否有权限访问该地址。
  10. 将物理地址和偏移量相加:如果访问权限位允许访问该物理地址,操作系统会将该物理地址和虚拟地址中的偏移量相加,得到实际的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

  1. 确定页表起始地址:操作系统会将页表的起始地址加载到内存中。
  2. 确定虚拟地址中的页号和页内偏移量:虚拟地址由两部分组成:页号和页内偏移量。页号用于在页表中查找对应的页表项,页内偏移量则是在找到对应的页表项后用于计算物理地址的一部分。
  3. 在页表中查找相应的页表项:通过页号,操作系统可以在页表中查找对应的页表项。
  4. 检查该页表项是否包含有效的物理地址:在访问虚拟地址之前,操作系统必须确保该虚拟地址对应的是一个有效的物理地址。
  5. 检查该页表项的访问权限位:如果该页表项包含一个有效的物理地址,操作系统还需要检查该页表项的访问权限位,以确定是否有权限访问该地址。
  6. 将物理地址和偏移量相加:如果访问权限位允许访问该物理地址,操作系统会将该物理地址和虚拟地址中的偏移量相加,得到实际的物理地址。
  7. 检查是否命中TLB:如果TLB(Translation Lookaside Buffer)缓存了刚刚进行的虚拟地址到物理地址转换的结果,那么操作系统可以直接从TLB中获取物理地址,而无需再次进行虚拟地址到物理地址的转换。
  8. 重复步骤1-7,直到找到最终的物理地址为止。
    7.5 三级Cache支持下的物理内存访问
    三级缓存(三级缓存)是计算机CPU的缓存级别。通常情况下,CPU的缓存分为四级:L1、L2、L3和L4。其中,L1和L2缓存通常集成在CPU中,而L3缓存则位于主板上,L4则是更高速的存储设备,如SSD。CPU访问内存时,需要遵循一定的访问次序和机制。下面是三级Cache支持下的物理内存访问步骤:
  9. 确定需要访问的数据是否在CPU的寄存器中。如果数据已经在寄存器中,CPU可以直接访问该数据。
  10. 如果数据不在寄存器中,CPU会首先检查一级缓存(L1缓存)。如果数据在一级缓存中,CPU可以直接访问该数据。
  11. 如果数据不在一级缓存中,CPU会检查二级缓存(L2缓存)。如果数据在二级缓存中,CPU可以直接访问该数据。
  12. 如果数据不在二级缓存中,CPU会检查三级缓存(L3缓存)。如果数据在三级缓存中,CPU可以直接访问该数据。
  13. 如果数据既不在一级缓存中,也不在二级缓存和三级缓存中,CPU需要从主存储器(如内存)中读取数据。从主存储器中读取数据比从缓存中读取数据速度要慢得多。
  14. 为了提高CPU访问数据的效率,操作系统和硬件会根据数据的使用频率和重要性等因素来管理缓存中的数据。如果数据在某个时刻不再被频繁使用,它可能会被从缓存中移除,以便为新的数据腾出空间。
    7.6 hello进程fork时的内存映射
    当程序调用 fork() 函数时,它会返回两次:一次是在父进程,另一次是在子进程。这是通过使用 copy-on-write 技术实现的,该技术允许父子进程共享相同的虚拟地址空间。
    在 fork() 调用之后,父子进程的虚拟地址空间被映射到相同的物理内存,但它们之间是相互独立的。这意味着它们可以同时访问相同的虚拟地址,但它们不会相互干扰。这是因为每个进程都有自己的虚拟地址空间,并且操作系统会确保它们不会相互冲突。
    在 Linux 中,父进程和子进程之间的内存映射关系由拷贝控制区域 (VMA, Virtual Memory Area) 来表示。每个 VMA 对象表示一个进程中的一段虚拟地址范围。VMA 对象包含有关该地址范围的访问权限、映射的物理内存页面等信息。
    在 fork() 调用之后,父进程和子进程的 VMA 对象被复制,以反映它们各自的虚拟地址空间。但是,实际的物理内存页面只有在被访问时才会被复制。这是因为 fork() 函数使用了写时复制 (Copy-on-Write) 技术,它允许父子进程共享相同的物理内存页面,直到它们中的某个进程试图修改这些页面时才会发生复制。
    7.7 hello进程execve时的内存映射
    在Linux系统中,execve()函数用于将新进程映像加载到现有进程的地址空间中,它实现了文件的共享和替换机制,具有更灵活和高效的特点。
    在execve()函数调用时,会进行以下操作:
  15. 打开文件:open()函数调用,获取指定文件路径的file data结构。
  16. 拷贝参数:将用户地址空间的命令行参数和环境变量复制到内核地址空间。
  17. 释放内存:释放与原程序相关的内存,如打开的文件、共享库等。
  18. 修改页表:更新进程的页表,以反映新进程的地址空间布局。
  19. 执行代码:修改页表后,CPU开始执行新进程的代码。
    7.8 缺页故障与缺页中断处理
    缺页故障(Page Fault)是指在计算机执行程序时,需要访问的页面(或称为页)不在主存(RAM)中的情况。当程序访问一个不在主存中的页面时,就会发生缺页故障。
    缺页中断处理是指当发生缺页故障时,操作系统介入并处理该故障的过程。下面是缺页中断处理的基本步骤:
  20. 产生中断:当程序访问不在主存中的页面时,CPU会产生一个缺页中断信号,将控制权交给操作系统。
  21. 中断处理程序执行:操作系统会保存当前程序的上下文(包括寄存器状态、程序计数器等),并开始执行缺页中断处理程序。
  22. 判断缺页原因:缺页中断处理程序首先会判断缺页的原因,可能是由于页面不存在于主存中(缺页错误),或者是页面存在于主存但没有合法访问权限(保护错误)。
  23. 处理缺页错误:如果是缺页错误,操作系统需要将所需的页面从辅存(通常是硬盘)加载到主存中,更新页表等数据结构,使得该页面对程序可见。
  24. 处理保护错误:如果是保护错误,操作系统会检查程序是否有足够的权限来访问该页面。如果权限不足,操作系统会终止程序的执行或者给予适当的权限。
  25. 恢复上下文:处理完缺页错误或保护错误后,操作系统会恢复被中断的程序的上下文,包括寄存器状态、程序计数器等。
  26. 重新执行被中断的指令:操作系统将控制权交还给被中断的程序,并重新执行引发缺页中断的指令。
    7.9动态存储分配管理
    动态存储分配管理的基本方法与策略有如下几种:
  27. 首次适应(First Fit):空闲区按照起始地址从小到大排列,从链首开始查找,直到找到一个能满足要求的空闲区为止,并从中划出一块与请求大小相等的内存空间。
  28. 循环首次适应(Next Fit):在查找空闲区时,不再每次从链首开始查找,而是从上一次找到的空闲区的下一个空闲区开始查找,直到找到一个能满足要求的空闲区为止,并从中划出一块与请求大小相等的内存空间。
  29. 最佳适应(Best Fit):总是把满足要求,又使最小的空闲区分配给请求作业,即在空闲区表中,按空闲区的大小从小到大排列,建立索引,当用户作业请求内存空间时,从索引表中找到第一个满足该作业的空闲区分给它。
  30. 最差适应(Worst Fit):总是把最大的空闲区分配给请求作业,空闲区表(空闲区链)中的空闲分区要按大小从大到小进行排序,自表头开始查找到第一个满足要求的空闲分区分配给作业。
  31. 可利用空间表(Free List):目录表和链表。
  32. 边界标识法(Boundary Tagging):在每个内存区的头部和底部两个边界上分别设有标识,以标识该区域为占用块还是空闲块。
    下面是一般的动态申请内存的过程:
    1、确定内存需求:首先,程序需要确定所需的内存大小。这可以通过计算数据结构的大小、需要存储的数据量等方式来确定。
    2、调用内存分配函数:一般情况下,编程语言或操作系统提供了内存分配函数,例如C语言中的malloc()或C++中的new运算符。程序可以调用这些函数来申请所需的内存空间。
    3、指定内存大小:在调用内存分配函数时,需要指定所需内存的大小。函数会尝试找到足够大小的内存块,并将其分配给程序。
    4、分配内存空间:内存分配函数会在可用内存区域中寻找足够大小的连续空间。如果找到了足够的空间,就会将其标记为已分配,并返回一个指向该内存块的指针。
    5、错误处理:如果没有足够的连续内存空间供分配,内存分配函数可能会返回一个空指针或者抛出异常,表示内存分配失败。程序可以根据返回值进行错误处理,例如释放已分配的内存并采取适当的措施。
    6、使用内存:一旦内存分配成功并返回了指向内存块的指针,程序就可以使用该内存空间存储数据或执行其他操作。
    7、释放内存:在动态申请内存后,当不再需要使用该内存时,应该显式地释放内存。这可以通过调用内存释放函数,例如C语言中的free()或C++中的delete运算符来完成。释放内存后,该内存空间可以被重新分配给其他需要的程序。
    7.10本章小结
    本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
Hello经历的过程如下:

  1. 输入:输入源代码得到hello.c的源程序。
  2. 预处理:使用命令gcc -E进行预处理使得hello.c成长为hello.i。
  3. 编译:使用命令gcc -S进行编译使得hello.i成长为hello.s(汇编语言文件)。
  4. 汇编:使用命令gcc -c进行汇编使得hello.s成长为hello.o(可重定位目标文件)
  5. 链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程
    序hello,至此可执行hello程序正式诞生。
  6. 运行:在shell中输入./hello 2021112957 温家杰1
  7. 创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()函数创建一个子进程。
  8. 加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
  9. 执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
  10. 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
  11. 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
  12. 信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
  13. 终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。至此,hello的一生就此结束!

附件
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式

参考文献
[1] 深入理解计算机系统原书第3版-文字版.pdf
[2] https://blog.csdn.net/sy06106/article/details/118274118

猜你喜欢

转载自blog.csdn.net/m0_64206188/article/details/130834441