计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院
学 号 2022111568
班 级 22WL028
学 生 郑乔译
指 导 教 师 郑贵滨
计算机科学与技术学院
2024年5月
hello.c程序是c语言中最简单的一个程序,其往往是初学者最先写的一道程序,然后,在简单代码的背后,确实不寻常,蕴含人类伟大智慧的实现过程。hello.c经过预处理,编译,汇编,链接,最终成为可执行程序hello,这个过程中涉及到了从基于自然语言的高级程序到机器可识别的二进制代码的过程,涉及到虚拟地址到物理地址的转化过程,在shell中键下./hello命令时,系统悄无声息的执行复杂的操作,为何多个程序不会互相影响?为何系统能够同时执行多个线程?秘密藏在系统之中,我们将深入简单命令下的复杂过程,理解hello程序真正精彩的一生。
关键词:计算机系统;Linux;程序;编译;汇编;链接;进程管理;I/O管理;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
1.1.1 P2P(From Program to Process)过程
在Linux中,最开始程序员通过键盘编辑创建了hello.c程序(Program),但这个程序到进程(Process)还有以下的漫长过程。
hello.c文件要通过编译系统变换为hello可执行程序,具体过程如图1所示。首先hello.c文件经过cpp预处理器得到文本文件hello.i,之后经过ccl编译器生成汇编程序hello.s,接着再经过as汇编器生成可重定位目标程序hello.o,最后经过ld链接器,与其它用到的库函数可重定位文件链接,生成可执行程序hello。
当执行hello程序时,系统会新创建一个进程再将hello程序加载进入,从而最终实现了从程序到进程的整个过程。
1.1.2 O2O(From Zero-0 to Zero-0)过程
当执行hello程序后,shell创建新的子进程,在其中调用execve函数将hello程序由虚拟内存加载进入物理内存,然后运行main函数,shell调用waitpid函数,当hello运行完毕成为僵尸进程后,shell就将该僵尸进程回收,同时释放虚拟内存并删除hello的相关内容,这时hello运行过的痕迹都被清空,控制权重新传回shell,等待运行下一条输入的命令,从而实现了O2O的全过程
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2.3GHz;32G RAM;931GHD Disk
1.2.2 软件环境
VMware® Workstation 17 Pro 17.0.0 build-20800274,
Ubuntu 22.04.1 64位
1.2.3 开发工具
Visual Studio 2022 64位
Vim gcc gdb edb
1.3 中间结果
文件名 | 作用 |
hello.c | hello源文件 |
hello.i | hello修改后的源程序 |
hello.s | hello的汇编文件 |
hello.o | hello的可重定位目标文件 |
hello | hello的可执行文件 |
elfheader | hello.o的ELF头 |
sectionheader | hello.o的节头部表 |
relohello | hello.o的重定位节 |
hellosymbol | hello.o的符号表 |
helloass | hello.o的反汇编 |
helloelfhead | hello的节头部表 |
disassem | hello的反汇编 |
1.4 本章小结
本章概述了hello的P2P和O2O的过程。此外,还介绍了本实验用到的硬软件环境和开发调试工具,最后介绍了本次实验的中间结果文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
1.预处理的概念:
预处理指编译器将原本的.c文本文件改写成更加规范的文本文件,其主要操作包括注释的删除,宏定义的替换,引用库的调用等。
2.预处理的作用:
预处理能够使编译器更好的理解程序员所写的代码,同时也为程序员编写模块化代码提供了便利。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
可以看到main函数之前的库调用被扩展了,同时注释也被删除。
2.4 本章小结
本章介绍了预处理的相关操作以及Linux下的指令调用。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译即指编译器将C语言编码转换为汇编语言代码的一个过程,从文件形式来看就是将hello.i文件转换为hello.s文件的一个过程。
作用:将高级编程语言转换为机器能够识别的低级语言,一方面便于程序员使用各种编程语言进行程序编写,另一方面也使得机器能够更好的接收程序员的命令
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
.file | 程序源文件 |
.text | 代码段,立即数也存放在这里 |
.section | 到节头部表的索引 |
.rotata | 存放只读变量 |
.align | 对齐 |
.type | 类型,数据或者函数 |
.global | 全局变量 |
.string | 字符串 |
3.3.1数据
1.常量
1.1字符串常量:
.rodata(只读数据段)中:
两个字符串L0,L1
L0:这个标签下存储了一个中文字符串,被编码为八进制数。
L1:这个标签下存储了一个英文字符串 "Hello %s %s %s\n"。
1.2数字常量:
其中24,16为立即数,存放在.text段。
2.变量
此处的-32(%rbp)为变量。
3.3.2全局函数
从汇编代码的声明中可以看出主函数(main)被定义成为了全局函数,并且main中的字符串常量被存在了.rodata段中。
3.3.3赋值操作
此处为一个赋值操作,此外,该程序中还通过lea指令进行地址传送。
3.3.4算数运算
Add指令代表将两个操作数相加。
3.3.5关系操作
其中的cmp为判断大小的指令。
3.3.6控制转移指令
其中的jle为条件跳转指令。
3.3.7函数操作
call函数调用了getchar函数。
3.4 本章小结
本章阅读了hello.s的汇编代码,对hello.i的编译结果进行了分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编就是编译器将低级的汇编语言进一步改写,转化为最低级的,机器可直接识别的机器语言,即二进制代码。在文件表示上就是将文本编辑器可以打开的hello.s文件转换为不可打开的hello.o文件。
作用:
机器语言是最低级的语言,其对计算机的操作无需通过软件进行,而是直接通过硬件电路进行,速度最快,效率最高。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
1.ELFheader的相关信息
ELF Header以一个16字节序列开始,前四个字节确定文件类型,第五个字节表示系统位数,第六个字节表示大端法还是小端法,第七个字节表示ELF文件版本,剩余的9个字节未定义用00填充,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF Header文件还包含文件类别,数据类型,系统信息,链接器语法分析,程序入口地址等内容
2. 查看符号表
符号表存放着hello中定义和引用的全局变量和函数的相关信息。Value表示该符号距离起始地址的偏移量,Type代表数据类型,Bind表明符号来源,可以看出hello程序中所有符号均为外来符号,这里还未定义,Name代表符号名称。
3.查看节头部表
可以看出其基本属性有Name,Type,Address,Offset等,由于还没有进行重定位,所以他们的值还是0。
4. 查看重定位节的相关信息
由上表可以确定重定位的具体地址和信息,其含有Offset,Type,Name+Addend,反映了重定位符号关于.text起始位置的偏移量,寻址方式,还有修正偏移量。
4.4 Hello.o的结果解析
ello.o反汇编与hello.s比较
1.机器数格式的不同,hello.o的反汇编中含有十六进制表示的机器二进制代码,机器可以直接读懂这些指令而无需翻译,而hello.s中并不含二进制代码。
2.hello.o反汇编的立即数使用十六进制表示,而hello.s中的数仍用十进制表示。
3.hello.s跳转指令之后跟着的是标签,比如je .L2,而hello.o反汇编之后跟着的是实际地址(重定位之前),比如je 2f <main+0x2f>
4.调用函数指令:hello.s中为函数名@PLT,而hello.o反汇编其为真实地址。
机器语言的构成:机器语言是由一系列0,1组成的机器代码构成的,在hello.o反汇编代码中可以看到对应的机器代码。
机器语言与汇编代码的映射关系:每一个机器指令都对应着一条汇编指令,汇编指令含有操作,源,目的地,而对应的机器代码同样有操作码,对应使用的寄存器或要寻址的地址。
4.5 本章小结
本章主要介绍了编译器将汇编指令转化为二进制文件的一个过程,汇编器接受汇编代码,产生可重定位目标文件。它可以和其他可重定位目标文件合并而产生一个可以直接加载被运行的可执行目标文件。正因为它并不包含最终程序的完整信息,它的符号尚未被确定运行时位置,并用0占位。在第五部分中将说明如何将多个可重定位目标文件合并,并确定最终符号的最终运行位置。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各个单独的二进制代码文件加载到同一个文件,并使之可以加载到内存中执行的一个过程。链接可以在编译时被执行,也可以在程序运行中被执行。文件表示为若干个.o文件被合并成一个单独的可执行文件(Linux下默认为a.out文件)。
作用:链接可以使代码完成分块化操作,大幅度简化了程序员对代码的编写,同时使程序的生成变得更加灵活,可移植性强。
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.3 可执行目标文件hello的格式
1.查看elf格式指令:readelf -a hello > helloelfhead.txt
可以看出,相比于hello.o的ELF头,入口地址由0x0变为0x4010f0,文件类型由REL变为EXEC。节头数量由14变为27。
2.Section Headers
在可执行文件中,每个节的地址为实际地址,其根据节自身的大小和对齐规则计算得出偏移量。
3.重定位节:
可以看出,相比于hello.o的重定位节,hello的重定位节被分成两部分,包含动态链接的重定位内容,同时每一部分均有了准确的偏移量,因而所有的加数都变成了0。
4. 符号表
相比于hello.o符号表多出了动态链接解析出的符号,还含有其他的符号。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb加载hello:
Data dump:
可以看到是虚拟地址是从0x401000开始的
起始地址为0x401000在elf中可以看到对应:init。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
反汇编指令:objdump -d -r hello >helloass.txt
相比于hello.o,hello反汇编文件中的每一个地址值都是确定的,可以认为,链接器完成了对于hello的重定位工作。
在重定位中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输人模块的.data 节被全部合并成一个节,这个节成为输出的可执行目标文件的.data 节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输人模块定义的每个节,以及赋给输人模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
hello中增加了许多hello.o没有,但是定义在了动态库被hello直接使用的函数,hello也对其进行了重定位。
5.6 hello的执行流程
使用edb执行hello:
调用的子程序名有:_start(0x4010f0),_init(0x401000),main(0x40112d),puts@plt(0x401090),exit@plt(0x4010d0),_finl(0x4011c0),sleep@plt(0x4010e0),atoi@plt(0x4010c0),printf@plt(0x4010a0)。
5.7 Hello的动态链接分析
1.通过edb调试:
在.got节中存放的是变量的全局偏移量,在链接之后由于动态添加了很多目标执行所需要的程序,所以.got节中的内容会发生改变。
打开ELF文件,找到节头表,找到.got节的首地址,是0x403ff0,执行链接前后如下图所示:
2.查看反汇编代码:
在hello程序中可以发现调用共享库的函数名称后面都加了“@plt”,通过观察反汇编文件,可以发现使用这些函数时都会跳转到.plt节中, hello程序调用了6个共享库的函数,.plt节中就有6处跳转指令,它们跳转到了同一个地址0x401020,然后在0x401026处它们会进行一个间接跳转,
跳转到<_GLOBAL_OFFSET_TABLE_+0x10>处,及0x404120内存中存放的地址处。而这个地址在dl_init前,这个值是一个空值,而当调用一个动态链接库中的函数时,该地址内容值会发生变化,使其跳转到所调用的对应的函数地址处。
5.8 本章小结
本章主要对hello.c的链接和ELF文件格式做了详细介绍,同时使用edb和obdjump使我们对连接过程中虚拟空间的使用和动态链接过程有了更直观的了解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是指计算机中已运行的程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。进程是程序真正运行的实例。
进程的作用:
现代计算机中给每个进程一个独立的PID,这使得程序员可以更好的调度某个正在运行程序的资源和数据。同时,每个程序独占一个进程可以更好的保护程序内部资源。CPU的一个核只能处理一个进程,这使得计算机的硬件资源能够得到更好的使用。进程为程序提供了两个抽象:逻辑控制流和私有地址空间。逻辑控制流使得每个进程都好像独立占用cpu,而私有地址使得每个程序好像独立占有系统内存。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Shell是一个交互型的应用程序,代表用户运行其他程序,执行一系列的读,求值步骤,然后终止。
Shell-bash的处理流程:终端进程读取用户由键盘输入的命令行。分析命令行字符串,获取命令行参数,并构造传递给execve的argv参数。检查第一个命令行参数是否是一个内置的shell命令。如果不是内部命令,调用fork创建一个新进程在新进程中,用步骤2获取的参数,调用execve执行指定程序如果用户没要求后台运行(末尾没有&号)否则shell使用waitpid如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
当父进程调用fork函数可创建一个新的子进程。新创建的子进程得到与父进程用户级虚拟地址空间相同(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。父进程与子进程之间最大的区别在于它们有不同的PID,父进程中fork返回子进程的PID,子进程的PID总是非0,而子进程中fork返回0。
以运行hello程序为例,当输入./hello时,父进程为shell,它会对这条命令进行解析,因为这不是内置shell命令,它判定要执行hello这个可执行文件,于是它就调用fork函数创建一个新的子进程以便接下来将hello加载到这个进程中执行。
6.4 Hello的execve过程
execve函数声明为:int execve(char *filename,char *argv[],char *envp[])
execve函数会在当前进程的上下文中加载并运行一个新程序,它加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp,在execve加载filename时会调用一个加载器,加载器会创建内存映像并将可执行文件的片(chunk)复制到代码段和数据段,接下来,加载器会跳转到程序的入口,即_start函数的地址点来设置用户栈。初始化程序后就会把控制权传递给main函数。
以运行hello程序为例,当shell调用fork函数创建一个新的子进程后,它会调用execve函数加载并运行可执行目标文件hello,如果命令./hello后跟有参数,shell也会把这些参数当作参数列表argv一起传入进程,这样hello就实现了由程序到进程的转变,之后完成初始化程序后,就会正式运行main函数。
6.5 Hello的进程执行
一个进程和其它进程轮流进行的概念称为多任务,一个进程执行它的控制流的一部分的每一时间段叫做该进程的时间片,操作系统内核使用一种称为上下文切换的异常控制流实现多任务。
内核为每个进程维持一个上下文,系统中的每个程序都运行在某个进程的上下文中,进程上下文信息就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,包括寄存器、程序计数器、用户栈等。
图6-5-1进程执行
以执行程序hello中的sleep函数为例,当main函数执行了sleep系统调用函数时,触发了陷阱异常,此时从用户模式切换为内核模式,main所在进程休眠一段时间,控制权交给其它进程,执行了上下文切换,此时会切换回用户模式。当休眠结束时,会发送信号给内核,此时又会进入异常处理程序,又从用户模式切换为内核模式,它会执行从其它进程到main所在进程的上下文切换,结果控制权又交回给main所在进程。调用其它系统函数时也会有类似的进程调度过程。
图6-5-2 上下文切换
6.6 hello的异常与信号处理
6.6.1可能出现的异常:
hello 执行过程中可能出现四类异常:中断、陷阱、故障和终止。
1. 中断是来自 I/O 设备的信号,异步发生,中断处理程序对其进行处理,返 回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
2. 陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指 令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
3. 故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成 功,则将控制返回到引起故障的指令,否则将终止程序。
4. 终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程 序会将控制返回给一个 abort 例程,该例程会终止这个应用程序。
6.6.2 hello执行时的异常:
(1)运行时乱按或输入回车
当按程序要求输入学号+姓名+手机号+秒数的参数后启动程序,在程序执行过程中乱按或输入回车。可以发现在hello执行过程中,按回车会正常换行,且引起中断,但因为没有对应的中断处理程序,因此回车指令不会对程序起什么影响。但是乱按输入命令时只会显示在终端上,并不会在程序运行过程中处理乱按的指令,在程序结束后,Shell会依次解析运行指令。
(2)运行时输入Ctrl-C
程序运行时输入Ctrl-C会导致进程立即终止,因为Ctrl-C会发送一个SIGINT信号给hello进程,而SIGINT信号的默认操作是终止进程,因此终止处理程序会立即终止进程。
(3)运行时输入Ctrl-Z
程序运行时会使进程暂停,能输入并执行其它的命令。因为输入Ctrl-Z时会发送一个SIGTSTP信号给hello进程,而SIGTSTP信号的默认操作是暂停当前进程,这使得正在执行的hello进程暂停挂起,可以输入执行其它命令。
因此,输入Ctrl-Z后可以输入一些其它命令。
输入ps命令可以监视后台其它的进程。
输入jobs命令可显示当前shell环境中已启动的作业状态。
输入fg命令,会向挂起的的hello进程收到SIGCONT信号,从而切换到前台继续运行。
输入pstree命令,会将所有进程以树状图显示。
当输入kill -9 29603命令表示给PID为29603的hello进程发送9号信号SIGKILL,它的默认操作是终止进程,于是hello进程会被终止。
6.7本章小结
本章首先从理论上介绍了进程的概念和作用,然后以hello程序为例,具体展现了hello由程序一步步到进程中间经历的过程。另外本章还具体演示了一些hello在Shell中执行时的异常及其信号处理,使得对进程管理有了更深刻的理解。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指在汇编代码中通过偏移量+段基址得到的地址,hello.s中汇编代码显示的所有地址均为逻辑地址,如0x00401000等。
线性地址:线性地址是逻辑地址和物理地址之间的地址,通过将逻辑地址的段地址和偏移量相加,即可得到线性地址。在linux系统中,段基址通常是0,在hello程序中我们将代码段的地址和虚拟地址相加即可得到对应的线性地址。
虚拟地址:虚拟地址并不存在于实际的物理内存中,虚拟地址可以理解为逻辑地址经过某种映射后得到的地址。在hello程序中线性地址经过MMU转化为虚拟地址。
物理地址:实际的物理内存地址,再hello程序中,虚拟地址通过MMU的地址映射机制转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理指将一串地址分成多端,每一段都对应一个单独的索引。首先逻辑地址会加上偏移量,得到一个新的线性地址,线性地址中每一段都对应不同的索引,如在RAM访问中,我们可以看到地址被分为三段,在寻址中分别对应不同的标志,从而能够找到地址对应的数据。
图7-2-1 段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理被用到虚拟内存中,可以大大减少额外内存的开销,加快数据访问速度。在页式管理中,地址对应同样被分为多段,不过相较于直接的段式管理,页式管理多了一个数据到页表的映射,数据的虚拟地址会与页表中的基址相加形成新的段地址,再发送到内存中。页式管理由MMU进行操作,页表是一个目录,存放着每一页对应的数据的虚拟地址和物理地址偏移量,每一页都有自己唯一的页表条目(PTE)。
图7-3-1页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:翻译后备缓冲器。页表中的线性地址可以分为虚拟页号(VPN)和虚拟页偏移(VPO)两部分。然后VPN在查找过程中又可以被拆分为两部分,一部分是TLB标记(TLBT),另一部分是TLB索引(TLBI)。这两部分可以类似于cache查找的方式在TLB缓存中找到对应的物理页号,最后的物理页号(PPN)加上VPO得到的新地址就是最后生成的物理地址。
图7-4-1 四级页表
7.5 三级Cache支持下的物理内存访问
Cache中物理地址可以被分为三部分,一部分是CT(标记),标记对应cache中的组数;CI(索引),索引用来查找某组中对应的行号;最后是CO(偏移量),在对应行号中根据偏移量就能找到对应数据在cache中存放的位置。当有多级cache时,首先从L1开始查找,如果不命中将会访问L2,进行一个块的替换,如果还不命中就L3,直到访问内存。
图7-5-1 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
在Shell中输入./hello时,Shell会调用fork函数创建一个新的子进程,内核会为新的子进程创建各种数据结构,并分配给它一个唯一的PID。为了给新的子进程创建虚拟内存,内核会创建当前进程的mm_struct、区域结构(vm_area_struct)的链表和页表的原样副本,它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
图7-6-1 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
execve调用hello时,需要为hello在对应地址开辟新的空间。我们知道在计算机中就是一个数据覆盖的过程。首先需要覆盖对应的用户区域,刷新数据栈,使用mmap函数为hello映射一块新的空间。
然后,将hello中的各个段映射到新的结构中,具体操作如图:
图7-7-1 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
当引用一个地址时,在页表中无法找到,即与该地址对应的物理页面不在内存中时,MMU发出一次异常,触发缺页异常,内核调用缺页处理程序。通过查询该页在磁盘上的位置,缺页处理程序从指定的地址加载页面到物理内存中,然后更新PTE,再将控制返回到缺页故障的指令,重新执行该指令,此时,该物理页已经在内存中,因此能够命中。
7.9动态存储分配管理
通过动态内存分配器可实现动态储存分配管理,它维护一个进程的虚拟内存区域——堆。分配器将堆视为一组不同大小块的集合来维护,每个块就是一个连续的虚拟内存片(chunk),它要么是已分配的,要么是空闲的。已分配的块供应用程序使用,空闲的块可用来分配给程序。动态存储分配管理的过程就是块的分配和释放,这要么是程序显式执行的,要么是动态内存分配器隐式执行的。
分配器有两种,分别是显式分配器和隐式分配器,它们都要求应用显式地分配块,它们的不同之处在于由哪个实体来负责释放已分配的快。
显式分配器(explicit allocator)要求应用显式地释放任何已分配的块,例如C标准库提供malloc程序包的显式分配器。C程序可通过调用malloc函数分配一个块,调用free函数释放一个块。显式分配器常用一种叫做隐式空闲链表的数据结构来实现。另外还会使用显式空闲链表、分离的空闲链表等。
图7-9-1 显式分配器
隐式分配器(implicit allocator)也叫垃圾收集器,它可检测当一个分配块不再被程序调用时,就释放这个块。自动释放未使用的已分配块的过程叫做垃圾收集。隐式分配器将内存视为一张有向可达图,当有节点不可达时,将该节点对应为垃圾。
图7-9-2 隐式分配器
7.10本章小结
介绍了不同地址空间的概念和关系,介绍了逻辑地址到线性地址到虚拟地址再到物理地址的转换过程,重点介绍了段式管理和页式管理的概念和实现。解释了fork和execve函数中内存映射的相关操作。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个 Liunx 文件就是一个 m 个字节的序列:B0,B1,…,Bm-1。所有的 I/O 设备都被模型化为文件。
文件的类型有:
1. 普通文件:包含任何数据,分两类
i. 文本文件:只含有 ASCII 码或 Unicode 字符的文件
ii. 二进制文件:所有其他文件
2. 目录:包含一组链接的文件。每个链接都将一个文件名映射到一个文件
3. 套接字:用于与另一个进程进行跨网络通信的文件
而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
1. open:打开文件,将文件名转换为文件描述符,并返回一个小的非负整数作为描述符,用于标识该文件。通过记录描述符,程序可以管理打开的文件
2. close:关闭文件,释放文件相关的数据结构和内存资源,并将描述符返回给可用的描述符池。在进程终止时,内核会自动关闭所有打开的文件。
3. read:从文件中读取数据,将最多n个字节复制到指定的内存位置buf,并返回实际传送的字节数。返回值-1表示错误,0表示文件结束(EOF)。
4. write:向文件中写入数据,将最多n个字节从内存位置buf复制到文件的当前位置,并返回实际写入的字节数。
这些函数通过文件描述符来操作文件,可以进行文件的打开、读取、写入和关闭等操作。它们提供了对文件的基本控制和数据处理能力,使程序能够进行文件的操作和数据的传输。Unix I/O接口是Unix-like操作系统中常用的文件操作接口,被广泛应用于Unix、Linux和类Unix系统。
8.3 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;
}
首先printf函数输入的参数中含有“…”,这代表可变形参,即传递参数的个数不确定。
之后va_list arg = (va_list)((char*)(&fmt) + 4)中va_list为一个定义的字符指针类型,arg即为一个字符指针,而fmt是一个指针,这个指针指向第一个const参数中的第一个元素。那么清楚arg即为printf可变形参中的第一个参数的地址。
vsprintf的定义为:int vsprintf(char *buf,const char *fmt,va_list args)。这命令的目的是将从第一个传递参数开始,依次把这个字符串传入buf缓冲区中,其中返回值为i即是要打印出来的字符串的长度。
最后write函数就会把buf中的i个元素的值写到终端。
但从硬件上看,从vsprintf到最终显示器上显示出字符串还有对应漫长过程:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。接着字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb = buf:
static int n = 0;
if (n == 0) {
n = read(0, buf, BUFSIZ);
bb = buf;
}
return (–n >= 0) ? (unsigned char)*bb++ : EOF;
}
当用键盘输入字符串时,会触发键盘中断异常,执行键盘中断处理子程序。字符接受按键扫描码转化成ASCII码,保存到系统的键盘缓冲区,getchar调用read函数,将键盘缓冲区中的字符读入buf中,并测得该字符串的长度为n,然后令字符指针bb指向buf。最后返回buf中第一个字符。如果长度n<0,则会报EOF错误。
8.5本章小结
本章主要简单介绍了Linux下系统I/O的一些相关概念和系统级函数调用。介绍了Linux下一些接口的使用和一些读写函数。同时针对printf函数内部原理进行了分析,还对getchar进行了一定的回顾。
(第8章1分)
结论
hello所经历的过程:
文本文件:程序员在文本文件中写好hello的代码,得到最初的hello.c文件
预处理:系统展开宏定义,删除注释,引入头文件函数,生成新的hello文本代码文件。
编译:编译器将C语言文本文件转化为汇编语言文件,即hello.s
汇编:编译器进一步将汇编语言文件转化成机器语言文件,生成重定位条目,生成可重定位文件hello.o
链接:链接器进行符号解析,重定位将hello.o与库文件链接一起生成一个可执行文件hello
运行:父进程调用fork函数,提供一个新的进程,在进程中调用execve函数运行hello程序。
运行中:在运行中,CPU和操作系统负责处理hello程序中的数据变化和内存调用。同时hello程序可以接收各种信号,各个存储单元互相工作。
运行结束:hello运行结束或接收结束信号,程序终止。由其父进程或init进程将其回收,并且删除hello进程在内存中的记录,清理空间,整理碎片化空间。hello继续在磁盘中等待下一次调用。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
hello.c | hello源文件 |
hello.i | hello修改后的源程序 |
hello.s | hello的汇编文件 |
hello.o | hello的可重定位目标文件 |
hello | hello的可执行文件 |
elfheader | hello.o的ELF头 |
sectionheader | hello.o的节头部表 |
relohello | hello.o的重定位节 |
hellosymbol | hello.o的符号表 |
helloass | hello.o的反汇编 |
helloelfhead | hello的节头部表 |
disassem | hello的反汇编 |
为完成本次大作业你翻阅的书籍与网站等
[1] 《深入理解计算机系统》Randal E. Bryant David R.O`Hallaron
[2] https://zhuanlan.zhihu.com/p/128654625
[3] https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86
(参考文献0分,缺失 -1分)