摘 要
本文以一个简单的程序hello.c为例,分析了在Linux系统下程序执行的过程,包括预处理、编译、汇编、链接、进程管理、存储管理、IO管理等过程,并在此过程中完善加强对计算机系统各构件的相互连接和作用。
关键词:预处理、编译 、汇编、链接、进程、存储、IO管理
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:要运行Hello.c,要完成由C语言控制台应用到可执行文件的转变,其中要对Hello.c进行四步操作:预处理,编译,汇编,链接,从而生成可执行文件,再在shell中运行该可执行文件,shell为其分配进程空间。Hello.c程序向进程转化的过程称为P2P过程。
020:shell使用execve函数运行hello程序,映射虚拟内存,并从程序入口开始载入物理内存,再进入main函数执行目标代码,此时CPU为运行的hello分配时间片执行逻辑控制流,并通过流水线机制运行该程序,在此过程中,计算机通过TLB、4级页表、3级Cache,Pagefile等机制加速hello程序的运行,程序结束后,shell父进程负责回收hello进程,内核删除相关的数据结构。这个过程从hello这个程序不存在开始(即为0),到被回收(即为0)的过程,称为020
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
Windows 10(64位),VMware,Ubuntu
codeblocks,gcc,gdb,
1.3 中间结果
hello.c 源程序
hello.i 经过预处理的文件
hello.s 编译后的汇编语言文件
hello.o 汇编后的可重定位目标执行文件
hello 连接后的可执行文件
helloo.elf hello.o的ELF格
helloo.txt hello.o的反汇编代码,分析hello.o
hello.elf hello的ELF格式,分析重定位过程
hello.txt hello的反汇编代码,分析重定位过程
1.4 本章小结
本章主要简单介绍了hello的P2P,020过程,列出了本次实验的环境、中间结果。还有本文完成所需要生成的一些中间文件,为后续实验提供了基本思路。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
预编译又称为预处理,作用是代码文本替换。预处理指令是以“#”开头的代码为编译做准备工作。C语言在编译之前会进行一步处理——预处理,例如#include<stdio.h>命令告诉处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。进而得到另外一个C语言程序,以.i作为拓展名。预处理包括宏定义、文件包含、条件编译等。大多数预处理器指令属于下面3种类型:
宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
文件包含:#include指令导致一个指定文件的内容被包含到程序中。
条件编译:#if,#ifdef,#ifndef,#elif,#else和#dendif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。
2.1.2预处理的作用:
将源文件中以”include”格式包含的文件复制到编译的源文件中。
用实际值替换用“#define”定义的字符串。
根据“#if”后面的条件决定需要编译的代码。
预处理阶段根据已放置在文件中的预处理指令来修改源文件的内容,这种预处理机制提高了源文件的灵活性,能适应不同的计算机和操作系统;而且通过预处理指令,可以使用已经封装好的库函数,极大地提高了编程效率。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
对比源程序发现,原本几行的源程序扩展成了三千多行,原本的C语言代码放在了最后。主函数中定义的变量没有发生变化,注意到以#include开头的代码发生了很大改变,被替换成头文件中的内容,同时,源文件中的注释也被删除了。说明预处理对源程序进行了文本性质的处理,进而生成hello.i文件。
2.4 本章小结
本章介绍了预处理指令的概念和作用,用简单的hello程序实际演示了从hello.c到hello.i的过程,展示了预处理指令和生成文件,并结合具体代码对预处理结果进行了简单的分
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
将高级语言翻译成汇编语言或机器语言的过程,即由hello.i转化成hello.s的过程。
3.1.2编译的作用
进行语法分析,对编译完成的程序使用语法分析器对编译程序输入的符号进行检查,如果不完全符合语法标准则发生错误;编译器产生一种优化的中间代码,这种优化能提高程序性能和编译器的运行效率。
3.2 在Ubuntu下编译的命令
编译指令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
Hello.s内容:
3.3.1汇编初始部分
.file 声明源文件为“hello.c”
.text 表示代码节
.section .rodata 表示只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式,8字节对齐
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号的类型(函数类型或者数据类型)
3.3.2数据部分
- 字符串
.string声明了两个字符串.LC0, .LC1
分别将rdi设置为两个字符串的起始地址。
- 参数argc
参数argc是main函数的第一个参数,被存放在寄存器%rdi中,由上图可知,argc被存放在寄存器并压入栈中。
- 局部变量
程序中局部变量只有i,由图可知局部变量i被存放在栈上-4(%rbp)的位置。
3.3.3全局函数
Hello.c中只声明了一个全局函数int main。
3.3.4赋值操作
for循环开头i=0,因为i是int型为32位变量故用movl指令传递双字实现。
3.3.5算术操作
for循环中i++,将指针存储的数据加立即数1后再存入到指针
3.3.6关系操作
(1)条件判断语句
用cmpl指令比较立即数4和存储在-20(%rbp)的数据即参数argc大小,并设置条件码,若不相等则执行该指令后面语句,否则跳转到.L2。
(2)循环条件判断语句
for循环每次循环结束要判断一次i<5
3.3.7控制转移指令
对i赋初值0后直接跳转.L3循环体
通过条件码判断每次循环是否跳转到.L4.
3.3.8函数操作
(1)main函数
参数传递:main函数的参数为int argc, char *argv[ ]。
函数调用:通过使用call内部指令调用语句进行函数调用,并且将要调用的函数地址写入栈中,然后自动跳转到这个调用函数内部。Main函数里调用了printf、exit、sleep函数。
局部变量:使用局部变量i
(2)printf函数
参数传递:printf函数调用参数argv[1],argv[2].
函数调用:调用了两次该函数,第一次将寄存器%rdi设置为待传递字符串.L0的起始地址,第二次设置为待传递字符串.L1的起始地址。
(3)exit函数
函数调用:用call指令调用exit函数。
(4)atoi、sleep函数
参数传递与函数调用:atoi将argv[3]放在寄存器%rdi中用作参数传递,使用call指令调用;将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call指令调用。
(5)getchar函数
无参数传递,直接调用即可。
3.3.9类型转换
atoi函数将字符串转换为sleep函数需要的整型参数。
3.4 本章小结
本章主要介绍了编译器将hello.i文件翻译为hello.s文件的过程,简单概括了编译的概念和作用,实践编译指令,并展示了hello.s文件中的汇编代码,以及对数据、赋值、算术、关系操作、函数调用、控制转移和类型转移等指令的分析。
析。
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编器将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。这个过程就叫做汇编。
4.1.2汇编的作用
将汇编语言翻译成机器语言,因为机器语言是计算机能直接识别和执行的一种语言,使文件能被机器识别和执行。
4.2 在Ubuntu下汇编的命令
编译指令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
生成hello.o文件elf格式的命令:readelf -a hello.o > helloo.elf
4.3.1 ELF头
ELF以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条口。
4.3.2 节头
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
4.3.3 重定位节
.rela.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
4.3.4 符号表
.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。
4.4 Hello.o的结果解析
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.4.1 生成反汇编文件
命令:objdump -d -r hello.o
4.4.2 与hello.s的对照分析
我们发现,每行代码末尾的指令基本是相同的,但是在每条指令前面都会有一串十六进制的编码。从hello.o与hello.s文件内容分析,hello.s是由汇编语言组成的,相对于计算机能识别的机器级指令,汇编代码仍是抽象语言;而反汇编得到的代码不仅仅有汇编代码,还有机器语言代码。机器语言代码是计算机可识别执行的,是一种纯粹的二进制编码。机器指令由操作码和操作数构成,从反汇编代码我们可以很清楚地看到,每一条汇编语言操作码都可以用机器语言二进制编码表示,所以可以将所有的汇编代码与二进制机器语言建立一一对应的映射关系。
(1)操作数
反汇编文件中的所有操作数都改为十六进制,而hello.s中的操作数是十进制。
- 分支转移
反汇编文件的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称。例如:
在反汇编文件中:
在hello.s文件中:
- 函数调用
反汇编文件中对函数调用与重定位条目相对应。在可重定位文件中call后面不再是函数名称,而是一条重定位条目指引的信息。
在反汇编文件中:
在hello.s文件中:
4.5 本章小结
本章简单概括了汇编的概念和作用,展示了如何通过命令进行汇编操作,分析了可重定位文件的结构和功能;并且查看了hello.o的ELF格式;利用objdump进行反汇编,对比观察反汇编文件和原来的汇编文件的区别,了解了从汇编语言到机器语言编译器需要实现的转换。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
5.1.2 链接的作用
链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。
5.2 在Ubuntu下链接的命令
链接的命令:
5.3 可执行目标文件hello的格式
生成hello的ELF格式命令:readelf -a hello > hello.elf
- ELF头
hello的ELF头和hello.o的ELF头基本相同,但hello为可执行文件,类型为EXEC,节头有25个。
- 节头
对hello所有节头进行声明。
- 程序头表
- 动态节
- 可重定位条目
- 符号表
- 版本信息
5.4 hello的虚拟地址空间
程序从地址0x400000开始到0x401000被载入,根据5.3中的节头,可以通过edb找到各段的信息。
例如,.interp节在hello.elf文件中能看到开始的虚拟地址。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.5.1 分析hello与hello.o的不同
对hello执行反汇编命令:objdump -d -r hello > hello.txt
- 函数个数增多:hello中多了很多经过重定位的函数,如.plt函数。
- 地址改变:hello.o地址是从0x000000开始,是相对地址;而hello的地址是从0x4004c0开始的,这是经过重定位后的虚拟地址。
5.5.2 重定位过程
重定位由两部组成:
(1)重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一个类型的新的聚合节,然后,链接器将运行时内存地址赋给新的聚合节,赋给输出模块定义的每个节,体积赋给输出模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
(2)重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的地址,要执行这一步,链接器依赖于可冲定位目标模块中称为重定位条目的数据结构。
5.6 hello的执行流程
00000000004004c0 <_init>
00000000004004e0 <.plt>
00000000004004f0 <puts@plt>
0000000000400500 <printf@plt>
0000000000400510 <getchar@plt>
0000000000400520 <atoi@plt>
0000000000400530 <exit@plt>
0000000000400540 <sleep@plt>
0000000000400550 <_start>
0000000000400582 <main>
0000000000400610 <__libc_csu_init>
0000000000400680 <__libc_csu_fini>
0000000000400684 <_fini>
5.7 Hello的动态链接分析
之前:
之后:
5.8 本章小结
本章首先阐述了链接的概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,并利用edb观察了hello文件的虚拟地址空间的使用情况,最后对hello程序进行重定位过程,执行过程和动态链接过程的分析。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是一个正在运行的程序的实例。系统中的每个程序都在某个进程的上下文中运行。上下文由程序正确运行所必需的状态组成。这种状态包括存储在内存中的代码和程序数据、堆栈、通用寄存器的内容、程序计数器、环境变量和文件描述符的集合。
6.1.2 进程的作用
进程为应用程序提供了两种抽象,一种是独立的逻辑控制流,一种是私有地址空间。提高CPU执行效率,减少因程序等待造成的CPU空闲和其他计算机软硬件资源的浪费。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell是一个交互型应用级程序,也被称为命令解析器,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。
6.2.2 Shell-bash的处理流程
读取命令行的字符串
分割字符串,读取对应的命令
判断是否为内置命令,如果是则直接运行,不是则fork一个子进程运行
子进程用execve运行对应的命令
等待前台进程结束
返回读取阶段
6.3 Hello的fork进程创建过程
./hello 学号 姓名 秒数
分割命令行作为参数
发现不是内置命令,fork()一个子进程,子进程除PID外完全继承父进程
将参数填入
运行,结束后回到shell
6.4 Hello的execve过程
fork()之后,子进程调用execve()函数在当前子进程的上下文中加载一个新的程序,删除程序原有的虚拟栈空间等,并调用启动代码,加载器创建内存映像,在程序头部的带领下,加载器将可执行文件的片复制到代码段和数据段,接下来。加载器跳转到程序的入口点。
6.5 Hello的进程执行。
Hello程序在运行时,进程提供给应用程序的抽象有:一个独立的逻辑控制流,它提供一个假象,好像进程独占的使用处理器;一个私有的地址空间,它提供一个假象,好像程序独占的使用CPU内存。
操作系统提供的抽象:
- 逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计数器PC的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象的指令。这个PC值的序列叫做逻辑控制流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流。
- 上下文切换。操作系统内核使用一种称为上下文切换的叫高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需状态。
- 时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。多任务也叫时间分片。
- 用户模式和内核模式。处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式里。用户模式里的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。
- 上下文信息。上下文就是内核重新启动一个被抢占顶点进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
Hello程序执行过程中,在进程调用execve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf函数输出字符串,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,CPU不断切换上下文,使运行过程被分成时间片,与其他进程交替占用CPU,实现进程的调度。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 异常的分类
6.6.2 各命令及运行结果
(1)正常运行(回车
(2)Ctrl-Z
输入Ctrl-Z默认结果为挂起前台的任务,hello程序并没有回收,而是在后台运行,使用ps指令观察进程,可以看到hello没有被回收;使用fg指令将hello程序调到前台,shell先打印了行命令,然后打印了剩下的三行信息,最后输入字符串abc,程序结束.
- Ctrl-C
输入Ctrl-C后,内核发送一个SIGNT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现hello程序被终止。
- 不停乱按
输入都被缓存到stdin,不影响程序。
- jobs命令
- pstree命令
- kill命令
6.7本章小结
本章简单概括了进程的概念和作用、shell的作用和处理流程,了解了hello程序的进程创建启动和执行过程,最后对hello程序的异常和运行结果的输入作了简要分析。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
是指由程序产生的与段相关的偏移地址部分。页式存储器的逻辑地址由两部分组成:页号和页内地址。[段标识符 : 段内偏移地址]的表示形式,其中的段内偏移地址就是指逻辑地址。
7.1.2 线性地址
是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址[段基地址+段内偏移地址]就是线性地址。
7.1.3 虚拟地址
程序访问存储器所使用的逻辑地址称为虚拟地址,虚拟地址经过翻译得到物理地址。
7.1.4 物理地址
在存储器中以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,也叫实地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址=段选择符+偏移量;每个段选择符大小为16位,段描述符为8字节(注意单位);GDT为全局描述符表,LDT为局部描述符表;段描述符存放在描述符表中,也就是GDT或LDT中;段首地址存放在段描述符中;
每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。
简单地说,从逻辑地址到线性地址,首先要获得段偏移有效地址;然后取出段寄存器对应的描述符的基地址;最后将二者计算相加,于是得到了线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,物理内存也被分割成物理页。利用页表来管理虚拟页,页表是一个页表条目的数组,每个PTE由一个有效位和一个n位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段表示DRAM中相应的物理页的起始位置,如果发生缺页,则从磁盘读取。
7.4 TLB与四级页表支持下的VA到PA的变换
Corei7采用四级页表的层次结构,CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。若命中,则得到物理地址PA,若没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后 MMU按照上述操作获得了物理地址 PA。根据cache大小组数的要求,将 PA分为 CT,CS,CO。根据CS寻找到正确的组,比较每一个 cacheline是否标记位有效以及 CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去 L2,L3,主存判断是否命中,当命中时,将数据传给 CPU同时更新各级 cache的 cacheline。
7.6 hello进程fork时的内存映射
当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要一下几个步骤:
(1)删除已存在的用户区域;
(2)映射私有区域:为新程序 hello 的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的;
(3)映射共享区域:如果 hello 程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;
(4)设置程序计数器(PC):execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障概念:当指令引用一个虚拟地址,而与该地址相对于的物理页面不在内存中,因此必须从磁盘中取出时,就会发送缺页故障。
缺页中断处理:在试图翻译某个虚拟地址A时,触发了一个缺页异常。这个异常转移到内核的缺页异常处理程序。
缺页异常处理程序要进行判断:
(1)如果虚拟地址是一个不存在的页面,则视为段错误;
(2)如果虚拟地址不合法,比如违反了只读的约定,则触发保护异常机制;
(3)如若虚拟地址可访问且合法,那么视为正常缺页。对于正常缺页,程序会选择一个牺牲页,牺牲掉它,然后将虚拟地址不知道内存中,并更新PTE。这样,再访问虚拟地址对应的物理地址,就不会缺页了。
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存域,称为堆。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器有两种基本风格。两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体负责释放已分配的块。
显式分配器:要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。
隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集,例如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
基本方法与策略:
1.隐式空闲链表
任何实际的分配器都需要一些数据结构,允许它来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身,有一种方法是:一个块除了是由一个字的头部、有效载荷、可能的一些额外的填充组成外,还有一个与头部相同的脚部组成。头部和脚部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。
我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。在带边界标签的隐式空闲链表中,我们的脚部就标记了一个块的结束。
合并的时候分配器就可以通过检查脚部来检查前一块的状态和大小了。
2.显式空闲链表
还有一种方法,是将空闲块组织为某种形式的显示数据结构是一种更好的方法,因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
另一种方法是按照地址顺序来维护链表,其中链表中的每一个块的地址都小于它后一个块的地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要地介绍了hello进程的内存地址空间管理,并对intel的段式管理和页式管理做了介绍,对TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问进行了介绍,进而结合hello进程对fork与execve从虚拟内存视角进行了分析。最后介绍了缺页故障与缺页中断处理和动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
一个 Linux 文件是一个 m 字节的序列,所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件。每个文件都有一个类型来标明他在系统中的角色:
普通文件包含任意文件;目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录;套接字是用来与另一个进程进行跨网络通信的文件
设备管理:unix io接口
所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件,而所有的输入输出都被当做相应的文件的读和写来执行,这种将设备映射为文件的方式,允许linux内核引出一个低级的,简单的应用接口称为Unix I/O
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口
(1)打开文件:一个应用程序要求内核打开相应文件,来访问I/O设备。会返回一个非负整数,称为描述符,他在后面的操作中标志这个文件,内核记录有关打开文件的所有信息,程序只需要记住该描述符。
(2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入 、标准输出和标准错误。
(3)改变文件位置:内核对于每个文件保持一个初始为0的文件位置,该位置标示从头部开始的文件的偏移量。程序可以通过seek函数,显示地修改此文件位置。
(4)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。
(5)关闭文件:一个应用程序完成对文件访问后,要求内核关闭相应文件。
8.2.2 Unix IO函数
(1)打开文件:int open(char* filename,int flags,mode_t mode)将filename文件转为操作符,mode为访问权限
(2)关闭文件:int close(fd)关闭fd文件,返回操作结果
(3)读文件:ssize_t read(int fd,void *buf,size_t n)从fd文件复制至多n个字节到buf处
(4)写文件:ssize_t wirte(int fd,const void *buf,size_t n)从buf处复制至多n个字节给fd文件
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是字符指针,(char*)(&fmt | 4)表示fmt后的第一个参数的地址。vsprintf函数返回值是要打印出来的字符串长度,其作用是格式化,产生格式化的输出并保存在buf中。最后的write函数即为写操作,把buf中的i个元素的值写到终端。
在write函数中,追踪之后的结果如下:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call函数。在write函数中可以理解为其功能为显示格式化了的字符串。
字符显示驱动子程序:从ASCII到字模库到显示vram。显示芯片按照刷新频率逐行读取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;
}
在getchar函数中,首先声明了几个静态变量:buf表示缓冲区,BUFSIZ为缓冲区最大长度,而bb指向缓冲区首地址。
Getchar调用read函数,将缓冲区读入到buf中,并将长度送给n,再重新令bb指针指向buf。最后返回buf中第一个字符。异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
Getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接收到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,简述了Unix的IO接口和函数,对printf函数和getchar函数进行了分析。
结论
首先我们在计算机的文本编辑器上用C语言编写hello.c的源文件,依次要经过以下步骤:
1.预处理。hello.c在预处理之后,将头文件的内容插入到程序文本中,得到hello.i文件;
2.编译。编译器对hello.i进行编译,从而得到汇编语言文件hello.s;
3.汇编。计算机仍不能直接识别并执行汇编代码,还需经过汇编器汇编,得到与汇编语言一一对应的机器语言指令,在汇编之后,得到了可重定位目标文件hello.o;
4.链接。即使hello.o文件是机器语言文件,但指令尚未确定虚拟地址,还需链接器对hello.o中调用函数的指令进行重定位,将调用的系统函数如printf.o等链接到hello.o,得到可执行目标文件hello;
5.运行。hello是可以在计算机运行的文件了。我们可以在计算机上正式运行hello;
6.创建进程。首先在shell-Bash中输入运行hello的命令行./hello,OS就为hello创建一个子进程,hello就在进程当中运行;
7.加载程序。shell调用execve函数,启动加载器映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数;
8.执行指令。CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制流;
9.访问内存。MMU将程序中使用的虚拟内存地址通过页表映射成物理地址;
10.动态申请内存。printf调用malloc向动态内存分配器申请堆中的内存;
11.信号管理。当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGSTP信号给进程,并将前台作业停止挂起;
12.终止。当子进程执行完成时,shell父进程回收终止的hello进程。内核删除这个进程创建的数据结构。
通过本次实验,我深切感受到了计算机系统的精细和复杂及其功能的强大。
附件
hello.c 源程序
hello.i 经过预处理的文件
hello.s 编译后的汇编语言文件
hello.o 汇编后的可重定位目标执行文件
hello 连接后的可执行文件
helloo.elf hello.o的ELF格
helloo.txt hello.o的反汇编代码,分析hello.o
hello.elf hello的ELF格式,分析重定位过程
hello.txt hello的反汇编代码,分析重定位过程
参考文献
[1] https://www.cnblogs.com/pianist/p/3315801.html
[2] Random E.Bryant David R.O’Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016
[3] https://www.cnblogs.com/knife-king/p/11090029.html
[4] https://www.cnblogs.com/pianist/p/3315801.html
[5] https://www.cnblogs.com/diaohaiwei/p/5094959.html