赞
踩
本文重点介绍了hello.c经过预处理、编译、汇编、链接变为可执行目标程序hello的具体过程,并结合进程管理、存储管理和IO管理的内容对hello的加载运行过程进行了详细的分析。本文的内容涵盖计算机系统的关键内容,有助于掌握理解计算机系统的整体框架。
关键词:计算机系统;预处理;编译;汇编;链接;异常;进程管理;虚拟内存;Unix IO
(摘要0分,缺失-1分,根据内容精彩程度酌情加分0-1分)
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
(1)Hello的P2P过程(From Program to Process)
源程序hello.c经过预处理变为hello.i文本文件,再经过编译变为hello.s文本文件(包含汇编语言程序),经过汇编变为hello.o二进制文件,也就是可重定位目标文件(机器语言),最后经过链接变为可执行目标文件。在shell中输入./hello,通过fork()来创建子进程并通过execve将可执行程序hello加载到子进程,实现了Program到Process的过程。
(2)Hello的020过程(From Zero-0 to Zero-0)
在shell中输入./hello,fork()创建新的子进程,execve在该进程的上下文加载并运行hello,通过内存映射为其分配相应的虚拟内存空间,加载其所需的物理内存,CPU在流水线上依次执行每一条指令。程序运行结束后,相关内存被释放,有关的进程上下文被删除,就像hello进程从未存在过,程序重新回归“0”。
硬件环境:Intel i5 12500H ;RAM:16GB;
软件环境:win11、Ubuntu22.04
开发工具:gcc、ld、vim、edb、readelf、Vscode
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名字 | 文件作用 |
---|---|
hello.i | hello.c经过预处理得到的文本文件 |
hello.s | hello.i经过编译得到的文本文件 |
hello.o | hello.s经过汇编得到的二进制文件 |
hello | hello.o经过链接得到的可执行目标文件 |
本章概述了hello程序的一生——阐述了P2P和020的具体过程,讲述了hello.c源程序是如何经过一系列操作变成运行的进程,以及hello从开始到结束的资源分配及回收过程。交代了实验环境及工具,对中间结果进行说明。
(1)预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。预处理器(cpp)将源程序hello.c翻译成一个ASCII码的中间文件,通常是以.i作为文件扩展名。
(2)预处理的作用:
a. 源文件包含:搜索指定的文件,在源文件中插入包含文件的内容。比如hello.c中的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。
b. 宏替换:把一个标识符指定为其他一些成为替换列表的预处理记号,当这个标识符出现在后面的文本中时,将用对应的预处理记号把它替换掉。
c. 条件编译:根据条件有选择性的保留或者放弃源文件中的内容。常见的条件包含#if、#ifdef、#ifndef指令开始,以#endif结束。用#undef 指令可对用#define定义的标识符取消定义。
d. 行控制:行控制指令以"#"和“line”引导,后面是行号和可选的字面串。它用于改变预定义宏"LINE"的值,如果后面的字面串存在,则改变“FILE”的值。
e. 抛错:抛错指令是以“#”和“error”引导,抛错指令用于在预处理期间发出一个诊断信息,停止转换。
f. 杂注:向C实现传递额外的信息(编译选项),对程序的某些方面进行控制。
g. 删除注释,将程序中的注释都用空格替换。
预处理命令:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
可以看出预处理操作将一些.h文件插入到程序文本中,同时删除了所有的注释。经过预处理,整个程序由最初的24行变为3092行,原来的hello.c程序出现在3078行之后(已删除注释)。在此之前出现的是stdio.h、unistd.h、stdlib.h头文件及其他文件的依次展开。
本章介绍了预处理的概念和作用,演示了对hello.c进行预处理的指令并解析了hello.c经过预处理之后生成的hello.i文件内容,对于预处理过程进行了较为全面的阐述和展示。
(第2章0.5分)
(1)编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。编译通过词法分析和语法分析,确认所有指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
(2)编译的作用:
a. 词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
b. 语法分析:检验是否符合语法标准,如果不符合则给出warning或error的提示信息。
c. 优化程序性能:实现原有机器语言功能下的前提下提高编译器程序的性能和运行的效率。
d. 生成汇编代码:从高级语言转换为汇编语言,让语言更容易被机器理解。
编译命令:gcc -m64 -no-pie -fno-PIC -S hello.s -o hello.i
(1)常量
a. 立即数常量
在源程序中有一些固定的数字——如红圈标注的5和10
它们在hello.s中以$+数字的形式出现(右图中i<10被优化为i≤9)
b. 字符串常量
这两个printf的字符串常量被编译器声明在.rodata中的.LC0和.LC1
(2)变量
a. 局部变量——程序运行时,局部变量一般保存在栈中
b. 全局变量
main函数
(3)表达式
mov指令实现赋值操作i=0
add指令实现算术操作i++
cmp指令实现关系操作i<=9(注:实际代码中为i<10,这里被优化为<=)
cmp指令实现关系操作argc!=5
argv数组存储在栈中,采用地址偏移+寄存器寻址的方式实现访问数组元素。
(1)if条件判断
如果argc==5,则跳转至for循环。否则输出相应字符串后退出。
(2)for循环
如果i<=9,则跳转至.L4(继续循环)。否则执行getchar,然后退出
(1)main函数
参数是int型的argc和char*型的argv[],返回值为0.
(2)printf函数
把参数argv[1]、argv[2]、argv[3]及字符串首地址.LC1传递给printf函数
(3)exit函数
把1传给exit函数
(4)sleep函数
argv[4]作为参数传递给atoi函数,atoi函数的返回值又作为参数传递给sleep。
(5)getchar函数
无需传递参数,直接调用。
(6)函数调用时的参数传递
调用函数时参数的传递大多是利用寄存器,如果参数超过6个则可用栈来传递。传递函数参数的寄存器是按照特殊顺序来使用的:
本章介绍了编译的概念与作用,并较为详细地对于编译得到的结果进行了解析。展示了汇编代码实现常量、变量、表达式、赋值、算术操作、关系操作、数组操作、控制转移、函数操作的具体过程。有益于更好地理解掌握C语言翻译为汇编语言的过程。
(第3章2分)
(1)汇编的概念:指汇编器将.s文件翻译成机器语言指令,并把指令打包成一种可重定位目标程序的格式,并将结果保存在目标文件.o文件(二进制文件)的整个过程。
(2)汇编的作用:将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。汇编器把这些指令打包成一个可重定位目标文件的格式。
汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
-分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
(1)ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
(2)节头
不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
(3)重定位节
重定位节是一个.text节中位置的列表,包含.text节中需要进行重定位的信息,在链接时用于重新修改代码段的指令中的地址信息。如图所示,需要重定位的信息有puts,exit,printf,atoi,sleep,getchar及.rodata中的模式串。
(4)符号表
每个可重定位目标模块都有一个符号表,它包含可重定位目标模块的定义和引用的符号的信息。.symtab存放着程序中定义引用的函数和全局变量的信息,包括函数puts、exit、printf、atoi、sleep、getchar的信息。
(1)hello.o的反汇编结果:
(2)hello.o的反汇编与hello.s的对照分析:
a. 数字的进制表示
hello.o反汇编中的操作数——16进制
hello.s中的操作数——10进制
b. 条件跳转格式
hello.o反汇编中的条件跳转:跳转时显示跳转目的地址,也就是相对的偏移地址。
hello.s中的条件跳转:跳转时显示段名称。
c. 函数调用
hello.o反汇编中的函数调用:call指令+下一条指令地址及重定位条目指引信息。(这是由于尚未进行链接,无法最终确定所调用函数的最终地址)
hello.s中的函数调用:call指令+所调用函数的函数名。
本章介绍了汇编的概念与作用,对汇编得到的二进制文件hello.o的ELF格式进行列举分析。对比分析了hello.o反汇编结果与hello.s的不同之处——以数字的进制表示、条件跳转格式、函数调用为重点进行解析。
(第4章1分)
(1)链接的概念:链接(linking)是指将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
(2)链接的作用:链接使得分离编译成为可能。无需将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解成更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
链接命令: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
-分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1)ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
(2)节头
节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
(3)程序头
程序头描述了系统准备程序执行所需的段及其他信息。主要作用是描述磁盘上可执行文件的内存布局以及如何映射到内存中。
(4)段节
(5)动态节
(6)重定位节
重定位节内容变为需要动态链接调用的函数,同时重定位类型发生改变。
(7)符号表
符号表记录了存放在程序中定义和引用的函数和全局变量的信息。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
(1)使用edb加载hello,查看本进程的虚拟地址空间各段信息(以.init段和.text段为例)
运行进程后,代码的起始虚拟地址为00000000:00401000.
.init段起始地址:0x00401000
.text段起始地址:0x004010f0
(2)对照5.3,进行分析(以.init段和.text段为例)
经比对,节头中各段均与edb中各段相对应,可在edb中找到对应的部分。这表明节头表中存储各段的起始地址与各段的虚拟地址之间存在对应关系。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
(1)hello与hello.o的不同
a. 链接后函数数量增加
hello的反汇编中,链接器将hello.c中用到的库函数puts,printf等的位置加入到了文件中。
b. 链接后增加了节
hello的反汇编中比hello.o多了节,如_init节等。
c. 链接后指令的地址参数发生变化
链接后的hello的反汇编中删除了hello.o的重定位条目,跳转的目的地址和函数地址均为虚拟内存地址;hello.o跳转的目的地址和函数地址都是相对偏移地址。
(2)链接的过程
a. 符号解析:目标文件定义和引用符号,每个符号对应于一个函数、一个局部变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
b. 重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
(3)重定位过程
a. 编译源代码时,代码中的绝对地址转换为相对地址。
b. 链接器将hello.o与其他目标文件或库文件进行链接,生成可执行文件或共享库。
c. 对hello.o进行重定位,将代码和数据段中的相对地址转换为绝对地址。重定位过程中,链接器会将hello.o中的每个符号引用与全局符号表中的相应符号定义进行匹配,并计算出符号引用的绝对地址。
d. 如果hello.o中存在未解决的符号引用,则链接器会报告链接错误。
e. 链接器对合并后的可执行文件或共享库进行地址重定位,将全局符号表中的符号地址更新为正确的地址。
f. 生成可执行文件或共享库,供操作系统加载和执行。
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
(1)执行过程
a.从加载hello到_start
先调用_init函数,之后是puts等库函数,再调用_start函数。
b.从_start到call main
先调用__libc_csu_init等函数完成初始化工作,然后调用main函数。
c.程序终止前
程序执行main函数调用main函数用到的一些函数,main函数执行完毕之后调用__libc_csu_fini、_fini完成资源释放和清理的工作。
(2)调用与跳转的各个子程序
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
程序调用由共享库定义的函数,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定将过程地址的绑定过程推迟到第一次调用该过程时。通过两个数据结构——GOT和PLT协作在运行时解析函数的地址。
GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
从ELF头可看出.got.plt的首地址为0x404000
通过edb查看动态链接前后的内容变化:
可以看出动态链接的过程中,GOT中用来存放函数目标地址,PLT使用GOT中存储的地址跳到目标函数。
本章介绍了链接的概念和作用、链接的命令,图文并茂地展示了可执行目标文件hello的格式,详细分析了链接的重定位过程,展示并分析了hello的执行过程和动态链接过程。
(第5章1分)
(1)进程的概念:进程的经典定义就是一个执行中程序的实例。
(2)进程的作用:系统中的每个程序都运行在某个进程的上下文中。每次用户向shell输入一个可执行文件名,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行自己的代码或其他应用程序。进程提供给应用程序的关键抽象:
a.一个独立的逻辑控制流:它提供一个假象,好像我们的程序独占地使用处理器。b.一个私有的地址空间:它提供一个假象,好像我们的程序独占地使用内存系统。
(1)壳Shell-bash的作用:bash是变种缺省的Linux shell,是信号处理的代表,负责各进程创建与程序加载运行及前后台控制、作业调用、信号发送与管理等;是一个交互型应用级程序,代表用户运行其他程序;执行一系列的读/求值等步骤;读步骤读取用户的命令行,求值步骤解析命令,代表用户运行。
(2)壳Shell-bash的处理流程:
a.读取用户输入的命令
b.对命令进行解析,包括识别命令名和参数
c.执行命令,包括调用系统程序或执行脚本
d.输出命令结果
调用fork( ) 函数创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork函数只被调用一次,但会返回两次;一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到可执行目标文件,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次从不返回。在execve加载了可执行目标文件之后,调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
(1)进程上下文信息
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制之上的。
内核为每个进程维持一个上下文。上下文就是内核重新启动一个被强占的进程所需的状态。它由一些对象的值组成,这些对象包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
(2)进程时间片
进程时间片(time slice)是操作系统用于管理多任务处理的关键概念之一。它指的是分配给一个进程在CPU上运行的时间间隔。在多任务操作系统中,CPU会在多个进程之间快速切换,以使它们看起来像是同时运行的。
(3)进程调度的过程
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占看的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用上下文切换的机制来将控制转移到新的进程。
上下文切换:a. 保存当前进程的上下文,b. 恢复某个先前被抢占的进程被保存的上下文,c. 将控制传递给这个新恢复的进程
(4)用户态与核心态转换
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
当执行hello进程时,系统进行调度,为其分配时间片,当hello下的进程从磁盘内存中读取数据的时候,就会出发陷阱异常,切换到内核模式。此时系统就会重新开始一个先前被抢占了的进程,进行上下文切换。当切换回的进程运行了一段时间后,系统就会将hello由内核态切换为用户态,进行抢占执行,继续执行下一条指令。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
(1)异常
a. 中断
原因:来自I/O设备的信号
异步/同步:异步
返回行为:总是返回到下一条指令
处理方式:
b. 陷阱
原因:有意的异常
异步/同步:同步
返回行为:总是返回到下一条指令
处理方式:
c. 故障
原因:潜在可恢复的错误
异步/同步:同步
返回行为:可能返回到当前指令
处理方式:
d. 终止
原因:不可恢复的错误
异步/同步:同步
返回行为:不会返回
处理方式:
(2)信号
hello执行过程中可能产生的信号:
a.SIGTSTP信号,中断信号,默认行为是停止直到下一个SIGCONT。
b.SIGINT信号,终止信号,默认行为是终止。
(3)程序运行过程中各命令及运行结果
a.正常运行:程序每隔2秒输出一次,共输出10次
b.运行程序后在键盘上乱按
屏幕输出缓存至缓冲区,不影响当前进程的运行。
c.在键盘按回车
并不影响进程的执行
d.Ctrl-Z
在进程执行时按Ctrl-Z,进程停止。
e.Ctrl-Z后运行ps命令
hello进程被挂起,通过ps可查看进程的相关信息
f.Ctrl-Z后运行jobs命令
hello进程被挂起,通过jobs可查看任务列表及状态。
g.Ctrl-Z后运行pstree命令
hello进程被挂起,通过pstree命令可查看进程间的关系,所有进程以树状图显示。
h.Ctrl-Z后运行fg命令
hello进程被挂起,fg命令使得被挂起的进程收到SIGCONT信号,进程继续运行。
i.Ctrl-Z后运行kill命令发送信号
hello进程被挂起,通过kill -9 进程号的命令,给hello进程发送SIGKILL信号,终止hello进程。
j.Ctrl-C
发送SIGINT信号,结束hello进程。
本章介绍了进程的概念和作用,壳Shell-bash的作用与处理流程、fork进程创建过程、execve过程等。重点演示了进程执行和异常与信号处理的具体过程。
(第6章1分)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
(1)逻辑地址:逻辑地址是由程序产生的与段相关的偏移地址部分。如hello.o中跳转的目的地址和函数地址都是与main函数的相对偏移地址。
(2)线性地址:段地址+偏移地址=线性地址,线性地址是逻辑地址到物理地址变换的中间结果。如hello中代码与数据的地址。
(3)虚拟地址:虚拟内存是对整个内存的抽象描述,是相对于物理内存来讲的。虚拟内存使得应用程序不需要知道物理地址的实际位置就可以调用。虚拟内存也就是线性地址空间。如hello中的地址。
(4)物理地址:物理地址是内存单元的真实地址。物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。在hello运行中,需要根据虚拟地址通过地址翻译得到物理地址,并通过物理地址访问其在内存中的位置。
在Intel架构的处理器中,逻辑地址到线性地址的变换通过段式管理机制来实现。
段式管理是内存管理的一部分,用来将逻辑地址转换为线性地址。在段式管理中,逻辑地址由两个部分组成:段选择子和段内偏移量。段选择子用于选择一个段描述符。段描述符存储在段描述符表中,可以是全局描述符表(GDT)或局部描述符表(LDT)。段描述符包含段的基址、段界限、段类型和特权级别等信息。
通过段选择子找到段描述符,然后将段描述符中的基址与段内偏移量相加,得到线性地址。
在Intel架构的处理器中,线性地址到物理地址的变换通过页式管理机制来实现。
页式管理是内存管理的一部分,用于将线性地址转换为物理地址。页式管理使用页表来进行地址转换,并且通过分段和分页的组合机制来有效地管理内存。线性地址即虚拟地址,虚拟内存与物理内存都被划分为页,并与页号相对应。虚拟地址由虚拟页号(VPN)+虚拟页偏移量(VPO)组成。页表是建立虚拟页号与物理页号(PPN)映射关系的表结构,页表项(PTE)包含有有效位、物理页号、磁盘地址等信息。通过虚拟页号和页表初始地址能找到相对应的页表项,页表初始地址存储在页表基址寄存器中。页表项存储的物理页号加上物理页偏移量即为物理地址,而物理页偏移量与虚拟页偏移量相同,可以从虚拟地址中直接得出。
若TLB命中,则MMU从TLB中取出相应的PTE,将这个虚拟地址翻译为物理地址;若TLB不命中,根据VPN1在一级页表选择对应的PTE,该PTE包含二级页表的基地址;根据VPN2在二级页表选择对应的PTE,该PTE包含三级页表的基地址;根据VPN3在三级页表选择对应的PTE,该PTE包含四级页表的基地址;在四级页表取出对应的PPN,与VPO串联起来,就得到相应的物理地址。
Cache是一种在CPU寄存器和物理内存之间的高速存储设备,在三级Cache中分为L1,L2,L3,其容量依次增大。
首先访问一级Cache,寻找该物理内存对应的内容是否已被缓存且有效,若已被缓存且有效,则缓存命中;否则缓存不命中,则需要访问二级Cache,重复上述步骤;若二级Cache中依然缓存不命中,则需要访问三级Cache,直到访问主存。将访问到的内容分别加载进上一层缓存,再进行后续操作。
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域都被映射为hello文件中的.text和.data区.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器指向代码区域的入口点。
(1)缺页故障: DRAM缓存未命中。说明该页并没有保存在主存中。
(2)缺页中断处理
当发生缺页故障时,处理器会触发缺页中断,操作系统会保存当前的CPU状态,包括寄存器和程序计数器,以便中断处理完成后能够恢复执行。然后操作系统会从页表中查找缺页的虚拟地址,确定是合法的页面访问还是非法访问(如访问未分配的内存区域),若为合法访问,则在硬盘上的交换区或文件系统中查找对应的页面。还需为缺页分配物理内存。
如果内存不足,可能需要选择一个页面进行交换(页替换算法,如LRU算法)。然后将所需页面从硬盘加载到分配的物理内存中。这可能涉及读取文件系统中的程序代码或数据,也可能从交换区加载页面。更新进程的页表,将新加载页面的物理地址记录到页表项中,并将页面状态设置为有效。恢复之前保存的CPU状态,包括寄存器和程序计数器,重新执行引发缺页故障的指令。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
(1)动态内存管理的基本方法
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接着未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
(2)动态内存管理的策略
分配器将堆视为一组大小不同的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
a. 显式分配器,要求应用显式地释放任何已分配的块。
b. 隐式分配器,另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程就叫做垃圾收集。
本章介绍了存储地址空间、Intel段式管理、页式管理、TLB、地址翻译、三级cache下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。
(第7章 2分)
设备的模型化:文件。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对对应文件的读和写来执行。
设备管理:unix io接口。将设备映射为文件的方式,使得Linux内核引出一个简单、低级的应用接口,称为I/O接口,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
(1)Unix IO接口:所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O
(2)Unix IO函数
a.open函数——打开文件
int open(char *filename, int flag, mode_t mode); 进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,也可以是一个或者更多位掩码的或,为写提供给一些额外的指示。mode参数指定了新文件的访问权限位。
b.close函数——关闭文件
int close(int fd);进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。
c.read函数——读文件函数
ssize_t read(int fd, void *buf, size_t n);应用程序通过调用read函数执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
d.write函数——写文件函数
ssize_t write(int fd, const void *buf, size_t n);应用程序通过调用write函数执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
e.lseek函数——修改字节偏移量的函数
off_t lseek(int fd, off_t offset, int whence); lseek函数可以修改文件的字节偏移量。
https://www.cnblogs.com/pianist/p/3315801.html
printf函数调用vsprintf函数进行格式化,接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出;调用write函数把buf中的i个元素的值写到终端。syscall函数不断地打印出字符,直到遇到:‘\0’。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
用户从键盘输入,键盘接口得到对应的键盘扫描码,同时发送中断请求,通过键盘中断处理子程序,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar的返回值是用户输入字符的ascii码,若到文件结尾则返回-1(EOF),且将用户输入显示到屏幕。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
本章介绍了Linux的IO设备管理方法、Unix IO接口、Unix IO常见函数,以及对printf和getchar的实现分析等内容。
(第8章1分)
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(1)hello所经历的过程
a.预处理——hello.c经过预处理得到文本文件hello.i
b.编译——hello.i经过编译得到汇编代码文件hello.s
c.汇编——hello.s经过汇编得到二进制可重定位文件hello.o
d.链接——hello.o经过链接得到可执行文件hello
e.输入运行命令./hello 2022110549 sunyuwen 2,shell接收命令并进行分析
f.调用fork函数创建新进程
g.调用execve函数在新的子进程中加载hello进程
h.从虚拟地址到物理地址的转换及访问内存
i.各种函数的执行及调用
j.在进程运行过程中的异常处理,接收相应信号并执行对应的异常处理
k.hello进程运行结束,被父进程回收,进行资源释放。
(2)深切感悟及创新理念
一个简单的hello程序的运行实际经过了很多的步骤,以hello为例理解程序执行到结束的过程有助于掌握计算机系统的相关知识。各章的内容被整合梳理,知识的脉络在这个探索过程中逐渐清晰。程序执行过程中的基本流程,包括预处理、编译、汇编、链接等阶段以及相应的细节得以呈现,也对进程管理、存储管理、IO管理等有了更深刻的认识。
关于计算机系统的设计,可以在安全性、高效性等方面对程序做进一步优化;针对各种可能存在的安全风险进行有效防范。熟练掌握计算机系统相关知识对于加深对计算机的理解和编写更加优秀的程序都至关重要。
(结论0分,缺失 -1分,根据内容酌情加分)
列出所有的中间产物的文件名,并予以说明起作用。
文件名字 | 文件作用 |
---|---|
hello.i | hello.c经过预处理得到的文本文件 |
hello.s | hello.i经过编译得到的文本文件 |
hello.o | hello.s经过汇编得到的二进制文件 |
hello | hello.o经过链接得到的可执行目标文件 |
(附件0分,缺失 -1分) |
为完成本次大作业你翻阅的书籍与网站等
[1]程序预处理阶段,在做什么_预处理阶段主要做的是哪两件事-CSDN博客http://t.csdnimg.cn/Z5Vfu
[2] C语言的预处理阶段都能完成哪些工作? - 千锋实践训练营的文章 - 知乎https://zhuanlan.zhihu.com/p/142108566
[3]程序详细编译过程(预处理、编译、汇编、链接) - 知乎
[4]《深入理解计算机系统》 Randal E.Bryant & David R.O’Hallaron 机械工业出版社
[5] 逻辑地址、物理地址、虚拟地址_虚拟地址 逻辑地址-CSDN博客
[6] 编译和链接的过程_编译链接四个步骤-CSDN博客http://t.csdnimg.cn/L2vIw
(参考文献0分,缺失 -1分)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。