计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 *
学 号 *
班 级 *
学 生 *
指 导 教 师 *
计算机科学与技术学院
2023年4月
本文以程序人生-Hello’s P2P为题,揭示了编程界的传奇hello的一生。当hello.c源程序被编写完毕时,hello开启了它的传奇一生——从预处理、编译、汇编、链接生成可执行文件到在系统上运行hello,再到运行完毕被回收,hello结束了它的传奇一生,完成了P2P与020的过程。hello的传奇一生反映了计算机系统底层的基本内容与基本原理,跟踪hello的一生就是深入理解计算机系统的过程。
关键词:P2P、020、预处理、编译、汇编、链接、进程
目 录
第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 -
第1章 概述
1.1 Hello简介
P2P即From Program to Process,从程序到进程,顾名思义,hello程序从program,也就是hello.c开始,到process,也就是可执行程序hello结束,在我们的计算机上,可能只需要点击一下鼠标即可,但其实在这背后却包含了四个阶段。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统。
第一个阶段是预处理阶段,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,通常
是以.i作为文件扩展名。
接下来是编译阶段,编译器(cc1)将其翻译为汇编程序hello.s。
然后进行汇编阶段,汇编器(as)把汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序hello.o。
最后完成链接阶段,通过链接器(ld)调用标准C库中的函数的单独的预编译好了的目标文件(如printf.o)把其以某种方式与hello.o文件进行合并,得到可执行目标程序hello。这就完成了hello的整个P2P的过程。
图1.1.1编译系统
020,即From Zero to Zero,从无到有再到无。指的是用fork()函数在shell中创建子进程,再用exceve语句加载可执行程序hello,此时,操作系统为其分配虚拟内存,映射到物理内存,完成了从无到有。内存管理器和中央处理器在执行过程中调用三级cache、快表(TLB)、内存等行物理内存上的取数据操作,再通过I/O根据代码指令输出。程序执行完后,对其进行回收,操作系统内核把它从操作系统清除,完成了从有到无。这就是整个020的过程。
1.2 环境与工具
发与调试工具。
X64 CPU;2GHz;16G RAM;256GHD Disk
软件环境:
Windows 10、Vmware16.2.4、Ubuntu 22.04.2
开发工具:
Vi/Vim/gedit/gcc
1.3 中间结果
hello.c:C语言源程序文件
hello.i:预处理后生成的文件
hello.s:编译产生的汇编程序文件
hello.o:汇编产生的可重定位目标文件
hello:链接产生的可执行文件
elf.txt:hello.o的elf格式文件
elf1.txt:hello的elf格式文件
asm.txt:hello.o反汇编的结果文件
asm1.txt:hello反汇编的结果文件
1.4 本章小结
本章介绍了hello的P2P以及020的过程,并且介绍了本次大作业的软硬件环境及开发工具。同时还列出了完成大作业的中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理也叫做预编译,是指在正式开始编译前的操作。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,读取系统头文件中的内容并把它直接插入程序文本,获得另一个文件后缀为.i的文件。
预处理的作用:
(1)能够完成头文件的包含,将包含的文件插入到程序文本中;
(2)可以进行宏替换,把它的符号用实际存在的常量加以替换;
(3)删除注释部分;
(4)实现特殊控制指令(如#error);
(5)选择符合条件的代码送至编译器编译,完成条件编译,有选择地执行相关操作(如#if,#elif,#else)。
2.2在Ubuntu下预处理的命令
预处理的命令为:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
图2.2.1预处理命令
生成的hello.i文件如图所示:
图2.2.2生成的hello.i文件截图
2.3 Hello的预处理结果解析
分别打开hello.c和hello.i两个文件的内容如图所示。.i文件最后的代码段与原来的hello.c源程序的代码是相同的,即预处理没有对代码段进行处理。同时我们发现hello.i文件中删除了.c文件中原本的注释部分,也即预处理会删除注释部分。
图2.3.1源文件hello.c的内容
图2.3.2的hello.i文件
从下图我们还能看到,程序的最开始的几行给出了这个对该文件的解释。此外,程序中把stdio.h,unistd.h,stdlib.h三个文件解析出来,找到它们的实际地址,把其中的语句直接插入到hello.i文件中,这也是导致hello.i文件有约三千行代码,远远多于hello.c的几十行代码的最主要原因。
图2.3.3对头文件的解析
2.4 本章小结
本章介绍了预处理的概念和作用,在Ubuntu下进行预处理的命令,给出了预处理后的结果hello.i文件,并对hello.c源文件和hello.i文件进行比较,分析了其中的关联以及变化,
第3章 编译
3.1 编译的概念与作用
编译的概念:把用高级语言(如C语言)所写出的源程序翻译成汇编语言的目标程序。
编译的作用:编译时,编译器(cc1)把预处理后的hello.i文本文件翻译成汇编语言程序hello.s。把方便程序员编写的顶层高级语言源程序翻译成为更加贴近底层机器指令的汇编语言程序,使得程序员更加清晰地“看到”程序在底层上是如何实现的。同时,编译器会根据编译等级选项对程序进行一些适当的优化,以期提高程序的执行效率。
3.2 在Ubuntu下编译的命令
命令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
图3.2.1 Ubuntu下编译的命令
编译后生成hello.s文件如图所示:
图3.2.2生成hello.s文件
3.3 Hello的编译结果解析
3.3.1数据
(1)常量
我们可以看到第3行的.rodata(read only data)表明下面的第六行的.string数据为只读,也即源程序两个printf函数中的字符串常量。而第21行中的$32表示的是32这个立即数,是数字常量。
(2)局部变量
局部变量通常保存在寄存器或栈中。我们的程序中有三个局部变量:i、argc、argv。我们以argc为例,分析其过程。程序的第22行表明如下信息:%edi中为传入的参数argc,-20(%rbp)表示要把其值送入栈中,接下来可以通过rbp的相对偏移量进行访问。
图3.3.1数据
3.3.2赋值
在汇编语言中,通常使用mov指令进行赋值,根据赋值的字长不同,后面跟上不同的字母。如汇编代码中第28行把1赋值给%edi寄存器内部。
图3.3.2赋值
3.3.3类型转换
在程序的第48行,跳入atoi函数把字符串显式转换成整型int
图3.3.3跳入atoi函数完成类型转换
3.3.4算术操作
在程序的51行,完成了一个加法操作,并且结合下面的代码可以看出,这是对存放于栈中-4(%rbp)位置的局部变量i的值进行加法操作,每次加1直到它的值达到5才不发生跳转而是继续执行。
图3.3.4算术操作
3.3.5关系操作
使用cmp语句(后面接上表示数据长度的字母)来完成一个比较操作。如程序的第24行比较立即数4和-20(%rbp)中的值是否相等。这就是一个关系操作。
图3.3.5第24行的关系操作
3.3.6数组/指针/结构操作
如图所示,源代码中对数组的操作在汇编程序中变为对地址的加减操作。其中-32(%rbp)存放数组首地址即argv[0],后续在其基础上进行地址偏移,完成了对数组的访问。
图3.3.6数组访问
3.3.7控制转移
程序的32行是一个无条件跳转。而25行是一个条件跳转,只有当-20(%rbp)的值等于4时才发生跳转。
图3.3.7控制转移
3.3.8函数操作
在hello.c源程序中共有5个函数调用,由于printf函数调用了2次,而其他函数都是1次,所以总的调用次数是6次,如图所示,除main主函数外,其他标红色的部分就是函数调用:
图3.3.8C语言源程序中的函数
对于printf函数的调用如下图所示,编译器采取用puts函数优化的方式进行打印,把要打印的数据放在%rdi寄存器中作为函数参数,然后call指令调用函数。其他函数调用的过程类似,如图所示:
图3.3.9汇编程序中的函数调用
3.4 本章小结
本章介绍了编译的概念、作用、功能。并在Ubuntu下把hello.i文件汇编生成hello.s文件。并根据C语言中的不同的数据类型、操作类型以及函数调用等对hello.s文件中的汇编语句进行分析、比对、解析。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编是指经过汇编器(as)把汇编语言程序(hello.s)翻译成机器指令,并把这些指令打包成可重定位目标程序的形式,并保存在hello.o文件中。
汇编的作用:把汇编语言翻译成计算机能够直接执行的0、1机器语言,把文本文件转化成二进制文件。
4.2 在Ubuntu下汇编的命令
命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图4.2.1Ubuntu下汇编的命令
汇编后结果如图所示:
图4.2.2汇编后结果所示
4.3 可重定位目标elf格式
查看hello.o的elf格式文件如下,把其输出到文本文件elf.txt中
图4.3.1Ubuntu下生成elf.txt文件命令
图4.3.2生成的elf.txt文件
ElF文件包括ELF头、节头表、重定位节、符号表等,并且在其中列出了各节的基本信息,包括类型、地址等等,具体的分析如下:
(1)ELF头
如图4.3.3所示,ELF头中描述了文件类别、数据存放方式、版本号、操作系统等信息。并且给出了入口点地址和程序头起点以及节头表的偏移。
图4.3.3ELF头
(2)节头表
如图4.3.4所示,节头表中描述了文件中各个节的类型、大小、地址、偏移、读写访问权限等信息。
图4.3.4节头表
(3)重定位节
文件中有两个重定位节,分别是.rela.text和.rela.eh_frame节。不同的重定位节对应不同节的重定位信息。连接器在处理目标是要对目标文件进行重定位,这时需要重定位节中的信息才能知道如何进行重定位。如图4.3.5所示,偏移量指出重定位的字节偏移量,类型指出重定位类型等。有了这些信息才能正确地进行重定位。
图4.3.5.rela.text重定位节
(4)符号表
如图4.3.6所示,在符号表中列出了程序中所有定义和引用的全局变量以及函数的信息:
图4.3.6符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
在Ubuntu下利用反汇编指令生成asm.txt反汇编文件,如图4.4.1和图4.4.2所示:
图4.4.1反汇编指令
图4.4.2生成的反汇编文件
对反汇编文件asm.txt和汇编程序hello.s比对分析如下,下面的图示4.4.3中,汇编程序hello.s在左,反汇编程序asm.txt在右:
图4.4.3;两种文件的对比分析
(1)数字的进制变化:
左边的汇编程序在进行栈指针的移动时运用十进制数,而到右侧变成了十六进制数,也即二进制数。
(2)函数调用:
在hello.s中函数调用直接引用函数名称,而在反汇编文件中,函数的调用是用call指令加上待引用函数的首地址来形成这条指令。
(3)分支转移:
在hello.s中分支转移目标的目标位置用.L表示,即给出一个跳转的索引位置(助记符),类似C语言程序中的Goto flag语句,而反汇编程序中每一个要跳转的分支都是直接给出跳转地址。
4.5 本章小结
本章介绍了汇编的概念和作用,并在Ubuntu下实际操作进行汇编,被汇编程序hello.s转化成可重定位目标文件hello.o。通过readelf分析了hello.o的ELF格式,分析了其各节的基本信息,包括ELF头、节头表、重定位节以及符号表的功能与包含的信息。同时还用objdump反汇编生成asm.txt反汇编文件,并与汇编程序文件进行比对分析。
第5章 链接
5.1 链接的概念与作用
链接的概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小,更好管理的模块,可以独立的修改和编译这些模块,当我们改变这些模块中的一个时,只需简单的重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接令
在Ubuntu下使用链接命令
图5.2.1Ubuntu下的链接命令
生成hello可执行程序
图5.2.2生成hello可执行程序
5.3 可执行目标文件hello的格式
通过指令readelf -a hello > elf1.txt来把elf文件写入到elf1.txt中如下图所示:
图5.3.1生成elf1.txt文件
打开elf1.txt,如图5.3.2所示,可以看到可执行文件hello的elf格式,其中最左侧一列为节名,第二列为节类型,第三列为起始地址,第四列是偏移量。
图5.3.2hello的elf格式文件
5.4 hello的虚拟地址空间
首先使用edb加载hello,查看本进程的虚拟地址空间各段信息,如图5.4.1所示。
图5.4.1在edb中加载hello
与5.3对照分析说明如下:在edb中的虚拟地址和在elf文件中看到的虚拟地址是对应相同的,具体如下:在elf文件中我们可以看到节头信息如图5.4.2左侧所示,在edb中我们可以选择打开symbol viewer进行loaded symbls,也会加载出节头的虚拟地址信息,如图5.4.2右侧所示,可以看出,两侧的虚拟地址是对应相同的。
图5.4.2虚拟地址对应
上图中我们画出了.interp的对应,我们还可以在edb中利用goto expression进入地址查看信息。比如我们进入地址0x400000中,找到我们的elf头,如图5.4.3所示:
图5.4.3elf头
5.5 链接的重定位过程分析
运用objdump -d -r hello > asm1.txt把hello反汇编写入asm1.txt如图5.5.1和图5.5.2所示,接下来我们对asm1.txt(hello的反汇编文件)和asm.txt(hello.o的反汇编文佳)进行分析比对,这实际上就是在比较hello与hello.o的不同,具体分析如下:
图5.5.1反汇编hello
图5.5.2反汇编生成asm1.txt
(1)为指令分配虚拟地址
如图5.5.3所示,左侧是链接前的反汇编,右侧是链接后的。在hello.o的反汇编程序中,函数中的语句前面的地址都是从函数开始从依次递增的,而不是虚拟地址;而经过链接后,在右侧的返回变种每一条指令都被分配了虚拟地址。
图5.5.3为指令分配虚拟地址
(2)函数调用
如图5.5.4所示,在左侧helllo.o的反汇编程序中,图中选中的函数调用指令由于还没有分配虚拟地址,所以只能用偏移量跳转,而右侧链接后已经分配好了虚拟地址,可以直接用call虚拟地址进行跳转.
图5.5.4函数调用
(3)跳转指令
同理函数调用,链接后可以采用虚拟地址跳转,如图5.5.5所示
图5.5.5跳转指令
(4)调入C标准库函数
在hello.o中只有main函数段,还没有把标准库函数插入,经过链接后,调用的C标准库函数的代码被插入其中,如图5.5.6所示。
图5.5.6链接过程插入C语言标准库函数
综上所述,链接的过程主要分为两个过程:符号解析和重定位。
符号解析时解析目标文件定义和引用符号,并建立每个符号引用和符号定义之间的关联。
重定位时分为两个步骤,先重定位节和符号定义,把相同类型的节合并,并为其分配内存。接下来进行符号引用的重定位,修改代码和数据中对符号的引用,使得他们指向正确地址。
5.6 hello的执行流程
各个函数名和对应的地址如下:
<_init>:401000
<.plt>:401020
<puts@plt> :401090
<printf@plt>:4010a0
<getchar@plt>:4010b0
<atoi@plt>:4010c0
<exit@plt>:4010d0
<sleep@plt>:4010e0
<_start>:4010f0
<_dl_relocate_static_pie>:401120
<main> :401125
<_libc_scu_init>:4011c0
<_libc_csu_fini>:401230
<_fini>: 401238
5.7 Hello的动态链接分析
当程序调用共享链接库时,我们无法预测这个函数的地址,因为定义他的模块可以再运行时加载到任何位置。这时采用延迟绑定的策略,把过程地址的加载推迟到第一次调用该进程。动态链接器使用GOT(全局偏移量表)和PLT(过程链接表)实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GOT中地址跳转到目标函数。GOT和PLT信息如图5.7.1所示。
图5.7.1.got和.got.plt信息
我们可以在edb中找到该节的内容并观察其在运行dl_int前后内容的变化,如图5.7.2和5.7.3所示,有内容发生变化。
图5.7.2运行dl_int前的信息
图5.7.3运行dl_int后的信息
5.8 本章小结
(1)介绍了链接的概念以及作用,在Ubuntu下完成了链接的过程,并从可执行文件的elf格式文件中分析其信息。
(2)对hello进行反汇编与前一章节中的hello.o的反汇编程序比对,分析出链接这一环节所做的工作。
(3)介绍了链接的两个过程并,符号解析和重定位
(4)简要介绍分析了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程的经典定义就是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的程序的代码和数据、它的栈、通用目的寄存器的内容,程序计数器、环境变量,以及打开文件描述的集合。
进程的作用:提供给应用程序两个关键抽象,
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。
(2)一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Shell是一种命令行解释器,它为应用程序的执行提供一个界面,shell读取用户输入的字符解释并执行。
处理流程:
(1)用户从终端输入命令。
(2)将输入字符串依次分为数个参数。
(3)判断是否是内置命令,是则立即执行,否则调用相应的程序为其分配子进程并运行。
(4)Shell接受键盘输入信号并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在终端输入./hello的输入命令后,shell进行命令行解释,由于不是内部指令,通过fork函数创建子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户及虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈,子进程还获得与父进程任何打开文件,描述符相同的副本,这就意味着,当父进程调用fork函数时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
执行exexve时将会加载并运行程序。加载器将删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0), 新程序启动后的栈结构如图6.4.1所示,并将虚拟地址空间中的页映射到可执行文件的页大小的片chunk,新的代码与数据段被初始化为可执行文件的内容,然后跳到_start。执行过程中还会覆盖当前进程的代码、数据、栈,保留有相同的PID,继承已打开的文件描述符和信号上下文。execve函数调用一次一般不返回,除非有错误。
图6.4.1新程序启动后的栈结构
6.5 Hello的进程执行
(1)上下文信息:上下文信息是操作系统内核重新启动一个挂起的进程所需要恢复的原来的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的信息构成。
(2)时间片;一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(3)进程调度:在进程执行过程中,操作系统内核可以决定抢占当前进程,并重新开始一个先前被挂起的进程,这样的一种决策成为进程调度。当抢占进程时,要完成:
①保存之前进程的上下文
②恢复要执行的新进程的上下文
③把控制转让给新恢复的进程完成上下文切换
(4)用户模式和内核模式:两种模式用一个寄存器来提供区分。简单来说两种模式有不同的“权限”,用户模式权限较低,不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;内核模式权限较高,可以执行任何命令,并且可以访问系统中的任何内存位置。
(5)进程执行与用户核心态转换:
当开始运行hello时,内存为hello分配时间片,若同时运行多个进程,则它们轮流使用处理器,物理控制流被划分成多个交错的逻辑控制流,存在并发执行的现象。然后在用户态下执行并保存上下文。如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,把控制权让给其他进程。当hello进程执行到sleep时,hello会进入休眠状态,此时再次进行上下文切换,控制交付给其他进程,一段时间后hello休眠结束,此时再次完成上下文切换,恢复休眠前的上下文信息,控制权送回到hello继续执行。循环结束后,程序调用 getchar() ,hello从用户模式进入内核模式,并再次上下文切换,控制交付给其他进程。最后,内核会从其他进程回到 hello 进程,在执行到return后进程结束。整个过程如图6.5.1所示。
图6.5.1进程上下文切换的流程
6.6 hello的异常与信号处理
6.6.1执行过程中可能出现的异常
(1)异步异常(中断)
在程序执行过程中由处理器外部IO设备引起的异常,如键盘上敲击Ctrl-C。处理过程如图6.6.1所示。
图6.6.1中断的处理过程
(2)同步异常之陷阱
陷阱是有意的异常,是指令的执行结果,如系统调用。处理过程如图6.6.2所示。
图6.6.2陷阱的处理过程
(3)同步异常之故障
故障不是有意的,但可能被修复,如缺页故障是可恢复的,但保护故障是不可恢复的,其处理过程如图6.6.3所示。
图6.6.3故障的处理过程
(4)同步异常之终止
终止是非故意的,不可恢复的致命错误造成的,如非法指令。其处理过程如图6.6.4所示。
图6.6.4终止的处理过程
6.6.2可能出现的信号
hello执行过程中可能出现的信号有:SIGINT、SIGKILL、SIGSEGV、SIALARM、SIGCHLD。
6.6.3程序的运行
(1)正常运行
在终端输入./hello 2021111536 ymd 1,终端每隔1秒打印输出一次Hello 2021111536 ymd,共5次。输出后不会立刻结束程序,由于有一个getchar()函数的存在,键入一个回车后停止程序执行。如图6.6.5所示。
图6.6.5正常运行
(2)不停乱按
执行过程中不断乱按,会把按的内容显示在屏幕上,最后依然要由getchar()函数接受到键盘输入的一个回车后结束执行。
图6.6.6不断乱按
(3)输入回车
在程序执行过程中按入三个回车,它们被保存在缓冲区中,最后程序结束时,最后一个回车被getchar()读走,结束程序,还有两个回车会输出出来,如图6.6.7所示,程序结束后,会有两个换行输出。
图6.6.7键入三个回车
(4)按下Ctrl-C
在执行过程中如果按下Ctrl-C会立即停止程序的执行。因为这个键盘输入会导致发送SIGINT信号给hello,该信号要求hello立刻终止该进程,如图6.6.8所示。
图6.6.8按下Ctrl-C结束进程
(5)按下Ctrl-Z及相关操作
程序在运行过程中按下Ctrl-Z会产生中断异常,发送SIGSTP信号,暂时挂起hello进程并打印相关信息。如图6.6.9所示。
图6.6.9暂时挂起进程
且挂起后还可以执行若干命令,如输入ps,将打印各进程的PID,如图6.6.10所示;
图6.6.10打印各进程PID
输入jobs将打印出被挂起的hello的相关信息,如图6.6.11所示;
图6.6.11打印hello的相关信息
输入pstree指令将打印进程树,如图6.6.12所示;
图6.6.12打印进程树
输入fg会让其继续执行,如图6.6.13所示;
图6.6.13继续执行进程
输入kill将发送SIGINT信号,杀死进程,如图6.6.14所示。
图6.6.14杀死进程
6.7本章小结
(1)介绍了进程的概念和作用,Shell-bash作用与处理流程
(2)给出了hello的fork()创建进程以及execve加载执行进程的过程.
(3)介绍了hello异常与信号处理,分析并测试了在hello执行过程中外部键盘输入时的处理结果。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1、逻辑地址:程序经过编译后出现在汇编代码中的地址。
2、线性地址:逻辑地址向物理地址转换时的中间环节,hello中的偏移地址加上相应段的基地址就是线性地址。
3、虚拟地址:就是线性地址
4、物理地址:实际出现在CPU外部地址总线上的地址信号,表示实的物理内存所对应的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
和一个段有关的信息需要8个字节来描述,这就是段描述符。段描述符的索引就是前面提到的段选择符的前13位的索引号。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里所有的描述符都在一起集中存放,这就构成了一个描述符表,描述符表分为两种,GDT(全局段描述符表)和LDT(局部段描述表)。
当我们得到一个完整的逻辑地址:段标识符+段内偏移量时,把它转换成线性地址的基本环节如下
1.看段选择符的T1=0还是1,区分出是GDT还是LDT中的段。
2.取出段选择符的前13位作为索引进行查找对应的段描述符,确定了基地址。
3.把基地址和偏移量相加,就是线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换由页式管理实现,它通过分页机制对虚拟内存空间进行分页。计算机会利用页表,通过MMU来完成从虚拟地址到物理地址的转换。而页表是一个页表条目(PTE)的数组,存储在内存中,将虚拟页地址映射到物理页地址。
如图7.3.1所示,一个虚拟地址包含两个部分:一个虚拟页面偏移(VPO),其中VPO和PPO是相同的。以及一个虚拟页号(VPN),MMU利用VPN选择适当的PTE,如果命中,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。如果不命中,会触发缺页故障,调用缺页处理子程序进行处理。
图7.3.1基于页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(Translation Lookaside Buffer),俗称快表,是MMU中的一个小的具有高相联度的集合,实现虚拟页码向物理页码的映射。
如图7.4.1所示,CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN的高位作为TLBT和TLBI,去快表TLB中寻找匹配。如果命中,则可以直接得到物理地址PA。反之则去查询页表,从CR3确定第一级页表的起始地址,而一到三级页表中存放的数据是指向下一级页表的首地址,这样可以逐步访问到第四级页表,而第四级页表中装的才是物理页号,从四级页表读出的物理页号再与VPO组合即可得到物理地址PA。
图7.4.1TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
当我们获得物理地址之后,先取出组索引对应位,在一级cache中寻找对应组。如果存在,则比较tag位,相等后检查valid是否为1。如果都满足则cache命中,取出值传给CPU,否则按顺序对二级cache和三级cache、内存进行相同操作,直到出现命中。然后再一级一级写回,如果有空位则直接写回,否则采用替换算法驱逐出一块后再写回。
7.6 hello进程fork时的内存映射
fork函数创建新进程时,操作系统内核通过下面的步骤为其分配虚拟内存:
(1)创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.
(2)两个进程中的每个页面都标记为只读
(3)两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。
在新进程中返回时,新进程拥有与fork进程相同的虚拟内存空间,随后的写操作通过写时复制机制创建新页面,因此也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
如图7.7.1所示,execve函数在加载并运行新程序时,包括如下几个步骤:
(1)删除已存在的用户区域
(2)创建新的区域结构:所有这些新区域都是私有的、写时复制的,代码和初始化数据映射到hello的.text和.data区,.bss和栈堆映射到匿名文件,栈堆的初始长度0。
(3)共享对象由动态链接映射到本进程共享区域。
(4)设置PC,指向代码区域的入口点:Linux根据需要换入代码和数据页面。
图7.7.1execve函数的内存映射
7.8 缺页故障与缺页中断处理
缺页故障是指CPU想要读取虚拟内存中的某个数据,但是该数据所在的页还没有存放在主存中,此时就发生缺页中断,系统会对其进行处理。
当发生缺页中断时,系统的处理过程如图7.8.1所示:
①处理器将虚拟地址发送给 MMU 。
②③ MMU 使用内存中的页表生成PTE地址。
④有效位为零, 因此 MMU 触发缺页异常。
⑤缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)。
⑥缺页处理程序调入新的页面,并更新内存中的PTE。
⑦缺页处理程序返回到原来进程,再次执行缺页的指令。
图7.8.1缺页异常时的处理过程
7.9动态存储分配管理
动态内存分配是指在程序运行时程序员使用动态内存分配器(如malloc)获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆被分配器视为一组不同大小的块的集合,每个块仅会是以下两种状态之一;“已分配”和“空闲”。已分配的块显式保留为应用程序使用;空闲块保持空闲只到被分配使用。
分配器有两种方式:显式分配器和隐式分配器。显式分配器要求应用显式地释放任何已分配的快,如C语言中的malloc和free的组合对内存进行分配与释放;隐式分配器要求分配器自动完成上述任务。
常用的分配器有显式链表和隐式链表。前者的特点是堆中的空闲块通过头部中的大小字段隐含地连接;后者则是在在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
7.10本章小结
本章主要介绍了存储管理的相关内容。首先介绍各种不同地址的概念,再到通过段式管理和页式管理完成不同形式地址之间的转化以及物理内存的访问。接下来介绍了hello进程的fork和execve的存储映射以及缺页故障和缺页中断处理方式。最后简要介绍了动态内存存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化::所有IO设备都被模型化为文件,所有的输入输出均可作为文件的读写来执行。
设备管理:Linux内核有一个简单、低级的接口,称为Unix I/O,使得所有的输入输出都能以统一方式来执行。
8.2 简述Unix IO接口及其函数
1、Unixix IO接口:Unix IO接口以完成若干功能。首先是打开文件,操作系统内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。还可以改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。接口还能进行读写文件,完成信息传输。所有操作完成后关闭文件,当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。
2、Unixix IO函数
(1)open()函数:打开文件
(2)lseek()函数:将文件指针定位到相应位置
(3)read()函数:读文件
(4)write()函数:写文件
(5)close()函数:关闭文件
8.3 printf的实现分析
我们调用vsprintf函数后,vsprintf函数将我们需要输出的字符串格式化并把内容存放在缓冲区中。并返回要输出的字符个数i。然后调用系统函数write来在屏幕上打印缓冲区中的前i个字符,也就是我们要输出的字符串。调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall等,通过字符驱动子程序打印我们的字符串。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
最后程序返回实际输出的字符数量i。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生中断请求,调用键盘中断处理子程序,程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串。
8.5本章小结
本章阐述了系统级IO,介绍了IO设备的管理方法,UnixIO接口及其函数,最后简要分析了printf函数和getchar函数的实现过程。
结论
一、hello的传奇一生:
1、编写高级语言程序:编写源程序hello.c,开启hello的传奇一生
2、预处理:预处理器cpp对源程序hello.c进行处理,生成hello.i文件。
3、编译:编译器cc1把hello.i文件翻译为汇编文件hello.s。
4、汇编:汇编器as把hello.s汇编生成可重定位目标文件hello.o。
5、链接:链接器ld把可重定位目标文件hello.o和其他目标文件链接生成可执行文件hello。
6、在shell输入./hello 2021111536 ymd 1调用命令行解释功能。
7、创建进程:用fork函数为hello创建子进程。
8、运行进程:调用execve函数运行hello。
9、执行指令:CPU为hello分配时间片,执行控制逻辑流。
10、访问内存:通过MMU将需要访问的虚拟地址转化为物理地址并访问。
11、动态申请内存:hello运行过程中printf中调用的malloc动态地申请内堆中的内存空间。
12、信号与异常:hello运行过程中可能会产生各种异常与号,系统会做出处理。
13、父进程回收子进程,操作系统内核删除为这个进程创建的所有数据结构,结束hello的传奇一生。
二、收获与感悟
本次大作业全面串联起了课堂上所讲授的知识,把每章零散的知识点串成脉络,揭示计算机系统底层的工作原理。它使我对于程序有了更深刻的认识,以前对于程序的理解是粗浅的,只知道“点击运行”,经过学习这门课程和完成大作业,我才真正理解了hello的“一生”,也真正做到了“漫游”计算机系统并深入理解计算机系统,这对以后编写高质量程序有莫大帮助。
附件
hello.c:C语言源程序文件
hello.i:预处理后生成的文件
hello.s:编译产生的汇编程序文件
hello.o:汇编产生的可重定位目标文件
hello:链接产生的可执行文件
elf.txt:hello.o的elf格式文件
elf1.txt:hello的elf格式文件
asm.txt:hello.o反汇编的结果文件
asm1.txt:hello反汇编的结果文件
参考文献
[1] 《深入理解计算机系统》原书第三版
[2] https://blog.csdn.net/shengin/article/details/21530337
[3] https://blog.csdn.net/rabbit_in_android/article/details/49976101
[4] https://www.cnblogs.com/pianist/p/3315801.html
[5] https://zhuanlan.zhihu.com/p/419683114