计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机专业
学 号 1190202016
班 级 1903010
学 生 李润哲
指 导 教 师 史先骏
计算机科学与技术学院
2021年5月
程序的开发和执行涉及计算机系统的各个层面,因而计算机系统层次结构的思想体现在程序开发和执行过程的各个环节中。本文简述了hello.c源程序的预处理、编译、汇编、链接、运行的主要过程,以及进程管理、存储管理、I/O管理等各个方面。加深对程序的编译、加载、运行的理解。
关键词:hello 编译 进程 存储 虚拟内存 I/O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:From Program to Process
hello.c经过cpp预处理器转化为hello.i,接着经过ccl编译器转化为hello.s,接着经过as汇编器转化为hello.o,最后,hello.o在ld链接器的作用下,与其它可能会用到的.o(例如printf.o)文件进行链接,得到hello。至此,hello.c经过一系列转化,得到了可执行目标程序hello。在Linux中,我们启动终端,在shell中键入运行hello的命令,接下来hello在软硬件的结合作用下被运行,实现了from program to process。
020:From Zero-0 to Zero -0
在Linux中,shell在fork产生的子进程中调用execve函数加载hello。删除掉原来进程的上下文,并为hello的代码、数据等创建新的区域结构,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。当程序运行结束后,shell父进程会回收hello进程,并且内核会从系统中删除hello所有痕迹。这就是020的过程
1.2 环境与工具
环境:Windows10,Ubuntu 64位
工具:GCC,EDB,Hexedit,Objdump,readelf,codeblocks
1.3 中间结果
hello.c:hello的源程序
hello.i:经预处理器预处理后的文本文件
hello.s:经编译器编译后汇编程序文本文件
hello.o:经汇编器汇编后的可重定位目标程序
hello:经链接器链接后的可执行目标文件
1.4 本章小结
根据hello的自白,简单地介绍了一下hello程序的P2P和020的过程。也介绍了一下实验环境与工具。最后列出了这个实验中生成的文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位。
预处理的作用:当对一个源文件进行编译时,系统把自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。c语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2在Ubuntu下预处理的命令
预处理命令为:gcc -E hello.c -o hello.i
图2-1 预处理命令及生成文件
2.3 Hello的预处理结果解析
图2-2 Hello的预处理文件截图1
图2-3 Hello的预处理文件截图2
根据图2.2我们能看出来,在hello的预处理文件hello.i文件的开始几行,已经没有了#include,相反有许多的以#开头的行。而根据图2.3我们能看出来,在整个hello.i文件的末尾还留有hello.c的源程序。其实,在预处理阶段,.i文件不仅保留着.c文件原有的源代码,而且,根据.c文件引用的头文件,.i文件添加了很多相关头文件的内容,包括各种变量的定义,函数的声明,结构体的声明等等。在预处理的作用下,短短几行的.c文件可能会被扩展到几千行。
2.4 本章小结
本章介绍了预处理的一些概念及其作用,并对hello.c经过预处理生成的hello.i文件进行了简单分析。预处理是源文件转化为可执行目标文件的过程中一个不可获取的一个阶段,有了预处理我们才能通过包含头文件来调用很多系统函数,这对我们的编程非常有用。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译可以指将程序的源代码文件通过特定的方式翻译成计算机能够理解的机器语言程序的过程。
编译的作用:经过编译生成的.s文件包含一个汇编语言程序,因此,编译后生成的.s汇编语言程序更容易让机器理解、让程序员理解。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
图3-1 编译命令及生成文件
3.3 Hello的编译结果解析
3.3.1数据
常量:
图3-2 String字符串
在.s汇编文件中,源程序中的printf中的格式串常量,被定义为.LC0,.LC1。格式串常量被编码,存放在.rodata段中。
局部变量:
图3-3 局部变量i
根据图3-3我们知道,main函数里的局部变量i被放在了内存栈里边。且在for循环开始的时候,i被赋初值0。
3.3.2 赋值
根据图3-3我们能看出来,汇编程序在给变量赋值的时候,是通过movl指令来赋值的,这等价于源程序中的“=”。
3.3.3 算术操作
图3-4 hello.s中的i++操作
根据图3-4我们可以看出来,for循环中的i++操作,在汇编代码中对应于图3-4上所示的内容。
3.3.4 关系操作
“>”:
图3-5 代码中出现的“>”关系
在for循环中,如果i>7,那么for循环就会结束,这对应于图3-5所示的汇编代码,每次让i与7比较,并设置条件码,以用于jle的判断。
“!=”:
图3-6 代码中出现的“!=”关系
在源代码的if判断中,如果argc!=4,那么就不会执行if语句体里边的代码,这对应于图3-6所示的汇编代码,让argc与4比较,并设置条件码,以用于je的判断。
3.3.5 数组/指针/结构操作
图3-7 argv的数组操作
对argv数组的相关操作的汇编代码如图3-7。对argv[1]和argv[2]的输出使用了将字符串首地址作为屏幕输出函数的传入参数的方式来实现。具体将&argv[1]存储在-32(%rbp)+$16的位置,将&argv[2]存储在-32(%rbp)+$8的位置,并分别将其值存储在%rdx和%rax寄存器中,作为参数传递给printf函数来输出。对于argv[3],计算机将&argv[3]存储在-32(%rbp)+$24的位置,并将值存储在%rax寄存器中,然后又复制该值到%rdi寄存器中,方便接下来的函数调用。
3.3.6 控制转移
if/else:
对于条件判断if(argc != 4)使用了cmpl和指令组合来实现,具体见图3-6。
for:
图3-8 for循环的汇编代码
源代码中for循环对应的汇编代码如图3-8所示。.L2标签对应的部分是赋初值给i,.L3标签对应的部分是比较i与7的大小,判断是否继续执行for循环里的代码,.L4标签对应的部分是执行for循环体里面的代码。
3.3.7 函数操作
图3-9 if语句体里的函数调用
图3-9为if语句体里的函数调用,首先要判断是否执行if语句里边的代码,如果执行,那么就要调用printf语句。根据图3-9,我们能看出来,在汇编语言中,此处的printf函数被编译成了puts函数,且传入参数被传递给寄存器%rdi,再把%rdi作为参数传递给puts函数。对于exit函数的调用,汇编代码将1存进寄存器%edi中,作为exit函数的传入参数。
图3-10 for循环体里的函数调用
我们直接根据图3-10来查看for循环体里的函数调用。对于printf函数,程序将argv[1]、argv[2]的地址存进寄存器%rsi、寄存器%rdx中,将printf格式串.LC1(%rip)地址存进寄存器%rdi中,调用printf函数。对于atoi函数,程序将argv[3]的地址存进寄存器%rax中,接着又复制给了%rdi,来作为atoi函数的传入参数。对于sleep函数,它的传入参数是atoi函数的返回值,因此程序直接将atoi函数返回后的寄存器%eax里的值给了寄存器%edi,来作为sleep函数传入参数。
图3-11 其它的函数调用
在main函数里,还有一个getchar函数的调用。由图3-11,我们可以知道,程序直接调用getchar函数,调用后将存储main函数返回值的%rax寄存器置0,并返回结束main函数。
3.4 本章小结
本章介绍了编译器通过编译将.i文件转换为汇编语言.s文件的过程。同时解析了变量,相关运算,以及各类c语言的基本语句的汇编表示,更容易理解高级语言的底层表示方法。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将hello.s文件翻译成二进制机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存到目标文件hello.o中。
汇编的作用:由汇编得到的.o文件为二进制机器代码文件,这种文件能够让计算机理解并执行。
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图4-1 汇编命令及生成文件
4.3 可重定位目标elf格式
4.3.1 ELF头
图4-2 ELF头
由图4-2,我们可以知道ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2 节头部表
图4-3 节头部表
不同节的位置和大小是由节头部表描述的,其中目标文件的每个节都有一个固定大小的条目(entry)。具体内容见图4-3。
4.3.3 符号表
图4-4 符号表
一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。符号表由汇编器构造,使用编译器输出汇编语言.s文件中的符号。如图4-4所示,hello.o程序的符号表包含Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。
4.4.4 重定位信息
图4-5 重定位信息
汇编器会把对所有不确定最终位置的对外部数据或外部函数的引用放进重定位条目里。在链接时,链接器循着这些条目信息更改条目位置数据的引用位置。
两种重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。
4.4 Hello.o的结果解析
图4-6 hello.o的反汇编代码
图4-7 hello.s的部分汇编代码
图4-6为hello.o的反汇编代码,根据要求,我们将其与在第三章得到的hello.s进行比较。图4-7为hello.s的部分汇编代码截图,方便比较。观察两个不同版本的代码,我们发现汇编代码与反汇编代码大体上非常相似,但有一些具体实现的细节不太一样,下面是具体的差异性:
在反汇编代码中,每一行代码前面有相应的机器指令码,而汇编代码中没有。
观察立即数,我们可以发现,在反汇编代码中,立即数是以十六进制出现的,而在汇编代码中,立即数的格式是十进制。
观察操作符,我们能够发现,对于某些操作符,比如mov命令,在反汇编代码中就只是mov,没有后面的位数表示。而在汇编代码中,mov可以写作movl、movq等,可以表示操作数的位数。相反,call命令,在反汇编代码中写成了callq,而在汇编代码中却只是call。
反汇编代码中分支转移使用的是跳转的地址,而在汇编代码中分支转移是jxx后接跳转位置的名称。
对于call指令,在反汇编代码中call指令后边接的是函数地址,而在汇编代码中,call指令后边接的是函数名称。
4.5 本章小结
本章了解程序的汇编过程、阅读了程序的ELF条目,了解了汇编、反汇编这两种相近而不相同的程序表现形式,并领会到了汇编过程的重要性。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。
链接的作用:把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件。使得分离编译成为可能。
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-1 链接命令及生成文件
5.3 可执行目标文件hello的格式
图5-2 hello的各段信息
观察图5-2,与之前的可重定位目标文件的elf文件比较,可以知道,可执行文件的elf文件与可重定位文件的elf文件非常相似。然而.text、.rodata和.data节在可执行目标文件中已经被重定位到它们最终的运行时的内存地址。另外,因为可执行文件是完全链接的,所以它不再需要.rel节。
5.4 hello的虚拟地址空间
图5-3 虚拟空间开始位置
图5-4 .text节
由图5-3可知,虚拟空间从0x400000开始。
在图5-2中我们可以看到.text节从0x4010f0开始,我们在edb中找到相应的位置,结果如图5-4所示。
图5-5 hello的反汇编部分代码
我们比较图5-4与图5-5,可以看到反汇编结果中.text节的机器代码与edb中使用Data Dump查看对应地址的内容完全一致。
图5-6 .rodata节
查看图5-2,我们可以看到.rodata节从0x402000开始,我们在edb中找到相应的位置,结果如图5-6所示,发现里面存的是printf的格式串。
5.5 链接的重定位过程分析
图5-7 hello反汇编代码(1)
图5-8 hello反汇编代码(2)
图5-9 hello反汇编代码(3)
图5-10 hello反汇编代码(4)
我们查看图5-7到图5-10,这是hello的反汇编代码,与hello.o的反汇编代码相比,有如下的不同:
hello.o反汇编代码虚拟地址从0开始,而hello反汇编代码从0x400000开始。
hello.o反汇编代码就直接是.text段,然后为main函数。而hello反汇编的结果中,由于链接过程中重定位而加入进来各种函数、数据。所以经过链接之后的hello中,除了text节以外还有很多其他的部分。
hello.o反汇编代码中标注的重定位信息的部分在hello反汇编代码中均被具体的地址,数据所代替。
5.6 hello的执行流程
我们使用edb工具一步一步执行hello程序,观察其调用的函数:
图5-11 调用地址为0x7f03dadc2df0的函数
图5-12 调用地址为0x7f03dadd2c10的函数
图5-13调用main函数
上边的图已经说明了在进入main函数之前,程序调用的函数,接下来就进入到main函数里边,按照hello.c的步骤运行程序。
在main函数里边运行的时候,程序调用了如下的函数:
hello!puts@plt
hello!exit@plt(地址为:0x4010d0)
hello!printf@plt(地址为:0x4010a0)
hello!atoi@plt(地址为:0x4010c0)
hello!sleep@plt(地址为:0x4010e0)
hello!getchar@plt(地址为:0x4010b0)
在main函数结束之后,使用同样的方法一步步跟踪edb中调用的函数可以得到在main函数结束之后,hello调用的其它的函数:
libc-2.31.so!exit
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本阶段完成了对hello.o的链接工作。使用Ubuntu下的链接指令可以将其转换为可执行目标文件,其中用到了rodata中的重定位条目,最终分析了程序如何实现的动态库链接。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用:通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。bash是大多数Linux系统以及Mac OS X默认的shell,它能运行于大多数类Unix风格的操作系统之上。
处理流程:
第一步:从终端读取输入的指令。
第二步:将读取的指令字符串进行切分,获得需要执行的程序名称以及运行该程序的各项参数。
第三步:根据参数判断是否为内置命令。若为内置命令,调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。
第四步:判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。
第五步:shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。fork函数只会被调用一次,但会返回两次。在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
当我们在shell命令行上输入./hello的时候,shell对其进行解释,发现这个不是一个内置命令行,此时shell会在计算机上寻找这个文件并通过调用某个驻留在存储器中被称为加载器的操作系统代码来运行它,运行时终端程序首先会调用 fork 函数创建一个新的运行的子进程。
6.4 Hello的execve过程
调用fork函数后,在子进程中加载execve函数,载入并运行hello函数,execve带列表argv以及环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。加载运行时加载器删除子进程现有的虚拟内存段, 并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后设置PC指向程序入口点,完成对可执行程序的execve加载过程。
6.5 Hello的进程执行
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重启一个被抢占的进程所需得状态。
当shell从其它时间片要上下文切换到hello程序时,首先会加载hello进程的上下文信息,然后控制从内核转换到用户模式,也就是控制给了hello程序。接着hello程序会运行一段时间。当hello程序一段时间片结束后,控制会返还给内核,保存hello的进程上下文信息,上下文切换到其它程序的时间片。整个hello程序的执行就是这种循环往复的形式。
6.6 hello的异常与信号处理
图6-1 乱按一些字符串的效果
图6-1是当程序运行时乱按字符串的效果截图。从图中我们能看到,当程序运行时乱按一些字符串,会被直接显示到终端,并且不会打断程序的运行。包括按下回车键也是一样,终端正常换行,接着hello程序会正常执行。
图6-2 按下CTRL+C的效果
图6-1是当程序运行时按下CTRL+C的效果截图。从图中我们能看到,在程序执行过程中,按下CTRL+C会直接结束程序的运行。这是因为,当按下CTRL+C之后,内核会发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。
图6-3 按下CTRL+Z的效果
图6-4 按下CTRL+Z后按ps的效果
图6-5 按下CTRL+Z后按jobs的效果
图6-3说明了程序运行时按下CTRL+Z会使程序暂时被挂起,我们用jobs命令查看也能看出来,此时程序的状态使Stopped。我们也能通过图6-4看出来hello程序此时还没有被回收。
图6-6 按下CTRL+Z后按fg的效果
从图6-6我们能看出来,当程序因为CTRL+Z的信号被暂时挂起来的时候,我们可以按下fg命令让其继续运行。fg的功能是使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg后hello程序又在前台开始运行,并且是继续刚才的进程,输出剩下的6个字符串。
图6-7 使用kill命令的效果
从图6-7我们能看出来,kill命令能杀死程序。首先可以用ps命令查看暂时被挂起的hello程序的进程ID,然后给终端发送信号kill -9 6038(6038为进程ID)。之后用ps查看,发现hello进程已被kill杀死,也可以通过jobs命令查看当前的jobs,发现hello程序确实已经不存在了。
6.7本章小结
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。讲述了shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程、hello进程的上下文切换。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
线性地址:
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
物理地址:
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
虚拟地址:
这是对整个内存的抽象描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理(segmentation),是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
7.3 Hello的线性地址到物理地址的变换-页式管理
把物理地址空间和虚拟地址空间都划分为一个个特定大小的块,把这些块称为页。在主存上进行数据换入换出时以页为单位进行。计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
7.4 TLB与四级页表支持下的VA到PA的变换
首先确定一些规定:每个VA都被分为几个部分,即VPN,VPO,其中VPN又被划分为TLBI,TLBT。PA被分为PPN与PPO。且VPO与PPO相等。
当从VA变换为PA的时候,首先根据VA的VPN在TLB缓存中找对应的页表。如果TLB中缓存了对应VPN的条目,则直接从中得到PPN。如果没有缓存,就得利用多级页表。每级页表中的每个PTE(页表条目)都是记载下级页表的基址,这样一层层寻找,直到第四层页表中PTE存放对应PPN。和快表一样,我们只要结合PPN和PPO就能得到PA。
7.5 三级Cache支持下的物理内存访问
在上一步由VA得到PA后,接下来我们就要根据PA来进行物理内存访问了。PA被分为CO,CI,CT三部分。CO由块大小决定,CI由cache的构造,即有多少组决定,而剩下的CT就是标记。首先根据CI查找对应的组,再根据CT看是否命中。如果命中,则根据CO直接将数据传给CPU。如果一级Cache没有命中,就得根据相同的地址以及相同的规则去二级Cache,三级Cache,甚至主存中寻找数据。
7.6 hello进程fork时的内存映射
当shell调用fork创建hello进程时,内核为hello进程创建各种数据结构,将某个唯一的PID分配给它。同时,为了给hello进程创建其唯一的虚拟内存,内核会复制当前进程的mm_struct,区域结构和页表的原样副本。内核将两个进程中的每个页面都标记为只读,两个进程中的区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
首先删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
然后映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射到 hello 文件中的.text 和.data 节,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在可执行文件 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
最后设置程序计数器(PC)。execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当程序想要通过虚拟地址寻找对应的物理地址,但却还没被物理地址保存对应的数据的时候就会引发缺页异常,缺页异常会调用内核中的缺页异常处理子程序,内核会将对应磁盘上对应地址的内容加载进内存,同时更新之前的PTE。
之后,处理程序会返回到触发缺页异常的那条指令重新执行,而此时之前要获取的数据已经在内存中了,因此程序会正常运行,缺页异常处理结束。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:
带边界标签的隐式空闲链表分配器管理:
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
显示空间链表管理:
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
在本章中整理了有关内存管理的知识,讲述了在hello运行的64位系统中内存管理方法,虚拟内存和物理内存之间的关系,了解intel环境下的段式管理和页式管理、fork和exceve的内存映射,知道了缺页故障和缺页中断管理机制,了解了如何根据缓存或页表寻找物理内存。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
设备可以通过Unix I/O接口被映射为文件,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O 函数:
int open(char* filename,int flags,mode_t mode) :进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open 函数将 filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
int close(fd):fd是需要关闭的文件的描述符,close返回操作结果,成功返回0错误返回EOF。
ssize_t read(int fd,void *buf,size_t n):read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置 buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
ssize_t wirte(int fd,const void *buf,size_t n):write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
off_t lseek(int handle, off_t offset, int fromwhere):通过调用lseek函数,应用程序能够显式地修改当前文件的位置。
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;
}
vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度,write函数将buf中的i个元素写入终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。键盘输入的字符都存到缓冲区内,一旦键入回车,getchar就进入缓冲区读取字符,一次只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到’\n’。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备的基本概念和管理机制,分析了Unix IO接口以及相关函数,还简要说明了printf和getchar函数的实现方法与操作过程。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello所经历的过程:
预处理:分析了其中引用的头文件,得到hello.i
编译:hello.i经过编译,得到hello.s文件
汇编:汇编器将汇编代码文件hello.s转为二进制可重定位目标文件hello.o
链接:将hello.o以及其他可重定位目标文件和动态链接库链接成为可执行目标文件hello
之后在shell命令行输入参数,shell对其进行解析,构造传递给execve函数的字符串数组,然后fork子进程,调用execve函数将hello文件复制到内存。shell自动为hello进程分配PID、时间片。
计算机系统是一门挺大的学科,要想真正学会计算机,必须要理解计算机系统。通过这一学期的学习,我不敢说已经深入理解计算机系统,不过我已经清楚了计算机系统的各个部分,也理解了为什么一个程序就那样地被执行了。这门课带给了我很多的认识,从这门课中我学到了许多知识,同时也增加了我对计算机的兴趣,以后我会继续深入学习了解计算机系统的。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c:hello的源程序
hello.i:经预处理器预处理后的文本文件
hello.s:经编译器编译后汇编程序文本文件
hello.o:经汇编器汇编后的可重定位目标程序
hello:经链接器链接后的可执行目标文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- https://baike.baidu.com/item/预处理/7833652?fr=aladdin
- https://baike.baidu.com/item/shell/99702
- https://blog.csdn.net/weixin_44551646/article/details/98076863
- 课本
(参考文献0分,缺失 -1分)