hello 的一生

计算机系统

大作业

题     目  程序人生-Hello’s P2P   

专       业     计算学部              

学     号     120L022218            

班     级      2003012               

学       生       裴家豪            

指 导 教 师       郑贵滨                

计算机科学与技术学院

2021年5月

摘  要

本文主要通过观察hello.c程序在Linux系统下的生命周期,探讨hello.c源程序的预处理、编译、汇编、链接、生成可执行文件并运行的主要过程。同时结合课本中所学知识详细说明系统是如何实现对hello程序的进程管理,存储管理和I/O管理。通过对hello.c程序的生命周期的探索,让我们对可执行文件的生成和执行以及其它相关的计算机系统的知识有更深的理解。
关键词:预处理,编译,汇编,链接,加载,进程管理,存储管理,I/O管理                  

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

目录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

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

1.3 中间结果... - 4 -

1.4 本章小结... - 4 -

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

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

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

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

2.4 本章小结... - 7 -

第3章 编译... - 8 -

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

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

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

3.4 本章小结... - 11 -

第4章 汇编... - 12 -

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

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

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

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

4.5 本章小结... - 17 -

第5章 链接... - 18 -

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

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

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

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

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

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

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

5.8 本章小结... - 28 -

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

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

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

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

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

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

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

6.7本章小结... - 33 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 38 -

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

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

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

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

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

8.5本章小结... - 41 -

结论... - 41 -

附件... - 43 -

参考文献... - 44 -

第1章 概述

1.1 Hello简介

(020)Hello程序最初以hello.c的文件存放在磁盘中,之后通过我们编译器驱动器(gcc)的编译下,首先进行预处理,生成hello.i文件,主要是展开头文件并将定义的宏赋值。然后经过编译步骤,生成汇编代码,也就是hello.s文件。之后通过汇编器(AS)对文件进行进一步转化,生成hello.o文件(可重定位目标文件)之后再通过链接器(LD)链接,生成可执行文件hello.out。

      (p2p)文件的运行,首先在终端(Terminal)下,输入命令gcc hello.c,完成对上述文件的生成(中间文件不保留),得到hello.out文件,而后输入命令./hello.out,这时shell会创建一个子进程(fork()),并且在防止父子进程竞争导致进程管理错误的情况下,将程序hello.out加载之内存中该子进程的上下文中(execve())。通过流水线化的CPU来处理该程序。在程序执行结束之后,盖子进程会变为一个僵死进程,发送一个信号(SIGCHLD)给shell,shell会来回收该子进程(将其从内存中删除),至此hello.out程序运行完毕。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境 :Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上

开发工具:Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc

1.3 中间结果

Hello.i:预处理之后得到的文件,将头文件展开,并且将所有的宏进行赋值。

Hello.s:将.i文件编译后得到的文件,由更加基础的汇编代码组成。

Hello.o:可重定位目标文件,是二进制文件,可以与静态库文件进行链接

Hello.out:可执行目标文件,可以被加载到内存中并执行的文件。

1.4 本章小结

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。

预处理是为编译做的准备工作,能够对源程序.c文件中出现的以字符“#”开头的命令进行处理,包括宏定义#define、文件包含#include、条件编译#ifde等,最后将修改之后的文本进行保存,生成. i .i.i文件,预处理结束。

2.2在Ubuntu下预处理的命令

gcc -e -o Hello.c Hello.i

2.3 Hello的预处理结果解析

得到了hello.i文件:

结果解析:显然,上述所有的12个预处理指令都以符号#开始,每条预处理指令必须独占一行。我们都可以从hello.i中寻找到。

#include<stdio.h>、#include<unistd.h>、#include<stdlib.h>等头文件包含的文件被插入到该预编译指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。

另外,通过对比,我们可以看到.i文件中删除了.c中的注释,并且插入了.c文件的头文件。

2.4 本章小结

本章详细介绍了hello预处理的阶段。由于hello.i的介绍在网上介绍的资料太少,只能结合自己的分析,尽量的写出这些:

.c文件的头文件也会又外部文件,还有一些宏定义和注释、一些条件编译和完善程序文本文件操作都需要预处理来实现。

预处理可以使程序在后边的操作中不受阻碍,是整个过程中非常重要的一步。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:编译器ccl将文本文件hello.i翻译成文本文件 hello.s,它包含一个汇编语言程序。它以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。就是将高级语言转化为指令集的过程,更有助于后续文件的生成 。

作用:将预处理后的程序编译为更接近计算机语言,更容易让计算机理解的语言,相当于一个翻译的过程。除此之外,编译程序还具备语法检查、目标程序优化等功能。

3.2 在Ubuntu下编译的命令

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

3.3 Hello的编译结果解析

我们可以看到:

这表明,我们的程序在运行过程中使用到两个常量,并且都是string类型的常量。

而由于int i,因此我们可以看到汇编指令里将i赋初值为0,并将其存储在栈中-4(%rbp)的位置。

通过下图我们知道,mian()函数的两个参数argc与*argv[]是分别存在寄存器edi与rsi中。

而对应于输出与exit(1)的汇编代码为:

接下来是控制转移语句,判断argc的值是否为4,若为4,跳转至L2:

下图是循环体的内容

下图是循环的判断语句:

aoti()函数是将字符串类型的值改为int类型的值,并且传入参数argv[3],表示休眠多少秒:

3.4 本章小结

本章主要讲述了编译的概念和作用。并且对hello.c程序的编译过程进行了截图展示。详细说明了hello.c程序的编译结果,展示了相应语句的汇编代码。通过对编译过程和结果的展示,加深了对编译结果的理解和运用。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。

用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。

作用:汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。

4.2 在Ubuntu下汇编的命令

汇编命令是:gcc hello.s -c -o hello.o

4.3 可重定位目标elf格式

   用命令readelf -h hello.o得到如下结果:

以上就是elf文件的表头。我们可以观察到一些有关程序的信息,例如:数据利用小端法存储,文件类型为REL文件。由于hello.o文件还未重定位,因此程序入口地址为0X0,ELF头的大小为64bytes。

在输入命令readelf -S hello.o后,我们得到了上图,即节头表。我们可以根据节头表,获得每一节在文件中的起始位置。节头表还标记了该节是否可以读、写或者执行等信息。同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

在输入readelf -s hello.o之后得到上图,符号表(.symtab)节。其中,(Ndx)表示它是第几个节中的符号,value代表在节中的偏移量,bind表示在程序中的符号类型(是否是全局符号),而size表示符号的大小,name代表符号的名称。

输入readelf -r hello.o后,可以查看重定位节(rel.text)。由上图可以看出,.rela.text节包含很多项,每一项中包含了很多个字段。
各个字段的含义如下:

Offset:需要被修改的引用节的偏移。

Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节。symbol:标识被修改引用应该指向的符号;type:重定位的类型。

Type:告知链接器应该如何修改新的应用。

Addend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整。 Name:重定向到的目标的名称。

4.4 Hello.o的结果解析

利用objdump -d -r hello.o命令将文件反汇编,结果如下:

通过与hello.s文件的比较观察,我们发现,反汇编的结果与.s文件并无太大差别。反汇编代码的左侧是面向计算机的机器语言,机器语言代码中包含操作码,数据,寄存器编号等内容,其中机器语言的每一个操作码,寄存器编号等都与汇编语言一一对应。机器语言中的数据采用小端存储的二进制形式表示,而在汇编语言中采用的是顺序十六进制形式表示。通过这些映射关系就可以实现机器语言与汇编语言的一一对应。

在条件分支语句中,反汇编没有乐死.L1等符号,这些只是条件注记符,在机器码中自然就不存在了。

另外,在汇编代码.s文件中,调用函数语句后面紧跟的是函数名,那么在反汇编程序中,call后面跟的是所调用函数的地址与下一条指令的地址的差值,但是由于我们使用的是动态链接库,因此只有在链接的时候才会将这些0改为实际的值。

4.5 本章小结

       本章介绍了汇编的概念和作用,并通过对hello.s的汇编结果的分析简要讲述了可重定位文件的格式,重点讲述了可重定位文件的ELF头,节头表,符号表和.rela.text节的内容。最后比较了反汇编结果与汇编文件hello.s的区别

(第41分)

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

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

输入命令readelf -h hello,得到以下结果:

从上图我们可以看出,Type说明我们的elf文件是EXECx型,也就是可执行文件,同样采用小端序,并且由于链接过程已经完成,因此这时程序入口已经确定:0X4010f0,并且比hello.o程序多了一个段表头,即.init节。另外可执行文件的节头表的数目比.o文件要多,为27个。

接下来是节部头表,输入readelf -S hello命令后有:

我们发现,每一个节中有了实际地址,而不是像.o文件那样全为0.

接下来是符号表,在输入readelf -s hello后 得到以下结果:

由上图可以看出,hello程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。依旧没有变,只不过value等值发生了改变。

接下来是段头部表,通过指令readelf -l hello查看:

段头表描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从段头表中可以看到根据可执行目标文件的内容初始化为两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。

5.4 hello的虚拟地址空间

       通过观察我们发现,程序从0X00400000处开始:

根据上一小节中的截图,我们知道在ELF头部表中,程序入口地址为0X004010f0观察如下:

我们还可以看到任意一个节中地址所包含的信息,如.data(0X404048)节中:

5.5 链接的重定位过程分析

利用命令objdump -d -r hello > hello1.txt得到了反汇编程序,并在文件hello1.txt中

我们比较了hello.o文件的反汇编,与hello.out的反汇编,发现了一些不同:

  1. hello.o的起始是0X00000000,而hello.out的起始地址为0X00400000。
  2. hello中加入了许多其它函数的汇编代码,比如_init等:

由节头部表知,.init节是初始化代码时执行的函数,而.plt则是动态链接的过程链接表。

关于跳转指令,我们发现,在hello.o中跳转指令的地址均为0X00000000,而在hello中,却有了真实的地址:

Hello的重定位过程:

在汇编器生成一个模块时,汇编器并不能确定该模块在可执行文件中最终的位置,因为链接可能会打乱这些地址,因此,它会先将未知的地址全赋值为0,并生成一个重定位条目:

其中,offset是需要被修改的引用的节偏移。symbol标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
ELF中定义了32中不同的重定位类型。其中比较常用的是R_X86_64_PC32和R_X86_64_32。前者重定位一个使用32位PC相对地址的引用,后者重定位一个使用32位绝对地址的引用。

通过查找资料,我们得到PC相对寻址代码如下:

我们在反汇编代码中,看到了一个利用相对地址重定位类型:

它的类型是只读数据,重定位类型为R_X86_64_PC32,因此我们在rela.text节中找到它的重定位条目:

代入我们的公式进行计算:

*refptr=(unsigned)(ADDR(.rodata)+Addend-ADDR(main)-offset)

       =(unsigned)(0x402008-4-0x401125-1c)

       =0xec3

5.6 hello的执行流程

ld-2.31.so!_dl_start

0x7f8e7cc34ed0

ld-2.31.so!_dl_init

0x7f8e7cc486a0

hello!_start

0x4010f0

libc-2.31.so!_libc_start_main

0x7ff 825425fc0

libc-2.31.so!_cxa_atexit

0x7ff 825448f60

hello!_libc_csu_int

0x4011c0d0

libc-2.31.so!_setjmp

0x7ff 82fdb2e00

libc-2.27.so!exit

0x7ff 82fdc3bd0

5.7 Hello的动态链接分析

   上述延迟绑定是通过GOT和PLT实现。GOT是数据段的一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目;而PLT是代码段的一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

用hello进行分析,具体如下:

首先找到GOT的起始地址为0x403ff0,然后在EDB中查看其值,发现在dl_init之前0x601008后的16个字节都是0。这是调用前的图:

这是调用后的图:

从图中可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。
和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

5.8 本章小结

本章主要介绍了链接的概念和作用,详细介绍了hello.o是如何链接生成一个可执行文件的。同时展示了可执行文件中不同节的内容。最后分析了程序是如何实现的动态链接的。

(第51分)

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进程创建过程

当在shell上输入./hello命令时,命令行会首先判断该命令是否为内置命令,如果是内置命令则立即对其进行解释。否则将其看成一个可执行目标文件,再调用fork创建一个新进程并在其中执行。

当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。加载并运行需要以下几个步骤:

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

6.5 Hello的进程执行

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。

hello的进程执行过程如下:
hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程以加载新的进程进行执行。同时将hello进程从运行队列中移入到等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程并执行,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

6.6 hello的异常与信号处理

我们知道,异常分为如下几类:

在hello程序执行过程中这几类异常都可能出现。当出现异常时,操作系统会根据异常表进行一个间接过程调用,找到异常对应的异常处理程序。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下3种情况中的一种:
1.处理程序将控制返回给当前指令,即当事件发生时正在执行的指令。
2.处理程序将控制返回给当前指令的下一条指令,即如果没有异常发生将会执行的下一条指令。
3.处理程序终止被中断的程序。

首先我们输入一些./hello,在输入回车之前输入的所有字符,都会被当作argv[]来存储,最终在输入回车后输出:

在运行中不停乱按的结果:

如果按下ctrl-z就会发送一个SIGSTP信号,使得进程停止:

按下jobs进行观察,发现有一个暂停的进程:

如果在运行中按下ctrl-c进程就会终止,用jobs命令观察进程状态,发现只有一个被暂停了的工作(进程),而被终止的进程没有在jobs里面:

Ps命令,fg命令结果如下图:

6.7本章小结

本章介绍了进程的概念和作用,同时介绍了Shell的一般处理过程和作用。分析了fork和execve函数的功能,展示了hello进程的执行以及hello的异常和信号处理。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
虚拟地址:也就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号就是段描述符的索引。段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。
一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了。

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

计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

具体过程如下图:

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度。

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

如果采用多级页表策略,例如四级页表层次结构,每个四级页表进程都有他自己私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的PTE全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。四级页表的层次结构操作流程如下:36位虚拟地址被寄存器划分组成四个9位的片,每个片被寄存器用作一个页表中偏移量。CR3寄存器内储存了一个L1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。VPN1提供了到一个L1PTE的偏移量,这个PTE寄存器包含一个L2页表的起始基地址。VPN2则提供了到一个L2PTE的偏移量,一共四级,逐级以此层次类推。

7.6 hello进程fork时的内存映射

当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

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

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

缺页故障是指用户想要写内存地址(虚拟地址)时该地址对应的物理页不在内存,而是在磁盘中的一种可恢复的故障。这时,系统调用缺页处理程序,选择牺牲页面。具体步骤为:

1)CPU为MMU生成并传送一个虚拟地址。

2)地址管理单元生成PTE,高速缓存或主存向MMU传送PTE。

3)PTE的有效位0触发一次MMU异常,传递CPU的控制信号到操作系统内核中执行缺页异常处理程序。

4)处理程序决定物理内存中的牺牲页,如果这个页是非空的就把它换到磁盘。

5)处理程序调入新页面并更新主存中的PTE。

6)处理程序返回,程序继续执行触发缺页异常的那条指令。

7.9动态存储分配管理

动态内存分配器维护者一个进程的虚拟内存区域,称为堆。(如图7.9.1所示),分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。

7.10本章小结

本章主要介绍了有关内存管理的知识。详细阐述了hello程序是如何存储,如何经过地址翻译得到最终的物理地址。介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix io 接口:

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

2.Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

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

4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。

5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix io 函数:

1.int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2.int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3.ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置 buf。返回值-1 表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4.ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n个字节到描述符为 fd的当前文件位置。

8.3 printf的实现分析

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程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数将buf中的i个元素写到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar的函数体如下:

int getchar(void)
{        static char buf[BUFSIZ];//缓冲区
    static char* bb=buf;//指向缓冲区的第一个位置的指针
     static int n=0;//静态变量记录个数        if(n==0)        {              n=read(0,buf,BUFSIZ);                bb=buf;//并且指向它        }        return(--n>=0)?(unsigned char)*bb++:EOF;}

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf和getchar函数的实现。

(第81分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。
1.首先编写hello.c的源程序,从此hello.c诞生。此时hello.c仍是一个文本文件,还没有变成二进制文件。
2.对hello.c进行预处理(gcc -E),hello.c变成了hello.i。
3.对hello.i进行编译处理(gcc -S),hello.i变成了hello.s。
4.对hello.s进行汇编处理(gcc -c),hello.s变成了hello.o。此时hello变成了二进制文件。
5.对hello.o进行链接处理,将其与其它可重定位目标文件以及动态链接库进行链接生成可执行目标文件hello。此时hello程序就可以在计算机上执行。
6.在shell命令行上输入./hello 1190201809 夏韵 1来运行hello程序。
7.shell首先判断输入命令是否为内置命令。经过检查后发现其不是内置命令,则shell将其当作程序执行。
8.shell调用fork函数创建一个子进程。
9.shell调用execve函数,execve函数会将新创建的子进程的区域结构删除,然后将其映射到hello程序的虚拟内存,然后设置当前进程上下文中的程序计数器,使其指向hello程序的入口点。
10.运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache协同工作,完成对地址的翻译和请求。
11.execve函数并未将hello程序实际加载到内存中。当CPU开始执行hello程序对其进行取指时,发现其对应的页面不在内存中。此时会出现缺页故障这一异常,操作系统通过异常表对缺页处理程序进行间接调用,缺页处理程序实现虚拟内存和物理内存的映射,将缺失的页面加载到内存中。此时CPU重新执行引起故障的指令,指令可以正常执行,程序从而继续向下执行。
12.当hello程序调用sleep函数后进程休眠进入停止状态。而CPU不会等待hello程序休眠结束,而是通过内核进行上下文切换将当前进程的控制权转移到其它进程。当sleep函数调用完成后,内核再次进行上下文切换重新执行hello进程。
13.当hello程序执行printf函数时,会调用malloc函数从堆中申请内存。
14.在hello进程执行时,当在命令行中输入Ctrl-C时,shell会向前台作业发送SIGINT信号,该信号会终止前台作业,即hello程序终止执行。当输入Ctrl-Z时,shell会向前台作业发送SIGTSTP信号,该信号会挂起当前进程,即hello程序停止执行,之后再向其发送SIGCONT信号时,hello程序会继续执行。
15.当hello进程执行完成后,父进程会对子进程进行回收。内核删除为这个进程创建的所有数据结构。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。抽象是计算机系统设计与实现的重要基础:文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象,进程是对一个正在运行的程序的抽象,虚拟机是对整个计算机的抽象。
计算机的工作效率是评价其好坏的一个重要的方面。为了提高计算机的工作效率,解决不同设备之间速度差异大的问题,利用缓存的机制设计了高速缓存来作为更底层的设备的缓存,如cache就是设计为主存的缓存,快表设计为虚拟内存页表的缓存等等。

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

附件

hello.c

源程序

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标文件

hello1.txt

hello.o得到的反汇编文件

hello2.txt

hello得到的反汇编文件

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值