计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 ( 2+x)人工智能
学 号 2021111482
班 级 21WL022
学 生 吴欣俊
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
本文对hello.c这个程序编写完成后在Linux下运行的“生命周期”进行了描述。在这个过程中,我们分别运行C预处理器、C编译器、汇编器得到了hello.i、hello.s、hello.o文件,最后利用链接器生成了可执行文件hello。 之后,我们探索了计算机系统对hello的进程管理、存储管理,随着对其完整“生命周期”的探索,hello的“生命”也走向了尽头。
关键词:预处理;编译;汇编;链接;进程;存储;IO管理;
目 录
第1章 概述
1.1 Hello简介
P2P:从hello.c程序到二进制可执行文件hello的过程。在linux下,hello.c经过C预处理器、C编译器、汇编器和链接器变成了可执行目标程序hello,然后在shell中输入启动命令,shell为其fork生成了一个子进程,这就使得hello从程序变成了进程。
020:通过shell输入命令,让shell为fork函数创建一个新的进程,之后调用execve映射虚拟内存。在进入程序入口后便开始载入物理内存,进入main函数执行目标代码。此时,CPU为运行hello规划时间片。当程序运行结束之后,shell父进程回收hello进程,内核删除其数据结构。
1.2 环境与工具
1.硬件环境:Intel(R) Core(TM) i7-10750H CPU 2.60GHz 2.59 GHz;16.0RAM;1024DISK
2.软件环境:Windows10 64位 Ubuntu-20.04.4 64位
3.工具:codeblocks;Visual Studio;gdb;Objdump;HexEditor
1.3 中间结果
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。
1.4 本章小结
本章根据hello的自白,概括介绍了hello的P2P和O2O的过程。此外,还介绍了本实验用到的硬软件环境和开发调试工具。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预处理指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。
所有的预处理器(cpp)命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
(1)添加对应的头文件在#include处
(2)删除#define并展开对应的宏,#undef取消已经定义的宏。
(3)#ifdef, 若宏已经定义,则返回真;#ifndef, 若宏没有定义,则返回真
(4)处理所有的条件预编译指令,例如#if#endif,根据“#if”后面的条件决定需要编译的代码。
(5) #if,如果给定条件为真,则编译下面代码;#else是#if 的替代方案;#elif,如果前面的#if给定条件不为真,当前条件为真,则编译下面代码;#endif,结束一个 #if……#else 条件编译块。
(6)#error,当遇到标准错误时,输出错误信息。
(7)#pragma,使用标准化方法,发布特殊的命令到编译器中。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
2.4 本章小结
本章首先介绍了预处理的概念与作用,接着以hello.c为例,演示了在Ubuntu下如何预处理程序,并对结果进行分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器将预处理之后生成的文本文件hello.i翻译成文本文件hello.s,它通常包含一个汇编语言程序。
将高级语言书写的源程序转换为一条条机器指令,机器指令和汇编指令一一对应,使机器更容易理解,为汇编做准备。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
file 源文件
.text 代码段
.section .rodata 存放只读变量
.type 函数/对象类型
.size 大小
.data 存放已经初始化的全局变量和静态c变量
.long long 类型
3.3.1数据
字符串类型数据,分别对应代码中输出的两个字符串
main中输入的变量的int类型argc,和char类型argv,分别储存在edi和rsi中,在随后的提取使用中被临时储存在栈中。
变量i储存在栈中,在每次循环一次结束后进行加一操作。
3.3.2赋值
将i值赋为0。
3.3.3算术运算
位置偏移:分别为移动8位和16位;
加法:对i执行加1操作;i++;
比较大小:将i与7进行比较,小于7则继续进行加法运算;for(i=1;i<8;i++)
将argc与4进行比较,小于4则继续运算;即代码中的if(argc!=4)
3.3.4函数调用
调用printf函数;
调用getchar函数;
调用puts函数和exit函数;
调用sleep函数。
3.4 本章小结
本章首先介绍了编译的概念和作用,然后在Ubuntu下以hello.s为例,通过分析其汇编程序,理解编译器是如何处理各种数据类型和各类操作的。编译是从高级语言程序生成可执行文件的过程中的关键一步。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将汇编程序翻译为机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中
生成机器指令,方便机器直接分析。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1典型的ELF可重定位目标文件格式
1、ELF头以16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助两届其语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可执行、可重定位或者共享的)、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。
2、.text:已编译程序的机器代码。
3、.rodata:只读数据。
4、.data:已初始化的全局变量和局部静态变量。
5、.bss:未初始化的全局变量和局部静态变量,仅是占位符,不占据任何实际磁盘空间。
6、.symtab:符号表,存放函数和全局变量(符号表)信息,不包括局部变量。
7、.rel.text:.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
8、.rel.data:.data节的重定位信息,用于对被模块使用或定义的全局变量重定位的信息。
9、.debug:调试符号表,只有以-g方式调用编译器驱动程序时,才会得到这张表。
10、.line:原始C源程序中的行号和.text节中机器指令之间的映射。
11、.strtab节:字符串表,包括.symtab和.debug节中的符号表。
12、节头表:每个节的节名、偏移和大小。
4.3.2 hello.o分析
用readelf -S指令查看hello.o的节头表,查看节的基本信息。
用readelf -h指令可以查看hello.o的ELF头信息。
类别:64位版本
数据:使用补码表示,且为小端法
Version:版本为1
OS/ABI:操作系统为UNIX – SYSTEM V
类型:REL表明这是一个可重定位文件
系统架构:64位机器上编译的目标代码为Advanced Micro Devices X86-64
入口点地址s:为0x0表示程序的入口地址为0
程序头起点:为0表示没有程序头表
Start of section headers:节头表的起始位置为1240字节处
Size of section headers:64表示每个表项64个字节
Number of section headers:14表示共14个表
Section header string table index:13为.strtab在节头表中的索引
readelf -s hello.o查看符号表,Name为符号名称,Value是符号相对于目标节的起始位置偏移,Size为目标大小,Type是类型,数据或函数,Bind表示是本地还是全局。
readelf -r hello.o
readelf -g hello.o显示节组信息。hello.o没有节组
4.4 Hello.o的结果解析
在终端输入objdump -d -r hello.o查看hello.o的反汇编
Hello.s文件内容如下:
通过将反汇编与hello.s比较发现,汇编指令代码几乎相同,反汇编代码除了汇编代码之外,还显示了机器代码,在左侧用16进制表示。机器指令有操作码和操作数组成,和汇编指令一一对应。最左侧为相对地址。
其中跳转指令和函数调用等指令,在反汇编代码中表示为对应地址的偏移,而在hello.s中直接表示为函数名或定义的符号。在反汇编代码中,立即数是16进制显示的,而在hello.s中立即数是以十进制显示的。
4.5 本章小结
本章首先介绍了汇编的概念和作用,接着通过实操,对hello.s文件进行汇编,生成ELF可重定位目标文件hello.o,接着使用readelf工具,通过设置不同参数,查看了hello.o的ELF头、节头表、可重定位信息和符号表等,通过分析理解可重定位目标文件的内容。最后将其与hello.s比较,分析不同,并说明机器语言与汇编语言的一一对应关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
链接使得分离编译,一个大的应用程序可以被分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
5.2 在Ubuntu下链接的命令
在终端输入
ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
5.3 可执行目标文件hello的格式
可执行目标文件与可重定位文件稍有不同,ELF头中字段e_entry给出执行程序时的第一条指令的地址,而在可重定位文件中,此字段为0。可执行目标文件多了一个程序头表,也成为段头表,是一个结构数组。还多了一个.init节,用于定义init函数,该函数用来执行可执行目标文件开始执行时的初始化工作。因为可执行目标文件不需要重定位,所以比可重定位目标文件少了两个.rel节。
查看hello的ELF头:发现hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件,这与hello.o不同。hello中的节的数量为23个。
查看hello的节头表
发现刚才提到的23个节的具体信息,在节头表中都有显示,包括大小Size,偏移量Offset,其中Address是程序被载入虚址地址的起始地址。
查看hello的程序头表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
在edb中打开hello后,打开Memoty Regions,其中前16个字节这一部分就是我们ELF文件的头。
第二个区域是我们指令的装载地址。
第三个区域编码了我们代码中字符串常量等数据。
第四个部分是运行时堆,由malloc函数管理内存分配,同时作为全局变量的数组也会保存在这一部分。
最后一部分是用户的栈。
5.5 链接的重定位过程分析
输入objdump -d -r hello得到hello的反汇编代码。
hello和hello.o的不同点在于多出了一些函数,多了.plt节和.init节等,地址从相对地址变成了虚拟地址,call指令,jmp指令等添加了正确地址。
在重定位时,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置,这样每条指令和全局变量就有唯一的运行时内存地址了。
5.6 hello的执行流程
观察寄存器中的寄存器中的RIP即可得知程序调用的子程序及其地址。
调用子程序名:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
hello!_init
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
libc-2.27.so!exit
5.7 Hello的动态链接分析
在elf文件中我们可以找到got pit开头地址为0x404000,在edb中找到他
此为dl_init前,0x404008之后的16个字节均为0
此为dl_init后多了多了两个地址,即函数的最终地址。
5.8 本章小结
本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是指计算机中已运行的程序, 是系统进行资源分配和调度的基本单位, 是操作系统结构的基础。
提供给应用程序的关键抽象:
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
(2)一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1Shell-bash的作用:
Shell 是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。Shell 有自己的编程语言用于对命令的编辑,它允许用户编写由 shell 命令组成的程序。 Shell 编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的 Shell 程序与其他应用程序具有同样的效果。
6.2.2 Shell-bash的处理流程:
Shell 首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。 将命令行划分成小块 tokens, 程序块 tokens 被处理,检查看他们是否是 shell 中所引用到的关键字。 tokens 被确定以后, shell 根据 aliases 文件中的列表来检查命令的第一个单词。如果这个单词出现在 aliases 表中,执行替换操作并且处理过程回到第一步重新分割程序块 tokens。 对~符号和所有前面带有$符号的变量进行替换, 并将命令行中的内嵌命令表达式替换成命令。然后将命令行中的内嵌命令表达式替换成命令→将命令行中的内嵌命令表达式替换成命令→命令的检查→初始化所有的输入输出重定向→执行命令。
6.3 Hello的fork进程创建过程
进程的创建采用fork函数:pid_t fork(void);创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。
父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。
在这里,父进程为shell,在输入./hello的时候,首先shell会对我们输入的命令进行解析,shell会认为时执行当前目录下的可执行文件hello,因此shell会调用fork()创建一个子进程。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序,调用成功不会返回。与fork不同,fork一次调用两次返回,execve一次调用从不返回。
6.5 Hello的进程执行
hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
操作系统所提供的进程抽象:
①逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
②上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
⑤上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 1190201016 石衍,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
1.异常和信号异常种类
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
- 运行结果
①正常运行
②按下 ctrl-z
输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 info,之后输入字串,程序结束,同时进程被回收,如下图。
③按下Ctrl+c
在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程,如图所示。
④不停乱按
无关输入被缓存到stdin,并随着printf指令被输出到结果。
6.7本章小结
本章介绍了进程的概念和作用、shell-bash的处理过程与作用并且着重分析了调用fork创建新进程,调用execve函数执行hello,hello的进程执行过程,以及hello在运行时遇到的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3.虚拟地址:就是线性地址。
4.物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
linux下逻辑地址转换成线性地址的流程
(1)使用段选择符中的偏移值 在GDT或LDT中定位相应的段描述符。
(2)利用段描述符检验段的方位权限和范围,以便确定该段是可访问的并且偏移量位于段的段界限内。
(3)把段的偏移量加到段的基地址上最后形成一个线性地址。
段式管理特点:
(1)段式管理以段为单位分配内存,每段分配一个连续的内存区。
(2)由于各段长度不等,所以这些存储区的大小不一。
(3)同一进程包含的各段之间不要求连续。
(4)段式管理的内存分配与释放在作业或进程的执行过程中动态进行
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址通过分页机制对应到物理地址,具体的说,就是通过页表查找来对应物理地址。分页是CPU提供的一种机制,Linux根据这种机制的规则,利用它实现了内存管理。
保护模式下,控制寄存器CR0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接做为物理地址。
分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表。
为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。
32位的线性地址被分成3个部分:最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。
页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。
页式管理优点:
(1)由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。
(2)动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。
页式管理缺点:
(1)要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本。
(2)增加了系统开销,例如缺页中断处理机,
(3)请求调页的算法如选择不当,有可能产生抖动现象。
(4)虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面较大,则这一部分的损失仍然较大。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。
7.5 三级Cache支持下的物理内存访问
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
①删除已存在的用户区域
删除当前进程虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
③映射共享区域
hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC)
execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。
处理程序执行如下步骤:
1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
2.检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,
程序终止。
3.两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换
出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk,它指向对的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的。要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。
1.显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫
做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放
这个块。隐式分配器也叫垃圾收集器。
隐式空闲链表的带边界标记的堆块格式:
使用边界标记的堆块的格式其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。通过这种结构,隐式动态内存分配器会对堆进行扫描,通过头部和脚部的结构实现查找。
显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。如图所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。
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函数:
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
③ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
④ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
printf代码:
int printf(const char *fmt, …)
{
int i;
char buf[256];
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
vsprintf代码:
int vsprintf(char *buf, const char fmt, va_list args)
{
char p;
char tmp[256];
va_list p_next_arg = args;
}
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall
syscall实现:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),于是我们的打印字符串就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。
(第8章1分)
结论
1.输入:将hello.c代码从键盘输入。
2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,
生成hello.i文件。
3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。
4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。
5.链接(ld):将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程
序hello,至此可执行hello程序正式诞生。
6.运行:在shell中输入./hello 1190201016 石衍 1
7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()
函数创建一个子进程。
8.加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口
后程序开始载入物理内存,然后进入main函数。
9.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源, 顺
序执行自己的控制逻辑流。
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进
程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
抽象对计算机系统是很重要的概念,底层信息用二进制来抽象表示,进程是对处
理器、主存和I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,文件是对I/O
设备的抽象,等等。另外,存储器的“过渡”策略也十分精妙,由于CPU的处理
速度比主存快得多,为了减少“供不应求”的现象,在CPU和主存之间增加了一
级,二级,三级cache大大提高了CPU访问主存的速度。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名称 功能
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
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.
(参考文献0分,缺失 -1分)