Hello一生
计算机系统
大作业
题 目 程序人生-Hello's P2P
专 业 计算机科学与技术
学 号 1170300618
班 级 1703006
学 生 杨重阳
指 导 教 师 吴锐
计算机科学与技术学院
2018年12月
摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:预处理,编译,汇编,链接,fork(),exevce(),信号,储存,地址,IO。
本次实验通过对hello.c的编译和运行全过程跟踪研究对计算机的结构和运行原理进行深入的理解。我们首先对hello.c进行预处理,编译,汇编,和链接观察中间产物的不同点比较得出这些步骤的作用。然后了解运行hello可执行文件时的步骤,主要是通过fork(),exevce()函数使其运行mmap分配空间,然后在运行过程中受信号影响。运行时需要需要访存,我们接着了解了系统是如何解析地址并为程序分配空间的。最后我们通过了解IO明白了结果如何让输出到屏幕上。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 25 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 32 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 35 -
7.7 hello进程execve时的内存映射 - 37 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
首先我们通过高级语言编写hello.c源文件。然后通过cpp预处理器预出理形成hello.i,即将头文件中的内容加入.c文件之中。然后通过ccl翻译器形成.s文件hello.s的汇编文本文件。然后再通过汇编器as形成hello.o可重定位二进制文件。最后和库文件进行链接形成可执行文件。当shell执行它时父shell通过fork一个子进程(其父进程的复制)。子进程通过execve系统启动加载器。加载器删除子进程现有的虚拟内存,并创建一组新的代码、数据、堆和栈段。新的栈和堆会被初始化为零、通过将虚拟地址空间中的页映射到可以执行文件的页大小的片,新的代码和数据被初始化为可执行文件内容。最后加载器跳转到_start地址,它最终会调用应用程序main函数。完成了hello,p2p的罪恶生涯。
那我们来看看hello先生的o2o,在程序执行时需要分派内存空间。如上文所说进程通过execve系统启动加载器。加载器删除子进程现有的虚拟内存,并创建一组新的代码、数据、堆和栈段。新的栈和堆会被初始化为零、通过将虚拟地址空间中的页映射到可以执行文件的页大小的片,新的代码和数据被初始化为可执行文件内容。最后加载器跳转到_start地址,它最终会调用应用程序main函数。然后在运行过程中它会可能会使用到内存中的数据。这
使用到内存中的数据。这些数据通过各级存储,包括磁盘、主存、Cache等, 并使用页表等辅助存储,实现访存的加速。在这个过程中还涉及操作系统 的信号处理,控制进程,使得系统资源得到充分利用。在printf I/O系统把他 精彩的一生得奋斗结果打印在屏幕上最后被回收结束了其罪恶的一生。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2.19GHz;4.00GB RAM
软件环境:Windows 10 64位;Ubuntu 16.04 LTS
开发工具:gcc;gdb;objdump;edb;
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 |
作用 |
hello.i |
预处理.c文件 |
hello.s |
(编译后)有汇编语言的文本文件 |
hello.o |
(汇编后)二进制重定位目标文件 |
hello |
可执行文件 |
1.4 本章小结
hello.c的一生从预处理,编译,汇编,执行过程。体现了计算机软件硬件各个方面的合作。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
程序设计语言的预处理的概念:在编译之前进行的处理。 C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3.条件编译。 预处理命令以符号"#"开头。
1.不带参数的宏定义:
宏定义又称为宏代换、宏替换,简称"宏"。
格式:
#define标识符文本
其中的标识符就是所谓的符号常量,也称为"宏名"。
预处理(预编译)工作也叫做宏展开:将宏名替换为文本(这个文本可以 是字符串、可以是代码等)。
带参数的宏:
除了一般的字符串替换,还要做参数代换
格式:
#define 宏名(参数表)文本
2.一个文件包含另一个文件的内容
格式:
#include "文件名"或#include <文件名>编译时以包含处理以后的文件为编 译单位,被包含的文件是源文件的一部分。编译以后只得到一个目标文 件.obj被包含的文件又被称为"标题文件"或"头部文件"、"头文件", 并且常用.h作扩展名。修改头文件后所有包含该文件的文件都要重新编译
头文件的内容除了函数原型和宏定义外,还可以有结构体定义,全局变量 定义
3.有些语句希望在条件满足时才编译。
格式:(1)
#ifdef 标识符
程序段1
#else
程序段2
#endif
或
#ifdef
程序段1
#endif
当标识符已经定义时,程序段1才参加编译。
格式:(2)
#ifndef 标识符
#define 标识1
程序段1
#endif
如果标识符没有被定义,则重定义标识1,且执行程序段1。
格式:(3)
#if 表达式1
程序段1
#elif 表达式2
程序段2
……
#elif 表达式n
程序段n
#else
程序段n+1
#endif
当表达式1成立时,编译程序段1,当不成立时,编译程序段2。
作用:使用条件编译可以使目标程序变小,运行时间变短。
预编译使问题或算法的解决方案增多,有助于我们选择合适的解决方案。
2.2在Ubuntu下预处理的命令
图(2.1)预处理命令
图(2.2)文本结果显示,前3000多行全是加入的内容。
2.3 Hello的预处理结果解析
将hello.c宏展开,将#include <stdio.h>,#include <unistd.h>,#include <stdlib.h>这些文件的内容加入其中。变为了3000多行,但是任然可读。
2.4 本章小结
完成了预处理工作预处理的变化不大只是将宏定义展开,头文件插入等,文件一依然可读,文件长度增大明显变为了3000多行。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
将某一种程序设计语言写的程序翻译成等价的另一种语言的程序的程序, 称之为编译程序(compiler)。其实这里就是将我们的高级语言转换为汇编语言。
作用:可以转化时优化,必须经过汇编才能变为可执行文件
3.2 在Ubuntu下编译的命令
图(3.1)指令
图(3.2)这是编译过后的部分汇编代码
3.3 Hello的编译结果解析
我们的高级语言代码变成了汇编代码。
3.3.1变量
首先的定义全局变量int sleepecs = 2.5,但是由于是int型数据会自动舍入到2(隐式转换)
图(3.3.1)
我们看到sleepecs为4个字节的即int型。并且在.rodata节里面
图(3.3.2)
两个后面需要打印的字符串,后者在.text段
.LC0就是"Usage: Hello 学号 姓名!\n"
.LC1就是"Hello %s %s\n"
图(3.3.3)
以及全局main函数
3.3.2main函数
图(3.3.4)
首先初始化,压入返回地址,使栈底等于栈顶。然后向下申请31个字节的空间
图(3.3.4)
将main函数的两个参数压入栈中。%edi中是我们的argc,而%rsi是我们的char指针数组argv[]。
图(3.3.5)
这里是判断部分。让argc与3比较。如果不等于就将.LC0作为参数调用put就是打印.LC0对应的字符串即"Usage: Hello 学号 姓名!\n"就是提醒我们输入姓名学号,然后调用exit结束。如果等于3就跳入.L2部分。
图(3.3.6)
L2是一个循环模块。将for循环改成while型再用跳转到中间的类型进行翻译。
其中
图(3.3.7)
上图是i = 0;初始状态
i放在栈中%rsp下四个位置
图(3.3.8)
上图是 i= i+1;变化条件
图(3.3.8)
上图就是i<=9就是i不大于10的判断条件。
然后我们看看循环体内的操作
图(3.3.9)
首先将字符指针数组。放入%rax,然后将其中第二个argv[1]和第三个的指针 argv[2]分别放入%rsi和%rdx将LC1放入%rdi再调用printf。就相当于c语言 中的printf("Hello %s %s\n",argv[1],argv[2]);然后将sleepsecs放入%edi再调用 sleep相当于sleep(sleepsecs);
图(3.3.10)
结束就调用getchar(),然后将0放入%rax中并返回。结束
3.4 本章小结
hello.i到hello.s编译形成了汇编代码,不再是c语言而是汇编语言,但是依然可见,格式会发生巨大的改变。可以进行汇编阶段了。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
作用:用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
4.2 在Ubuntu下汇编的命令
图(4.2.1)
图(4.2.2)无法直接显示文本了
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图(4.3.1)可以知道里面有13个节
图(4.3.2)
用readelf -S hello.o得到13个节的大小,偏移量对其方式等信息
图(4.3.3)
用readelf -s hello.查看符号表
图(4.3.4)
重定位最为关键的信息。里面给了这些信息的重定位方式。.rela.text 和.rela.en_frame中只有。.rodata + 0和 .rodata +1e(如果猜得不错这两个就是 那两个字符串.LC0和.LC1)使用的绝对地址寻址。其他的数据,代码使用的 相对地址寻址。依靠当前的指令的位置加上偏移量和加数。
节的信息和符号表等的信息,依靠rel.text和rel.en_frame进行重定位。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
图(4.4.1)
如图
我们首先发现他们的格式有不同。
(1)我们反编译过来后就前面多了一些编号。
(2)其次来说其他基本都一样。就是有的.s文件有的用10进制,.o反汇编有的用16进制。如下
hello.s的申请栈
hello.o反汇编的申请栈
-
对文件中操作几乎一模一样不同的是跳转时的重定位。比如
图(4.4.2)
这里是一个用了直接地址寻址找到了第一个字符串的所在之地。(印证了开始认为.rodata和.rodata+1e是字符串的猜想)然后后面是相对地址寻址找到了puts函数
而hello.s文件直接用的全局变量和调用的函数。
Jmp函数也是一样的。
Jmp到一个偏移量处
这里是直接写的模块
全部函数。基本也如此,引用字符串时.s直接使用全局变量,反汇编.o的用绝 对地址寻址找到.rodata里全局变量。对于函数hello.s直接调用,而hello.o反 汇编后会用相对地址寻址找到其地址。Jmp也是。
4.5 本章小结
本阶段完成了对hello.s的汇编工作。使用Ubuntu下的汇编指令可以将其转换为.o可重定位目标文件。此外,本章通过将.o文件反汇编结果与.s汇编程序代码进行比较,了解了二者之间的差别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。目标文件是包括机器码和链接器可用信息的程序模块。简单的讲,链接器的工作就是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。链接器还要完成程序中各目标文件的地址空间的组织,这可能涉及重定位工作。
作用:到目前为止我们昕描述的内容表明,对于源程序任意一行代码的修改都需要重新编译和汇编整个程序:全部重新翻译是对计算资源的严重浪费。这种重复对于标准库程序尤为浪费,因为程序员要编译和汇编那些在定义上几乎从未改变过的过程。另一种方法是单独编译和汇编每个过程,以使得某一行代码的改变只需要编译和汇编一个过程。这种方法需要一个新的系统程宁,称为链接编辑器(link editor)或链接器(linker),它把所有独立汇编的机器语言程序"拼接"在一起。
5.2 在Ubuntu下链接的命令
图(5.2.1)链接过程
图(5.2.2)生成的hello可执行文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
直接用read -a hello 输出所有信息
图(5.3.1)
上图节头表里显示有25个节
图(5.3.2)各节的信息
图(5.3.3)各段的信息(看不清点大图)
可以看出这里有七个段
PHDR大小偏移0X40,大小是0X18,虚拟地址是0X400040,物理地址也是0X400040,是可读可执行的,且为8字节对其。其他的同理。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图(5.4.1)
在symbol处查找5.3相应的节就可以在Data Dump里找相应的节然后右边就是相应的二进制形式。
图(5.4.2)
这样就可以通过这些找到段了。
5.5 链接的重定位过程分析
图(5.5.1)指上面三张图合在一起
上面的三张图就是我们hello反汇编的结果。
一以上是我们hello反汇编的结果。
- 我们可以看出前面的编号有明显的不同。因为是放在了虚拟内存的相应的位置0X400000相的位置。而hello.o反汇编是从0开始的
-
.o反汇编就直接是.text然后main函数如下图
图(5.5.2)
而hello直接反汇编前面有因为重定位加入进来各种函数,数据。如开始的函数和调用的函数等等才会到main函数。所以之前的位置发生了巨大的改变。
图(5.5.3)
(3)
而这些call函数,引用全局变量,和跳转模块值时地址也有所变化。
图(5.5.4)
可执行文件跳转和应用就是虚拟内存地址(相对或绝对)。
图(5.5.5)
.o反汇编的跳转的就是只要hello数据时,对应的位置。
5.6 hello的执行流程
(这个各个地方都有调用)
图(5.6.1)
5.7 Hello的动态链接分析
图(5.7.1)
黑框框住的部分(右下角不算是)GLOBAL_OFFSET_TABLE上图是初始化前。
图(5.7.2)
运行后划线部分发生了明显的变化,因为这里是共享模块,使用PIC数据引用和PIC函数调用,在一开始是并没有将这些全局变量链接进来。加载时GOT会重定位其中的每个条目。
5.8 本章小结
本阶段完成了对hello.o的链接工作。使用Ubuntu下的链接指令可以将其转换为可执行目标文件,其中将库中的函数等东西加入其中并且进行了重定位等操作,hello先生的人生达到了高潮。
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一个命令行解释器,为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动、挂起、停止甚至是编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在shell中可以直接调用Linux命令。
处理流程:逐行处理,没有加&后台处理方式时,是当前行命令行处理完成后才开始执行下一条命令。并且首先判断是否为内部命令,然后判断是否为可执行程序。得到其参数。内部命令立即执行,可执行程序找到所在然后执行。fork()一个子进程然后在其中执行。
6.3 Hello的fork进程创建过程
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的"副本",这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。
hello
Shell fork()
6.4 Hello的execve过程
exceve函数再当前进程的上下文中加载并运行一个新程序。execve函数加载并运行科执行文件filename,且带参数列表argv和环境变量envp。只有当出现从错误是,exceve才会返回到调用程序。ecceve调用一次并从不返回。
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。注意,execve函数再当前进程的上下文中加载并运行一个新程序。它会覆盖当前进程的地址空间,但是并没有创建一个新进程。新进程仍然有相同的PID,并继承了调用exceve函数时已打开的所有文件。
6.5 Hello的进程执行
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重启一个被抢占的进程所需得状态。
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。
hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。所以其实hello在sleep时就是这样的切换。
程序在进行一些操作时会发生内核与用户状态的不断转换。这是为了保持在适当的时候有足够的权限和不容易出现安全问题。
图(6.5.1)
6.6 hello的异常与信号处理
来自ctrl+c和ctrl+c的信号分别为SIGTSTP和SIGINT,kill会用SIGKILL(9)信号。
图(6.6.1)这是ctrl+c的情况。发送SIGINT使其终止。
图(6.6.2)这是ctrl+z的情况。发送SIGSTP使其终止。
图(6.6.3)使用ps查看
图(6.6.4)
以上的几张图就是pstree的显示信息
图(6.6.5)
用fg让其再度运行
图(6.6.6)
使用kill发送不可忽略的SIGKILL信号使其终止。
6.7本章小结
Hello在运行的生涯之中依靠shell通过fork()建立子进程再在里面exceve(),然后在里面运行。运行过程中可能是前台可能是后台,可能会被各种信号影响。hello先生真的命运多舛啊。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
——这个概念应该是这几个概念中最好理解的一个,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是"与地址总线相对应",是更贴切一些。
虚拟内存这是对整个内存的抽像描述。它是相对于物理内存来讲的,可以直接理解成"不直实的","假的"内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它"转换"成真正的物理地址。这个"转换",是所有问题讨论的关键。有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。甚至多个进程可以使用相同的地址。
逻辑地址Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。"一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些"
线性地址或也叫虚拟地址跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:
图(7.2.1)
索引号,可以理解为数组的下标——而它将会对应一个数组,它又是什么的索引呢?这就是"段描述符(segment descriptor)",段描述符具体地址描述了一个段(对于"段"这个字眼的理解:我们可以理解为把虚拟内存分为一个一个的段。比如一个存储器有1024个字节,可以把它分成4段,每段有256个字节)。这样,很多个段描述符,就组了一个数组,叫"段描述符表",这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么——就理解段究竟有什么东西了,每一个段描述符由8个字节组成,如下图:
图(7.2.2)
这些东西很复杂,虽然可以利用一个数据结构来定义它,不过,这里只关心Base字段,它描述了一个段的开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在"全局段描述符表(GDT)"中,一些局部的,例如每个进程自己的,就放在所谓的"局部段描述符表(LDT)"中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT;=1表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
好多概念。这张图看起来要直观些:
下图中首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
图(7.2.3)(其实看完百科后发现后与物理地址寻址很像)
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。线性地址被分为以固定长度为单位的页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,页整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2、每一个活动的进程,因为都有其独立的对应的虚似内存,那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)[假定]
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址;
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址;
图(7.3.1)注意这里我们没有讨论TLB的情况。
7.4 TLB与四级页表支持下的VA到PA的变换
图(7.3.2)如图左半部分是上述情况。
-
获得VA首先通过它的v*n查找TLB。如果找到我们就得到了了我们的PPN物理页的信息。如果不命中我们就到页项里面寻找,将v*n划分为9位一份的四份然后对各级页表分别进行查找匹配,找到下一级页表的项,最后在第四级页表找到PPN。找到了就获得了PPN可能会更改TLB中的信息。如果找不到就会发生缺页。然后就会访问下一级存储获得里面的页,然后将页写入上一级快速缓存。依次循环。
(2)最后获得的PPN与VPO结合形成PA
7.5 三级Cache支持下的物理内存访问
如图(7.3.1)右半部分,我们得到VP后经过分配知道前六位为块偏移,中级6位是组索引,前面40位是标记。我们看到了整个数组的情况,依靠中间六位找到了L1中相应的组数。然后通过CT匹配相应的行数。当标记位为1且CT相同时命中。得到行然后再取块偏移处的数据即可。其他情况为不命中的情况,此时要在L2的寻找是否存在该数据有就将其加入L1,如果不空就要寻找到牺牲行。并且如果牺牲行有什么变化还要将其些回到我们的L2中。同理L2再次不命中就要找L3。并且将数据放入L2中,如果L2满了就要,选择牺牲的数据,有可能也会把牺牲的数据修改的写回给L3,然后再实行上面的步骤。同理L3也会不命中,就要和主存发生交换了。
7.6 hello进程fork时的内存映射
创立了一个带有自己独立的虚拟内存空间的新进程。当fork()时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前程序的mm_struct,区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
(1)删除已存在的用户区域。即当前虚拟地址用户部分已存在的结构
(2)映射私有区域。为新程序代码,数据,.bss和栈区域创建新的区域结构。这些区域都是私有的写时复制的。
(3)映射共享区域。如果与共享对象链接比如libc.a,那么这些对象都是动态链接到这个程序的,然后在映射到用户虚拟地址的共享区域。
(4)设置程序计数器。设置当前进程上下文的程序计数器,使之指向代码区域的入口。
7.8 缺页故障与缺页中断处理
图(7.8.1)
DRAM 缓存不命中为缺页。缺页产生缺页异常。缺页异常处理程序选择一个牺牲页,将目标页加载到物理内存中。最后让导致缺页的指令重新启动,页面命中。
但是学完虚拟内存后就会有更新的理解。
在翻译虚拟内存A时出发了一个缺页。这个异常导致控制转移到了内核然后
- 判断地址A是否合法,把A和每个结构的vm_start和vm_end做比较如果不合法就会触发一个段错误,从而终止这个进程。上图中标记为1
- 试图进行的访问是否合法。进程是否有读写或执行这个区域的权限。如果试图进行的访问时不合法的就会触发一个保护异常从而终止这个进程。在上图中标记为2
-
此刻内核知道这个缺页是由于对合法位置的访问所造成的。他即会选择一个牺牲页,如果这个牺牲页被修改过,那么就将它交换出去,换入新的页面并更新的页表。缺页当处理返回时,CPU就会重新启动引起缺页的指令,这条指令将再次发送A地址到MMU。MMU这次就可以正确翻译A而不会产生缺页中断了。
7.9动态存储分配管理
运行时需要额外的虚拟内存是就用动态内存分配器更为方便。内存分排气维护着一个进程的虚拟内存区域称为堆。堆进阶在为初始化的数据区域以后,并向上生长向更高的地址。对于每个进程,内核维护着一个变量brk他指向堆顶。分配器把堆视为一组大小不相等的块的集合。每个块都是一个连续的虚拟内存片,要么是已分配的要么是空闲的。已分配的块显示的保留为供应程序使用。空闲块可以用来分配。一个已分配的块保持已分配状态直到它被释放,这种释放要么是应用程序显示的执行要么是内存分配器自身隐式的执行。
性能目标:吞吐量。
性能目标:最大化内存利用率
目标: 最大化吞吐量,最大化内存利用率,这些目标经常是互相矛盾的。
定义: 聚集有效载荷 (Aggregate payload) Pk
malloc(p) 分配一个有效载荷p字节的块
请求 Rk 完成后, 聚集有效载荷 Pk 为当前已分配的块的效载荷之和
分配器两种风格
定义: 堆的当前的大小 Hk
假设 Hk 是单调非递减的
比如, 只有分配器使用sbrk时堆才会增大或减小
定义: 前 k+1 个请求的峰值利用率
Uk = ( maxi<=k Pi ) / Hk
内部碎片: 对一个给定块, 当有效荷载小于块的大小时会产生内部碎片
外部碎片:是当空闲内存合计起来足够满足一个分配请求,但是没有
一个独立的空闲块足够大可以来处理这个请求时发生的。
显式和隐式
显式分配器: 要求应用显式地释放任何已分配的块例如,C语言中的 malloc 和 free
隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块,比如Java, ML和Lisp等高级语言中的垃圾收集(garbagecollection)
首次适配 (First fit):
从头开始搜索空闲链表,选择第一个 合适的空闲块:
搜索时间与总块数 ( 包括已分配和空闲块 ) 成线性关系
在靠近链表起始处留下小空闲块的 "碎片"
下一次适配 (Next fit):
和首次适配相似,只是从链表中上一次查询结束的地方开始
比首次适应更快: 避免重复扫描那些无用块
最佳适配:搜索一遍找到最合适的块(能放得下的最小的空闲块):
为了满足上面的要求和碎片等的考虑下面有几种标记空闲块方法
图(7.9.1)
图(7.9.2)
第一字节存大小因为对其一定为8的倍数所以前3位一定为0可以考虑作为分配位。
图(7.9.2)
但是在合并空闲块时如果前面是空闲块就不太好寻找。
于是进行改进引入边界标记。及在头出的长度和是否分配给尾也来一个。这样就方便双向合并。但是会有点浪费空间。
图(7.9.4)
但是有与不可能每个空间都会是连续的。所以加入指针。显示寻址。
图(7.9.5)
图(7.9.6)
是这样的分配方式也是遍历的时间从,与所有数成正比到与空闲块成正比。因为已经不会经过分配的块了。
图(7.9.7)
图(7.9.8)
最后达到了垃圾收集
我们把块看成点,整个内存变成了有向可达图。其中不可达的点就是垃圾。
图(7.9.9)
图(7.9.10)
图(7.9.11)
这里c语言之所以成为保守是因为他把可以看作指针的都当做指针这样,有些可
以释放的垃圾并没有释放。
7.10本章小结
本节从各个方面介绍了内存方面的问题。从逻辑内存到虚拟内存,从虚拟内存到物理内存。然后又到了malloc动态内存分布。我们了解了程序运行时发生的种种问题。需要各个系统配合才能是运行的准备时间最短,运行性能最好。Hello的一生也算是值得了。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有I/O设备都被模型化为文件。而所有的输入和输出都被当做对相应文件的的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引用出一个简单、低级的应用接口,称为Unix I/O,这使得所有输入和输出都能以一种传统且一致的方式执行。
打开文件:一个应用要求内核打开文件。宣告它想访问一个I/O设备。内核返回一个小的非负整数,叫描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建时每个进程开始时都有三个打开文件:标准输入(描述符0),标准输出(描述符1)和标准错误(描述符2)
改变当前文件位置:从文件开始的字节偏移量。应用程序能够通过seek操作,显示的设置文件的当前位置为k。
读写文件:复制n个字节到内存。当看k>=m时候回触发EOF
关闭文件:访问完,通知内核关闭,释放内存然后将描述符恢复为可用。
8.2 简述Unix IO接口及其函数
打开和关闭文件open(char *filename, int flags, mode_t mode) 和 close(int fd)
open将filename转换成文件描述符。flags有O_CREAT(创建截断文件),O_TRUNC(截断文件),O_APPEND(设置文件位置在文件尾)
close关闭文件
读写文件read(int fd,void *buf,size_t n) and write(int fd,const *buf,size_t n)
read成功返回读的字节数,若EOF则为0,失败输出-1
write成功则为写的字节数,若出错则为-1
改变当前的文件位置 (seek)lseek()指示文件要读写位置的偏移量
无缓冲的输入输出函数rio_readn和 rio_writen
带缓冲的输入函数rio_readlineb 和 rio_readnb
下面两个函数查看文件信息
stat(const char *filename ,struct stat *buf)
Fstat(int fd ,struct stat *buf)
关于目录的操作
opender(const char *name)
readdir(DIR *dirp)
closedir(DIR *dirp)
8.3 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系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
int getchar(void)
{
static char buf[BUFSIZ];//缓冲区
static char* bb=buf;//指向缓冲区的第一个位置的指针
static int n=0;//静态变量记录个数
if(n==0)
{
n=read(0,buf,BUFSIZ);//把BUFSIZ大小的缓冲区的数全部读进来出来
bb=buf;//并且指向它
}
return(--n>=0)?(unsigned char)*bb++:EOF;//读完了就输出EOF否则就返回读取字符的ascii码
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
Linux将I/O输入都抽象为了文件,并提供Unix I/O接口。个接口使程序只需要简单的操作符就能够进行输入与输出,底层硬件实现操作系统就可以实现。然后通过各种函数丰富其功能完善它的所用。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
-
首先我们对hello.c进行编译。hello.c->hello.i->hello.s->hello.o->hello
分别是:
- 预处理把头文件宏定义等展开。形成hello.i行数增加了3000多行
- 编译把c语言变成汇编语言,汇编语言离机械语言更近hello.s
- 汇编把汇编语言转化为机械语言hello.o
- 连接把我们的程序没有但是库里存在函数等需要信息和我们的hello.o文件连接到一起,并且通过冲定位节进行重定位,这样使整个文件可执行形成最终的hello。但是共享库依然需要重定位。
- 在shell中运行首先在fork()一个子程序,然后再里面exceve()将现有虚拟内存删除然后,把现有的内容加入其中。再为其各个数据分配空间便可以运行了。再运行过程中系统会接受各种信号,比如ctrl+z和ctrl+c分别表示挂起(SIGSTP)和终止(SIGINT)的效果。可以用fg,bg决定前台后台运行,kill发送杀死程序的信号(SIGLKILL)。
- 从逻辑地址,到线性地址(虚拟地址),经过查找TLB,页表,得到物理地址,再用物理地址得到cache中或者DRAM的数据。虚拟内存到物理内存到分配空间。当需要的空间未知时我们也可以手动malloc一定空间给数据用。malloc()里面的知识很多,有关于标记的,数据结构显示链表,隐式链表,分配方式的首次适配下一次适配,和最佳适配。他们各有特点。
-
通过Unix的IO系统简化了读写操作。我们可以从屏幕上看到我们的结果。
(结论0分,缺少 -1分,根据内容酌情加分)
附件
中间产物
文本作用
hello.i
把宏定义,头文件等内容加入hello.c文件中形成
hello.s
我们把hello.i编译汇编。形成汇编文本。
hello.o
将hello.s文件进行汇编形成的可执行二目标进制文件。
hello
可执行文件
列出所有的中间产物的文件名,并予以说明起作用。
(附件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.
[7] pistni.printf函数的深入剖析[DB/OL].https://www.cnblogs.com/pianist/p/331580
1.html
[8] 百度条目.getchar[DB/OL].https://baike.baidu.com/item/getchar/919709?fr=aladdi
n
[9] DWJ-Blog. **** [DB/OL].https://blog.****.net/AGambler/article/details/817093
06
(参考文献0分,缺少 -1分)