计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2022年5月
本文主要描述了hello在linux下的诞生到真正运行再到终止的生命周期,其经历了预处理,编译,汇编,链接的过程,之后在linux的终端下运行,经历进程管理,储存管理,I/O管理,虽然其只是一个简单的hello程序,但却很有代表性。
关键词:hello;预处理;编译;汇编;链接;进程;存储;虚拟内存;地址翻译;I/O管理;
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:在linux中,hello.c在被编写好后,依次经过预处理,编译,汇编,链接,然后就成为了可执行文件hello,此时在linux的shell中被启动时,shell就fork一个子进程,hello在此从program变为了process
020:hello成为进程之后,从虚拟内存转换到物理内存,这其中还会通过缓存cache,快表,页表等进行加速,cpu会对其hello分配时间片,运行结束后,shell对hello进程进行回收,完成020
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf
1.3 中间结果
文件名 | 文件作用 |
hello.i | hello.c经过cpp预处理后的文本文件 |
hello.s | hello.i经过ccl编译后的汇编文件 |
hello.o | hello.s经过as汇编后的可重定位目标文件 |
hello | hello.o经过ld链接后的可执行目标文件 |
hello.out | hello经过反汇编后的可重定位目标文件 |
1.4 本章小结
(第1章0.5分)
在本章中,列举了从hello.c到hello所需要经历的步骤和中间产物文件及其简介与作用,同时列举了本次论文的软硬件和环境
第2章 预处理
2.1 预处理的概念与作用
概念:对源程序进行编译前,对源程序的预处理命令(带#的命令,宏定义,文件包含命令,条件编译命令等)进行处理,将处理后的结果与源程序一起编译,得到目标代码
作用:(1)处理宏定义,和条件编译命令
(2)删除源代码中的注释和多余的空白字符
(3)将源程序引用的所有库导入并合并为一个完整的文本文件
(4)对特殊符号处理
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c > hello.i
2.3 Hello的预处理结果解析
打开hello.i可以看到,文件的内容增加了许多,大小也增加了许多,文件大量使用了extern关键字,typedef语句和结构体,在预处理得到的文件最后仍为原来在hello.c中的main函数,预处理则是将hello.c中的宏展开,将头文件中的内容放入了hello.i文件中。
2.4 本章小结
本章对预处理的概念和作用进行了分析,并且列出了在shell中进行预处理的指令,预处理会对程序中的宏进行替换和宏展开,会将所包含的头文件引入,还会根据指令进行选择性编译等。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译将hello.i翻译成文本文件hello.s,其进行语法的检查,生成汇编,将代码翻译称汇编语言或者机器语言
作用:将hello.i转换为汇编文件hello.s,进行语法检查,同时在不同的参数(-o1,-o2等)下还有一定的优化作用,对不同的高级程序语言产生一样的汇编语言。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据处理
按从上至下遇到的数据有:
- 字符串
字符串在只读的数据段之中,在调用printf的时候作为其参数
2.整型变量
对如图的局部变量i赋值以及,编译器为以下
储存位置及赋初值:
i自增:
比较:
对于变量argc:从main中进入后,其被储存在%edi中,然后被赋值给了-20(%rbp):
3.数组
对数组的操作,即对 char *argv[]的处理,编译器先将其保存在%rsi中,之后被保存在-32(%rbp)的位置
3.3.2 赋值操作
在hello.c中,赋值操作只有如图所示的i=0一个,对应的汇编语句为:
说明i被存储在-4(%rbp)之中
其余的hello.s中的赋值操作还有:movq,与movl不同之处是赋值的大小不同,movq进行4字即8字节的赋值,而movl进行双字即4字节的赋值
3.3.3 算数操作
hello.c中出现的算数操作为i++,即i的自增,结合上方所说的i的储存位置在汇编语句中体现如下:
即i+=1,由于i为int型,所以增加的语句为addl表示双字
3.3.4 关系操作
3.3.5 数组操作
在hello.c涉及到的数组操作为以上,分别取了argv[1],argv[2]以及argv[3]
汇编语句为以下:
其中34行表明数组的首地址存在-32(%rbp),且该地址被放入%rax之中,35行则在该地址上加了16,而由于char*大小为8字节,故此时%rax中存的便是argv[2],同理,在37、38行则为argv[1],在44、45行为argv[3],也对应了c语句中的顺序
3.3.6 关系操作
!=对应以下汇编语句:
汇编语句意为cmpl(双字大小的比较)将-20(%rbp)所存的数(即argc)比较,在argc等于4时跳转(此时会设置条件码),否则会继续向下执行
<对应以下汇编语句:
汇编语句意为cmpl(双字大小的比较)将-4(%rbp)所存的数(即i)与7比较,在7<=i,即i>=7时跳转(此时会设置条件码),否则会继续向下执行
3.3.7 控制转移
(1)在hello.c中第一个控制转移为if
汇编如下:
cmpl(双字大小的比较)将-20(%rbp)所存的数(即argc)比较,在argc等于4时跳转(此时会设置条件码),否则会继续向下执行
(2)第二个为for
汇编如下:
首先在30行处,i被赋值为0,然后跳转到比较语句,cmpl(双字大小的比较)将-4(%rbp)所存的数(即i)与7比较,在7<=i,即i>=7时跳转(此时会设置条件码)到.L4,每次在.L4执行完后,i++
3.3.8 函数操作
hello.s调用了5个函数:puts(printf),exit,atoi,sleep,getchar
在调用时需要传参,即将参数传给某个寄存器,供被调用的函数使用,同时在调用完后需要返回,一般将返回值存在%rax中
- printf在这里转换为put函数,结合hello.c来看,puts和printf中传入的参数为.LC0处的字符串
- exit在被调用前,1被传给了%edi,与c语句中相同
- atoi在被调用前,%rax中的数被传给了%rdi,结合上文看为argc[3],与c语句中相同,结合下文来看其返回值被存入了%eax之中
- sleep在被调用前,将%eax中的值传给了%edi,结合上文来看是经过atoi处理后返回的值
- getchar无需参数
对于main函数,其返回值为0:
3.4 本章小结
本章中我们对编译过程进行了分析,同时列举了在linux终端中编译的指令,同时对编译所得到的hello.s文件中的汇编语句进行了分析
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编指的是从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,机器码可以被cpu直接执行
作用:可以把汇编语言对应地翻译成计算机可以直接识别的机器码,其将汇编语言转换成可重定位目标程序的格式(.o文件)并且保存
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
(1)命令:readelf -h hello.o 查看hello.o的ELF头的信息
如图可看到,节头数量有14个,ELF头大小为64字节......等基本信息
(2)命令:readelf -S hello.o 查看hello.o的节头信息
(3)命令:readelf -s hello.o查看符号表信息
(4)命令:readelf -r hello.o查看重定位信息
如图在.rela.text中有8个项目需要重定位,通过后面的信息可以看出,其中有6个函数,2个段,而在.rela.eh_frame中有1个段需要重定位
(5)命令:readelf -a hello.o查看ELF所有信息
其中包含ELF格式文件的所有基本信息
4.4 Hello.o的结果解析
运行odjdump -d -r hello.o得到结果如下:
hello.s如下:
可以看到,机器语言与hello.s几乎是一一对应的关系,而反汇编出来的代码多了机器代码,即反汇编出来的语言为机器代码➕汇编语言,在操作数上两者有一定的差别,下列表举出:
odjdump | .s文件 | |
分支转移 | 指令的地址 | 跳转表格式 |
进制 | 16进制 | 10进制 |
函数调用 | 函数名 | 函数地址 |
4.5 本章小结
本章中,对hello.s进行了汇编,列出了汇编时的命令,生成了可重定位目标文件hello.o并对其的信息进行了分析:ELF头、节头部表、符号表、可重定位节等,比较了hello.s与hello.o通过反汇编所得到的代码的不同,并分析了这些不同,以及分析了机器代码与汇编语言一一对应的关系
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:分为静态链接和动态链接。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。动态链接则并没有这么做,而是在其中加入了所调用函数的描述信息。编译时,加载时,运行时都可以链接。
作用:把多个可重定位文件合并在一起,找到这些文件之间的关系,生成一个大的、有绝对位置的目标程序,使得机器可以执行。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
命令为: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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1)通过readelf -h hello指令,可以看到如图所示的信息,其与hello.o不同,表现在文件类型(可执行文件)和节的数目上,hello的elf中节的数目增加了
节头部表对所有hello中的所有的节的信息进行了声明,其中包括大小和 偏移量,根据这些信息,我们能定位各个节的位置与大小
rela.text重定位节
symtab 符号表
5.4 hello的虚拟地址空间
从程序头部信息可看出,可加载的程序段的地址为0x400000
用edb加载hello,查看本进程的虚拟地址空间各段信息:
从date dump中,我们可以看出,hello的虚拟空间从0x400000开始,结束于0x400ff0
与5.3中的信息进行对照,我们可以看到各段信息,如.data段开始于0x404048,大小为0x4,在edb中便可以查看:
5.5 链接的重定位过程分析
不同点在于:
- hello的反汇编有着确定的虚拟地址,即已经完成了重定位后所具有的确定的虚拟地址,而hello.o的反汇编则没有,其虚拟地址默认开头为0,未完成重定位
- hello在反汇编时多了许多库函数的代码,即已经将库函数的代码链接到了整个程序中,程序的各个节变得完整,地址也变得确切
如图:
hello的反汇编:
hello.o的反汇编:缺少具体函数,直接进入main()
重定位过程分析:
-
5.6 hello的执行流程
流程及函数和地址如下:
程序
地址
_dl_start
0x7ffff7de2630
_dl_init
0x7ffff7de3f30
_start
0x4010f0
__libc_start_main
0x7ffff7de3fc0
_cxa_atexit
0x7ffff7e06e10
libc_csu_init
0x4011c0
__setjmp
0x7ffff7e02cb0
main
0x401125
_exit
0x7ffff7e06a70
5.7 Hello的动态链接分析
动态链接,与静态链接将所有程序函数打包到一起,成为一个完整的可执行文件后再执行不同,其旨在在程序运行时将它们链接在一起从而执行一个完整的程序,在形成可执行程序时,如果遇到了一个外部函数,检查动态链接库,若发现其为一个动态链接符号,则程序不会对其进行重定位,而是等其装载的时候才执行。
动态链接在调用函数时才会进行符号的映射,使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。
如图为.got.plt在调用init前后的变化
调用前:
调用后:
可以看到出现了变化
5.8 本章小结
本章主要介绍了链接的方法及作用,阐述了从hello.o到可执行文件的过程,同时分析了hello程序的虚拟空间,重定位的流程,程序的执行流程,动态链接和静态链接的不同,至此,终于得到了一个可执行文件hello
(第5章1分)
第6章 hello进程管理6.1 进程的概念与作用
概念:进程是程序运行时的一个实例,其有自己的的地址空间。
作用:有了进程这个概念(假象)后
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell-bash时用户和linux内核之间的一个接口程序,用户输入的命名通过shell解释后传给内核
处理流程:(1). 终端读取用户输入的命令
(2). 终端分析用户输入的命令并分割,将其分割后传给argv数组
(3). Shell对参数进行检查,判断其是否为内置命令,是则按预定的流程进行显示,否则当作为一个可执行程序
(4). fork()一个进程,在该进程中调用execve()函数执行步骤(2)中分割出来的程序
(5). 若在程序后未带&则说明其为前台进程,shell调用等待前台程序结束的函数等待其运行完,若为后台进程,则返回
6.3 Hello的fork进程创建过程
终端中输入命令:./hello 120L022310 牟铎 1之后,Shell处理该命令,由于该命令不为内置命令,所以Shell会fork()一个子进程,子进程与父进程有相同的上下文,得到相同的虚拟空间(但独立)一个副本,且PID不同
6.4 Hello的execve过程
Shell在fork()后,在得到的子进程的上下文中执行新程序,然后继续用命令行中的剩余参数解析调用execve,其调用启动加载器执行hello,覆盖当先进程的代码、数据、栈,继承已经打开的文件描述符和信号,加载器将PC指向hello程序的开始处,下条指令开始执行hello,在无错误的情况下,调用后从不返回
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
如6.1中说,进程似乎独占的使用了处理器,程序似乎独占了CPU内存
hello进程在运行的过程中正是基于这些抽象:
有了以上的信息,hello进程的执行便清晰多了,在execve之后,hello在用户模式下运行直到sleep函数,此时便经历了上下文切换,在切换时进入内核模式,然后将将控制交给其他的进程,内核会等待sleep完成,然后再切换回hello,恢复hello的上下文信息,经历如上的上下文切换过程,如此经历8次结束。
6.6 hello的异常与信号处理
随便乱按:
-
随便乱按不会影响程序的执行,这些都被读入到stdin中,等待一个回车以结束getchar
回车:
-
不影响程序执行,回车被读入到stdin之中,在最后调用getchar时结束整个hello的执行
Ctrl-z:
-
输入Ctrl-z后,内核向前台进程组发送SIGTSTP信号,hello挂起
然后输入jobs,可看到暂停的hello:
-
输入fg恢复其执行:
输入ps查看系统中进程:
-
输入pstree查看进程树,看到terminal中的进程:
-
Ctrl-c:向前台进程组发送SIGINT信号,终止前台进程组的每个作业
-
6.7本章小结
本章对进程的概念作用进行了梳理,且梳理了shell对进程的处理,包括如何创建新进程(fork),如何执行hello(execve),进程的执行,异常以及信号处理
(第6章1分)
第7章 hello的存储管理7.1 hello的存储器地址空间
(1)逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。
(2)物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
(3)虚拟地址:即线性地址。
(4)线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器,如图所示。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成,如图7.3.2所示
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
如图为一个段描述符:
-
如图为段选择符:
-
转换过程为:
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(即虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。
虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小时相同的。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如图7.3.1所示,页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在次胖的起始地址。
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。一般来说,VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
-
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(快表)翻译后备缓冲器
(1)MMU中一个小的具有高相联度的集合
(2)实现虚拟页号向物理页号的映射
(3)页数很少的页表可以完全放在TLB中
TLB与缓存的使用方式十分相似,也是分为标记位,组索引,不同点在于剩余部分是VPO即虚拟页面偏移:
如图所示:
-
MMU通过虚拟地址的VPN部分访问TLB,当TLB命中后,地址翻译在芯片上的MMU中完成,极快的完成了地址翻译
如图为多级页表的翻译模式:
-
此为intel的翻译模式:
-
以此为参考,intel四级页表的翻译模式如下:
- 在搞清楚翻译模式前,要先明白在该页表模式下一个虚拟地址是如何分的:虚拟地址有48位,而物理地址52位,页表大小4kb即2^12bytes,TLB四路,16组相连。页表4kb,PTE8b情况下,一个页表存储512个条目,为9位,4个页表则36位,即VPN为36位,则VPO12位,则PPO12位,故PPN40位;而又TLB16组,故TLB组索引位为4位,由此TLBT得到32位。
- CPU 产生虚拟地址 VA,VA 传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40位)与 VPO(12位)组合成 PA(52位)
- 如果 TLB 中没有命中,MMU 向页表中查询,CR3 将会确认第一级页表的起始地址,VPN1(9位)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址
- 以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
-
7.5 三级Cache支持下的物理内存访问
下以L1Cache为例进行讨论:
- 首先对虚拟地址的组索引位转化后进行寻找,找到相应的组
- 在组中通过标记位进行比较,匹配且有效位为1时说明命中
- 缓存命中后通过块偏移就知道了所要寻找字节的位置,此时将字节取出返回给CPU,完成访存
-
若未命中,则从下一级中取出,并通过策略替换到当先的高速缓存行中
-
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。
处理程序执行如下步骤:
1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
2.检查进程是否有权限,如果不具有便读、写或执行该区域页面,则触发保护异常,程序终止。
3.两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。紧接未初始化的数据区域后面开始向上(高地址增长)。对于每一个进程,内核维护者一个变量brk,它指向堆的顶部。
分配器有两种基本的风格。两种风格都要求应用显式分配块。不同之处在于那个实体负责释放已分配块
显式分配器:C malloc 与 free
隐式分配器:要求分配器检测一个已分配块何时不被程序使用时释放块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配块的过程叫做垃圾收集器。
(1)隐式空闲链表的堆块格式:
(2)隐式空闲链表的带边界标记的堆块格式:
使用边界标记的堆块的格式其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。通过这种结构,隐式动态内存分配器会对堆进行扫描,通过头部和脚部的结构实现查找。
(3)显式空闲链表:
显式空闲链表是将空闲块组织为某种形式的显示数据结构。如图所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。
-
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章对hello的储存进行了分析,论述了对hello运行时fork和exceve的过程,同时对虚拟内存向物理内存的翻译进行了阐述,分析了Intel的TLB,三级Cache结构和四级页表结构以及对缺页故障的应对策略,分析了Linux下的动态储存分析管理。
(第7章 2分)
第8章 hello的IO管理8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:
一个Unix 文件就是一个m个字节的序列:B0, B1, … , Bk , … , Bm-1
所有的IO设备,如网络、磁盘和终端,都被模型化为文件,Unix里面的所有事物都是文件。
Unix文件类型有
Unix I/O主要特点有:
文件到设备的优雅映射允许内核导出称为Unix I / O的简单接口
所有输入和输出均以一致的方式统一处理
Unix IO函数:
8.3 printf的实现分析
(1)下为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;
}
(2)这其中vsprintf声明如下:
int vsprintf(char *buf, const char fmt, va_list args)
{
char p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
其作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,它的返回值为打印出来的字符串的长度,以上的代码中vsprintf只实现了对16进制的格式化。
(3)而对于系统函数write,如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL(4)对于其中涉及到的int INT_VECTOR_SYS_CALL,其表示要通过系统来调用sys_call这个函数,这个函数的功能便是根据以上所得到的元素个数和第一个字符不断地打印出字符,直到遇到'\0',其采用直接写显存的方法显示字符串,详细来说便是将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上,至此,我们便理解了printf的底层实现
8.4 getchar的实现分析
异步异常-键盘中断的处理:用户在按键的时候,键盘接口得到对应的键盘扫描码并产生中断请求,该请求抢占当前进程并运行键盘中断处理子程序,其接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回整个字串。
8.5本章小结
本章介绍了Linux的IO管理,Unix IO接口和其函数,对printf和getchar函数实现的底层逻辑进行了分析。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello的一生经历了以下的步骤
我的感想如下:
在一个c程序到可执行程序并运行的过程中,计算机完成了许许多多的严谨的工作,其不厌其烦 的重复着每一次,每一步,而为了性能的考量,又发明了Cache这样的存在,为了程序运行的简便,有了虚拟内存这个伟大的概念,可以说,从.c到可执行文件再到其真真切切的快速正确的运行,每一步都严谨且精妙,在各种条件的限制下,设计者进行了诸多的努力
(结论0分,缺失 -1分,根据内容酌情加分)
附件列出所有的中间产物的文件名,并予以说明起作用。
hello.i
hello.c经过cpp预处理后的文本文件
hello.s
hello.i经过ccl编译后的汇编文件
hello.o
hello.s经过as汇编后的可重定位目标文件
hello
hello.o经过ld链接后的可执行目标文件
hello.out
hello经过反汇编后的可重定位目标文件
hello1.out
hello.o经过objdump反汇编后的文件
(附件0分,缺失 -1分)
参考文献为完成本次大作业你翻阅的书籍与网站等
(参考文献0分,缺失 -1分)
- 我们的程序好像独占了处理器和内存,似乎是系统中唯一运行的程序
- 上下文切换中,处理器似乎无间断的执行了我们程序中的指令,让程序的运行更加简单
- .上下文信息:上下文信息是内核在恢复一个被抢占进程的运行时所需要的信息,其包括通用寄存器、用户栈、状态寄存器等对象的状态和值,在恢复该进程时需要恢复这些信息状态
- .上下文切换:进程在被抢占时所经历的机制,即保存当前进程(即被抢占的进程)的上下文信息,恢复要切换的进程的上下文信息,将控制交给要切换的进程。
- .进程时间片:进程在运行其控制流时所经历的时间段为进程时间片
- .核心态与用户态的转换:进程在进行上下文切换时就会经历用户态与核心态的转换,也成为内核模式与用户模式的转换
- 在搞清楚翻译模式前,要先明白在该页表模式下一个虚拟地址是如何分的:虚拟地址有48位,而物理地址52位,页表大小4kb即2^12bytes,TLB四路,16组相连。页表4kb,PTE8b情况下,一个页表存储512个条目,为9位,4个页表则36位,即VPN为36位,则VPO12位,则PPO12位,故PPN40位;而又TLB16组,故TLB组索引位为4位,由此TLBT得到32位。
- CPU 产生虚拟地址 VA,VA 传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40位)与 VPO(12位)组合成 PA(52位)
- 如果 TLB 中没有命中,MMU 向页表中查询,CR3 将会确认第一级页表的起始地址,VPN1(9位)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址
- 以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
- 首先对虚拟地址的组索引位转化后进行寻找,找到相应的组
- 在组中通过标记位进行比较,匹配且有效位为1时说明命中
- 缓存命中后通过块偏移就知道了所要寻找字节的位置,此时将字节取出返回给CPU,完成访存
- 若未命中,则从下一级中取出,并通过策略替换到当先的高速缓存行中
- 普通文件 包含用户/应用程序数据(二进制,文本等)的文件,除了“字节顺序”以外,操作系统对格式一无所知
- 目录文件 一个包含有名字和其它文件位置的文件
- 字符设备和块设备 终端(Terminals特殊字符)和磁盘(disks特殊块)
- FIFO(管道) 一种用来进程内部通信的文件类型
- Socket 一种用来不同进程问的网络通信文件路径
- open:一个应用程序通过此方法来要求内核打开相应的文件,内核返回一个非负整数,叫做文件描述符,后续所有的操作都基于这个文件描述符。内核记录了对应文件描述符的所有信息,应用程序只需要记住这个描述符
- close: 成功的时候返回0,现在异常的时候返回-1.关闭文件会通知内核已经完成访问该文件,不能重复关闭同一个文件,请务必检查返回码
- read 读取文件会从当前文件位置复制字节到内存,然后更新文件位置(前提是文件支持seeking),返回从文件fd读取到buf的字节数
- write 写入文件会将字节从内存复制到当前文件位置,然后更新当前文件位置(前提是文件支持seeking)
- seek:对于每个打开的文件,内核保存着一个文件位置k,表示从文件开头起始字节的偏移量,默认为0.应用程序可以通过seek显示的设置k的值
- 编写,程序员也就是我们通过键盘按照规则编写hello.c代码
- 预处理(cpp::将hello.c进行预处理,进行宏展开,消除那些对计算机无用的,只是给人看的注释,同时合并外部库,生成了hello.i文件
- 编译(ccl):将hello.i文件翻译,至此hello终于向着机器语言进发,成为了汇编语言文件hello.s
- 汇编(as):将hello.s翻译为可重定位目标文件hello.o
- 链接(ld):俗话说,众人拾柴火焰高,hello的运行也要依赖外部库,其进行静态和动态链接,将助力一一放到自己的身上,终于hello这个可执行目标文件诞生了
- 运行:在shell中,我们键入指令运行hello
- 创建子进程:shell在执行我们所键入的指令时发现其不为内部命令,于是fork一个子进程
- 加载程序:在子进程中,shell调用execve函数,启动加载器并映射虚拟内存,进入程序载入物理内存,然后进入main函数
- 执行:cpu在hello运行时为其分配时间片,这之中,hello顺序执行自己的逻辑控制流
- 访存:MMU翻译虚拟地址,通过页表映射成为物理地址并访问
- 动态内存申请:hello在运行过程中会调用printf,而其会调用malloc向动态内存申请堆之中的内存
- 信号的管理:hello在运行时,我们输入的一些指令会对其产生影响:Ctrl-z会发送SIGTSTP信号让其停止并挂起,Ctrl-c会发送SIGINIT信号终止其运行
- 终止:在hello结束或终止时,内核会回收hello,将退出状态传递给父进程,并删除为hello创建的所有数据结构,至此hello运行完毕
- 《深入理解计算机系统》 机械工业出版社 第三版
- https://blog.csdn.net/Pipcie/article/details/105670156?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165271048516780357224351%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165271048516780357224351&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-3-105670156-null-null.142^v9^pc_search_result_cache,157^v4^control&utm_term=Intel%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80%E5%88%B0%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E7%9A%84%E5%8F%98%E6%8D%A2-%E6%AE%B5%E5%BC%8F%E7%AE%A1%E7%90%86&spm=1018.2226.3001.4187
- https://blog.csdn.net/yubo112002/article/details/82527157?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-82527157-blog-49102903.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-82527157-blog-49102903.pc_relevant_default&utm_relevant_index=1
- [转]printf 函数实现的深入剖析 - Pianistx - 博客园
- https://blog.csdn.net/qq_43583902/article/details/122289843?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%8A%A8%E6%80%81%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E5%99%A8%E7%BB%B4%E6%8A%A4%E8%80%85%E4%B8%80%E4%B8%AA%E8%BF%9B%E7%A8%8B%E7%9A%84%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%EF%BC%8C%E6%88%90%E4%B8%BA%E5%A0%86%E3%80%82&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-5-122289843.142^v10^pc_search_result_control_group,157^v4^control&spm=1018.2226.3001.4187
- https://blog.csdn.net/weixin_42695485/article/details/110248969?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165277907316781818760683%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165277907316781818760683&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-3-110248969-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=Unix+IO%E6%8E%A5%E5%8F%A3%E5%8F%8A%E5%85%B6%E5%87%BD%E6%95%B0&spm=1018.2226.3001.4187