程序人生—Hello‘s P2P

 

计算机系统

 

大作业

 

 

  程序人生-Hello’s P2P 

 

 

 

 

 

 

 

计算机科学与技术学院

2021年5月

摘  要

本文对hello.c直到hello可执行程序进行逐步分析,在通过hello程序的分析,更加深入地理解计算机系统各个部分地运作方式和作用,把这个学期csapp中各章节知识进行融合,能够对计算机系统产生一个较为整体的认知,同时对计算机系统中不同部分的任务和实现方式获得一定程度了解。

关键词:编译;汇编;链接;进程;虚拟内存;系统级I/O;计算机系统    

目录

第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的预处理结果解析... - 6 -

2.4 本章小结... - 6 -

第3章 编译... - 7 -

3.1 编译的概念与作用... - 7 -

3.2 在Ubuntu下编译的命令... - 7 -

3.3 Hello的编译结果解析... - 7 -

3.4 本章小结... - 11 -

第4章 汇编... - 13 -

4.1 汇编的概念与作用... - 13 -

4.2 在Ubuntu下汇编的命令... - 13 -

4.3 可重定位目标elf格式... - 13 -

4.4 Hello.o的结果解析... - 15 -

4.5 本章小结... - 18 -

第5章 链接... - 19 -

5.1 链接的概念与作用... - 19 -

5.2 在Ubuntu下链接的命令... - 19 -

5.3 可执行目标文件hello的格式... - 19 -

5.4 hello的虚拟地址空间... - 21 -

5.5 链接的重定位过程分析... - 22 -

5.6 hello的执行流程... - 24 -

5.7 Hello的动态链接分析... - 25 -

5.8 本章小结... - 25 -

第6章 hello进程管理... - 26 -

6.1 进程的概念与作用... - 26 -

6.2 简述壳Shell-bash的作用与处理流程... - 26 -

6.3 Hello的fork进程创建过程... - 26 -

6.4 Hello的execve过程... - 27 -

6.5 Hello的进程执行... - 27 -

6.6 hello的异常与信号处理... - 29 -

6.7本章小结... - 31 -

第7章 hello的存储管理... - 33 -

7.1 hello的存储器地址空间... - 33 -

7.2 Intel逻辑地址到线性地址的变换-段式管理... - 33 -

7.3 Hello的线性地址到物理地址的变换-页式管理... - 33 -

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

7.5 三级Cache支持下的物理内存访问... - 34 -

7.6 hello进程fork时的内存映射... - 34 -

7.7 hello进程execve时的内存映射... - 35 -

7.8 缺页故障与缺页中断处理... - 35 -

7.9动态存储分配管理... - 35 -

7.10本章小结... - 37 -

第8章 hello的IO管理... - 38 -

8.1 Linux的IO设备管理方法... - 38 -

8.2 简述Unix IO接口及其函数... - 38 -

8.3 printf的实现分析... - 39 -

8.4 getchar的实现分析... - 39 -

8.5本章小结... - 39 -

结论... - 40 -

附件... - 41 -

参考文献... - 42 -

 

第1章 概述

1.1 Hello简介

hello.c是由高级语言编写的c语言格式的.c文件,分别经过预处理器cpp的预处理生成hello.i文件、编译器ccl的编译生成hello.s文件,汇编器as的汇编生成hello.o文件,最后使用链接器ld进行链接最终成为可执行目标程序hello.

1.2 环境与工具

硬件:X64 i7 CPU;16GRAM;512GDisk

软件:Windows10;VirtualBox;Ubuntu 20.04

开发调试工具:gcc;objdump;readelf;edb

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c:源代码

hello.i:hello.c经预处理生成的文本文件。

hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序。

hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件。

hello-elf.txt:hello.o的ELF格式文件的文本。

hello-odis.txt:hello.o反汇编生成的代码文本。

hello:经过hello.o链接生成的可执行目标文件。

1.4 本章小结

本章主要简要介绍了hello的程序生命周期,包括其P2P、O2O的过程,并详细介绍了本文撰写时的软件环境、硬件环境以及所使用工具、中间结果等。

 

 

第2章 预处理

2.1 预处理的概念与作用

概念:在编译之前进行的处理。C语言的预处理主要有以下三个方面的内容:宏定义、文件包含、条件编译,预处理命令以符号#开头,如#if, #endif等。

作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。得到另一个C程序,通常以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

 

图表 1 预处理生成.i文件

2.3 Hello的预处理结果解析

预处理前的文件仅30行,预处理后的文件达到了三千多行。原因是#include的文件stdio.h等内容直接插入到程序文本中。

2.4 本章小结

本章介绍了预处理的概念和作用,同时介绍了linux下预处理的命令,并对预处理生成的hello.i文件进行了简单分析。

第3章 编译

3.1 编译的概念与作用

概念:利用编译程序从预处理文件(.i)产生汇编程序(.s)的过程。

作用:将输入的高级程序设计语言源程序翻译成以汇编语言或机器语言表示的目标程序。

3.2 在Ubuntu下编译的命令

 

图表 2 编译生成.i文件

3.3 Hello的编译结果解析

.file:源文件名

.text:代码段

.global:全局变量

.data:数据段

.align:对齐方式

.type:指定类型是对象类型或者函数类型

.size:大小

.long:长整型

.section .rodata:rodata节(只读数据节)

.string:字符串

3.3.1 数据

(1)局部变量:整型i

本次实验中存在的局部变量为int i,主要作为计数器使用。

该局部变量存储在运行时栈中,地址为%rbp-4,如下图所示:

 

图表 3 局部变量i地址

(2)字符串常量:两个printf中的格式串

两个printf分别是要打印:

“用法:Hello 学号 姓名 秒数!\n”

“Hello %s %s\n”

字符串常量以uft-8格式编码并存储在.rodata段,如下图所示:

 

图表 4 字符串常量以uft-8格式编码

由于该文件还未进行汇编,这里使用符号.LC0和.LC1代表两个字符串的首地址。由于字符串存储在只读数据区(.rodata节),因此对字符串的访问只读不写。在访问该字符串常量时,使用PC相对寻址的方式对其进行访问,汇编代码见本节讨论函数调用处。

(3)main函数参数:整型argc、字符指针argv

main函数参数主要有int argc和字符型指针数组首地址argv

对于整型变量argc,如下汇编代码于C代码相对应,分析C代码可知,此处汇编代码的行为是将main函数参数argc与4比较,故argc应存储于内存中%rbp-20地址处。

 

图表 5 argc地址

字符型指针数组首地址argv,如下汇编代码与C代码相对应,根据分析可知,argv存储于%rbp-32地址处。

 

图表 6 argv地址

(4)整型常量:代码中的0、1、2、4、8等整型常量

代码段中的整型常量被改写为汇编语言中的立即数,作为代码段的一部分存储,上文中的0、1、2、4、8等均在此列。

3.3.2赋值操作

 

图表 7 对循环变量i赋值0

对循环变量i赋初值0

3.3.3操作算符

       (1)addl指令

 

图表 8 循环变量进行加1

对循环变量进行加1

(2)leaq指令

 

图表 9.LC0和.LC1的段地址并与(%rip)相加之后传递给%rdi

分别计算.LC0和.LC1的段地址并与(%rip)相加之后传递给%rdi

3.3.4关系操作

(1)argc!=4

 

 

图表 10 argc!=4时的操作

(2)执行循环时的判断部分

 

 

图表 11 执行循环时的判断部分

3.3.5控制转移

(1)红线标出的为执行的if语句,如果arc==3则执行L2中语句即2号黑线标出区域的代码,反之则执行1号黑线标出区域的代码。

 

图表 12 arc==3 与arc!=3情况

(2)此处为for循环语句,L2黑线标出区域的代码语句为循环变量i赋初值并跳到L3部分,L3黑线标出区域的代码部分为循环的判断部分,如果判断条件成立则进入循环体L4执行,每次循环完给循环变量+1并再次进入L3黑线标出区域的代码部分判断。知道判断条件不成立跳出循环。

 

图表 13 for循环语句

3.3.6函数

(1)main函数

参数传递: argv和argc

函数调用:系统调用

函数返回:返回0(将%eax赋值为0并返回)

(2)printf函数

参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传 入了 argv[1]和argc[2]的地址

函数调用:前者是if语句条件满足时执行,后者是在每次循环时被调用

(3)exit函数

参数传递:传入参数1,执行退出命令

函数调用:满足if语句时执行

(4)sleep函数:

参数传递:传入参数sleepsecs,

传递控制:call sleep

函数调用:for循环下被调用

(5)getchar函数

传递控制:call getchar

函数调用:在main中被调用

3.4 本章小结

在本章编译操作后,经过预处理操作的hello.i文件由C代码文件变为汇编代码文件hello.s,主要讲述了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以将汇编语言翻译成c语言。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将汇编程序翻译成机器语言指令。

作用:把这些指令打包成可重定位目标程序的格式,并将结果保存在(.o) 目标文件中,(.o) 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

 

图表 14 汇编生成.o文件

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

 

图表 15 hello.o的ELF格式

4.3.1 ELF头

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

该文件为可重定位目标文件;数据为小端法存储;共14个节。

 

图表 16 ELF头的序列Magic

4.3.2节头部表

节头部表(section header table)包含目标文件各节的语义,包括节的名称、大小、类型、地址、偏移量、是否链接、读写权限等信息。第53-57行是对节头部表中出现的各个符号的含义。以.text节为例分析节头部表条目的含义。.text节存储着已编译程序的机器代码。其大小为0x92字节,即十进制下146字节;虚拟内存地址为0x0000000000000000,这是由于hello.o是可重定位目标文件,所以每个节都从0开始,用于重定位;读写权限为AX,即可执行;相对于文件头的偏移量为0x40字节(十进制下64字节)。

 

图表 17 节头部表

4.3.3符号表

 

图表 18 符号表

符号表是由汇编器构造的,使用编译器输出到汇编语言(.s)文件中的符号。.symtab节中包含一个ELF符号表,这张符号表包含一个条目的数组。符号表每个条目都对应一个符号的语义,具体包括:符号名称name、符号位置value、符号类型type、符号范围bind、分配目标section。

有三个特殊的伪节:

(1)ABS代表不该被重定位的符号;

(2)UNDEF代表未定义的符号,即本模块中引用的外部符号;

(3)COMMON表示还未被分配位置的未初始化的数据目标。

4.4 Hello.o的结果解析

将对比hello.s以及hello.o的反汇编文件,比较汇编前后代码发生的变化,并简单介绍机器码与汇编代码的映射关系。

在终端中输入objdump -d -r hello.o > hello-odis.txt,得到hello.o的反汇编代码,hello.s汇编代码与hello-odis.txt内容如下图所示

 

 

图表 19 反汇编文件hello-odis.txt

 

将该反汇编结果与第3章的hello.s的主函数部分进行对照,可以发现其主要流程没有不同,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出以下不同的地方。

(1)分支转移:反汇编的跳转指令用的不是段名称比如.L3,二是用的确定的地址,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后不存在,而是确定的地址。

(2)函数调用:在(.s)文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章完成了对hello.s的汇编工作。使用Ubuntu下的汇编指令可以将其转换为.o可重定位目标文件。此外,本章通过将.o文件反汇编结果与.s汇编程序代码进行比较,了解了二者之间的差别。目前该文件仍不能被加载到内存中执行,还需要对其进行链接操作。

第5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存并执行。

作用:链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

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

 

图表 20 链接生成可执行目标文件hello

5.3 可执行目标文件hello的格式

5.3.1 ELF头

 

图表 21 ELF头的内容

 hello的ELF头与hello.o的ELF头差异项如图中红框所示。

由图可知hello是一个可执行目标文件,有25个节。

5.3.2 节头

 

 

图表 22 节头的内容

5.3.3程序头

 

图表 23 程序头的内容

5.4 hello的虚拟地址空间

使用edb调试hello,可以看到,hello的虚拟内存地址空间是从0x401000开始,到0x401ff0结束,如下图所示,这段地址空间内存储着hello的全部信息。

 

 

图表 24 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.5.1重定位

链接器在完成符号解析以后,把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。重定位步骤,将合并输入模块,并为每个符号分配运行时的地址。

重定位节和符号定义:链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。此时,程序中的每条指令和全局变量都有唯一的运行时内存地址。

重定位节中的符号引用:链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

 

 

 

图表 25 重定位

5.5.2将以hello程序的部分代码为例,介绍重定位的具体步骤,hello案例的源文件部分C代码、可重定位目标文件部分汇编代码、部分重定位条目、可执行文件部分汇编代码如下所示:

 

图表 26  源文件部分C代码

 

图表 27 可重定位目标文件部分汇编代码

 

图表 28 部分重定位条目

 

图表 29 可执行文件部分汇编代码

从C代码中我们可以看到,案例程序需要重定位的符号有两处:printf函数与其格式串参数。

在可重定位目标文件中,leaq指令的代码部分和功能部分已被编码为机器码,但数据部分仍为0x00000000,在其后跟随着该地址的重定位信息;callq指令也是同样的模式。 在可执行目标文件中,这些指令的数据部分已经被编码为机器码。

根据算法的伪代码,我们首先观察hello.o重定位表中的第一项,目标符号引用出现在偏移0x1c处,其运行时地址为0x401141,即leaq的数据部分;目标符号定义在.rodata节中,其运行时地址为0x402008。由于使用PC相对寻址,我们记录下一条指令的运行时地址0x401145,将其与目标符合定义处的运行时地址做差得0x0ec3,将其转化为小端法表示则为c3 0e 00 00,与结果相符。

5.6 hello的执行流程

ld-2.27.so!_dl_start                 --

ld-2.27.so!_dl_init                  --

hello!_start                       0x400550

hello!init                          0x4004c0

hello!main                       0x400582

hello!puts@plt                 0x4004f0

hello!exit@plt                 0x400530

hello!printf@plt               0x400500

hello!sleep@plt               0x400540

hello!getchar@plt             0x400510

sleep@plt                          0x400540

5.7 Hello的动态链接分析

   动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而非像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

5.8 本章小结

在本章中主要介绍了链接的概念与作用,阐述了hello.o是怎么链接成为一个可执行目标文件的过程,介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

第6章 hello进程管理

6.1 进程的概念与作用

概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

6.2 简述壳Shell-bash的作用与处理流程

Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。

其基本功能是解释并运行用户的指令,重复如下处理过程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令

(4)如果不是内部命令,调用fork( )创建新进程/子进程

(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回。

(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下四个步骤:

(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中已存在的区域结构

(2)映射私有区域,为新程序的代码、程序、bss和栈区域创建新的区域结构。代码和数据区域被映射为hello文件的.text和.data区。bss区域是请求二进制零的,映射到匿名文件其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

(3)映射共享区域。

(4)设置程序计数器使之指向代码区域的入口点。

6.5 Hello的进程执行

进程提供给应用程序的抽象:

(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。

(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。

如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,每个进程1个。这些逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。这样的机制使进程在执行时仿佛独占了处理器。

hello进程的执行依赖于操作系统所提供的抽象,下面阐述操作系统所提供的进程抽象:

①逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。

②并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。

③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。

⑤用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

⑥上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

⑦上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。

hello进程执行:

进程调用execve函数之后,由上面分析可知,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.txt和.data节分配虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出hello 7203610414 刘子彤,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

Hello执行截图如下:

 

图表 30 Hello执行情况

6.6 hello的异常与信号处理

可能会出现四类异常:中断、陷阱、故障、终止。

(1)中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码。

(2)陷阱是有意的异常,是执行一条指令的结果,调用后返回到下一条指令。用来调用适当的内核程序,使程序从用户模式切换到内核模式。

(3)故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。如果无法修正,则终止程序。

(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序。处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。

6.6.1正常

 

图表 31 正常情况

6.6.2随便乱按

 

图表 32 随便乱按情况

无意义输入均被缓存到 stdin,当调用getchar时读出一个‘\n’结尾的字串(作为一次输入),其他字符会当做shell命令输入,无意义输入并不会影响到hello进程的运行。

6.6.3 Ctrl+z

按下Ctrl-Z后,hello进程运行暂停。组合键Ctrl-Z会导致内核发送一个SIGSTP信号到前台进程组的每个进程,默认情况下,结果是挂起前台作业。

 

图表 33 Ctrl+z情况

使用ps指令可以看到,hello进程并没有被回收,此时其后台作业号为1。然后使用命令fg 1将其变为前台进程,此时shell继续执行hello进程。最后使用ps指令hello已不在进程表中, hello进程执行完毕,其被父进程shell回收。

 

图表 34 fg 1将其变为前台进程

6.6.4 Ctrl+c

按下Ctrl-C后,hello进程运行终止。组合键Ctrl-C会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况下,结果是终止前台作业。使用ps指令可以看到,hello进程已经父进程回收,进程表中无hello进程。

 

图表 35 Ctrl+c情况

6.7本章小结

本章分析了shell的作用、处理流程和fork、execve过程以及各种异常。最后分析了hello执行过程中的异常和异常处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

(2)线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。

(3)虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB索引)、TLBT(TLB标记)。

(4)物理地址: 用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。

其中TI指示段描述符是在GDT还是LDT中,而索引指示段描述符在段描述符表中的位置。由此,便可以通过段选择符的指示在段描述符表中找到对应的段描述符,然后便可从段描述符中获得段首地址,将其与逻辑地址中的偏移量相加,就得到了线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。

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

TLB与四级页表支持下的VA到PA的变换在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下:虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小 4KB,一个PTE 条目8B,共 512 个条目,使用9位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位,因为VA 48位,所以VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需4位,因为 VPN 36 位,所以 TLBT 32 位。

CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

7.5 三级Cache支持下的物理内存访问

已知Core i7的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。Corei7实现支持48位虚拟地址空间和52位物理地址空间。因为L1块大小为64字节,所以B=64,b=6.因为L1是8路组相联所以S=8,s=3.所以标记位为43位。根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为1,若有,则说明命中,从这一行对应物理地址b位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级cache,访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。

7.6 hello进程fork时的内存映射

当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

(2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。

(3)映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,即可正常翻译VA。

7.9动态存储分配管理

动态内存分配器维护者一个进程的虚拟内存区域,成为堆。(如图7.9.1所示),分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。

(1)显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。

(2)隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

7.9.1显示分配器的约束条件

(1)处理任意的请求序列

(2)立即相应请求

(3)只使用堆

(4)对其块(对齐要求)

(5)不修改已分配的块

7.9.2隐式空闲链表

这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。

头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。

(1)放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。

(2)分割空闲块一旦分配器找到一个匹配的空闲块,就必须做一个决定,那就是给这个块分配多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。

(3)获取额外堆内存如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。

(4)合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,只需要通过改变头部的信息就能完成合并空闲块。

7.9.3显示空闲链表

显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。

按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章对hello的存储空间进行了简要概况,介绍了逻辑地址、虚拟地址、物理地址与线性地址的概念与转换方法,回顾了fork和execve函数,介绍了动态内存分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:在设备模型中,所有的设备都通过总线相连。每一个设备都是一个文件。设备模型展示了总线和它们所控制的设备之间的实际连接。

设备管理:在最底层,Linux 系统中的每个设备由一个 struct device 代表,而Linux统一设备模型就是在kobject kset ktype的基础之上逐层封装起来的。设备管理则是通过unix io接口实现的。

8.2 简述Unix IO接口及其函数

(1)打开文件

程序要求内核打开文件,表示它想访问一个I/O设备。内核返回一个小的非负整数称为描述符。内核记录有关这个打开文件的信息。程序只要记录这个描述符就能记录打开文件所有信息。

int open(char *filename, int flags, mode_t mode);

pen函数将filename转换为一个文件描述符,并返回描述符数字。

flags参数指明进程准备如何访问这个文件,mode参数制定了新文件的访问权限位。

(2)关闭文件

内核释放打开文件时创建的数据结构以及占用的内存资源并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix IO接口的函数

int close(int fd);

关闭一个文件返回操作结果。

(3)读文件

一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k>=m 时执行读操作会触发一个称为EOF 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的EOF符号 。

ssize_t read(int fd, void *buf, size_t n);

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则返回值表示的是实际传送的字节数量。

(4)写文件

ssize_t write(int fd, const void *buf, size_t n);

write函数从内存位置buf复制只多n个字节到描述符fd的当前文件位置。

8.3 printf的实现分析

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux中I/O设备的管理方法,Unix IO函数分析了printf和getchar函数的实现方法。

结论

Hello的一生历程

hello.c:编写c程序,hello.c诞生,它是一个二进制文本文件,hello.c中的每个字符都是用ascall编码表示。

hello.i:hello.c经过预处理阶段变为hello.i。

hello.s:hello.i经过编译阶段变为hello.s。

hello.o:hello.s经过汇编阶段变为hello.o。

hello:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。至此可执行hello程序正式诞生。

编译结束后,我们在终端中运行hello程序,shell首先使用fork和execve函数加载映射虚拟内存,为hello创建新的代码数据堆栈段,CPU为hello分配一个时间片,将程序计数器指向hello代码,开始执行hello程序。在此过程中,还有动态链接的延迟绑定和系统级I/O的问题需要解决。最后,hello运行完毕,shell回收子进程,内核会删除这个进程使用所需要创建的一系列数据结构。至此,hello程序运行结束。

通过对hello一生的分析,我体会到计算机系统的本质概念,学会了如何提高程序的性能、避免易犯的错误、理解控制系统在计算机中的控制方法、理解不同部件发挥的不同作用等。

附件

hello.c:源代码

hello.i:hello.c预处理生成的文本文件。

hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序。

hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件

hello-elf.txt:hello.o的ELF格式文件的文本

hello-odis.txt:hello.o反汇编生成的代码文本

hello:经过hello.o链接生成的可执行目标文件

参考文献

[1]  [美]兰德尔E.布莱恩特. 深入理解计算机系统.3版 [M] 龚奕利,贺莲译 北京:机械工业出版社,2016.7

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值