【无标题】

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 未来技术
学   号 2021112848
班   级 21WL021
学 生 王靖森    
指 导 教 师 史先俊

计算机科学与技术学院
2023年4月
摘 要
本文以hello的自白为出发点,依次介绍而分析了hello,c文件预处理、编译、汇编和链接生成可执行文件的全过程。并在生成可执行文件后进一步分析hello进程的管理、储存管理以及IO管理。通过对hello程序一生的追踪,对计算机系统的知识有更加深入的理解。

关键词: hello;预处理;编译;汇编;链接;进程管理;储存管理;IO管理;

目 录

第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简介
P2P:From Program to Process,即从程序到进程。hello.c文件经过预处理、编译、汇编、链接四个过程,生成可执行文件hello,并在命令行执行后,通过调用fork函数创建子进程执行hello,使得其变成了一个进程。
020:From Zero-0 to Zero-0,即从无到有,再从有到无。产生hello的子进程后,通过调用execve加载hello,映射虚拟内存,并将其载入物理内存。之后开始执行hello程序的相关代码。当进程执行结束后,父进程将回收该进程,并且内核删除其相关的数据结构。hello便结束了其一生。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows11 64位;VirtualBox/Vmware 16;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上
开发工具:Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c——源文件
hello.i——hello.c经过预处理生成的文件
hello.s——hello.i经过编译生成的文件
hello.o——hello.s经过汇编生成的文件
hello——hello.o经过汇编生成的可执行文件
hello.o.elf——hello.o的elf文件
hello.o.objdump——hello.o的反汇编文件
hello.elf——hello的elf文件
hello.objdump——hello的反汇编文件
1.4 本章小结
本章中简述了hello的P2P和020的整个过程,将本次实验过程中所处的软硬件环境和开发与调试工具列了出来,同时也将整个实验过程中生成的中间文件名称及作用做了说明,是对本次实验主要内容的一个概括。

第2章 预处理
2.1 预处理的概念与作用
概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(CPP)根据以字符#开头的命令,修改原始的C程序。结果结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:
①文件包含:#include 。将文件内容插入到程序中。如hello.c中的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。
②宏定义:#define 。根据一系列预定义的规则替换一定的文本模式。如#defined N 200,那么程序中就会将所有的N换成200。
③条件编译:#if/#ifdef/#ifndef/#else/#elif/#endif 。通过在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃。
④行控制:#line 。
⑤错误指令:#error 。
⑥和现实相关的杂注:#pragma 。
⑦空指令:# 。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
预处理产生的文件与hello.c文件相比,在程序之前多了许多的内容,这主要是对原文件中三个#include语句进行预处理的结果,将其头文件所包含的文件插入到了程序文本中。

2.4 本章小结
在本章中本章主要对预处理的概念、作用、过程和结果做了详细的介绍。同时,以hello.c转变为hello.i这个预处理过程为例,具体的观察分析了程序进行预处理的过程。总的来说,预处理过程中,对.c文件进行文件包含、宏定义、条件编译等的预处理,并最终生成了.i文件。

第3章 编译
3.1 编译的概念与作用
概念:编译器(cc1)将源程序的每一条语句都编译成汇编语言,并保存成二进制文件的过程。
作用:对程序的编译经过词法分析、语法分析、代码优化等一系列过程最终完成,并生成最终的二进制文件。在这个过程中,编译器可以实现对程序编写的纠错和优化等,并最终将程序代码翻译成计算机可以读懂的汇编语言,使得计算机可以直接执行。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 数据
3.3.1.1 常量
在本程序中,只存在字符串常量,即下图语句:

在hello.s文件中,该字符串常量被转换成编码来进行表示,如下图所示:

3.3.1.2 整型变量
程序中存在两个整型变量,且均为局部变量。一个为函数参数argc,一个为函数内定义的i。
这两个变量均为局部变量,所以储存在栈中,不同的是,由于argc为函数参数,所以刚开始时会放在寄存器中传给函数,之后存放在栈中。
可以看到,argc作为第一个参数通过寄存器edi传递进来,并储存在了栈里。
同样的,i也被存放在栈里,并被赋初值0。

3.3.1.3 字符串数组
该字符字符串数组通过指针的形式作为函数的参数传递进来,那么同样的,它的起始地址也是起初保存在寄存器中,后又被转存到栈中,如下图语句所示:

而在下文对数组内容进行输出时,通过将字符串数组内容保存到寄存器rsi和rdx后,传递给printf函数,实现对内容的输出。

3.3.1.4 类型转换
在原程序中,通过‘atoi(argv[3])’将字符串类型的argv[3]转换成了整型。在编译后的文件中,首先将字符串转存到寄存器rdi中,之后将其作为一个参数,调用atoi实现类型的转换。

3.3.2 赋值
原文件中只涉及对变量i的赋值操作,将其赋值为0。在编译后的文件中,通过movl指令将0赋给i,由于i是整型,四个字节,所以采用l后缀。

3.3.3 算术操作
程序中涉及的算数操作即为每次循环变量i的i++。在编译后的文件中,采用addl指令实现对变量i的++操作。同样的,由于i是整型,所以采用l后缀。

3.3.4 关系操作
3.3.4.1 “!=”
在if语句中,进行了argc!=4的操作,在编译后的文件中是通过cmpl和je指令合作实现的。如下图,首先通过cmpl指令进行比较,在通过je控制跳转来实现argc!=4的判断,je即若相等则跳转到.L2处,这样就实现了两数不等的比较。

3.3.4.2 “<”
在for循环语句中,控制退出的条件是i<8,对于<的实现,编译后的文件是通过cmpl和jle来实现的。如下图,通过cmpl进行两数比较,jle判断是否小于等于,若i小于等于7则跳到.L4,大于7执行另外的语句,这样就完成了i<8的关系操作。

3.3.5 数组/指针/结构操作
程序中存在着一个字符串数组argc[],而该数组实现的过程中也采用了指针的实现形式。通过定义char* argc[]的形式实现二维的字符数组来储存字符串,数组的每一行保存一个字符串。
正如前面分析的那样,该数组首地址被储存在栈中。而对给数组的访问可以通过首地址地址加偏移量的方式实现,如程序中对数组第三行储存的字符串的访问如下,由于地址为8位,所以加上偏移量16后,获得了第三行字符串的首地址:

3.3.6 控制转移
3.3.6.1 “if”
程序中存在一个if语句,通过判断argc是否等于4,实现两个分支,如下图,通过与4比较,若等于4,则跳转到.L2的位置,即if语句后的for循环语句,不符合if判断条件,跳过了if内的语句;而若是不等于4,则不执行跳转,继续执行下面的语句,而这就是if内的语句。

3.3.6.2 “for”
程序中存在一个for循环,控制条件为i<8,其初始化如下,给i赋初值0后跳转,开始执行循环。

每次循环开始前,都会i与7作比较,小于等于7,即满足i<8,则跳转到.L4执行,即for循环内的语句,若i大于7了,则不再进行跳转,正常执行后面的语句,即for循环接下来的程序内容。

3.3.7 函数操作
3.3.7.1 参数传递
函数的参数通过寄存器来传递,根据参数的顺序,依次用寄存器rdi,rsi,rdx,rcx,r8,r9,进行保存。如程序中的argc和argc[]:

传给printf的参数,该三个参数传递的均为地址:

传给atoi的参数,字符串的首地址地址:

传给sleep的参数,值:

3.3.7.2 函数调用
通过call指令和函数名进行调用,参数通过寄存器传递。
3.3.7.3 函数返回
函数的返回值存放在寄存器rax,可以通过该寄存器获取返回值。如下图中,将atoi函数的返回值存放在edi中又作为参数传递给了sleep函数。

3.4 本章小结
编译是将我们所写的代码翻译为机器所能理解的汇编语言的过程。在本章中,介绍了编译的概念与作用。并结合hello.s的实例,详细分析了各种类型的数据、操作、函数等在汇编语言中是如何实现的。

第4章 汇编
4.1 汇编的概念与作用
概念:汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。
作用:汇编器(as)将.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式
4.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可文件重定位)、机器类型(x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。

4.3.2 节头部表
节头部表中包含了文件各节的信息,包含名称、类型、地址、偏移量、大小、链接等等。其中,各节所包含的内容如下:
.text:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局和静态C变量。
.bss:未初始化的全局和静态C变量。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.rel.data:被模块引用或定义的所有全局变量的重定位信息。
.bebug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C原文件。
.line:原始C源程序中的行号和.text节中机器指令之间的映射。
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

4.3.3 重定位节
在重定位时,链接器将所有相同类型的节合并为同一类型的新的聚合节。编译器还修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。 这些重定位的信息都记录在ELF文件表里面,对于每个须要重定位的代码段和数据段,都会有一个相应的重定位表,例如 .rel.text 表对应.text段。也就是说,重定位表记录了需要被重定位的地址都在相应段的哪些地方。
偏移量指重定位入口的偏移,如下图中exit偏移量为2b,则就在.text中偏移2b的位置保存着所需符号。

4.3.4 符号表
.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。
name是字符串表中的字节偏移,指向符号的以NULL结尾的字符串的名字。value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小。tape通常要么是数据要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。bind字段表示符号是本地的还是全局的。

4.4 Hello.o的结果解析
hello.o的反汇编与hello.s基本上是相同的,但也存在这明显的不同之处,在此只分析一下不同的地方。
4.4.1 操作数
如下图中,这是main函数的形参存到栈时的操作语句。左侧图为hello.s,右侧图为hello.o。可以明显的看到,在hello.s中,运用的操作数为十进制的。而在hello.o的反汇编中,运用的操作数为十六进制。

4.4.2 分支转移
以源程序中的if部分作为例子
下图为hello.s中的实现。可以看到,程序根据分支被分为了不同的段,有.L2、.L3等等,需要进行跳转时,就说明要跳转到哪个段即可。

而在hello.o中是不分段的,它的跳转是通过main入口的值加上偏转量实现的。比如下图的if中,跳转到<main+0x2f>,则找到偏移量为2f的地方跳转即可。

4.4.3 函数调用
以程序中对函数atoi的调用为例。
在hello.s中,调用函数的方式为call加函数名。

而在hello.o中,调用函数的方式为call加偏移地址。但此时我们看到的地址为下一条指令的地址。我们可以看到语句后还有这重定位标记。所以程序运行时,需要重定位以确定函数调用的地址。所以对于这些不确定地址的函数调用,将其call指令后的地址定为下一条指令的地址,然后添加重定位条目,等待链接进一步确定。

4.4.4 字符串常量访问
同样的,在对字符串访问时,出现了和上述函数调用相同的情况,将访问的地址定为了0x0,并添加重定位条目。

4.5 本章小结
在本章中,简单介绍了汇编的概念与作用。在这个阶段中,将程序变为了机器语言。并通过以hello.o为实例,分析了ELF格式,依次详细地说明了其中的各节所包含的信息,并重点分析了重定位项目。之后再通过与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 /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

5.3 可执行目标文件hello的格式
如图,其中代码段号为15,数据段号为24,bss段号为25。

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
如下图,程序的虚拟空间开始于0x400000。

对照5.3各段的地址进行查看。代码段在0x4010f0

5.5 链接的重定位过程分析
5.5.1 连接过程

  1. 地址
    在hello.o中,对地址的访问为相对地址,即相对于main函数开头的偏移。而在hello中,对地址的访问为虚拟空间的地址。main函数在0x40011d6的地址处开始。

  2. 函数调用
    在hello中,需要重定位的项目都进行了重定位,对函数的调用变成了虚拟地址,同样的,之前提到的对字符串的访问也是如此。

  3. 新增节
    增加了.init节、.plt节等的内容。

  4. 新增函数
    对于在程序中用到的函数,也链接进了文件中。比如printf、exit等

链接过程中,链接器主要完成两个任务,符号解析和重定位。符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。
5.5.2 重定位
重定位由两步组成:
1.重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
2.重定义节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。比如在hello.o程序中,需要重定位的函数调用地址都为下一条语句地址,其都有着重定位条目。在hello中,对这些需要重定位的项目进行了重定位,使得其调用地址变为了一个具体的虚拟地址。
5.6 hello的执行流程
执行过程如下,其中,通过外层程序调用或跳转内层程序

5.7 Hello的动态链接分析
共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程成为动态链接,是由一个叫做动态链接器的程序来执行的。

在数据段开始的地方有一个全局偏移量表(GOT),在GOT中,每个被这个目标模块引用的全局数据目标都有一个8字节条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的GOT。
根据5.3中的内容找到got表的位置,可以看到在dl_init之前,其内容为0。
而在dl_init之后,内容发生了变化。这两个地址便是GOT[1]和GOT[2]。

和PLT联合使用时,GOT0和GOT1包含动态链接器在解析函数地址时会使用的信息。GOT2是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。例如,GOT [4]和 PLT[2]对应于addvec。初始时,每个GOT条目都指向对应PLT条目的第二条指令。
根据GOT[2]找到ld-linux.so的模块如下:

在某个函数被第一次调用时,延迟解析它的运行时地址:
1.不直接调用该函数,程序调用进入其PLT条目;
2.第一条PLT指令通过GOT条目进行间接跳转,因为每个GOT条目初始时都指向它所对应的PLT条目的第二条指令,这个跳转只是简单地把控制传送回PLT中的下一条指令
3.把该函数的ID压入栈中之后,跳转到PLT[0]
4.PLT[0]通过GOT [1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定该函数的运行时位置,用这个地址重写对应的的GOT条目,再把控制传递给该函数。
后续再次调用该函数时:
1.控制传递到该函数对应的PLT条目;
2.通过其对应的GOT条目的间接跳转,直接控制转移到该函数。
5.8 本章小结
在本章中,介绍了链接的作用和概念。并结合hello可执行文件,分析了可执行目标文件的格式,分析了虚拟空间地址的情况。并对连接的过程和重定位的过程进行了分析。结合hello的实际执行情况说明了hello的执行流程,并在最后对hello的动态链接过程进行了分析。

第6章 hello进程管理
6.1 进程的概念与作用
概念:在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈。通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程提供给应用程序关键抽象:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
2.一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell-bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Shell-bash还能从文件中读取命令,这样的文件称为脚本。它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。
处理流程:
1.读取用户从键盘输入的命令行。
2.分析命令行字符串并获取命令行参数。
3.检查第一个命令行参数是否是一个内置命令。
4.如果不是内置命令,调用fork创建新进程/子进程。
5.在子进程中,调用execve执行指定程序。
6.如果要求后台运行,等待作业终止后返回
7.如果要求后台运行,则返回。
6.3 Hello的fork进程创建过程
当在终端中输入./hello 学号 姓名 秒数 的指令后,终端对输入的命令行进行解析。由于这不是一个内置命令,所以终端会调用fork函数来创建一个子进程来执行命令。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有着不同的PID。
6.4 Hello的execve过程
根据命令行的文件名参数,execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。
6.5 Hello的进程执行
1.上下文信息:内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
2.进程时间片:一个进程执行它的控制流的一部分的每一个时间段。
3.用户态与核心态转换:
内核通过某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式位从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码是=时,处理器就把模式从内核模式改回到用户模式。
4.进程调度过程
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
6.6 hello的异常与信号处理
1.乱按+回车
可以看到,这并不会影响到程序的正常执行。除了第一次回车前的内容被程序内的gerchar()读取,其余乱按输入的内容会在程序执行结束后键入命令行,而回车同样会在原本的语句后面执行。

  1. Ctrl-Z
    在输入Ctrl-Z的指令后,显示程序停止的提示信息后,进程就被停止了。
    在这个过程中,内核发送一个SIGTSTP的信号,使得进程停止,触发中断异常。

此时输入ps查看进程信息,可以看到停止的hello进程。

输入jobs查看当前终端所有的后台进程也可以看到hello进程。

输入pstree查看进程树,找到hello进程。

使用fg指令可以使得后台的hello进程回到前台继续运行。在这个过程中,内核向进程发送SIGCONT信号,使得进程在中断的下一条指令开始执行。

使用kill指令,将hello进程杀死,后台没有了hello进程。-9传递的为SIGKILL信号,使得进程被杀死。

3.Ctrl-C
输入Ctrl-C后,进程被终止,后台也不存在该进程。在这个过程中,内核向进程发送SIGINT信号,触发中断异常,并最终将进程终止。

6.7本章小结
在本章中,介绍了进程的概念与作用,简述了壳Shell-bash的作用与处理流程。说明了hello调用fork进程创建过程和execve过程。并结合了进程上下文信息、进程时间片的概念,阐述了用户态与核心态转换和进程调度的过程。最后,结合hello的实例,说明了hello的异常与信号处理过程。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:用段基址(段地址)和段内偏移量(偏移地址)来表示,段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置,这种地址表示方式称为逻辑地址,通常表示为段地址:偏移地址的形式。例如在hello程序的跳转中,其跳转显示的地址并非真实的地址,而是逻辑地址,是相对于段的偏移地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。即在上述例子中的地址,加上代码段的基地址得到线性地址。
虚拟地址:即线性地址。CPU从一个有个地址的地址空间中生成虚拟地址。显然虚拟地址是连续的,线性的。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。即hello存储在内存中的实际的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,引向“段描述符”,段描述符具体地址描述了一个段。很多个段描述符,组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成。
其中的Base字段描述了一个段的开始位置的线性地址。
一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。由段选择符中的T1字段进行区分,=0,表示用GDT,=1表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射是地址翻译的过程。
CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 TLB加速地址翻译
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。
如图,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有个组,那么TLB索引是由VPN的t个最低位组成的,而TLB标记是由VPN中剩余的位组成的。
当TLB命中时:
①CPU产生一个虚拟地址。
②③MMU从TLB中取出相应的PLE。
④MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
⑤高速缓存/主存将所请求的数据字返回给CPU。
由于所有的地址翻译步骤都在芯片上的MMU中执行的,所以非常快。
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。
7.4.2 四级页表
用来压缩页表的常用方法是使用层级结构的页表。一级页表中的每个PTE负责映射虚拟地址空间中的一个片,每一片都由多个连续的页面组成。如果片i中的每个页面都未被分配,那么一级PTE i就为空。以同样的方法,二级页表映射三级页表,三级页表映射四级页表。
四级页表中的每个PTE负责映射一个虚拟内存页面,就想我们查看只有一级的页表一样。
对于使用四级页表层次结构的地址翻译,虚拟地址被划分成为四个VPN和一个VPO。每个VPN i都是一个到第i级页表的索引,其中1≤i≤4。第j级页表中的每个PTE,1≤j≤3,都指向第j+1级的某个页表的基础。第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。在能够确定PPS之前,MMU必须访问4个PTE。
7.5 三级Cache支持下的物理内存访问
假设每个储存器地址有m位,形成个不同地址。高速缓存被组织成一个有个高速缓存组的数组。每个组包含E个高速缓存行。每个行是由一个字节的数据块组成的,一个有效位指明这个行是否包含有意义的信息,还有t=m-(b+s)个标记位,唯一地标识存储在这个高速缓存行中的块。高速缓存的大小C=S×E×B。
当一条加载指令指示CPU从主存地址A中读一个字时,它将地址A发送到高速缓存。如果高速缓存正保存着地址A处那个字的副本,它就立即将那个字发回给CPU。
如上图所示,参数S和B将m个地址位分为了三个字段。A中s个组索引位是一个到S个组的数组的索引。第一个组是组0,第二个组是组1,以此类推。索引为被解释为一个无符号整数,表明这个字必须存储在哪个组中。A中的t个标记位表明了这个组中的哪一行包含这个字。当且仅当设置了有效位并且该行的标记位与地址A中的标记位相匹配时,组中的这一行才包含这个字。b个块偏移位给出了在B个字节的数据块中的字偏移。
7.6 hello进程fork时的内存映射
当fork函数被Shell进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了Shell进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有的这些新的区域都是私有的、写时复制的。
3.映射共享区域。hello程序与共享对象的链接都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域段的入口点。
下次调度这个进程时,它将从这个入口点开始执行。
7.8 缺页故障与缺页中断处理
假设CPU引用了VP i中的一个字,VP i未被缓存在DRAM中。地址翻译硬件从内存中读取PTE i,从有效位推断出VP i未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。如果该牺牲页已经被修改了,那么内核就会将它复制回磁盘。
接下来,内核从磁盘复制VP i到内存中的PP i,更新PTE i,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP i已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的区域后开始,并向上生长。对于每个进程,内核维护着变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
1.显式分配器,要求应用显式地释放任何已分配的块。
2.隐式分配器,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配块的过程叫做垃圾收集。
7.10本章小结
在本章中结合hello程序的实例,介绍了逻辑地址、线性地址、虚拟地址、物理地址四种地址的基本概念。之后结合hello分析了存储管理中的段式管理和页式管理,并分析了TLB和多级页面在页式管理中是如何发挥作用的。之后也介绍了三级cache是如何进行物理内存的访问的。之后又在内存的层面再次分析了fork和execve函数。分析完各部分的工作原理,又对缺页故障和如何进行缺页中断处理做了详细介绍。最后对动态内存做了简单叙述。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:B0,B1,…,BK,…,Bm-1。
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O接口
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
2.Lnux shell创建的每个进程开始时有三个打开的文件:标准输入、标准输出和标准错误。
3.改变当前文件的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。应用程序能够通过执行seek操作。显式地设置文件的当前位置为k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。
5.关闭文件。当应用程序完成了对文件的访问之后,它就通知内核关闭这个文件。
8.2.2 相关函数
1.int open(char *filename, int flags, mode_t made);
进程通过调用open函数来打开一个已存在的文件或者创建一个新的文件。
2.ssize_t read(int fd, void *buf, size_t n);
应用程序通过调用read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
3.ssize_t write(int fd, const void *buf, size_t n);
应用程序通过调用write函数来执行输出。write函数从内存位置buf复制最多n个字节到描述符fd的当前文件位置。
4.ssize_t rio_readn(int fd, void *userbuf, size_t n);
rio_readn函数在内存和文件之间直接传送数据。rio_readn函数从描述符fd的当前文件位置最多传送n个字节到内存位置usrbuf。
5.ssize_t rio_writen(int fd, void *userbuf, size_t n);
rio_writen函数在内存和文件之间直接传送数据。rio_writen函数从位置usrbuf位置传送n个字节到描述符fd。
6.DIR *opendir(const char *name);
函数opendir以路径名为参数,返回指向目录流的指针。
7.struct dirent *readdir(DIR *dirp);
每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更多目录项则返回NULL。
8.3 printf的实现分析

va_list的定为 typedef char va_list 是一个字符指针。(char)(&fmt) + 4) 表示的是…中的第一个参数。
Vsprintf(bus, fmt, arg)函数内容如下:

vsprintf返回的是要打印出来的字符串的长度。所以vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write为写操作,把buf中的i个元素的值写到终端。
一个call save是为了保存中断前进程的状态。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar()是c语言中的io流的获取一个字符的函数。
getchar源码分析:
1,getchar-》getchar-》_fgetchar
2、在_fgetchar中调用了getc,getc调用了_getc_nolock(stream)。
3、_getc_nolock(stream)是个宏
#define _getc_nolock(_stream) _fgetc_nolock(_stream)
_fgetc_nolock(_stream)也是个宏,
#define _fgetc_nolock(_stream)
(–(_stream)->_cnt >= 0 ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))
由此可以看出,如果stream->_cnt是大于0的话,getchar就会读取上次读到的值,并不需要用户输入。
4、_filbuf掉用_read,_read调用_read_nolock,_read_nolock调用ReadFile进行用户输入。
以上大致是一个getchar的函数流程。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
在本章中,详细分析了计算机中I/O的管理。首先介绍了Linux中I/O的管理方法,并介绍了I/O接口以及相关的函数功能。之后以printf和getchar这两个函数为例对它们的实现进行了分析。
结论
1.hello的一生从编写好的源代码开始。这是一个hello.c文件,其中蕴含着程序员编写的初始代码。
2.hello.c文件经过预处理的过程,转换为hello.i文件,源代码被翻译为了目标代码。
3.hello.i文件经过编译阶段,生成了hello.s文件,其代码又被编译为汇编语言。
4.hello.s文件经过汇编阶段,生成了hello.o文件,从汇编语言变为了机器语言,并把这些指令打包成了可重定位目标程序。
5.hello.o文件经过汇编阶段,生成了hello可执行文件,将各种代码和数据片段收集并组合成为一个单一文件,现在,hello就可以在命令行中运行了。
6.通过输入命令行运行hello文件,shell通过调用fork创建新的进程,之后新的进程调用execve函数加载hello。
7.在hello进程被调度时,CPU 为其分配时间片,在一个时间片中,hello 享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,执行控制逻辑流。
8.MMU借助TLB、多级页表等,将虚拟地址翻译为物理地址,并通过三级cacha进行访存。
9.I/O接口来进行输入和输出,对hello文件进行读和写操作。
10.hello运行结束后,shell进程回收该子进程,内核删除该进程的所有数据结构。至此,hello结束了它的一生。
附件
hello.c——源文件
hello.i——hello.c经过预处理生成的文件
hello.s——hello.i经过编译生成的文件
hello.o——hello.s经过汇编生成的文件
hello——hello.o经过汇编生成的可执行文件

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[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分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值