计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L020713
班 级 2003010
学 生 毛佳男
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
本文通过csapp课程我们通过学习hello的一生,对我们所学的知识进行全面的梳理和复习,hello从一个用户输入的.c文件到一个可执行文件的全过程。我们在linux的系统下,合理使用命令操作,完成本实验,更近一步对计算机系统的了解。
关键词:hello;预处理;编译;汇编;链接;进程管理;存储管理;IO管理;P2P;O2O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
Hello的起源是程序员编写的一个.c文件但是经过预处理,编译,汇编,链接等一系列操作后完成了蜕变,摇身一变成了可执行文件。
Hello的p2p:在linux中,首先我们手写生成一个hello.c文件,然后通过编译命令 gcc -E hello.c -o hello.i 将hello.c编译生成hello.i的文本文件;然后通过编译器ccl使用命令 gcc -S hello.i -o hello.s 将hello.i编译生成汇编文件hello.s这一步可能消耗的时间较长;接着通过汇编器as使用命令 gcc -c hello.s -o hello.o 将hello.s文件生成二进制文件hello.o;最后由ld通过命令 gcc hello.o -o hello 将hello.o文件变成可执行文件hello。然后若想执行此进程,在shell中输入./hello,shell就会为其fork出子进程,然后hello就可以变成一个进程一样运行。
Hello的020:shell为hello进程execve,映射虚拟内存,,在进入程序入口后程序开始载入物理内存;接着进入main函数执行目标代码,将结果显示在显示器上;在进程结束后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
hello.o 的 ELF 格式 elf.txt
hello的ELF 格式 hello1.elf
1.4 本章小结
本章让我们对hello可执行文件的产生有了更详细的了解,首先介绍了p2p和020的意义,介绍了本次作业所需要的硬件开发环境、软件开发环境和开发工具,具体阐释了hello从.c文件到可执行文件的流程。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.2.1预处理的概念
在程序设计中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
2.2.2预处理的作用
- 首先对头文件进行展开:将#include后对应文件插入
- 删除所有的#define,展开所用的宏定义
- 删除文件注释
- 删除行号和文件名标识
- 保留#pragma命令
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
我们可以很明显的看到原来一个20多行的代码在进行预处理后变成了3000多行的代码,在开头我们可以看到,里面存在着大量头文件引入后的内容,由此我们可以判断预处理最大的作用就是解析头文件(如下图2.3.1)。其次由于这个程序中没用宏定义,所以这里面没有对宏定义的处理,然后就是删除文件注释以及删除行号和文件标识名(如图2.3.2)。
图 2.3.1
图2.3.2
2.4 本章小结
本章主要介绍了预处理的相关知识,了解了机器对文件进行预处理时都会产生什么变化以及会进行什么处理,其中我认为比较重要的是头文件处理。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
ccl将 hello.i 文本文件编译成文本文件 hello.o。
3.1.2编译的作用
- 代码扫描(词法分析):对源程序进行扫描,将原程序代码分割成一系列序号
- 语法分析:根据词法分析得到的一系列单词符号串,并且根据自上而下或者自下而上两种方法进行构建语法树
- 中间代码(中间语言):中间代码的作用就是让程序在逻辑上面更加简单明了,中间代码使程序分为前端和后端,前端产生无关的中间代码,后端将前端代码转换为机器代码
- 代码优化:使程序进行等价变换,让程序更加高效
- 目标代码:生成编译的最后一个阶段,将语法分析后或者是优化后的中间代码变换成目标代码。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1常量
在源文件中if(argc != 4)中可知其中的常量存储在.test中
同理可知
中的0,1,2,3,8同样存储在.test中
而其中printf输出的常量则存储在.section 中的 .rodata
3.3.2变量
1.全局变量
此程序中没有全局变量,全局变量的初始化不需要编译语句可以直接编译生成。
2.局部变量
局部变量一般存储在寄存器中或者栈中。本程序局部变量i的定义为:int i
上图为当前i = 0时循环前,当前i被保存在栈中rbp-4的位置上
3.静态变量
此程序没有静态变量
3.3.3算术操作
下图为每次进行一次循环操作i++;对应的每次循环完成之后对栈存储元素+1
3.3.4关系操作
下图中存在一个’!=’
对应的汇编语言为
若两个操作数不相等时跳转到其对应的地址。
3.3.5 数组/指针/结构操作
在主函数main中存在指针数组char *argv[],在数组中argv[0]存储着数组的地址路径和名称。
其中由上图可知argv[0]和argv[1]是两个字符串。
如上图可知argc存储在%edi中保存在-20(%rbp)而argv[]存储在%rsi中保存在-32(%rbp)。由于*char是8字节长度所以argv[1]和argv[2]本别存储在%rsi-8和%rsi-16中。
3.3.6控制转移
上图对应的汇编代码为
Jle用于判断cmpl产生的条件,如果后面的操作数小于等于前面的操作数那么跳转到指定地址。
3.3.7函数操作
1.main函数
参数传递:传入参数argc和argv[],并且用%rdi和%rsi存储
函数调用:系统启动函数时调用
函数返回:return 0
2.printf函数
函数调用:if判断满足条件后调用;for循环中被调用。
调用1:
调用2:
3.exit函数
参数传递:当传入的参数为1时,程序停止,执行退出
函数调用:满足if判断条件
4.sleep函数
参数传递:传入参数atoi(argv[3])
函数调用:函数存在for循环下
5.getchar函数
函数调用:在main函数中调用
3.4 本章小结
本章主要是对汇编代码的深层次的了解,逻辑上是介绍了汇编代码怎样实现常量、变量、算数操作、关系操作、数组/指针/结构控制、控制转移、函数操作等等。我从这个简单的hello.s文件来大概总结一下我们整个汇编操作需要学习的知识。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编器将hello.s编译成机器执行指令并将结果存储在hello.o文件中hello.o是一个二进制文件是一个计算机可以认识读取的文件
4.1.2汇编的作用
将汇编文件编译成二进制文件
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
通过命令readelf -a hello.o > ./elf.txt导出ELF文件
4.3.1ELF头
如上图可知ELF是以16个字节开始的,该序列描述了系统的字的大小和字节顺序。ELF头剩下的信息都含有:ELF头大小、目标文件类型(REL)、机器类型(X86-64)、节头部表中条目的数量以及大小等等。不同节的位置和大小是由节头部表描述的,目标文件的每个节都有一个固定条目的大小。
4.3.2节点头表
描述了hello.o文件中出现的各个节的类型、位置、所占空间大小等信息。
1).text:已编译程序的机器代码
2).rodata:只读数据
3).data:已初始化的全局和静态变量。局部变量在运行的时候保存在栈中,既不出现在.data中,也不出现在.bss中。
4).bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态C变量。其中在目标文件中这个节不占用实际的空间,它仅仅是一个占位符。并且在目标文件中,未初始变量不需要单独占据磁盘空间。在运行的时候,内存分配这些变量,初始值都是0。
5).symtab:一个符号表,它存放在程序中定义和引用的的函数和全局变量的信息。
6).rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
7).debug:一个调试符号表,其条目时程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
8).line:原始C程序中的行号和.text节中机器指令之间的映射。
9).strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
4.3.3重定位节
在链接时,需要通过重定位节对这些位置元素进行修改。连接器会通过了解重定位条目类型进行判定,以及他对应的偏移量信息计算出正确的地址。
此程序的重定位信息是:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar。
4.3.4符号表
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。4.3.2中有提到。
4.4 Hello.o的结果解析
4.4.1对比分析
1.hello.s代码中是没有地址信息的,但是在hello.o的反汇编文件中是存在的,并且按照顺序存储
2.hello.s文件中是通过标号L2 L3跳转;但是在hello.o的反汇编文件中是通过地址进行跳转
3.hello.s文件中操作数都是十进制的但是hello.o的反汇编文件中都是十六进制的
4.hello.s通过函数名称进行函数调用,但是hello.o的反汇编文件通过函数偏移量地址进行函数调用
5.hello.s文件没有重定位条目,但是hello.o的反汇编文件存在重定位条目
如上图所示,当前的PC位置是main+25,减去0x04后是main+21,刚好是其应该填充的位置,在重定位后就会存储在这个位置。
4.4.2机器语言
机器语言就是机器可以直接识别的二进制代码,是我们人类无法理解看懂的语言,一般是由操作码和操作数构成的
4.4.3汇编语言
汇编语言就是人类创造出的一种人类方便理解计算机语言的一种中间语言,可以帮助人们理解计算机语言,并且快速俩姐计算机的二进制编码。
4.4.4关系
两者的关系就是一一对应的,每一条汇编语言帮助我们理解对应的计算机语言。
4.5 本章小结
本章我们在汇编器的帮助下将汇编语言转换位机器语言,同时也是将汇编文件变成了可重定位的目标文件。首先分析了可重定位的目标文件的ELF格式、ELF头、节头表、符号表、重定位表等等。然后生成hello.o的反汇编文件与之前生成的hello.s文件进行对比分析,进一步了解汇编语言与机器语言的关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接是将各种不同文件的代码和数据部分收集(符号解析和重定位)起来并组合成一个单一文件的过程。
5.1.2链接的作用
链接器的存在可以使我们不用将一个整个的应用文件组织为一个巨大的编译文件,而是可以将它分解为更小的模块去执行编译,这样使分离编译成为了可能,节省了大量的空间。
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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
首先使用命令readelf -a hello > hello1.elf生成hello1.elf文件
5.3.1ELF文件头
5.3.2节头
描述了各个段的名称、大小、偏移量。链接器链接时会将各个文件相同的部分合成一个大段,重定位的时候会根据偏移量以及段大小重新分配地址。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
首先用edb打开hello文件,Data Dump窗口可以查看到加载到虚拟地址中的hello程序。接着我们查看ELF文件的program headers,由它来告诉链接器加载时的内容,并且提供信息。表中对应的每一项都列出了偏移量、虚拟地址空间和物理地址空间。可以看到程序包含以下几部分。
其中PHDR保存了各个segment的VirtAddr和MemSiz,INTERP包含ELF程序解析器的路径名,LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。DYNAMIC 保存了动态链接库由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:是否可执行标志,用于标志栈是否是可执行(可读可写的)。GNU_RELRO:权限设置,指定在重定位结束之后哪些内存区域是需要设置只读。
5.5 链接的重定位过程分析
输入命令objdump -d -r hello后如下图所示
5.5.1对比hello.o与hello
1.链接中加入了新的函数,比如exit、puts、sleep、getchar等等。
2.hello中增加了一些新的节:.init和.plt里面加入了一些新的函数
3.函数调用的不同:hello没有了hello.o重定位条目,因为所有的地方都已经确定了位置,而hello.o因为还没有链接,所以还保留有许多汇编器生成的重定位条目供之后的链接使用。链接的本质是合并不同文件的相同节,如数据节合并在一起,而代码节合并在一起。在这个过程中因为文件要使用其他文件中符号的定义,所以就需要用到在汇编过程中生成的.rel.data和.rel.text节中的重定位信息。
4.地址访问的不同:首先最明显的差别就是地址的长度不同,未链接的文件中只是简单的使用相对偏移量,而链接后的文件中则是使用虚拟地址空间中的地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
5.5.2链接的过程
根据对hello和hello.o不同的分析,我们可以知道链接其实就是链接器将所有的.o文件也就是目标文件全部组装到一起,文件中的各个函数按照一定的顺序排列起来。
5.6 hello的执行流程
使用edb执行hello程序,后不断地step into 如下图
执行过程如下:
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
ld-2.31.so!_dl_init
hello!_libc_csu_init
hello!__init
libc-2.31.so!_setjmp
libc-2.31.so!_sigsetjmp
hello!main
hello!.plt+0x70
hello!printf@plt
hello!exit@plt
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在ELF文件中可以看到:
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。其对应的位置如上图所示,对应到edb如下图所示
调用dl_init之前:
调用dl_init之后:
我们可以明显的看到在调用dl_init之后这两个位置的8字节全都发生了改变。
5.8 本章小结
本章主要介绍了链接的概念和作用,说明了链接生成一个可执行文件的完整过程。同时通过查看hello的虚拟地址空间存放的,对比hello与hello.o的反汇编代码,更好的掌握了链接与重定位的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程实际上就是正在运行的程序。在更广义的层面上来看,进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。
6.1.2进程的作用
每次程序在运行的时候,shell就会创建一个新的进程,在这个进程的上下文切换的过程中去运行实现我们选中的可执行文件。当系统在执行一个程序的时候,我们会看到一个假象就好像我们的系统只唯一运行当前的程序,我们的程序好像是自己占据了所有的处理器和内存。
但事实上并不是这样的。程序的并发处理让我们能够同时运行多个程序在电脑上;程序的异步处理让我们的程序之间是互不影响的。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1shell-bash的作用
1.shell连接用户和Linux系统内核,可以使用户更高效、更安全的使用连接linux系统内核
2.可以直接接受用户输入的命令,然后将结果输出,比如输出到显示器上、写入指定的文件中。
3.用户在shell中输入的命令中一部分是shell本身就可以识别的内置命令;还有一部分是其他程序,其中每一个应用程序就是一个外部命令。这种性质使得shell可以执行的命令可以无限拓展,这使得shell的功能十分强大
6.2.2shell-bash的处理流程
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有处理的结果中用到的注释删除,并且按照下面的顺序实行命令的检查:
I. 内建的命令
II. shell函数(由用户自己定义的)
III. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。
6.3 Hello的fork进程创建过程
在我们输入执行hello的命令之后,shell首先会对其进行判断,判断这是不是内置命令,即会通过fork函数创建出子进程。子进程与父进程有相同的虚拟空间——其中包括数据段、代码、共享库、用户栈。子进程相对于父进程而言是独立存在的,但是父进程打开的文件,对于子进程来说子进程也可以对他进行读写。客观而言两者最大的区别就是PID不同。创建出的fork函数会调用一次,但是会返回两次,父进程返回子进程的PID,子进程返回0。因为子进程的PID总是为非0,返回值就提供了一个明确的方法来分辨程序是在父进程还是在子进程中执行。
6.4 Hello的execve过程
首先我们先来看一下execve函数
int execve(const char *filename, const char *argv[], const char *envp[]);
若函数成功找到filename,那么函数不返回,若函数没有找到filename,那么函数返回0,filename指的就是hello程序,且带参数列表argv和环境变量列表envp。
在execve执行了hello程序后,会调用启动代码,启动代码设置栈,并将控制传递给新的主函数:int main(intargc , char \*\*argv , char \*envp)。
当main函数开始执行的时候,用户栈的组织结构从栈底到栈顶存储的结构依次是:首先是参数和环境字符串,栈往上紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。紧随环境变量数组之后的是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数libc_start_main的栈帧。
6.5 Hello的进程执行
6.5.1上下文信息
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.2进程时间片
时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
6.5.3进程调度
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进城后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。进程调度过程如下图所示:
图 6.5.3.1
6.5.4用户态与核心态的转换
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
首先我们先来观察一下正常的运行状态下的结果:
我们在执行了一会程序之后,然后输入ctrl+c命令,之前在前台终止它,这样进程就会收到一个SIGINT信号。
接下来我们再次运行程序,运行一段时间后输入ctrl+z中断程序,然后输入ps发现hello进程没有被结束依旧存在。
我们也可以通过命令jobs来进行查看
接下来我们可以用pstree命令来查看进程树可以看到bash和hello位置,下图为进程树:
接下来,我们使用kill指令为其传入一个SIGCONT信号,让它继续执行下去。
最后,我们使用kill指令为其传入一个SIGKILL信号杀死hello程序
6.7本章小结
本章我们主要了解了hello进程的执行过程,主要讲述了hello程序创建、加载和终止。同时我们也通过这章的学习,我们也了解到了进程的用户模式和内核模式、上下文切换、两个重要函数(fork和execve)、四大异常(终端、陷阱、故障、终止)等等。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
逻辑地址是指由程序hello产生的与段相关的偏移地址部分。
7.1.2线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。程序的代码会产生逻辑地址,或者说是段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
7.1.3虚拟地址
是指计算机呈现出要比实际拥有大的多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。有时我们也把逻辑地址称为虚拟地址
7.1.4物理地址
物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。若启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;若没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1等于0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它的基地址就知道了。总的来说,段式存储管理的优点是:没有碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点也相同,进程必须全部装入内存。
7.3 Hello的线性地址到物理地址的变换-页式管理
将程序的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。下图为页式管理的相关流程。
图 7.3.1
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,内存管理单元就必须查阅一个页表条目,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果页表条目碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在内存管理单元中包括了一个关于页表条目的小的缓存,称为翻译后备缓存器。
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。下图为k级页表进行翻译,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。
图 7.4.1
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后内存管理单元按照上述操作获得了物理地址PA。根据cache的大小要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据提供的组号去寻找正确的组,比较每一级的cache是否命中以及CT是否正确。如果命中就直接返回自己想要的数据,如果不命中,那么就依次去L2和L3判断是否命中,当主存命中时,将数据传给CPU同时更新各级cache的cacheline。
下图为三级cache示意图
图 7.5.1
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念
7.7 hello进程execve时的内存映射
下面是加载并运行hello的几个步骤:
1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构
2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。
3)映射共享区域:如果hello程序域共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
首先我们确定一个事情,页面命中是由硬件完成的,但是处理缺页却是硬件和操作系统协作完成的结果。
接下来我们再看一下整体的处理流程:
1.处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.如果试图进行的访问不合法,那么缺页处理程序会触发一个保护异常,从而终止这个进程。PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU,此时,MMU就能正常的翻译A了。
7.9动态存储分配管理
动态内存分配主要有两种基本方法与策略:
7.9.1隐式空闲链表管理
隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
7.9.2显示空闲链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
我认为本章的内容对我们更深一步理解计算机有很大的帮助,从内存的分页式管理,到三级cache的高层内存管理,再到程序内部的一个简单的堆的处理,都不能说是十分轻松简单的,其中蕴含的丰富内容让我十分有兴趣去一探究竟。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备(例如网络、磁盘、终端)都被模型化为文件。
设备管理:unix io接口
这种将设备优雅地映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等等。
8.2 简述Unix IO接口及其函数
8.2.1打开和关闭文件
打开函数原型为:int open(const char* path, int oflag, .../*mode_t mode*/);
返回值说明:若文件打开失败返回-1,打开失败原因可以通过errno或者strerror(errno)查看;若成功将返回最小的未用的文件描述符的值。
文件打开模式标识当前进程对打开文件的操作权限。通常用一个或者多个权限的或来表示。权限列表如下:
图 8.2.1.1
关闭函数原型:int close(int fd);
返回值说明:文件关闭成功返回0,关闭失败返回-1。
8.2.2读写文件操作
读入函数原型为:ssize_t read(int fd, void *buf, size_t n);
返回值说明:read函数从描述符为fd的当前位置复制最多n个字节到内存位置buf。若成功则返回读的字节数,若EOF则为0,若出错则为-1。
写出函数原型为:ssize_t write(int fd, const void *buf, size_t n);
返回值说明:write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。若成功则返回为写的字符数,若出错则返回-1。
8.3 printf的实现分析
首先我们先来看一下printf函数:
int printf(const char \*fmt, ...)
{
int i;
va\_list arg = (va\_list)((char \*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
它其中引用的vsprintf函数为:
int vsprintf(char \*buf, const char \*fmt, va\_list args)
{
char \*p;
chartmp\[256\];
va\_listp\_next\_arg = args;
for (p = buf; \*fmt; fmt++)
{
if (\*fmt != '%')
{
\*p++ = \*fmt;
continue;
}
fmt++;
switch (\*fmt)
{
case 'x':
itoa(tmp, \*((int \*)p\_next\_arg));
strcpy(p, tmp);
p\_next\_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
return (p - buf);
}
}
这个函数是要返回打印的字符串长度。
接下来我们要调用write函数,我们反汇编追踪一下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
我们可以找到INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL DA_386IGate,sys_call,PRIVILEGE_USER);
我们可以从上述函数发现它是要调用sys_call这个函数:
sys_call:
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
接着执行字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简要总结了unix I/O的有关知识,以及unix I/O接口以及函数。最后我们也分析了print发和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函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
6.hello的变化过程中,会有各种地址,但最终我们真正需要的是PA物理地址。
7.hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
8.hello最终被shell父进程回收,内核会收回为其创建的所有信息
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
预处理后的文件 hello.i
编译之后的汇编文件 hello.s
汇编之后的可重定位目标文件 hello.o
链接之后的可执行目标文件 hello
hello.o 的 ELF 格式 elf.txt
hello的ELF 格式 hello1.elf
(附件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] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[8] 博客园 printf函数实现的深入剖析
[9] CSDN博客 Ubuntu系统预处理、编译、汇编、链接指令
[10] 博客园 从汇编层面看函数调用的实现原理
(参考文献0分,缺失 -1分)