计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 软件工程
学 号 1183000118
班 级 1837101
学 生 张智琦
指 导 教 师 史先俊
计算机科学与技术学院
2019年12月
摘 要
本文通过对于hello程序的分析,从hello.c直到hello可执行程序进行逐步分析,结合课本上的知识和一些资料,通过乌邦图虚拟机进行试验,试验gdb,edb以及gcc等工具进行试验,把这个学期csapp中各章节知识进行融合,形成自己的语言,展示自己的收获,体现自己对于对计算机系统这门课程的理解。
关键词:计算机系统,hello程序的一生
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 7 -
2.4 本章小结 - 8 -
第3章 编译 - 9 -
3.1 编译的概念与作用 - 9 -
3.2 在UBUNTU下编译的命令 - 9 -
3.3 HELLO的编译结果解析 - 9 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
4.1 汇编的概念与作用 - 15 -
4.2 在UBUNTU下汇编的命令 - 15 -
4.3 可重定位目标ELF格式 - 15 -
4.4 HELLO.O的结果解析 - 18 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在UBUNTU下链接的命令 - 21 -
5.3 可执行目标文件HELLO的格式 - 21 -
5.4 HELLO的虚拟地址空间 - 23 -
5.5 链接的重定位过程分析 - 27 -
5.6 HELLO的执行流程 - 28 -
5.7 HELLO的动态链接分析 - 28 -
5.8 本章小结 - 28 -
第6章 HELLO进程管理 - 30 -
6.1 进程的概念与作用 - 30 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 30 -
6.3 HELLO的FORK进程创建过程 - 30 -
6.4 HELLO的EXECVE过程 - 31 -
6.5 HELLO的进程执行 - 32 -
6.6 HELLO的异常与信号处理 - 33 -
6.7本章小结 - 34 -
第7章 HELLO的存储管理 - 36 -
7.1 HELLO的存储器地址空间 - 36 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 36 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 37 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 38 -
7.5 三级CACHE支持下的物理内存访问 - 39 -
7.6 HELLO进程FORK时的内存映射 - 39 -
7.7 HELLO进程EXECVE时的内存映射 - 40 -
7.8 缺页故障与缺页中断处理 - 41 -
7.9动态存储分配管理 - 43 -
7.10本章小结 - 44 -
第8章 HELLO的IO管理 - 46 -
8.1 LINUX的IO设备管理方法 - 46 -
8.2 简述UNIX IO接口及其函数 - 46 -
8.3 PRINTF的实现分析 - 46 -
8.4 GETCHAR的实现分析 - 49 -
8.5本章小结 - 49 -
结论 - 50 -
附件 - 51 -
参考文献 - 52 -
第1章 概述
1.1 Hello简介
Hello程序的生命周期是从一个源程序,或者说源文件的,即程序员通过编辑器创建并保存的文本文件,文件名是hello.c.
hello.c分别经过预处理器cpp的预处理,、编译器ccl的编译,汇编器as的汇编依次生成生成hello.i文件,hello.s文件、,生成hello.o文件、最后使用链接器ld进行链接最终成为可执行目标程序hello.
当我们在shell中输入字符串.\hello并使用回车代表输入结束后,shell通过一系列指令的调用将输入的字符读入到寄存器中,之后将Hello目标文件中的代码和数据从磁盘复制到主存。此时shell会调用fork函数创建一个新的进程,并通过加载报存上下文,将控制权交给这个新的进程,具体过程在后面会详细叙述。
在hello加载完成后,处理器就开始执行这个程序,翻译成机器语言再翻译成指令编码,最后把程序的调用如:printf等进行链接。新的代码段和数据段被初始化为hello目标文件的内容.然后,加载器会从_start的地址开始,之后会来到 main 函数的地址,之后进入 main 函数执行目标代码,CPU 为运行的 hello 分配时间片执行逻辑 控制流。
执行阶段把这个等执行的程序分解成几个阶段,分别执行对应的指令,最后输出字符串。之后输出的字符串从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示到屏幕上。
这标志着进程的终止,shell的父进程回收这个进程操作系统恢复shell的上下文,控制权重回shell,由shell等待接受下一个指令的输入。
1.2 环境与工具
硬件环境:Intel Core i7 x64CPU 16G RAM 512G SSD
软件环境:Ubuntu18.04.1 LTS
开发与调试工具:visual studio edb,gcc,gdb,readelf,HexEdit,ld
1.3 中间结果
文件名 文件作用
hello.i 预处理器修改了的源程序,分析预处理器行为
hello.s 编译器生成的编译程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
5.3.txt 可执行hello的elf格式,作用是重定位过程分析
5.5.txt 可执行hello的反汇编,作用是重定位过程分析
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章主要介绍了hello程序的P2P、020的过程并列出实验基本信息
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的基本概念:在源代码编译之前对源代码进行的错误处理。c语言的预错误处理主要包括有三个预处理方面的基本定义内容:1.宏的定义;2.文件的包含;3.条件的编译。c语言预处理的命令以原始的字符符号"#"开头。
c语言的预处理器(cpp)根据以原始的c字符串中#开头的预处理命令,修改原始的c语言程序。在c语言和c/c++中常见的编译程序需要进行预处理的程序指令主要有:#define(宏定义),#include(源文件包含),#error(错误的指令)等。通过一个预处理命令使得一个源代码编译程序可以在不同的程序运行语言环境中被各种语言编译器方便的进行编译。
预处理的作用效果:以#include为例,预处理器(cpp)把程序中声明的文件复制到这个程序中,具体到hello.c的就是#include
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图2-2-1
2.3 Hello的预处理结果解析
使用文本编辑器打开输出的hello.i文件:
图2-3-1
我们可以发现,原本仅仅只有几行的头文件hello.c已经被程序扩展到了数千行,hello.i程序的依次开始是程序的头文件stdio.hunistd.hstdlib.h的依次展开。以#define
在乌邦图中发现程序使用的头文件#define
图2-3-2
而我们可以发现原始的hello.c的代码已经在程序的最后,前面都是预处理器复制的内容。
2.4 本章小结
本章首先介绍了预处理的定义与作用、之后结合预处理之后的hello.i程序对预处理的结果进行了简单分析。
预处理过程为接下来对程序的操作打下了基础,是十分重要,不可或缺的。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译要对程序进行以下的过程:首先进行的是语法分析,编译完成的程序使用语法分析器对编译程序输入的符号进行语法检查,检验其语法是否完全符合了语法的标准,如果不完全符合就是产生了错误。
编译器使用ccl把一个简单文本翻译文件的其中的一个hello.i翻译成为一个新的hello.s,它的翻译目的主要是为了包含一个可以使用多种汇编语言的应用程序。
在机器语言编译的整个过程中,编译器或程序都会产生一种新的中间代码,这种优化就是为了保证下一步的编译器为优化程序进行了充分的准备,大部分的编译器或程序都会针对编译器或程序的功能进行一些优化,目的之一就是在编译器可以很好地实现其原有机器语言功能的前提下提高编译器程序的性能和运行的效率。最后就是使用编译器把已经优化好的编译器和中间代码提取出来进行机器语言编译,调用机器语言汇编的程序,最后生成机器语言的代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3-2-1
3.3 Hello的编译结果解析
图3-3-1
3.3.1 数据类型
1.局部变量int i
图3-3-2
局部数据空间的每个变量被认为储存在一个栈的寄存器或者栈中,在一个hello.s.的范例里面,编译器将i变量存储在栈上的局部空间-4(%rbp)中,i可以作为一个intt的类型大约占4个单字节。
2.局部变量 argc
作为第一个参数存在存在寄存器edi中
图3-3-3
3.数组argv[]
main类型函数程序执行时需要输入的是命令行,char类型单个字符串元素的命令行大小为8为,这个数组文件中存放的指针是输入字符串的指针,程序中的字符串argv[1]和程序中的argv[2]的字符串地址分别被输入的rax分两次进行读取,argv[3]的秒数作为字符串的秒数被第三次读取。
图3-3-4
4.字符串
字符以UTF-8编码的格式存在只读的数据段(rodata)。
3.3.2 算数操作
算数操作对应的机器语言实现如下
图3-3-5
i++被实现为:
3.3.3 关系操作
Argc!=3这里是使用je进行条件跳转。
图3-3-6
I<8 这里是使用jle进行条件跳转。
图3-3-7
3.3.4 控制转移
if(argc!=4)这个条件判断被编译成
图3-3-8
:当argv不等于3的时候执行程序段l2中的第一行代码。cmpl比较执行完毕后,设置一个条件执行代码,je根据zf判断程序段是否完全符合条件,如果符合,就对程序段执行一次跳转,否则程序段会继续执行原来的语句。
2.for(i=0;i<8;i++)这个循环语句被编译成
图3-3-9
I被存放在-4(%rbp)中,每次循环+1,直到符合比较进行跳转。
3.3.5 函数操作
一个非常典型的基于c编程语言的库应用程序为例,程序由唯一的一个主执行函数和若干个互相对应的库函数相互组合一起构成。在编译程序中自己自动调用的库函数或者自己在编译程序中自己定义的一个库主调用函数,在每次进行程序编译时都很有可能因为需要自动地对进行编译程序中所对应的一个库主调用函数进行处理。
1.main函数
函数调用:基于系统内部指令调用编译器系统可以通过使用系统相应的一个call内部指令调用语句对一个main调用函数内部指令进行函数调用.call被系统启动的一个函数会被__libc_start_main调用,并且将会对下一个系统调用函数指令的初始化和地址数据进行自动压缩并将其写入栈中,然后自动依次跳转这个调用指令语句到系统相应的一个main调用函数内部。
参数传递:向main函数传递的参数argc和argv,分别使用%rdi和%rsi存储,在上面的叙述已经十分充分,在此就不过多重复了。
图3-3-10
内存管理:main函数调用结束时,也指的是在一个程序的运行结束时,调用一个leave指令,把栈的空间进行恢复,称之为针对栈的恢复现场,让栈的空间保持跟调用前一样的状态,然后调用一次ret进行返回。
2.printf函数
函数调用&参数传递:两次.
图3-3-11
第一次将Rdi设置为待传递的字符串"用法: Hello 学号 姓名 秒数!\n"的首地址。第二次将%rdi 为“Hello %s %s\n”的 起始的地址,使用对应的寄存器rsi 来分别进行针对 argv[1]的传递,,相应的,系统使用%rdx 传递 argv[2]。
3.exit函数
函数调用&参数传递:rdi设置为0,使用call(用法同上,不过多赘述)
图3-3-12
4.atoi 和sleep函数:
函数调用&参数传递:atoi函数把需要传入的变量argv[3]放入rdi中,传给atoi函数。将转换完毕存在eax的秒数通过edi传给sleep函数,调用都是使用call函数进行。
图3-3-13
(5) getchar 函数:
函数调用&参数传递:无需参数传递,直接使用call进行函数调用。
3.4 本章小结
本章简单的描述了编译器处理c语言程序的基本过程,根据hello程序中使用的各种数据类型,运算。循环操作和函数调用等操作对hello.s程序进行分析,查看编译器针对这些操作会采取什么样的策略。
编译器将.i 的拓展程序编译为.s 的汇编代码。经过编译之后, hello 程序便从 C 语言被解读成为为更加低级的汇编语言。当然,根据不同类型的CPU可能会产生不同类型的汇编语言,在此仅做简要分析,以x86-64指令类型作为例子。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器把这些hello.s翻译成机器语言的指令,并且把这些机器语言指令编码打包成一个可重定位目标程序的指令编码格式,结果指令编码保存在了hello.o中。
hello.o文件是一个简单的二进制指令编码文件,它可以包含目标程序的所有指令进行编码。此外汇编器还可以通过翻译生成计算机能直接自动识别和控制指令执行的一种二进制语言,也即机器语言。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
图4-2-1
4.3 可重定位目标elf格式
指令:readelf -a hello.o > elf.txt 获得hello的elf文件并重定向到elf.txt
图4-2-1
使用文本编辑器查看elf.txt
4.3.1 elf头
elf头以一个16字节的目标序列开始,这个字节的序列主要描述了一个生成该目标文件的操作系统的目标文件大小和生成目标字节的顺序。其他的描述还包括elf头的位置和大小,目标文件的位置和类型(例如是可重定位,可执行或者目标文件所共享的)机器文件类型,节头部表的目标文件位置和偏移,节头部表的偏移大小和目标文件数量。不同节的目标文件位置和偏移大小都是由一个节头部表条目来描述的,其中每个目标文件中每个目标字节都有一个固定大小的节头部表条目。
图4-3-2
4.3.2节头部表
描述目标文件的节,描述目标文件中不同节的位置和大小。
图4-3-3
分析hello.o采用的ELF文件的格式,用诸如readelf等多种工具准确列出其各个数据节的基本工作项目定位信息,特别是需要突出注意的一点是针对重定位项目的分析。
4.3.3重定位节
图4-3-4
偏移量:通常对应于一些需要重定向的程序代码所在.text或.data节中的偏移位置.
信息:重定位到的目标在符号表中的偏移量
类型:代表重定位的类型,与信息相互对应。
名称:重定向到的目标的名称
加数:用来作为计算重复或定位文件位置的一个辅助运算信息,共计约占8个字节。
4.3.4 符号表(Symbol Table)
符号表:用来存放程序中定义和引用的函数和全局变量的信息。局部变量存在于栈中,并不存于这里。
符号表索引:对此数组的索引。
根据书本中重定位的算法:
refaddr = ADDR(s) + r.offset 计算.text中对应节内部的位置的运行时地址+偏移量,下方的式子同理
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)
这就是计算重定位的方法之一。
图4-3-5
对hello.o由于程序比较简单没有程序头部表等其他的节。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > objdump.txt
图4-4-1
使用文本编辑器查看
图4-4-2
4.4.1.函数调用
在编译的过程中完成之后在生成的函数hello.s中,函数调用的后面接着就是下一个函数的目标地址名称,并没有直接确定下一条指令的相对地址位置,而在反汇编的过程中查看之后,可以很清楚的看到并且可以发现,call的后面跟着的函数就是调用目标函数相对地址时下一条的指令,相对的地址位置还是0。
因为我们在hello.s在程序中需要调用的每个函数都是一个共享库程序当中的重定位函数,这就是需要通过等待调用动态的链接把重定位的函数目标地址链接到我们的共享库程序当中。在没有重定位的函数时候,调用的动态链接函数的地址和运行时目标地址也是不可以确定的,只有通过等待调用这些动态的链接器很好的确定了函数的地址和运行时目标地址,汇编的过程中就将这些不可以确定的运行时间地址的调用的函数目标的地址直接设置为下一条程序执行指令的目标地址,也就是运行时目标的地址设置为0.这时等待调用动态的链接器也做的就是下一步程序要进行的指令的确定。
4.4.2.全局变量的访问
在编译完成生成的hello.s中,访问全局变量.rodata段的方式是在汇编的过程中使用了段的名称+寄存器,而在反汇编输出的文本文件中进行查看操作之后,变成了0+寄存器,也就是把段名称对应的地址设置为0。这个的原因和上面的类似,也是因为全局变量数据的地址也是在运行时候确定的,对他的数据进行访问的时候也是需要进行重定位操作的。
4.4.3 分支转移
在编译完成生成的hello.s中,程序为了进行分支转移进行了分段,如.L1.L2等,但是在这里没有所谓的段标记名称,而是明确的地址,那个名称仅仅是机器在编译的时候方便标记的助记符。在汇编成为机器语言之后,程序为了寻找明确的下一条指令的位置当然不能使用如此笼统的标记,而是需要使用明确的地址。
4.5 本章小结
本章针对hello.s汇编到hello.o。在本章的分析中,我们查看了hello.o的可重定位目标文件的格式,使用反汇编查看hello.o经过反汇编过程生成的代码并且把它与hello.s进行比较,分析和阐述了从汇编语言进一步翻译成为机器语言的汇编过程。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接操作是将各种机器代码和数据的片段进行收集并组合成一个单一的链接后的文件的编译过程,这个单一文件的链接可被应用程序加载(或者复制)到一个内存并加载和执行。
链接是十分重要,不可或缺的,而且应用十分广泛。这是因为链接可以是执行于应用程序编译时,也就是在一个源代码被翻译成一个机器代码时;也就是可以直接执行于应用程序加载时,也就是在应用程序被一个加载器自动加载到一个内存并加载和执行时;甚至可以直接执行于应用程序运行时,也就是由一个大的应用程序连接器来加载和执行。在传统和现代的计算机操作系统中,链接的重要性是由应用程序连接器帮助应用程序自动加载和执行的。因为链接可以是帮助应用程序构造大型的程序,在小型的计算机中也可以用链接帮助应用程序加载和单独运行应用程序。
链接重要性体现在它使得应用程序分离的编译过程成为了可能,程序员们可以把一个大的应用程序和连接器分开进行编译,独立进行管理和单独进行修改。极大的提升了进行大型文件编写的效率。
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-2-1
5.3 可执行目标文件hello的格式
指令:readelf -a hello > 5.3.txt
图5-3-1
5.3.1节头部表
图5-3-2
图5-3-3
显然我们可以清楚地看出,节点的头部表包含了应用程序的每个节,图中清楚的则列出了它们的详细信息详细信息.其中头部表包括:名称,类型,地址,偏移量,大小,全体大小,链接,信息,对齐。我们可以用hexedit定位各个全体字节所占的大小区间。
5.3.2.ELF头
图5-3-4
elf头是由一个16字节的序列开始,这个序列描述了生成该文件的系统的大小和字节顺序。其他还包括elf头的大小,目标文件的类型(如可重定位,可执行或者共享的)机器类型,节头部表的文件偏移,节头部表的大小和数量。不同节的入口文件位置和偏移大小都是由一个节头部表描述的,其中每个目标入口文件中每个节都有一个固定文件大小的入口文件条目。除此之外,跟链接之前相比section header 和 program header 均增加,并获得了入口地址。
5.3.3 程序头部表
图5-3-5
程序头部表描述了文件整个的布局情况。
5.3.4 符号表
图5-3-6
存放程序中定义和引用的函数和全局变量的信息还有长度等等等。符号表的索引就是这个数组的下标。
5.3.5 动态符号表
图5-3-7
保存与用于动态数据链接导入模块内部相关的数据导入值和导出通用符号,这些内容里面并不包括模块内部的符号。
5.3.6 重定位节
图5-3-8
这是一个表示.text节中目标位置的链列表,包含了.text节中一些需要对目标进行重定位的函数信息,当我们的链接器把这个函数的目标位置文件和其他目标文件直接组合在一起来的时候,需要修改这些函数的位置。上述分别的列表描述了我们的应用程序中那些需要重定位的链接器函数的位置和声明。
5.4 hello的虚拟地址空间
使用edb打开hello程序,查看进程的地址信息。
图5-4-1
程序包含以下几个小段:1, phdr 保存了二进制的程序头表。 2,interp 指定在程序已经从可执行文件映射到具体的内存区域之后,必须进程调用的解释器(如动态链接器)。 3,load表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存 了常量数据(如字符串)、程序的目标代码等,4,.dynamic 保存了由动态链接器使用的信息。5,note 保存辅助信息。 6,gnu_stack:权限标志,标志栈是否是可执行的。7,gnu_relro:这个节中指定了在重定位程序结束之后那些映射到内存的区域是需要重新设置为只读类型的。用户可以在程序中使用datadump查看虚拟地址段
图5-4-2
以.interp和.dynamic为例,使用Data Dump 查看虚拟地址段:
.interp
.dynamic
图
跟上面的节头部表是一致的,显然可以得到我们需要的结论。
5.5 链接的重定位过程分析
指令:objdump -d -r hello > 5.5.txt
图5-5-1
图5-5-2
跟上一次查看反汇编时存在着几点区别:
重定位:连接器通过把每个符合定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,从而重定位这些节。
函数个数的区别:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2。在动态共享库libc.so中,定义了hello需要的printf、sleep、getchar、exit 函数和_start 中调用的 __libc_csu_init,__libc_csu_fini,__libc_start_main。这些函数被链接器加入其中。
根据具体的类型对外部内存调用函数的地址进行重定位,链接器根据已经确定的.text与对应的.plt节的相对距离调用地址计算相对偏移距离,修改下条指令的相对偏移地址,指向链接器对应的函数,hello.o的相对距离偏移调用地址本身就变成了链接器hello.o中的虚拟内存调用地址。
节的改变:增加了.init和.plt节,和一些节定义中的函数。
5.6 hello的执行流程
函数名—地址
ld-2.27.so!_dl_start—
ld-2.27.so!_dl_init—
hello!_start—0x400550
hello!_init—0x4004c0
hello!main—0x400582
hello!puts@plt–0x4004f0
hello!exit@plt–0x400530
hello!printf@plt–0x400500
hello!sleep@plt–0x400540
hello!getchar@plt–0x400510
sleep@plt—0x400540
5.7 Hello的动态链接分析
共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。
而在一个动态的共享链接库中仍然存在着一个可以调用程序加载而动态链接无需重定位的位置无关代码,编译器在程序中的函数开始运行时是不能自动预测各个函数的开始运行时间和地址的,这就可能需要系统添加重定位的记录,交给一个动态共享链接器或者采用它来进行重定位的动态共享链接,动态共享链接器本身就是负责执行对动态链接的重定位过程,这样做就有效地防止了程序运行时自动修改或者调用目标模块的位置无关代码段。
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,由于静态的编译器本身无法准确预测变量和函数的绝对运行时地址,动态的链接器需要等待编译器在程序开始加载时再对编译器进行延迟解析,这样的延迟绑定策略称之为动态延迟绑定。got链接器叫做全局变量过程偏移链接表,在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现了函数的一个动态过程链接,这样一来,它就已经包含了正确的绝对运行时地址。
5.8 本章小结
本章通过对hello可执行程序的分析,回顾了链接的基本概念,文件的重定位过程,动态链接过程,虚拟地址空间,可重定位目标文件ELF格式的各个节等与链接有关的内容。链接的过程在软件开发中扮演一个关键的角色,它们使得分离编译成为可能。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个针对执行中的应用程序的程序的实例。系统中的每个应用程序都可以运行在某个应用进程的可执行上下文中。每次程序用户可以通过向系统中的shell应用程序输入一个可执行程序的英文名字,运行这个应用程序时,shell就可能会自动创建一个新的应用进程,然后在这个新应用进程的可执行上下文中自动运行这个可执行文件,应用程序也同样可以自动创建新的可执行进程,并且在这个新进程的可执行上下文中用户可以运行他们自己的可执行代码或者其他的应用程序。
我们应当感谢局部性的存在,进程所对应的处理功能部件会把它提供出来给所有的应用程序。它有两个关键抽象:一个独立的程序逻辑控制流:它可以提供一个独立的假象,好像我们的应用程序在一个独占的空间使用内存处理器。一个应用程序私有的地址处理器空间,它可以提供一个独立的假象,好像我们的应用程序独占的一个使用内存的系统。
6.2 简述壳Shell-bash的作用与处理流程
shell俗称壳,是一种指"为使用者提供操作界面"的嵌入式软件(也被称为命令解析器)。软件提供了一种允许用户与其他操作系统之间进行通讯的一种方式。这种简单的通讯方式可以以交互方式(从键盘输入,并且用户可以立即地得到命令响应),或者以交互方式shellscript(非交互)的方式允许用户执行。shell(即壳)它是一个简单的命令解释器,它允许系统接收到一个用户的命令,然后自动调用相应的命令执行应用程序。
Shell 的处理流程:shell读取用户从终端使用外部设备输入(通常是键盘输入)的指令。解析所读取的指令,如果这个指令是一个内部指令则立即执行,否则,加载调用一个应用程序为申请的程序创建新的子进程,在子进程的上下文中运行。同时shell还允许接收从键盘读入的外部信号,(如:kill)并根据不同信号的功能进行对应的处理。
(部分资料来源于百度百科)
6.3 Hello的fork进程创建过程
用户在终端输入对应的指令(./hello 1183000118 张智琦),这时shell就会读取输入的命令,并开始进行以下操作:
第一步:判断hello不是一个内置的shell指令,所以调用应用程序,找到当前所在目录下的可执行文件hello,准备执行。
Shell会自动的调用fork()函数为父进程创建一个新的子进程,子进程就会因此得到与父进程(即shell)虚拟地址空间相同的一段各种的数据结构的副本(包括代码和数据段,堆,共享库和用户栈)。父进程与子进程最大的不同在于他们分别拥有不同的PID,父进程与子进程分别是两个并发的进程,在子进程中程序运行的这个过程中,父进程在原位置等待着程序的运行完毕。
图6-3-1
6.4 Hello的execve过程
Execve函数加载并运行可执行目标文件hello,且且包含相对应的一个带参数的列表argv和环境变量的列表exenvp,,只有当出现错误时,例如找不到hello文件时,execve才会返回-1到调用程序,execve调用成功则不会产生返回。
在shell调用fork函数之后,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,使用启动加载器,子进程调用execve函数,在当前进程即子进程的上下文中加载新程序hello,这个程序覆盖当前正在执行的进程的所有的地址空间,但是这样的操作并没有创建一个新的进程,新的进程有和原先进程相同的PID,并且它还继承了打开hello调用execve函数之前所有已经打开的文件描述符。新的栈和堆段都会被初始化为零,新的代码和数据段被初始化为可执行文件中的内容。只有一些调用程序头部的信息才可能会在加载的过程中被从可执行磁盘复制到对应的可执行区域的内存。
图6-4-1
6.5 Hello的进程执行
进程向每个程序人员提供一种假象,好像他们在一个独占的程序中使用了处理器,这种处理效果的具体实现效果本身就是一个逻辑控制流,它指的是一系列可执行程序的计数器pc的值,这些计数值唯一的定义对应于那些包含在程序的可执行文件目标对象中的可执行指令,或者说它指的是那些包含在程序运行时可以动态通过链接触到可执行程序的共享文件对象的可执行指令。时间片是指一个进程在执行控制流时候所处在的每一个时间段。
处理器通过设置在某个控制寄存器中的一个模式位来限制一个程序可以可以执行的指令以及它可以访问的地址空间。没有设置模式位时,进程就运行在用户模式中。用户模式下不允许执行特权指令,不允许使用或者访问内核区的代码或者数据。设置模式位时,进程处于内核模式,该进程可以 访问系统中的任何内存位置,可以执行指令集中的任何命令。进程从用户模式变为内核模式的唯一方式是使用诸如中断,故障或陷入系统调用这样的异常。异常发生时,控制传递到异常处理程序,处理器从用户模式转到内核模式。
上下文在运行时候的状态这也就是一个进程内核重新开始启动一个被其他进程或者对象库所抢占的网络服务器时该进程所可能需要的一个下文状态。它由通用寄存器、浮点数据寄存器、程序执行计数器、用户栈、状态数据寄存器、内部多核栈和各种应用内核数据结构等各种应用对象的最大值数据寄存器组合构成。
在调用进程发送sleep之前,hello在当前的用户内核模式下进程继续运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理等待休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,将当前调用hello的进程自动加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。
之后设置定时器,休眠的时间等于自己设置的时间,当计时器时间到时候,发送一个中断信号。内核收到中断信号进行中断处理,hello被重新加入运行队列,等待执行,这时候hello就可以运行在自己的逻辑控制流里面了。
图6-5-1
6.6 hello的异常与信号处理
正常执行并结束:
图6-6-1
当按下 ctrl-z 之后,hello被挂起,使用ps查看,发现有hello的PID
图6-6-2
按下 ctrl-c : 父进程就会接收到一个 SIGINT 信号,它的作用是结束运行 hello,并调用诸如wait等的函数回收 hello 这个进程。在此时我们可以使用ps查看,发现没有hello。
图6-6-3
乱按仅仅把输入的字符放到屏幕上输出:
图6-6-4
6.7本章小结
本章系统的回顾了进程的基本定义以及其作用,对shell的工作方式和各种类型的信号处理的方法进行了简单的分析,学习了与进程相关的几个重要的函数,了解了如fork创建一个新进程,execve是怎么样加载的,还有这些函数的作用和原理,同时我们以hello这个应用程序为例针对异常控制和信号处理进行简单的分析。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址用来指定一个操作数,它由选择符和偏移量组成。逻辑地址是程序代码在编译之后出现在汇编程序。
线性地址:一个逻辑地址在经过段地址机制的转化之后变成一个线性分页地址(与虚拟地址同意义)。具体的格式可以表示为:虚拟地址描述符:偏移量。它作为一个逻辑分页地址被输入到一个物理地址变换之间的一个中间层,在分页地址变换机制中需要使用一个线性分页地址描述符作为输入,线性地址可以再经过物理地址变换以产生一个新的物理分页地址。在不同时启用一个分页地址机制的情况下,线性地址本身就是与虚拟地址同意义的。
物理地址::一个计算机操作系统的物理主存被自动组织为一个由m个连续的字节相同大小的单元内存组成的计算机数据。单元内存没有唯一的字节都是因为有一个唯一的物理单元内存地址,中央处理器会通过地址总线的寻址,找到真实的物理单元内存对应的地址,这会使得具有相同单元内存地址的计算机物理数据通过存储器被自动进行读写。
7.2 Intel逻辑地址到线性地址的变换-段式管理
每个段的首地址就会被储存在各自的段描述符里面,所以的段描述符都将会位于段全局描述符表中(每个段的全局描述符表一个局部称为gdgdt和另一个局部的的段描述符表一个局部称为ldldt),通过段选择符我们可以快速寻找到某个段的段全局描述符。逻辑上段地址的偏移量结构就是段选择符+偏移量。
段选择符的索引位组成和定义如下,分别指的是索引位(index),ti,rpl,当索引位ti=0时,段描述符表在rpgdt中,ti=1时,段描述符表在rpldt中。而索引位index就类似一个数组,每个元素内都存放一个段的描述符,索引位首地址就是我们在查找段描述符时再这个元素数组当中的索引。一个段描述符的首地址是指含有8个元素的字节,我们通常可以在查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址。
在分段保护模式下,分段有两种机制:段的选择符在段的描述符表->分段索引->目标段的段描述符条目->目标段的描述符基地址+偏移量=转换为线性段的基地址。由于现代的macosx86系统内核使用的描述符是基本扁平的逻辑模型,即目标段的逻辑地址=线性段的描述符=转换为线性段的基地址,等价于描述符转换为线性地址时关闭了偏移量和分段的功能。这样逻辑段的基地址与转换为线性段的基地址就合二为一了。
7.3 Hello的线性地址到物理地址的变换-页式管理
在分页的机制下地址转化管理机制主要实现了虚拟地址(它也即非非线性内存地址)向虚拟地址物理页或内存地址的非线性分页转化。vm内存系统将虚拟内存的块大小分割成作为一个被我们称为基于虚拟页的内存大小固定的块块用来进行处理这个固定大小的内存问题,每个称为虚拟页的内存大小可以固定为2p=2p个单位字节。类似的,物理块和虚拟内存被再一次细分为一个物理页(也被通常称为页帧),大小与每一个虚拟页的地址大小与其对应的值相等。例如:一个32位的虚拟机器,线性的地址可以最大达到4gb,用4kb为一个页来进行划分,也可以分为1m个页,通过页表处理和查找这些虚拟页的数据,方便对线性地址的大小进行管理。
之后计算机会进行一个翻译操作,把一个n元素的虚拟地址空间中的虚拟元素与一个m元素的另一个物理虚拟地址空间相互进行映射,这个翻译操作被我们称为地址翻译。
地址翻译示意图如下
虚拟地址由对应的虚拟物理页号()和虚拟页偏移量(vpo)共同组成,类似的,物理地址由虚拟物理页偏移号(ppn)和对应的物理页偏移量(ppo)共同分配组成(这里没有特别考虑tlb快表的物理地址结构)。页表中物理地址存在三种常见的情况:未分配:没有在虚拟内存的空间中分配该条目的内存。未分配缓存:在虚拟内存的空间中已经分配了但是没有被直接缓存到对应物理地址的内存中。已分配已缓存:内存已经缓存在了对应物理地址的内存中。页表的基址寄存器paptbr+在页表中可以获得条目pte,通过对比条目对应的有效位判断物理地址是上述哪一种的情况,如果有效则通过提取得出对应物理地址的页号寄存器ppn,与对应的虚拟页偏移量共同分配构成了物理地址寄存器pa。
当页面命中时CPU硬件执行的步骤:
第1步:处理器会产生一个虚拟地址,并且将它传送给地址管理单元MMU。
第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它。
第3步:高速缓存或者主存向MMU返回PTE。
第4步:MMU构造物理地址,并把它传送给高速缓存/主存。
第5步:高速缓存或者主存会返回所请求的数据字给处理器。
7.4 TLB与四级页表支持下的VA到PA的变换
如果按照上述模式,每次CPU产生一个虚拟地址并且发送给地址管理单元,MMU就必须查找一个PTE行来用将虚拟地址翻译成物理地址。为了消除这种操作带来的大量时间开销,MMU中被设计了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)也叫快表。例如当每次cpu发现需要重新翻译一个虚拟地址时,它就必须发送一个得到虚拟地址mmu,发送一个vpo位得到一个l1高速缓存.例如当我们使用mmu向一个tlb的组请求一个页表中的条目时,l1高速缓存通过一个vpo位在页表中查找一个相应的数据标记组,并在页表中读出这个组里的个数据标记和相应的数据关键字.当mmu从一个tlb的组得到一个ppn时,代表缓存的工作在这个组的请求之前已经完全准备好,这个组的ppn与就已经可以与这些数据标记文件中的一个虚拟地址进行很好的匹配了。
corei7采用四级页表层次结构,每个四级页表进程都有他自己私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的pte全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。四级页表的层次结构操作流程如下:36位虚拟地址被寄存器划分出来组成四个9位的片,每个片被寄存器用作到一个页表的偏移量。cr3寄存器内储存了一个l1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。1提供了到一个l1pet的偏移量,这个pte寄存器包含一个l2页表的起始基地址.2提供了到一个l2pte的偏移量,一共四级,逐级以此层次类推。
图7-4-1
7.5 三级Cache支持下的物理内存访问
三级cache的缓存层次结构如下:
图7-5-1
物理地址的结构包括组索引位CI(倒数7-12位),使用它进行组索引,使用组索引位找到对应的组之后。假设我们的cache采用8路的块,匹配标记位CT(前40位)如果匹配成功且寻找到的块的有效位valid上的标志的值为1,则命中,根据数据偏移量CO(后6位)取出需要的数据然后进行返回。
图7-5-2
如果没有数据被匹配成功或者匹配成功但是标志位是 1,这些都是不命中的情况,向 下一级缓存中查询数据(L2 Cache->L3 Cache->主存),并且将查找到的数据加载到cache里面。这时候我们面临一个问题,替换谁。一般我们会选择使用一种常见的简单替换策略,查询得到的数据之后,如果我们映射得到的组内已经有很多个空闲块,则直接在组内放置;否则组内都已经是有效块,产生了冲突,则我们会采用最近最少最少使用(lfu)的策略,然后确定将哪一个块替换作为牺牲块。
7.6 hello进程fork时的内存映射
Fork函数在上面已经介绍过了,在此仅做简单的重复。
shell通过一个调用用fork的函数可以让进程内核自动创建一个新的进程,这个新的进程拥有各自新的数据结构,并且被内核分配了一个唯一的pid。它有着自己独立的虚拟内存空间,并且还拥有自己独立的逻辑控制流,它同样可以拥有当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效保护进程的私有数据和信息,同时为了节省对内存的消耗,进程的每个数据区域都被内核标记起来作为写时复制。
7.7 hello进程execve时的内存映射
execve函数在上面已经介绍过了,在此仅做简单的重复。
execve函数在当前代码共享进程的上下文中加载并自动运行一个新的代码共享程序,它可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但是它并没有自动创建一个新的代码共享进程。新的运行程序仍然在堆栈中拥有相同的区域pid。之后为新运行程序的用户共享代码、数据、bss和所有堆栈的区域结构创建新的共享区域和结构,这一步叫通过链接映射到新的私有代码共享区域,所有这些新的代码共享区域都可能是在运行时私有的、写时复制的。它首先映射到一个共享的区域,hello这个程序与当前共享的对象libc.so链接,它可能是首先动态通过链接映射到这个代码共享程序上下文中的,然后再通过映射链接到用户虚拟地址和部分空间区域中的另一个共享代码区域内。为了设置一个新的程序计数器,execve函数要做的最后一件要做的事情就是自动设置当前代码共享进程上下文的一个程序计数器,使之成为指向所有代码共享区域的一个入口点(即_start函数)。
图7-7-1
7.8 缺页故障与缺页中断处理
在指令请求一个虚拟地址时,MMU中查找页表,如果对于的物理地址没有存在主存内部,以至于我们必须要从磁盘中读出数据,这就是缺页故障(中断)。
图7-8-1
在发生缺页中断之后,系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。具体的操作过程如下:
第1步:CPU生成一个虚拟地址,并把它传送给MMU.
第2步: 地址管理单元生成PTE地址,并从高速缓存/主存请求得到它.
第3步:高速缓存/主存向MMU返回PTE.
第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序.
第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘.
第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE.
第7步: 缺页处理程序返回地址到原来的缺页处理进程,再次对主存执行一些可能导致缺页的处理指令,cpu,然后将返回地址重新再次发送给处理程序mmu.因为程序中虚拟的页面现在已经完全缓存在了物理的虚拟内存中,所以处理程序会再次命中,主存将所请求字符串的返回地址发送给虚拟内存的处理器。
图7-8-2
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆.系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) .对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块
显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.
隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
图7-9-1带边界标记
显示空闲链表:
一种更好的方法是将空闲块组织为某种形式的显式数据结构.因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面.例如,堆可以组织成一一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针.
(资料来源于课本)
图7-9-2
7.10本章小结
本章主要通过对hello程序运行时虚拟地址的变化进行分析,解析了适用于hello应用程序的虚拟存储地址空间,分析了虚拟地址,线性地址和虚拟物理线性地址之间的互相转换,页表的命中与不页表的命中,使用动态快表缓存作为页表的高速缓存以及如何加速页表,动态内存管理的操作,fork时的动态内存中断与映射、execve时的动态内存中断与映射、缺页的中断与缺页映射和中断的处理。这些缓存方面的基础知识有助于我们能够编写出更加对高速缓存友好的程序代码以及一些对高速缓存的优化解决手段,加速我们的程序运行。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列,:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 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 函数:
8.3 printf的实现分析
查看 printf 的函数体:
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;
}
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义:
typedef char *va_list
这说明它是一个字符指针。
其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。
vsprintf返回的是要打印出来的字符串的长度,它的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
下一步:调用write
可以找到INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL,DA_386IGate,sys_call,PRIVILEGE_USER);
int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
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
一个call save,是为了保存中断前进程的状态。
syscall 将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII 码。
8.4 getchar的实现分析
异步异常-键盘中断的处理:在用户敲击或者按键盘上面的按钮的时候,键盘接口获得一个键盘扫描码,这样会在此时同时产生一个中断的请求,这会调用键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区的内部。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回这个字符串。Getchar的大概思想是读取字符串的第一个字符之后再进行返回操作。
8.5本章小结
本章中简单的描述了linux的io的接口及其设备和管理模式,unixio的接口及其使用的函数,还有常见的printf函数和pritgetchar函数,当然,如果真的对此展开了分析,printf函数的设计和实现其实看起来还是相当困难的,所以仅仅是简单的了解,对此的深入了解还是有些过浅,需要学生通过不断的分析和学习实践来增进自己的认识和了解。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello程序的一生是从文本编辑器开始的,是程序员亲手一个个打出来的字符,成为最初的hello.c文件,也是我们平时经常要交的程序。
之后我们会把hello进行预处理操作,这一步进行之后,我们在hello.c内部进行的宏定义和为了省事用include调用先人大佬写好的库取出来合并到文件hello.i中。
但是这一堆字符计算机是无论如何都认识不了的,所以就需要进行编译操作,hello.i被汇编成为汇编文件hello.s。
编译进行完毕后,机器要让文件成为它能读懂的东西,hello.s经过汇编阶段变成了可重定位目标文件hello.o
下一步是动态链接过程,hello.o和动态链接库共同链接成可执行目标程序hello。到这里,所谓的P2P: From Program to Process过程就完成了。
然后hello就要开始发光发热了,我们要使用hello就在shell里面输入./hello 1183000118 张智琦 1 并按下回车键
Shell读懂了我们的意思,它使用fork和execve函数加载映射虚拟内存,给我们的hello创建新的代码数据堆栈段,让我们的hello误以为自己在独占的使用这些资源。
CPU为hello分配一个时间片,在这个时间片的时间里,hello获得对CPU的控制权力,在程序计数器中加入自己的代码,按顺序执行他们。
之后程序或者从cache中很快的取得需要的数据,或者从内存中取出需要的数据,又或是从磁盘加载数据。
如果我们想要在程序进行时休息一下,只要键入ctrl-z就可以让程序挂起,甚至还可以通过ctrl-c停止这个进程。这些都是信号实现的强大功能。
最后,shell回收子进程,内核会删除这个进程使用所需要创建的一系列数据结构。至此,hello程序结束了自己短暂的一生。
学习完了计算机系统这门课,我不禁为计算机内部如此精妙的设计感到震惊,一个个精细的设计无不体现前人的智慧,手中这台小小的机器,在软件硬件的双重加持之下,发挥出了如此强大的实力。
当然,计算机的发展远远没有结束,人们还在追求更快的数据传输,更大的存储空间,这些都是我们以后要学习的。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名 文件作用
hello.i 预处理器修改了的源程序,分析预处理器行为
hello.s 编译器生成的编译程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
5.3.txt 可执行hello的elf格式,作用是重定位过程分析
5.5.txt 可执行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分)