HIT-CSF2019大作业论文

HIT-CSF2019大作业论文

摘 要
本文以hello程序切入点,研究一个程序从一个高级C语言程序开始,经过预处理、编译、汇编、链接等到最后变成一个可执行文件的生命周期,以此来了解系统如何通过硬件和系统软件的交织、共同协作以达到运行应用程序的最终目的。将全书内容融会贯通,帮助深入理解计算机系统。
关键词:计算机系统;进程;汇编;抽象

目 录

第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简介
P2P:Program to Process
在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,产生子进程,产生子进程后shell为hello execve,于是hello便从Program(个人理解为程序项目)变为Process(进程),这便是P2P的过程。
2.O2O:Zero-0 to Zero-0
这里的020应该指的是Process在内存中From Zero to Zero。产生子进程后shell为hello execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,这便是020的过程。
1.2 环境与工具
硬件环境:
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:
Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位
开发与调试工具:
Visual Studio 2010 64位以上;GDB/OBJDUMP;DDD/EDB等
1.3 中间结果
hello.i —— 修改了的源程序(文本)
hello.s —— 汇编程序(文本)
hello.o —— 可重定位目标程序(二进制)
hello —— 可执行目标程序(二进制)
1.4 本章小结
本章内容是整个大作业的提纲,总结了hello程序一生经历的整体内容,为以后对hello的详细分析奠定了基础;对hello程序的分析是对所有程序的分析的缩影,将hello程序的一生分析透彻后,其他程序的运行也是类似的步骤。同时还列出了实验所需要的工具,为接下来实验提供了准备。

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在编译之前进行的处理。预处理主要有三个方面的内容:宏定义,文件包含和条件编译。宏定义预处理(预编译)工作也叫做宏展开:将宏名替换为文本。即在对相关命令或语句的含义和功能作具体分析之前就要换。文件包含预处理变译时以包含处理以后的文件为编译单位,被包含的文件是源文件的一部分。编译以后只得到一个目标文件.obj,被包含的文件又被称为“标题文件”或“头部文件”、“头文件”,并且常用.h作扩展名。条件编译预处理使用条件编译可以使目标程序变小,运行时间变短。此外,还有布局控制:#pragma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。
2.2在Ubuntu下预处理的命令
对hello.c进行预处理,输入gcc -m64 -no-pie -fno-PIC -E hello.c > hello.i,生成hello.i。

2.3 Hello的预处理结果解析
在预处理文本文件hello.i中,首先是对文件包含中系统头文件的寻址和解析,如下:

整个hello.i程序已经拓展为3116行

hello.i文件可以看到,在原有代码的基础上,将头文件stdio.h的内容引入,例如声明函数、定义结构体、定义变量、定义宏等内容。

通过结果分析可以发现,预处理实现了在编译前对代码的初步处理,对源代码进行了某些转换。另外,如果代码中有#define命令还会对相应的符号进行替换。

2.4 本章小结
本章了解了预处理的概念及作用,并实际练习了预处理的操作,解析了预处理过程中的内容,更加直观简洁,为代码的下一步编译做好了准备

第3章 编译
3.1 编译的概念与作用
编译的概念:
编译阶段是在预处理之后的下一个阶段,在预处理阶段过后,我们获得了一个hello.i文件,编译阶段就是编译器(ccl)对hello.i文件进行处理的过程。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。
编译的作用:
编译器会在编译阶段对代码的语法进行检查,如果出现了语法上的错误,会在这一阶段直接反馈回来,造成编译的失败。如果在语法语义等分析过后,不存在问题,编译器会生成一个过渡的代码,也就是汇编代码,在随后的步骤中,汇编器可以继续对生成的汇编代码进行操作。

3.2 在Ubuntu下编译的命令
我们可以利用如下指令来对hello.i文本继续进行编译:
linux> gcc –S hello.i –o hello.s
从图中我们可以看到,利用上述命令编译过后,我们得到了一个hello.s文件。

3.3 Hello的编译结果解析
将代码分成数据、赋值、类型转换、算数操作、控制转移数组/指针/结构操作和函数操作这么几个部分来具体分析一下。
1数据
关于数据的定义,我们可以看到hello.c中有一条如图中的语句,这条语句定义了一个sleepsecs的全局变量。对应到hello.s文件中,就是图中右侧的部分。我们可以看到定义的过程中用.globl声明了这是一个全局变量;.type说明了类型是一个数据;.size说明了这个变量的大小,这里sleepsecs变量占了4个字节。
2赋值
我们从上面的图中可以看到,在定义sleepsecs变量的过程中,同时对其进行了赋值在hello.s中。图中的语句就是赋值操作转化成汇编之后的语句。可以看到一共有三行,第一行声明了变量名,第二行中保存的是变量的值,第三行中是存储的位置,也就是.rodata节中(第五章链接的内容)。

   接下来我们看一下局部变量i是如何进行赋值的。可以注意到hello.s的36行中有一个对寄存器的寻址操作,这个操作将0放入到了这个地址中。可以确定这个就是对变量i的初始化,因为i是局部变量,所以直接可以在栈中用一个单元保存这个值,就不需要单独进行处理了。

3类型转换
这里的类型转换采用的是隐式的转换。我们可以从上一节的sleepsecs的赋值中可以看出,汇编器并没有对类型转换做特别的代码上的处理,而是直接将2赋值给了sleepsecs。这说明汇编器在对hello.i文件进行汇编的过程中,直接在这个过程中进行了代码的优化,也就是说在编译的过程中就完成了浮点型的转换,将其赋值给了整型,而并没有在汇编代码中通过具体的代码实现,所以对于类型转换而言,是隐式的。
4算数操作
在hello.c的代码中,只涉及到了一处算数操作,就是在for循环中每次对i进行的加一操作。在3.3.2节中,我们已经看到了,i存储在寄存器中保存的一个地址中,所以如图所示,对于i的运算,我们可以直接对寄存器保存的地址中的值进行操作,每次我们将这个值增加1。需要注意的一点是,每次我们进行的是寻址操作,是将地址中的值加1,而不是将地址加一。

5关系
在hello.c中有两个关系操作,分别如图所示,一个是不等于操作,另一个是小于操作。具体到汇编代码中我们看到他们分别对应了两句操作。一个是cmpl另一个是一个j加上一些字母。

这里就需要用到汇编语言的相关知识了。首先cmpl是一个比较函数,这个函数中将比较的结果保存在条件码中。条件码中一共有四位,每一位都有不同的含义。

6控制转移
我们看到,上一节中在每一个cmpl的操作之后,都紧跟着一个j的操作,这个j的含义就是jmp,起到控制函数跳转的作用,j后面跟的参数,就对应了在满足什么条件的时候进行跳转。图中列出了不同跳转指令的含义。我们可以看到,对于每一种跳转指令都对应了跳转码的一种形式。所以我们就可以知道,为什么在上一节中,cmpl和j这两条语句总是同时出现。是因为在执行条件跳转的时候,我们必须利用到操作码中的值。所以在每个条件跳转之前,都肯定有一个比较指令对操作码进行设置。
了解了条件跳转指令是如何执行的之后,我们可以看一下hello.s中具体的编译结果了。我们看下图中,右侧的汇编代码在执行左侧代码中的if判断操作,我们可以知道je是相等时跳转,也就是说,当argc等于3的时候,那么就跳转到.L2处执行,否则就继续向下执行。通过对L2的阅读以及L3中的操作我们可以知道,这是一个循环操作,也就正好对应了左侧代码中的情况。

7数组/指针/结构操作
hello.c中在输出的时候调用了argv数组中的元素。如图所示,我们可以看到,在汇编中,我们已经没有了数组、结构等概念,我们有的只是地址和地址中存储的值。所以对于一个数组的保存,在汇编中我们只保存了他的起始地址,对应的也就是argv[0]的地址,对于数组的中其他元素,我们利用了数组在申请的过程中肯定是一段连续的地址这样的性质,直接用起始地址加上偏移量就得到了我们想要的元素的值。

8函数操作
下图中展示了如何对函数进行调用。首先我们应该先了解一下调用函数的过程中我们会用到哪些东西。
%eax寄存器中保存了函数的返回值。作为一个函数,我们肯定需要向函数内进行传参操作,对于参数比较少的情况来说,就直接存储在特定的寄存器中,如%rdi,%rsi,%rdx,%rcx就分别用来存储第一至四个参数。X86的及其一共为我们提供了6个寄存器来保存参数。如果参数多于6个,那么就只能放在栈中保存了。
如图中所示,我们直接利用call指令,后面加上调用函数的名称,就直接可以去到被调用的函数的位置。在被调用的函数执行完毕之后,程序会将函数的返回值存在%eax中,然后执行ret语句,将函数程序返回到调用的地方。这样就完成了整个的函数调用。

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。

3.4 本章小结
本章我们主要介绍了编译器是如何将文本编译成汇编代码的。可以发现,编译器并不是死板的按照我们原来文本的顺序,逐条语句进行翻译下来的。编译器在编译的过程中,不近会对我们的代码做一些隐式的优化,而且会将原来代码中用到的跳转,循环等操作操作用控制转移等方法进行解析。最后生成我们需要的hello.s文件。

第4章 汇编
4.1 汇编的概念与作用
概念:
把汇编语言翻译成机器语言的过程称为汇编。
用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序。汇编程序的雏型是在电子离散时序自动计算机 EDSAC上研制成功的。这种系统的特征是用户程序中的指令由单字母指令码、十进制地址和终结字母组成。第一个汇编程序是符号优化汇编程序(SOAP)系统,它是50年代中期为IBM650计算机研制的。这种计算机用磁鼓作存储器,每条指令指出后继指令在磁鼓中的位置。当初研制SOAP系统的动机不是引入汇编语言的符号化特色,而是为了集中解决指令在磁鼓中合理分布的问题,以提高程序的运行效率。IBM704计算机的符号汇编程序(SAP)是汇编程序发展中的一个重要里程碑。此后的汇编程序大都以这一系统为模型,其主要特征未发生本质的变化。随着计算机软件的高速发展和广泛应用,汇编程序又吸收了宏加工程序、高级语言翻译程序等系统的一些优点,相继研制出宏汇编程序、高级汇编程序。
作用:
用汇编语言编写的程序,机器不能直接识别,需要将汇编语言翻译成机器语言,这种起翻译作用的过程叫汇编。
4.2 在Ubuntu下汇编的命令
在ubuntu的shell下输入“gcc hello.s -c -o hello.o”即可将hello.s文件汇编后生成hello.o文件。

4.3 可重定位目标elf格式
ELF格式分析:
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。

不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

symtab节:

重定位节:

其中,Offset是需要被修改的引用的节偏移;Info交代了需要修改的引用的信息;Type描述了被修改引用的重定位类型,其中主要包括32位PC相对地址的引用(R_X86_64_PC32)与32位绝对地址引用(R_X86_64_32);Addend是一个有符号常数,一些类型的重定位使用它对被修改引用的值做偏移调整。
4 Hello.o的结果解析
进行反汇编并将结果输入到hello.txt中。

hello.o的反汇编与hello.s的差别总体不大,主要体现在以下几方面:

全局变量引用 hello.o反汇编采用的是offset(%rip)的形式,而hello.s采用的是symbol(%rip)的形式。
函数调用 hello.o反汇编采用的是call offset的形式,而hello.s采用call symbol的形式。
分支转移 hello.o反汇编采用的是jmp offset的形式,而hello.s采用jmp Label的形式。
栈帧大小不同。
机器指令由指令指示符、(寄存器指示符)、(常数字)组成。
机器语言与汇编语言大致具有一一对应的关系。但有些特殊情况,比如:
转移控制 汇编语言中的jmp指令有直接跳转(在hello.o的反汇编中这个地址为绝对地址)和间接跳转,而转换成机器码后跳转指令会有几种不同的编码,最常用的是PC相对的,还有给出绝对地址的。而汇编器和链接器会选择适当的跳转目的编码。
一条指令可能有多个汇编码中的别名,例如jle和jg。
函数调用,在hello.o的反汇编文件中,call的地址是下一条指令的地址,而对应机器码中的操作码为0。这是因为hello.c中调用的函数都是共享库中的函数,在链接后才能确定函数的最终地址。因而在hello.o中只是将call的地址置为下一条指令的地址,而机器码的操作数则为目标位置(这里为下一条指令)相对于下一条指令的偏移,即0。
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章阐述了hello从hello.s到hello.o的汇编过程。分析了hello.o的ELF格式,并通过查看比较反汇编代码和汇编代码分析了汇编语言与机器码的关系。
汇编过程将汇编语言转换为机器码,生成可重定位目标文件,这个文件根据ELF格式对机器码进行打包,并为接下来的链接过程做好了准备。

第5章 链接
5.1 链接的概念与作用
概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以加载(复制)到内存并且执行。
作用
使得分离编译成为可能,不用将大型的应用程序组织成为一个巨大的源文件,而是可以将它分为更小,更容易管理的模块,可以独立的修改和编译这些模块。当我们改变其中的一个时,只要重新编译它,再链接上去应用就可以了。注意:这儿的链接是指从 hello.o 到hello生成过程。
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/5/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

5.3 可执行目标文件hello的格式

可执行目标文件的格式类似于可重定位目标文件的格式。ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.data、和.rodata节与可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时内存地址外。.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的,所以它不再需要.rel节。
5.4 hello的虚拟地址空间
将hello拖入edb中运行,在Data Dump中可以看到程序在0x4000000x401000段中载入,从虚拟地址0x400000开始,到0x401000结束。其余节.dynamic.shstrtab存放在虚拟地址0x40fff之后。

我们再回到图5.4所示的程序头表。程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐等信息。各表项功能如下:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:保存常量数据、程序目标代码等
DYNAMIC:保存动态链接器使用信息
NOTE:保存辅助信息
GNU_STACK:异常标记
GNU_RELRO:保存重定位后只读区域的位置
5.5 链接的重定位过程分析
输入命令objdump -d -r hello > hello.objdump反汇编hello,我们可以很明显地看到,这个反汇编文件比之前的hello_o.objdump长了很多,因为多了很多节,我们选取一部分列出:
.interp:保存ld.so的路径
.gnu.hash:GNU拓展的符号的哈希表
.dynsym:运行时/动态符号表
.rela.dyn:运行时/动态重定位表
.plt:动态链接-过程链接表
.rela.plt:.plt节的重定位条目
.init:程序初始化需要执行的代码
.fini:当程序正常终止时需要执行的代码
.dynamic:存放被ld.so使用的动态链接信息
.got:动态链接-全局偏移量表-存放变量
.got.plt:动态链接-全局偏移量表-存放函数
通过比较 hello.objdump 和 hello_o.objdump 了解链接器。
在进行链接后,程序将调用标准库中的函数加入到了程序的.plt节中,调用函数时,hello_o.objdump采用call+<相对main的偏移量>的方法,而在hello.objdump则可以使用call+<函数名>的方法。具体可看下图:

5.6 hello的执行流程
使用 edb 执行 hello,观察函数执行流程,将过程中执行的主要函数列在下面:
ld-2.23.so!_dl_start 0x00007f5ffaf54a50
ld-2.23.so!_dl_setup_hash 0x00007f5ffaf54a50
ld-2.23.so!_dl_sysdep_start 0x00007fcfbfcee210
ld-2.23.so!_dl_init 0x00007fcfbfce5740
hello!__libc_start_main@plt 0x00000000004004c0
ld-2.23.so!_dl_fixup 0x00007f9cb4e9c9f0
ld-2.23.so!_dl_lookup_symbol_x 0x00007f18464559d0
libc-2.23.so!__cxa_atexit 0x00007f18460bb280
libc-2.23.so!__new_exitfn 0x00007f18460bb0a0
hello!_init
libc-2.23.so!__sigjmp_save 0x00007f18460b6210
hello!puts@plt 0x00000000004004a0
hello!exit@plt 0x00000000004004e0
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表 GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
调用dl_init前的.got.plt节

调用dl_init后的.got.plt节
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello 的ELF格式,使用edb分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
在终端中键入 ./hello 1171820128 linmingcan,运行的终端程序会对输入的命令行进行解析,因为hello不是一个内置的shell命令所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件hello,之后终端程序首先会调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
加载器创建的内存映像如下

6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
简单看hello sleep进程调度的过程:当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程 ,来完成上下文切换。
如图,hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。
6.6 hello的异常与信号处理
如图(a),是正常执行hello程序的结果,当程序执行完成之后,进程被回收。
如图(b),是在程序输出2条info之后按下ctrl-z的结果,当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,此时他的后台job号是1,调用fg 1将其调到前台,此时shell程序首先打印hello的命令行命令,hello继续运行打印剩下的8条info,之后输入字串,程序结束,同时进程被回收。

   如图(c)是在程序输出3条info之后按下ctrl-c的结果,当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。

   如图(d)是在程序运行中途乱按的结果,可以发现,乱按只是将屏幕的输入缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做shell命令行输入。

(a)

(b)

(c)

(d)
6.7本章小结
Shell(Gnome-Terminal)下达命令,进程管理为hello提供了活动空间,Shell为其fork,为其execve,为其分配时间片,Linux是繁忙的,但是却依靠进程调度使得每个进程安稳运行。在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址空间:段地址:偏移地址
23:8048000 段寄存器(CS等16位):偏移地址(16/32/64)
实模式下:逻辑地址CS:EA=物理地址CS16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。
线性地址空间: 非负整数地址的有序集合::{0, 1, 2, 3 … }
虚拟地址空间: N = 2n 个虚拟地址的集合=线性地址空间
{0, 1, 2, 3, …, N-1}
物理地址空间: M = 2m 个物理地址的集合
{0, 1, 2, 3, …, M-1}
Intel采用段页式存储管理(MMU实现)
段式管理: 逻辑地址->线性地址==虚拟地址
页式管理: 虚拟地址->物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下:逻辑地址CS:EA=物理地址CS
16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理
hello的线性地址到物理地址的变换需要查询页表得出,hello的线性地址被分成两个部分,第一部分虚拟页号VPN用于在页表查询物理页号PPN,而第二部分虚拟页偏移量VPO则与查询到的物理页号PPN一起组成物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

虚拟地址VA被分成VPN和VPO两部分,VPN被分为TLBT和TLBI用于在TLB中查询。根据TLBI确定TLB中的组索引,并根据TLBT判断PPN是否已被缓存到TLB中,如果TLB命中,则直接返回PPN,否则会到页表中查询PPN。在页表中查询PPN时,VPN会被分为四个部分,分别用作一二三四级页表的索引,而前三级页表的查询结果为下一级页表的基地址,第四级页表的查询结果为PPN。将查询到的PPN与VPO组合,得到物理地址PA。
7.5 三级Cache支持下的物理内存访问
MMU发送物理地址PA给L1缓存,L1缓存从物理地址中抽取出缓存偏移CO、缓存组索引CI以及缓存标记CT。高速缓存根据CI找到缓存中的一组,并通过CT判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则继续从L2、L3缓存中查询,若仍未命中,则从主存中读取数据。
7.6 hello进程fork时的内存映射
当fork函数被新进程调用时,内核为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,加载并运行hello时出现的内存映射有:
映射私有区域 为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。Bss区域时请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域 如果hello程序与共享对象(或目标链接),比如C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

7.8 缺页故障与缺页中断处理
缺页故障:虚拟内存中的字不在物理内存中(DRAM缓存不命中)
如下图,VP3已经被映射到页表中,但却没有被缓存到物理内存中,此时堆VP3的引用会引发缺页故障。

缺页会导致页面出错引发一个缺页中断,而缺页异常处理程序会选择一个牺牲页(如下图选择了VP4,将VP4从内存交换到磁盘,并从磁盘读取VP3交换到物理内存)。

此时令导致缺页的指令重新启动,就可以使得页面命中了。
7.9动态存储分配管理
printf会调用malloc,接下来提一下动态内存分配的基本原理。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的需内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的,要么是内存分配器自身隐式执行的。
两种堆的数据结构组织形式:
带标签的隐式空闲链表
带标签的隐式空闲链表的数据组织方式如下图:

空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
显式空闲链表
显式空闲链表将链表的指针存放在空闲块的主体里面。堆被组织成一个双向空闲链表,在每个空闲块中,都包含一个pred和succ指针,如下图所示:

7.10本章小结
现代操作系统多采用虚拟内存系统,访存时地址需要从逻辑地址翻译到虚拟地址并进一步翻译成物理地址。
操作系统通过地址的页式管理来实现对磁盘的缓存、内存管理、内存保护等功能。
虚拟内存为便捷的加载、进程管理提供了可能。
程序运行过程中往往涉及动态内存分配,动态内存分配通过动态内存分配器完成。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的 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的实现分析
前提:printf 和 vsprintf 代码是 windows 下。
查看 printf 代码:
首先 arg 获得第二个不定长参数,即输出的时候格式化串对应的值。 查看 vsprintf 代码:
则知道 vsprintf 程序按照格式 fmt 结合参数 args 生成格式化之后的字符串,并 返回字串的长度。
在 printf 中调用系统函数 write(buf,i)将长度为i的 buf输出。write 函数如下:
在 write 函数中,将栈中参数放入寄存器,ecx 是字符个数,ebx 存放第一个 字符地址,int INT_VECTOR_SYS_CALLA 代表通过系统调用 syscall,查看 syscall 的实现:
syscall 将字符串中的字节“Hello 1171820128 linmingcan”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的 ASCII 码。
字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。
显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器 传输每一个点(RGB 分量)。
于是我们的打印字符串“Hello 1171820128 linmingcan”就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
对于 printf 和 getchar,hello 以前只知道调用之后一个能打印字符,一个能读入字符,可 究竟为啥,不知道,学习完 Linux 都市的 IO 管理手册之后,hello 多少明白了其中奥妙,原来他 们都是 Unix I/O 的封装,而真正调用的是 write 和 read 这样的系统调用函数,而它们又都是由 内核完成的,之所以键盘能输入是因为引发了异步异常,之所以屏幕上会有显示是因为字符串被 复制到了屏幕赖以显示的显存当中,至于其中细节,也值得好好研究一番……
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.程序编写;
2.预处理:将hello.c源程序的#开头的指令进行符号替换,生成hello.i文件
3.编译:将hello.i转化为汇编代码,生成hello.s文件
4.汇编:将hello.s转化为二进制的机器代码,生成hello.o的可重定位目标程序
5.链接:对hello.o中引用的外部函数、全局变量等进行符号解析,并重定位为可执行文件hello。
6.运行:在终端输入hello 117030
**
7.创建子程序:通过fork创建子程序;
8.执行:通过execve加载器载入,建立虚拟内存映射,设置当前进程的上下文中的程序计数器,使之指向程序入口处。CPU为其分配相应的时间分片,形成同其他程序并发执行的假想;
9.访存:CPU上的内存管理单元MMU根据页表将CPU生成的虚拟地址翻译成物理地址,将相应的页面调度;
10.动态内存申请:printf调用malloc进行动态内存分配,在堆中申请所需的内存;
11.接收信号:中途接受ctrl+z挂起,ctrl+c终止;
12.结束:程序返回后,内核向父进程发送SIGCHLD信号,此时终止的hello被父进程回收。
本次实验将这学期以来的所有知识点贯穿起来,从数据的运算处理,到汇编、链接、信号异常的处理、内存管理、再到后面的I/O管理。但是最让我震撼的是虚拟内存的实现,它将硬件异常、硬件翻译、汇编、链接、加载、内存共享等等一系列难题都大大简化了。将内存看作磁盘的高速缓存由它实现,从而使整个内存系统同时具备了高速度、大容量的特点;它为每个进程提供了一致的私有空间,从而大大简化了进程管理,同时它又保护了进程的地址空间不被破坏。虽然有时它会造成不易察觉的错误,但是无法否认,它极其的重要。因此,在硬件提高速度有限的同时,建立抽象模型,优化软件,在提高计算机性能方面也是信息信息时代技术革命不容忽略的主题。
附件
文件名 文件作用
hello.i 预处理之后的源程序
hello.s 编译之后的汇编程序
hello.o 汇编之后的可重定位目标程序
hello.objdmp hello的反汇编代码
hello 可执行目标程序

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值