HITICS 2019 大作业:程序人生

HITICS 2019 大作业:程序人生

摘 要

hello从诞生到消亡可谓是昙花一现,但他在生命周期中为我们带来的精彩与奥妙却未曾消散。预处理、编译、汇编、链接这些亲人们是怎样孕育这样一个凄美的生命,而hello为了完成他表演的梦想,又是怎样在shell-bash、OS与MMU等“恶霸”的威压之下实现从程序到进程的完美蜕变(P2P),得以在主存这个大家庭中栖身呢?最后hello又是怎样在一场完美的表演后安详地离开这个世界的呢?这一切的一切看上去是如此的不可思议与精彩纷呈,在这里我们就来仔细探究hello的一生,揭开他生命背后神秘的面纱。

关键词:hello;生命周期;P2P;O2O;系统
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述

1.1 Hello简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用
-2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
2.4 本章小结

第3章 编译

3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结

第5章 链接

5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处理流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献

第1章 概述

1.1 Hello简介
Hello程序的P2P过程:Hello程序的生命周期是从一个高级C语言程序开始的,之后通过预处理器cpp进行预处理,形成修改后的源程序hello.i,接着通过编译器ccl进行编译,形成汇编程序hello.s,再通过汇编器as进行汇编,形成可重定位目标程序,最后通过链接器ld将库函数与之链接,形成最终的可执行目标文件。这样,在shell中便可以创建进程来执行这个可执行目标文件。实现P2P的过程。
Hello的020过程:shell中创建作业,在作业中用fork()创建子进程,在子进程中便可以用execve函数来执行hello。读取hello文件的ELF头,形成可执行文件到虚拟内存的映射,这样虚拟内存中就形成了hello程序的页表。TLB中还存储着常用的虚拟页表条目,查找页表,映射到物理内存,发生缺页中断,进而将hello程序需要加载的部分加载到物理内存中去。执行结束后由父进程或养父进程进行回收,删除其所占用的资源,实现020的过程。

1.2 环境与工具
硬件环境:处理器:Intel® Core™ i5-8300H CPU @ 2.30GHz 2.30 GHz软件环境:Windows10 64位 ;Vmware Workstation 15.1.0 ;Ubuntu 18.04.3LTS 64位开发与调试工具:Visual Studio 2010 64位以上;CodeBlocks 64位;gedit+gcc;gdb与edb等
1.3 中间结果
hello.i:由预处理器cpp生成,解释hello.c源程序中#开头的命令,读取对应系统头文件并插入程序文本中。hello.s:由编译器ccl生成,将hello.i翻译成汇编语言程序。hello.o:由汇编器as生成,是由hello.s翻译成的机器语言指令,是一个二进制文件。hello:由链接器ld生成,由各个可重定位目标文件合并而来,是可执行目标文件,可被加载到内存中执行。
1.4 本章小结
hello程序从C语言源程序开始,经历预处理、编译、汇编、链接阶段,生成各种中间文件以及最终的可执行文件。之后在shell中的进程里运行,实现可执行文件到虚拟内存,虚拟内存到物理内存的映射,最终执行部分加载到物理内存中运行,进程终止后被父进程或养父进程回收,结束了他的精彩的一生。
(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用
概念:是C语言源程序编译之前的阶段,根据C语言源程序中以字符#开头的命令对其进行修改的处理阶段。
作用:根据#开头的命令对C语言源程序进行修改,如读取系统头文件并将其直接插入程序文本中或解释宏定义等。得到修改以后的C程序以供编译。
2.2在Ubuntu下预处理的命令
预处理命令:cpp hello.c -o
hello.i截图如下:
在这里插入图片描述
可用gedit hello.i查看hello.i的内容
在这里插入图片描述
2.3 Hello的预处理结果解析
gedit打开hello.i文件,发现main函数已经在文件的最末端,3099行以下:
在这里插入图片描述 经过观察可以发现,那些以#开头的头文件已经消失,
在这里插入图片描述
取而代之的是更多#开头的代码,如图:
在这里插入图片描述
这些代码是将#开头的头文件的具体代码插入进来进行头文件解释。因此预处理完成了对#开头的命令的“解释”,并将其对应的头文件或者宏定义插入程序文本的功能。
2.4 本章小结
预处理器为了让hello快乐起来,为他找来了许多许多的玩具!可是hello非但没有露出笑容,反而号啕大哭!!(预处理将以字符#开头的命令进行解释,并插入对应内容到程序文本中,实现了对hello的C语言源程序的进一步修改。)
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用
概念: 编译器ccl将修改后的源程序文本文件(xxx.i)翻译成汇编语言文本文件的阶段。
作用:将扩展源代码翻译为汇编代码。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -Og -S hello.i -o hello.s(-Og是实现优化,可选择不同优化程度,也可不加)
截图如下:
在这里插入图片描述
可用gedit hello.s打开查看hello.s的内容
在这里插入图片描述
3.3 Hello的编译结果解析
hello.c:
在这里插入图片描述hello.s:

在这里插入图片描述 在这里插入图片描述对照hello.c程序,以下按照程序执行的顺序对hello.s进行分析:
3.3.1:堆栈的使用。我们看到前面一条指令便是push语句压入栈数据。因为%rbp和%rbx是被调用者保存的寄存器,因此要使用该寄存器首先要保存其状态以供恢复。之后申请栈空间存放变量。
3.3.2:参数的传递。由于是64位系统,因此参数传递采用寄存器。故argc、argv分别存放在rdi,rsi寄存器中。
3.3.3:关系操作。hello.c中需要将argc与4进行比较,对应到hello.s中的汇编代码便利用cmp语句加上跳转实现if的分支cmpl语句实现argc与4的比较。
在这里插入图片描述
3.3.4:控制转移。上面的汇编语句同样实现了if 的控制转移jne可实现带条件的跳转,实现控制转移,这些条件与标志寄存器有关。当然也有无条件跳转:jmp指令便可实现无条件控制转移。在编译器编译时循环指令并不会被优化为Loop的循环指令,而是利用条件转移实现的。
在这里插入图片描述
3.3.5:赋值操作。
在这里插入图片描述
通过leaq指令可以实现赋值,这里通过leaq指令计算赋给寄存器的值是一个地址,用以实现函数调用。除此之外,还可通过mov指令实现赋值操作。如下的mov指令都是赋值操作。mov指令是非常常见的数据传送指令,可以实现赋值操作。
在这里插入图片描述
3.3.6:函数调用。通过call指令实现函数调用。如下call语句对应着C程序中诸如printf,exit,getchar,sleep函数的调用的汇编实现。
在这里插入图片描述
函数的返回结果都保存在寄存器%eax(%rax)中。
3.3.7:全局变量。
在这里插入图片描述
.L6中首条指令用leaq指令得到的地址便是一个全局变量的地址。全局变量的地址可通过基于rip的相对寻址得到。这个全局变量便对应着argc!=4时要打印的字符串。
3.3.8:局部变量。局部变量用寄存器来实现。如本源程序中的局部变量i,便是用%ebx来实现。
在这里插入图片描述
这一指令便对应着i的初始值为0.
3.3.9:指针/数组操作。由于argv是指针数组,每一个元素都是8字节指针,因此可利用带比例因子的相对基址变址的寻址方式实现。
在这里插入图片描述
在这里插入图片描述
3.3.10:++操作。C程序中每一次循环都会对循环变量i进行++操作,但在编译器优化时也未使用简单的INC指令,而是由add指令来完成。
3.3.11:函数返回。利用ret指令即可。但在返回时要进行堆栈平衡,同时函数的返回值保存在%eax(%rax)中。
在这里插入图片描述
3.4 本章小结
编译过程是hello人生过程中相当重要的一部分,它是高级语言的抽象与机器执行的枯燥指令之间的桥梁。同时我们也能发现编译器对于这些许许多多的“hello”孩子们的普适关爱:为了追求普适性和正确性,这些++操作、循环操作等会使用一样的诸如add指令与条件跳转来执行,而不是“偏爱”地对某些使用INC或是LOOP操作。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用
概念:在编译阶段之后,通过汇编器将.s文件翻译成机器语言指令,打包成可重定位目标文件的阶段。
作用:将汇编语言翻译为机器真正能够识别的机器语言指令,生成可重定位目标文件,这些文件是二进制文件,链接之后便可以真正的执行。
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc -Og -c hello.s -o hello.o(-Og是实现优化,可选择不同优化程度,也可不加)或者as hello.s -o hello.o
截图如下:
在这里插入图片描述
在这里插入图片描述
用gedit打开会是乱码。
在这里插入图片描述
4.3 可重定位目标elf格式
ELF头如下:
在这里插入图片描述 各节基本信息如下:
在这里插入图片描述 可重定位条目:
在这里插入图片描述
在这里插入图片描述
.rela.text是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,调用本地函数的指令则不需要修改。
offset表示需要进行重定向的代码在.text或.data节中的偏移位置,Type表示着该符号的重定义类型:是相对地址引用还是绝对地址引用。Addend是重定位时需要的偏移调整。
4.4 Hello.o的结果解析
在这里插入图片描述
与第三章hello.s对比分析,并与机器代码相结合进行分析:
在这里插入图片描述
在这里插入图片描述
发现凡是与地址相关的汇编指令全都不一样。
<1>先来分析机器代码的构成:机器指令由操作码和操作数两部分组成。操作码指定该指令要完成的功能,操作数指定该指令执行的对象。以callq 21及其机器指令e8 00 00 00 00为例,e8是call指令的机器码,后面的00 00 00 00应该对应21所表示的地址,只不过这里因为还未进行链接,所以地址无法确定,由0进行占位。
<2>再来分析机器代码与汇编代码的映射关系:机器代码的操作码部分映射为汇编代码的一种类型的指令,如e8可映射为函数调用的call指令。操作数部分可以映射为立即数,也可映射为寄存器的标号。映射的立即数既可能是要参加运算的立即常数,也可能是一个地址常量。
<3>与hello.s对比分析:
①分支转移不同。hello.s中无论是条件跳转还是无条件跳转,都使用诸如.L6,.L2等标志来标识;而hello.o的反汇编代码中是有一个明确的地址如jne 15,jmp57等等。但要注意,机器代码与之又有一些不同,如jne 15对应的机器码是75 0a,操作码部分75对应指令jne很正常,但是0a却对应的是0x15,这是因为分支转移的机器码计算的地址常量都采用的是距离表示,即要跳转到的地址的常值减去%rip的值即为机器码中的操作数。如75 0a指令跳转到0x15处,%rip此时的值是0x0b,因此相减得到距离0x0a。
②全局变量的引用不同。由上面重定位条目的分析可知,引用全局变量的指令需要在链接重定位时进行修改,因此在可重定位目标文件hello.o中对全局变量的地址还是不确定的,因此机器码中用0占位,汇编代码中则是%rip的值。如对argc!=4时要打印的字符串的引用:
在这里插入图片描述
地址还是不确定的因此用0占位。
③函数调用不同。由上面重定位条目分析可知,调用外部函数的指令也需要在重定位时进行修改。而在hello程序中调用的函数都为printf、exit、sleep、getchar等库函数,这些都属于外部函数的调用,在未链接重定位之前地址也是未知的,因此在机器码中用0占位,汇编代码中则是%rip的值。如对printf函数的调用:
在这里插入图片描述除此之外,二者其他地方都是相同的。
4.5 本章小结
hello程序又变得晦涩难懂了几分,现在真正地变成了机器代码,要面对冷血无情的机器了呢!可以通过readelf了解hello目前的情况。但是此时hello还有一些不完善的地方,因为它对那些外部的事物还不了解(外部函数、全局变量),因此它不能对这些有清醒的认识,所以把他们全都画成了圈圈(0占位)。
(第4章1分)

第5章 链接

5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接利用重定位信息将各种代码和数据包括库函数重新组合,生成可执行文件。同时,编译也使得分离编译成为可能。利用链接我们可以将一个大型的应用程序分解为更小的便于管理的模块,独立修改和编译这些小模块,最终链接即可。
5.2 在Ubuntu下链接的命令
使用ld的链接命令: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执行文件打开以后也为乱码:
在这里插入图片描述
5.3 可执行目标文件hello的格式
hello的elf头如下:
在这里插入图片描述
在这里插入图片描述
其中各个条目包括了各段的基本信息:size标识了该段的大小,address标识了该段的起始地址,offset标识了该段相对于文件的起始的偏移量。以.rodata段为例,它的size是0x3d,说明其大小是61个字节,它的Address是0x400670,这是它的起始地址,它的offset是0x670,说明.rodata段相对于该文件起始的偏移地址为0x670。
5.4 hello的虚拟地址空间
使用edb加载查看hello:
从Data Dump窗口可观察到各个地址的详细内容:
在这里插入图片描述
对应5.3中内容,虚拟地址空间从0x400000开始,到0x401000结束,在窗口中已经给出标识。可以详细去对应5.3中的每一段进行查看:
这里列出5.3中部分段在Data Dump中的对应:
在这里插入图片描述
0x400200至0x40021c为.interp段。
在这里插入图片描述
0x400550至0x400664为.text段。在这里插入图片描述
0x400670至0x4006b0为.rodata段。
在这里插入图片描述
我们还可以利用edb中的Symbol Viewer窗口进行简洁地对比:
在这里插入图片描述
可以发现每一个段对应的虚拟地址空间确实同5.3中elf头所指示的信息相同。
5.5 链接的重定位过程分析
部分反汇编截图如下:
在这里插入图片描述
在这里插入图片描述
与hello.o进行对比:
在这里插入图片描述
<1>反汇编对比分析:
①由图中可以非常明显地看出一点不同:hello的反汇编代码中添加了许多hello.o中没有的东西:那些在hello.o中没有明确地址和内容的函数在hello中有了明确的地址与内容。这是因为链接器已经将每一个符号的引用与符号表中明确的定义关联起来,故在重定位时可以明确地将各个可重定位目标文件中的代码和数据进行重新组合,将那些原本在hello中引用的全局变量和外部函数整合了进来。
②我们还可以发现原来在hello.o中不明确的占位地址在hello中已经变成了明确的绝对地址或者相对地址。如原本在hello.o中对全局变量格式串的引用
在这里插入图片描述
在hello中已经变为基于%rip的明确的引用:
在这里插入图片描述
这是因为链接器已经对其完成重定位,将输入模块合并,为每个符号分配了明确的运行时地址。
<2>链接过程
由此分析我们可以得出链接的过程:
①链接的第一步是符号解析。符号解析将每一个符号的引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。局部符号的解析相对简单,因为我们知道诸如call以及静态局部变量的引用时是基于%rip的相对寻址,而这些局部符号在本文件的偏移是固定的,因此相对简单。而对于全局符号的解析则很复杂。首先在编译时编译器遇到一个并不在当前模块中定义的符号,它假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目。于是在链接时,链接器在它的所有模块中寻找该引用符号的定义,找到时将二者相关联,否则报错并终止。
②链接的第二步是重定位。首先重定位节和符号定义:链接器将所有相同类型的节合并为新的聚合节,然后链接器将运行时的内存地址赋给新的聚合节以及输入模块中定义的每一个符号。此时,每一条指令以及全局符号都有了自己明确的内存地址。接着就可以重定位节中的符号引用:基于符号解析时建立的符号引用与符号定义之间的关联,对引用进行修改。因为此时符号定义的地址已经修改,所以要修改这些引用以让它们指向符号定义的正确地址。重定位的具体分析如下。
<3>hello的重定位分析
我们与4.3中的重定位条目结合分析:
在这里插入图片描述
这些条目之前有过解释,offset是该符号在文件中的偏移位置,type可以标志重定位的类型。以重定位类型为R_X86_64_PC32的为例,如重定位条目中的第一个:通过其offset可以得到需要重定位的该符号的引用位置,因为每个节的位置现在已经是明确的地址了,将该地址与偏移量offset相加便可以得到待重定位的符号引用的位置,即refptr=s+r.offset。接下来便可以对refptr处的符号引用进行重定位。该符号重定位类型是R_X86_64_PC32,即使用PC相对寻址,因此我们需要计算该符号定义的内存地址与PC的差值。将该符号定义处的内存地址用ADDR(r.symbol)表示,refaddr为此时PC的值,二者相减即可得到重定位的内存地址。但是这个差值还需要该符号的addend进行偏移调整,故完整的重定位表达为:*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr);这里将最终的地址转为无符号整型。如果是使用绝对寻址的话,表达如下:
*refptr=(unsigned)(ADDR(r.stmbol)+r.addend) 无需减去PC的值,只需根据符号定义处的内存地址进行根据addend的偏移调整即可得到。
经过这样重定位条目的分析和计算便可得到重定位的引用。
5.6 hello的执行流程
调用与跳转的各个子程序名及程序地址如下:子程序名程序地址
<ld-2.27.so!_dl_start>0x7fcb305feea0
<ld-2.27.so!_dl_init>0x7fcb3060d630
<hello!_start>0x400550
<libc-2.27.so!__libc_start_main>0x7fcb3022dab0
<libc-2.27.so!__cxa_atexit>0x7fcb3024f430
<hello!__libc_csu_init>0x4005f0
<libc-2.27.so!_setjmp>0x7fcb3024ac10
<hello!main>0x400582
<hello!puts>0x4004f0
<hello!exit>0x400530
5.7 Hello的动态链接分析
在dl_init之前各个寄存器的值如下图:
在这里插入图片描述
在dl_init之后各个寄存器的值如下图:
在这里插入图片描述
5.8 本章小结
hello泪流满面,链接器为他带来了许多跟他相仿的小伙伴,大家一见面都泪流满面:原来不只有我一个人这么惨!很快便自觉形成内存中的一个大整体!(同类节合并)大家不分彼此,一个人使用别人的东西总是能得到响应!(外部函数、全局变量重定位完成,使得符号引用有确定的内存地址)。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用
概念:狭义上:进程的经典定义就是一个执行中程序的实例。
广义上:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
作用:进程提供给应用程序两个关键抽象:
1. 一个独立的逻辑控制流,它提供一个假象,好像我们的每个程序都独占的使用处理器。
2. 2. 一个私有的地址空间,它提供一个假象,好像我们的每个程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个交互型的应用级程序,它代表用户执行程序。Shell执行一系列的读和求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并代表用户运行程序。不仅如此,shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。shell中可以同步和异步的执行命令。在同步模式,shell需要等命令执行完才能接收下面的输入。在异步模式,命令运行的同时,shell就可以接受其它的输入。shell还有重定向功能,更细致地控制命令的输入输出。
处理流程:这里以简单的shell为例进行分析。在shell的main函数中,它可以分为两部分。第一部分是对用户的命令行进行读取,第二部分是对该命令行求值。第一部分比较简单,重点在第二部分,这一部分由eval函数完成。在eval函数中首先通过parseline函数将以空格分隔的命令行参数进行解析,并以此构造argv向量。之后判断用户的输入是否为内置命令,如果是内置命令,则立即对该内置命令进行解释并作出对应的反应。如果不是内置命令,则需要执行用户所请求的程序。具体来说,shell用fork函数创建一个子进程,在子进程中利用execve函数执行用户所请求的程序,在父进程中等待子进程终止并进行回收。终止后开始下一轮迭代。
6.3 Hello的fork进程创建过程
在shell的main函数中可通过fork函数创建一个新的子进程,这个新创建的子进程拥有着与父进程的虚拟地址空间以及打开的文件相同但独立的一份副本,二者仅有PID不同。子进程保留有父进程在调用fork函数时的状态,此后父进程与子进程便成为并发的进程,执行顺序不定。fork函数在两个进程中有着不同的返回值,在父进程中返回子进程的PID,在子进程中返回0,由此便可区分二者,并利用此当作判断条件,选定子进程,令其利用execve函数来执行hello程序。于是,这样便利用fork函数创建了一个可以执行hello的子进程。
6.4 Hello的execve过程
execve的函数原型为:int execve(char *filename,char *argv[],char *envp[])
execve函数加载并运行可执行目标文件filename,argv是其参数列表,envp是环境变量列表。令hello作为其第一个参数,使其可以运行hello程序。执行时其会删除当前子进程现有的虚拟内存段,创建一组新的段,其中新的代码段与数据段初始化为可执行文件hello的内容,实现覆盖(但还保留相同的PID),堆与栈初始化为0,同时将虚拟空间中的页映射到可执行文件对应内容实现页分配。之后调用启动代码,启动代码设置栈,将控制传递给hello程序的主函数,之后在具体执行hello的指令时由于未映射到物理内存会触发缺页中断并通过处理程序加载运行。
6.5 Hello的进程执行
进程的执行并不一定是完整、连续地执行完成,每一个进程在执行时都会有其对应的时间片。如hello程序在开始被操作系统调度执行时会分配一个大小固定的时间片,当时间片的时间用尽后或者被其他信号(如硬件异常)中断,如果hello程序此时还未执行完成,内核也可以决定抢占当前进程,重新恢复一个之前被抢占的进程如hello2,实现进程的调度。如hello与hello2两个进程,开始时hello进程在用户模式下执行,操作系统为其分配了固定大小的时间片,时间片用尽后hello程序还未执行完毕,但控制仍然转移给操作系统进入到内核模式,此时由内核选择执行哪一个进程,这个选择称作进程调度。这里假设操作系统调度进程hello2,之后将hello的上下文信息保存,恢复hello2的上下文信息,将控制转移给hello2,之后再次进入用户模式在固定时间片内执行进程hello2.之后重复上述过程至hello与hello2进程终止。
6.6 hello的异常与信号处理
分析如下:
首先带参数运行hello程序,这里将sleep秒数设的比较大方便操作。如图:
在这里插入图片描述
首先尝试不停按回车:这属于硬件异常,进程正常执行,并没有任何反应,如图:可见默认处理行为为忽略。
在这里插入图片描述
接着我们尝试按下Ctrl+C,这是来自键盘的中断,会产生SIGINT信号,对该信号的默认处理行为是终止前台进程,可用ps查看进程,发现hello进程确实已经终止,用jobs查看也已经并不存在作业了。如下图:
在这里插入图片描述
在这里插入图片描述
接着尝试Ctrl+Z,这属于来自终端的停止信号,会产生SIGTSTP信号,对该信号的默认处理行为是挂起前台进程,按下Ctrl+Z时便以提醒进程已停止,如图:
在这里插入图片描述
也可以用ps和jobs查看,发现hello进程、作业确实还存在,只不过处于停止态,如下图:
在这里插入图片描述
在这里插入图片描述
在Ctrl+Z使其处于停止态后尝试用fg命令,发现hello进程重新变回运行态,并且是前台作业。
fg是linux的内置命令,其也会产生SIGCONT信号,要求停止的进程继续进程,对该信号的默认处理行为是忽略,但是会令进程的状态发生改变,即从停止态变为运行态,如下图:
在这里插入图片描述
pstree命令打印如下:hello仍在正常运行
在这里插入图片描述
使用kill命令如下,采用kill默认发送的信号,即SIGKILL,要求杀死程序,对该信号的默认处理行为便是终止指定进程。如图:再次用ps查看时,该进程已被killed。
在这里插入图片描述
令该程序正常运行直至结束,将sleep秒数调小,设为1秒,再用ps查看,如图:
在这里插入图片描述
发现这里进程已经终止且被回收了。子进程运行结束时产生子进程终止终止信号SIGCHLD发送给父进程,对该信号的默认处理行为是忽略,但其父进程或init养父进程可以对其进行回收。
6.7本章小结
shell见到hello觉得瞧不上眼,便只安排自己的儿子(子进程)去找hello玩,shell的儿子带着hello与他的朋友们进行赛跑(进程间的并发),你追我赶(进程的调度),越过各种障碍(异常与信号),最终全都到达了终点(终止),非常开心。后来孩子们都被家长们带回了家(子进程被父进程回收)。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间
①物理地址是用于内存芯片的单元寻址,与处理器和CPU连接的地址总线相对应。即地址总线能够切实寻址的地址,与物理内存一一对应,对应到hello程序来说就是hello在真正被执行时机器访问用地址总线访问的hello的地址
②逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址可以表示为[段选择符:偏移地址]。对应到hello程序来说,你可以说某个变量的地址是xxx,这就是它的逻辑地址,是它相对于hello进程的数据段的偏移地址,与物理地址相干。但在Intel实模式下逻辑地址CS:EA =>物理地址CS*16+EA有这样的转换关系。
③虚拟地址指有程序产生的由段选择符和段内偏移地址组成的地址,是一种抽象,我们看到的hello的地址如0x400582等都是虚拟地址,每个进程都会有这样一个独立的虚拟地址空间,访问时先给出逻辑地址,需要再转换为虚拟地址,再通过虚拟地址到物理地址的映射转换为物理地址实现寻址。
④线性地址指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间中的地址。程序代码会产生逻辑地址,也就是段中的偏移地址,加上相应的段基址就成了线性地址。可表示为[段描述符:段偏移]形式。如果开启了分页机制,那么线性地址需要再经过变换,转为为物理地址。如果无分页机制,那么线性地址就是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
这里以48位逻辑地址为例。首先将逻辑地址分为两部分,一部分是段地址16位,另一部分是偏移地址32位。这段地址的16位用来存放段选择符。如图:
在这里插入图片描述
段选择符的RPL字段用来标识CPU当前特权级,是处于用户模式还是处于内核模式;TI字段用来标识描述符是存放在全局描述符表中还是局部描述符表中;其余作为索引位,用来确定段描述符在TI所确定的描述符表中的位置。这样一来,我们便可以根据段地址来确定段描述符的具体位置,将被选中的段描述符送至描述符cache,每次可以从cache中取出32位的段基址,而逻辑地址的后32位则确定了在该段内的偏移量,将段基址与段内偏移量相加即可的到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
要想实现线性地址到物理地址的变换,我们首先要知道虚拟内存系统到物理内存的映射是通过页表这一数据结构实现的。页表是一个页表条目的数组,每一个页表条目分为有效位和地址字段两部分。有效位为1,该地址字段则保存着缓存该虚拟页的物理地址。
得到n位线性地址以后,仍是将其分为两部分:p位的虚拟页面偏移以及n-p位的虚拟页号。我们首先可以从页表基址寄存器PTBR中读取当前进程的页表首地址,用该地址与n-p位的虚拟页号组合便可访问对应的页表条目。当该页表条目中有效位为0时,意味着该页面在物理地址中还没有映射,形成非法访问(在页表条目未分配时)或是缺页中断(页表条目已分配但未缓存);反之则说明虚拟页面已在物理页面中缓存,此时页表条目中的地址字段应该存放着物理页的页号,设为m-p位,用这m-p位的物理页号与线性地址中p位的页偏移量相组合即可得到m位的物理地址。这里之所以能直接使用线性地址中的虚拟页偏移量作为物理页偏移量是因为物理页面和虚拟页面的大小都是相同的,均为p字节,故二者对应的偏移量也是相同的。页式管理示意图如下:
在这里插入图片描述
7.4 TLB与四级页表支持下的VA到PA的变换
实际系统中计算机采用TLB与四级页表来进行优化。首先我们还是得到n位的虚拟地址,这里以64位系统为例,目前的虚拟地址是48位,即n=48.
还是将这48位虚拟地址分为两部分,36位的虚拟页号和12位的页面偏移。
根据这36位的虚拟页号首先去TLB中查找。TLB是虚拟寻址的缓存,每一行都保存着一个页表条目PTE。由于TLB是高度组相联cache,将虚拟页号VPN拆分成两部分:32位的TLBT和4位的TLBI进行TLB中的索引。根据4位的TLBI确定TLB中的组号,再根据TLBT进行组中每一行的比较。如果对应页表条目在TLB中命中,则可以直接从TLB中读取页表条目,得到页表条目中的物理页号,将其与12位的页面偏移相组合即得到物理地址。
如果在TLB中页表条目未命中,则仍需要访问主存中的页表,只不过计算机采用了4级页表来对页表进行压缩。这里仍需要对虚拟页号VPN进行分割,只不过这里的分割方法又有些不同,将其分为4部分,每一部分都是9个字节单独作为每一级页表的虚拟页号,如高9位可以作为1级页表的虚拟页号,低9位作为4级页表的虚拟页号。1-3级页表中的页表条目每一个页表条目存放一个下一级页表的基址,如1级页表中的第3个页表条目可能存放着第三个2级页表的基址。最终的第四级页表中的页表条目则存放着虚拟页面所映射的物理页号。这样,首先通过CR3获得第一个页表的基址,再通过4个VPN,每一个VPN对应着一个该级页表的虚拟页号,1级页表VPN确定一个条目,确定读取哪一个二级页表,再结合二级页表的VPN读取二级页表条目···以此类推,可从某一个四级页表中得到对应的物理页号,将其与12位的页面偏移结合即可得到物理地址。地址翻译示意图如下:
在这里插入图片描述
7.5 三级Cache支持下的物理内存访问
这里详细介绍1级cache的访问细节,二级cache、三级cache与之类似。
这里的cache是组相联的缓存每一组又分为多个块,每一块的大小为64字节。以L1 d-cache为例,如上图。其分为64组,每一组有8块。得到物理地址后仍需要将物理地址PA进行分割为三部分,如上图所示,分为CT,CI,CO三部分,其中6位的CI是组索引,根据CI的值确定去L1 d-cache中的哪一组去寻找。40位的CT是标记位,cache中每一块也都具有标记位,将该组的每一块的标记位与CT进行比较,如果找到相等的一块,查看该cache块的有效位,如果有效位有效,则说明缓存命中,这时便可以利用CO,由CO确定从cache块的64个字节当中哪一个字节开始读取。这是L1命中的情况。
当然,也存在L1不命中的情况。这时要去更高级的cache中重复上述过程,此时还需观察L1中是否存在空闲块,因为在高级cache中访问的内容还要再加载到L1中。如果此时L1中存在空闲块,即有效位为0的块,可以直接将访问内容加载到L1中,并更新有效位,否则需要用LRU、LFU等算法找到牺牲块,将牺牲块的内容替换为访问内容。
如果三级cache全部未命中,则非常不幸,需要去主存中去访问,并且逐级加载到各级cache中去,最终实现访问。
7.6 hello进程fork时的内存映射
fork函数为子进程创建虚拟内存,创建子进程的mm_struct ,vm_area_struct和页表的原样副本。同时将父进程与子进程两个进程中的每一个页面都标记为只读。同时将两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制。此时它们映射到相同的物理地址,但物理地址作为它们的私有写时复制对象,在对共享物理页进行写操作时会触发保护故障从而实现写时复制,创建新的页面,写的部分映射为不同部分。
7.7 hello进程execve时的内存映射
execve函数首先删除已存在的用户区域,接着创建新的区域结构,这些区域结构是私有的、写时复制的。即除共享函数区外的区域都是私有的。同时将代码区映射为hello程序中的.text区,初始化的数据区映射为hello程序的.data区,.bss区以及用户堆、用户栈都映射到匿名文件,用户堆以及用户栈的初始长度为0,共享对象则由动态链接映射到hello进程的共享区域,在加载运行hello时完成链接。最后设置程序计数器PC,使之指向代码区域的入口点。这便完成了execve函数中对于hello进程的内存映射,实现加载并运行hello程序。示意图如下:这里的a.out代表hello.out
在这里插入图片描述
7.8 缺页故障与缺页中断处理
execve函数进行的内存映射只是从可执行文件hello到虚拟内存的映射,并未建立与物理内存之间的联系。实际上,所有代码和数据全部都是通过缺页中断异常处理子程序实现到物理内存的加载的。
MMU在翻译虚拟地址时可能会触发缺页故障,该异常会导致控制转移到内核的缺页处理程序。缺页中断处理过程如下:
①首先判断该虚拟地址是否是合法的。我们知道在页表中存在着大量未分配的条目,这意味着虚拟内存中实际上有很多空间是未定义的。因此任意给出一个虚拟地址,这个地址有可能并不在进程区域的结构体定义区域内。如下图所示:
在这里插入图片描述
可以将该虚拟地址与每一个进程区域的vm_area_struct结构体中的vm_start与vm_end进行比较,确定虚拟地址是否在该区域内,如果与全部区域都比对过后发现都不符合,那说明这是一个非法地址,对应页表中的未分配条目,此时会触发一个段错误导致进程终止。图中标号①对应该情况。
②经过上述比对之后发现该虚拟地址确实是一个区域内的地址,接下来要检查指令要对该区域进行的操作是否符合该区域的权限。如果不符合权限,如运行在用户模式下的进程试图读取内核中虚拟内存中的字亦或是对只读代码段进行写操作等等,这些访问都是不合法的,会触发一个保护异常而导致进程终止。图中标号②对应该情况。
③如果上述两个比对都正常,则说明对该地址的访问是合法的,此时的缺页中断是由于页表条目中那些已分配但未缓存的条目引起的。因此此时的处理就是找到物理页对虚拟页进行缓存。首先依据一些算法确定物理页中的牺牲块,如果未对该牺牲块进行修改,则只需更新页表条目即可,如果对牺牲块进行了修改,则需要将该块的内容交换出去,换入新的页面并更新页表。接着缺页处理程序处理完毕返回,重新返回到引发缺页中断的指令处,重新执行,此时由于对应物理页面已经存在映射,因此这次MMU可以正常翻译该虚拟地址进行物理访存。图中标号③对应该情况。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护着一个变量brk,,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器的类型有两种:显式分配器(要求应用显示地释放任何已分配的块)和隐式分配器(分配器检测已分配块,当其不再被使用时释放),malloc和free函数就属于显式分配器。
用malloc函数进行动态内存分配的基本方法与策略如下:
1.隐式空闲链表。实际的分配器都需要一些数据结构来区别块的边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。一般的隐式链表块结构如图1:
图1
我们在内存块中嵌入了头部,用来编码内存块的大小以及该块的已分配/空闲的状态信息,因为实际情况都会存在地址对齐的要求,所以每个块的地址至少都是8的倍数,因此多出来的3位可以用来存放已分配/空闲的状态信息,即图中a位。有效载荷便是内存块中要存放的内容。这样一来,空闲块就可以通过头部中的大小字段隐含地连接着(该块头部加上从读取出来的该块长度就可以访问下一内存块),分配器可以不使用指针,通过遍历堆中所有的块,间接地遍历整个空闲块的集合。不过我们需要设置带某种特殊标记的结束块作为结束标志。这就是隐式空闲链表分配器的原理。而一般情况下我们还会加上边界标记(脚部),脚部是头部的一个副本。由于前面块的脚部距离当前块的头部只有一个字的距离,因此由当前块的头部可以很方便地找到前一块的脚部从而得知前一块的信息从而可以判断是否需要合并。这样就相当于用边界标记建立了一个“双向链表”,这个过程并没有利用指针,而且这个“链表”中的相邻元素也都是地址相邻的块,无法直接从一个特定空闲块直接到达下一个空闲块,而是按照物理地址顺序需要逐个查找,要么顺序,要么逆序。
2.显式空闲链表。对于已分配块,我们仍采取和隐式链表相同的策略,我们并没有必要来将所有的已分配块显示地组织起来。而对于空闲块,在隐式空闲链表的基础上,我们将每一个空闲块的数据结构中都添加两个指针嵌入其中,一个是前驱指针pred,另一个是后继指针succ,分别指向上一个空闲块和下一个空闲块这样就将所有空闲块组织成一个双向链表。我们每一次便可以再这个空闲链表中查找空闲块,首次适配的分配时间就从隐式空闲链表的块总数线性时间减少为空闲块数量的线性时间。不过释放块的时间取决于空闲链表中块的排序策略。
一种方法是后进先出的策略,将新释放的块放置在链表的开始处。分配器每次会最先检查最近使用过的块,释放一个块可以在常数时间内完成,若使用了边界标记,合并块也可在常数时间内完成。
另一种方法是按照地址顺序维护链表,每个块的地址都小于其后继的地址。此时释放块需要线性时间来搜索定位合适前驱,但内存利用率高。
与隐式空闲链表相比,显式空闲链表缩短了分配时间,这其实是我们很关心的一件事情,但它同时也占用了不小的空间,因为空闲块需要很大来包含所有需要的指针、可能的头部以及脚部,潜在地提高了内部碎片的程度。
3. 分离的空闲链表。创建一个空闲链表数组,其包含了多个空闲链表。分配器维护着这个数组,每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显式或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小都是某一大小类的成员。
这样在分配内存块时需要先确定申请空间大小的大小类,在空闲链表数组中找到该大小类对应的空闲链表,再采取同隐式链表或显式链表中相同的适配方式寻找合适的内存块。
典型的适配方式有以下三种:
①首次适配:每一次都从头开始搜索空闲链表,选择第一个合适的空闲块;
②下一次适配:从上一次查询结束的地方开始,选择第一个合适的空闲块;
③最佳适配:检查每个空闲块,选择适合所需块请求大小的最小空闲块。
7.10本章小结
要想执行hello程序,CPU给出一个逻辑地址,依据段式管理将其转化为线性地址,fork、execve函数可以使得hello可执行程序的各个区域映射到对应的虚拟内存。接着就是通过页式管理将虚拟地址转换为物理地址,如果页式管理仅仅依靠页表将是十分缓慢地,因此采用TLB进行加速翻译,又可以采用多级页表将页表进行压缩处理,当虚拟地址还未缓存至物理内存时又会触发缺页中断进行处理。通过一系列优化措施最终得到物理地址,终于可以结合3级cache访问物理内存。malloc使尽浑身解数为满足了hello中的各个请求,终于为它找到了合适的舞台能够登台演出。
hello上台奉献精彩演出,掌声不断,在他身后的OS与MMU却泪流满面:哥为你操碎了心啊!
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“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(int fd)进程通过调用close函数关闭一个已打开的文件,关闭一个已关闭的描述符会出错。
3. size_t read(int fd,void *buf,size_t n)
进程通过调用read函数执行输入功能。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示实际传送的字节数量
4. size_t write(int fd,const void buf,size_t n)进程通过调用write函数执行输出功能。write函数从内存位置buf赋值至多n个字节到描述符fd的当前文件位置。read和write函数传送的字节在某些情况下会比应用程序要求的少,这些不足值不表示有错误。
8.3 printf的实现分析
printf函数体如下:
在这里插入图片描述
(char
)(&fmt) + 4) 表示的是…中的第一个参数的地址。之后调用了vsprintf函数,其函数体如下:
在这里插入图片描述
该函数的作用是格式化。它接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,生成显示信息,产生格式化输出。
接下来是write系统函数:
在这里插入图片描述
该函数向寄存器传递特定参数,以一个int结束来利用陷阱实现系统调用int 0x 80或sys_call函数,用来驱动字符显示程序。
最后我们来看一下sys_call函数反汇编:
在这里插入图片描述
sys_call函数将字符串中的字节从寄存器中通过总线赋值到显卡的显存中,显存中存储的是自负的ASCII码。显示驱动子程序通过ASCII码在字模库中找到点阵信息并存储到vram中。显示芯片按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
8.4 getchar的实现分析
getchar函数体如下:
在这里插入图片描述
异步异常-键盘中断的处理:调用键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
为了让hello这个“倔脾气”能够和我们进行友好的交互,Unix I/O可谓是煞费苦心。Linux将所有I/O设备模型化为文件,所有的输入输出都被当作相应文件的读和写来执行,这使得所有输入和输出都以统一方式来执行。本章重点介绍了这种统一方式下的Unix I/O接口以及I/O函数,着重分析了printf以及getchar的实现。通过大家的重重努力,hello终于向我们说了第一句话!(I/O系统激动地热泪盈眶)
(第8章1分)

结论

hello的一生

  1. 程序员通过鼠标、键盘等I/O设备编写代码,hello.c诞生并存储在文件内。
  2. 预处理器对hello.c进行预处理,解释宏定义,插入系统头文件代码,修改hello.c源文件,形成hello.i。
  3. 编译器对hello.i文件进行编译,形成更接近机器代码的汇编代码,存储在hello.s文件中。
  4. 汇编器将汇编代码全部转换为机器代码,生成可重定位目标文件hello.o,hello就这样变成了我们的陌生人!
  5. 链接器将许多和hello相仿的小伙伴们结合起来,将每一个可重定位目标文件中的符号引用都能与一个确定地址的明确的符号定义关联起来,生成最终的可执行文件hello
  6. 通过I/O设备在shell中键入命令行./hello 1180300407 张高玮 10运行hello程序
  7. shell通过fork()函数创建一个子进程,在子进程中通过execve函数加载hello程序,建立hello可执行文件到虚拟内存的映射
  8. 子进程运行到hello程序入口处,发生缺页中断
  9. 操作系统通过调用缺页异常处理子程序,将hello加载到物理内存中去
  10. 逐条执行hello的指令,CPU为hello进程分配了固定时间片,与其他进程并发执行
  11. 指令执行过程中需要访问内存,通过TLB、4级页表、3级cache的多重帮助、加速之下快速访问主存
  12. 程序员从键盘上键入Ctrl+C、Ctrl+Z,OS向hello进程发送相应信号,调用相应的信号处理子程序
  13. printf函数调用malloc函数在堆上申请内存空间,从分离的空闲链表中寻找合适的空闲块将其分配
  14. hello进程终止,向父进程shell发送SIGCHLD信号
  15. 父进程接收到hello发送的信号,将hello进程进行回收,清除其占用的空间与资源
    深切感悟:整个计算机系统的设计与实现实在太过精巧,对于各种数据类型的运算都要保证准确地进行;对于稀缺的内存资源煞费苦心地用多种策略进行组织;对于从高级代码到低级的机器代码的转换有各种小技巧来实现性能的加速;对于效率的提高运用了多级缓存来保证···但与此同时,我也深刻体会到,计算机对于性能的追求要建立在绝对的正确性之上,那些即使只有小概率会犯错的加速技巧也是不被采用的。
    学习这门课程一路走来,从起初的懵懂无知到打开真正系统的大门,我真真切切地陪伴着一个个hello一样的生命经历着它们从诞生到谢幕的历程,曾经的我只知道编代码–出结果,现在的我已经大致了解了计算机背后为这一个个程序进行着多么细致、精密、巧妙的运作,诸如虚拟内存、多级缓存、动态内存分配等伟大的思想实在令我叹服!虽说现在我的理解说不上多透彻,但我相信我终可以带着这些闪光的思想走得更远!
    创新理念:生物计算机是现在极具潜力的发展趋势,它具有一些现代计算机无法比拟的优势:如更快的速度、抗干扰能力等,那么如何将上述闪光的思想融入生物计算机得以展现是一些艰巨但绝对意义非凡的挑战!!
    (结论0分,缺失 -1分,根据内容酌情加分)

附件

hello.i:由预处理器cpp生成,解释hello.c源程序中#开头的命令,读取对应系统头文件并插入程序文本中。hello.s:由编译器ccl生成,将hello.i翻译成汇编语言程序。
hello.o:由汇编器as生成,是由hello.s翻译成的机器语言指令,是一个二进制文件。
hello:由链接器ld生成,由各个可重定位目标文件合并而来,是可执行目标文件,可被加载到内存中执行。
(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] Csapp《深入理解计算机系统》
[2] 百度百科:getchar https://baike.baidu.com/item/getchar%28%29/6876946?fr=aladdin
[3] printf函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
[4] 虚拟地址、逻辑地址、线性地址、物理地址https://blog.csdn.net/rabbit_in_android/article/details/49976101
[5] 多级页表的原理https://blog.csdn.net/forDreamYue/article/details/78887035
[6] 内存管理策略:页式、段式、段页式https://blog.csdn.net/dreamer841119554/article/details/79965279 (参考文献0分,缺失 -1分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值