计算机系统

大作业

题     目 程序人生-Hello’s P2P

专       业 计算学部

学     号 1190200608

班     级 1903004

学       生 琚晓龙    

指 导 教 师 史先俊   

计算机科学与技术学院

2021年5月

摘  要

文章以最简单的hello程序为例,跟踪一个程序从以C语言形式创建,到一步一步生成可执行文件,然后被运行的过程。第一章简单介绍了用到的环境与工具。第二章到第五章详细介绍了hello程序如何从源代码经历预处理、编译、汇编、链接成为一个可执行文件。后几章详细介绍了hello程序是如何加载到内存中并被计算机执行的,包括进程创建、环境创建、内存访问、异常处理、I/O系统交互等。通过hello程序的一生,窥视了计算机系统执行一个程序的基本流程与原理,对我们深入理解计算机系统又很大的帮助。

关键词:预处理;编译;汇编;重定位;链接;进程;存储;数据访问;I/O;虚拟地址;异常与信号。

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.2 环境与工具... - 5 -

1.3 中间结果... - 5 -

1.4 本章小结... - 6 -

第2章 预处理... - 7 -

2.1 预处理的概念与作用... - 7 -

2.2在Ubuntu下预处理的命令... - 7 -

2.3 Hello的预处理结果解析... - 8 -

2.4 本章小结... - 9 -

第3章 编译... - 10 -

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

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

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

3.3.1数据... - 12 -

3.3.2赋值... - 13 -

3.3.3类型转换(隐式或显式) - 14 -

3.3.4Sizeof - 14 -

3.3.5算术操作&逻辑/位操作... - 15 -

3.3.6关系操作&控制转移... - 15 -

3.3.7数组/指针/结构操作... - 17 -

3.3.8函数操作... - 17 -

3.4 本章小结... - 19 -

第4章 汇编... - 20 -

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

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

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

4.3.1ELF头... - 21 -

4.3.2节头部表... - 22 -

4.3.3符号表... - 23 -

4.3.4重定位节... - 24 -

4.3.5其他节... - 25 -

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

4.5 本章小结... - 27 -

第5章 链接... - 28 -

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

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

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

5.3.1ELF头... - 29 -

5.3.2节头部表... - 30 -

5.3.3程序头部表... - 31 -

5.3.4 Section to Segment mapping. - 32 -

5.3.5 Dynamic section. - 32 -

5.3.6重定位节... - 33 -

5.3.7 Symbol table. - 33 -

5.3.8其他节... - 34 -

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

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

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

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

5.8 本章小结... - 39 -

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

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

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

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

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

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

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

6.7本章小结... - 49 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 59 -

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

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

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

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

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

8.5本章小结... - 63 -

结论... - 64 -

附件... - 65 -

参考文献... - 66 -

第1章 概述

1.1 Hello简介

  1. P2P:

Hello程序的生命周期是从一个高级的C语言程序开始的。GCC编译器驱动程序读取源程序文件hello.c(Program),并通过预处理、编译、汇编、链接四个阶段将它翻译成一个可执行目标文件。当在shell中输入这个可执行文件的名字时(由于该名字不是一个内置的shell命令,shell会假设这是一个可执行文件的名字,并将加载与运行这个文件),shell会调用fork函数为hello程序创建一个新进程(Process)。

  1. 020:

Hello程序最初并不存在(0),因为程序员的编写而诞生,生成一个C语言程序,由此开始,经历上边P2P的过程,得到一个执行hello程序的进程。之后hello程序执行完毕后,hello进程会终止,并由shell回收(0)。

1.2 环境与工具

硬件环境:X64 CPU;2.40 GHz;16G RAM;716G

系统环境:Windows10 专业版;最新Vmware; Ubuntu 20.04

开发与调试工具:gcc; objdump; readelf; edb; hexedit; 文本编辑器

1.3 中间结果

中间结果文件名

文件的作用

hello.c

hello C程序文本(源代码)

hello.i

hello.c预处理后生成的文件

hello.s

hello.i编译后生成的文件

hello.o

hello.s汇编后生成的文件

hello_o_readelf.txt

存放使用readelf工具查看hello.o文件的结果的文件

hello_o_objdump.txt

存放使用objdump工具查看hello.o文件的结果的文件

hello(.out)

Hello程序的可执行文件

hello_readelf.txt

存放使用readelf工具查看hello文件的结果的文件

hello_objdump.txt

存放使用objdump工具查看hello文件的结果的文件

1.4 本章小结

本章对hello进行了简介;列出了编写本文所用的硬件软件系统以及开发工具;列出了编写本文是生成的中间结果的文件名以及这些文件各自的作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理工作(如图2-1-1)是在程序被编译前进行的,修改原始的C程序。可能处理的工作有:将其他文件包含到即将被编译的文件中来,定义符号常量(Symbol constant)和宏(Macro),程序代码的条件编译(Conditional compilation)和有条件地执行预处理命令(Conditional execution of preprocessor directive)。所有的预处理命令都是以#开头的。在同一行中,只有空格和注释可以出现在预处理命令之前。

如图2-1-2,hello.c中第6行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以 .i 作为文件拓展名。

图2-1-1

图2-1-2

2.2在Ubuntu下预处理的命令

命令: gcc -E hello.c -o hello.i

如图2-2-1:

图2-2-1

2.3 Hello的预处理结果解析

打开生成的文件可以看到内容明显增多(如图2-3-1所示)。

图2-3-1

(由23行变为3069行)

具体增加的具体内容如下,举例见图2-3-2:

对原文件中的宏进行了展开。

将头文件中的内容添加到进该文件中。例如声明的函数、定义的结构体、定义的变量、定义的宏等内容。

将代码中有#define命令对应的符号进行了替换。

(a)

(b)

(c)

图2-3-2

2.4 本章小结

本章介绍了预处理的概念与作用,例如将头文件中的内容添加到C程序问文件,替换掉宏常量等。对如何使用gcc对c程序文件进行预处理进行了示范。对生成的预处理后的文件进行了简单的解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译器 (ccl) 将文本文件 hello.i 翻译成文本文件hello.s ,它包含一个汇编语言程序。这个过程叫做编译,如图3-1-1所示。

编译程序把源程序(高级语言)翻译成一个包含汇编语言的程序问文件,同时,还可以进行语法检查、优化源程序、分配寄存器的使用、将程序中的文件输出到汇编语言,确保每个模块中每个局部符号只有一个定义、唯一的名字,对无法解析的全局符号生成一个连接器符号表条目等。

图3-1-1

3.2 在Ubuntu下编译的命令

命令: gcc -S hello.i -o hello.s

如图3-2-1:

图3-2-1

3.3 Hello的编译结果解析

打开生成的文件可以看到该文件中都是汇编指令(如图3-3-1)。

图3-3-1

具体解析如下:

指导汇编器和链接器工作的伪指令及作用见图3-2-2和表3-2-1.

图3-3-2

.file

声明源文件

.text

代码段

.section

定义内存段

.rodata

只读数据段

.align

数据或者指令的地址对齐方式

.string

声明一个字符串(.LC0,.LC1)

.global

声明全局变量(main)

.type

声明一个符号是数据类型还是函数类型

表3-2-1

3.3.1数据

对于C语言中的常量,编译器会根据它们对应的类型进行编码:

1)对于有符号整型数据,编译器会将其转化为该数据对应的十六进制补码形式[1];(如图3-3-1-1)

2)对于无符号整数,编译器会将其转化为该数据对应的十六进制的源码形式。[2]

3)对于浮点类型,编译器会根据 IEEE754 标准将其转化为对应的十六进制数;

4)对于字符类型,汇编器会根据字符的ASII编码或是Unicode编码生成十六进制数。[3](如图3-3-1-2)

图3-3-1-1

图3-3-1-2

对于C语言中的全局变量,编译器会将其符号放在伪指令 .global后。(如图3-3-1-3),并将该数据存储到内存中。

对于C语言中的局部变量,编译器有两种处理方式:1)将该变量的值存放在一个寄存器中(如图3-3-1-4)。2)分配新的栈帧,并将局部变量存放在内存(栈)中,下面几种情况必须将变量数据存储在内存中:

  1. 寄存器不够存放所有的本地数据。
  2. 对一个局部变量使用地址运算符’&’,因此必须能够为它产生一个地址。
  3. 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。

对于C语言中的静态变量,编译器将该变量存储在栈中。不会释放直到程序退出或主函数返回零。

图3-3-1-3

图3-3-1-4(此处%edi存放argc)

[1]、[2]对于有些(绝对值)比较小的整数,编译器会直接使用十进制数编码。

[3]对于字符串来说,则是一个编码序列,最后以空字符编码结尾。

对于C语言中的表达式,编译器会通过一系列指令对其进行处理,然后将结果存放到寄存器或是内存中。

对于C语言中宏,由于在预处理阶段就已经处理,所以编译器只是把相对应的数据进行编码即可。

3.3.2赋值

对编译器来说,赋值操做就是将数据从一个位置复制到另一个位置。进行这些操做的指令叫做数据传送指令。逗号操作符会将一行代码分割成几个独立的赋值操做。对于赋初始值的变量编译器会在首次出现这个变量时为其赋相应值。对于没有赋初始值的变量,编译器有两种处理方式:1)相当于赋初始值为0,即在首次出现这个变量时为其赋值0;2)不进行处理,直到为其赋值时再申请内存并赋值。

数据传输指令可分为三种。

最简单形式的数据转送指令——MOV类,如图3-3-2-1.

图3-3-2-1

零扩展数据传送指令,如图3-3-2-2.

图3-3-2-2

符号扩展数据传送指令,如图3-3-2-3.

图3-3-2-3

Hello程序中的赋值操做在hello.s中的指令如图3-3-2-4:

图3-3-2-4(i = 0;语句)

3.3.3类型转换(隐式或显式)

对于整型与整型、整型与字符型之间的类型转换,直接按照位级表示截断(字长大的像字长小的转换,截断高位)或者扩展(字长小的像字长大的转换,分为有符号扩展和无符号扩展)即可。其中截断只需要在使用数据传输指令时将后缀减小,而扩展有具体的扩展指令,见图3-3-2-2和图3-3-2-3.

对于整型与浮点类型之间的转换,由编译器根据其表示的值来进行转换。

类型转换分为显式和隐式转换,其中显式转换需要需要程序员显式地利用 () 在代码中表示出来,而隐式转换由编译器编译时自行完成,不需要程序员的参与。

3.3.4Sizeof

Sizeof的结果由编译器根据源代码中的数据类型得出,作为一个常量使用。如图3-3-4-1,指针类型的大小为8,编译器在编译阶段确认类型后直接将其表示为常数。

图3-3-4-1

3.3.5算术操作&逻辑/位操作

图3-3-5-1列出了一些整数和逻辑操做。大部分操做都被分成了指令类,这些指令类有各种带不同大小的操作数的变种(leaq没有)。

图3-3-5-1(>>A为算数右移)

如图3-3-5-2,addl 指令实现了源代码中的 i++ (i的值保存在%ebp中)

图3-3-5-2

3.3.6关系操作&控制转移

关系操做通过CMP或TEST指令设置条件码,例如CF、ZF、SF、OF。注意,关系操做并不会改变寄存器或内存中的值。这些指令如图3-3-6-1所显示。

图3-3-6-1

图3-3-6-2显示了编译器是怎么实现源代码中的条件判断的:

a.(判断argc!=4)

b.(判断i<8)

图3-3-6-2

条件码不会直接读取,常用的使用方法有三种:1)可以根据条件码的某种组合,将一个字节设置成0或者1,set类指令;2)可以条件跳转到程序的某个其他的部分,jmp类指令;3)可以有条件的传送数据,条件传送指令。

后两种使用方式可以实现控制转移,jmp指令如图3-3-6-3.

图3-3-6-3

如图3-3-6-4,编译器通过jmp指令实现了控制转移。

(a)                                                 (b)

图3-3-6-4

3.3.7数组/指针/结构操作

对数组、指针、结构的操做通过使用基址加变址的方式访问操做,根本上都是对指针的操做。注意指针的算数操做是以它们指向的对象的大小为单位进行操作的(即伸缩),而这种大小并不一定是一个字节。

最长用的内存访问形式如 Imm(rb, ri, s) ,其中Imm是立即数偏移,是基址寄存器,是变址寄存器,s是比例因子(必须是1、2、4、8),在对数组、指针、结构访问时s往往是指针指向的对象大小。

图3-3-7-1显示了编译器是怎么实现对main函数的参数char *argv[]这一数组进行访问的(该数组的元素为一个指向字符的指针,大小为8字节,寄存器rbx中存放的是argv数组的首地址)

图3-3-7-1

3.3.8函数操作

call指令用来进行过程调用(即调用其他函数),ret指令用来进行从过程中调用返回(即return)。

在进行过程调用时需要设置被调用过程的参数。参数小于6个时,依次使用%rdi, %rsi, %rdx, %rcx, %r8, %r9传递参数;参数个数大于六个则需要使用栈进行参数传递,栈中存放参数的位置如图3-3-8-1所示。

图3-3-8-1

图3-3-8-2显示了printf函数调用时参数传递过程。(%rdi存放格式化用的字符串,%rsi与%rdx分别存放着两个要打印的字符串的首地址。)

图3-3-8-2

图3-3-8-3显示了atoi函数调用时参数传递过程。(%rdi中存放字符串的首地址)

图3-3-8-3

图3-3-8-4显示了sleep函数调用时参数传递过程。(%edi中存放所需要sleep的秒数)

图3-3-8-4

图3-3-8-5显示了exit函数调用时参数传递过程。(%edi中存放了退出状态)

图3-3-8-5

图3-3-8-6显示了main函数的返回。

图3-3-8-6

对于其他函数的调用这里不再赘述。

3.4 本章小结

本章简述了编译的概念与作用;示范了如何使用gcc工具对源程序进行编译操做;针对生成的汇编代码文件(.s后缀),在数据、赋值、类型转换、sizeof、算数操做&逻辑/位操做、关系操作&控制转移、数组/指针/结构操做、函数操做共8个方面进行了解析。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。这个过程便是汇编,如图4-1-1所示。

汇编将机器不可识别的指令转化成机器可识别的指令的同时还会进行构造符号表等工作。

图4-1-1

4.2 在Ubuntu下汇编的命令

命令: gcc -c hello.s -o hello.o

如图4-2-1.

图4-2-1

4.3 可重定位目标elf格式

可重定位目标文件的典型格式如图4-3-1.

图4-3-1

如图4-3-2,我通过输出重定向把readelf读出的信息存入了文件hello_o_readelf.txt中。之后我们可以直接在hello_o_readelf.txt中获取信息,也可以通过其他途径获取信息。

图4-3-2

获取的信息如下:

4.3.1ELF头

ELF头(如图4-3-1-1所示)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序(小端序)。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(REL可重定位文件)、机器类型(x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。

图4-3-1-1

4.3.2节头部表

不同节的位置和大小是由节头部表(如图4-3-2-1)描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。通过节头部表,我们可以直到各节的名称、类型、地址、偏移量、地址、对齐信息等。以.text节为例,我们可以得知其类型为PROGBITS,地址为0x0,偏移量为0x40字节,大小为0x41字节,全体大小为0字节,旗标为AX,链接标志为0,信息标志为0,对齐要求为1字节。

图4-3-2-1

4.3.3符号表

符号表(如图4-3-3-1)中存放着程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在. symtab 中都有一个符号表(除非程序员特意用STRIP命令去掉它)。和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。通过符号表,我们可以得出该符号名字(通过其在字符串表中的字节偏移量),定义它的节,它在定义它的节中的距起始位置的偏移,大小,类型,源文件路径名,符号是本地还是全局的等信息。

图4-3-3-1

4.3.4重定位节

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data中。这些重定位条目都放在重定位节(如图4-3-4-1)中。

图4-3-4-1

其中偏移量是需要被修改的引用的节偏移。符号名称标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用。加数是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

我们只关心其中两种最基本的ELF的重定义类型:

  1. R_X86_64 PC32。重定位一个使用32位PC相对地址的引用。当CPU执行一条使用PC 相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。
  2. R _X86_ 64_ 32。 重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

链接器的重定位算法的伪代码如图4-3-4-2所示。

图4-3-4-2

对于该重定位节中的R_X86_64_PLT32类型,PLT 是一个新的函数入口表的格式,目前不必深究。

4.3.5其他节

如图4-3-5-1所示,文件中没有section groups, 程序头和dynamic section.

图4-3-5-1

4.4 Hello.o的结果解析

如图4-4-1,我通过输出重定向将调用objdump -d -r hello.o命令生成的反汇编存储到文件hello_o_objdump.txt中。

图4-4-1

反汇编内容如图4-4-2.

图4-4-2

hello.s的内容如图4-4-3.

图4-4-3

对照分析汇编代码和反汇编代码得:

  1. 两者的指令并没有什么不同的地方。
  2. 汇编语言(操作码和操作数)和二进制机器语言存在双射的关系。
  3. 对分支转移,反汇编的跳转指令的操作数不是段名称,而是下一条指令的相对地址的偏移量。
  4. 对函数调用,原汇编(.s)文件中函数调用指令的操作数是函数名称,而反汇编函数调用指令的操作数是下一条指令的相对地址的偏移量。

4.5 本章小结

本章简述了汇编的概念与作用;示范了如何用gcc工具进行汇编操做;简单分析了可重定位目标文件elf格式中的ELF头、节头部表、符号表,重定位节中的内容;对hello.o文件的反汇编结果与源汇编(.s)文件进行了对照分析,得出了一些两者之间的共同之处与一些差别,以及汇编语言与二进制机器语言之间的关系。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源化码被翻译成机器代码时;也可以执行于加载时(load time), 也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。

这里的链接是指将hello.o与其依赖的模块中的各种代码和数据片段收集并组合为一个可执行文件hello的过程。以printf.o为例展示链接过程如图5-1-1.

图5-1-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

如图5-2-1所示。

图5-2-1

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

可执行目标文件的典型格式如图5-3-1.

图5-3-1

通过输出重定向将readelf读出的信息存放到hello_readelf.txt文件中,如图5-3-2所示。

图5-3-2

readelf读出的信息如下。(各段的起始地址,大小等信息存放在程序头部表中)

5.3.1ELF头

如图5-3-1-1所示。

图5-3-1-1

可执行目标文件的ELF头描述文件的总体格式,它还包括程序的入口点(11行),也就是当前程序运行要执行的第一条指令的地址。

5.3.2节头部表

如图5-3-2-1所示。

图5-3-2-1

不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。通过节头部表,我们可以直到各节的名称、类型、地址、偏移量、地址、对齐信息等。

5.3.3程序头部表

如图5-3-3-1所示。

图5-3-3-1

ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片(chunk)被映射到连续的内存段。程序头部表描述了这种映射关系。

例如,从程序头部表,我们可以看到根据可执行目标文件的内容初始化一个内存段。第95行和第96行告诉我们第一个段(代码段)有读/执行访问权限,开始于内存地址0x400000处,总共的内存大小是0x5c0 字节,并且被初始化为可执行目标文件的头0x5c0 个字节,其中包括ELF头、程序头部表以及.init.text和.rodata节。

5.3.4 Section to Segment mapping

如图5-3-4-1所示。

图5-3-4-1

5.3.5 Dynamic section

如图5-3-5-1所示。

图5-3-5-1

5.3.6重定位节

如图5-3-6-1所示。

图5-3-6-1

5.3.7 Symbol table

如图5-3-7-1所示。

图5-3-7-1

5.3.8其他节

如图5-3-8-1所示。

(a)

(b)

图5-3-8-1

5.4 hello的虚拟地址空间

利用hexedit工具,我们可以看到可执行程序中的二进制编码(以十六进制查看),根据节头部表我们可以得出各个节对文件开始处的偏移量,同时,我们可以利用edb工具加载hello,查看进程中的虚拟地址空间各段信息,由于节较多,所以我们只举几个例子。

  1. 首先看到代码段总是从0x400000处开始的。

图5-4-1(hello起始位置数据)

图5-4-2(内存起始位置数据)

比较二者可以发现内容一样。

  1. .text节:

图5-4-3(.text节在节头部表中的信息)

图5-4-4(.text节最前边部分在虚拟内存中的位置及数据)

图5-4-5(.text节最前边部分在hello文件中的偏移位置及数据)

比较二者可以发现内容一样。

  1. .rodata节

图5-4-6(.rodata节在节头部表中的信息)

图5-4-7(.rodata节最前边部分在虚拟内存中的位置及数据)

图5-4-8(.rodata节最前边部分在hello文件中的偏移位置及数据)

比较二者可以发现内容一样。

  1. .data节

图5-4-9(.data节在节头部表中的信息)

图5-4-10(.data节最前边部分在虚拟内存中的位置及数据)

图5-4-11(.data节最前边部分在hello文件中的偏移位置及数据)

比较二者可以发现内容一样。

可以看到进程的虚拟地址空间各段信息与使用readelf工具查看hello可执行文件得到的信息一致。

5.5 链接的重定位过程分析

使用输出重定向将输入命令objdump -d -r hello 后得到的信息存入文件hello_objdump.txt中。

hello_objdump.txt部分内容如图5-5-1.

图5-5-1

可以发现,主要区别在于在原.o文件标有R_X86_64_的标记下其编码后面几位都是0,在重定位后这些位置机器码被重写定位,这是重定位寻址的结果,重定位寻址主要由两种方式,一种是相对寻址(R_X86_64_PC32),一种是绝对寻址(R_X86_64_32)。对于前者,是将下一条PC值与相对地址做差;对于后者,直接将原地址放入其中即可。重定位算法见4.3.4小节。

继续分析可以发现,hello与hello.o不同处在于:

1.链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。

2.增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。多出的节表头及功能见表5-5-1.

3.函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。

4.地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。

节表头名称

功能

.init

程序初始化需要执行的代码

.plt

动态链接-过程链接表

.dynamic

存放被ld.so使用的动态链接信息

.data

初始化了的数据

.comment

一个包含编译器的NULL-terminated字符串

.interp

保存ld.so的路径

.note.ABI-tag

Linux下特有的section

.hash

符号的哈希表

.gnu.hash

GNU拓展的符号的哈希表

.dynsym

存放.dynsym节中的符号名称

.gnu.version

符号版本

.gnu.version_r

符号引用版本

.rela.dyn

运行时/动态重定位表

.rela.plt

.plt节的重定位条目

.fini

当程序正常终止时需要执行的代码

eh_frame

Contains exception unwinding and source language information.

表5-5-1

5.6 hello的执行流程

  1. 加载:_dl_start、_dl_init
  2. 开始执行:_start、_libc_start_main
  3. 执行main:_main、_printf、_exit、_sleep、_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
  4. 退出:exit

调用与跳转的各个子程序名如下:

._dl_start, ._dl_init, ._cax_atexit, ._new_exitfn, ._libc_start_main,

._libc_csu_init, ._main, ._printf, ._atoi, ._sleep, ._getchar,._exit

._dl_runtime_resolve_xsave, ._dl_fixup, ._dl_lookup_symbol_x, .exit

5.7 Hello的动态链接分析

对于动态共享链接库中位置无关代码,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理。编译器利用了数据段与代码段的距离是一个运行时常量的事实,在数据段开始的地方创建一个表,叫做全局偏移量表GOT,每个被这个目标模块引用的全局数据目标都有一个8字节条目,还会生成一个重定位记录,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的绝对地址.

程序调用共享库函数时,只有函数第一次被用到时才进行绑定(符号解析、重定位),如果没有用到则不进行绑定。这种方案被称为延迟绑定。

调用dl_init之前的GOT表如图5-7-1所示。

图5-7-1

调用dl_init之后的GOT表如图5-7-2所示。

图5-7-2

可以看出其已经动态链接,GOT条目已经改变。

5.8 本章小结

本章介绍了链接的概念与作用;示范了如何用gcc工具进行连接操做;查看了可执行目标文件的elf格式;将hello加载如内存后的虚拟地址空间与可执行目标文件中的节头部表进行了对照,得出了一致的结论;分析了链接的重定位过程,并与之前的.o的反汇编进行了对照,列出了一些区别;简述了hello的执行流程;简单分析了hello的动态链接过程。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程提供给应用程序两个关键抽象;

  1. 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
  2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

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

处理流程为:

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

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

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

(4)如果是内置的shell命令,则立即执行。

(5)如果不是内置的shell命令,调用fork( )创建子进程

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

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

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

6.3 Hello的fork进程创建过程

我们输入命令行./hello 1190200608 琚晓龙 1, shell对命令行进行解析,由于第一个命令行参数不是一个内置的shell命令,shell会调用fork()创建子进程。

终端程序通过调用fork()函数创建一个子进程,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID.

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

fork调用一次,却会返回两次:一次是在调用进程中(返回子进程的PID);一次是在新创建的子进程中(返回0)。

父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。

6.4 Hello的execve过程

execve 函数在子进程中加载并运行hello程序时,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

  1. 删除已存在的用户区城。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的. text 和.data区。.bss区域是请求二进制零的, 映射到匿名文件,其大小包含在hello中。 栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域。hello程序与共享对象链接,如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC)。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

图6-4-1概括了私有区域的不同映射。

图6-4-1

6.5 Hello的进程执行

  1. 上下文信息:

上下文(context)就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。内核为每个进程维持一个上下文。

  1. 进程时间片:

一个逻辑流的执行在时间上与另二个流重叠,称为并发流(Concurrer flow).这两个流被称为并发地运行。多个流并发地执行的般现象被称为并发(concurrency)。 一个进程和其他进程轮流运行的概念称为多任务(multitasking)。一个进程执行它的控制流的一部分的每一时间段叫做时间片(time slice)。

  1. 进程调度:

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切:1)保存当前进程的上下文;2)恢复某个先前被抢占的进程被保存的上下文;3)将控制传递给这个新恢复的进程。

  1. 用户模式和内核模式::

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

对于hello进程,最初运行在用户模式下,直到它通过执行系统调用函数read陷入到内核。内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。

磁盘取数据要用一段相对较长的时间(数量级为几十毫秒), 所以内核执行从hello进程到另一进程的上下文切换,而不是在这个间歇时间内等待,什么都不做。在切换之前,内核正代表hello进程在用户模式下执行指令(即没有单独的内核进程)。在切换的前部分中,内核代表hello进程在内核模式下执行指令。然后在某一时刻,它开始代表另一个进程(仍然是内核模式下)执行指令。在切换之后,内核代表另一进程在用户模式下执行指令。

随后,另一个进程在用户模式下运行一会儿,直到磁盘发出一个中断信号,表示数据已经从磁盘传送到了内存。内核判定该进程已经运行了足够长的时间,就执行一个从该进程到hello进程的上下文切换,将控制返回给hello进程中紧随在系统调用read之后的那条指令。

hello进程继续运行,输出“hello 1190200608 琚晓龙”,然后调用sleep函数,该函数显示地请求让hello进程休眠。此时会发生和之前类似的上下文切换,直到sleep函数发出一个信号表示休眠结束,内核判定正在运行的进程已经运行了足够长的时间,就执行一个从当前进程到hello进程的上下文切换,将控制返回给hello进程中紧随在系统调用sleep之后的那条指令。直到下一次异常发生,依此类推。

当程序输出8次“hello 1190200608 琚晓龙”并调用sleep完成休眠后,该进程调用getchar,而实际落脚到执行输入流是stdin的系统调用read,之后陷入内核,

内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。然后和之前的上下文切换相同,直到键盘缓冲区发出一个信号。内核判定正在运行的进程已经运行了足够长的时间,就执行一个从当前进程到hello进程的上下文切换,将控制返回给hello进程中紧随在系统调用read之后的那条指令。之后hello程序运行完毕,主函数返回0,进程结束,回收进程。

6.6 hello的异常与信号处理

Hello执行过程中会出现4种异常:中断、陷阱、故障、终止。

  1. 中断:外部的I/O设备造成的,这里是键盘。
  2. 陷阱:系统调用,hello会执行系统调用函数read陷入到内核。
  3. 故障:故障由错误情况引起,它可能被故障处理程序修正,hello在被加载器加载后,第一次执行取命令时,便会发生缺页故障,之后由缺页故障处理程序将虚拟页缓存到物理内存。
  4. 终止:不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

正常执行,如图6-6-1:

图6-6-1

程序正常执行,最后输入回车后hello正常返回。

回车,如图6-6-2:

图6-6-2

输入回车后,shell把回车存入输入缓冲区,hello在循环结束后从输入缓冲区中读取一个回车后正常返回,shell把剩下的回车从输入缓冲区读出。

Ctrl-Z,如图6-6-3:

图6-6-3

输入Ctrl-Z,内核发送一个SIGTSTP信号到前台进程组的每个进程,默认情况是终止前台作业hello进程停止。

Ps,如图6-6-4:

图6-6-4

此时输入命令ps可以看到当前存在的进程仍由hello.

Jobs,如图6-6-5:

图6-6-5

此时输入命令jobs,可以看到当前的工作(hello)已经停止。

Pstree,如图6-6-6:

图6-6-6

此时输入命令pstree,可以看到当前的进程树,此处为部分截图。

Fg,如图6-6-7:

图6-6-7

之后输入fg命令,hello程序继续执行。

Ctrl-C,如图6-6-9:

图6-6-9

输入Ctrl-C,内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,hello进程终止。

乱码:,如图6-6-10

图6-6-10

输入乱码,在hello执行期间,shell只是将这些乱码存入输入缓冲区,直到hello循环结束从缓冲区中读取一个字符,输入缓冲区中剩下的乱码作为命令输入到shell中。

6.7本章小结

本章简述了进程的概念与作用,Shell-bash的作用与处理流程,hello的fork进程创建过程、execve过程、进程执行、异常与信号处理。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 “段标识符:段内偏移量”。
  2. 线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
  3. 虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
  4. 物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。

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

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器,

索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。段标识符的具体作用,每一个段描述符由8个字节组成。

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。何时用GDT或是LDT由段选择符中的T1字段表示,0表示用GDT,1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,即逻辑地址。将内存分为不同的段,每个段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。

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

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

虚拟地址分为虚拟页号VPN和虚拟页偏移量VPO。通过页表基址寄存器在页表中获得页表条目PTE,一条PTE中包含有效位、权限信息以及物理页号,如果有效位是0 NULL则代表未分配;如果是有效位0 非NULL则代表未缓存,如果有效位是1则代表已缓存,可以通过PTE得到物理页号PPN,物理页号PPN与虚拟页偏移量共同构成物理地址PA。

其中:

未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相 关联,因此也就不占用任何磁盘空间。

缓存的:当前已缓存在物理内存中的已分配页。

未缓存的:未缓存在物理内存中的已分配页。

图7-3-1的示例展示了一个有8个虚拟页的小虚拟内存。虚拟页0和3还没有被分配,

图7-3-1

形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射,如图7-3-2.

图7-3-2

这里

图7-3-3展示了MMU如何利用页表来实现这种映射。CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offset, VPO)和一个(n- p)位的虚拟页号(Virtual Page Number, VPN)。MMU利用VPN来选择适当的PTE。例如,VPN0选择PTE0, VPN 1选择PTE1,以此类推。将页表条目中物理页号(Physical Page Number, PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。

图7-3-3

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

每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据, 代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一-个关于PTE的小的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer, TLB)。

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。如图7-4-1所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号取出来的。如果TLB由T=2t 个组,那么TLB索引(TLBI)就是由PTN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

图7-4-1

图7-4-2给出了如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含LI 页表的物理地址。VPN 1提供到一个LI PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。

图7-4-2

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

使用7.4中获得的PA,首先取组索引对应位,向L1 cache中寻找对应组。

如果存在,检查对应行的有效位是否为1,并比较标志位,。如果上述条件均满足则命中。否则按顺序对L2 cache、L3 cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1 cache,并将数据传送给CPU。数据块返回到L1 cache时,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块按照一定的策略驱逐,将目标块放到被驱逐块的原位置。

如果不存在则可能时地址错误,发生终止异常。

7.6 hello进程fork时的内存映射

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

当fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下步骤:

  1. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区城。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。 栈和堆区域也是请求二进制零的, 初始长度为零。
  3. 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

图7-7-1概括了私有区域的不同映射。

图7-7-1

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

MMU在试图翻译某个虚拟地址A时,有时会触发一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

虚拟地址A是合法的吗?换句话说,A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序会搜索区域结构的链表,把A和每个区域结构中的vm_start 和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在图7-8-1中标识为“1”。

因为一个进程可以创建任意数量的新虚拟内存区域(使用mmap函数),所以顺序搜索区域结构的链表花销可能会很大。因此在实际中, Linux 在链表中构建了一棵树,并在这棵树上进行查找。

试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图7-8-1中标识为“2”。

此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:1)选择一个牺牲页面, 如果这个牺牲页面被修改过,那么就将它交换出去,换人新的页面并更新页表。2)当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。

图7-8-1

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部.

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格。 两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块:

  1. 显式分配器(explicit allocator):

要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。

  1. 隐式分配器(implicit allocator):

要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。

堆中的块主要组织为两种形式:

1.隐式空闲链表(带边界标记)

在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。因为空闲块是通过头部隐含地连接着的,所以把这种结构成为隐式空闲链表。块的格式如图7-9-1

图7-9-1

2.显式空闲链表

也可以将空闲块组织成某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里。例如,堆可以组织成一个空闲链表,在每个空闲块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。块的格式如图7-9-2.

图7-9-2

还有一种流行的减少分配时间的方法,通常称为分离存储(segregated storage),就是维护多个空闲链表,其中每个链表中的块有大致相等的大小。一般的思路是将所有可能的块大小分成一些等价类, 也叫做大小类(size class)。有很多种方式来定义大小类。

分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为n的块时,它就搜索相应的空闲链表。如果不能找到合适的块与之匹配,它就搜索下一个链表,以此类推。

下边简单介绍三种基本方法。

1.简单分离存储

使用简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。

2.分离适配

使用这种方法,分配器维护着一个空闲链表的数组。每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显式或隐式链表。每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。有许多种不同的分离适配分配器。

3.伙伴系统

.伙伴系统是分离话配的一种特例。 其中每个大小类那是2的幂。基本的思路是假设一个堆的大小为2m个字, 我们为每个块大小2k维护一个分离空闲链表。其中0<=k<=m。请求块大小向上舍入到最接近的2的幂。最开始时,只有一个大小为2m个字的空闲块

常见的分配策略有首次适配(first fit)、下一次适配(next fit)和最佳适配(best fit)。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。

首次适配的优点是它趋向于将大的空闲块保留在链表的后面。缺点是它趋向于在靠近链表起始处留下小空闲块的“碎片”,这就增加了对较大块的搜索时间。下一次适配比首次适配运行起来明显要快一些,尤其是当链表的前面布满了许多小的碎片时。然而,一些研究表明,下一次适配的内存利用率要比首次适配低得多。研究还表明最佳适配比首次适配和下一次适配的内存利用率都要高一些。在简单空闲表组织结构中,比如隐式空闲链表中,使用最佳适配的缺点是它要求对堆进行彻底的搜索。

7.10本章小结

本章介绍了存储器地址空间的各个概念;介绍了如何通过段式管理将逻辑地址转换成线性地址,以及如何通过页式管理将线性地址转换为物理地址;介绍了TLB的概念和用法;简述了四级页表下的VA到PA的变换,以及三级Cache下的物理内存访问;在虚拟内存空间层面上,再次讲述了hello的fork和exeve过程;详细说明了当缺页异常发生时的处理;介绍了动态分配器的相关概念和分配管理方法与策略。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

输入/输出(I/O)是在主存和外部设备 (例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。

一个 Linux文件就是一个m个字节的序列:

B0, B1, .., Bk, ..,Bm-1

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。

8.2 简述Unix IO接口及其函数

这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输人和输出都能以一种统一且一致的方式来执行:

●打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

●Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h> 定义了常量STDIN FILENO、STDOUT FILENO和STDERR FILENO, 它们可用来代替显式的描述符值。

●改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。

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

类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k 开始,然后更新k。

●关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O函数:

●int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。这些位的符号名字如图8-2-1所示。

作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限被设置成mode & ~umask.

●int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。

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

●ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

图8-2-1

8.3 printf的实现分析

前提:printf和vsprintf代码是windows下的。

printf代码如下:

int printf(const char fmt, …){

int i;

char buf[256];

va_list arg = (va_list)((char)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

printf拥有一个不定长参数,arg 获得不定长参数,即输出的时候格式化串对应的值。

vsprintf代码如下:

int vsprintf(char *buf, const char fmt, va_list args){

char p;

char tmp[256];

va_list p_next_arg = args;

for (p = buf; *fmt; fmt++) {

if (*fmt != ‘%’){

*p++ = *fmt;

continue;

}

fmt++;

switch (*fmt) {

case ‘x’:

itoa(tmp, ((int)p_next_arg));

strcpy(p, tmp);

p_next_arg += 4;

p += strlen(tmp);

break;

case ‘s’:

break;

default:

break;

}

}

return (p - buf);

}

vsprintf 按照格式fmt结合参数args生成格式化之后的字符串,并返回字符串的长度。printf调用系统函数write(buf,i)将长度为 i 的 buf 输出。write函数的第一个参数为描述符,1代表标准输出。

syscall将字符串中的字节从寄存器通过总线以ASCII码格式复制到显卡的显存中,字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存储到vram中。

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

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

本章介绍了Linux的IO管理方法;简述了Unix IO接口机器函数;分析了printf函数和getchar的实现。

(第81分)

结论

Hello 的C程序文本由程序员编写,得到源代码。首先,预处理器根据字符#开头的命令,修改原始的C程序,得到另一个C程序,以 .i 作为文件扩展名。然后,编译器将文本文件hello.i翻译成一个包含汇编语言程序的文本文件hello.s。接下来汇编器将hello.s翻译成机器语言指令,并把这些语言指令打包成一种叫做可重定位目标程序的格式,将结果存放在文件hello.o中。最后,链接器将hello.o与其所依赖的所有模块文件中的各种代码和数据片段收集并组合为一个可执行文件,至此hello已经编程了一个可以加载并执行的文件。

然后由用户在shell中输入指令“./hello 1190200608 琚晓龙”,shell根据命令行通过调用函数fork创建子进程,并在子进程中调用函数execve执行hello程序。首先加载器会为hello进程分配物理内存(内存映射),然后再执行的时候不断的把代码段、数据段、等内容缓存到主存中(缺页异常处理)。Hello进程执行时,也会不断地检测着来自内核的各种异常信号,例如键盘(I/O)输入的中断信号,缺页故障信号等。当hello程序执行完成后,hello进程会终止,由其父进程(shell)回收。

到此,hello经历完所有过程。

全文以hello程序为示例,跟踪了在计算机系统中一个程序一步步被执行的过程。通过这个过程,我们得以窥见计算机系统执行一个程序的基本流程与原理,并进一步深入理解了计算机软件、硬件的相互配合。hello虽小,却是一切代码的映射,再大的工程在执行时也不外经历这么几步。对hello执行流程的深入分析对我们深入了解计算机系统有很大帮助。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

附件见资源链接:大作业.doc(带附件)

中间结果文件名

文件的作用

文件

hello.c

hello C程序文本(源代码)

hello.i

hello.c预处理后生成的文件

hello.s

hello.i编译后生成的文件

hello.o

hello.s汇编后生成的文件

hello_o_readelf.txt

存放使用readelf工具查看hello.o文件的结果的文件

hello_o_objdump.txt

存放使用objdump工具查看hello.o文件的结果的文件

hello(.out)

Hello程序的可执行文件

hello_readelf.txt

存放使用readelf工具查看hello文件的结果的文件

hello_objdump.txt

存放使用objdump工具查看hello文件的结果的文件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[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分)

2021春深入理解计算机系统大作业——程序人生相关推荐

  1. HIT 深入理解计算机系统 大作业 程序人生-Hello’s P2P

    HIT 深入理解计算机系统 大作业 程序人生-Hello's P2P 本论文旨在研究 hello 在 linux 系统下的整个生命周期.结合 CSAPP 课本, 通过 gcc 等工具进行实验,从而将课 ...

  2. 2021春深入理解计算机系统大作业---hello的一生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L021725 班    级 2003006 学       生 杨楠 指 导 ...

  3. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  4. 哈工大 2021春 计算机系统 大作业程序人生

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机 学 号 1190200828 班 级 1936601 学 生 赵英帅 指 导 教 师 刘宏伟 计算机科学与技术学院 202 ...

  5. 哈工大2021春计算机系统大作业 程序人生-Hello’s P2P

          计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学     号 1190200613 班     级 1903004 学       生 ...

  6. 2021哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 计算机科学与技术学院 2021年6月 摘  要 本文介绍了hello的整个生命过程.利用gcc,gdb,edb,readelf,H ...

  7. 2022春 计算机系统大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 班 级 学 生 指 导 教 师 计算机科学与技术学院 2022年5月 摘 要 为深入理解计算机系统,本文以hel ...

  8. 哈工大2022春计算机系统大作业:程序人生-Hello‘s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学   号 120L021305 班   级 2003002 学       生 李一凡 指 导 教 ...

  9. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

最新文章

  1. 深入浅出OOP(四): 多态和继承(抽象类)
  2. 自学python爬虫要多久-初学Python爬虫要学多久?原来这么快
  3. 图像的阈值分割(迭代法选择阈值)
  4. easyui收派标准客户端表单校验
  5. 二十年后的回眸(2)——顺风顺水的前三年
  6. ModuleNotFoundError: No module named 'distutils.core'
  7. 电话号码的字母组合—leetcode17
  8. lucene 第一天
  9. 无服务器–仅仅是构建现代应用程序的一种方法?
  10. Eclipse 版本升级:如何不卸载旧版本 Eclipse 实现在线升级到最新版本?
  11. C#从数据库导出数据到CSV
  12. Spring学习8-Spring事务管理(注解式声明事务管理)
  13. php将汉字转换为gb2312编码,php实现utf-8和GB2312编码相互转换
  14. iOS越狱设备安装Frida不成功
  15. Excel 2010去掉网格线
  16. 进qq空间显示服务器失败,QQ空间找不到服务器-进空间找不到服务器的解决办法...
  17. 优客365网站导航开源版 v1.5.2
  18. 【老九学堂】【C语言进阶】内置函数补充
  19. c++ 高精度 加减乘除 四则运算 代码实现
  20. LSM-tree原理与应用

热门文章

  1. vue的proxyTable的地址代理和重定向,配合nginx的地址代理问题
  2. 如何在kubernetes中使用共享GPU资源
  3. 一些汇编指令和寄存器。
  4. UpdateDate()
  5. Camera效果测试-色彩准确性及饱和度测试
  6. supersqli(SQL注入流程及常用SQL语句)
  7. CList POSITION
  8. 什么是反应式编程? 这里有你想要了解的反应式编程 (Reactive programming)
  9. html代码中的nofollow属性
  10. 业务:pdf转图片问题(解决非标准pdf转图片空白问题)