计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机系
学 号 1190202102
班 级 03002
学 生 何洋
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年6月
摘 要
本文章主要是讲hello源程序是如何一步步转变为系统可执行程序的可执行程序,通过Hello.càhello.i(预处理生成的文本文件)àhello.s(编译后的汇编语言文件)àhello.o(可重定位目标文件)àhello(链接后的可执行目标文件)这样的顺序,同时还有信号处理,虚拟内存,I/O等方面的知识。
通过这篇文章的阐述,我们讲更加深入理解计算机程序是如何在计算机之中运行和存储的,为我们以后的计算机研究生涯打下更加坚实的基础。
关键词:hello程序;预处理;编译;汇编;链接;异常处理;内存 ;I/O
(摘要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 -
Hello程序一开始是作为由程序员写的高级语言hello.c诞生的。此时它可以叫做源文件,源程序。接下来,hello.c将通过cpp预处理器,被转换为hello.i文件;然后再经过编译器ccl,转换为hello.s;然后在汇编器as的作用下转变为hello.o;最后,通过链接器ld进行多个.o文件的链接过程,最终形成hello可执行程序。在执行此程序时,操作系统会为其进行fork操作,产生子进程,再调用execve函数加载进程。
操作系统会在调用execve后映射虚拟内存,删除当前的数据结构,为hello创建新的区域结构。在通过程序入口载入物理内存之后,进入main函数开始执行整个hello代码。结束后,由父进程会回收hello子进程,并由内核删除相关的数据结构。
硬件环境:Intel Core i7 x64CPU 16G RAM 512G SSD
软件环境:Win10教育版 64位
虚拟机VMware Workstation Pro12.0
开发与调试工具:visual studio, edb, gcc, gdb, readelf, HexEdit, ld
Hello.càhello.i(预处理生成的文本文件)àhello.s(编译后的汇编语言文件)àhello.o(可重定位目标文件)àhello(链接后的可执行目标文件)
其中每一步具体是由怎么转换的在1.1简介中已经有了详细的阐述。
本章主要介绍了P2P、020的具体过程,介绍实验环境等基本信息。
概念:预处理又叫预编译,是完整编译过程的第一个步骤。预处理就是预处理器(cpp)对hello.c文件进行文本替换处理的工作。将源代码中的预处理指令进行预先处理,清理和标记工作,然后将结果代码输出到hello.i文件中。
作用:预处理包含以下操作
在预处理之后,可以使得一个源代码编译程序可以在不同的程序运行语言环境中被各种语言编译器方便的进行编译。
gcc -E hello.c -o hello.i
打开预处理后的.i文件,发现原来只有短短几行的hello.c在预处理后文本量已经达到了3065行(如下图)
原因是由于原来.c文件中的宏在解析后展开加入,头文件内容也引入了。这对应着前面操作步骤的第一步,#include就被引入了,所以hello.c的main函数代码也就排在了文件的后面部分。
图:hello.i声明函数部分
除此之外我们也可以发现,注释部分也是全部都没有了,这也就刚好对应了前面过程的第5部分清除注释。
图:hello.i源码位置
在这一个部分,我们了解了预处理的含义,预处理的具体实现过程。然后在ubuntu下进行实际操作输入指令,对hello.c文件进行预处理,对得到的hello.i文件进行分析和截图保存结果。
预处理阶段是.c文件执行的第一步变换,后面将接着将.i文件进行许多处理。
(第2章0.5分)
概念:编译是程序构建的核心部分,编译阶段就是编译器ccl将文本文件hello.i进行一系列的词法分析、语法分析等后,产生汇编代码,也就是文本文件hello.s。hello.s是汇编语言程序。它给不同高级语言的不同编译器提供了可选择的通用的输出语言。
作用:
1.词法分析:词法分析会按照描述号的词法规则将预处理之后生成的源代码分割成记号,将数字、字符串常量存放到文字表。
2.语法分析: 语法分析器会以单词符号作为输入,分析符号串是否符合语法的逻辑规则,如果在解析过程中出错,有符号不匹配,缺少操作符等问题时,编译器会进行报错。
3.语义分析: 由语义分析器完成,对静态语义(在编译期间可以确定的语义)和动态语义(运行期出现的语义相关问题)进行分析。语义分析过程会将所有表达式标注类型,对于需要隐式转换的语法添加转换节点,同时对符号表里的符号类型做相应的更新。
4.代码优化:编译器优化器会在源代码级别进行优化,对于编译期间就可以确定的表达式给出确定的值,这样而已达到代码优化,提高执行效率的作用。
在汇编阶段,会对原来的hello.i进行语法分析,和目标代码的生成。当确认无误后,生成汇编语言文本。
gcc -S hello.i -o hello.s
首先打开hello.s文件可以发现,这个文件中的是汇编语言程序:
对于其中的数据类型在这一编译阶段的具体变化分析如下:
3.3.1:常量
图:hello.c中常数部分
图:hello.i中常数部分
比如原来hello.i文件中的main函数内部的语句,其中if判断中的“3”常数将会被保存在.text部分中。
我们还可以看到其中包含了printf函数将要打印的一些字符串常量“Hello 1190202102 heyang”,这些字符串将会被存储在.rodata(只读存储段)中。
3.3.2:全局变量
(1)对于已经初始化的全局变量(如下图的sleepsecs),会存储在.data节中
(2)对于还没有初始化的全局变量,会被存储在.bss段中。例如上图中的sleepsecs不赋值为2.5的情况。
3.3.3:局部变量
对于在函数内部定义的变量,例如:
会在栈帧,或者是寄存器中存储,不会在ELF可重定位目标文件中。
图:局部变量i的运用
3.3.4:静态变量
静态变量是定义时加上static的变量。这种类型我们的hello.c函数中没有涉及。
3.3.5:算数操作:
其中在hello.c中,唯一涉及算数操作的位置就是for循环中的部分:
这部分在hello.s中表现为54行的addl操作:
将存储在-4(%rbp)中的i变量加1。
3.3.6数组和指针结构:
在本程序中,此处运用了数组指针的概念。
argc代表的是传入参数的个数,也就是指针数组argv[]的个数。argv[0]是代表文件名filename。
这两条语句就是处理argc和argv位置的,两者分别存放在%edi和%esi中。
本章介绍了编译器ccl如何对hello.i文件进行处理,最终成为hello.s文件,分别对于其中,,,,,等数据类型和操作进行了分析。经过这一次的编译操作,hello程序就成为了更加低级的汇编语言程序。
(第3章2分)
概念:汇编是指汇编器(as)将汇编代码文件hello.s转变为可执行的机器语言的过程。并且将这些指令打包成一种叫做“可重定位文件”的格式,将结果保存在目标文件hello.o中。
作用:这个阶段的作用是可以将高级语言转化为可执行的机器指令,当然此时只是可重定位目标文件hello.o(二进制EFL文件),进行了符号解析和重定位,产生的EFL结构包含.text, .data, .bss, .reltext, .reldata, symtab, .rodata等节。
as hello.s -o hello.o
命令:readelf -a hello.o > ./elf.txt
将生成的elf.txt截图如下:
首先介绍以下EFL文件格式的主要情况:
对于EFL可重定位目标文件,主要包括以下几个部分:
(1).text:已经编译完成的机器代码。
(2).rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
(3).data:已经初始化的全局变量和静态C变量。局部C变量在运行时被保存在栈中,既不会出现在.data中,也不会出现在.bss中。
(4).bss:未初始化的全局和静态C变量,以及所有初始化为0的全局变量或静态局部变量。在目标文件中,这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已经初始化和未初始化的变量就是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
(5)symtab:一个符号表,这里存放所有在程序中定义和应用的函数和全局变量的信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表,其中不包含局部变量的条目。
(6).rel.text:一个.text节中的位置列表当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
(7).rel.data:被模块引用或定义的所有全局变量的重定位信息。
(8).debug:这是一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
(9).line:原始C源程序中的行号和.text节中机器指令之间的映射。
(10).strtab:一个字符串表,其中的内容包括.symtab和.debug节中的符号表,以及节头部中的节名称。
上图该部分表示的是EFL头部分:
显示头文件信息:objdump -f hello.o
这些就是上面说明的:
(1).text:已经编译完成的机器代码。
(2).rodata:只读数据。
(3).data:已经初始化的全局变量和静态C变量。
(4).bss:未初始化的全局和静态C变量,以及所有初始化为0的全局变量或静态局部变量。
(5)symtab:一个符号表,这里存放所有在程序中定义和应用的函数和全局变量的信息。
(6).rel.text:一个.text节中的位置列表当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
(7).rel.data:被模块引用或定义的所有全局变量的重定位信息。
(8).debug:这是一个调试符号表,其条目是程序中定义的局部变量和类型定义。
首先对hello.o的反汇编进行分析:
命令:objdump -d -r hello.o
分析由hello.o反汇编得到的代码,与第3章的 hello.s进行对照分析:
同时,在call指令中,hello.s中显示的是相关函数的名称,但是hello.o中指明了函数的偏移量位置:
在这一章节,我们详细地介绍了从hello.s到hello.o直接的转变过程。在经过编译器的操作,得到的二进制文件具有怎么样的EFL文件格式。其实这样的文件格式也是为了以后的转变为可执行程序做准备工作。
在分析EFL文件格式时,我们还列出来.text,.data,.bss,symtab等等结构的相关作用,让我们对于这个可重定位文件的结构有了直观的理解。
除此之外,我们通过对比由hello.o反汇编后得到的汇编语言代码和原来的hello.s汇编代码的差别,让我们对于这里的变化过程有了更加深刻的认识。
(第4章1分)
概念:链接过程通过链接器(linker)将生成的可重定位文件hello.o文件等将
各种代码和数据片段收集并组合成为一个单一文件hello(可执行文件)的过程,这个文件可被加载(复制)到内存并执行,所以hello文件叫做可执行文件。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是程序被加载器加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。
作用:链接将各个模块链接起来,成为一个巨大的的源文件,而各个模块可以进行分离编译。也正是这个特性,使得我们不用将一个大型的应用程序组织成为一个巨大的源文件,而是可以把他分解为更小的,更好管理的模块,可以独立进行修改和编译这些模块。
命令: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
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello > hello1.elf
现在是可运行文件,所有没有.rel.text, .rel.data两个节。
EFL头:
(1)这部分以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。
(2)EFL头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括EFL头大小,目标文件类型,机器类型,节头部表的文件按偏移
以上是虚拟地址空间的具体分布情况,接下来我们将会同过edb进行分析:
Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序。查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
命令:objdump -d -r hello
hello与hello.o的不同:
过程:链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
通过跟踪每一步的运行,记录call指令进入的函数,我们得到:
401000 <_init>
401020 <.plt>
401030
401040
401050
401060
401070
401080
401090 <_start>
4010c0 <_dl_relocate_static_pie>
4010c1
401150 <__libc_csu_init>
4011b0 <__libc_csu_fini>
4011b4 <_fini>
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
在edb中进行分析:
变量的话,可以利用利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
本章节的主要内容是了解链接过程中,由可重定位文件hello.o通过链接器是如何转变为hello可运行文件的。同时,我们还运用了edb软件工具来查看了hello的虚拟地址空间。
还在objdump -d -r hello命令的作用下,分析了反汇编指令和hello.o的不同。最后,对hello的动态链接进行了分析。链接是生成可执行文件的最后一步,是对各个库进行链接的环节,理解这一部分非常重要。
(第5章1分)
进程的概念:进程的经典定义就是一个执行中程序的实例。其他的说法还有进程是程序的一次执行,进程是程序及其数据在CPU下顺序执行时所发生的活动,进程是具有独立功能的程序在数据集上运行的过程,它是系统进行资源分配和调度的一个独立单位。
进程的作用:系统中的每个进程都是运行在某个进程中的上下文中的。上下文是由程序正确运行所需状态组成的。
每次用户通过向shell输入一个可执行目标程序的名字,运行程序时,shell就会创建一个新的进程,然后在新进程的上下文中运行这个可执行程序文件。应用程序也可以创建一个新的进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
作用:shell是计算机和用户之间的一个交互程序,负责解释命令,搭建用户,操作系统,内核直接的桥梁。
处理流程:
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把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命
令的检查:A. 内建的命令;B. shell函数(由用户自己定义的);C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。
在shell中输入命令之后,通过判断,入关不是内部指令,就会通过fork创建新的子进程。从fork函数开始以后的代码父子共享,即父进程要执行这段代码,子进程也要执行这段代码。(子进程获得父进程数据空间,堆和栈的副本。但是父子进程并不共享这些存储空间部分。父,子进程共享代码段。)现在很多现实并不执行一个父进程数据段,栈和堆的完全复制。而是采用写时复制技术。这些区域有父子进程共享,而且内核将他们的访问权限改变为只读的。如果父子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。
execve执行的结果是,需要改变进程执行的上下文。在输入命令的时候,将要运行的文件名filename装在argv[0]处,相关的环境变量装载在变量列表envp中。调用这个时,会保持相同PID不变,但是通过上文中存数各个变量的地方读取相关的值,覆盖原来的进程中值。
具体的实现步骤可以大致有几个部分:
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文信息:
进程通过上下文切换,控制流通从一个进程传递到另一个进程。如下图:
刚开始时,进程在Process A中,并且处于用户模式。之后,进入了内核模式,将相关的变量,栈,环境都设置好,然后切换上下文,切换到Process A中,这样一来就完成了进程的上下文切换。
进程时间片:
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
进程的调度:
进程的调度是由内核代码进行执行的。在进程从一个切换到另一个的时候就叫做进程的调度。这个过程中需要配置相关的变量,栈,环境等,此时,控制权交由内核代码,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
就如同上图中的kernel code部分中的箭头部分,内核掌握控制,设置好了变量后就在A,B进程之间成功切换调度了。
用户态与核心态转换:
用户态是指应用程序在正常执行过程中,拥有的状态,拥有的管理,调用权限仅限于系统允许的部分;但是在遇到故障、中断或陷入系统调用时,就会进行用户态与核心态转换,此时,在内核状态中,程序拥有系统中所有的访问等权限,对于情况进行处理,来保持系统的安全性。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
首先介绍异常的类型有哪些:
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
处理方式:面对异常,会有一个叫做跳转表的结构,里面包含着,面对传来的不同异常的信号,应该跳转到的处理程序的首地址。
在处理结束过后,我们还需要将刚才异常引起的没能好好执行的程序命令重新执行,步骤有下图中表现的三种:
回车:
Ctrl-Z:会发送SIGSTP 信号。当前进程收到这个信号之后,会将hello进程挂起,就相当于停住了。接下来我们继续用ps;jobs;pstree;fg;kill 等命令对于这个挂起的程序进行分析:
Ctrl-Z:会发送SIGSTP 信号。当前进程收到这个信号之后,会将hello进程挂起,就相当于停住了。接下来我们继续用ps;jobs;pstree;fg;kill 等命令对于这个挂起的程序进行分析:
Ps:查看进程的PID
Jobs:查看此时hello的后台
Ctrl-C:会发送SIGINT 信号。当当前进程收到这个信号之后,会将hello进程结束掉,用ps也无法查询到该进程的PID。
Pstree:
Kill:执行该条命令,选中的PID对于的进程将会被终止杀死掉,在后面的中查看,发现已经没有kill的那个进程了。
本章节我们从进程的角度首先理解了shell的工作原理,到底是怎样从用户端读入指令,由shell解读后转变为即将执行的操作。
除此之外,我们还了解了,进程切换中,用户态和内核态直接的变换过程,这有利于我们理解异常处理中的过程,是如何通过内核来处理异常。
最后,通过实际上手操作,对hello程序进行发送不同类型的信号,来观察程序运行的具体情况。通过用ps;jobs;pstree;fg;kill 等命令对于这个程序进行分析。我们对hello执行过程中产生信号和信号的处理过程有了更多的认识,对使用linux调试运行程序也有了更多的收获。
(第6章1分)
逻辑地址:可以认为是CPU在执行程序时候的中间地址。一个逻辑地址,是由一个段标识符加上一个指定段内的相对地址的偏移量。是指有程序产生的与段相关的偏移地址部分。(hello.o)
线性地址:是逻辑地址到物理地址变换之间的中间层。如果逻辑地址对应的是硬件平台段式管理转换前的地址的话,那么线性地址则对应了硬件页式内存的转换前的地址。
虚拟地址:是对于整个内存的抽象描述,相对于物理内存而言,虚拟内存并不是只有一个而与某一空间一一对应的。(1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保留活动区域,并根据需要在磁盘和主存之间传输数据。(2)它为每个进程提供了一致的地址空间,简化了内存的管理。(3)它保护了每个进程的地址空间不受其他进程的损坏。
物理地址:计算机系统的主存被组织为一个由M个连续的字节大小的单元组成的数组。每字节都由一个唯一的物理地址。它也就是出现在CPU外部地址总线上的寻址物理内存的地址信号。
逻辑地址是由段标记符和段偏移量两部分共同构成的。其中,段标识符是一个16位长的字符段,其中前13位是一个索引号。索引号可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
首先是CPU会产生一个虚拟地址VA,然后,首先我们会根据需要分为VPN和VPO。其中,虚拟页表存储的就是VPN。在多级页表中,如上图,我们首先会在CR3中存储第一级页表的物理地址,然后通过VPN1来定位第一级页表中的具体条目。
将这个取出来的条目作为第二级页表的首地址,同样的,VPN2就是用来定位第二级页表中的具体条目。
这样循环操作4次,就可以在最后得到PPN的地址值,然后将VPO作为PPO,将两者组合起来,就得到了我们需要的PA。
首先,CUP发出的VA地址,在MMU等虚拟地址处理操作完成之后,会得到一个内容就存储在主存中的物理地址PA,此时来访问该内容就会运用到多级的cache访存机制。
首先将PA分为CT(标记位)CS(组号),CO(偏移量)。由CS找到对应的组,然后通过检查标记位判断其中某一行是否有效,用CT判断这一行是否符合要找的那一组。
如果判断都符合,那么我们就可以认为查找成功;如果失败,那么就继续在L2,L3中进行查找。当在其中某一级判断查找成功的时候,就把数据传给CPU同时更新各级cache的cacheline,如果cache满了,就可以采用最近最少使用原则进行换入换出操作。
Fork函数被调用的时候,需要被内核创建所有的环境变量,环境信息等等。除此之外,分配给子进程一个新的PID。过程如下:
首先,它会再task_struct中新建一个mm_struct条目,用来存储新的进程的起始信息。然后,只需要将当前的这个mm_struct指向和父进程相同的位置,也就是共享了所有的数据和代码段。此外,需要将所有的页面都标注为私有的写时复制,这样就可以保障父子进程共享之前所有数据信息的同时,相互独立执行。
1.首先是在shell中会对于输入的命令行进行解析
2.在当前进程中加载运行execve函数主要需要完成以下四步的操作:(在映射方面可以看成是对于task_struct中的条目进行操作)
首先,删除已经存在的用户区域
再映射本加载进程的私有区域
再映射共享区域
最后设置程序计数器PC指向程序的代码区的开头,接下来程序将会从这里开始运行。由此以后,也会根据这个映射出来的虚拟页面对于需要,对代码和数据再主存中进行换入换出。
首先处理器会产生一个需要调用的虚拟地址VA
页表在对PTEA进行查询,返回这个单元中存储的PTE地址
MMU在读取后发现地址不合法,于是出发处理缺页异常程序
会在主存中,选择牺牲页,将磁盘中的需要读入的缺页重写回到主存
完成上述步骤后,重新执行引起缺页异常的步骤,即7号
隐式空闲链表的方法:
隐式空闲链表的每个块中,除了需要留出需要存储的块的大小的相关空间,还需要在头部的第一块中存储这个块到底大小有多大,是已经分配的块还是没有分配的空闲块的头部块。从而我们就可以利用这个块中存储的信息来做到遍历,删除这些块等等的操作。
遍历这些空闲链表块的方法也分为以下几种,首次适配和最佳适配。顾名思义,首次适配法的意思就是从头开始遍历这些块,当我们第一次找到一个大小大于我们需要存储的大小,并且是空闲块的,就作为我们申请的地址。这种方法的作用是一般得到的速度会比较块,但是缺点就是会造成一定的空间浪费,产生碎片。最佳适配的方法就是从头遍历,直到找到最适合的的大小,或者遍历完成后,从所有块中选出最为合适的块。着种方法的好处就是不会产生很多的空间浪费,产生内部碎片。但是坏处就是可能会使得访问速度相当慢。
显式空闲链表的方法:
这个存储的方法就是只将还没有进行分配的空闲块进行链接成一个链表的结构。这样,就可以大大的提升访问的效率,不再需要访问已经分配的块,效率和还没有分配的空闲块的多少成正比。
但是这样的方法不好的地方是,需要在头部额外分配空间存储指向下一块的空闲块的位置的指针,这样的化,由于对齐需求,可能会导致产生的块造成更多的空间浪费。
访问方法采用后进先出的顺序维护链表。这种方法是将最新释放的块放置在链表的开始处。
这个章节主要介绍了hello在存储过程中,不同的几种地址形式,比如线性地址,虚拟地址,逻辑地址,物理地址等等。然后还有intel的段地址管理方式。
接下来着重介绍了从虚拟地址到物理地址变化过程中,运用TLB和多级页表的机制,然后和从物理地址访问的cache机制结合起来理解。
最后是学习了fork,execve的内存映射,缺页故障等的处理问题的机制。
(第7章 2分)
设备的模型化:
所有的I/O设备都被模型化为文件,包括内核也被模型化和映射为文件。
设备管理,unix io接口:
首先这种unix io接口是所有文件管理方法中最底层的,最基础的,最安全的方法。保障了异步信号的安全。这是由Linux内核支持的应用接口。其中包括操作:
打开文件:open
关闭文件:close
读操作:read
写操作:write
指定文件中位置:lseek
Unix IO接口:
1.打开文件接口:
int open(const char *pathname, int flags, mode_t mode);
pathname :要打开文件的路径+文件名称
flags :以何种方式打开
返回值:open和creat都返回一个新的文件描述符(若是有错误发生返回-1,并在errno设置错误信息)
2.关闭文件接口:
int close(int fd);
fd:文件描述符
返回值:close返回0表示成功,或者-1表示有错误发生.
3.改变当前文件位置接口:
off_t lseek(int fd, off_t offset, int whence);
fd:文件描述符
offset:偏移量
whence:偏移到哪里去
返回值:成功完成后,Lseek()返回从文件开始以字节为单位测量的偏移位置。在出现错误时,返回值(off_t) -1,并设置errno来指示错误。
4.写文件接口:
ssize_t write(int fd, const void *buf, size_t count);
fd :文件描述符
buf :写什么数据
count :写入数据的大小
返回值:成功时返回所写入的字节数(若为零则表示没有写入数据).错误时返回-1,并置errno为相应值.
5.读文件接口:
ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:数据存储的空间,读到的数据存在哪里去
count:最大读多少,一般都是指的是buf空间大小减1
返回值:成功时返回读取到的字节数(为零表示读到文件描述符),此返回值受文件剩余字节数限制.当返回值小于指定的字节数时并不意味着错误
Unix IO函数:
其中函数的总类型,返回值等的全部情况在下面这张图片中应该概括完整,以下做一个简单的补充和梳理:
一个应用程序通过此方法来要求内核打开相应的文件,内核返回一个非负整数,叫做文件描述符,后续所有的操作都基于这个文件描述符。内核记录了对应文件描述符的所有信息,应用程序只需要记住这个描述符
用于关闭一个被打开的的文件。成功的时候返回0,现在异常的时候返回-1.关闭文件会通知内核已经完成访问该文件,不能重复关闭同一个文件,请务必检查返回码
从文件读取数据。读取文件会从当前文件位置复制字节到内存,然后更新文件位置(前提是文件支持seeking),返回从文件fd读取到buf的字节数
向文件写入数据。写入文件会将字节从内存复制到当前文件位置,然后更新当前文件位置(前提是文件支持seeking)
用于在指定的文件描述符中将将文件指针定位到相应位置。对于每个打开的文件,内核保存着一个文件位置k,表示从文件开头起始字节的偏移量,默认为0.应用程序可以通过seek显示的设置k的值
源码分析:
或者根据老师提供的链接中,表示为:
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函数的部分,这个函数的接口情况如下:
int vsprintf(char *buf, const char *fmt, va_list args)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
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码,直到接受到回车键才返回。
这一部分的主要内容是Linux的I/O接口以及函数的分析,这里我们比较系统的分析其中的内部实现。最后,还学习了解了printf函数和getchar函数内部实现的过程,对于实现时候调用的情况有了更加详细的了解。
(第8章1分)
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello程序是我们学习高级程序编写以来的第一个最简单的程序,但是那时我们还没有深入去了解这些程序在计算机内部具体的执行流程是怎样的。在我们深入了解了之后,我们才发现,原来一个简单的C语言程序需要经过这么多的步骤,经过这么多的变换才可以最终生成一个可执行程序,在操作系统中运行。
一般来说,主要的步骤可以概括为以下几步:
Hello.càhello.i(预处理生成的文本文件)àhello.s(编译后的汇编语言文件)àhello.o(可重定位目标文件)àhello(链接后的可执行目标文件)
此外,在执行过程中,遇到相关的信号需要处理,还有异常信号处理机制,从内核层级对于异常情况进行处理;
程序在计算机中的存储也和我们以前想象的不一样,首先有着虚拟内存这样的机制进行映射和保护。同时,还方便我们共享一些代码,标记位写时私有复制这样。
通过这样的学习,让我明白了计算机设计的巧妙,而我们也将要立志更加深入了解计算领域的内容,为了我国计算事业的发展做出更加卓越的贡献。
(结论0分,缺失 -1分,根据内容酌情加分)
列出所有的中间产物的文件名,并予以说明起作用。
(附件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分)