计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1190201526
班 级 1936603
学 生 唐浩云
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年6月
摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:计算机系统,hello程序的一生;
将代码敲进电脑存成hello.c;
将代码预处理、编译、汇编、链接,hello一个完美的程序诞生了;
在Bash里,OS为hello fork,为hello execve,为hello mmap,分给hello 时间片,让hello 得以在CPU/RAM/IO上运行;
OS与MMU处理从VA到PA;TLB、4级页表、3级Cache,Pagefile等等为hello 加速;IO管理与信号处理,使程序能在键盘、主板、显卡、屏幕间正常显示, 虽然程序运行时间很短、效果很简略;
OS和Bash在程序运行完成后回收了它。
(摘要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.c.
hello.c分别经过预处理器cpp的预处理,、编译器ccl的编译,汇编器as的汇编依次生成生成hello.i文件,hello.s文件、,生成hello.o文件、最后使用链接器ld进行链接最终成为可执行目标程序hello.
当我们在shell中输入字符串.\hello并使用回车代表输入结束后,shell通过一系列指令的调用将输入的字符读入到寄存器中,之后将Hello目标文件中的代码和数据从磁盘复制到主存。此时shell会调用fork函数创建一个新的进程,并通过加载报存上下文,将控制权交给这个新的进程,具体过程在后面会详细叙述。
在hello加载完成后,处理器就开始执行这个程序,翻译成机器语言再翻译成指令编码,最后把程序的调用如:printf等进行链接。新的代码段和数据段被初始化为hello目标文件的内容.然后,加载器会从_start的地址开始,之后会来到 main 函数的地址,之后进入 main 函数执行目标代码,CPU 为运行的 hello 分配时间片执行逻辑 控制流。
执行阶段把这个等执行的程序分解成几个阶段,分别执行对应的指令,最后输出字符串。之后输出的字符串从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示到屏幕上。
这标志着进程的终止,shell的父进程回收这个进程操作系统恢复shell的上下文,控制权重回shell,由shell等待接受下一个指令的输入。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果
文件名 文件作用
hello.i 预处理器修改了的源程序,分析预处理器行为
hello.s 编译器生成的编译程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
1.4 本章小结
本章大致主要简单介绍了 hello 的 p2p,020 过程,列出了本次实验信息:环境、中间结果,并且大致简介了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
使用文本编辑器打开输出的hello.i文件:
我们可以发现,原本仅仅只有几行的头文件hello.c已经被程序扩展到了数千行,hello.i程序的依次开始是程序的头文件stdio.hunistd.hstdlib.h的依次展开。
在乌邦图中发现程序使用的头文件#define<stdio.h>后到一个乌邦图的一个环境变量下寻找一个stdio.h,把其中stdio.h的宏定义内容进行了复制,同样的把其中stdio.h其中的宏定义被复制进行了递归依次展开。
而我们可以发现原始的hello.c的代码已经在程序的最后,前面都是预处理器复制的内容。
2.4 本章小结
本章介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
1.程序中的字符串分别是:
1)“Usage: Hello 学号 姓名!\n”,printf传入的格式化参数。在hello.s中声明如下图,注意到字符串使用UTF-8的格式编码的,一个汉字在UTF-8中占三个字节。
2)“Hello %s %s\n”,仍然是由printf函数传入的格式化参数,hello.s声明如下。
2.数字常量:hello.s中出现的常量有以下几个地方:对全局变量赋值2,将argc与3比较,循环中每次给i加1,循环中止条件i<10的判断。
3.数字变量:
全局变量:有一个全局变量int sleepsecs,可见它作为全局变量被放在.data节中,设置了大小为4字节,并初始化为2。
局部变量:中局部变量int argc存放在栈中-20(%rbp)的位置,通过与3的比较操作可以找到它;局部变量int i存放在栈中-4(%rbp)的位置,从与9的比较和循环中每次加1的操作可以找到它;还有一个局部变量数组char argv[],可以通过L4(循环部分)中输出函数前的两次取值找到argv[1],argv[2]的位置。
4.赋值:赋值操作共有两个,一个是对全局变量sleepsecs的赋值,源程序里令int sleepsecs = 2.5。而因为sleepsecs为整型变量,所以编译时直接对其赋值为2。另一个是对局部变量i赋值,之前已经得知i存在栈中-4(%rbp)的位置。
5.类型转换:对全局变量sleepsecs的赋值存在一个隐式类型转换。int sleepsecs = 2.5因为它把一个浮点数赋给整型变量,所以它会把浮点数2.5强制转换为2(浮点数转整数时向零舍入)。
6.算术操作:存在一个算术操作i++,即在每次循环中对变量i加1,之前已经得知i存在栈中-4(%rbp)的位置,那么通过add每次对-4(%rbp)中内容加1即可。
7.关系操作:存在两个关系操作,第一个是判断argc!=3,即将argc(栈中-20(%rbp)的内容)与3通过cmp进行比较。第二个是判断i<10,即将i(栈中-4(%rbp)的内容)与9通过cmp进行比较(即判断i<=9)。
8.数组/指针/结构操作:存在一个对数组argv的操作,在printf函数中引用了数组argv的两个元素argv[1],argv[2], 可以通过L4(循环部分)中输出函数前的两次取值找到argv[1],argv[2]的位置。
9.控制转移:
第一处是判断argv是否等于3,若不等于,则继续执行,若等于,则跳转至L2处(循环前对i初始化)继续执行。
第二处是对i初始化为0后的无条件跳转,以跳到L4,即循环部分代码。
第三处是判断是否达到循环终止条件(i<10),这里用i与9进行比较,若小于等于则跳回L4重复循环,否则执行循环外的下一步。这里将i<10的比较改为了与其等价的i<=9。
10.函数调用:
共有三次函数调用,第一次调用puts函数输出一个字符串常量,参数存在%rdi中;
第二次调用printf函数输出字符串常量以及两个局部变量数组的元素,字符串常量作为参数1存在%rdi中,两个数组元素作为参数2、3分别存在%rsi和%rdx中。
第三次调用sleep函数,以sleepsecs为参数,参数存在%edi中。
3.4 本章小结
本章简单的描述了编译器处理c语言程序的基本过程,根据hello程序中使用的各种数据类型,运算。循环操作和函数调用等操作对hello.s程序进行分析,查看编译器针对这些操作会采取什么样的策略。
编译器将.i 的拓展程序编译为.s 的汇编代码。经过编译之后, hello 程序便从 C 语言被解读成为为更加低级的汇编语言。当然,根据不同类型的CPU可能会产生不同类型的汇编语言,在此仅做简要分析,以x86-64指令类型作为例子。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
(1) ELF Header:用命令:readelf -h hello.o,ELF Header
ELF Header:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。
根据头文件的信息,可以知道该文件是可重定位目标文件,有13个节。
(2) Section Headers:命令:readelf -S hello.o
Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
(3)查看符号表.symtab :命令readelf -s hello.o
.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
4.4 Hello.o的结果解析
通过反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
(1)分支转移:反汇编的跳转指令用的不是段名称比如.L3,二是用的确定的地址,因为,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(2)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章针对hello.s汇编到hello.o。在本章的分析中,我们查看了hello.o的可重定位目标文件的格式,使用反汇编查看hello.o经过反汇编过程生成的代码并且把它与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.3 可执行目标文件hello的格式
(1)ELF Header:hello的文件头和hello.o文件头的不同之处如下图标记所示,Type类型为EXEC表明hello是一个可执行目标文件,有25个节
(2)节头部表Section Headers:Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
(3)符号表.symtab
5.4 hello的虚拟地址空间
程序包含以下几个小段:1, phdr 保存了二进制的程序头表。 2,interp 指定在程序已经从可执行文件映射到具体的内存区域之后,必须进程调用的解释器(如动态链接器)。 3,load表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存 了常量数据(如字符串)、程序的目标代码等,4,.dynamic 保存了由动态链接器使用的信息。5,note 保存辅助信息。 6,gnu_stack:权限标志,标志栈是否是可执行的。7,gnu_relro:这个节中指定了在重定位程序结束之后那些映射到内存的区域是需要重新设置为只读类型的。用户可以在程序中使用datadump查看虚拟地址段
5.5 链接的重定位过程分析
命令:objdump -d -r hello >hello.out,获得hello的反汇编代码.
通过分析hello与hello.o的不同,说明链接的过程。可以发现以下不同的地方:
(1)hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。
(2)(2)hello反汇编的代码中多了很多的节以及很多函数的汇编代码,这些节都具有一定的功能和含义。
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt
5.6 hello的执行流程
5.7 Hello的动态链接分析
共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。
而在一个动态的共享链接库中仍然存在着一个可以调用程序加载而动态链接无需重定位的位置无关代码,编译器在程序中的函数开始运行时是不能自动预测各个函数的开始运行时间和地址的,这就可能需要系统添加重定位的记录,交给一个动态共享链接器或者采用它来进行重定位的动态共享链接,动态共享链接器本身就是负责执行对动态链接的重定位过程,这样做就有效地防止了程序运行时自动修改或者调用目标模块的位置无关代码段。
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,由于静态的编译器本身无法准确预测变量和函数的绝对运行时地址,动态的链接器需要等待编译器在程序开始加载时再对编译器进行延迟解析,这样的延迟绑定策略称之为动态延迟绑定。got链接器叫做全局变量过程偏移链接表,在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现了函数的一个动态过程链接,这样一来,它就已经包含了正确的绝对运行时地址。
5.8 本章小结
本章通过对hello可执行程序的分析,回顾了链接的基本概念,文件的重定位过程,动态链接过程,虚拟地址空间,可重定位目标文件ELF格式的各个节等与链接有关的内容。链接的过程在软件开发中扮演一个关键的角色,它们使得分离编译成为可能。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个针对执行中的应用程序的程序的实例。系统中的每个应用程序都可以运行在某个应用进程的可执行上下文中。每次程序用户可以通过向系统中的shell应用程序输入一个可执行程序的英文名字,运行这个应用程序时,shell就可能会自动创建一个新的应用进程,然后在这个新应用进程的可执行上下文中自动运行这个可执行文件,应用程序也同样可以自动创建新的可执行进程,并且在这个新进程的可执行上下文中用户可以运行他们自己的可执行代码或者其他的应用程序。
我们应当感谢局部性的存在,进程所对应的处理功能部件会把它提供出来给所有的应用程序。它有两个关键抽象:一个独立的程序逻辑控制流:它可以提供一个独立的假象,好像我们的应用程序在一个独占的空间使用内存处理器。一个应用程序私有的地址处理器空间,它可以提供一个独立的假象,好像我们的应用程序独占的一个使用内存的系统。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端输入./hello 1190201526 唐浩云
shell会读取输入的命令,并开始进行以下操作:
第一步:判断hello不是一个内置的shell指令,所以调用应用程序,找到当前所在目录下的可执行文件hello,准备执行。
Shell会自动的调用fork()函数为父进程创建一个新的子进程,子进程就会因此得到与父进程(即shell)虚拟地址空间相同的一段各种的数据结构的副本(包括代码和数据段,堆,共享库和用户栈)。父进程与子进程最大的不同在于他们分别拥有不同的PID,父进程与子进程分别是两个并发的进程,在子进程中程序运行的这个过程中,父进程在原位置等待着程序的运行完毕。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
进程向每个程序人员提供一种假象,好像他们在一个独占的程序中使用了处理器,这种处理效果的具体实现效果本身就是一个逻辑控制流,它指的是一系列可执行程序的计数器pc的值,这些计数值唯一的定义对应于那些包含在程序的可执行文件目标对象中的可执行指令,或者说它指的是那些包含在程序运行时可以动态通过链接触到可执行程序的共享文件对象的可执行指令。时间片是指一个进程在执行控制流时候所处在的每一个时间段。
处理器通过设置在某个控制寄存器中的一个模式位来限制一个程序可以可以执行的指令以及它可以访问的地址空间。没有设置模式位时,进程就运行在用户模式中。用户模式下不允许执行特权指令,不允许使用或者访问内核区的代码或者数据。设置模式位时,进程处于内核模式,该进程可以 访问系统中的任何内存位置,可以执行指令集中的任何命令。进程从用户模式变为内核模式的唯一方式是使用诸如中断,故障或陷入系统调用这样的异常。异常发生时,控制传递到异常处理程序,处理器从用户模式转到内核模式。
上下文在运行时候的状态这也就是一个进程内核重新开始启动一个被其他进程或者对象库所抢占的网络服务器时该进程所可能需要的一个下文状态。它由通用寄存器、浮点数据寄存器、程序执行计数器、用户栈、状态数据寄存器、内部多核栈和各种应用内核数据结构等各种应用对象的最大值数据寄存器组合构成。
在调用进程发送sleep之前,hello在当前的用户内核模式下进程继续运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理等待休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,将当前调用hello的进程自动加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。
之后设置定时器,休眠的时间等于自己设置的时间,当计时器时间到时候,发送一个中断信号。内核收到中断信号进行中断处理,hello被重新加入运行队列,等待执行,这时候hello就可以运行在自己的逻辑控制流里面了。
6.6 hello的异常与信号处理
正常执行hello程序
按下 ctrl-z 的,输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,如图6.6.3所示用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。
按下Ctrl+c的结果,在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程。
程序运行过程中按键盘,不停乱按。乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入。
6.7本章小结
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址用来指定一个操作数,它由选择符和偏移量组成。逻辑地址是程序代码在编译之后出现在汇编程序。
线性地址:一个逻辑地址在经过段地址机制的转化之后变成一个线性分页地址(与虚拟地址同意义)。具体的格式可以表示为:虚拟地址描述符:偏移量。它作为一个逻辑分页地址被输入到一个物理地址变换之间的一个中间层,在分页地址变换机制中需要使用一个线性分页地址描述符作为输入,线性地址可以再经过物理地址变换以产生一个新的物理分页地址。在不同时启用一个分页地址机制的情况下,线性地址本身就是与虚拟地址同意义的。
物理地址::一个计算机操作系统的物理主存被自动组织为一个由m个连续的字节相同大小的单元内存组成的计算机数据。单元内存没有唯一的字节都是因为有一个唯一的物理单元内存地址,中央处理器会通过地址总线的寻址,找到真实的物理单元内存对应的地址,这会使得具有相同单元内存地址的计算机物理数据通过存储器被自动进行读写。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
在分页的机制下地址转化管理机制主要实现了虚拟地址(它也即非非线性内存地址)向虚拟地址物理页或内存地址的非线性分页转化。vm内存系统将虚拟内存的块大小分割成作为一个被我们称为基于虚拟页的内存大小固定的块块用来进行处理这个固定大小的内存问题,每个称为虚拟页的内存大小可以固定为2p=2p个单位字节。类似的,物理块和虚拟内存被再一次细分为一个物理页(也被通常称为页帧),大小与每一个虚拟页的地址大小与其对应的值相等。例如:一个32位的虚拟机器,线性的地址可以最大达到4gb,用4kb为一个页来进行划分,也可以分为1m个页,通过页表处理和查找这些虚拟页的数据,方便对线性地址的大小进行管理。
之后计算机会进行一个翻译操作,把一个n元素的虚拟地址空间中的虚拟元素与一个m元素的另一个物理虚拟地址空间相互进行映射,这个翻译操作被我们称为地址翻译。
虚拟地址由对应的虚拟物理页号(vpn)和虚拟页偏移量(vpo)共同组成,类似的,物理地址由虚拟物理页偏移号(ppn)和对应的物理页偏移量(ppo)共同分配组成(这里没有特别考虑tlb快表的物理地址结构)。页表中物理地址存在三种常见的情况:未分配:没有在虚拟内存的空间中分配该条目的内存。未分配缓存:在虚拟内存的空间中已经分配了但是没有被直接缓存到对应物理地址的内存中。已分配已缓存:内存已经缓存在了对应物理地址的内存中。页表的基址寄存器paptbr+vpn在页表中可以获得条目pte,通过对比条目对应的有效位判断物理地址是上述哪一种的情况,如果有效则通过提取得出对应物理地址的页号寄存器ppn,与对应的虚拟页偏移量共同分配构成了物理地址寄存器pa。
当页面命中时CPU硬件执行的步骤:
第1步:处理器会产生一个虚拟地址,并且将它传送给地址管理单元MMU。
第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它。
第3步:高速缓存或者主存向MMU返回PTE。
第4步:MMU构造物理地址,并把它传送给高速缓存/主存。
第5步:高速缓存或者主存会返回所请求的数据字给处理器。
7.4 TLB与四级页表支持下的VA到PA的变换
在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下: 虚拟地址空间 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 位。
如图 7.4,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.5 三级Cache支持下的物理内存访问
物理地址的结构包括组索引位CI(倒数7-12位),使用它进行组索引,使用组索引位找到对应的组之后。假设我们的cache采用8路的块,匹配标记位CT(前40位)如果匹配成功且寻找到的块的有效位valid上的标志的值为1,则命中,根据数据偏移量CO(后6位)取出需要的数据然后进行返回。
如果没有数据被匹配成功或者匹配成功但是标志位是 1,这些都是不命中的情况,向 下一级缓存中查询数据(L2 Cache->L3 Cache->主存),并且将查找到的数据加载到cache里面。这时候我们面临一个问题,替换谁。一般我们会选择使用一种常见的简单替换策略,查询得到的数据之后,如果我们映射得到的组内已经有很多个空闲块,则直接在组内放置;否则组内都已经是有效块,产生了冲突,则我们会采用最近最少最少使用(lfu)的策略,然后确定将哪一个块替换作为牺牲块。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数在上面已经介绍过了,在此仅做简单的重复。
execve函数在当前代码共享进程的上下文中加载并自动运行一个新的代码共享程序,它可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但是它并没有自动创建一个新的代码共享进程。新的运行程序仍然在堆栈中拥有相同的区域pid。之后为新运行程序的用户共享代码、数据、bss和所有堆栈的区域结构创建新的共享区域和结构,这一步叫通过链接映射到新的私有代码共享区域,所有这些新的代码共享区域都可能是在运行时私有的、写时复制的。它首先映射到一个共享的区域,hello这个程序与当前共享的对象libc.so链接,它可能是首先动态通过链接映射到这个代码共享程序上下文中的,然后再通过映射链接到用户虚拟地址和部分空间区域中的另一个共享代码区域内。为了设置一个新的程序计数器,execve函数要做的最后一件要做的事情就是自动设置当前代码共享进程上下文的一个程序计数器,使之成为指向所有代码共享区域的一个入口点(即_start函数)。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆.系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) .对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块
显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.
隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列,:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
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的实现分析
(以下格式自行编排,编辑时删除)
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章中简单的描述了linux的io的接口及其设备和管理模式,unixio的接口及其使用的函数,还有常见的printf函数和pritgetchar函数,当然,如果真的对此展开了分析,printf函数的设计和实现其实看起来还是相当困难的,所以仅仅是简单的了解,对此的深入了解还是有些过浅,需要学生通过不断的分析和学习实践来增进自己的认识和了解。
(第8章1分)
结论
Hello程序的一生是从文本编辑器开始的,是程序员亲手一个个打出来的字符,成为最初的hello.c文件,也是我们平时经常要交的程序。
之后我们会把hello进行预处理操作,这一步进行之后,我们在hello.c内部进行的宏定义和为了省事用include调用先人大佬写好的库取出来合并到文件hello.i中。
但是这一堆字符计算机是无论如何都认识不了的,所以就需要进行编译操作,hello.i被汇编成为汇编文件hello.s。
编译进行完毕后,机器要让文件成为它能读懂的东西,hello.s经过汇编阶段变成了可重定位目标文件hello.o
下一步是动态链接过程,hello.o和动态链接库共同链接成可执行目标程序hello。到这里,所谓的P2P: From Program to Process过程就完成了。
然后hello就要开始发光发热了,我们要使用hello就在shell里面输入./hello 1183000118 张智琦 1 并按下回车键
Shell读懂了我们的意思,它使用fork和execve函数加载映射虚拟内存,给我们的hello创建新的代码数据堆栈段,让我们的hello误以为自己在独占的使用这些资源。
CPU为hello分配一个时间片,在这个时间片的时间里,hello获得对CPU的控制权力,在程序计数器中加入自己的代码,按顺序执行他们。
之后程序或者从cache中很快的取得需要的数据,或者从内存中取出需要的数据,又或是从磁盘加载数据。
如果我们想要在程序进行时休息一下,只要键入ctrl-z就可以让程序挂起,甚至还可以通过ctrl-c停止这个进程。这些都是信号实现的强大功能。
最后,shell回收子进程,内核会删除这个进程使用所需要创建的一系列数据结构。至此,hello程序结束了自己短暂的一生。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 文件作用
hello.i 预处理器修改了的源程序,分析预处理器行为
hello.s 编译器生成的编译程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
(附件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分)