HIT计算机系统大作业:hello的一生

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

Hello的P2P:从hello.c的源文件开始,通过GCC编译器驱动程序对它进行预处理后生成hello.i文件,再通过编译器生成汇编程序hello.s,汇编程序再通过汇编器生成可重定位目标程序,最后再与标准库函数进行链接生成可执行文件。然后在shell中运行可执行文件,shell通过fork()函数为它开辟新的子进程。Hello.c就从Program变成了Process。

Hello的020:Hello变成进程后,shell再调用execve()函数将程序加载进来:先删除已存在的用户区域,再为Hello映射新的私有区域,映射共享区域。CPU来控制Hello的运行,为它分配资源。当进程结束后,通过父进程回收,通过内核将数据删除,将Hello运行的所有记录消除。

1.2 环境与工具

软件环境:VMware® Workstation 14 Player,Ubuntu 16.04 LTS 64位。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk。
开发调试工具:GCC编译器,EDB,as,ld,readelf。

1.3 中间结果

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

Hello.c           Hello的c语言代码

Hello.i           预处理之后的文本文件

Hello.s           编译之后产生的汇编文件

Hello.ld          链接后的文件

Hello.o           可重定位的目标文件

Hello            可执行文件hello

Helloo.objdump    Hello.o反汇编文件

Hello.elf          Hello的ELF格式

Hello.objdump     Hello的反汇编文件

1.4 本章小结

即使是对于再简单的hello,也同样涉及到了关于计算机系统的从硬件到软件的各个部分的共同配合才能实现一个hello的功能。因此我们要根据hello来了解计算机系统的各个组成部分,从顶层到底层,然后再由底层到顶层,这样我们就进一步了解计算机系统是如何构造和运作的,便于我们更加深刻的了解计算机系统。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理是指在编译之前的预处理阶段,预处理程序对程序进行的操作. 预处理器根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。

1.将所有的#define删除,并展开所有的宏定义;

2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;

3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;

4.添加行号信息文件名信息,便于调试;

5.删除所有的注释:// /**/;

6.保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作。

生成.i文件。

包括:1. 宏定义 2. 文件包含(文件包含是指把指定文件的全部内容包括到当前源程序文件中。) 3. 条件编译(指在特定的条件下,对满足条件和不满足条件的情况分别进行处理——满足条件时编译某些语句,不满足条件时编译另一些语句。)

预处理的主要作用仅仅是对程序代码文本进行替换操作,如将以#include格式包含的文件内容复制到编译的源文件中,用实际值替换#define定义的宏,以及根据#if的条件决定需要编译的代码。预处理过后程序代码中的预处理指令会被删除。所以预处理器的输出是原程序的一个编辑后的、不包含指令的版本。

2.2在Ubuntu下预处理的命令

预处理命令行:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

经过预处理程序处理hello.c源文件由23行扩展成为3000多行,main函数被放在了最后,而前面3000多行是hello.c引用的头文件,如stdio.h。预处理过程中预处理器(cpp)识别到#include这种指令就会在环境中搜寻该头文件并将其递归展开。

#include,#include‘stdio.h’这两种表示形式有区别,第一种系统会直接在c库函数头文件所在的目录中查找要包含的文件,第二种是用户在当前目录中查找,如果找不到才回到c库函数头文件所在目录中华查找要包含的文件,一般用于自己编写的头文件

2.4 本章小结

通过本章我们了解到了预处理.预处理是整个编译过程的第一步,在翻译器中,C预处理器(cpp)将源文件翻译成一个ASCII码的中间文件在这个过程中仅仅是将一些.c文件里面用到的、涉及到的函数库头文件复制到程序源文件中、在程序代码中替换宏定义,并根据条件编译的条件保留相应的内容, 直到预处理文件中不包含任何预处理命令为止,就这样完成了预处理的指令为其继续翻译成机器能够识别的汇编语言文件打下基础。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

概念:编译是指编译器(ccl)将文本文件hello.i翻译成文本文件hello.s的过程,这个文本文件内包含了一个汇编语言程序。

作用:编译过程编译器实现了经过词法分析、语法分析、语义分析等过程,在检查无错误后将代码翻译成汇编语言。得到的汇编语言代码可供编译器进行生成机器代码、链接等操作。由于计算机并不能直接接受和执行用高级语言编写的源程序(此处指.c,.i文件),因而利用编译器,能将高级语言编写的程序全盘扫描,翻译成用机器语言表示的与之等价的目标程序(此处指.s,即汇编语言程序),该目标程序能被计算机接受和执行,以便后续翻译等操作进行。

    

3.2 在Ubuntu下编译的命令

编译命令行:gcc -E hello.c -o hello.s

结果展示为:

3.3 Hello的编译结果解析

文件声明:

.file 源文件

.text 代码段

.globel 全局变量

.align 对齐格式

.type 对象类型或函数类型

.size 数据空间大小

.section .rodata 只读代码段

.string 字符串

数据:

.s文件中主要数据类型为:整形,字符串,指针数组

1.整形

(1).main参数 int argc:

Argc是函数传入的第一个int的参数,存储在%edi中,表示终端输入的参数的个数多少。

(2).main函数中的局部变量 int i:

函数内部的局部变量存于堆栈中,其中用movl $0,-4(%rbp)进行数据在堆栈内的存储,并且由于-4所以可知i占用了4字节大小的栈空间

2.字符串:

(1) 第一个字符串.LC0中包含汉字,其中汉字的编码方式为utf-8编码,汉字被编码为三个字节。

(2) 第二个字符串.LC1其中的两个%S对应与输入的两个参数:argv[1]和argv[2]。

3.指针数组argv[]:

作为main函数的参数出现在栈帧中。

赋值:

1.对局部变量的赋值:

通过movl指令,利用寄存器和$0, -4(%rbp)指令对局部变量进行赋初值0

算术操作:

Hello.c中的主要的算术操作为循环遍历增加(i++)以及语句argc!=4

编译器将i++翻译为addl$1,-4(%rbp)

关系操作:

Hello.c中的主要的算术操作为循环的控制(i<10)以及语句argc!=4

编译器将i<8翻译为$7,4(%rbp)

将argc!=4翻译为cmpl$4,-20(%rbp)

数组/指针/结构操作

指针数组(char *argv[]):

在argv数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]和argc[3]分别指向两个用户从终端输入的字符串分别对应学号姓名时间。根据图3-11,可知通过%rax+16和%rax+24%rax+8中存储的地址,(语句:addq $24,%rax addq $16,%rax 以及 addq $8,%rax)分别得到argv[1]和argc[2] 和argc[3]

控制转移:

hello.c内部的控制转移主要有if语句以及for循环内部的控制转移

1.for(i=0;i<8;i++):

for循环的控制时比较cmpl $7, -4(%rbp) ,当i大于9时跳出循环,否则进入.L4循环体内部执行

2.if(argc != 4):

当argc不等于4时进行跳转。cmpl语句比较 -20(%rbp)和-4,设置条件码,然后根据ZF进行判断,如果最近的操作得出的结果为0,则跳到.L2中,否则顺序执行下一条语句。

函数操作:

内部主要有5个函数:main(),printf(),exit(),atoi(),sleep(),getchar()

1.main():

参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储

函数调用:被系统启动函数调用,

函数返回:设置%eax为0并且返回

函数作用:作为程序运行的唯一入口

2.printf():

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

函数调用:在for循环中被调用

函数作用:用来打印信息

3.exit():

参数传递:传入一个布尔变量

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

函数作用:如果传入的参数为1,则执行退出命令

4.atoi():

参数传递:传入参数argv[3]

传递控制:call atoi

函数调用:for循环下被调用

函数作用:将传入的参数从字符串形式转化为int整形

5.sleep():

参数传递:传入参数argv[3]经过atoi函数的输出值

传递控制:call sleep

函数调用:for循环下被调用

函数作用:使计算机程序(进程,任务或线程)进入休眠

6.getchar():

传递控制:call getchar

函数调用:在main中被调用

函数作用:用来读取字符串

3.4 本章小结

编译器将预处理后的文本进行汇编处理,对于常量,编译器将它储存到一个特定的位置,记录它的一些信息,比如类型;对于一些特定的常量,比如printf()函数中的信息,编译器会把它提取出来保存。程序中的语句,例如赋值语句,编译器通过寄存器,栈等结构进行赋值;分支语句用je,jle,jge等条件跳转语句进行实现。每种语句都有对应的实现方法。程序中的函数,如果不是库函数,则会对函数进行逐句的语法分析和解析,如果是标准的库函数,编译器可以直接用call语句进行调用。

在本章中,编译器将高级语言编译成汇编语言,在以上的分析过程中,详细的分析了编译器是怎么处理C语言的各个数据类型以及各类操作的,按照不同的数据类型和操作格式,解释了hello.c文件与hello.s文件间的映射关系。在此阶段,编译器将hello.i文件编译成更抽象更低级的hello.s汇编语言文件,为汇编阶段产生机器可识别的机器语言指令打下基础。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:通过汇编器将汇编语言转化为机器语言。

汇编的作用:汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o中,hello.o是二进制文件。

4.2 在Ubuntu下汇编的命令

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

应截图,展示汇编过程!

4.3 可重定位目标elf格

    ELF可重定位目标文件的格式:
 

汇编器在对hello.s文件转化为机器代码后,对程序做出了最基本的处理,在rel.text对每个调用的标准库函数和全局变量给出了偏移量,以及在信息一类中给出的符号在symbol节中的偏移量,以及符号的类型。

4.4 Hello.o的结果解析

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

反汇编后的汇编代码已经进行了初始化的定位,形成EIF可重定位文件,并且每条指令都给出了对应的机器码。

1)对于条件分支:.s文件中通过使用.Ln等助记符来标记跳转位置,而在反汇编文件中去掉了这种记号,直接使用偏移地址来进行跳转。

hello.s文件:

反汇编文件:

2)对于函数调用:在.s文件中,call指令后面接的是函数名,并没有给出地址,而在反汇编文件中,给出了函数的偏移地址。因为在汇编的过程中,对于程序中的函数,汇编器在EIF文件的rel.text段中给出了定位,所以反汇编中函数就有了相对于首地址的偏移地址。

hello.s文件:

反汇编文件:

3)对于全局变量调用:.s文件中通过全局变量的名字加上%rip的值来调用,而在反汇编代码中通过0x0加上%rip的值来调用。因为在.s文件中并没有给出全局变量地址的信息,所以只能通过变量名来标记;在反汇编代码中,已经对全局变量作出了定位处理,当后续操作进行链接后就可以给出绝对地址了。

hello.s文件:

反汇编文件:

4.5 本章小结

汇编器对hello.s文件进行汇编,生成了可重定位文件。对文件的全局变量,函数,程序语句都进行了分析,给出了初始的相对位置信息,相当于对整个文件做出了一个初始的整理,为后面的链接等操作做准备。

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

(第4章1分)


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-1链接截图

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

执行命令:readlf -a hello

得到Section header的信息截图如下,其中offset为各section的在程序中的地址偏移量,size为各段的大小,基本信息与可重定位文件相似,见4.3节。这里的Section header与可重定位文件有一个明显的区别就是address不再都是0了,被分配了相应的虚拟空间的地址。

图5-2 hello的ELF文件section header

5.4 hello的虚拟地址空间

通过view->memory regions开业查看程序运行各个段的地址。

第一段0x400000~0x401000为1~11段的地址

第二段0x401000~0x402000为12~16段的地址

第三段0x402000~0x403000为17~28段的地址。

第四段0x403000~0x405000为19~22段的地址。

剩下的.so为共享库的地址,还有[stack]堆栈的地址。

在0x400000~0x405000段中,程序被载入,自虚拟地址0x400000开始,自0x404fff结束,这之间每个节(开始 ~ .data节)的排列即开始结束同图5.2中Address中声明。

  图5-3 hello程序的虚拟内存区域

5.5 链接的重定位过程分析

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

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

与hello.o反汇编文本helloo.objdump相比,在hello.objdump中多了许多节,列在下面。通过比较hello.objdump和helloo.objdump了解链接器。

  1.函数个数:在使用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。链接器将上述函数加入。

2.函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。

 3.  rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

图5-4 初始化函数_init反汇编代码

图5-5 .plt函数反汇编代码

图5-6  hello调用库函数的反汇编代码

图5-7 _start程序(调用hello.c中的main函数)反汇编代码

我们来看main函数反汇编代码,他调用的函数都已经在hello的反汇编代码中显示的定义了出来,有地址有名称,可以直接call这个函数,跳转到函数的地址。他调用的rodata也是可以直接跳转到具体地址的。

图5-8 main函数反汇编代码

    

图5-9 调用的动态库的反汇编代码。

5.6 hello的执行流程

  1. 在edb中加载hello可执行文件
  2. 列出所有过程(终端输入 hello 1190301610 wangjiaqi)

子程序名

地址

ld-3.31.so!_dl_start

7efbff4d8ea0

ld-3.31.so!_dl_init

7efbff4e7630

Hello!_start

400500

libc-3.31.so!_libc_start_main

7efbff100ab0

Hello!printf@plt(10)

4004c0

Hello!sleep@plt(10)

4004f0

Hello!getchar@plt

4004d0

Libc-3.31.so!exit

7efbff122120

  1. 列出所有过程(未输入)

子程序名

地址

ld-3.31.so!_dl_start

7efbff4d8ea0

ld-3.31.so!_dl_init

7efbff4e7630

Hello!_start

400500

libc-3.31.so!_libc_start_main

7efbff100ab0

Hello!puts@plt

4004b0

Hello!exit@plt

4004e0

5.7 Hello的动态链接分析

  对于动态共享库里面的PIC函数,编译器没有办法预测函数运行时候的地址,所以需要为他天骄重定位记录,并等待动态链接器来处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存在函数目标地址,PLT使用GOT中的地址跳转到目标函数。

5.8 本章小结

本章详细讲述了hello在执行的时候的链接操作,包括链接的概念,作用,在Unbuntu下怎么链接。并对hello的ELF文件进行了详细的解析,同时结合了虚拟地址的空间知识。通过反汇编hello文件,将它与hello.o反汇编文件进行对比,加深了对重定位的理解。最后对hello的动态链接过程进行了分析。

(第5章1分)

6hello进程管理

6.1 进程的概念与作用

进程的概念:进程是操作系统一个正在运行的程序的一种抽象。程序在系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器,主存和I/O设备。处理器看上去就像在不间断地一条接一条的执行程序中的指令。

进程的作用:通过进程的概念,系统可以实现多线程,并发运行等操作。

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

shell作为父进程通过fork函数为hello创建一个新的进程,供其执行。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本。

处理流程:shell 执行一系列的读/求值(read /evaluate ) 步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

6.3 Hello的fork进程创建过程

Shell通过调用fork 函数创建一个新的运行的子进程。也就是Hello程序,Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Sehll和Hello进程之间最大的区别在于它们有不同的PID。

6.4 Hello的execve过程

创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。

在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。

6.5 Hello的进程执行

创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。

在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。

6.5 Hello的进程执行

逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

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

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

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

hello程序执行过程中存储时间分片,与操作系统的其他进行并发运行。并发执行涉及到操作系统内核采取的上下文交换策略。内核为每个进程维持一个上下文,上下文就是内核重新启动一个先前被抢占的进程所需的状态。

在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。

在此基础上,hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。

程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再及时改用户态,从而保证系统的安全与稳定。

6.6 hello的异常与信号处理

  Ctrl-C命令,hello被终止,如下图。

图6-4  Ctrl-C

  Ctrl-z命令,hello被暂停,利用ps查看,还可以看到hello仍在进程里面。

图6-5  Ctrl-z后运行ps

Ctrl-z命令,hello被暂停,利用jobs查看,还可以看到hello状态为stopped。

图6-6  Ctrl-z后运行jobs

Ctrl-z命令,hello被暂停,执行fg,hello继续执行,jobs里面没有hello,hello被父进程回收。

图6-7  Ctrl-z后运行fg

6.7本章小结

在本章中hello已经正式上岗工作运行了,这都多亏于“进程”这个概念的提出。进程给hello提供了抽象的概念,使得进程能够有条不紊的并发执行。各个进程之间也不会产生严重的矛盾和冲突,是进程使得不同的程序能在相安无事的运行直到结束。

本章主要介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间搭建桥梁,介绍了fork函数和execve函数在调用程序中的作用,程序的异常如何处理,不同进程之间是如何进行上下文切换等等内容。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

物理地址:物理地址是内存单元的绝对地址,与地址总线具有对应关系。无论CPU如何处理地址,最终访问的都是物理地址。CPU实模式下段地址+段内偏移地址即为物理地址,CPU可以使用此地址直接访问内存。物理地址的大小决定了内存中有多少个内存单元,物理地址的大小由地址总线位宽决定。

线性地址(虚拟地址):CPU在保护模式下,“段基址+段内偏移地址”为线性地址,如果CPU在保护模式下未开启分页功能,线性地址将被当成物理地址使用。若开启了虚拟分页功能,线性地址等同于虚拟地址,此时虚拟地址需要通过页部件电路转化为最终的物理地址。虚拟地址是CPU由N=2n个地址空间中生成的,虚拟地址即为虚拟空间中的地址。

逻辑地址:无论cpu在什么模式下,段内偏移地址又称为有效地址/逻辑地址

Hello中的指令地址都是16位的虚拟地址,在程序中虚拟地址和逻辑地址没有明显的界限。

逻辑地址转换成线性地址(虚拟地址),由段式管理执行的

线性地址转换成物理地址,是由页式管理执行的

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

一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由16位字段组成,前13位为索引号。

索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前13位,再这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。

段描述符中的base字段,描述了段开始的线性地址,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由T1字段决定使用哪个。

以下是具体的转化步骤:

1.给定一个完整的逻辑地址。

2.看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小。

3.取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址。

4.线性地址等于基地址加偏移。

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

分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。

为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。

在分页机制转换高速缓存中的数据与页表中数据的相关性,不是由80386处理器进行维护的,而必须由操作系统软件保存,也就是说,处理器不知道软件什么时候会修改页表,在一个合理的系统中,页表只能由操作系统修改,操作系统可以直接地在软件修改页表后通过刷新高速缓存来保证相关性。高速缓存的刷新通过装入处理器控制寄存器CR3完成。

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

TLB可以理解为页表的一个小的,虚拟寻址的高速缓存(类似cache是内存的一个缓存),用于组选择和行匹配的索引和标记字段是从虚拟地址的页号中提取出来的,如下图所示,如果TLB有2^t组,TLB索引(TLBI)由VPN的t个低位组成的,TLB标记(TLBG)是由VPN中剩余的位组成的。

图7- TLB寻址

为了减少页表在内存空间的占用,引入了多级页表的机制,k级页表示意图如下。如果虚拟内存不存在,则不分配页表来记录这段内存。

1级页表指向2级页表,2级页表存储的是3级页表基地址,只有最后一级页表的内容是PTE和物理页号。

图7- K级页表示意图

前提如下:虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。

解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。

如图 ,CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。 如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

图7- TLB与四级页表支持下的VA到PA的变换

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

前提:只讨论L1 Cache的寻址细节,L2与L3Cache原理相同。L1 Cache是8路64组相联。块大小为64B。

解析前提条件:因为共64组,所以需要6bit CI进行组寻址,因为共有8路,因为块大小为64B所以需要6bit CO表示数据偏移位置,因为VA共52bit,所以CT共40bit。

在上一步中我们已经获得了物理地址VA,使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。

如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。

图7- 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。

为新进程创建虚拟内存。

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

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

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

在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存。

随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

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

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

3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

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

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault) 。图7-8展示了在缺页之前我们的示例页表的状态。CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。缺页处理程序从磁盘上用VP3的副本取代VP4,在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常。

7.9动态存储分配管理

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

分配器分为两种:显式分配器、隐式分配器。显式分配器:要求应用显式地释放任何已分配的块,例如malloc()函数。隐式分配器(垃圾收集器):要求分配器检测一个已分配块何时不再使用,那么就释放这个块;自动释放未使用的已经分配的块的过程叫做垃圾收集。

隐式空闲链表:所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中Header和Footer中的block大小间接起到了前驱、后继指针的作用。

图7-  隐式空闲链表结构

涉及到的操作包括查找空闲块,malloc一个块,合并空闲块等等。其中比较复杂的就是合并空闲块这个操作,因为合并空闲块,不仅要看此块之后是不是空闲块,还要看之前是不是空闲块,这也是加入头部和角标的原因之一。对于此块前后是否为空闲块,分为以下四种情况,对每种情况要分别考虑。

图7-  隐式空闲链表合并空闲块

7.10本章小结

本章着重介绍了被许多现代操作系统所采用的虚拟内存系统,即访存时地址需要从逻辑地址翻译到虚拟地址并进一步翻译成物理地址,并详细阐述了在TLB和四级页表支持下VA到PA的转换,以及得到了PA后,三级cache下的物理内存的访问过程,最后还讨论了malloc堆区的动态内存分配

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:

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

设备管理:unix io接口

设备管理方法:一个linux文件就是一个m个字节的序列:B0 , B1 , … , Bk , … , Bm-1所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

(Unix I/O接口:

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

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

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

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

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

Unix I/O函数:

1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:

int open(char *filename, int flags, mode_t mode);

open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。

返回:若成功则为新文件描述符,若出错为-1。

2. 进程通过调用close 函数关闭一个打开的文件。

int close(int fd);

返回:若成功则为0, 若出错则为-1。

3. 应用程序是通过分别调用read write 函数来执行输入和输出的。

ssize_t read(int fd, void *buf, size_t n);

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

返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。

ssize_t write(int fd, const void *buf, size_t n);

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

返回:若成功则为写的字节数,若出错则为-1。

8.3 printf的实现分析

  1. 首先观察printf函数的实现

    

我们发现printf函数中,定义了一个字符指针va_list类型的arg,这个函数 调用了vsprintf,继续看一下他:

函数主要实现的功能:格式化。接受确定输出格式的字符串fmt,勇哥是字符串对个数变化参数进行格式化,产生格式化输出。

  1. 系统函数write

反汇编write函数,发现要调用INT_VECTOR_SYS_CALL,他通过系统调用sys_call这个函数。

再来看sys_call这个函数,通过分析,知道这个函数的主要功能是显示格式化的字符串,将要输出的字符串从总线复制到显卡的显存里面

  1. 字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
  2. 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。我们要显示的“hello 1190301610 王家琪”就被打印输出到了显示器上。

8.4 getchar的实现分析

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

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

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

8.5本章小结

输入/输出(I/O) 是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。Linux本身提供的一些系统函数已经实现了对底层的调用,例如write函数。printf函数正是通过它间接向标准输出这个文件输出内容,它会调用syscall触发中断以内核模式对硬件进行操作。

(第8章1分)

结论

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

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

hello最开始是一段代码,描述了它想要进行的操作,但它却不能实现操作,因为它缺少各种条件,它只有代码。当系统想要执行它的时候,hello就开始了它的一生:

首先先对hello.c源文件进行预处理,将hello需要的头文件中的信息插入到文本中,生成了hello.i文件;为了更好的翻译为机器代码,hello.i文件被汇编为hello.s汇编文件,对于程序中的函数,变量,语句都进行了语法分析;再将汇编文件编译成为hello.o机器代码文件,是一个二进制文件,并且对于程序中的函数,变量做出了最基础的处理,为它们划分了不同的段,储存了不同的信息,包括偏移量,访问权限等等信息;最后将程序调用的子函数进行链接,并且将逻辑地址转化为虚拟地址,形成了可执行文件。这个时候hello就有了可以被执行的条件,但是它还不能执行,因为它没有系统分配的资源,包括它要运行的空间等等。

当在shell中输入命令行执行hello时,shell首先分析参数,然后调用fork()函数为它开辟新的子进程,这个时候hello就有了资源:它拥有了和父进程完全一样但独立的用户虚拟空间。execxe()函数将hello程序加载到进程。子进程执行hello所请求的操作,并且通过调用异常处理子程序处理执行过程的异常。对于hello的储存管理,处理器采用逻辑控制流的抽象来保证hello的执行,并且采用虚拟内存的抽象,好像hello独占整个内存。系统采用页表来管理hello的虚拟地址,hello的虚拟地址要翻译为物理地址才能找到数据,物理地址首先到cache中找,然后再到主存中找,找到后返回给进程。

hello进程和I/O设备有交互,所以调用了printf()函数。系统对I/O设备采用映射文件的管理方式。

最后hello进程执行完操作后,会由父进程来回收,并且将它所占用的资源删除,这时候hello就结束了。它又变成了一段代码。下一次在用它时,开启的进程是不是和上一个进程是一个呢?还是上一个进程已经永远的结束了呢?

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


附件

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

hello.c :hello的c语言源代码

hello.i :预处理后的文本文件

hello.s :hello.i编译后的汇编文件

hello.o :hello.s汇编后的可重定位目标文件

hello.txt:hello.o的elf文件

hello_obj.txt:hello.o的反汇编结果文件

hello :helllo.o与预编译文件链接后的可执行文件

hello_e.txt:可执行文件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分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值