哈工大计算机系统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 120L021529
班   级 2003002
学 生 岳野    
指 导 教 师 史先俊

计算机科学与技术学院
2022年5月

摘 要

从《hello的自白》中,我们以第一视角生动形象的的感受到了,hello程序的重要程度。从最开始的形成敲入代码形成hello.c文件,一点点通过进程处理,分时间片、存储管理、IO管理一步步得到运行的文件和结果,让我们能够以第一人称视角了解到编译运行的过程。最后,还穿插着自己对于各编程思路的重要开端,P2P(From Program to Process)、O2O(From Zero-0 to Zero-0)。通过学习计算机系统了解了hello的一步步过程,同时此次的大作业更能让我们深入了解体会改进运用这些知识。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理;

第1章 概述

1.1 Hello简介

P2P: From Program to Process从程序到进程
在Linux中,hello.c经过预处理、编译、汇编、链接成为可执行目标程序hello(hello.out),在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。

020: From Zero-0 to Zero-0从零到零
shell为此子进程execve,映射虚拟内存,开始从0写出文本文件hello.c。当我们在Bash里面执行hello的时候,hello被装载进去。进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。最后执行结束后,shell的父进程负责回收hello,而内核来删除相关的数据结构。最后,一切归为0.

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk以上
软件环境:Windows10 64位;VirtualBox;Ubantu 16.04 LTS 64位
开发与测试工具:gcc,vim,edb,readelf,HexEdit

1.3 中间结果

1、hello.i:预处理的文件
2、hello.s:编译生成的汇编文件。
3、hello.o:汇编生成的可重定位的文件。
4、hello:链接生成的可执行文件。
5、link.txt链接生成的可执行文件的编译代码
6、elf.txt汇编生成的可重定位的文件的elf文件
7、elf1.txt链接生成的可执行文件的elf文件。

1.4 本章小结

了解了hello.c文件的执行全过程,一步一步了解了在计算机运行的生成的每一步文件即来源和用途。同时了解了P2P和020的对于hello.c执行过程。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1概念

程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。

2.1.2作用

宏定义指令(#define a b):
对于这种伪指令,预编译将程序中的a用b替换,而字符串常量的 a不被替换。#undef,取消对某个宏的定义,使以后该串的出现不被替换。
头文件包含指令(#include <>):
对于这种伪指令,头文件中一般用宏定义指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。在需要用到这些定义的C源程序中,加#include语句即可。预编译将把头文件中的定义都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在 /usr/include目录下。在程序中#include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号(“”)。
条件编译指令(#ifdef,#ifndef,#else,#elif,#endifd…):
对于这种伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
特殊符号,预编译程序可以识别一些特殊的符号:
在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

指令:gcc hello.c -E -o hello.i
截图:
在这里插入图片描述

图 2-1
使用后出现hello.i文件
在这里插入图片描述

图 2-2

2.3 Hello的预处理结果解析

截取部分代码,发现代码比hello.c文件多的多。两图相对比。
在这里插入图片描述

图 2-3
其中的3047行开始对应hello.c文件多了3000多行代码。
在这里插入图片描述

图 2-4

2.4 本章小结

我们了解了预处理的概念作用,预处理的具体实现过程。linux下预处理语句gcc hello.c -E -o hello.i。 在ubuntu下实际操作,对hello.c文件进行预处理,得到hello.i文件打开对比hello.c得到预处理的意义。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1概念

利用编译程序从源语言编写的源程序产生目标程序的过程。

3.1.2作用

用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译语言是一种以编译器来实现的编程语言。它不像直译语言一样,由解释器将代码一句一句运行,而是以编译器,先将代码编译为机器码,再加以运行。理论上,任何编程语言都可以是编译式,或直译式的。它们之间的区别,仅与程序的应用有关。

3.2 在Ubuntu下编译的命令

指令:gcc -S hello.i -o hello.s
截图:
在这里插入图片描述

图 3-1
使用后出现hello.s文件

在这里插入图片描述

图 3-2

3.3 Hello的编译结果解析

在这里插入图片描述

图 3-3
1.数据:
常量字符串
在这里插入图片描述

全局(局部)变量
在这里插入图片描述
在这里插入图片描述

图 3-4
等等
2.赋值
在这里插入图片描述

mov即赋值语句
3.类型转换(隐式或显式) unsigned/char/int/long/float/double
在这里插入图片描述

atoi (表示 ascii to integer)是把字符串转换成整型数的一个函数
4.算术操作:+ - * / % ++ – 取正/负± 复合“+=”等
在这里插入图片描述

add加法
在这里插入图片描述

sub减法等等
5.6. 关系操作和控制转移
在这里插入图片描述

je相等
在这里插入图片描述

jle不相等 等等
7.数组
在这里插入图片描述

图 3-5
8.函数操作:
puts 传入LC0的字符串为参数
在这里插入图片描述

exit
在这里插入图片描述

sleep
在这里插入图片描述

getchar
在这里插入图片描述

3.4 本章小结

操作了了编译,一步步展示编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。了解对于编译阶段的计算机的操作。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1概念

汇编执行指令是机器指令的符号化表示,其操作码用记忆符表示,地址码直接用标号、变量名字、常数等表示。汇编执行指令经汇编程序翻译为机器指令,二者之间基本上保持一一对应的关系。汇编伪指令又称作汇编指示,用于向汇编程序提供用户自定义的符号、数据的类型、数据空间的长度,以及目标程序的格式、存放位置等提示性信息,其作用是指示汇编程序如何进行汇编。使用汇编语言编写的源代码,需要通过使用相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。

4.1.2作用

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

4.2 在Ubuntu下汇编的命令

指令:gcc hello.s -c -o hello.o
截图:
在这里插入图片描述

图 4-1
使用后出现hello.o文件
在这里插入图片描述

图 4-2

4.3 可重定位目标elf格式

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

输入readelf hello.o -a指令。得到全部信息
可以将其输入到文件elf.txt中。
在这里插入图片描述

图 4-3
elf头的信息是以一个16字节的Magic开始、类别、数据、机器类型。
节头文件:(如下图)
在这里插入图片描述

图 4-4
节头一共14个节,每个节都指定了一个类型,定义了节数据的语义。各节都指定了大小和在二进制文件内部的偏移量,地址部分可用于指定节加载到虚拟地址空间中的位置。标志代表如何访问或处理。
重定位和符号表:![在这里插入图片描述](https://img-blog.csdnimg.cn/5642265bbc874bf49dd5403f6f044115.png在这里插入图片描述

图 4-5

4.4 Hello.o的结果解析

4.4.1objdump

执行objdump -d -r hello.o
在这里插入图片描述

图 4-6
1.在call函数指令中hello.s中显示的是函数的名称,hello.o中显示的是函数的偏移量位置。
2.84行中跳转了明确位置,hello.s只显示了L4

4.4.2机器语言的构成,与汇编语言的映射关系

比较hello.o反汇编的代码和hello.s,二者在语句、语法、语义上基本没有区别,但反汇编代码除了显示汇编代码以外,还会显示机器代码,因此可以将汇编语言转化为机器语言。

分支转移:hello.s跳转指令call直接使用段名称L4跳转,在反汇编代码中显示跳转目标语句的地址。
函数调用:在.s文件call调用函数的函数名,在机器指令中,call调用了函数地址。

4.5 本章小结

本章,实践了从hello.s到hello.o的汇编过程。了解得到的二进制文件具有怎么样的EFL文件格式,也是为了以后的转变为可执行程序做准备工作。
同时,通过比较hello.o反汇编后的汇编语言代码和hello.s汇编代码,更加深刻的理解了对于这的变化过程。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

5.1.1概念

通过链接器将生成的可重定位文件hello.o文件等,将各种代码和数据片段收集并组合成为一个单一文件hello(可执行文件)的过程,这个文件可被加载(复制)到内存并执行,所以hello文件叫做可执行文件。

5.1.2作用

链接将各个模块链接起来,成为一个巨大的的源文件,而各个模块可以进行分离编译。使得不用将一个大型的应用程序组织成为一个巨大的源文件,而是把他分解为更小的,更好管理的模块,可以独立进行修改和编译这些模块。

5.2 在Ubuntu下链接的命令

命令:gcc hello.o -o hello
截图:
在这里插入图片描述

图 5-1
使用命令后得到
在这里插入图片描述

图 5-2

运行:
在这里插入图片描述

图 5-3

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

使用命令readelf -a hello得到信息。
可以将其输入到文件elf1.txt中。

在这里插入图片描述

图 5-4
ELF头与编译后大体相同,但是会发现最后几行的数字发生了变化,是因为链接后添加了新的内容。
在这里插入图片描述

图 5-5
在这里插入图片描述

图 5-6
节头的数量增加了。其他同第四章的信息。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息。
在memory regions找到只读进行dump
在这里插入图片描述

图 5-7

找到了elf头的位置
在这里插入图片描述

图 5-8
对比5.3的magic一样

同理在memory regions找到
在这里插入图片描述

图 5-9
随后在cpu打开
在这里插入图片描述

图 5-10
正好与我们在EDB里面看到的指令装载的地址一样

其他均类似此操作。

5.5 链接的重定位过程分析

执行objdump -d -r hello > link.txt把内容存储到 link.txt文件中。

5.5.1分析hello与hello.o的不同,说明链接的过程。

图为hello的内容
在这里插入图片描述

   图 5-11                               

在这里插入图片描述
图 5-12
1.同第四章对比,hello.o文件里面只有一个函数main,而上图方的hello进行反汇编的结果里面有一整个过程的函数,如:_init函数
2.原来hello.o只有.text段,现在hello不仅有.text段,还有.init段、.plt段、.fini段等等。
3…text段的内容增多了。
4.增加了外部的共享库函数

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

在这里插入图片描述

图 5-13
在hello.o的elf信息
在这里插入图片描述

图 5-14
所以 r.offset = 0x1c r.symbol = .rodata r.type = R_X86_64_PC32 r.addend = -4
在elf1找到.rodata地址0x2000
在这里插入图片描述

main函数的地址是:0x11e9
在这里插入图片描述

需要引用的字符串的地址Addr(r.symbol)=0x2008
在这里插入图片描述

计算过程:
refaddr =Addr(s)+r.offset =0x11e9+0x1c =0x1205 *refptr = (unsigned)(Addr(r.symbol)+r.addend-refaddr) = (unsigned)(0x2008 +(-4) -0x1205) = 0xdff
验证正确
在这里插入图片描述

图 5-15

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start
libc-2.31.so!__libc_start_main
libc-2.31.so!__cxa_atexit
libc-2.31.so!__libc_csu_init
libc-2.31.so!_setjmp
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!sleep@plt
hello!getchar@plt
ld-2.31.so!_dl_runtime_resolve_xsave
ld-2.31.so!_dl_fixup
ld-2.31.so!_dl_lookup_symbol_x
libc-2.31.so!exit

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
首先找到GOT的存放地址:
在这里插入图片描述

图 5-16
运行前
在这里插入图片描述

图 5-17
运行后发现got值变动。
GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址。
GOT[2]指向的目标程序是动态链接器ld-linux.so运行时地址。

5.8 本章小结

本章介绍了链接的概念和作用,对链接后生成的可执行文件hello的elf格式文件进行了分析,分析了hello的虚拟地址空间、重定位过程、执行过程的各种处理操作。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

进程的概念:进程的经典定义就是一个执行中程序的实例。其他的说法还有进程是程序的一次执行,进程是程序及其数据在CPU下顺序执行时所发生的活动,进程是具有独立功能的程序在数据集上运行的过程,它是系统进行资源分配和调度的一个独立单位。

6.1.2作用

进程的作用:系统中的每个进程都是运行在某个进程中的上下文中的。上下文是由程序正确运行所需状态组成的。每次用户通过向shell输入一个可执行目标程序的名字,运行程序时,shell就会创建一个新的进程,然后在新进程的上下文中运行这个可执行程序文件。应用程序也可以创建一个新的进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

6.2.1作用

shell是计算机和用户之间的一个交互程序,负责解释命令,搭建用户,操作系统,内核直接的桥梁。

6.2.2处理流程

1.首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有 符 号 的 变 量 进 行 替 换 。 6 . S h e l l 将 命 令 行 中 的 内 嵌 命 令 表 达 式 替 换 成 命 令 ; 他 们 一 般 都 采 用 符号的变量进行替换。 6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用 6Shell(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:A. 内建的命令;B. shell函数(由用户自己定义的);C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。

6.3 Hello的fork进程创建过程

在shell中输入命令之后,通过判断,入关不是内部指令,就会通过fork创建新的子进程。从fork函数开始以后的代码父子共享,即父进程要执行这段代码,子进程也要执行这段代码。现在很多现实并不执行一个父进程数据段,栈和堆的完全复制。而是采用写时复制技术。这些区域有父子进程共享,而且内核将他们的访问权限改变为只读的。如果父子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一页。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据 复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用 它的页面调度机制自动将页面从磁盘传送到内存。
在这里插入图片描述

图 6-1

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文信息:
进程通过上下文切换,控制流通从一个进程传递到另一个进程。刚开始时,进程在Process A中,并且处于用户模式。之后,进入了内核模式,将相关的变量,栈,环境都设置好,然后切换上下文,切换到Process A中,这样一来就完成了进程的上下文切换。
进程时间片:
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
进程的调度:
进程的调度是由内核代码进行执行的。在进程从一个切换到另一个的时候就叫做进程的调度。这个过程中需要配置相关的变量,栈,环境等,此时,控制权交由内核代码,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
用户态与核心态转换:
用户态是指应用程序在正常执行过程中,拥有的状态,拥有的管理,调用权限仅限于系统允许的部分;但是在遇到故障、中断或陷入系统调用时,就会进行用户态与核心态转换,此时,在内核状态中,程序拥有系统中所有的访问等权限,对于情况进行处理,来保持系统的安全性。
在这里插入图片描述

图 6-2

6.6 hello的异常与信号处理

6.6.1异常

类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
中断:中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任 何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常 称为中断处理程序
陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序 将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一 样的接口,叫做系统调用。
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控 制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故 障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会 终止引起故障的应用程序。
终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。

对于信号出现可能是以下:
在这里插入图片描述

图 6-3

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

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

图 6-4
图1:
运行后crtl+z中断。
此时调用ps指令查看后台进程。
使用fg继续执行该进程

在这里插入图片描述

图 6-5
图2:
使用kill-9结束进程。通过前后对比发现进程结束。
在这里插入图片描述

图 6-6

图3:pstree

crtl+c:
在这里插入图片描述

图 6-7
在输出过程中按下CTRL+C,程序终止,此时调用ps指令查看后台进程,未发现hello程序,说明程序已终止。

乱码:
在这里插入图片描述

图 6-8
乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入。

6.7本章小结

在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:可以认为是CPU在执行程序时候的中间地址。一个逻辑地址,是由一个段标识符加上一个指定段内的相对地址的偏移量。是指有程序产生的与段相关的偏移地址部分。(hello.o)

线性地址:是逻辑地址到物理地址变换之间的中间层。如果逻辑地址对应的是硬件平台段式管理转换前的地址的话,那么线性地址则对应了硬件页式内存的转换前的地址。

虚拟地址:是对于整个内存的抽象描述,相对于物理内存而言,虚拟内存并不是只有一个而与某一空间一一对应的。(1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保留活动区域,并根据需要在磁盘和主存之间传输数据。(2)它为每个进程提供了一致的地址空间,简化了内存的管理。(3)它保护了每个进程的地址空间不受其他进程的损坏。

物理地址:计算机系统的主存被组织为一个由M个连续的字节大小的单元组成的数组。每字节都由一个唯一的物理地址。它也就是出现在CPU外部地址总线上的寻址物理内存的地址信号。

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

1)基本原理:
在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
在这里插入图片描述

图 7-1
在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。
在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。
段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。
程序通过分段划分为多个模块,如代码段、数据段、共享段:
–可以分别编写和编译
–可以针对不同类型的段采取不同的保护
–可以按段为单位来进行共享,包括通过动态链接进行代码共享
这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。
总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。
2)段式管理的数据结构:
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。
进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。
在系统中为每个进程建立一张段映射表
系统段表:系统所有占用段(已经分配的段)。
空闲段表:内存中所有空闲段,可以结合到系统段表中

3)段式管理的地址变换
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址(见图4—5)。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。

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

在这里插入图片描述

图 7-2
1)基本原理:
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
磁盘中的字节都有唯一的虚拟地址,然后虚拟内存被分割成虚拟也。虚拟页的大小P=2p字节。然后有个页表条目PTE的数组,虚拟地址空间中的每个页在页表中都有一个固定偏移量。
在这里插入图片描述

图 7-3
2)页式管理的数据结构:
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。为了完成上述的功能,—个页式系统中,一般要采用如下的数据结构。
进程页表:完成逻辑页号(本进程的地址空间)到物理页面号(实际内存空间,也叫块号)的映射。
每个进程有一个页表,描述该进程占用的物理页面及逻辑排列顺序,如图:
物理页面表:整个系统有一个物理页面表,描述物理内存空间的分配使用状况,其数据结构可采用位示图和空闲页链表。
对于位示图法,即如果该页面已被分配,则对应比特位置1,否置0.

请求表:整个系统有一个请求表,描述系统内各个进程页表的位置和大小,用于地址转换也可以结合到各进程的PCB(进程控制块)里。
在这里插入图片描述

图 7-4
3)页式管理的地址变换:
在页式系统中,指令所给出的地址分为两部分:逻辑页号和页内地址。
原理:CPU中的内存管理单元(MMU)按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址。
逻辑页号,页内偏移地址->查进程页表,得物理页号->物理地址:
上述过程通常由处理器的硬件直接完成,不需要软件参与。通常,操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存:
第一次用来查找页表将操作数的 逻辑地址变换为物理地址;
第二次完成真正的读写操作。
这样做时间上耗费严重。为缩短查找时间,可以将页表从内存装入CPU内部的关联存储器(例如,快表) 中,实现按内容查找。此时的地址变换过程是:在CPU给出有效地址后,由地址变换机构自动将页号送人快表,并将此页号与快表中的所有页号进行比较,而且这 种比较是同时进行的。若其中有与此相匹配的页号,表示要访问的页的页表项在快表中。于是可直接读出该页所对应的物理页号,这样就无需访问内存中的页表。由于关联存储器的访问速度比内存的访问速度快得多。
在这里插入图片描述

图 7-5

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

首先是CPU会产生一个虚拟地址VA,然后,首先我们会根据需要分为VPN和VPO。其中,虚拟页表存储的就是VPN。在多级页表中,如上图,我们首先会在CR3中存储第一级页表的物理地址,然后通过VPN1来定位第一级页表中的具体条目。
在这里插入图片描述

图 7-6
将这个取出来的条目作为第二级页表的首地址,同样的,VPN2就是用来定位第二级页表中的具体条目。
这样循环操作4次,就可以在最后得到PPN的地址值,然后将VPO作为PPO,将两者组合起来,就得到了我们需要的PA。

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

首先,CUP发出的VA地址,在MMU等虚拟地址处理操作完成之后,会得到一个内容就存储在主存中的物理地址PA,此时来访问该内容就会运用到多级的cache访存机制。
首先将PA分为CT(标记位)CS(组号),CO(偏移量)。由CS找到对应的组,然后通过检查标记位判断其中某一行是否有效,用CT判断这一行是否符合要找的那一组。
如果判断都符合,那么我们就可以认为查找成功;如果失败,那么就继续在L2,L3中进行查找。当在其中某一级判断查找成功的时候,就把数据传给CPU同时更新各级cache的cacheline,如果cache满了,就可以采用最近最少使用原则进行换入换出操作。
在这里插入图片描述

图 7-7

7.6 hello进程fork时的内存映射

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的人一个后来进行写操作时,写时复制机制就回创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数进行了以下步骤:
删除已存在的用户区域
删除当前进程虚拟地址的用户部分中已存在的内存区域
映射私有区域
将目标文件hello的代码和初始化的数据映射到.text和.data区
同时,将.bss和栈映射到匿名文件
映射共享区域:
将libc.so等内容映射到共享库里面的映射区域.
设置PC,指向代码区域的入口点
在这里插入图片描述

图 7-8

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

1.首先处理器会产生一个需要调用的虚拟地址VA。
2.然后,MMU单元会生成PTEA,向主存中的页表进行访问页表在对PTEA进行查询,返回这个单元中存储的PTE地址。
3.MMU在读取后发现地址不合法,于是出发处理缺页异常程序会在主存中,选择牺牲页,将磁盘中的需要读入的缺页重写回到主存。
4.完成上述步骤后,重新执行引起缺页异常的步骤,即7号。
在这里插入图片描述

图 7-9

7.9动态存储分配管理

动态内存分配器维护着堆,堆顶指针是brk。有两种风格,一种叫显式分配器,使用malloc和free等;一种叫隐式分配器,也叫垃圾收集器。
显式分配器必须要满足以下条件:1、处理任意请求序列;2、立即响应请求;3、只使用堆;4、对齐块;5、不修改已分配的块。在这些限制条件下,分配器试图实现吞吐率最大化和使用率最大化。吞吐率就是每个单位时间里完成的请求数。内存利用率可以用峰值利用率来衡量,也就是有效载荷占已堆当前当前大小的比值。
造成堆利用率的一个原因是碎片现象。碎片分为内部碎片和外部碎片。你不水平是分配一个已分配块比有效载荷大时发生的。外部碎片是当空闲内存合计一起来满足一个分配请求但没有一个单独的空闲块足够大时发生的。
为了实现一个分配器,必须考虑:1、空闲块的组织;2、放置;3、分割;4、合并。
可以设计一个隐式空间链表,如图所示。在这种情况下,一个块是由字的头部和有效载荷组成的。
在这里插入图片描述

图 7-10
放置已分配的块的策略有首次适配、下一次适配和最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配是从上一次查询家属的地方开始检查空闲块。最简式配检查每个空闲块,选择所需请求的最小空闲块。
分割空闲块通常是把第一部分变成分配块,剩下的变成新的空闲块。
在这里插入图片描述

图 7-11
当合适的空闲块不够的时候将申请额外的堆内存,插入空闲链表中。
合并空闲块,当分配器释放一个分配块时,可能有其他空闲块与这个新释放的空闲块相邻,就必须合并。与下一块合并很简单,但是和链表中的上一块合并很困难,所以提出了边界标记,就是在结尾处增加一个脚部,如下图所示。
还可以使用显式空闲链表。也就是堆可以组织成一个双向空闲链表。使得首次适配的分配时间从块综述的线性时间减少到了空闲块的线性时间。对于释放可以使用后进先出的顺序对链表进行维护。
在这里插入图片描述

图 7-12
对于一个使用单向空闲块链表的分配器为了减少分配时间还可以使用分离存储。就是维护多个空闲链表,一般是将所有可能的块大小分成一些等价类,也叫做大小类。有两种基本方法:1、简单分离存储;2、分离适配。还有一种特例叫伙伴系统。
程序应当使用free来释放堆块。也有一种动态内存分配器叫垃圾收集器,可以自动释放程序不再需要的已分配块。基本想法就是,将内存视为一张可达图,对于不可达的点那么就是垃圾就可以回收。C语言可以使用Mark&Sweep垃圾收集器,但是是保守的,也就是说平衡树方法会保证标记所有根节点可达的节点,但可能不正确地标记实际上不可达的块。

7.10本章小结

学习了解了hello的存储器的地址空间、四种地址空间的差别和地址的相互转换、hello的四级页表的虚拟地址空间到物理地址的转换、三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:
所有的I/O设备都被模型化为文件,包括内核也被模型化和映射为文件。
设备管理,unix io接口:
首先这种unix io接口是所有文件管理方法中最底层的,最基础的,最安全的方法。保障了异步信号的安全。这是由Linux内核支持的应用接口。其中包括操作:
打开文件:open
关闭文件:close
读操作:read
写操作:write
指定文件中位置:lseek

8.2 简述Unix IO接口及其函数

Unix I/O 接口:
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 I/O 函数:
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的实现分析

https://www.cnblogs.com/pianist/p/3315801.html
如链接中所写:
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;
}
这个函数的接口情况如下:
int vsprintf(char *buf, const char *fmt, va_list args)
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
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;
}
当使用getchar时,程序发生陷阱的异常。当按键盘时会产生中断。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
getchar函数有两个全局的变量长度len,和指向缓存区的指针。getchar函数开头就检查缓存区的长度,判断缓存区是否有数据,缓存区没有数据就调用写入缓存的函数,等待用户输入数据,用户键入回车时,数据和回车键都存入了缓存区,缓存区有数据就直接用指针取当前指向的字符,取出一个数据指针就要指向下一个字符。将取出的字符赋给接收字符的变量ch,判断ch的值是否是结束符。不是结束符就输出字符ch。再判断缓存的长度,看看有没有数据,没有数据写入缓存。有数据提取当前指向的数据,再判断是否结束符。

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

8.5本章小结

主要内容是Linux的I/O接口以及函数的分析,这里我们比较系统的分析其中的内部实现。最后,学习了解了printf函数和getchar函数内部实现的过程,对于实现时候调用的情况有了更加详细的了解。
(第8章1分)

结论

按照这次大作业的章节顺序。

从hello.c到之后的各个中间产物的变化,了解到hello程序的一生,具体深刻地理解了程序是怎么执行的,从预处理开始,编译、汇编、链接等过程hello完成了从一个c语言程序文件到可执行目标文件的华丽蜕变。
预处理阶段,完成对hello.c中带#的指令解析,将声明的头文件包含进来,将宏定义展开,进行条件编译、行控制等操作,生成hello.i文件。
编译阶段,编译器根据C语言程序到汇编指令的翻译规则将hello.i文件中的语句翻译为汇编代码,得到汇编文件hello.s。
汇编阶段,汇编器将hello.s中的汇编指令一一翻译为对应的二进制机器级指令,为各个符号引用生成所需的重定向信息,得到可重定向目标hello.o文件。
链接阶段,链接器解析hello.o中引用的内部、外部符号,处理重定位信息,为hello.o找到它需要的文件模块和外部链接库,生成可执行目标文件hello。
作为父进程,shell-bash进程调用fork函数,为hello创建进程——这将是它接下来的绽放舞台。随后在这个创建出的“舞台“进程里,execve函数被调用,操作系统删除原来的进程内容,加载器将hello文件中的代码和数据从磁盘复制到内存中,hello进程得到自己的虚拟内存空间,然后通过跳转到程序的第一条指令或入口点来运行该程序。
运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache同舟共济,完成对地址的请求。
异常处理机制保证了hello对异常信号的处理,使程序平稳运行;Unix I/O让程序能够与文件进行交互。
结束阶段,当hello运行完毕,shell父进程回收hello进程,内核删除为hello进程创建的所有数据结构,hello最终结束了它的演出。
Hello程序的运行历程,既是一个Program­-to-process的过程,也是一个Zero-to-Zero的过程。而这正如计算机系统的发展历史:从无到有,从0到1。如果没有计算机低层硬件系统,软件层面的逻辑和设计就无从谈起;如果没有操作系统联合低层硬件提供的抽象,程序员在开发软件的时候就必须陷入复杂的硬件实现细节。这将是一件可怕的事情,而且大量的精力花费在这个重复的、没有创造性的工作上也使得程序员无法集中精力在更具有创造性的程序设计工作上去。操作系统将硬件细节与程序员隔离开来,使得计算机成为一种简单的,高度抽象的可以与之打交道的设备。
通过对计算机系统的初步学习,我深刻体会到计算机系统设计之精巧,考虑之全面。为了解决速度快的设备存储小、存储大的设备慢的不平衡,计算机系统的设计者们设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度;为了应对一切可能出现的实际情况,工程师们设计出一系列的满足不同情况的策略,比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。计算机系统的设计与实现凝聚着无数聪明大脑的智慧,其中的奥秘值得我们每个人深入地探寻。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

列出所有的中间产物的文件名,并予以说明起作用。
1、hello.i:预处理的文件
2、hello.s:编译生成的汇编文件。
3、hello.o:汇编生成的可重定位的文件。
4、hello:链接生成的可执行文件。
5、link.txt链接生成的可执行文件的编译代码
6、elf.txt汇编生成的可重定位的文件的elf文件
7、elf1.txt链接生成的可执行文件的elf文件。

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

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1]https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86/7833652?fr=aladdin 预处理百度百科
[2]https://baike.baidu.com/item/%E7%BC%96%E8%AF%91/1258343?fr=aladdin 编译百度百科
[3]https://baike.baidu.com/item/%E6%B1%87%E7%BC%96%E7%A8%8B%E5%BA%8F/298210?fromtitle=%E6%B1%87%E7%BC%96&fromid=627224&fr=aladdin 汇编百度百科
[4]https://blog.csdn.net/qq_36459662/article/details/106188180?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165287640616781685346755%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165287640616781685346755&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-3-106188180-null-null.142v10control,157v4control&utm_term=%E9%93%BE%E6%8E%A5&spm=1018.2226.3001.4187
[5]深入理解计算机系统原书第三版
(参考文献0分,缺失 -1分)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值