计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 119020XXXX
班 级 XXXX
学 生 XXXX
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年6月
摘 要
本文从计算机系统的角度介绍了hello的一生。
P2P,O2O。
从hello.c(Program),接着预处理、编译、汇编、链接,历经艰辛,Hello - 一个完美的生命诞生了。
在壳(Bash)里,伟大的OS(进程管理)为其fork(Process),为其execve,为其mmap,分其时间片,竭尽所能让其得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);
OS(存储管理)与MMU为VA到PA为其操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为其加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使其能在键盘、主板、显卡、屏幕间游刃有余。最终,被bash所回收。
计算机系统 = Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO等
关键词:计算机系统;编译;存储系统;链接;进程管理;I/O管理;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 9 -
1.3 中间结果 - 9 -
1.4 本章小结 - 9 -
第2章 预处理 - 10 -
2.1 预处理的概念与作用 - 10 -
2.2在UBUNTU下预处理的命令 - 11 -
2.3 HELLO的预处理结果解析 - 11 -
2.4 本章小结 - 14 -
第3章 编译 - 15 -
3.1 编译的概念与作用 - 15 -
3.2 在UBUNTU下编译的命令 - 15 -
3.3 HELLO的编译结果解析 - 15 -
3.4 本章小结 - 21 -
第4章 汇编 - 22 -
4.1 汇编的概念与作用 - 22 -
4.2 在UBUNTU下汇编的命令 - 22 -
4.3 可重定位目标ELF格式 - 23 -
4.4 HELLO.O的结果解析 - 29 -
4.5 本章小结 - 30 -
第5章 链接 - 31 -
5.1 链接的概念与作用 - 31 -
5.2 在UBUNTU下链接的命令 - 31 -
5.3 可执行目标文件HELLO的格式 - 32 -
5.4 HELLO的虚拟地址空间 - 35 -
5.5 链接的重定位过程分析 - 39 -
5.6 HELLO的执行流程 - 41 -
5.7 HELLO的动态链接分析 - 42 -
5.8 本章小结 - 44 -
第6章 HELLO进程管理 - 45 -
6.1 进程的概念与作用 - 45 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 45 -
6.3 HELLO的FORK进程创建过程 - 46 -
6.4 HELLO的EXECVE过程 - 46 -
6.5 HELLO的进程执行 - 46 -
6.6 HELLO的异常与信号处理 - 48 -
6.7本章小结 - 52 -
第7章 HELLO的存储管理 - 53 -
7.1 HELLO的存储器地址空间 - 53 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 53 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 53 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 55 -
7.5 三级CACHE支持下的物理内存访问 - 57 -
7.6 HELLO进程FORK时的内存映射 - 58 -
7.7 HELLO进程EXECVE时的内存映射 - 59 -
7.8 缺页故障与缺页中断处理 - 59 -
7.9动态存储分配管理 - 60 -
7.10本章小结 - 60 -
第8章 HELLO的IO管理 - 58 -
8.1 LINUX的IO设备管理方法 - 61 -
8.2 简述UNIX IO接口及其函数 - 61 -
8.3 PRINTF的实现分析 - 62 -
8.4 GETCHAR的实现分析 - 63 -
8.5本章小结 - 63 -
结论 - 63 -
附件 - 63 -
参考文献 - 66 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
hello 1
编译流程具体如下:
1) 预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2) 编译阶段。编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。每条语句都以一种文本格式描述了一条低级机器语言指令。
hello 2
3) 汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的17 个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
4) 链接阶段。请注意,hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。printf 函数存在于一个名为printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
3. 运行hello程序:
此刻,hello.c 源程序已经被编译系统翻译成了可执行目标文件hello,并被存放在磁盘上。要想在Unix系统上运行该可执行文件,我们将它的文件名输人到称为shell的应用程序中,如下:
linux> ./he1lo
he1lo,world
linux>
shell是一个命令行解释器,它输出一个提示符,等待输人一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令,那么shell 就会假设这是一个可执行文件的名字,它将加载并运行这个文件。所以在此例中,shell将加载并运行hello程序,然后等待程序终止。hello 程序在屏幕上输出它的消息,然后终止。shell随后输出一个提示符,等待下一个输人的命令行。
Hello程序具体实现如下:
1) 初始时,shell 程序执行它的指令,等待我们输入一个命令。当我们在键盘上输人字符串“. /hello"后,shell 程序将字符逐一读入寄存器,再把它存放到内存中,如图hello 3所示。
hello 3(摘自csapp)
2) 当我们在键盘上敲回车键时,shell 程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串“hello, world\n"。利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。这个步骤如图hello 4所示。
hello 4//如无具体说明均摘自csapp书
3) 一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将“hello, world\n”字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。这个步骤如hello 5所示。
hello 5
4. hello程序运行时操作系统中软硬件的交互
当shell加载和运行hello程序时,以及hello程序输出自己的消息时,shell 和hello程序都没有直接访问键盘、显示器、磁盘或者主存。取而代之的是,它们依靠操作系统提供的服务。我们可以把操作系统看成是应用程序和硬件之间插入的一层软件,如图hello 6所示。所有应用程序对硬件的操作尝试都必须通过操作系统。
hello 6
操作系统有两个基本功能: (1)防止硬件被失控的应用程序滥用;(2)向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。如图hello 7所示,文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。
hello 7
进程:
进程是操作系统对一个正在运行的程序的一种抽象。在-一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。
Hello程序运行中有两个并发的进程: shell进程和hello进程。最开始,只有shell 进程在运行,即等待命令行上的输入。当我们让它运行hello程序时,shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传给新的hello进程。hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,shell进程会继续等待下一个命令行输入。
2)hello在虚拟内存中的映射
虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。
在linux中,地址空间最上方区域是保留给操作系统中的代码和数据的,这对所有进程来说都是一样。地址空间的底部区域存放用户进程定义的代码和数据。
hello 8 进程的虚拟地址空间
a. 程序代码和数据
在开始时,对hello的代码是在hello进程中,从虚拟地址0x400000位处开始存放的(事实上,所有进程都从此处开始),紧接着是和c全局变量相对应的数据位置(数据段和代码段),代码区和数据区是直接按照可执行目标文件(hello)的内容初始化。
b.堆
代码和数据区后紧随着的是运行时堆。代码和数据区在进程开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。
在hello程序中,没有分配空间,用不到堆。
c.共享库
在地址空间的中间部分用来存放c标准库和数学库这样的代码和数据的区域。在hello运行时,共享库加载包含hello程序中调用的printf函数的stdio库相关编码。
d.栈
在用户可视虚拟地址空间顶部是用户栈,编译器用它来实现函数调用。用户栈在程序执行的期间动态地扩展和收缩。
e.内核虚拟内存
地址空间顶部区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。应用程序通过调用内核执行这些操作。
Hello程序在运行时,通过调用内核进行上下文切换。结束时也由内核发送信号释放hello程序占用的内存空间,至此P2P与O2O结束。
1.2 环境与工具
1.2.1 硬件环境
Legion Y7000P 2019 PG0
CPU:Intel® Core™ i7-9750H CPU @ 2.60GHz (12 CPUs), ~2.6GHz
RAM: 16384MB
1.2.2 软件环境
Windows 10 家庭中文版 64-bit
Ubuntu 20.04.2 LTS
VMware® Workstation 16 Player 16.1.0 build-17198959
1.2.3 开发工具
Microsoft Visual Studio Community 2019版本 16.9.2
Microsoft Visual 1.54.3
GCC 9.3.0
1.3 中间结果
hello.i 预处理之后的文本文件。
hello.s 汇编后的文本文件。
hello.o 可重定位文件hello.o。
hello 从hello.o链接而成的可执行目标文件。
hello.elf 由可重定位目标文件hello.o生成的elf文件。
Hello1.elf 由可执行目标文件hello生成的elf文件。
hello.d 由可重定位目标文件hello.o反汇编得到的文件。
hello1.d 由可执行目标文件hello反汇编得到的文件。
1.4 本章小结
Hello从代码到可执行这一系列的过程是所有的程序的必经之路,也就是说,hello的一生是所有程序一生的缩影。Hello到可执行程序的过程中,经历了编译整个过程,内存的分配,CPU的工作等等等等。下面将详细进行介绍。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理概念
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3.条件编译。[1]
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.1.2 预处理作用
典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
C语言预处理程序的作用是根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。
预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。[2]
预处理对代码中的一些标记进行解析,使其能够被编译器处理。这个过程一般报包括以下内容:
1. 处理#define:展开
2. 处理#include:插入文件
3. 处理注释:删除
4. 处理条件预编译指令:执行删除
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i 或者 cpp hello.c -o hello.i
截图hello 9:
hello 9 预处理
2.3 Hello的预处理结果解析
2.3.1 预处理结果
生成的hello.i内容如下(hello 10):
hello 10 预处理结果
2.3.2 预处理结果解析
hello.i文件如下所示。
注释部分被抹去。在文件最开始写了hello.c的来源(见hello 11 hello.i)。接下来将hello.c中的#include <stdio.h> 、#include <unistd.h> 、#include <stdlib.h>命令告诉预处理器读取系统头文件stdio.h 、unistd.h、 stdlib.h的内容,并把它直接插入程序文本中(见hello 12 hello.i)。hello.i文件末尾是hello.c其余部分内容(见hello 13 hello.i)。
hello 11 hello.i
hello 12 hello.i
hello 13 hello.i
2.4 本章小结
本章介绍了预处理的相关概念及作用,并利用hello作为示例进行展示。将hello.c经过cpp的处理,转换成hello.i文件。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译会把预处理后的文件转换为汇编语言所描述的文件,汇编语言的产生是最初的程序员为了简化枯燥繁琐的机器语言(即指令)编程所创造的帮助记忆的符号(助记符),后发展成一门语言,再后来有了高级语言(例如C语言),编程逐渐变的简单了。汇编语言与指令是一一对应的,一套指令集对应一套汇编语言,如果我们换个一个CPU,例如 ARM,就需要使用ARM处理器所对应的汇编语言。
编译阶段首先进行词法分析和语法分析,检查代码的规范性、是否有语法错误等。接下来对代码进行优化,最后生成汇编代码。[3]
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
hello 14 hello.s
3.3 Hello的编译结果解析
生成.s文件。在.s文件如下:
hello 15 hello.s
hello 16 hello.s
hello 17 hello.s
而源程序(hello.c)为:
hello 18 hello.c
观察源代码(.c文件)我们发现,源代码中存在的数据如下:
表达式 数据类型 变量名 使用操作 赋初值
int sleepsecs = 2.5 int(全局) sleepsecs = 是
int argc int(局部) argc 传参 是
If(argc != 3) int(局部) argc != 是
char argv[] Char*(局部) argv 传参 是
Int main() Int(全局函数) Main 函数
Int i; Int(局部变量) i 无 否
for(i=0;i<10;i++) Int i =;<;++ 是
Printf(“Usage: Hello 学号 姓名!\n”); Char*(局部变量字符串) 是
printf(“Hello %s %s\n”,argv[1],argv[2]); Char*(局部变量字符串) 是
Return 0; Int Return
再在.s文件中查找相应的存储位置结果如下:
3.3.1 全局变量
对于全局变量sleepsecs,在.s文件中,他放置在开头的.globl模块,并设置为四字节对齐,如下图所示:
hello 19 sleepsecs
3.3.2 字符串
对于字符串类型的"Usage: Hello 学号 姓名!\n"以及"Hello %s %s\n",在.s文件中他们存在于.rodata段中,并将其命名为.LC0与.LC1,具体表示如下:
hello 20 字符串
3.3.3 函数类型
对于函数类型的主函数mian,.s文件将放置在.text的代码段中,标明它是全局的,且是一个函数。如下图:
hello 21 main函数
3.3.4 局部变量
对于局部变量argc,aggv,i返回值0等,都在具体代码中直接放置在栈上或使用寄存器存储。对于argc,aggv,放置在%rdi、%rsi中来传送。将立即数0移至%rax进行返回,将argc放置在-20(%rbp)处,将argv放置在-32(%rbp)处,当argc==3时,将i放置在-4(%rbp)处,并进行相应运算。
hello 22 局部变量argc,argv
hello 23 局部变量i
hello 24 返回值0
3.3.5 各种操作
文中出现了种种操作,比如=赋值,指针取址,!=、<比较,++,if控制跳转语句等,在.s文件中他们表现如下:
hello 25 比较操作
hello 26 比较操作
2. 对于赋值操作
.s文件中的赋值操作直接使用mov指令进行:
hello 27 赋值
3. 对于控制语句
直接使用跳转到某一个开关:
hello 28 跳转
4. 对于取值指令
通过使用指针的加减来进行位置的确定,并使用寄存器之间的mov来进行地址的调用与printf的传参。
hello 29 取址
5. 对于++操作
直接使用add指令进行:
hello 30 ++
3.4 本章小结
概括了汇编的概念及作用,以hello.s为例结合源码和汇编码分析解释了在汇编语言下是如何实现一些c中的数据类型及各种操作的,即c是如何转变为汇编语言的。经过编译,中间文件hello.i已经转换成hello.s并将继续在后续 步骤中转化为可执行文件。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编
汇编实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
代码段:该段中所包含的主要是程序的指令。
该段一般是可读和可执行的,但一般却不可写。
数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
UNIX环境下主要有三种类型的目标文件:
(1)可重定位文件
其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
(2)共享的目标文件
这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个 目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件
它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。[4]
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
截图如下:
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
节名 作用
节 ELF头 ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table) 的文件偏移,以及节头部表中条目的大小和数量。
.text 已编译程序的机器代码。
.rodata 只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data 已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率,在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
.symtab .symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,symtab符号表不包含局部变量的条目。
.rel.text 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
.rel.data 被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug 一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.line 原始C源函数程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.strtab 一个字符串标,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以NULL结尾的字符串的序列。
描述目标文件的节 节头部表 不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
hello 31 可重定位目标文件格式
使用指令:readelf -a hello.o > hello.elf
hello 32
查看hello.elf结果如下:
hello 33 hello.elf
hello 34 hello.elf
hello 35 hello.elf
对照.elf文件格式进行进一步分析:
节头部表如下:
hello 36 elf1
节的各项描述分别如下:(分别为名字、类型、地址、偏移、大小、entsize、标记、链接、信息以及对齐方式)
hello 37 elf2
重定位表(hello 38 elf3):
每个段都有重定位表,其间的重定位条目脑喊了连接时重定位所需的全部信息:
hello 38 elf3
符号表(hello 39 elf4):
name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value 是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size 是目标的大小(以字节为单位)type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding 字段表示符号是本地的还是全局的。每个符号都被分配到目标文件的某个节,由section字段表示,该字段也是一个到节头部表的索引。有三个特殊的伪节( pseudosection),它们在节头部表中是没有条目的:
ABS代表不该被重定位的符号;
UNDEF代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号;
COMMON表示还未被分配位置的未初始化的数据目标。对于COMMON符号,value 字段给出对齐要求,而size给出最小的大小。
注意,只有可重定位目标文件中才有这些伪节,可执行目标文件中是没有的。COMMON和.bss的区别很细微。现代的GCC版本根据以下规则来将可重定位目标文件中的符号分配到COMMON和.bss中:
COMMON:未初始化的全局变量
. bss:未初始化的静态变量,以及初始化为0的全局或静态变量。
hello 39 elf4
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
hello 40 汇编与反汇编
如图(hello 40 汇编与反汇编),反汇编见左,汇编见右。可见二者在大体结构上相似,在细节上有所不同。
我们可以观察到,.o文件省略了汇编文件中的部分描述(如push指令后的
.cfi_def_cfa_offset 16
.cfi_offset 6, -16)
),将call指令与跳转指令(如je)原本直接调用函数或者PLT一直修改为相对位置,通过相对寻址来确定位置。
4.5 本章小结
本章通过对hello.s进行汇编,生成可重定位文件,并就文件elf头、节头部表符号表进行了分析,最后分析了反汇编代码与汇编代码的区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够操作系统装入执行的统一整体。
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
(1)静态链接
在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
(2) 动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。[4]
5.2 在Ubuntu下链接的命令
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
生成了hello可执行目标文件。
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1 ELF头
hello 41
经对比可见与.o文件的EL头一致。
5.3.2 节头部表
hello 42
hello 43
可以发现,节头部表由原来的13个元素扩充到了26个元素,但重定位信息(.rela.text与rela.eh_frame)由于已经进行重定位而消失了。
5.3.3 程序头部表
程序头部表是一个数组,数组中的每一个元素就称为一个程序头,每一个程序头描述一个内存段或者一块用于准备执行程序的信息;内存中的一个目标文件中的段包含一个或多个节;也就是ELF文件在磁盘中的一个或多个节可能会被映射到内存中的同一个段中;程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。
hello 44
5.3.5 其他信息
动态节:
hello 45
符号表:
hello 46
等等。
5.3 hello的虚拟地址空间
根据5.2的程序头表中load处为从0x400000处开始查看,发现为elf头表。
hello 47
在偏移0x1000处显示为_init函数,gdb证实如hello 49:
hello 48
hello 49
在偏移0x2000处显示为.rodata
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
观察hello和hello.o的反汇编代码,可以发现二者之间的不同:(4.4的补充)
(1)在hello.o中有的符号引用或者是函数引用并没有提供确定的虚拟地址,而在hello的反汇编代码中由于进行了重定位,所以可以看到虚拟地址在这里都被填充上了。
(2)同时在链接之后hello当中多出了很多节,这些节都具有一定的功能和含义。
如.init节是用来初始化程序需要执行的代码;.plt是动态链接-过程链接表;.dynamic是用来存放被ld.so使用的动态链接的信息;.data是初始化的数据;.fini程度正常终止所需代码。上述说的这些节都是我们有必要了解的。
重定位的过程:
在重定位的时候,链接器将合并输入模块,并未每个符号分配运行时地址。重定位由两步组成:
重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,这个节成为输出的可执行文件的节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量就有唯一的运行时内存地址了。
重定位节的符号引用:在这一步中,链接器修改代码节和数据节对每个符号的引用,使得他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位条目这一数据结构。
当汇编器生成一个目标模块时,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目。
重定位条目结构如下:
hello 50
hello 51
重定位算法如下:
hello 52 重定位算法
可执行文件的反汇编中,puts@plt在0x401080处
在call调用函数将已重定位的地址写入机器编码中:
机器指令显示的值为0xff ff ff 56 = -0x66,指行此处 pc = 0x40112a
pc+(-0x56) = 0x401080 正好是puts@plt的地址。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
_init(0x7ffff7de6f30) -> __GI___ctype_init(0x7ffff7df7460) -> _start(0x4010d0) -> _init(0x401000)-> __libc_csu_init () -> _init -> __libc_start_main(0x7ffff7de6fc0)->mian
5.7 Hello的动态链接分析
共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamiclinking),是由一个叫做动态链接器(dynamiclinker)的程序来执行的。共享库也称为共享目标(shared object),在Linux系统中通常用.so后缀来表示。微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)。
共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统
中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.
so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌人到引用它们的可执行的文件中。其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。
在调用共享库的时候,编译器没有办法预测这个函数的运行地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
现代系统以这样一种方式编译共享模块的代码段,使得可以把它们加载到内存的任何位置而无需链接器修改。使用这种方法,无限多个进程可以共享一个共享模块的代码段的单一副本。(当然,每个进程仍然会有它自已的读/写数据块。)
可以加载而无需重定位的代码称为位置无关代码( Position-Independent Code, PIC)。用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。共享库的编译必须总是使用该选项。在一个x86-64 系统中,对同一个目标模块中符号的引用是不需要特殊处理使之成为PIC。可以用PC相对寻址来编译这些引用,构造目标文件时由静态链接器重定位。
GOT和PLT协同工作,在plt中的函数被第一次调用时,延迟解析它的运行时地址:
●第1步。不直接调用调用的函数假设为(getchar), 程序调用进入PLT[2], 这是getchar的PLT条目。
●第2步。第一条PLT指令通过GOT[2]进行间接跳转。因为每个GOT条目初始时
都指向它对应的PLT条目的第二条指令,这个间接跳转只是简单地把控制传送回PLT[2]中的下一条指令。
第二部分在系统上运行程序
●第3步。在把getchar的ID压入栈中之后,PLT[2]跳转到PLT[0]。
●第4步。PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT [2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定getchar的运行时位置(0x404028),用这个地址重写GOT[4],再把控制传递给getchar。
5.8 本章小结
在本章中,主要阐述了进程的定义和作用,同时阐述了命令行的作用,以及在linux下通过shell是如何运行程序的,同时详细介绍了fork函数和execve函数,以及程序如何实现并行。还有一些信号异常的处理。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
异常是允许操作系统内核提供进程(process)概念的基本构造块,进程是计算机科学中最深刻、最成功的概念之一 。
在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序-样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接 .条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
每次用户通过向shell 输入 一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
进程提供给应用程序的关键抽象:
●一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
●一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash的作用: shell-bash 是一个C语言程序,是用户使用Unix/Linux的桥梁,它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能来执行程序、建立文件、进行并行操作等等。同时它也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。bash 还提供了一个图形化界面,提升交互的速度。:
shell-bash的处理流程:
1.从终端或控制台获取用户输入的命令
2.对读入的命令进行分割并重构命令参数
3.如果是内部命令则调用内部函数来执行
4.否则执行外部程序
5.判断程序的执行状态是前台还是后台,若为前台进程则等待进程结束;否则直接将进程放入后台执行,继续等待用户的下一次输入。
6.3 Hello的fork进程创建过程
使用./hello 1190202307 詹儒彦运行程序。
Shell在接收到指令后,会对指令进行分析,确定该指令不是内置指令(build_in),便将其视为执行加载这个名字的程序。在该进程调用fork函数,创建一个子进程,该进程与shell属于同一进程组,gpid相同除了pid不同以外,其他均相同(读时共享)。
6.4 Hello的execve过程
在fork一个子进程完毕以后,对子进程进行execve,将hello程序加载到当前子进程的上下文中。Execve调用ld-linux-x86-64.so的加载器进行装载进程,给hello分配新的虚拟地址,并映射hello中的文件或匿名文件至虚拟内存中。跳转至_start函数中,通过调用libc_start_main函数初始化环境变量,b并将程序执行至main入口。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称逻辑流
进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(preempted)(暂时挂起),然后轮到其他进程。对于一个运行在这些进程之一的上下文中的程序,它看上去就像是在独占地使用处理器。
多个流并发地执行的一般现象被称为并发(concurrency)。一个进程和其他进程轮流运行的概念称为多任务(multitasking)。一个进程执行它的控制流的–部分的每一时间段叫做时间片(time slice)。因此,多任务也叫做时间分片(time slicing)。
处理器通常是用某个控制寄存器中的一个模式位(mode bit)来提供限制一个应用可以执行的指令和它可以访问的地址空间范围的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中(有时叫做超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令( privileged instruction), 比如停止处理器、改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
操作系统内核使用一.种称为上下文切换(contextswitch)的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在8.1节中已经讨论过的那些较低层异常机制之上的。
内核为每个进程维持一个上下文(context)。上下文就是内核重新启动- -个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式地请求让调用进程休眠。一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。
中断也可能引发上下文切换。比如,所有的系统都有某种产生周期性定时器中断的机制,通常为每1毫秒或每10毫秒。每次发生定时器中断时,内核就能判定当前进程已经运行了足够长的时间,并切换到一个新的进程。
下图展示了一对进程A和B之间上下文切换的示例。在这个例子中,进程A初始运行在用户模式中,直到它通过执行系统调用read陷入到内核。内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。
hello 53 进程上下文切换
当运行hello时若进行系统调用时就将进行上下文切换,内核将另一个进程的上下文复制到现在的进程的上下文中,进行调度,再恢复用户模式执行另一个进程;在sleep时,也可调度执行另一个进程。此时就将hello程序分成多个时间片来进行。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
Hello中可以出现4中异常(异常总共有4种):
1.中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序(interrupt handler)。
在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令(也即如果没有发生中断,在控制流中会在当前指令之后的那条指令)。结果是程序继续执行,就好像没有发生过中断一样。
hello 54 中断
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
用户程序经常需要向内核请求服务,比如读一个文件(read)、创建一个新的进程(fork)、加载一个新的程序(execve),或者终止当前进程(exit)。为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的“syscall n”指令,当用户程序想要请求服务n时,可以执行这条指令。执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。
hello 55 陷阱
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort 例程会终止引起故障的应用程序。
一个经典的故障示例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。一个页面就是虚拟内存的一个连续的块(典型的是4KB)。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。
hello 56 故障
4.终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。
处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
hello 57 终止
hello在加载时会出现缺页异常(故障),在对应虚拟地址处开始加载,当进行时可能会因为键盘输入而导致系统调用(陷阱),若i/o设备发送信号,也可能出现故障,当故障接触后就会继续执行下一条指令。而在程序进行的过程中,还可能会因为硬件错误而终止、
在hello执行的过程中,还会接收到一些信号:
6.7本章小结
本章介绍了可执行目标文件hello是如何被加载执行的,如何在上下文进行切换与调度的,了解了信号的机制。在shell中执行hello程序,并进行多种非正常操作,并对出现的不同结果进行分析。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1.逻辑地址:即机器指令中所带有的地址,是程序中某些数据或函数相对于其所在段的偏移地址。
2.线性地址: CPU 中还存在4个段寄存器,用来保存段基址。在寻址时,通过加和段基址与逻辑地址,即可得到线性地址。
3.虚拟地址:页式内存管理机制下,每个程序中的内容被分为与虚拟页大小相同的块,并映射至虚拟内存中,其地址为虚拟地址。
4.物理地址:物理内存同文件一样,亦被切分为多个页进行管理。虚拟地址通过查找页表,获取物理页号,再接上虚拟页偏移量,即可获得物理地址。物理地址可以直接用于向主存访问数据。
在hello中,汇编的地址都是逻辑地址,在寻址时,逻辑地址与段地址相加得到线性地址。在页式管理下,线性地址就是虚拟地址。再通过使用时查询cache和页表,将虚拟地址转换为物理地址,进行寻址访存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段基址+逻辑地址 = 线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换涉及到页表和MMU概念。页表是一个页表条目的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE,PTE中有一个有效位用来表示该区域是否被已缓存在内存中。
MMU即分页内存管理单元,MMU位于处理器内核和连接高速缓存以及物理存储器的总线之间,能够将有效地址映射成对应的物理地址,以访问指令和数据。
当hello需要访问一个内存中的地址时,处理器将虚拟地址发送给MMU,MMU利用虚拟地址对应的虚拟页号生成页表项(PTE)地址,并从页表中找到对应的PTE,如果PTE中的有效位为0,则MMU会触发缺页异常。缺页处理程序选择物理内存中的牺牲页(若页面被修改,则换出到磁盘),缺页处理程序调入新的页面到内存,并更新PTE,缺页处理程序返回到原来进程,再次执行导致缺页的指令。
hello 58
当产生页命中时:
●第1步:处理器生成一个虚拟地址,并把它传送给MMU。
●第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它,
●第3步:高速缓存/主存向MMU返回PTE。.
●第4步:MMU构造物理地址,并把它传送给高速缓存/主存。
●第5步:高速缓存/主存返回所请求的数据字给处理器。
当产生缺页时:
●第1步到第3步:和图9-13a中的第1步到第3步相同。
●第4步: PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
●第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
●第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE。
●第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了相关步骤之后,主存就会将所请求字返回给处理器。
hello 59
7.4 TLB与四级页表支持下的VA到PA的变换
下图描述使用k级页表层次结构的地址翻译。虚拟地址被划分成为k个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引,其中1≤i≤k。第j级页表中的每个PTE,1≤j≤k-1,都指向第j+1级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。对于只有一级的页表结构,PPO和VPO是相同的。
hello 60
总结了完整的Corei7地址翻译过程,从CPU产生虛拟地址的时刻一直到来自内存的数据字到达CPU。Core i7采用四级页表层次结构。每个进程有它自己私有的页表层次结构。当一个Linux进程在运行时,虽然Core i7体系结构允许页表换进换出,但是与已分配了的页相关联的页表都是驻留在内存中的。CR3控制寄存器指向第一级页表(L1)的起始位置。CR3的值是每个进程上下文的一部分,每次上下文切换时,CR3的值都会被恢复。
hello 61
Corei7MMU如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推。
hello 62
7.5 三级Cache支持下的物理内存访问
存储器存储结构如下:
hello 63
以cashe1为例,cashe2,cache3原理与cashe1相同
由于L1cache有64组,因此组索引s为6,每组有八个高速缓存行,由于每块大小为64B,所以块偏移为6,因此标记为为64-6-6=40位。
使用cache1访问物理内存的过程大致如下:
(1).对于CPU提供的地址w,从其中间抽取出6位组索引为。这些位被解释个一个对应于一个组号的无符号整数。也就是说,将高速缓存看作是一个关于组的一维数组,那么这些组索引位就是一个到这个数组的索引。
(2).在选定了某个组i之后,接下来需要确定是否有字w的一个副本存储在组i的一个高速缓存行中,只有标记位相匹配且有效位为1的时候才缓存命中,否则缓存不命中。
(3).如果缓存命中,根据提供的块偏移位(所需要的字的第一个字的偏移),我们就可以将其取出 并返还给CPU。
(4).如果缓存不命中,那么它需要从存储器层次结构中的下一层取出被请求的块,然后将新的块存储到组索引位指示的组中的一个高速缓存行中。如果组中都是有效高速缓存,那么必须驱逐出一个现有的行,可以采用最近最少使用策略LFU进行替换。
hello 64
7.6 hello进程fork时的内存映射
fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
hello 65
7.7 hello进程execve时的内存映射
在当前进程中的程序执行了execve(”a.out”,NULL, NULL)调用时,execve函数在当前程序中加载并运行包含在可执行文件a.out中的程序,用a.out代替了当前程序。加载并运行a.out主要分为一下几个步骤:
1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构;
2.映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零;
3.映射共享区域:hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内;
4.设置程序计数器(PC):设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:当指令给出的虚拟内存对应的物理地址并不在主存中时,会发生缺页故障,这时会触发缺页中断处理程序。
缺页处理程序会根据页表中提供的信息到相应的磁盘位置上取出页面并重新加载到内存中,然后更新PTE,然后将控制返回给缺页故障的指令。当指令再次执行时,相应的页面已经驻存在内存中,因此指令可以没有故障的运行。
·7.9动态存储分配管理
动态存储分配涉及到对运行时堆的使用,动态内存分配器维护着一个进程中的称为堆的虚拟内存区域,brk指向堆的顶部,分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
Hello调用malloc函数,会先访问堆中是否有合适大小的空闲块,如果有相应的空闲块则分配这个块给Hello,返回一个指针指向这个分配块的首地址。如果没有合适的空闲块,系统会拓展堆空间,改变brk的值来保证有合适的空闲块。
hello 66
7.10本章小结
本章简要分析来程序运行时包括页面管理,高速缓存管理,虚拟地址与实地址的转化全流程以及动态内存分配等内容
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个linux文件就是一个m个字节的序列:
B0,B1,····Bk···Bm-1
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2).
(3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置k开始,然后将k增加到 k+n,给定一个大小为m字节的文件,当 k>=m 时,触发EOF。类似一个写操作就是从内存中复制 n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
UNIX I/O函数:
(1)进程通过调用open函数来打开一个存在的文件或创建一个新文件:
int open(char* filename,int flags,mode_t mode)
open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
(2)进程通过调用close函数关闭一个文件的打开:
int close(int fd)
(3)应用程序通过read来执行输入操作:
ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
(4)应用程序通过write来执行输出:
ssize_t write(int fd,void *buf,size_t n)
write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
printf函数可以发送格式化输出到标准输出 stdout中,即显示用户想要显示的字符串到屏幕上。
printf()通过调用vsprintf函数来解析字符串中的%参数,然后得到整个字符串的长度之后使用Unix I/O函数write()输出到标准输出设备上。
其中,arg指向变量参数,例如printf(“hello %d , %s”, i , name); arg 指向第一个参数i。fmt指向"hello %d, %s"这个字符串。
vsprintf会扫描整个字符串并进行复制直到遇到%(第一个for循环),然后使用swtich对字符串进行修改。
随后调用write函数,其中会调用一个syscall函数,它会执行一个系统调用,根据指定的参数number和所有系统调用的汇编语言接口来确定调用哪个系统调用,例如触发一个陷阱-系统调用。驱动程序去/proc/devieces中申请得到设备名和主设备号,并使用mknod命令建立设备节点文件。通过主设备号将设备节点文件和设备驱动程序联系在一起,然后利用fops函数,绑定应用层的write和驱动层的write。当应用层写一个设备文件的时候,系统找到对应的设备驱动子程序(驱动程序和设备号一一对应),对于printf而言,字符显示驱动子程序根据ASCII根据字模库更新vram。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符。
用户通过输入设备输入字符串到内存缓冲区,操作系统执行一个异步异常-键盘中断,这个键盘信号处理子程序将接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
通过对linux下Unix I/O几个主要函数的阐述分析,讲解了文件的概念及其Unix I/O的原理及其应用。
结论
Helloword的一生很短,短到不到一秒就可以运行完。Helloworld的一生很长,长到是一代又一代程序员、工程师接续奋斗的成果,从hello.c,无意识经过预处理、编译、汇编、链接,才能形成一个hello可执行文件
在Bash中,操作系统为可执行文件fork、execve、mmap,分时间片,使其得以运行。
(存储管理)与MMU为VA到PA不断进化: TLB、4级页表、3级Cache,Pagefile,IO管理与信号处理软硬结合,才使各种可执行文件能在键盘、主板、显卡、屏幕间赛跑,最终被回收。
一切都是那么漂亮地进行,P2P,O2O,却仍然进行不断的改进,每隔几年就出现一大飞跃。也许这就是系统之美,计算机之美吧。
附件
hello.i 预处理之后的文本文件。
hello.s 汇编后的文本文件。
hello.o 可重定位文件hello.o。
hello 从hello.o链接而成的可执行目标文件。
hello.elf 由可重定位目标文件hello.o生成的elf文件。
Hello1.elf 由可执行目标文件hello生成的elf文件。
hello.d 由可重定位目标文件hello.o反汇编得到的文件。
hello1.d 由可执行目标文件hello反汇编得到的文件。
hello 67
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 百度百科.预处理命令.
https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86%E5%91%BD%E4%BB%A4/10204389?fr=aladdin
[2] C语言中文网. C语言预处理程序(preprocessor)有什么作用?. http://c.biancheng.net/cpp/html/1553.html
[3] 冰封的海盗. https://www.bilibili.com/read/cv2137860/ , bilibili
[4] 胡薇.了解“预编译、编译、汇编、链接”这四个过程对你有很大帮助.2018-04-18.
http://m.elecfans.com/article/663750.html
[5] csapp。
[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分)