HITICS 大作业 hello的一生

1180200109-申旭弘 1803002班

 

 

 

 

计算机系统

 

大作业

 

题     目  程序人生-Hello’s P2P 

专       业     计算机科学与技术   

学     号      1180200109        

班     级       1803002          

学       生        申旭弘       

指 导 教 师           史先俊       

 

 

 

 

 

 

计算机科学与技术学院

2019年12月

摘  要

一个程序,从它编写完成到输出在屏幕,对于大部分人而言都不会去注意到它中间发生了什么变化。事实上,对于我们计算机学科,了解并掌握其中间变化过程,对于我们优化程序,了解系统构造,了解程序运行过程,寻找程序错误都大有脾益.本文将以一个所有程序员都熟悉的hello.c,探究它从编写完成,经过预处理,汇编,编译,链接生成可执行文件,再由系统创建进程,分配内存空间,I/O设备工作共同运行可执行文件,最终输出在屏幕上.

关键词:P2P,编译,进程,计算机系统;                           

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

 

 

 

 

 

 


 

目  录

 

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

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

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 4 -

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

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

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

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

2.4 本章小结......................................................................................................... - 5 -

第3章 编译............................................................................................................. - 6 -

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

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

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

3.4 本章小结......................................................................................................... - 6 -

第4章 汇编............................................................................................................. - 7 -

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

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

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

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

4.5 本章小结......................................................................................................... - 7 -

第5章 链接............................................................................................................. - 8 -

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

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

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

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

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

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

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

5.8 本章小结......................................................................................................... - 9 -

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

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

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

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

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

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

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

6.7本章小结........................................................................................................ - 10 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结...................................................................................................... - 12 -

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

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

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

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

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

8.5本章小结........................................................................................................ - 13 -

结论......................................................................................................................... - 14 -

附件......................................................................................................................... - 15 -

参考文献................................................................................................................. - 16 -

 


第1章 概述

1.1 Hello简介

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

Hello的P2P:P2P是指Program to Process。在编写完hello.c过后,预处理器(cpp)会修改程序得到文本文件hello,i文件,即hello.c的与处理文件.编译器(cll)将hello.i翻译成文本文件hello.s,即汇编程序.通过汇编器(cs)将hello.s翻译成机器语言指令,得到可重定位目标文件hello.o.通过链接器(ld)得到可执行目标文件hello.由此实现了从hello.c源程序到hello可执行目标文件的转换,也就是P2P过程

得到可执行文件hello后,需要shell为其创建一个新的子进程,并用execve打开,分配虚拟内存空间,并开始执行.执行过程中需要通过Unix I/O控制输入和输出,最终执行结束后,shell回收子进程.至此完成hello的O2O过程(zero 0 to zero 0)

1.2 环境与工具

硬件环境:X64 CPU ,2.50GHz , 8G RAM

软件环境:Windows 10 64位 ,Vmware 14 ,Ubuntu 16.04

开发工具:gcc + gedit , Codeblocks , gdb edb

1.3 中间结果

hello 可执行文件

hello.c C文件

hello.i 预处理之后的文件

hello.ld 链接后的文件

hello.o 可重定位目标文件

hello.s 汇编语言文件

read 反汇编文本文件

1.4 本章小结

     这一章简单介绍了hello的p2p和020过程,列出了中间文件即研究平台环境,是后面研究p2p和o2o过程的基础.

(第1章0.5分)

 


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:在编译器工作前,将源程序hello.c翻译成一个ASCII码的中间文件hello.i。预处理器CPP通过以#为行首的指示,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等,分别对应: 将源文件中以include格式包含的文件复制到编译的源文件中。用实际值替换用#define定义的字符串。根据“#if”后面的条件决定需要编译的代码。除此之外还会加上行号,将头文件包含的写入文件中.以及删除注释.

预处理的作用:合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

2.2在Ubuntu下预处理的命令

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

应截图,展示预处理过程!

2.3 Hello的预处理结果解析

   

hello.c的字节数为527bytes,经过预处理得到的hello.i的字节数达到了63946bytes。

hello.i读取了stdio.h,unistd.h,stdlib.h的内容并展开,若stdio.h,unistd.h,stdlib.h中仍然存在上述所说的#开头的命令行时,还需要继续进行预处理,直到所有的#都被解析完,因此最后的hello.i中也不存在#.同时,预处理过程中删除了注释

2.4 本章小结

本章介绍了hello.c到hello.i的预处理过程。对于预处理文件,阅读与处理文件能够更好的帮住我们理解预处理过程。在一般情况下,我们往往会忽略预处理这个过程,但了解预处理过程是很有必要的。

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

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译的概念:检查hello.i,判断其是否具有语法错误,确认无误后,编译器(ccl)将预处理文本文件hello.i编译成文本文件hello.s. hello.s包含汇编语言.汇编语言以文本格式描述机器语言指令

编译的作用:将hello.i转变为hello.s

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

.file:源文件名

.text:代码段

.align:对齐方式

.global 全局变量

.section  .rodata:rodata节

.string:字符串

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

3.31数据

在hello.s里有常量,局部变量

常量:hello.s里的常量包含string

第一串string对应: Hello 学号 姓名 秒数!

第二串string对应:Hello %s %s\n

局部变量有i, int argc char*argv[]。

int i是main函数内局部变量,保存在-4(%rbp)中,在之后和i相关的操作都是在-4(%rbp)上操作

int argc, char*argv[]是参数,因为是64位,所以会用寄存器保存参数:%edi是第一个参数argc,%rsi是第二个参数argv

3.32赋值

对局部变量i的赋值:i是局部变量,程序用栈帧保存局部变量

3.33类型转换

hello.c中无类型转换实例。

3.34算术操作

hello.c中存在i++,在.s文件中是这样表达的

3.35关系操作

用于判断参数argc!=4的语句,条件跳转。

用于判断局部变量i<8的语句。循环

3.36数组/指针/结构操作

在这一句中,作为参数传递的argv被保存在了栈帧中。

在下面这几句中,因为64位,一个地址长度为8字节,所以分别通过在argv+8,argv+16得到argv[1],argv[2]的地址,也就是存放的字符串的地址。然后用.LC1取得要打印的字符串。从而将argv[1],argv[2]的内容打印出来。

之后还有调用argv[3],方法也和上面类似,下面截图有显示

3.37控制转移

if(argv!=4)的具体实现:-20(%rbp)和4进行比较,实际就是argv与4比较

for(i=0;i<8;i++)的具体实现。检查i和7的值大小,小于则跳转到.L4执行for循环内的语句。

3.38函数操作

根据hello.c,包含的函数有main,printf,exit,sleep,atoi,getchar

下图中包含main,printf,exit三个函数。对于printf函数,printf调用前,将字符串地址赋值给%rdi。对于exit函数,将结束码赋值给%edi。

通过下面的语句调用atoi和sleep

通过下面的语句调用getchar

3.4 本章小结

在这一章里,编译器将上一章得到的与处理文件编译成汇编语言.通过阅读汇编语言文件hello.s,我们更加了解了数据,函数,跳转等操作是如何在机器上实现的.这为我们日后优化代码奠定了基础.hello.s也是汇编阶段产生机器代码的基础.

(第32分)


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器语言指令,这些指令将会被打包成可重定位目标程序过程,这个过程称为汇编。

汇编的作用:将汇编语言文件hello.s转换成可重定位目标文件hello.o

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

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

1.ELF头:ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,及其类型,节头部表的文件便宜,节头部表中条目的大小和数量。

hello.o的ELF头的分析:包括类别(Class),数据(Data),机器类型(Machine),入口点地址(Start of program headers),程序头起点地址(Start of section headers),本头大小,节头部大小等信息。

2.节头部表(section header table)

节头部表描述了不同节的位置和大小,目标文件中每个节都有一个固定大小的条目

可以看到包含,text,.tela.text.data,.bss,.rodata等节的信息,包括其大小,地址,偏移量等信息。

对于各节的基本信息:

.text 已编译程序的机器代码

.rel.text 一个.text节中位置的列表

.data 初始化的全局和静态变量

.bss 未初始化的全局和静态变量

.rodata 只读数据(switch语句跳转表)

.symtab 符号表

.rel.data 被模块引用或定义的所以全局变量的重定位信息

.debug 调试符号表

.line 行号与.text机器指令之间的映射

.strtab 一个字符串表

3.重定位信息:

根据书本信息,可以得到对于64位的可重定位文件,其重定位记录结构为:

typedef struct{

  long offset

  long type

  long symbol

  long attend

}Elf64_rela

R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。

可以看到,因为调用了公共库函数puts,exit,printf,atoi,sleep,getchar等,其函数在代码所在的位置是需要重定位的。并且在打印时有调用保存在.rodata(只读数据)的内容,所以打印时也需要进行重定位。

Symbol table符号表:函数和全局变量的信息。

4.4 Hello.o的结果解析

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

对照分析:

在操作数方面,hello.s采用10进制,hello.o的反汇编采用16进制。

分支转移方面:hello.s对于分支跳转采用.L2,.L3等段名称进行跳转。hello.s对于第一个if跳转采用.L2段跳转,对第二个for跳转采用.L4段。而hello.o的反汇编采用的分支跳转方法为通过偏移量跳转到相对偏移的地址。

函数调用方面:hello.s调用函数时是函数的全名。而在hello.o的反汇编中,call指令调用了偏移地址,通过偏移地址得到函数。

全局变量方面:hello.s中对printf,atoi等函数的调用是直接写出.而在hello.o中,因为这些函数实际需要重定位,地址是在运行时确定的,因此在汇编成机器语言时,操作数全部置为0,添加重定位条目.

4.5 本章小结

通过分析得到hello.o及其elf格式,明白了汇编时,汇编器所做的工作.而通过hello.o的反汇编代码与hello.s作比较,更加了解了汇编代码与机器代码之间的关系.

(第41分)


第5章 链接

5.1 链接的概念与作用

链接的概念:将各种代码和数据片段手机并组合成一个单一文件的过程,这个文件可被加载到内存并执行

链接的作用:当程序调用函数库的函数时,会将这个函数已经预编译得到的.o文件合并,得到可执行文件。链接的作用在于,链接使得分离编译成为可能。

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的格式

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

Hello的ELF格式:可执行文件hello的elf格式用elf头描述文件总体.包括程序的入口,即程序运行时的第一条指令的地址。其中.text和.rodata和.data节相对于hello.o已经进行了重定位到了最终运行时的地址。还添加了一个.init节,定义了一个小函数_init,程序初始化代码时会调用它。

5.4 hello的虚拟地址空间

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

通过edb可以看到虚拟地址从0x401000开始

通过上图确定各节的位置,下面用edb查看各节信息.

.init节在0x401000处,存放了_init函数

.plt节

.rodata节

5.5 链接的重定位过程分析

1.地址的变化:hello.o中的地址是相对偏移地址,而hello的地址是虚拟地址。同时,因为打印字符串需要访问.rodata(只读数据区),而.rodata节的位置在运行中确定,因此也需要重定位。  重定位的方式:rip寄存器相对寻址。因为.rodata节中的字符串和当前.text节的语句之间的偏移量是固定的,下一条指令的虚拟地址 + 偏移得到全局变量地址。

2.新增加的函数及函数调用调用:对于hello.o来说,因为不存在调用的库函数信息,所以在callq语句后有4个字节的0表示需要重定位。而对于hello,因为需要重定位的函数信息已经获得,所以不再存在需要重定位的条目,跳转和函数调用都变成了虚拟地址。  重定位的方式: 虚拟地址直接寻址。当链接器将库函数和hello.o链接后,链接器能够直接得到调用的函数的地址,直接call语句调用即可。(但是call语句实际也是用指令计数器rip相对寻址的方式)

3.增加的节:增加了.init和.plt节,及一些新函数

链接的过程: 在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的_libc_csu_init,_libc_csu_fini, _libc_start_main。链接器将上述函数加入。

重定位过程: 链接器完成符号解析后,将代码中每个符号引用和一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来,此时,链接器就知道了它的输入目标模块中的代码节和数据节的确切大小。重定位首先应进行重定位节和符号定义,在这一步骤中,链接器将所有相同类型的节合并为同一类型的新的聚合节,然后程序将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,由此程序中的每一条指令和全局变量都有唯一的运行时内存地址。其次,进行重定位节中的符号引用,这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

重定位地址计算: 首先获取各节的大小,然后直接通过对应的节与程序头之间的相对距离大小确定节头的地址,节中对应的指令地址则由与节头之间的相对地址计算,各指令的具体地址均可由此获知。而在之后call,jmp调用时,利用pc相对寻址即可.

5.6 hello的执行流程

按照顺序:

_start   地址:0x401090

__libc_start_main@GLIBC_2.2.5 地址:0x403ff0(共享库的函数)

main    地址:0x4010c1

puts@plt  地址:0x401030

exit@plt   地址:0x401070

printf@plt  地址:0x401040

atoi@plt    地址:0x401060

sleep@plt   地址:0x401080

getchar@plt  地址:0x401050

5.7 Hello的动态链接分析

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

首先通过这个信息得到GLOBAL_OFFSET_TABLE的信息.进入edb调试

可以看到,在执行前几乎为0

执行后可以发现加上了一个偏移值,偏移值为7f5a9cfcc190,对应了一个地址.

通过上述所述,不难推测在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到dl_init的过程进行。

5.8 本章小结

链接对于程序开发具有重大作用,它使得分离编译成为了可能.由此出现的静态库与动态库概念,也减少了不必要的存储开销.仔细研究可重定位目标文件,能够帮助我们更好的理解程序调用函数,重定位的过程.链接也是上述过程生成hello可执行文件的最后一步,也是整个程序能够运行的关键.

(第51分)


第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:狭义进程是指:进程是一个进行中的程序的示例。广义进程是指:一个具有一定独立功能的程序关于某个数据集合的一次运行活动。

进程的作用:得到一个假象,就好像我们的程序是系统中当前的唯一的程序一样,好像独占地使用处理器和内存,处理起好像是无间断地一条接一条地执行我们程序中的指令,代码和数据好像是系统内存中唯一的对象。   并且运行程序时,shell都会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。

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

Shell的作用:shell用于处理命令,能够访问操作系统某一个具体的文件。

Shell的处理流程:首先收到命令后判断是否是内置命令,如果是,则进行相应处理,如jobs输入后就会显示任务列表。如果不是内置命令,则判断是否是打开某一个应用程序(包括自身的程序或者购买的程序)。然后在搜索路径里面搜索打开。如果搜索路径没有用户输入的应用程序,或者用户输入的字符串不属于上面的任意一种,则会产生一条错误信息。

6.3 Hello的fork进程创建过程

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

hello作为可执行文件,按照上文所述的shell的处理流程来说,父进程创建(fork)一个子进程,子进程内会打开hello。hello进程几乎但不完全与父进程相同,hello进程得到与父进程用户级虚拟空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库、以及用户栈。hello进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,hello进程可以读写父进程中打开的任何文件。父进程和hello进程最大的区别在于它们有不同的PID。

fork函数只被调用一次,却会返回两次。在父进程中,fork返回hello进程的PID,在hello进程中,fork返回0 。

6.4 Hello的execve过程

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

Execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。出现错误时,execve才会返回到调用程序。所以实际上,如果不出现错误,execve调用一次且从不返回。在execve加载了filename之后,它调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数。

加载的过程:shell通过调用某个驻留在存储器中称为加载器的操作系统代码来运行它。任何Linux程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。

每个Linux程序都有一个运行时内存映像。加载时,在程序头部表的应到下,加载器将可执行文件的片复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址,然后调用启动函数。然后在需要的时候把控制返回给内核。

6.5 Hello的进程执行

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

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

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

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

用户模式和内核模式:通过设置模式位,用于区分进程运行在用户模式还是内核模式。用户模式进程不允许执行特权指令:如停止处理器,改变模式位,发起一个I/O操作。也不允许用户模式中的进程直接应用地址空间中内核区内的代码和数据。在/proc文件系统下,允许用户模式进程访问内核数据结构内容,/proc文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。

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

当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。例如sleep系统调用,显式地请求让调用进程休眠。一般而言,内核可以决定执行上下文切换。中断也可能引发上下文切换,所有的系统都有某种产生周期性定时器终端的机制。每次发生定时器中断,内核就能判定当前进程已经运行了足够长的时间,并切换到一个新的进程。

因此,对于hello来说在Hello执行的某些时刻,比如sleep函数,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,当内核调度了一个新的进程运行后,它就抢占Hello进程,并且使用上下文切换机制来将控制转移到新的进程。

hello进程初始运行在用户模式,hello调用sleep显式地进行调度,进行上下文切换。当sleep定时器结束后,又会上下文切换回用户模式。

6.6 hello的异常与信号处理

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

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

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

1.输入ctrl-c

Ctrl-c输入。此时一个SIGINT信号被发送给父进程,会终止为hello而fork的进程,并回收这个进程。

2.乱按键盘

乱按时输入的内容并不会影响当前hello进程的运行,但是当hello进程结束时,所输入的字母都会被当作命令行进行解析

3.输入ctrl-z的结果

输入ctrl-z过后,父进程会接收到SIGSTP信号,将子进程挂起,放到后台。

4.ctrl-z后输入jobs

Jobs命令的作用是,打印出当前shell正在处理的进程的信息。Jobs还可以在后面添加命令行,如jobs -r表示当前正在运行的进程,jobs -l表示当前暂停的进程。

5.ctrl-z后输入fg

fg的作用是将进程跳至前台并继续工作,可以看到fg后打印的行数和输入ctrl-z前打印的行数相加为8,证明是从挂起暂停的状态到继续工作。

6.pstree命令

在ctrl-z后,输入pstree,可以看到进程数,上图是部分命令显示结果.

7.kill命令

可以看到,在挂起后,用kill命令发送杀死进程的信号给进程,进程被杀死.

6.7本章小结

这一章让我们更加了解了系统是如何运行hello的. 系统为了运行hello需要fork一个新的进程. hello的创建、加载和终止,通过键盘输入,对hello执行过程中产生信号和信号的处理过程有了更多的认识,从而对异常的掌握加深了同时对系统的进程调度有了更好的认识.

(第61分)


第7章 hello的存储管理

7.1 hello的存储器地址空间

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

逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是个索引号,后面3位包含一些硬件细节 。段内偏移量:16/32/64位。

线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。

虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。

物理地址:一个物理地址空间,对应于系统中物理内存的M个字节。

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

首先逻辑地址由两部分组成:段标识符(16位),段内偏移量(16/32/64位)

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

段描述符分两类:1.用户的代码段和数据描述段。2.系统控制段描述符。

描述符表又包括:1.全局描述符表(GDT)。2.局部描述符表(LDT)。3.中断描述符表(IDT)。

从逻辑地址到线性地址的过程如下:

1.查看TI,为0则明白要转到GDT中的段。为1则明白要转到LDT中的段。

2.通过索引*8得到被选中的段描述符。由此,得知32位段基地址。

3.之前还有未使用的段内偏移量,段内偏移量+段基地址就得到了线性地址。

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

线性地址通过分页机制得到物理地址。

分页机制:把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间。(这使得每一页地址都是对齐的)。

页表:虚拟内存系统用页表来判定一个虚拟页是否缓存在DRAM中的某个地方。页表存放在物理内存中,页表将虚拟页映射到物理页。每次进行线性地址到物理地址时都会读取页表。

操作:CPU中的一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。N位虚拟地址包含两个部分,一个p位的虚拟页面偏移(VPO),一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE(页表条目)。得到页表条目中的物理页号后,将物理页号和虚拟地址中的(VPO)串联起来,就能得到相应的物理地址。物理页面偏移和VPO是相同的。

在得到PTE地址,找到对应PTE后,会因为PTE的有效位产生下面两种情况。

页面命中时:

1) 处理器生成一个线性地址,并将其传送给MMU

2-3) MMU 根据得到的VPN,使用内存中的页表生成PTE地址,从PTE中得到物理地址。

4) MMU 将物理地址传送给高速缓存/主存

5) 高速缓存/主存返回所请求的数据字给处理器

缺页异常:

1) 处理器将线性地址发送给 MMU

2-3) MMU 使用内存中的页表生成PTE地址

4) PTE中的有效位为零, 因此 MMU 触发缺页异常

5) 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)

6) 缺页处理程序调入新的页面,并更新内存中的PTE

7) 缺页处理程序返回到原来进程,再次执行缺页的指令

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

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

TLB加速地址翻译:如果纯粹地按照上面的做法,每次CPU产生一个线性地址,MMU就必须查阅一个PTE。为了消除开销,MMU包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)。

TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。为了使用TLB,线性地址中的VPN还可以对等于下面的形式:

TLB假速后的地址翻译就会变成下面的流程:

1.CPU产生一个线性地址

2,3.MMU从TLB中取出相应的PTE。

4.MMU将这个虚拟地址翻译成一个物理地址,发送到高速缓存/主存中

5.高速缓存/主存将所请求的数据字返回给CPU。

由于局部性原理,TLB不命中发生概率并不高。

四级页表:多级页表是常用于压缩页表的技术。其原理是用1页的每个PTE都映射下一级页表地址,由此形成4级页表。采用多级页表后,虚拟地址也有了一个新的映射。通过多级分页机制减少因为存放页表造成的不必要的内存浪费。还可以仿照cache原理,建立一个页表缓冲器,将常访问的页表缓存在其中。

在有了上面所述的知识后,下面介绍在TLB和四级页表支持下VA-PA的变化(用书上的core i7作为例子):

首先访问TLB,TLB存在则直接生成物理地址向主存/高速缓存申请引用。如果不存在则进入下面的步骤:

首先CR3寄存器有着第一个页表的物理地址,通过此进入第一个页表。VPN1包含了1个一级页表的偏移量,由此得到要查找的二级页表的地址。VPN2包含了1个2级页表偏移量,通过二级页表基址+二级页表偏移量得到三级页表基址。由此类推,最后得到PPN,与VPO进行组合就能得到物理地址。

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

如上图所示,得到了一个长度52的地址。将其分为3部分,前40位作为CT,后12位PPO作为6位CI和6位CO。CI表示索引,CT表示标记。通过这两点与cache的有效位能够得到是否命中的信息。如果未命中,则用同样的方法去查找2级,3级cache如果还未找到则需访问主存。之后根据CO得到偏移量,返回相应的字节。

7.6 hello进程fork时的内存映射

当shell为hello进程fork时,内核为这个创建的新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存。然后如下操作:

创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.

两个进程中的每个页面都标记为只读

两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)

7.7 hello进程execve时的内存映射

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

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

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

3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

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

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

DRAM缓存不命中称为缺页。地址翻译硬件从常驻内存的页表读取PTE,从有效位能够推断出是否被缓存。如果没有被缓存则出发了缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会通过某种算法选择一个牺牲页。如果这个牺牲页已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改。

7.9动态存储分配管理

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

动态内存分配:引入动态内存分配是因为,时常程序运行时才知道所需要空间大小。为了时常分配内存,引入动态内存分配器。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。动态内存分配器分为以下两种情况:

1.显式分配器,要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。

2.隐式分配器,也叫垃圾收集器,会自动监测不再使用的块将其释放回收。例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

分配内存时,会产生两种碎片:

内部碎片:当一个已分配块比有效载荷的,由对齐要求产生

外部碎片:空闲内存中的块合计大小虽然满足要求分配的大小,但处于不连续的块中。

为了标记是否分配块,块大小等信息,需要设计块的数据结构。

1.隐式空闲链表

Knuth提出边界标签方法,在一个块中既存在一个头部用于记录长度,分配情况等,也存在一个脚部作为头部的副本。这样在合并的时候,可以快速得到被合并块的信息。这样就能在常数时间内完成对块的合并。

2.显式空闲链表:显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。

为了寻找到合适的块,有三种分配策略

1.首次适配:从头搜索,遇到第一个合适的块就停止;

2.下次适配:从头搜索,遇到下一个合适的块停止;

3.最佳适配:全部搜索,选择合适的块停止。

7.10本章小结

本章主要介绍了,与hello的存储地址空间有关的定义及解决策略。存储的段式管理与页式管理,包括发生缺页异常时的处理方法. 在后面也介绍了常用的动态内存分配器,其块的数据结构,分配策略等. 了解虚拟内存,了解cache策略,更加有助于我们写出存储器友好的程序.

(第7 2分)


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

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

Unix IO接口:接口就是连接CPU与外设之间的部件,它完成CPU与外界的信息传送。还包括辅助CPU工作的外围电路,如中断控制器、DMA控制器、定时器、高速CACHE。

所有的I/O设备都被模型化为文件,所有输入输出都被当作相应文件的读和写.Linux shell创建的每个进程开始时都有三个打开文件:标准输入(描述符0),标准输出(描述符1)标准错误(描述符2).

将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,对于文件有以下操作:

1.打开文件:利用open函数,返回一个小的描述符数字---- 文件描述符。返回的描述符总是在进程中当前没有打开的最小描述符。

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

3.读文件。利用read,write函数,进行操作。read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。

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

8.3 printf的实现分析

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

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

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

8.4 getchar的实现分析

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

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

8.5本章小结

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

了解系统级IO是重要的,有时你除了使用Unix I/O 以外别无选择。在某些重要的情况中,使用高级I/O 函数不太可能,或者不太合适。例如,标准I/O 库没有提供读取文件元数据的方式,例如文件大小或文件创建时间。同时这对于我们理解网络编程和并行流是很有帮助的。

(第81分)

 

结论

在”经历”了hello的一生过后,明白了从一个hello.c的文本文件到打印到屏幕上的不易,在这些过程中,预处理,编译,汇编,链接,进程调度等,如何更好地优化这些过程是我们需要考虑的.同样,在编写程序时,对于硬件性能的掌控也是我们需要学习的.如何更好的编写出cpu友好,存储器友好的程序也是十分重要的.  虽然整个过程十分繁琐,但正是这些繁琐的过程保护了hello和系统的稳定性,因此我们必须要掌握好计算机系统.

下面是整个流程复述:

得到hello.c(hello.c是通过键盘等I/O设备输入到计算机里,并存储在硬盘上的)

预处理器将hello.c预处理成为hello.i

编译器将hello.i翻译成汇编语言hello.s

汇编器将hello.s汇编成可重定位二进制代码hello.o

链接器将外部文件和hello.o链接成为可执行二进制文件hello

shell进程调用fork为其创建子进程

shell调用execve,execve加载程序 ,将pc设置为开始代码.

访问内存,MMU将虚拟内存映射成物理地址

异常情况的处理等等(键盘输入等)

结束,shell父进程回收子进程。

创新理念:对于现在的pc,发展在一部分程度上受制于材料,物理等学科的发展.在未来,会得到更快的cpu,容量更高速度更快更加便宜的存储设备. 在未来计算机系统应该为了更加优秀的硬件去不断优化. 如当前的cache系统,在未来甚至可以采用虽然昂贵但是更加优秀的全相联cache.

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


附件

hello.i 预处理之后文本文件

hello.s 编译之后的汇编文件

hello.o 汇编之后的可重定位目标执行

Hello 链接之后的可执行目标文件

read  hello.o的反汇编代码

read2 hello的反汇编代码

read3 hello.o文件的ELF格式输出到的文本文件

read4 hello程序的ELF格式文件

(附件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分)

猜你喜欢

转载自www.cnblogs.com/fatdragon/p/12116492.html