2021哈工大计算机系统大作业——程序人生-Hello’s P2P
计算机系统
大作业
题 目 程序人生-Hello’s P2P
计算机科学与技术学院
2021年6月
关键词:Hello’s P2P;进程管理;内存管理;I/O管理
6.2 简述壳Shell-bash的作用与处理流程... - 36 -
6.3 Hello的fork进程创建过程... - 36 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 42 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 43 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 43 -
7.5 三级Cache支持下的物理内存访问... - 45 -
7.6 hello进程fork时的内存映射... - 46 -
7.7 hello进程execve时的内存映射... - 46 -
第1章 概述
1.1 Hello简介
1.2 环境与工具
硬件环境:X64 CPU;2.6GHz;16G RAM;256GHD Disk
软件环境:Windows10 64位;Vmware 16;Ubuntu 16.04 LTS 64位
开发与调试工具:gcc,gdb,edb,readelf,HexEdit
1.3 中间结果
hello.i:C预处理器产生的一个ASCII码的中间文件,用于分析预处理过程。
hello.s:C编译器产生的一个ASCII汇编语言文件,用于分析编译的过程。
hello.o:汇编器产生的可重定位目标程序,用于分析汇编的过程。
hello:链接器产生的可执行目标文件,用于分析链接的过程。
hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
hellold.txt:hello的反汇编文件,用于分析可执行目标文件hello。
helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
helloldelf.txt:hello的ELF格式,用于分析可执行目标文件hello。
1.4 本章小结
本章简述了Hello的P2P、020的整个过程并介绍了实验的基本信息:环境、工具以及实验的中间结果。
第2章 预处理
2.1 预处理的概念与作用
合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
2.4 本章小结
本章介绍了预处理的概念和作用,结合实际程序分析了预处理的过程,包括宏替换、头文件引入、删除注释、条件编译等。
第3章 编译
3.1 编译的概念与作用
汇编语言程序比源程序的层次更低,但是与机器代码相比程序员更容易理解,汇编语言相当于高级语言和机器语言之间的过渡,是从源程序转换到机器代码的关键中间环节。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
(1)常量:hello.c源程序中的两个printf的参数是字符串常量,分别为"Usage: Hello 学号 姓名!\n"和"Hello %s %s\n"。
在编译生成的hello.s中可以看到,这两个字符串常量分别由.LC0和.LC1指示,均存放在只读数据段.rodata中。
(2)全局变量:hello.c源程序中的sleepsecs是全局变量,且已被赋初值。
(3)局部变量:hello.c源程序中的局部变量包括i,用于循环的计数。
hello.c中的其他局部变量还包括argc和argv,同样地,它们都存放在栈上的,并通过相对栈顶(%rsp)的偏移量来访问。
3.3.2赋值操作
hello.c源程序中一共包括两次赋值操作,分别是对全局变量sleepsecs赋初值和对循环变量i赋初值。
3.3.3类型转换
hello.c源程序中只包含一次隐式的类型转换,出现在全局变量赋初值的时候。
对于隐式类型转换,编译器会自己直接进行转换,在这个例子中,2.5被隐式类型转换为int型,编译器直接将转换后的值2放在了相应的数据段中。
3.3.4算术操作
hello.c源程序中只包含一次算术操作,出现在循环变量i每次增加1的时候。算术操作为++。
算术操作++代表自增1的运算,编译时转化成add类的加法指令,使用立即数1来实现每次增加1.
其他和算术操作相关的指令还包括inc,dec,neg,sub,imul等等。
3.3.5关系操作
编译时使用cmpl指令将argc和3进行比较,并设置条件码。跳转指令je根据条件码决定是否跳转。对于关系操作!=来说,可以选择je或者jne跳转指令。
类似地,编译时使用cmpl指令将i和9进行比较,并设置条件码。跳转指令jle根据条件码决定是否跳转。这里进行比较的值是9而不是10,与编译的过程中进行了优化有关。
3.3.6数组操作
hello.c源程序中有关数组的操作出现在访问argv元素的时候,通过argv[1]和argv[2]访问了字符指针数组中的元素。
3.3.7控制转移
编译时使用cmpl指令将argc和3进行比较,并设置条件码。跳转指令je根据条件码决定是否跳转。控制转移由指令je完成。
类似地,编译时使用cmpl指令将i和9进行比较,并设置条件码。跳转指令jle根据条件码决定是否跳转。控制转移由指令jle完成。
3.3.8函数操作
(1)函数的调用:hello.c源程序中一共出现了五次函数调用。
编译时,所有的函数调用都转换成了指令call,后面跟着调用函数的名字。
对于第二个函数exit,只有一个参数,通过寄存器%edi传递。
对于第三个函数printf,有三个参数,分别通过寄存器%edi、%rsi、%rdx传递。
对于第四个函数sleep,只有一个参数,通过寄存器%edi传递。
(3)函数的返回:编译时,在函数的最后添加指令ret来实现函数的返回。在hello.c这个例子中,只能看到main函数的返回。
3.4 本章小结
本章介绍了编译的概念和作用,并针对具体的例子hello.s,详细地分析了编译器如何处理C语言的各种数据以及各类操作。
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o
4.3 可重定位目标elf格式
使用readelf命令readelf -a hello.o > helloelf.txt查看hello.o的ELF格式,并将结果重定向到helloelf.txt便于查看分析。
4.3.1 ELF头
4.3.2节头部表
节头部表描述不同节的位置和大小,目标文件中的每个节都有一个固定大小的节头部表条目。
以hello.s为例,节头部表一共描述了13个不同节的位置、大小等信息。依次为:
[1].text节:已编译程序的机器代码,大小为0x7d字节,类型为PROGBITS,偏移量为0x40,标志为AX(表明该节的数据只读并且可执行)。
[2] .rela.text节:一个.text节中位置的列表,大小为0xc0字节,类型为RELA,偏移量为0x318,标志为I。
[3].data节:已初始化的全局和静态C变量,大小为0x4字节,类型为PROGBITS,偏移量为0xc0,标志为WA(表明该节的数据可读可写)。
[4].bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。大小为0x0字节,类型为NOBITS,偏移量为0xc4,标志为WA(表明该节的数据可读可写)。
[5].rodata节:只读数据,大小为0x2b字节,类型为PROGBITS,偏移量为0xc4,标志为A(表明该节的数据只读)。
[6].comment节:包含版本控制信息,大小为0x36字节,类型为PROGBITS,偏移量为0xef,标志为MS。
[7].note.GNU_stack节:标记可执行堆栈,大小为0x0字节,类型为PROGBITS,偏移量为0x125。
[8].eh_frame节:处理异常,大小为0x38字节,类型为PROGBITS,偏移量为0x128,标志为A(表明该节的数据只读)。
[9].rela.eh_frame节:.eh_frame节的重定位信息,大小为0x18字节,类型为RELA,偏移量为0x3d8,标志为I。
[10].shstrtab节:包含节区名称,大小为0x61字节,类型为STRTAB,偏移量为0x3f0。
[11].symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。大小为0x180字节,类型为SYMTAB,偏移量为0x160。
[12].strtab节:一个字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字。大小为0x37字节,类型为STRTAB,偏移量为0x2e0。
4.4.3符号表
4.3.4重定位节
以hello.s为例,重定位节.rela.text一共描述了8个重定位条目。重定位节.rela.eh_frame描述了1个重定位条目。
4.4 Hello.o的结果解析
使用命令objdump -d -r hello.o对hello.o进行反汇编,得到结果如图。
(1)立即数的变化:hello.s中的立即数都是用10进制数表示的。
但是在机器语言中,由于转换成了二进制代码,因此立即数都是用16进制数表示的。
(2)分支转移的不一致:hello.s中的分支转移(即跳转指令)直接通过像.LC0,.LC1这样的助记符进行跳转,会直接跳转到相应符号声明的位置。
助记符只是帮助程序员理解的,从汇编语言转换成机器语言之后,助记符就不再存在了,因此机器语言中的跳转使用的是确定的地址。下图中的main+0x29就表明要跳转到距main函数偏移量为0x29的位置。
(3)函数调用的不一致:hello.s中的函数调用直接在call指令后面加上要调用的函数名。
4.5 本章小结
本章介绍了汇编的概念和作用,通过对比hello.s和hello.o分析了汇编的过程,同时分析了可重定位目标文件的ELF格式。
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
使用readelf命令readelf -a hello > helloldelf.txt查看可执行目标文件hello的ELF格式,并将结果重定向到helloldelf.txt便于查看分析。
5.3.1 ELF头
5.3.2 节头部表
节头部表描述不同节的位置和大小,目标文件中的每个节都有一个固定大小的节头部表条目。
与hello.o相比,hello的节头部表一共描述了25个不同节的位置、大小等信息,比hello.o多出12个节。各节的起始地址由偏移量给出,同时也给出了大小等信息。
5.3.3 程序头部表
程序头部表描述了可执行文件的连续的片映射到连续的内存段的映射关系。包括目标文件的偏移、段的读写/执行权限、内存的开始地址、对齐要求、段的大小、内存中的段大小等。
5.3.4 符号表
hello的符号表一共描述了48符号,比hello.o多出32个符号。多出的符号都是链接后产生的库中的函数以及一些必要的启动函数。
hello中还多出了一个动态符号表,表中的符号都是共享库中的函数,需要动态链接。
5.3.5 重定位节
5.4 hello的虚拟地址空间
使用edb加载hello,可以看到进程的虚拟地址空间各段信息。可以看出,段的虚拟空间从0x400000开始,到0x400ff0结束。
由5.3中的节头部表可以获得各个节的偏移量信息,从而得知各节在虚拟地址空间中的地址。
例如,对于.interp节,节头部表中给出了它的偏移量为0x1c8,大小为0x1c字节。
因此它的虚拟地址空间就从0x4001c8开始,在edb中查看该虚拟内存地址,可以看出,.interp节确实在这个位置。
类似地,对于.rodata节,节头部表中给出了它的偏移量为0x600,大小为0x2f字节。
因此它的虚拟地址空间就从0x400600开始,在edb中查看该虚拟内存地址,可以看出,.rodata节确实在这个位置,程序中的两个字符串常量就存储在这里。
对于.data节,节头部表中给出了它的偏移量为0x900,大小为0x8字节。
因此它的虚拟地址空间就从0x400900开始,在edb中查看该虚拟内存地址,可以看出,.data节确实在这个位置,程序中的全局变量sleepsecs就存储在这里,并且值为2。
对于.text节,节头部表中给出了它的偏移量为0x4d0,大小为0x122字节。
因此它的虚拟地址空间就从0x4004d0开始,在edb中查看该虚拟内存地址,可以看出,.text节确实在这个位置,第一条指令的二进制机器码的第一个字节为0x31。
5.5 链接的重定位过程分析
(1) hello中的汇编代码已经使用虚拟内存地址来标记了,从0x400000开始;而hello.o中的汇编代码是从0开始的,还没有涉及到虚拟内存地址。
查看hello.o中的重定位条目,重定位条目给出了需要被修改的引用的节偏移、重定位类型、偏移调整等信息。
对于第二种重定位类型,以第二个条目为例,第二个条目的信息说明需要重定位的位置在.text中偏移量为0x1b的地方。在hello.o中找到相应的位置:
而这条call指令的地址为0x400514,它的下一条指令的地址为0x400519.
5.6 hello的执行流程
从加载hello到_start,到call main,以及程序终止的所有过程中调用的子程序名以及程序地址(调用顺序为从上到下):
名称 |
地址 |
ld-2.23.so!_dl_start |
0x7f7c8a4619b0 |
ld-2.23.so! dl_init |
0x7f7c8a470780 |
hello!_start |
0x4004d0 |
hello!__libc_start_main |
0x400480 |
libc-2.23.so!__libc_start_main |
0x7f7c8a0b6750 |
libc-2.23.so! cxa_atexit |
0x7f7c8a0d0290 |
hello!__libc_csu_init |
0x400580 |
hello!_init |
0x400430 |
libc-2.23.so!_setjmp |
0x7f7c8a0cb260 |
libc-2.23.so!_sigsetjmp |
0x7f7c8a0cb1c0 |
hello!main |
0x4004fa |
hello!puts@plt |
0x400460 |
hello!exit@plt |
0x4004a0 |
hello!printf@plt |
0x400470 |
hello!sleep@plt |
0x4004b0 |
hello!getchar@plt |
0x400490 |
ld-2.23.so!_dl_runtime_resolve_avx |
0x7f7c8a477870 |
libc-2.23.so!exit |
0x7f4ea0c8d5b0 |
5.7 Hello的动态链接分析
延迟绑定是通过两个数据结构之间的交互来实现的,分别是GOT和PLT,GOT是数据段的一部分,而PLT是代码段的一部分。PLT与GOT的协作可以在运行时解析函数的地址,实现函数的动态链接。
5.8 本章小结
本章介绍了链接的概念与作用,简要分析了可执行文件的ELF格式,hello的虚拟地址空间和执行流程,同时详细地分析了静态链接的重定位过程以及动态链接的过程。至此,一个完美的生命——hello诞生了。
第6章 hello进程管理
6.1 进程的概念与作用
其中上下文是由程序正确运行所需的状态组成的,包括存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
shell是指为使用者提供操作界面的软件,是一个交互型应用级程序,它接收用户命令,然后调用相应的应用程序。shell是系统的用户界面,提供了用户与内核进行交互操作的接口。
shell的处理流程:从终端读入输入的命令行->解析输入的命令行,获得命令行指定的参数->检查命令是否是内置命令,如果是内置命令则立即执行,否则在搜索路径里寻找相应的程序,找到该程序就执行它。
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
execve函数用hello程序有效替代当前程序,需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(3)映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
当内核调度这个进程时,它就将从这个入口点开始执行。Linux根据需要换入代码和数据页面。
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.6.1可能出现的异常及处理方法
hello执行过程中,四类异常都可能会出现,四类异常分别为:
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
hello执行过程中发生陷阱:hello中调用了系统调用sleep,产生陷阱。陷阱的处理:将控制传递给适当的异常处理程序,处理程序解析参数,调用适当的内核程序。处理程序返回时,将控制返回给下一条指令。
hello执行过程中发生错误:hello执行过程中,DRAM或者SRAM可能发生位损坏,产生奇偶错误。发生错误时会将控制传递给终止处理程序,终止引起错误的应用程序。
6.6.2可能产生的信号及处理方法
(1)程序运行过程中不停乱按键盘,包括回车。如果乱按不包括回车,输入的字符串会缓存到缓冲区;如果输入的最后是回车,则getchar会读进回车,把回车前的字符串作为输入shell的命令,
(2)程序运行过程中键入Ctrl-Z。键入Ctrl-Z会发送SIGTSTP信号给前台进程组的每个进程,结果是停止前台作业,也就是停止hello进程。
使用jobs命令可以查看当前的作业,可以看出当前的作业是hello进程,且状态是已停止
使用ps命令可以查看当前所有进程以及它们的PID,进程包括bash,hello以及ps。
使用fg命令可以使停止的hello进程继续在前台运行。也可以再次键入Ctrl-Z停止hello的运行。
(3)程序运行过程中键入Ctrl-C。键入Ctrl-C会发送SIGINT信号给前台进程组的每个进程,结果是终止前台进程,即终止hello进程。
使用ps命令可以发现,hello进程已经终止并被回收,不再存在了。使用jobs指令也看不到当前的作业了。
6.7本章小结
本章介绍了进程的概念和作用,简述shell的工作过程,并分析了使用fork+execve加载运行hello,执行hello进程以及hello进程运行时的异常/信号处理过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。例如,在进行C语言指针编程中,可以使用&操作读取指针变量的值,这个值就是逻辑地址,是相对于当前进程数据段的地址。一个逻辑地址由两部份组成:段标识符和段内偏移量。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址生成了一个线性地址。如果启用了页式管理,那么线性地址可以再变换产生物理地址。若没有启用页式管理,那么线性地址直接就是物理地址。
虚拟地址:因为虚拟内存空间的概念与逻辑地址类似,因此虚拟地址和逻辑地址实际上是一样的,都与实际物理内存容量无关。
物理地址:存储器中的每一个字节单元都给以一个唯一的存储器地址,用来正确地存放或取得信息,这个存储器地址称为物理地址,又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和段内偏移量两部分组成。段标识符由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,是对段描述符表的索引,每个段描述符由8个字节组成,具体描述了一个段。后3位包含一些硬件细节,表示具体是代码段寄存器还是栈段寄存器还是数据段寄存器等。通过段标识符的前13位,可以直接在段描述符表中索引到具体的段描述符。每个段描述符中包含一个Base字段,它描述了一个段的开始位置的线性地址。将Base字段和逻辑地址中的段内偏移量连接起来就得到转换后的线性地址。
对于全局的段描述符,放在全局段描述符表中,局部的(每个进程自己的)段描述符,放在局部段描述符表中。全局段描述符表的地址和大小存放在gdtr控制寄存器中,而局部段描述符表存放在ldtr寄存器中。
给定逻辑地址,看段选择符的最后一位是0还是1,用于判断选择全局段描述符表还是局部段描述符表。再根据相应寄存器,得到其地址和大小。通过段标识符的前13位,可以在相应段描述符表中索引到具体的段描述符,得到Base字段,和段内偏移量连接起来最终得到转换后的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页表是一个页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。每个PTE由一个有效位和一个n位地址字段组成,有效位表明该虚拟页是否被缓存在DRAM中。如果设置了有效位,那么地址字段表示相应的物理页的起始位置;如果没有设置有效位,那么空地址表示虚拟页还未被分配,否则这个地址指向该虚拟页在磁盘的起始位置。
MMU利用页表实现从虚拟地址到物理地址的变换。CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含一个p位的虚拟页面偏移VPO和一个n-p位的虚拟页号VPN。MMU利用VPN选择适当的PTE,如果这个PTE设置了有效位,则页命中,将页表条目中的物理页号和虚拟地址中的VPO连接起来就得到相应的物理地址。否则会触发缺页异常,控制传递给内核中的缺页异常处理程序。缺页处理程序确定物理内存中的牺牲页,调入新的页面,并更新内存中相应PTE。处理程序返回到原来的进程,再次执行导致缺页的指令,MMU重新进行地址翻译,此时和页命中的情况一样。同时,也可以利用TLB缓存PTE加速地址的翻译。
图7-1 线性地址到物理地址的变换
7.4 TLB与四级页表支持下的VA到PA的变换
TLB的支持:在MMU中包括一个关于PTE的缓存,称为翻译后备缓冲器(TLB)。TLB是一个小的、虚拟寻址的缓存,每一行保存着一个由单个PTE组成的块。由于VA到PA的转换过程中,需要使用VPN确定相应的页表条目,因此TLB需要通过VPN来寻找PTE。和其他缓存一样,需要进行组索引和行匹配。如果TLB有2t个组,那么TLB的索引TLBI由VPN的t个最低位组成,TLB标记TLBT由VPN中剩余的位组成。
图7-2 TLB
当MMU进行地址翻译时,会先将VPN传给TLB,看TLB中是否已经缓存了需要的PTE,如果TLB命中,可以直接从TLB中获取PTE,将PTE中的物理页号和虚拟地址中的VPO连接起来就得到相应的物理地址。这时所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。如果TLB不命中,那和7.3中描述的过程类似,需要从cache或者内存中取出相应的PTE。
图7-3 TLB支持下的线性地址到物理地址的变换
四级页表的支持:多级页表可以用来压缩页表,对于k级页表层次结构,虚拟地址的VPN被分为k个,每个VPNi是一个到第i级页表的索引。当1≤j≤k-1时,第j级页表中的每个PTE指向某个第j+1级页表的基址。第k级页表中的每个PTE和未使用多级页表时一样,包含某个物理页面的PPN或者一个磁盘块的地址。对于Intel Core i7,使用了4级页表,每个VPNi有9位。当TLB未命中时,36位的VPN被分为VPN1、VPN2、VPN3、VPN4,每个VPNi被用作到一个页表的偏移量。CR3寄存器包含L1 页表的物理地址,VPN1提供到一个L1 PTE的偏移量,这个PTE包含某个L2页表的基址。VPN2提供到这个L2页表中某个PTE的偏移量,以此类推。最后得到的L4 PTE包含了需要的物理页号,和虚拟地址中的VPO连接起来就得到相应的物理地址。
图7-4 四级页表支持下的线性地址到物理地址的变换
7.5 三级Cache支持下的物理内存访问
当MMU完成了从虚拟地址到物理地址的转换后,就可以使用物理地址进行内存访问了。Intel Core i7使用了三级cache来加速物理内存访问,L1级cache作为L2级cache的缓存,L2级cache作为L3级cache的缓存,而L3级cache作为内存(DRAM)的缓存。
进行物理内存访问时,会首先将物理地址发送给L1级cache,看L1级cache中是否缓存了需要的数据。L1级cache共64组,每组8行,块大小64B。因此将物理地址分为三部分,块偏移6位,组索引6位,剩下的为标记位40位。首先利用组索引位找到相应的组;然后在组中进行行匹配,对于组中的8个行,分别查看有效位并将行的标记位与物理地址的标记位匹配,当标记位匹配且有效位是1时,缓存命中,根据块偏移位可以直接将cache中缓存的数据传送给CPU。如果缓存不命中,需要继续从存储层次结构中的下一层中取出被请求的块,将新块存储在相应组的某个行中,可能会替换某个缓存行。
L1级cache不命中时,会继续向L2级cache发送数据请求。和L1级cache的过程一样,需要进行组索引、行匹配和字选择,将数据传送给L1级cache。同样L2级cache不命中时,会继续向L3级cache发送数据请求。最后,L3级cache不命中时,只能从内存中请求数据了。
值得注意的是,三级cache不仅仅支持数据指令的访问,也支持页表条目的访问,在MMU进行虚拟地址到物理地址的翻译过程中,三级cache也会起作用。
图7-5 三级Cache下的物理内存访问
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。内核给新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。同时延迟私有对象中的副本直到最后可能的时刻,充分利用了稀有的物理内存。
7.7 hello进程execve时的内存映射
exceve()函数在当前进程的上下文中加载并运行我们需要的hello程序。execve函数加载并运行可执行文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
execve函数用hello程序有效替代当前程序,需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序(即hello)的代码、数据、bss和栈区域等创建新的区域结构。所有这些区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
当内核调度这个进程时,它就将从这个入口点开始执行。Linux根据需要换入代码和数据页面。
图7-6 execve时的内存映射
7.8 缺页故障与缺页中断处理
缺页故障的产生:CPU产生一个虚拟地址给MMU,MMU经过一系列步骤获得了相应的PTE,当PTE的有效位未设置时,说明虚拟地址对应的内容还没有缓存在内存中,这时MMU会触发缺页故障。
缺页故障的处理:缺页异常导致控制转移到内核的缺页处理程序。处理程序随后执行以下步骤:(1)判断虚拟地址是否合法。缺页处理程序搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end做比较。如果指令不合法,缺页处理程序会触发一个段错误,从而终止这个进程。(2)判断内存访问是否合法。比如缺页是否由一条试图对只读页面进行写操作的指令造成的。如果访问不合法,缺页处理程序会触发一个保护异常,从而终止这个进程。(3)这时,内核知道缺页是由合法的操作造成的。内核会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。处理程序返回时,CPU重新执行引起缺页的指令,这条指令将再次发送给MMU。这次,MMU能正常地进行地址翻译,不会再产生缺页中断了。
图7-7 缺页中断处理
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但不失通用性,假设堆是一个请求二进制零的区域,紧接在未初始化数据区域后开始,向上生长。对每个进程,内核维护一个全局变量brk指向堆顶。分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留,供应用程序使用;空闲块可用来分配。空闲块保持空闲,直到空闲块显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的(即显式分配器),要么是内存分配器自身隐式执行的(即隐式分配器)。显式分配器和隐式分配器是动态内存分配器的两种基本风格。两种风格都要求应用显式地分配块,不同之处在于由哪个实体来负责释放已分配的块。显式分配器要求应用显式地释放任何已分配的块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。
图7-8 动态内存分配的区域——堆
显式分配器必须在一些约束条件下工作:处理任意请求序列;立即响应请求;只使用堆;对齐要求;不修改已分配的块。在这些限制条件下,分配器试图实现吞吐率最大化和内存使用率最大化,但这两个性能目标通常是相互冲突的。
分配器的具体操作过程以及相应策略:
(1)放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。执行这种搜索的常见策略包括首次适配、下一次适配和最佳适配等。
(2)分割空闲块:一旦分配器找到了匹配的空闲块,需要决定分配这个空闲块中多少空间。可以选择用整个块,但会造成额外的内部碎片;也可以选择将空闲块分割为两部分,第一部分变成已分配块,剩下的变成新的空闲块。
(3)获取额外的堆内存:如果分配器不能为请求块找到空闲块,分配器通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插到空闲链表中,然后被请求的块放在这个新的空闲块中。
(4)合并空闲块:分配器释放一个已分配块时,要合并相邻的空闲块。分配器决定何时执行合并,可以选择立即合并或者推迟合并。合并时需要合并当前块和前面以及后面的空闲块。
组织空闲块的形式有很多,包括隐式空闲链表、显式空闲链表、分离的空闲链表等等。
带边界标签的隐式空闲链表分配器:一个块由一个字的头部、有效载荷、可能的一些额外的填充以及一个脚部。头部位于块的开始,编码了这个块的大小(包括头部、脚部和所有的填充)以及这个块是已分配的还是空闲的。由于对齐要求,头部的高位可以编码块的大小,而剩余的几位(取决于对齐要求)总是零,可以编码其他信息。使用最低位作为已分配位,指明这个块是已分配的还是空闲的。脚部位于每个块的结尾,是头部的一个副本,是为了方便释放块时的合并操作。头部后面就是调用分配器时请求的有效载荷,有效载荷后面是一片不使用的填充块,其大小可以是任意的。填充的原因取决于分配器的策略。如果块的格式是如上所述,就可以将堆组织成一个连续的已分配块和空闲块的序列,这种结构为隐式空闲链表。空闲块通过头部的大小字段隐含地连接,可以通过遍历堆中所有的块间接遍历整个空闲块的集合。同时,需要一个特殊标记的结束块(设置分配位而大小为零的头部),这种设置简化了空闲块合并。
图7-9 隐式链表的块结构
显式空间链表:已分配块的块结构和隐式链表的相同,由一个字的头部、有效载荷、可能的一些额外的填充以及一个脚部组成。而在每个空闲块中,增加了一个前驱指针和后继指针。通过这些指针,可以将空闲块组织成一个双向链表。空闲链表中块的排序策略包括后进先出顺序、按照地址顺序维护、按照块的大小顺序维护等。显式空闲链表降低了放置已分配块的时间,但空闲块必须足够大,以包含所需要的指针、头部和脚部,这导致了更大的最小块大小,潜在提高内部碎片程度。
图7-10 显式链表的块结构
而malloc采用的是分离的空闲链表。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小升序排列,当分配器需要一个大小为n的块时,就搜索相应大小类对应的空闲链表。如果不能找到合适的块,就搜索下一个链表,以此例推。
7.10本章小结
本章总结了hello运行过程中有关内存管理的内容。简述了TLB、多级页表支持下的地址翻译、cache支持下的内存访问、缺页的处理、fork+execve过程的内存映射以及动态存储分配的过程。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:
B0,B1 ,B2……Bm-1
所有的 IO 设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux内核引出一个简单、低级的应用接口,称为 Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO 接口,使得所有的输入和输出都能以一种统一且一致的方式来执行:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,即描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件中的常量可以代替显式的描述符值。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发EOF条件,应用程序能检测到这个条件。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
(1)进程通过调用open函数打开一个存在的文件或者创建一个新文件。
int open(char* filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;mode参数指定了新文件的访问权限位。
(2)进程通过调用close函数关闭一个打开的文件。
int closefd;
fd是需要关闭的文件描述符,成功返回0,错误返回-1。关闭一个已关闭的描述符会出错。
(3)应用程序通过分别调用read和write函数来执行输入和输出。
ssize_t read(int fd,void *buf,size_t n);
ssize_t wirte(int fd,const void *buf,size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则返回值表示的是实际传送的字节数量。write函数从内存位置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;
}
printf函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。printf中调用了两个函数,分别为vsprintf和write。
vsprintf函数根据格式串fmt,并结合args参数产生格式化之后的字符串结果保存在buf中,并返回结果字符串的长度。
write函数将buf中的i个字符写到终端,由于i保存的是结果字符串的长度,因此write将格式化后的字符串结果写到终端。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar的源代码:
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函数会从stdin输入流中读入一个字符。调用getchar时,会等待用户输入,输入回车后,输入的字符会存放在缓冲区中。第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar的返回值是读取字符的ASCII码,若出错则返回-1。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法和Unix IO接口及其函数,并分析了printf和getchar函数的实现。
(第8章1分)
结论
hello所经历的过程:
源程序:在文本编辑器或IDE中编写C语言代码,得到最初的hello.c源程序。
预处理:预处理器解析宏定义、文件包含、条件编译等,生成ASCII码的中间文件hello.i。
编译:编译器将C语言代码翻译成汇编指令,生成一个ASCII汇编语言文件hello.s。
汇编:汇编器将汇编指令翻译成机器语言,并生成重定位信息,生成可重定位目标文件hello.o。
链接:链接器进行符号解析、重定位、动态链接等创建一个可执行目标文件hello。此时,hello才真正地可以被执行。
fork创建进程:在shell中运行hello程序时,shell会调用fork函数创建子进程,供之后hello程序的运行。
execve加载程序:子进程中调用execve函数,加载hello程序,进入hello的程序入口点,hello终于要开始运行了。
运行阶段:内核负责调度进程,并对可能产生的异常及信号进行处理。MMU、TLB、多级页表、cache、DRAM内存、动态内存分配器相互协作,共同完成内存的管理。Unix I/O使得程序与文件进行交互。
终止:hello进程运行结束,shell负责回收终止的hello进程,内核删除为hello进程创建的所有数据结构。hello的一生到此结束,没有留下一丝痕迹。
对计算机系统的设计与实现的深切感悟:
hello从诞生到结束,经历了千辛万苦,在硬件、操作系统、软件的相互协作配合下,终于完美地完成了它的使命。这让我认识到,一个复杂的系统需要多方面的协作配合才能更好地实现功能。同时,计算机系统提供的一系列抽象使得实际应用与具体实现相互分离,可以很好地隐藏实现的复杂性,降低了程序员的负担,使得程序更加容易地编写、分析、运行。这让我认识到抽象是十分重要的,是计算机科学中最为重要的概念之一。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i:C预处理器产生的一个ASCII码的中间文件,用于分析预处理过程。
hello.s:C编译器产生的一个ASCII汇编语言文件,用于分析编译的过程。
hello.o:汇编器产生的可重定位目标程序,用于分析汇编的过程。
hello:链接器产生的可执行目标文件,用于分析链接的过程。
hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
hellold.txt:hello的反汇编文件,用于分析可执行目标文件hello。
helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
helloldelf.txt:hello的ELF格式,用于分析可执行目标文件hello。
参考文献
[1] RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.
[2] https://www.cnblogs.com/clover-toeic/p/3851102.html
[3] https://www.runoob.com/linux/linux-comm-pstree.html
[4] https://www.runoob.com/cprogramming/c-function-vsprintf.html
[5] https://www.cnblogs.com/pianist/p/3315801.html
2021哈工大计算机系统大作业——程序人生-Hello’s P2P相关推荐
- 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P
2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...
- 哈工大计算机系统大作业 程序人生-Hello‘s P2P
哈工大计算机系统大作业 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2 在Ubuntu下预处理的 ...
- 哈工大计算机系统大作业 程序人生-Hello’s P2P 2022
2022哈工大计算机系统大作业 目录 摘 要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubun ...
- 哈工大2021春计算机系统大作业 程序人生-Hello’s P2P
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 1190200613 班 级 1903004 学 生 ...
- 哈工大计算机系统大作业——程序人生-Hello’s P2P
计算机系统 大作业 题 目程序人生-Hello's P2P 专 业 计算机科学与技术 学 号120L022401 班 级 200300 ...
- 哈工大计算机系统大作业: 程序人生-Hello’s P2P/ hello 的一生
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 1190201801 班 级 1903012 学 生 耿健 指 导 教 师 史先俊 计算机科学与技术学院 202 ...
- 哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 120L020512 班 级 2003004 学 生 黄鹏程 指 导 ...
- 哈工大计算机系统大作业-程序人生-Hello’s P2P
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术 学 号 2021110802 班 级 21w0312 学 生 黄键树 ...
- 哈工大计算机系统大作业 程序人生-Hello‘s P2P 020
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术 学 号 2021112808 班 级 2103103 学 生 陶丽娜 指 导 教 师 刘宏伟 摘 要 本文详细介 ...
最新文章
- 对ListenSocket 的研究(四)
- html怎么移动文字的位置,css怎么移动文字
- 谈谈Java运行机制
- [ARM-assembly]-ARMV9-A64指令汇总-指令速查
- 关于点名的简单python编程_如何用python编写一个简易的随机点名软件
- 10万元奖金语音识别赛进行中!CTC 模型 Baseline 助你轻松上分
- 使用Eclipse创建Web工程后未生成web.xml文件
- 【渝粤题库】陕西师范大学152212 政府绩效管理 作业(专升本)
- html键盘制作,HTML手写键盘(一)
- Iterator主要有三个方法:hasNext()、next()、remove()详解
- ConfirmCancelUtilDialog【确认取消对话框封装类】
- Spark源码分析:多种部署方式之间的区别与联系
- window安装python报错_win10下Python安装pycrypto报错
- 《中国人工智能学会通讯》——6.16 基于统计的推理方法
- Eclipse 安装 Fatjar.jar失败的解决方法
- Mac IDA单步调试本地程序
- SAS入门教程1---SAS系统简介
- Matlab实现人脸识别
- 百度长期不收录网站怎么办?9个方法解决不收录
- SEO内链优化,网站内部链接优化方法