计算机系统大作业

摘  要

本文主要阐述hello程序在Linux系统的生命周期,借助edb、gcc等工具探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。同时比较全方面的涉及了Hello程序在其生命周期中可能出现的特殊情况以及处理方法等。

关键词:程序;进程;计算机系统;shell;预处理;编译;汇编;                          

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

目  录

第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,020的整个过程。

Hello的P2P(From Program to Process)过程:在文本编辑器或IDE中编写C语言代码,得到最初的hello.c程序,即最初的Program。编译器驱动程序代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。驱动程序首先运行C预处理器(cpp),将C的源程序hello.c翻译成一个ASCII码的中间文件;然后运行C编译器(cc1)将中间文件翻译成一个ASCII汇编语言文件;之后运行汇编器(as)将汇编语言文件翻译成可重定位目标文件;最后运行链接器(ld)创建一个可执行目标文件hello。在shell中输入执行hello的命令,shell解析命令行,通过fork新建一个子进程来执行hello,这时Hello已经从Program转换为Process了。

Hello的020(From Zero-0 to Zero-0)过程:子进程调用execve,重新为hello进行内存映射,设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。进入程序入口后通过存储管理机制将指令和数据载入内存,CPU以流水线形式读取并执行指令,执行逻辑控制流。操作系统负责进程调度,为进程分时间片。执行过程中通过L1、L2、L3高速缓存、TLB、多级页表等进行存储管理,通过I/O系统进行输入输出。当程序运行结束后,shell回收hello进程,删除和该进程相关的内容,这时hello进程就不存在了。hello从开始的未被内存映射到运行再到回收后不再存在,就是020的过程。

2 环境与工具

    硬件环境

X64 CPU;2.60GHz;16G RAM;256GHD Disk

软件环境

Windows10 64位

开发工具

VM VirtualBox 6.1;Ubuntu 20.04 LTS 64位;

Visual Studio 2022 64位;CodeBlocks 17.12 64位;vi/vim/gedit+gcc

1.3 中间结果

hello.c 源程序

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

hello1.txt hello.o的反汇编代码

hello.i 预处理后文件

hello       链接后的可执行文件

hello2.txt   hello的反汇编代码

hello.s      编译后的汇编文件

hello.elf     hello.o的ELF格式

hello1.elf    hello的ELF格式

1.4 本章小结

     本章简述了Hello的P2P、020的整个过程并介绍了实验的基本信息:环境、工具以及实验的中间结果。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译,预处理中会展开以#起始的行,修改原始的C程序。将所引用的所有库展开,处理所有的条件编译,并执行所有的宏定义,得到另一个通常是以.i作为文件扩展名的C程序。

预处理的作用:

1.宏的替换。将宏名(#define定义的字符串)替换为实际值(可以是字符串、代码等)。

2.文件包含。将c程序中所有#include声明的头文件复制到新的程序中。将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。

3.条件编译。根据#if以及#endif和#ifdef以及#ifndef来判断是否处理之后的代码。

2.2在Ubuntu下预处理的命令

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

2.3 Hello的预处理结果解析

 hello.i文件部分截图如下:

结果分析:

经过预处理后,hello.c被处理成为hello.i文件。打开文件后发现hello.i文件中文件内容大大增加,且仍为可阅读的c语言程序文本文件。hello.i文件对hello.c程序中的宏进行了宏展开,该文件包含了头文件中的内容。如果代码中有#define命令还会对相应符号进行替换。

2.4 本章小结

本章主要介绍了预处理的概念及其作用,给出了在Linux下预处理的指令,接着给出了hello.i文件的分析,了解了实际上一个预处理文件是怎样的,有了更加深入的理解

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译的概念

编译就是将源语言经过词法分析、语法分析、语义分析以及经过一系列优化后生成汇编代码的过程。这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

编译的作用

编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。

1.词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。

2.编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。编译程序的语法规则可用上下文无关文法来刻画。

3.中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。

4.代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。

5.目标代码生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。    

3.2 在Ubuntu下编译的命令

编译的命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1汇编初始部分

节名称        作用

.file           声明源文件

.text           代码节

.section.rodata   只读数据段

.globl          声明全局变量

.type           声明一个符号是函数类型还是数据类型

.size           声明大小

.string          声明一个字符串

.align           声明对指令或者数据的存放地址进行对齐的方式

3.3.2 数据

1)字符串

程序中有两个字符串,这两个字符串都在只读数据段中,如图所示:

hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起点存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf函数。

如图所示,这两个字符串作为printf的参数。

2)局部变量

main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i放入堆栈中。i被放置在栈中-4(%rbp)的位置,如下图所示。

3)参数argc

参数argc是作为用户传给main函数的参数。同样被放置到堆栈之中了。

4)数组char*argv[]

char*argv[]是main函数的第二个参数,数组的起始地址存放在-32(%rbp)的位置,数组中的每一个元素都是一个指向字符类型的指针,在内存中被两次调用穿给printf函数。

5)立即数

立即数直接体现在汇编代码中。

3.3.3全局函数

hello.c声明了一个函数int main(int argc,char *argv[]),通过阅读汇编代码我们发现此函数是一个全局函数,如图所示。

3.3.4赋值操作

hello中的赋值操作主要有:i=0,而这个操作在.s文件中汇编代码主要使用mov指令来实现。mov指令根据操作数的字节大小分为:

movb:一个字节赋值,movw:两个字节赋值,

movl:四个字节赋值,movq:八个字节赋值。

3.3.5算术操作

hello.c中的算数操作主要有i++,i是int类型,在汇编代码中用addl实现此操作。

3.3.6关系操作

1)hello.c中 “if(argc!=4)”,这是一个条件判断语句,在进行编译时,被编译为:cmpl$4, -20(%rbp)。比较后设置条件码,根据条件码判断是否需要跳转。

  1. hello.c源程序中的for循环条件是for(i=0;i<8;i++),该条指令被编译为cmpl$7,-4(%rbp)。同样在判断后设置条件码,为下一步的jle利用条件码跳转做准备。 

3.3.7控制转移指令

汇编语言中设置了条件码,然后根据条件码来进行控制程序的跳转。通过阅读hello.c 的汇编代码,我们发现有如下控制转移指令。

1)判断argc是否等于4。如果等于4,则不执行if语句;反之if不等于4,则执行后续的语句,对应的汇编代码为:

2 ) for循环中,每次都要判断i是否小于8来决定是否继续循环。先对i进行赋初值,然后无条件跳转至判断条件的.L3中,然后判断i是否符合循环的条件,若符合则直接跳转到.L4中。这一部分的汇编代码为:

3.3.8函数操作

hello.c中涉及的函数操作主要有以下几个:main,printf,exit,sleep,和getchar函数。main函数的参数是argc和*argv,printf函数的参数是字符串,exit函数的参数是1,sleep函数的参数是atoi(argv[3])。所有函数的返回值都会存储在%eax寄存器中。函数的调用与传参的过程是给函数传递参数需要先设定一个寄存器,将参数传给这个设定的寄存器后,再通过call来跳转到调用的函数开头的地址。

3.3.9类型转换

hello.c中的atoi(argv[3])将字符串类型转换为整形。int、float、double、short、char之间可以进行相互转化。

3.4 本章小结

本章主要介绍了编译的概念及其作用,以及在linux下编译的指令,最后我们根据hello.c文件的编译文件hello.s文件中的汇编代码详细的解析了数据类型,各类操作是如何实现的。经过这一章,最初的hello.c已经被转换成了更底层的汇编程序。(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

1.汇编的概念

汇编器as,将.s文件翻译成机器指令,也即.o文件,这一过程称为汇编,同时这个.o文件也是可重定位目标文件。

2.汇编的作用

将编译器产生的汇编语言进一步翻译为计算机可以理解的机器语言,生成.o文件。

4.2 在Ubuntu下汇编的命令

命令为:gcc -c -o hello.o hello.s

4.3 可重定位目标elf格式

    

典型的ELF可重定位目标文件

在linux下生成hello.o文件的elf格式命令:readelf -a hello.o > hello.elf

分析hello.elf中的内容:

  1. ELF头 :ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

  1. 节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

 3)重定位节:重定位节保存的是.text节中需要被修正的信息(任何调用外部函数或者引用全局变量的指令都需要被修正),调用外部函数的指令和引用全局变量的指令需要重定位,调用局部函数的指令不需要重定位。Hello程序中需要被重定位的有printf、puts、exit、sleep、sleepseces、getchar和.rodata中的.L0和.L1。

.rela.eh_frame节是.eh_frame节的重定位信息。

4 ) 符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o>hello1.txt

 

结果解析:与hello.s的差异

1)分支转移

hello.s

hello1.txt

反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

2)对函数的调用与重定位条目对应

hello.s

hello1.txt

在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在反汇编文件中可以看到,call后面直接加的是偏移量。

3)立即数变为十六进制格式

hello.s

hello1.txt

在编译文件中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。

4.5 本章小结

本章主要介绍了汇编文件hello.o,以及汇编的概念与作用,如何得到汇编文件的操作命令。同时对elf文件做了详细的分析,也比对了hello.o的反汇编文件与之前得到的hello.s文件,使得我们对这部分内容有了更加深入的理解。

(第4章1分)


5链接

5.1 链接的概念与作用

1.链接的概念:

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件 可被加载到内存并执行。

2.链接的作用:

链接器使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨 大的源文件,更便于我们维护管理,可以独立的修改和编译我们需要修改的小 的模块。

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

命令:readelf -a hello > hello1.elf

  1. ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节。

 

2)节头:对 hello中所有的节信息进行了声明,包括大小和偏移量。

3) 重定位节.rela.text:

4 )符号表.symtab:

5.4 hello的虚拟地址空间

    分析程序头LOAD可加载的程序段的地址为0x400000

  

通过edb加载hello程序,打开Data Dump查看hello加载到虚拟地址的状况,并查看各段信息。

在0x401000~0x402000段中,程序被载入,虚拟地址0x401000开始,到0x401ff0结束,根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145。

5.5 链接的重定位过程分析

命令: objdump -d -r hello > hello2.txt

与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000。hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。

hello的重定位过程:

1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。

3)重定位条目当编译器遇到对最终位置未知的目标引用时,它会生成一个重定位条目。代码的重定位条目放在.rel.txt中。

5.6 hello的执行流程

1.开始执行:_start、_libc_start_main

2.执行main:_main、_printf、_exit、_sleep、_getchar

3.退出:exit

程序名 程序地址

_start 0x4010f0

_libc_start_main 0x2f12271d

main 0x401125

_printf 0x401040

_exit 0x401070

_sleep 0x401080

_getchar 0x401050

5.7 Hello的动态链接分析

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

.got.plt起始表的位置为0x404000。

GOT表调用dl_init前0x404008后的16个字节均为0;

调用dl_init后的.got.plt:

从图中可以看到.got.plt的条目已经发生变化。

5.8 本章小结

本章主要介绍了链接的概念及作用,以及生成链接的命令,对hello的elf格式文件进行了深入的分析,同时也分析了hello的虚拟地址空间,重定位过程,遍历了整个hello的执行过程,并且比较了hello.o的反汇编和hello的反汇编。对于链接有了更加深入的理解

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

进程的概念:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

进程的作用:

它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

shell-bash的作用:是一种交互型的应用级程序,是Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。

shell-bash的处理流程:

(1)读取用户输入的命令行。

(2)分析命令行字符串,若是内置命令,则立即执行

 (3)如果不是内置命令,则调用fork()创建新子进程,再调用execve()执行指定程序

6.3 Hello的fork进程创建过程

在终端中输入./hello 学号 姓名 1命令后,shell会处理该命令,判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是虚拟地址独立、PID也不相同的一份副本。

6.4 Hello的execve过程

调用fork函数之后,子进程将会调用execve函数,来运行hello程序,如果成功调用则不再返回,若未成功调用则返回-1。

完整的加载运行hello程序需要以下几个步骤

1.首先加载器会删除当前子进程虚拟地址端。,然后创建一组新的代码、数据、堆端,并初始化为0。

2.接着映射私有区域和共享区域,将新的代码和数据段初始化为可执行文件中的内容

3.最后设置程序计数器,使其指向代码区的入口,下一次调度这个进程时,将直接从入口点开始执行

6.5 Hello的进程执行

 6.5.1 逻辑控制流和时间片:

进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。

 6.5.2用户模式和内核模式:

 Shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。

 6.5.3上下文切换

 如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。

 6.5.4调度

 在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。

 6.5.5用户态与核心态转换

 为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。

 核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

1. 可能出现的异常

(1) 中断:来自I/O设备的信号。比如输入CTRL -C或者CTRL-Z

(2) 陷阱:有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。

(3)故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

2. 可能产生的信号

SIGINT,SIGSTP,SIGCONT,SIGWINCH等等

3.各种处理的分析

(1)正常运行,程序结束后,被正常回收

2.按下ctrl-c,此举会给进程发送SIGINT信号,程序将被终止回收

3.运行过程中按下CTRL-Z,此举会给进程发送SIGSTP信号,hello程序将被挂起,用ps命令可以看到hello进程并没有回收

4.不停乱按

6.7本章小结

本章主要介绍了进程的概念及其作用,对shell的功能和处理流程也进行了介绍,然后详细分析了hello程序从fork进程的创建,到execve函数执行,最后具体执行过程以及出现异常的处理。对于整个进程管理有了更加深入的理解。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

1)逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。

2)线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。

3)虚拟地址:就是线性地址。

4)物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

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

逻辑地址由段标识符和段内偏移量两部分组成。段标识符由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,是对段描述符表的索引,每个段描述符由8个字节组成,具体描述了一个段。后3位包含一些硬件细节,表示具体是代码段寄存器还是栈段寄存器还是数据段寄存器等。通过段标识符的前13位,可以直接在段描述符表中索引到具体的段描述符。每个段描述符中包含一个Base字段,它描述了一个段的开始位置的线性地址。将Base字段和逻辑地址中的段内偏移量连接起来就得到转换后的线性地址。

对于全局的段描述符,放在全局段描述符表中,局部的(每个进程自己的)段描述符,放在局部段描述符表中。全局段描述符表的地址和大小存放在gdtr控制寄存器中,而局部段描述符表存放在ldtr寄存器中。

给定逻辑地址,看段选择符的最后一位是0还是1,用于判断选择全局段描述符表还是局部段描述符表。再根据相应寄存器,得到其地址和大小。通过段标识符的前13位,可以在相应段描述符表中索引到具体的段描述符,得到Base字段,和段内偏移量连接起来最终得到转换后的线性地址。

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

线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

为了减少页表过大导致的空间浪费,我们采用多级页表来压缩大小

图7.4.1多级页表

在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。

图7.4.2 四级页表翻译

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

当MMU完成了从虚拟地址到物理地址的转换后,就可以使用物理地址进行内存访问了。Intel Core i7使用了三级cache来加速物理内存访问,L1级cache作为L2级cache的缓存,L2级cache作为L3级cache的缓存,而L3级cache作为内存(DRAM)的缓存。

进行物理内存访问时,会首先将物理地址发送给L1级cache,看L1级cache中是否缓存了需要的数据。L1级cache共64组,每组8行,块大小64B。因此将物理地址分为三部分,块偏移6位,组索引6位,剩下的为标记位40位。首先利用组索引位找到相应的组;然后在组中进行行匹配,对于组中的8个行,分别查看有效位并将行的标记位与物理地址的标记位匹配,当标记位匹配且有效位是1时,缓存命中,根据块偏移位可以直接将cache中缓存的数据传送给CPU。如果缓存不命中,需要继续从存储层次结构中的下一层中取出被请求的块,将新块存储在相应组的某个行中,可能会替换某个缓存行。

L1级cache不命中时,会继续向L2级cache发送数据请求。和L1级cache的过程一样,需要进行组索引、行匹配和字选择,将数据传送给L1级cache。同样L2级cache不命中时,会继续向L3级cache发送数据请求。最后,L3级cache不命中时,只能从内存中请求数据了。

值得注意的是,三级cache不仅仅支持数据指令的访问,也支持页表条目的访问,在MMU进行虚拟地址到物理地址的翻译过程中,三级cache也会起作用。

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

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。内核给新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。同时延迟私有对象中的副本直到最后可能的时刻,充分利用了稀有的物理内存。

7.7 hello进程execve时的内存映射

exceve()函数在当前进程的上下文中加载并运行我们需要的hello程序。execve函数加载并运行可执行文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。

execve函数用hello程序有效替代当前程序,需要以下几个步骤:

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

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

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

(4)设置程序计数器。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

当内核调度这个进程时,它就将从这个入口点开始执行。Linux根据需要换入代码和数据页面。

图7-7 excecve时的内存映射

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

指令引用一个虚拟地址,传给MMU,MMU在查找页表时,发现对应的物理地址并不在内存中,此时便发生了异常,内核中的缺页异常处理程序会先选择一个牺牲页面,如果这个页面已经牺牲过了则会把它换到磁盘,换入新的页面并更新页表。之后返回原来的进程,再次执行引起缺页的命令,此时已经可以正常运作了

图7-8 缺页异常处理

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但不失通用性,假设堆是一个请求二进制零的区域,紧接在未初始化数据区域后开始,向上生长。对每个进程,内核维护一个全局变量brk指向堆顶。分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留,供应用程序使用;空闲块可用来分配。空闲块保持空闲,直到空闲块显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的(即显式分配器),要么是内存分配器自身隐式执行的(即隐式分配器)。显式分配器和隐式分配器是动态内存分配器的两种基本风格。两种风格都要求应用显式地分配块,不同之处在于由哪个实体来负责释放已分配的块。显式分配器要求应用显式地释放任何已分配的块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。

图7-9.1  动态内存分配的区域-堆

显式分配器必须在一些约束条件下工作:处理任意请求序列;立即响应请求;只使用堆;对齐要求;不修改已分配的块。在这些限制条件下,分配器试图实现吞吐率最大化和内存使用率最大化,但这两个性能目标通常是相互冲突的。

分配器的具体操作过程以及相应策略:

(1)放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。执行这种搜索的常见策略包括首次适配、下一次适配和最佳适配等。

(2)分割空闲块:一旦分配器找到了匹配的空闲块,需要决定分配这个空闲块中多少空间。可以选择用整个块,但会造成额外的内部碎片;也可以选择将空闲块分割为两部分,第一部分变成已分配块,剩下的变成新的空闲块。

(3)获取额外的堆内存:如果分配器不能为请求块找到空闲块,分配器通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插到空闲链表中,然后被请求的块放在这个新的空闲块中。

(4)合并空闲块:分配器释放一个已分配块时,要合并相邻的空闲块。分配器决定何时执行合并,可以选择立即合并或者推迟合并。合并时需要合并当前块和前面以及后面的空闲块。

组织空闲块的形式有很多,包括隐式空闲链表、显式空闲链表、分离的空闲链表等等。

带边界标签的隐式空闲链表分配器:一个块由一个字的头部、有效载荷、可能的一些额外的填充以及一个脚部。头部位于块的开始,编码了这个块的大小(包括头部、脚部和所有的填充)以及这个块是已分配的还是空闲的。由于对齐要求,头部的高位可以编码块的大小,而剩余的几位(取决于对齐要求)总是零,可以编码其他信息。使用最低位作为已分配位,指明这个块是已分配的还是空闲的。脚部位于每个块的结尾,是头部的一个副本,是为了方便释放块时的合并操作。头部后面就是调用分配器时请求的有效载荷,有效载荷后面是一片不使用的填充块,其大小可以是任意的。填充的原因取决于分配器的策略。如果块的格式是如上所述,就可以将堆组织成一个连续的已分配块和空闲块的序列,这种结构为隐式空闲链表。空闲块通过头部的大小字段隐含地连接,可以通过遍历堆中所有的块间接遍历整个空闲块的集合。同时,需要一个特殊标记的结束块(设置分配位而大小为零的头部),这种设置简化了空闲块合并。

图7-9.2 隐式链表的块结构

显式空间链表:已分配块的块结构和隐式链表的相同,由一个字的头部、有效载荷、可能的一些额外的填充以及一个脚部组成。而在每个空闲块中,增加了一个前驱指针和后继指针。通过这些指针,可以将空闲块组织成一个双向链表。空闲链表中块的排序策略包括后进先出顺序、按照地址顺序维护、按照块的大小顺序维护等。显式空闲链表降低了放置已分配块的时间,但空闲块必须足够大,以包含所需要的指针、头部和脚部,这导致了更大的最小块大小,潜在提高内部碎片程度。

图7-9.3  显式链表的块结构

而malloc采用的是分离的空闲链表。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小升序排列,当分配器需要一个大小为n的块时,就搜索相应大小类对应的空闲链表。如果不能找到合适的块,就搜索下一个链表,以此例推。

7.10本章小结

本章主要介绍了hello的存储地址空间,段式管理,页表管理,TLB与四级页表支持下的VA到PA的变换,三级cache支持下的物理内存访问,hello进程fork和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理等内容。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

Linux将文件所有的I/O设备都模型化为文件,甚至内核也被映射为文件。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。Linux就是基于Unix I/O实现对设备的管理。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix IO接口:

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

 2.Linux 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.int open(char* filename,int flags,mode_t mode)

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

    2.int close(fd)

    进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。

   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函数:

引用的vsprintf函数:

vsprintf函数将所有的参数内容格式化之后存入buf,返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

1.getchar函数运行时,控制权会交给os,用户按键,输入的内容便会显示在屏幕上。按下回车键表示输入完成,这时控制权将被交还给程序。

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

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

8.5本章小结

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

(第8章1分)

结论

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

1.预处理:hello.c预处理到hello.i文本文件;

2.编译:hello.i编译到hello.s汇编文件;

3.汇编:hello.s汇编到二进制可重定位目标文件hello.o;

4.链接:hello.o链接生成可执行文件hello;

5.创建子进程:bash进程调用fork函数,生成子进程;

6.加载程序:execve函数加载运行当前进程的上下文中加载并运行新程序hello;

7.访问内存:hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象;

8.交互:hello的输入输出与外界交互,与linux I/O息息相关;

9.终止:hello最终被shell父进程回收,内核会收回为其创建的所有信息。

感悟:

一个简简单单的hello程序背后,是设计者庞大而缜密的设计实现。一个个的步骤是如此的精密而准确,不得不让人去感叹计算机的精巧,同时通过这次作业也更加加深了我对计算机系统的理解,让我大受震撼。

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


附件

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

hello.c                  源程序

hello.i                  预处理后文件

hello.s                  编译后的汇编文件

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

hello                   链接后的可执行文件

hello.elf                 hello.o的ELF格式

hello1.txt                hello.o的反汇编

hello2.txt                hello的反汇编代码

hello1.elf                hello的ELF格式

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


参考文献

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

[1]  RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.

[2]  https://www.runoob.com/linux/linux-comm-pstree.html

[3]  https://www.runoob.com/cprogramming/c-function-vsprintf.html

[4]  https://www.cnblogs.com/diaohaiwei/p/5094959.html

[5]  https://blog.csdn.net/yueyansheng2/article/details/78860040

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值