CSAPP大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
学   号 7203610126
班   级 2036011
学 生 黄梓涵    
指 导 教 师 刘宏伟

计算机科学与技术学院
2021年5月
摘 要
本文阐述了hello.c程序在linux系统下的生命周期,以计算机系统的视角,探讨了hello.c的预处理、编译、汇编、链接生成可执行文件的过程,并对可执行文件进程管理和存储管理的机制进行了深入研究,使得我们对可执行文件的生成和执行过程有了更加深入的理解和认识

关键词:预处理,编译,汇编,链接,进程管理,存储管理

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 7 -
2.4 本章小结 - 8 -
第3章 编译 - 9 -
3.1 编译的概念与作用 - 9 -
3.2 在UBUNTU下编译的命令 - 9 -
3.3 HELLO的编译结果解析 - 9 -
3.4 本章小结 - 15 -
第4章 汇编 - 16 -
4.1 汇编的概念与作用 - 16 -
4.2 在UBUNTU下汇编的命令 - 16 -
4.3 可重定位目标ELF格式 - 16 -
4.4 HELLO.O的结果解析 - 20 -
4.5 本章小结 - 22 -
第5章 链接 - 23 -
5.1 链接的概念与作用 - 23 -
5.2 在UBUNTU下链接的命令 - 23 -
5.3 可执行目标文件HELLO的格式 - 24 -
5.4 HELLO的虚拟地址空间 - 28 -
5.5 链接的重定位过程分析 - 29 -
5.6 HELLO的执行流程 - 32 -
5.7 HELLO的动态链接分析 - 33 -
5.8 本章小结 - 34 -
第6章 HELLO进程管理 - 36 -
6.1 进程的概念与作用 - 36 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 36 -
6.3 HELLO的FORK进程创建过程 - 37 -
6.4 HELLO的EXECVE过程 - 37 -
6.5 HELLO的进程执行 - 38 -
6.6 HELLO的异常与信号处理 - 39 -
6.7本章小结 - 45 -
第7章 HELLO的存储管理 - 46 -
7.1 HELLO的存储器地址空间 - 46 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 46 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 47 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 47 -
7.5 三级CACHE支持下的物理内存访问 - 49 -
7.6 HELLO进程FORK时的内存映射 - 50 -
7.7 HELLO进程EXECVE时的内存映射 - 51 -
7.8 缺页故障与缺页中断处理 - 52 -
7.9动态存储分配管理 - 53 -
7.10本章小结 - 55 -
结论 - 56 -
附件 - 57 -
参考文献 - 58 -

第1章 概述
1.1 Hello简介
P2P:From Program to Process,在编写源程序hello.c之后,首先运行C预处理器cpp把hello.o预处理成一个ASCII码的中间hello.i,然后运行编译器ccl把hello.i编译成一个ASCII汇编语言文件hello.s,接着运行汇编器as把hello.s翻译成一个可重定位目标文件hello.o,在通过链接器ld与库函数链接,形成一个可执行文件hello。然后在shell中键入./hello+学号+姓名+秒数,shell会自动fork一个新的子进程,然后再调用execve函数将hello程序加载到新创建的子进程中。由此便实现了由程序(Program)到进程。
020:From Zero-0 to Zero-0,由shelld调用加载器execve将fork产生的子进程hello程序加载到新创建的进程中,之后通过虚拟内存映射将程序从磁盘载入物理内存中执行,然后CPU为该进程分配时间片,执行该程序对应的逻辑控制流,当程序执行结束后,hello进程向其父进程发送一个SIGCHLD信号,最后父进程回收hello进程,释放hello的内存并删除进程上下文,至此程序完成运行过程中从零又到无的过程。
1.2 环境与工具
1.2.1硬件环境:
处理器: Intel® Core™ i5-7200U CPU @ 2.50GHz 2.71GHz
已安装的内存(RAM):16.0GB
系统类型64位操作系统,基于x64的处理器
磁盘大小:磁盘0:931.39GB
磁盘1:223.56GB
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.2.2软件环境
Windows 10 Education, 64-bit (Build 16299.2166) 10.0.16299
VMware® Workstation 16 Pro 版本16.1.1 build-17801498
Ubuntu 9.3.0-17ubuntu1~20.04 版本20.04
1.2.3开发工具
x86_64-linux-gnu gcc
1.3 中间结果
文件名称 作用
hello.c 源代码
hello.i 预处理后的文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行文件
hello 链接之后的可执行文件
helloo.txt hello.o的反汇编文件,查看反汇编代码
hello.txt hello的反汇编文件,查看反汇编代码
helloo.elf hello.o的elf文件,查看各节的信息
hello.elf hello的elf文件,查看各节信息

1.4 本章小结
本章从hello的一生对其进行了简单的概括,对其的P2P过程、020过程进行了阐述和概括,并且对实验的环境和生成的中间产物进行了简要概括。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理,这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。

预处理的作用:
预处理器根据以#开头的命令,修改原始的C程序,其中#开头的命令包括宏定义#define、文件包含#include、条件编译#if等,最后将修改之后的文本进行保存,生成. i文件,此外预处理器还会去除代码注释的内容。
比如hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就是得到了另一个C程序,通常是以.i作为文件的扩展名

2.2在Ubuntu下预处理的命令
Ununtu下预处理hello.c文件成hello.i文件的命令为:gcc hello.c -E -o hello.i
在这里插入图片描述
图 1预处理命令使用
2.3 Hello的预处理结果解析
查看hello.i文件可以发现文件长达3060行,添加了许多我们原先没有的代码,而我们原有的头文件和注释也被删除了,查看最后一段代码发现和原先C代码相同。
这是因为首先预处理器会删除代码中的注释,其次预处理器会对源代码中以字符“#”开头的语句进行处理。由于在程序包含下图的

这一段的头文件内容,所以预处理器会分别读取其中的内容,并且根据读入的顺序依次进行内容的展开。如果此时头文件中仍然有以字符“#”开头的内容,则预处理器继续对其进行处理,直到hello.i文件中没有宏定义、文件包含及条件解析等内容。
0.png)
在这里插入图片描述
图 2预处理文件查看1
在这里插入图片描述
图 3预处理文件查看2
2.4 本章小结
本章在介绍预处理的概念与作用之后,结合具体的hello.c程序在Ubuntu系统下的操作,对预处理的指令进行了运用,最后对预处理中的过程和处理结果hello.i的形成原因进行了进一步分析。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是将文件hello.i中的高级语言翻译成文件 hello.s中的汇编语言的过程,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。

编译的作用:
编译的作用是将预处理后的程序编译成为更接近计算机语言,更容易让计算机理解的语言,相当于一个翻译的过程,如果分析过程中发现有语法错误,还会给出提示信息。

3.2 在Ubuntu下编译的命令
Ununtu下编译hello.i文件成hello.s文件的命令为:gcc -S hello.i -o hello.s
在这里插入图片描述
图 4编译指令的使用
3.3 Hello的编译结果解析
3.3.1数据
(1)常数:
在这里插入图片描述
图 5常数数据信息
上图为printf中字符串常数的解析,其中汉字被解析成汉字机内码的方式存储,它们都存在只读数据段(.rodata)中。
(2)变量
在这里插入图片描述
图 6常数示例2
首先是全局变量main使用.gobal定义
在这里插入图片描述
图 7局部变量示例
接着,由上图代码分析结合传递参数时第一个为rdi第二个为rsi可以得知变量argc被存储在edi及-20(rbp)栈中,变量argv存储在rsi及-32(%rbp)栈中
在这里插入图片描述
图 8.L2节代码
再由.L2可判断变量i的存储位置为-4(%rbp) ,由于i是局部变量所以只是临时存在栈中
3.3.2赋值
hello.c程序中的赋值操作为i=0,hello.s中对应的汇编代码为.L2中的movl $0, -4(%rbp),其中l后缀表明i是双字类型的整数数据,即占32位4个字节的int型整数数据。
3.3.3类型转换
hello.c程序中涉及的类型转换为atoi(argv[3]),该语句实现了将字符串类型转换为整数类型,其对应如下图hello.s中的48行的调用函数call atoi@PLT这个函数的参数rdi为地址-32(%rbp)+24处的数据,即argv[3]。
在这里插入图片描述
图 9类型转换示例
3.3.4算术操作
hello.c程序中涉及的算术操作为i++,等价于使用addl指令将i即-4(%rbp),具体实现指令为hello.s中的addl $1, -4(%rbp)。
在这里插入图片描述
图 10算术操作示例
3.3.5关系操作
该main程序中存在两个关系操作
(1)argc != 4,对应hello.s中的下图24,25行,该语句用于判断argc即-20(%rbp)处的值是否与4相等,第24行在通过cmp比较完成后会设置条件码,而后在第25行会通过条件码来判断跳转位置。如下
在这里插入图片描述
图 11 关系操作示例
(2)i<8,对应hello.s中的下图53,54行,该语句用于判断i即-4(%rbp)处的值是否小于8,即是否小于等于7,第53行在通过cmp比较完成后会设置条件码,而后在第54行会通过条件码来判断跳转位置,如果i不是小于等于7则会退出循环继续执行。如下
在这里插入图片描述
图 12关系操作示例2
3.3.6数组操作
argv为hello.c中的数组,在printf中被调用argv[1]以及argv[2],对应hello.s中如下图的部分,首先将argv的首地址-32(%rbp)存入%rax中,再通过对%rax+16,%rax+8地址的访问来获得argv[1]和argv[2]的值
在这里插入图片描述
图 13数组操作示例
3.3.7控制转移
该main程序中存在两个控制转移操作
(1) If判断语句的控制转移
如下图的汇编代码对应if(argc!=4),此时-20(%rbp)内储存的为argc的值,如果该argc==4即if不成立会跳转至.L2处继续执行,如果argc不等于4则会按照顺序从26行继续执行。
在这里插入图片描述
图 14控制转移示例
(2) for循环语句的控制转移
如下图为for循环的汇编代码对应for(i=0;i<8;i++),从.L2进入,首先将i即-4(%rbp)的值赋值为0接着跳转至L3判断循环条件是否成立,若成立进入循环体.L4进行运行,若不成立则继续执行第55行退出循环运行。
在这里插入图片描述
图 15控制转移示例2
3.3.8函数操作
该程序中涉及到main,printf,exit,atoi,sleep,getchar六个函数
(1)main函数
main函数的参数为argc和argv,变量argc被存储在edi及-20(rbp)栈中,变量argv存储在rsi及-32(%rbp)栈中。
main中的局部变量如i也是通过存在栈中-4(%rbp)来调用的
最后在结束前使用movl $0, %eax,来将rax置0返回0。
(2)printf函数
hello.c程序中调用了两次printf函数,通过使用call的方式调用,但是两次调用传入的参数不同。
第一次是直接把.LC0处的字符串的地址存入%rdi传递:
在这里插入图片描述
16printf示例
第二次除了把.LC1处的字符串的地址存入%rdi中传递还有argv[1]和argv[2],argv[1]和argv[2]通过存在寄存器rsi和rdx中来传递参数 :
在这里插入图片描述
图 17printf示例2
(3) exit函数
先将1存入%edi作为参数表示非正常退出程序,接着通过call方式调用exit函数
在这里插入图片描述
图 18exit函数示例
(4) atoi函数
atoi函数实现将字符串类型的数据转变成int类型的数据,传入的参数为argv[3]即(-32(%rbp)+24),存放在%rdi中:
在这里插入图片描述
图 19aoti函数示例
(5) sleep函数
sleep函数实现程序休眠, atoi(argv[3])退出后返回值存储在%rax中,将其传入%edi中作为参数调用sleep函数:
在这里插入图片描述
图 20sleep函数示例
(6) gechar函数
getchar从缓冲区读入一个字符,无需传递参数直接通过call调用
在这里插入图片描述
图 21getchar函数示例
3.4 本章小结
本章在介绍编译的概念与作用之后,结合具体的hello.i文件在Ubuntu系统下的操作,对编译的指令进行了运用,接着重点对编译结果hello.s进行了包括常量、赋值、类型转换、算术操作、关系操作、数组操作、控制转移和函数操作在内的各个数据类型以及各类操作的分析,通过结合展示相应语句的汇编代码图片,加深了对编译结果的理解和运用。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编是指把汇编语言书写的程序翻译成与之等价的机器语言程序的过程。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。

汇编的作用:
汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制代码因为机器的不同而不同,是程序硬件能够直接执行在机器语言。

4.2 在Ubuntu下汇编的命令
Ununtu下汇编hello.s文件成hello.o文件的命令为gcc hello.s -c -o hello.o
在这里插入图片描述
图 22汇编命令的使用
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
首先使用命令readelf –a hello列出hello.o的全部信息
4.3.1 ELF Header(ELF头)
在这里插入图片描述
图 23EFL头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。

4.3.2Section Headers(节头部表)
在这里插入图片描述
图 24节头部表1
在这里插入图片描述
图 25节头部表2
节头表包含了文件中出现的各个节的含义,包括节的类型、偏移量和大小等信息。在文件头中可以得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
4.3.3.real.text(代码重定位节)
在这里插入图片描述
图 26代码重定位节
.rela.text节包含很多重定位条目,每个重定位条目中,Offset是需要被修改的引用节的偏移,Info包括symbol和type两个部分,symbol为前四个字节,type为后四个字节,symbol标识了被修改引用应该指向的符号,type表示重定位的类型,Addend是一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整,Name是重定向到的目标的名称。
hello.c需要重定位的信息有:.rodata中的模式串、puts、exit、printf、slepsecs、sleep、getchar等符号
对比利用objdump -d -r hello.o反汇编出来的代码,如下图,可以看出
1)对于第一项重定位条目.rodata只读数据,offset 0x1c对应代码段的0x1c处的重定位,类型为R_X86_64_PC32为PC相对地址的引用,故而跳转地址对应refaddr = ADDR(s) + r.offset,*refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr),计算得要加载的字符串的实际地址
2) 对于第二项重定位条目puts函数,offset 0x21对应代码段的0x21处的重定位,类型R_X86_64_PLT32为使用程序链接表进行重定位,利用R_X86_64_PLT32的计算规则可以得出refptr = 0x25
其他几项重定位条目同理计算地址
在这里插入图片描述
图 27代码重定位位分析示例
4.3.4.rela.eh_frame
在这里插入图片描述
图 28.eh_frame节
.eh_frame是程序执行错误时的指令,.rela.eh_frame节是.eh_frame节重定位信息
4.3.4Symbol table(符号表)
在这里插入图片描述
图 29符号表
由上图可以看出,符号表中存储了程序中定义和使用的各种符号,包括多个条目数组,其中每一个符号对应的条目有其对应的值(Value),大小(Size),类型(Bind),符号的可见范围(Vis),在哪一个节(Ndx,Ndx=1表示.text节,Ndx=3表示.data节),名字(name)等等内容。
4.4 Hello.o的结果解析
在这里插入图片描述
图 30hello.o反汇编代码1
在这里插入图片描述
图 31hello.o反汇编代码2
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
通过与hello.s代码对比,可以看出反汇编代码和hello.s中的代码仅有少量差别,同时反汇编代码中不仅有汇编代码,还有其对应的机器语言代码。机器语言代码是完全面向计算机的二进制数据表示的语言。机器语言代码中包含操作码,数据,寄存器编号等内容,其中机器语言的每一个操作码,寄存器编号等都与汇编语言一一对应。
操作数的对应:机器语言使用的是小端序的二进制表示,hello.o反汇编出的代码使用的是十六进制显示,而我们原始的汇编文件hello.s中的操作数使用的是十进制的表示。通过进制的转换的映射关系就可以实现机器语言与汇编语言的操作数一一对应。
分支转移:在hello.s汇编代码的跳转语句中,使用的是.L2和.L3等来表示跳转的位置,而在机器语言反汇编的反汇编语句中,使用的是相对地址的表示,这是因为hello.s中的.L2等只是便于阅读注解的方式,而实际在机器中是通过计算跳转地址来实现跳转的。
函数调用:在hello.s汇编代码的函数调用中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址,也就是call的目标地址是下一条指令的地址,这是因为 hello.c 中调用的函数都是动态共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址。因此在.rela.text节中为其添加了重定位条目,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0,最后在链接时才填入正确的地址。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结
本章在介绍汇编的概念与作用之后,结合具体的hello.s文件在Ubuntu系统下的操作,对编译的指令进行了运用,接着重点对汇编结果可重定位ELF文件hello.o中的包括Elf头、节头部表和重定位节等内容进行了分析,最后通过对比hello.s与hello.o的反汇编结果,分析了两者的映射在操作数、分支转移和函数调用上的差别以及形成原因,期间通过结合展示相应语句的机器代码和汇编代码对比的图片,加深了对汇编结果的理解和运用
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:
链接使分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以分解为更小的、更好管理的模块,可以独立地修改和编译单一模块。
5.2 在Ubuntu下链接的命令
Ununtu下链接hello.o文件成hello文件的命令为:
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
在这里插入图片描述
图 32链接命令的使用
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
首先使用命令readelf –a hello列出hello.o的全部信息
5.1.1.查看ELF Header,由下图可以看出文件为EXEC可执行目标文件类型。
此时hello的入口地址为0x4010f0不再是hello.o中的0x0说明此时重定位工作已经完成。
此外还可以看到program headers的数量有12个,这个是hello.o中没有的,他的作用是将连续的文件映射到内存段中。
同时hello.o中的section headers的数量为27,也比hello.o的文件来的多,这是在链接的过程中从共享库中获得的。
在这里插入图片描述
图 33ELF头
5.2.2Section Header
再看节头部表可以看出,hello中节头表的条目数为27条多于hello.o中节头表的条目数。
之所以会多出这些节是因为链接器在链接的时候,为了能够成功实现的动态链接,会将各个文件的相同段合并成一段,并且根据该段的大小以及偏移量重新设置各个符号的地址
查看Address一列对比4.3中hello.o的节头部表可以发现,每一节都有了实际地址,而不同于hello.o中地址值全部为0,这说明重定位工作已完成,程序有了内存中的实际地址。
在这里插入图片描述
图 34节头部表1
在这里插入图片描述
图 35节头部表2
在这里插入图片描述
图 36节头部表3
5.3.3Program Headers
这是是段头部表,是hello.o中所没有的,它的作用是将连续的文件节映射到运行时的内存段。其中offset是在目标文件中的偏移,VirtAddr是内存地址,align是对齐要求,filesiz是目标文件中的段大小,MemSiz是内存中的段大小,flags是运行时的访问权限
在这里插入图片描述
图 37段头部表
5.3.4符号表.symtab
符号表中存储了程序中定义和使用的各种符号,包括多个条目数组,其中每一个符号对应的条目有其对应的值(Value),大小(Size),类型(Bind),符号的可见范围(Vis),在哪一个节(Ndx,Ndx=1表示.text节,Ndx=3表示.data节),名字(name)等等内容。
相对于hello.o的符号表,hello的符号表有了更多的符号,这由于在完成链接时需要引入共享库中符号。
在这里插入图片描述
图 38符号表1
在这里插入图片描述
图 39符号表2
5.3.5 因为文件是完全可链接的(已经被重定位),所以它不再需要.rel节。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.4.1通过edb查看验证可以看出,程序的ELF头开始确实为0x400000处,
在这里插入图片描述
图 40edb查看elf头

对比Entry point address以及.text节的偏移可以看出,程序的入口就是.text节的开始,
在这里插入图片描述
在这里插入图片描述
图 41elf文件相关信息
在edb中查看如下
在这里插入图片描述
图 42edb查看.text节

5.4.2通过edb查看对比节头部表中的.interp节可以发现,offset 为2e0对应data dump中的地址0x400000+0x2e0=0x4002e0
在这里插入图片描述
图 43节头部表相关信息
在这里插入图片描述
图 44.edb查看.interp节

5.4.3对比节头部表中的.rodata只读数据节,偏移量为0x2000,查看data dump中0x402000得到验证为printf中的输出字符串的机器表示
在这里插入图片描述
图 45elf文件相关信息
在这里插入图片描述
图 46edb查看.rodata

5.5 链接的重定位过程分析
首先运行objdump -d -r hello,对比hello与hello.o的反汇编结果可以发现
1.首先,可以看出两者的起始位置不同,hello的起始地址是是一个确切的值,而hello.o的起始地址是0x0,这是因为hello.o还未实现重定位,每个符号还没有一个内存对应的地址,而hello已经实现了重定位,每次符号都有在运行时对应的内存中的地址。
在这里插入图片描述
图 47反汇编文件对比
2.其次,hello的反汇编代码相比hello.o的反汇编代码多出了许多内容,比如.init初始化的内容,又比如printf,getchar等先前在main中使用了但是没有被定义如何工作的函数,这是因为在链接的过程中,我们从共享库.so文件中引入了我main函数中缺失的printf,getchar等符号的引用,并通过符号解析实现了数据节和代码节的合并。
在这里插入图片描述
图 48hello反汇编文件多出的部分函数
3.还可以发现,对于跳转的位置,在hello.o的反汇编中只是一个相对偏移的值,而在hello中由于已经进行了链接,除了给出一个相对偏移还有一个具体的跳转地址,这是因为链接会給不确定的地址的符号计算出一个确定的地址,而符号的引用处也会被修改为确定的值
在这里插入图片描述
图 49 跳转地址的对比
4.对于加载地址的计算,分析重定位过程
以图中相对main偏移为19处的地址计算为例可以看出箭头所指示的位置,重定位方式为PC相对寻址
利用refaddr = ADDR(s) + r.offset
*refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr)
查看hello.s中的重定位表,得到r.offset为0x1c,r.addend=-4,查看hello.o的重定位节.rodata偏移地址为0x2000,
refaddr = ADDR(main) + r.offset = 0x401141
*refptr = 0x402000+0x4-0x401141 =0x402000-0x40113d=0xec3
查看hello的反汇编为0xec3得到验证
在这里插入图片描述
图 50elf文件相关信息
在这里插入图片描述
图 51重定位地址计算
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
程序名称 程序地址
ld-2.31.so(不知道为什么实在找不到ld-23.so的dl_start和dl_init,使用edb的analyse没有显示这两个) 0x00007f792545fbb0
hello!_start 0x00000000004010f0
libc-2.31.so!_libc_start_main 0x00007f5edf8b2fc0
libc-2.31.so!_cxa_atexit 0x00007f5edf8d5e10
hello!_libc_csu_init 0x00000000004011c0
libc-2.31.so!_setjmp 0x00007f5edf8d1cb0
hello!main 0x0000000000401125
hello!puts@plt0x401030 0x0000000000401030
hello!exit@plt 0x0000000000401070
libc-2.31.so!exit 0x00007f5edf8d5a70
在这里插入图片描述
图 52执行流程查看截图
上图为一edb运行到libc-2.31.so!_setjmp时的查询截图
可以观察到libc-2.31.so是在一直变化的,不知道是不是因为ASLR保护(还没学过这个不是很清楚)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
当GOT和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应与一个被调用的函数,其地址需要在运行时被解析。
查看hello的ELF可以看到GOT起始表位置为0x404000
在这里插入图片描述
图 53elf文件相关信息
进入EDB中查看0x404000中的数据如下图
在这里插入图片描述
图 54调用前got表
可以看出GOT表位置在调用dl_init之前0x404008后的16个字节均为0.
调用_start之后再次查看GOT表中的内容可以看到如下图
在这里插入图片描述
图 55调用后got表
从中可以看出在调用_start之后0x404008后的16个字节分别为0x7f7925476190和0x7f792545fbb0,其中GOT0和GOT1包含了动态链接器在解析函数地址时会使用的信息。GOT2是动态链接器在ld-linux,so模式中的入口点。

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章在介绍了链接的概念与作用之后,结合具体的hello.o文件在Ubuntu系统下的操作,对链接的指令进行了运用,接着重点对汇编结果可执行定位ELF文件hello中的包括Elf头、节头部表等内容与hello.o的ELF文件进行了对比分析,接着通过使用edb加载hello,查看本进程的虚拟地址空间各段信息,验证查看的ELF文件中的信息,又对链接的重定位过程(包括地址的计算)和执行流程(使用edb的analyzer功能)进行了详细的分析,最后分析动态链接并展示了调用dl_init前后GOT和PLT表的变化。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是一个正在运行程序的实例。系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
进程的作用:
进程向用户提供了一种假象,就好像我们的程序是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。处理器好像是无间断地一条接一条执行我们程序中的指令。最后我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
shell功能包括:
1)通配符
2)命令补全、别名机制、命令历史
3)重定向
4)设置管道
5)命令替换
6)Shell编程语言
7)文件目录操作–
8)用户间通信
9)备份压缩
Shell的处理流程:
处理流程:
1)读取一个来自用户的命令行。
2)调用parseline函数解析以空格符分隔的命令行参数,并构造最终会传递个execve的argv向量。
3)判断在后台执行还是在前台执行程序
4)检查函数的第一个命令行参数是否是一个内置的shell命令,如果是就立即解释并返回1
5)创建一个子进程并在子进程中执行所请求的程序。
6)如果用户要求在后台运行该程序,那么shell返回到循环的顶部,等待下一条命令行,否则使用waitpid函数等待作业终止。
6.3 Hello的fork进程创建过程
当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
fork函数只被调用一次,却会返回两次。一次是在调用进程中,一次是在新创建的子进程中。在父进程中,fork返回子进程的pid,在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
创建过程:
1.给新进程分配一个标识符
2.在内核中分配一个PCB(进程管理块),将其挂在PCB表上
3.复制它的父进程的环境(PCB中大部分的内容)
4.为其分配资源(程序、数据、栈等)
5.复制父进程地址空间里的内容(代码共享,数据写时拷贝)
6.将进程设置成就绪状态,并将其放入就绪队列,等待CPU调度
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
格式为int execve(const char *filename, const char *argv[], const char *envp[]);
其中filename是可执行目标文件,且带参数列表argv和环境遍历列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。

在execve加载了filename后,它调用驻留在内存中的启动加载器的操作系统代码以此执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的人代码、数据、堆和栈,栈和堆被初始化为0,通过虚拟地址映射到可执行目标文件的片,新的代码和数据段被初始化为可执行文件中的内容,然后跳转至_start函数,_start函数调用系统启动函数_libc_start_main进行初始化,最后调用主函数main。
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。
Hello程序在被shell调用execve之后,首先进程为程序分配好虚拟地址空间并运行。Hello起初在用户模式运行,然后系统调用sleep函数,显式地请求让调用进程休眠,此时进入内核模式会处理休眠请求主动释放当前进程以加载新的进程进行执行,同时将hello进程移动至等待队列并开始计时,当定时器计时结束,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,此时hello进程就可以继续进行自己的控制逻辑流,然后重复循环,直到main函数中对应的for循环结束。用户态和内核态的转换如下图所示:
在这里插入图片描述
图 56进程切换示例
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
6.6.1异常可以分为四类:中断、陷阱、故障和终止。
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
例如linux下可能产生如下信号
在这里插入图片描述
图 57信号种类与信息
一旦触发了异常,剩下的工作会由异常处理程序在软件中完成。在处理程序处理完事件之后,它通过执行一条特殊的“从中断返回”指令,可选地返回到被中断的程序,该指令将适当的状态弹回到处理器的控制和数据寄存器中,如果异常中断的是一个用户程序,就将状态恢复为用户模式,然后将控制返回给被中断的程序。
6.6.2不停乱按,包括回车
如果乱按过程中没有按回车,则只会在屏幕上显示输入的内容。如果输入回车,则getchar读回车,并把回车前的字符串当作shell输入的命令
在这里插入图片描述
图 58不停乱按包括回车情况
6.6.3 Ctrl-Z
输入ctrl-z之后,会发送一个SIGTSTP信号给前台进程组的每个进程,停止前台作业,此时hello进程运行在后台
在这里插入图片描述
图 59输入Ctrl-z之后暂停进程
(1)ps
使用ps命令可以看到如下图:
在这里插入图片描述
图 60ps命令结果
此时hello进程在后台的PID时39236
(2)jobs
使用jobs命令可以看到如下图:此时job号为1
在这里插入图片描述
图 61job命令结果
(3)pstree
输入pstree可查看进程树
在这里插入图片描述
图 62pstree进程树结果1
在这里插入图片描述
图 63pstree进程树结果2
在这里插入图片描述
图 64pstree进程树结果3
在这里插入图片描述
图 65pstree进程树结果4
在这里插入图片描述
图 66pstree进程树结果5
(4)fg
调用fg 1将其调到前台,此时shell继续执行hello进程
在这里插入图片描述
图 67fg命令结果
(5)kill
先用ps查看进程号,再kill -9 +进程号 杀死进程,再用ps查看你发现进程已经成功结束
在这里插入图片描述
图 68kill命令结果
6.6.4 Ctrl-C
输入ctrl-c之后程序退出,这因为输入ctrl-c之后程序向进程组发生一个SIGINT的信号,该信号的默认信号处理程序是终止前台作业。
在这里插入图片描述
图 69Ctrl-c命令结果
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章在介绍了进程的概念与作用之后,,又简述了shell-bash的作用与处理流程,接着总结了hello的fork进程的创建过程以及execve的过程。在详细分析hello进程的执行过程之后,接着详细分析hello的异常及信号种类,以及发生异常时候的处理方法,最后分析并查看了程序运行时在键盘输入各种按键字符可能发生的情况。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址:是指由程序产生的与段相关的偏移地址部分,程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)
7.1.2线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
7.1.3虚拟地址:虚拟地址是程序保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。就是hello里面的虚拟内存地址
7.1.4物理地址:物理地址是指出现CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
首先,在给定一个完整的逻辑地址[段选择符:段内偏移地址]之后,看段选择符的T1=0还是1,知道当前要转换是全局描述符表GDT中的段,还是局部描述符表LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,根据段描述符就可以得到基地址Base,把基地址Base加上段内偏移地址offset计算出的就是要转换的线性地址了。
在这里插入图片描述
图 70段选择符示例
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址就是线性地址,计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
在接收到CPU传递来的虚拟地址的时候,首先将虚拟地址看出虚拟页号VPN和虚拟页面偏移VPO的串联组合,MMU利用虚拟页号VPN向TLB表/高速缓存/主存请求获取页表条目PTE(其中通过页表的基址寄存器来定位页表条目,在有效位为1的时候取出PTE否则引发缺页异常更新并载入新页,让CPU重新发送一条访问虚拟地址指令,下一轮的时候对应PTE的有效位就是1了),从页表条目中取出信息物理页号PPN,因为物理页面和虚拟页面都是P字节的,所以物理页面偏移PPO和虚拟页面偏移VPO是相同的,通过将物理页号与虚拟页偏移量VPO结合,得到由物理地址PPN和物理页偏移量(PPO)组合的物理地址。
在这里插入图片描述
图 71命中时的处理
在这里插入图片描述
图 72不命中时的处理
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,内存管理单元MMU就必须查阅一个页表条目PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度。TLB 通常有高度的相联度,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。
下图展示了TLB命中和不命中时的操作图
在这里插入图片描述
图 73命中时的处理
在这里插入图片描述
图 74不命中时的处理
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
虚拟地址被划分为4个虚拟页号VPN和1个虚拟页面偏移VPO。每个VPN i 都是一个到第i级页表的索引,其中 1≤i≤4。第j级页表中的每个PTE,1≤j≤3,都指向第j+1级的某个页表的基址。第 4 级页表中的每个PTE包含某个物理页面的物理页号(PPN),或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN 之前,MMU必须访问4个PTE。和只有一级到页表结构一样,PPO和VPO是相同的.下图展示了使用k级页表的地址翻译。
在这里插入图片描述
图 75k级页表地址翻译示例

7.5 三级Cache支持下的物理内存访问
虚拟地址通过地址翻译得到物理地址后,MMU发送物理地址给L1缓存,缓存从物理地址中抽取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则表示缓存命中,则读出在偏移量CO处的数据字节,并将它返回给MMU,随后MMU将它传递给CPU。若缓存未命中的话,则从L2缓存中进行匹配,若命中则将其存储在L1缓存且返回给MMU,若不命中,则从L3缓存中进行匹配,若命中则将其存储与L2缓存和L1缓存,而后返回给MMU,若不命中,则从主存中寻找。
下图以Core i7为例是一个四级页表和三级Cache支持下的物理内存访问
在这里插入图片描述
图 76三级cache访问示例
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程用中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个后来进行写操作的时候,写时的复制机就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
在这里插入图片描述
图 77一个私有的写时复制对象
7.7 hello进程execve时的内存映射
Execve函数在当前进程中加载并运行包含在可执行文件hello.out中的程序时,用hello.out程序有效地替代了当前程序。加载并运行hello.out需要一下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
在这里插入图片描述
图 78加载器映射用户地址空间区域示图
7.8 缺页故障与缺页中断处理
DRAM 缓存不命中称为缺页,以下图为例假设CPU引用了(虚拟页)VP 3中的一个字VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE 3,从有效位推断VP 3未被缓存,并且触发一个缺页异常。缺页异常会调用内核中的缺页异常处理程序,缺页处理程序就执行下面步骤:
1)查看虚拟地址是否合法,若不合法,那么缺页处理程序就触发一个段错误,从而终止这个进程;
2)查看试图进行的内存访问是否合法,如果试图访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程;
3)选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送虚拟地址到MMU,MMU就能正常翻译它了,而不会再产生缺页中断了。
在此例中就是牺牲页就是存放在(物理页)PP 3中的VP 4。如果VP 4已经被修改了,那么内核就会将它复制回磁盘。无论哪种结果,内核都会修改VP 4的页表条目,反映出VP 4不再缓存在主存中这一事实。接下来,内核从磁盘复制 VP 3 到内存中的 PP 3,更新 PTE 3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。
在这里插入图片描述
图 79缺页处理示例
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。假定堆是一个请求二进制零的区域,该区域在未初始化的数据区域之后开始,并向上增长(向更高的地址)。对于每个进程,内核都维护一个变量brk,该变量指向堆的顶部。
分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块已明确保留供程序使用。空闲块可用于分配。空闲块将保持空闲,直到由应用程序明确分配为止。分配的块将保持分配状态,直到被释放。分配的块将保持分配状态,直到它被释放。这种释放要么是由应用程序明确执行,要么是由内存分配器本身隐式地执行。
分配器有两种基本风格。两种风格都是要求显示的释放分配块。它们地不同之处在于由哪个实体来负责已分配地块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
隐式空闲链表:
一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。
在这里插入图片描述
图 80一个简单的堆块格式
隐式空闲链表法细节的图例及执行示例如下:
在这里插入图片描述
图 81隐式空闲链表示例
放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
当分配当分配器找到一个匹配的空闲块时,通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
器找不到合适的空闲块一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。为了提高合并效率,Knuth提出了一种采用边界标记的技术允许在常数时间内进行对前面块地合并。其结构如下图。
在这里插入图片描述
图 82使用边界标记的堆块格式
7.10本章小结
本章在介绍了逻辑地址、线性地址、物理地址、虚拟地址四个概念之后,,紧接着讲解了从逻辑地址到线性地址到物理地址地变换,又介绍了TLB与页表地页面替换流程以及三级cache的物理访问流程。接着又对hello进程中的fork和execve的内存映射进行了详细分析,最后对缺页故障与中断处理以及动态存储分配管理进行了举例与阐述。

(第7章 2分)

结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello所经历的历程:
1.首先由人为编写hello.c程序,此时hello.c是一个文本文件,还无法被计算机所执行。
2.使用gcc –E对hello.c进行预处理,得到hello.i
3.使用gcc –S对hello.i进行编译,得到hello.s
4.使用gcc –c对hello.s进行汇编,得到hello.o
5.使用ld命令加入动态链接库中库函数与hello.o链接,得到hello可执行文件
6.在shell命令行上输入./hello 7203610126 黄梓涵 1来运行hello程序。
7.shell首先判断输入命令是否为内置命令。经过检查后发现其不是内置命令,则shell将其当作程序执行。
8.运行hello时,父进程通过调用fork创建子进程,通过调用execve在子进程上下文加载新的程序,在进程执行过程中可能会出现一些异常,此时内核会调用处理程序进行相关处理
9.程序加载到内存,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache协同工作,完成对地址的翻译和数据的读写
10.Unix IO能够实现对输入的处理和进行相对应的输出
11.当hello进程执行完成后,父进程会对子进程进行回收。内核删除为这个进程创建的所有数据结构。

Hello程序运行生命周期让我感到极为震撼,一个看似逻辑简单的hello.c程序,很快便可以在计算机上编写出来并执行,然而这背后的实现却是如此繁琐的步骤,一环扣一环,各个硬件底层各自分工却又相互配合的井井有条,通过这次大作业的学习,我对计算机系统的组成和结构有了更加深入的理解和认识。
让我感触最深一点是实验的内容绝对不是课堂上通过听课,或者自己对着书本死读可以理解的,这需要我们一步一步的去实践与探索,一步步地在计算机上验证每一个步骤的执行流程才能深入体会计算机这一发明的伟大之处!

(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
helloo.txt:hello.o的反汇编文件,查看反汇编代码
hello.txt:hello的反汇编文件,查看反汇编代码
helloo.elf:hello.o的elf文件,查看各节的信息
hello.elf:hello的elf文件,查看各节信息
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E. Bryant, David R. O’Hallaron. Computer Systems A Programmer’s
Perspective [Third Edition]. 2016
[2] https://blog.csdn.net/reworoReol/article/details/118229976
[3] 课程PPT
(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

for-nothing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值