Hello的一生 HIT CSAPP大作业

 

 

 

计算机系统

 

大作业

 

 

题 目 程序人生-Hello's P2P    

专 业 计算机类

学   号 1190201704

班   级 1936601

学 生 梅艳婷    

指 导 教 师 刘宏伟   

 

 

 

 

 

 

计算机科学与技术学院

2021年6月

摘 要

摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。

 

本文浅谈hello.c在Linux系统的整个生命周期。从hello.c源程序开始,经过预处理、编译、汇编、链接成为可执行文件,到加载、运行、终止、回收的整个过程。结合深入学习计算机系统一书,阐述各个阶段的内容与实现机制。通过对hello的一生的学习,更深刻的理解计算机系统。

关键词:深入理解计算机系统;计算机系统;hello程序;

 

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

 

 

 

 

 

 

 

 

目 录

 

1 概述    - 4 -

1.1 Hello简介    - 4 -

1.2 环境与工具    - 4 -

1.3 中间结果    - 4 -

1.4 本章小结    - 5 -

2 预处理    - 6 -

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

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

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

2.4 本章小结    - 7 -

3 编译    - 8 -

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

3.2 Ubuntu下编译的命令    - 8 -

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

3.4 本章小结    - 13 -

4 汇编    - 15 -

4.1 汇编的概念与作用    - 15 -

4.2 Ubuntu下汇编的命令    - 15 -

4.3 可重定位目标elf格式    - 15 -

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

4.5 本章小结    - 19 -

5 链接    - 21 -

5.1 链接的概念与作用    - 21 -

5.2 Ubuntu下链接的命令    - 21 -

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

5.4 hello的虚拟地址空间    - 25 -

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

5.6 hello的执行流程    - 28 -

5.7 Hello的动态链接分析    - 28 -

5.8 本章小结    - 28 -

6 hello进程管理    - 30 -

6.1 进程的概念与作用    - 30 -

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

6.3 Hellofork进程创建过程    - 30 -

6.4 Helloexecve过程    - 30 -

6.5 Hello的进程执行    - 31 -

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

6.7本章小结    - 36 -

7 hello的存储管理    - 37 -

7.1 hello的存储器地址空间    - 37 -

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

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

7.4 TLB与四级页表支持下的VAPA的变换    - 38 -

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

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

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

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

7.9动态存储分配管理    - 40 -

7.10本章小结    - 41 -

8 helloIO管理    - 42 -

8.1 LinuxIO设备管理方法    - 42 -

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

8.3 printf的实现分析    - 42 -

8.4 getchar的实现分析    - 44 -

8.5本章小结    - 44 -

结论    - 44 -

附件    - 46 -

参考文献    - 47 -

 

 

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述HelloP2P020的整个过程。

P2PFrom Program to Process,从hello.c这一个程序开始,经过预处理、编译、汇编、链接生成可执行目标,在shell中,输入运行指令后,shell解析参数,为其fork一个子进程,内核为其创建数据结构,此时hello就从程序变为进程(Process)

020From Zero-0 to Zero-0shell为其调用execve,映射虚拟内存,进入程序入口后载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片,执行逻辑控制流,将内容输出。当程序运行结束后,父进程回收hello进程,内核删除相关数据结构,回收其内存空间。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

1.2.1 硬件环境

Intel Core i5-7200U CPU@2.50GHz 2.70GHz 8GBRAM

1.2.2 软件环境

Windows10 64位;Vmware 16;Ubuntu 16.04 LTS 64位;

1.2.3 开发工具

Visual Studio 2019 64位;CodeBlocksgcc

1.3 中间结果

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

hello.ihello.c预处理生成的文本文件,分析预处理器行为

hello.s:由hello.i经编译器编译后产生的汇编文件,分析编译器行为

hello.o:由hello.s经汇编器处理后产生的可重定位目标文件,分析汇编器行为

hello:链接之后生成的可执行目标文件 ,分析链接器行为

hello.objdump: hello.o的反汇编,主要是为了分析hello.o

 

1.4 本章小结

本章简单介绍了hello的p2p,020过程,列出了本次实验的环境与工具,在实验过程中得到的中间结果。

 

(第1章0.5分)

 

 

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器在程序编译之前,根据以字符#开头的命令(头文件、define等),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。

作用:

1.#include形式将源文件中声明的文件复制到新的程序中。

2.#define将数值或字符定义为字符串

3.方便编译器在对程序进行翻译的时候更加方便。

 

2.2在Ubuntu下预处理的命令

利用命令行生成hello.i

1生成hello.i指令

2ello.i出现在路径下

可以看出在该路径下生成了hello.i

2.3 Hello的预处理结果解析

打开hello.i,发现这个文件相比于hello.c大了很多,一共有三千多行

3hello.i部分内容

最后才是hello.cmain函数部分,前面都是头文件的一些源码,方便编译器将其翻译成汇编语言的操作。

2.4 本章小结

本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,在Ubuntu下预处理的命令以及处理的结果,并且解析了一下预处理的结果。预处理可以添加需要的条件编译和完善程序文本文件等。预处理可以使得程序在后序的操作中更加方便,不受阻碍,非常重要。

 

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译:将预处理好的高级语言程序文本翻译成能执行相同操作的汇编语言的过程。在本文中就是hello.i翻译成hello.s

作用:直接读懂高级语言对于计算机来说太困难,需要一个编译器将高级语言直接转换成更接近机器语言的汇编语言,使得将高级语言转换成计算机可执行的二进制文件这个操作更加方便。

 

3.2 在Ubuntu下编译的命令

利用命令行生成hello.s

4:生成hello.s命令

5hello.s出现在该路径下

可以看出在该路径下生成了hello.s

 

 

3.3 Hello的编译结果解析

打开helli.s,一共只有六十多行,比hello.i少了许多。

6hello.s部分内容

  • 3.3.1开头

.file 声明源文件

.text 代表代码段

.section .rodata 以下是rodata

.global 声明一个全局变量

.type 用来指定是函数类型或对象类型

.size 声明大小

.long声明一个long类型

.string 声明一个string类型

.align对齐方式

7hello.s的头

  • 3.3.2数据

1.常量

8:一些具有常量的汇编代码

以立即数的形式体现

2.变量

全局变量、局部变量、指针变量

全局变量:初始化过的sleepsecs,sleepsecs被声明为globl类型,类型是object(对象),占据4个字节被保存在.rodata段;

9sleepsecs

局部变量:编译器将局部变量存储在寄存器或者栈空间中

hello.c中一个局部变量i,将i存储在栈上空间(-4(%rbp)

10:局部变量i

11:局部变量i存储在栈空间中

main函数参数argc%edi保存,并储存在-20(%rbp)

12main函数参数argc

指针变量:argv作为第二个参数%rsi被保存在-32(%rbp)中。

13:指针变量argv

  • 3.3.3赋值

1.int sleepsecs=2.5 :在.data节中将sleepsecs声明为值为2long类型数据,如图9

2.i=0:整型数据的赋值使用mov指令,因为i4Bint类型,所以使用movl进行赋值,如图11.L2

  • 3.3.4类型转换

int sleepsecs=2.5; sleepsecs原本为long类型,在赋值时赋值为2.5,是一个double型,为隐式类型转换,实际上被赋值为long类型的2,符合转换时的舍入原则,遵从向零舍入的原则,将2.5舍入为2,如图9

  • 3.3.5算术操作

1.i++ addl $1, -4(%rbp)即为i++的操作,对计数器i加一,使用addl,表示是一个4字节的操作数,如图13

2.leaq .LC1(%rip), %rdi:使用了加载有效地址指令leaq计算LC1的段地址,段地址为%rip+.LC1并传递给%rdi,如图13

  • 3.3.6关系操作

1. argc !=3,一个关系的判断,如图12

2. i < 10,如图11

  • 3.3.7控制转移

编译器将iffor等控制转移语句都使用了cmp来比较然后使用了条件跳转指令来跳转。

if(): 如图12

for()如图8 .L2:内为i = 0.L4内为for执行代码,.L3内为i < 10循环条件

  • 3.3.8函数操作

返回值:一个函数的返回值由其类型决定,不同类型返回值不同,都存放在%rax

1.有返回值

main函数:

函数返回:将%eax设置为0并且返回,对应于return 0

2.无返回值

printf函数、exit函数、sleep函数、getchar函数

函数调用及参数传递:调用一个函数时,可以传入参数也可以不传入参数,如果传入的话,需要放置在几个寄存器中,传入需要的参数,执行call跳转语句,调转到调用函数的开头

1.main函数:

参数传递: argcargv,分别保存在%rdi%rsi

函数调用:系统调用

2.printf函数:

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

函数调用:for循环中调用

3.exit函数:

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

函数调用:满足if判断条件后调用

4.sleep函数:

参数传递:传入参数sleepsecs

函数调用:for循环中调用

5.getchar函数

函数调用:main中被调用

 

 

3.4 本章小结

本章显示简述了编译的概念和作用,分析了一个c程序是如何被编译器编译成一个汇编程序的过程,对汇编代码hello.s从常量、变量、赋值、类型转换、算术操作,关系操作、控制转移、函数操作几个方面进行了分享,对汇编代码有了进一步的了解。

汇编语言的一个特点就是它所操作的对象不是具体的数据,而是寄存器或者存储器,汇编语言的执行速度要比其它语言快,但也使编程更加复杂,汇编代码已经与高级语言编写的代码有了很大的不同,汇编语言程序与机器有着密切的关系。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器将hello.s 翻译成与之等价的机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。hello.o文件是一个二进制文件,它包含的是程序的指令编码。

作用:汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表进行翻译。

注意:这儿的汇编是指从 .s .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

利用命令行生成hello.o

14:生成hello.o命令

15hello.o出现在该路径下

可以看出在该路径下生成了hello.o

 

4.3 可重定位目标elf格式

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

图16:hello.o 的elf头

利用readelf列出各节的基本信息,Hello.o的elf格式由elf头、节头、重定位信息、符号表等组成。

ELF 头:用于总的描述ELF文件各个信息的段。hello.o是可重定位文件,采用补码、小端序。

图17:节头信息

节头:描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息,知道了hello的各个节的名称,类型,地址,偏移量信息

图18:重定位节

重定位节:包含了.text(具体指令)节中需要进行重定位的信息。这些信息描述的位置,在由.o文件生成可执行文件的时候需要被修改(重定位)。在这个hello.o里面需要被重定位的有,rodata , puts , exit , printf,sleepsecs, sleep , getchar

图19:符号表

符号表:所用的各符号的信息

4.4 Hello.o的结果解析

以下格式自行编排,编辑时删除

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

输入命令 Linux>objdump -d -r hello.o > hello.oasm得到hello.s的反汇编文本

20hello.o的反汇编代码

21hello.s的部分代码

由图20、图21可以发现,跟hello.s相比,hello.o反汇编之后虽然右边的汇编代码没有太大的差别,但是左边多了运行时候的机器指令的位置,每一行汇编语句所对应的机器指令,机器语言为十六进制。在hello.o中,立即数以十六进制出现,在hello.s中,立即数以10进制出现,

分支转移:hello.s中跳转到的目标位置都是用.L1.L2来表示的,在hello.o反汇编之后,这些目标位置以指令的地址为跳转

函数调用:在hello.o中,函数调用的目标地址是当前指令的下一条指令的地址,在hello.s中,函数调用直接使用函数名称

全局变量:在hello.s中,调用一个函数只需被表示成call+函数名,在hello.o中,使用0x0(%rip)进行访问,call一个具体的地址位置。

4.5 本章小结

本章简述了hello.s汇编指令被转换成hello.o机器指令的过程,通过readelf查看hello.oELF,通过反汇编的方式查看了hello.o反汇编的内容,比较和hello.s之间的不同。了解到从汇编语言映射到机器语言汇编器需要实现的转换

(第4章1分)

第5章 链接

5.1 链接的概念与作用

概念:在链接过程中,把相关的代码和数据连接到一个可执行文件中,这个文件可加载。链接行为可以在编译/汇编/加载/运行时执行。

作用:有些目标代码不能直接执行,要想将目标代码变成可执行程序,需要进行链接操作,才可以生成真正可以执行的可执行程序。

注意:这儿的链接是指从 hello.o hello生成过程。

5.2 在Ubuntu下链接的命令

22:使用ld链接命令

23:生成hello

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

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

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

图24:hello的ELF头

图25:hello节头

图26:hello程序头

图27:dynamic section

 

 

图28:hello重定位节

图29:hello符号表

 

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

图30:hello的虚拟地址空间

可以看出程序是在0x00400000地址开始的

和5.3中程序头对应

PHDR:程序头表

INTERP:程序执行前需要调用的解释器

LOAD:程序目标代码和常量信息

DYNAMIC:动态链接器所使用的信息

NOTE::辅助信息

GNU_EH_FRAME:保存异常信息

GNU_STACK:使用系统栈所需要的权限信息

GNU_RELRO:保存在重定位之后只读信息的位置

5.5 链接的重定位过程分析

31hello的反汇编代码

 

32hello反汇编代码main部分

hello中导入了诸如putsprintfgetcharsleep等在hello程序中使用的函数,增加了许多的外部链接来的函数。而这些函数的在hello.o中就没有出现。hello.o中的相对偏移地址到了hello中变成了虚拟内存地址。hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。

重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在hellohello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

 

5.6 hello的执行流程

程序名称 程序地址

ld-2.27.so!_dl_start 0x7f5d6118fea0

ld-2.27.so!_dl_init 0x7f5d6119e630

hello!_start 0x400500

hello!_init—0x400488

hello!puts@plt 0x4004b0

hello!exit@plt 0x4004e0

hello!main—0x4005e7

hello!printf@plt–0x4004c0

hello!sleep@plt–0x4004f0

hello!getchar@plt–0x4004d0

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

5.7 Hello的动态链接分析

共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定的技术解决这个问题,将过程地址的延迟绑定推迟到第一次调用该过程时。

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。

而在一个动态的共享链接库中仍然存在着一个可以调用程序加载而动态链接无需重定位的位置无关代码,编译器在程序中的函数开始运行时是不能自动预测各个函数的开始运行时间和地址的,这就可能需要系统添加重定位的记录,交给一个动态共享链接器或者采用它来进行重定位的动态共享链接,动态共享链接器本身就是负责执行对动态链接的重定位过程,这样做就有效地防止了程序运行时自动修改或者调用目标模块的位置无关代码段。

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

5.8 本章小结

本章介绍了链接的概念和作用,链接前后的elf的分析,符号解析和重定位。了解了虚拟地址空间的分配,动态链接的延迟绑定机制。理解了链接中重要的重定位过程的大致流程。

 

(第5章1分)

 

第6章 hello进程管理

6.1 进程的概念与作用

概念:一个执行中程序的实例。进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。系统中的每个程序都运行在某个进程的上下文中,由程序正确运行的状态组成的。

进程的作用:进程提供给应用程序两个关键的抽象。一个独立的逻辑控制流,他提供一个假象,好像我们的程序独占的使用处理器。一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

作用:shell 是一个交互型的应用级程序。

处理流程:shell 执行一系列的读/求值(read /evaluate ) 步骤,读取用户的输入,分析输入内容,获得输入参数。如果是内核命令则直接执行,否则调用相应的程序执行命令。

 

6.3 Hello的fork进程创建过程

Shell通过调用fork 函数创建一个子进程。也就是Hello程序,Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程有不同的pidfork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0.

 

6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。

加载并运行hello需要删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。

6.5 Hello的进程执行

系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。上下文是由程序正确运行所需的状态组成的。

子进程:当shell 运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制。子进程通过execve 系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。

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

进程调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定

程序在执行sleep函数时,sleep系统调用显式地请求让调用进程休眠,调度器抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。sleep的倒计时结束后,控制会回到hello进程中。程序调用getchar()时,内核可以执行上下文切换,将控制转移到其他进程。getchar()的数据传输结束之后,引发一个中断信号,控制回到hello进程中。

6.6 hello的异常与信号处理

异常分为中断,陷阱,故障,终止。

常用的信号:SIGCHLD,SIGINT,SIGSTOP等

Ctrl+Z

图33:Ctrl+Z的结果

Ctrl+C

图34:Ctrl+C的结果

乱按

图35:乱按的结果

图36:使用fg的结果

使后台挂起的进程继续运行。

图37:使用jobs的结果

图38:使用pstree的结果(太大了没有截完)

用进程树的方法进行连接

图39:使用kill的方法

 

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.7本章小结

本章介绍了进程的概念和作用,讲述了shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程。使用shell运行hello程序可以向其发送各种命令,可以通过信号处理各类异常,并且进程调度十分合理,这样使得系统运行稳定且高效。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:又称相对地址,是由程序产生的与段相关的偏移地址部分。逻辑地址是指就是hello.o里面的相对偏移地址。

线性地址:地址空间是一个非负整数地址的有序集合,是经过段机制转化之后用于描述程序分页信息的地址。是对程序运行区块的一个抽象映射。如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。就是hello里面的虚拟内存地址。

虚拟地址:CPU 通过生成一个虚拟地址。就是hello里面的虚拟内存地址。

物理地址:程序运行时加载到内存地址寄存器中的地址,用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

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

逻辑地址可分成段选择符+段描述符的判别符(TI)+地址偏移量的形式,每个段的首地址就会被储存在各自的段描述符里面,所以的段描述符都将会位于段全局描述符表中,通过段选择符我们可以快速寻找到某个段的段全局描述符

然后先判断TI字段,看看这个段描述符究竟是局部段描述符(ldt)还是全局段描述符(gdt),然后再将其组合成段描述符+地址偏移量的形式,这样就转换成线性地址了。

段选择符的索引位组成和定义如下,分别指的是索引位(index),ti,rpl,当索引位ti=0,段描述符表在rpgdt,ti=1,段描述符表在rpldt中。我们通常可以在查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址。目标段的逻辑地址=线性段的描述符=转换为线性段的基地址,等价于描述符转换为线性地址时关闭了偏移量和分段的功能。这样逻辑段的基地址与转换为线性段的基地址就合二为一了。

 

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

分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)

在这个转换中要用到翻译后备缓冲器(TLB),首先我们先将线性地址分为VPN(虚拟页号)+VPO(虚拟页偏移)的形式,然后再将VPN拆分成TLBTTLB标记)+TLBITLB索引)然后去TLB缓存里找所对应的PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到PPN之后,将其与VPO组合变为PPN+VPO就是生成的物理地址了。

 

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

 可以将VPN分成三段,对于TLBTTLBI来说,如果出现缺页的情况,则需要到页表中。此时,36VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3是对应的L1PT的物理地址,然后一步步递进往下寻址,CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,越往下一层每个条目对应的区域越小,寻址越细致。

 

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

得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),然后L1cache内部找,了L1里面以后,寻找物理地址检测是否命中,如果未能寻找到标记位为有效的字节(miss)的话就去二级和三级cache中寻找对应的字节,找到之后返回结果。到这里就是使用到我们的CPU的高速缓存机制了,使得机器在翻译地址的时候的性能得以充分发挥。

 

 

7.6 hello进程fork时的内存映射

fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。创建当前进程的mm_struct,vm_area_struct和页表的原样副本。Fork的子进程完全与父进程一致,有相同的虚拟内存空间。每个页面都标记为只读,两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。因此,也就为每个进程保持了私有地址空间的抽象概念。

 

 

7.7 hello进程execve时的内存映射

execve函数在当前代码共享进程的上下文中加载并自动运行一个新的代码共享程序,它可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但是它并没有自动创建一个新的代码共享进程。新的运行程序仍然在堆栈中拥有相同的区域pid。之后为新运行程序的用户共享代码、数据、bss和所有堆栈的区域结构创建新的共享区域和结构。

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,删除已存在的用户区域,创建新的私有区域(.malloc,.data,.bss,.text,映射共享区域设置PClibc.so.data,libc.so.text),指向代码的入口点。

 

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

缺页故障是一种常见的故障,在指令请求一个虚拟地址时,MMU中查找页表,如果对于的物理地址没有存在主存内部,以至于我们必须要从磁盘中读出数据,这就是缺页故障(中断)。

情况1:段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault

情况2:非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。

情况3:如果不是上面两种情况那就是正常缺页,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VAMMU,这次MMU就能正常翻译VA了。

 

7.9动态存储分配管理

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

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

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

分配器有两种基本风格:显式分配器、隐式分配器。

显式分配器(explicit allocator)

要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的newdelete操作符与C中的mallocfree相当.

隐式分配器(implicit allocator)

要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如LispML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

链表维护策略:

1)放置策略:首次适配、下一次适配、最佳适配。

首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。

2)合并策略:立即合并、推迟合并。

立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是推迟到某个稍晚的时候再合并空闲块。

在每个块的结尾添加一个脚部,分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,从而使得对前面块的合并能够在常数时间之内进行。

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

7.10本章小结

本章简要介绍了hello的存储地址空间。简单讲述了逻辑地址、虚拟地址、物理地址与线性地址的概念与转换方法,介绍了进程forkexecve时的内存映射的内容,描述了系统如何应对那些缺页异常,介绍了动态内存分配管理的方法。

 

 

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

文件的类型:

普通文件,目录(文件夹),套接字,命名通道,符号链接,字符和块设备

设备管理:unix io接口

所有的I/O设备都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输人和输出都能以一种统一且一致的方式来执行。unix io接口可以打开和关闭文件,读取和写入文件,改变当前文件的位置。

8.2 简述Unix IO接口及其函数

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

打开和关闭文件:

open():这个函数会打开一个已经存在的文件或者创建一个新的文件

close();这个函数会关闭一个打开的文件,close返回操作结果。

读写文件:

read():这个函数会从当前文件位置复制最多n个字节到内存位置,返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

write();这个函数从内存复制字节到当前文件位置

改变当前的文件位置

lseek()

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

研究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;

}

调用了两个外部函数,一个是vsprintf,还有一个是write

40vsprintf函数

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

write函数是将buf中的i个元素写到终端的函数。

Printf的运行过程:

vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

 

8.4 getchar的实现分析

getchar有一个int型的返回值。当程序调用getchar.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码。如果用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

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

 

8.5本章小结

本章中简单的描述了linuxio的接口及其设备和管理模式,unixio的接口及其使用的函数,以及printf函数和pritgetchar函数的实现方法以及操作过程。Printf函数在仔细研究中还是很有意思,学无止境,很多看起来很常用貌似很简单的东西,内涵还是很丰富。

(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

编写:通过编辑器编辑出hello.c文件。

预处理:将c文件的所有外部库展开为i文件。

编译:编辑器将i文件编译成汇编语言的s文件。

汇编:汇编器将汇编语言翻译成二进制机器语言,生成二进制可重定位目标程序o文件。

链接:链接器将所引用的目标文件符号解析,重定位后完全链接成可执行的目标文件hello。

运行:在shell中输入命令行指令,运行hello程序。

创建子进程:shell调用 fork创建一个子进程。

运行程序:对子进程用命令行分析的参数execve加载,mmap映射虚拟内存。

改变PC到_start,进入程序入口后程序开始载入物理内存,最后开始执行main函数

执行指令:hello进程逐条执行调用系统函数。

打印:配合I/O接口到字符显示驱动子程序,将内容打印在屏幕。

结束:return或exit后子进程终止被回收,内核删除为这个进程创建的所有数据结构。

hello完成了它的程序人生。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

    计算机系统深奥有趣,有各种精密的处理设计互相融合,组成了一整个系统,每一个部分都是特别重要且严密的,在软硬件的共同支持下,发挥出了其无与伦比的效果,当然还有许多需要学习的地方,也有很多还需要完善的地方,计算机的发展还远远没有结束。

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

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.i 预处理器修改了的源程序,分析预处理器行为

hello.s 汇编编译器生成的编译程序,分析编译器行为

hello.o 编译生成的可重定位目标程序,分析汇编器行为

hello 链接生成的可执行目标程序,分析链接器行为

hello.objdump: hello.o的反汇编,主要是为了分析hello.o

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

参考文献

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

[1]兰德尔·布莱恩特. 大卫·奥哈拉伦. 深入理解计算机系统 机械工业出版社

[2]动态链接原理分析 https://blog.csdn.net/shenhuxi_yu/article/details/71437167

[3]printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html

[4]ELF 构造:https://www.cs.stevens.edu/~jschauma/631/elf.html

[5]从逻辑地址到线性地址的转换流程

http://www.cnblogs.com/image-eye/archive/2011/07/13/2105765.html

 

(参考文献0分,缺失 -1分)

原文地址:https://www.cnblogs.com/lllllllm/p/14925139.html