计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 2021113560
班 级 WL026
学 生 陈禹西
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
摘 要
本文以一个简单的hello.c程序开始,介绍了一个程序在Linux下运行的完整生命周期,包括预处理、编译、汇编、链接、进程管理、存储管理、I/O管理这几部分,一步步详细介绍了程序从被键盘输入、保存到磁盘,直到最后程序运行结束,程序变为僵尸进程的全过程。清晰地观察hello.c的完整周期,生命历程。
关键词:Liunx;P2P;O2O;计算机系统;hello
目 录
第1章 概述................................................................................................................ - 4 -
1.1 Hello简介......................................................................................................... - 4 -
1.2 环境与工具........................................................................................................ - 4 -
1.3 中间结果............................................................................................................ - 4 -
1.4 本章小结............................................................................................................ - 4 -
第2章 预处理............................................................................................................ - 5 -
2.1 预处理的概念与作用........................................................................................ - 5 -
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
2.3 Hello的预处理结果解析................................................................................. - 5 -
2.4 本章小结............................................................................................................ - 5 -
第3章 编译................................................................................................................ - 6 -
3.1 编译的概念与作用............................................................................................ - 6 -
3.2 在Ubuntu下编译的命令................................................................................ - 6 -
3.3 Hello的编译结果解析..................................................................................... - 6 -
3.4 本章小结............................................................................................................ - 6 -
第4章 汇编................................................................................................................ - 7 -
4.1 汇编的概念与作用............................................................................................ - 7 -
4.2 在Ubuntu下汇编的命令................................................................................ - 7 -
4.3 可重定位目标elf格式.................................................................................... - 7 -
4.4 Hello.o的结果解析.......................................................................................... - 7 -
4.5 本章小结............................................................................................................ - 7 -
第5章 链接................................................................................................................ - 8 -
5.1 链接的概念与作用............................................................................................ - 8 -
5.2 在Ubuntu下链接的命令................................................................................ - 8 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
5.4 hello的虚拟地址空间..................................................................................... - 8 -
5.5 链接的重定位过程分析.................................................................................... - 8 -
5.6 hello的执行流程............................................................................................. - 8 -
5.7 Hello的动态链接分析..................................................................................... - 8 -
5.8 本章小结............................................................................................................ - 9 -
第6章 hello进程管理....................................................................................... - 10 -
6.1 进程的概念与作用.......................................................................................... - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
6.4 Hello的execve过程..................................................................................... - 10 -
6.5 Hello的进程执行........................................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................... - 10 -
6.7本章小结.......................................................................................................... - 10 -
第7章 hello的存储管理................................................................................... - 11 -
7.1 hello的存储器地址空间................................................................................ - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理.......................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问............................................................. - 11 -
7.6 hello进程fork时的内存映射..................................................................... - 11 -
7.7 hello进程execve时的内存映射................................................................. - 11 -
7.8 缺页故障与缺页中断处理.............................................................................. - 11 -
7.9动态存储分配管理........................................................................................... - 11 -
7.10本章小结........................................................................................................ - 12 -
第8章 hello的IO管理.................................................................................... - 13 -
8.1 Linux的IO设备管理方法............................................................................. - 13 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
8.3 printf的实现分析........................................................................................... - 13 -
8.4 getchar的实现分析....................................................................................... - 13 -
8.5本章小结.......................................................................................................... - 13 -
结论............................................................................................................................ - 14 -
附件............................................................................................................................ - 15 -
参考文献.................................................................................................................... - 16 -
P2P过程:
hello的生命周期经过如下过程:第一步通过预处理器cpp进行预处理,生成文本文件hello.i,第二部使用编译器ccl生成hello.s汇编程序,第三步会由汇编器as生成hello.o文件,最后一步会使用链接器ld将其与引用到的库函数链接,生成可执行文件hello。最后进行系统创建新进程接着会将程序内容加载,实现由程序到进程的转化。
O2O过程:
在一个程序运行前,shell调用execve函数将hello程序加载到相应的上下文中,将程序内容载入物理内存。然后调用main函数。程序运行结束后,父进程回收进程,释放虚拟内存空间,删除相关内容。
硬件环境:X64 CPU;4GHz;8G RAM;256GHD Disk
软件环境:Windows11 64位;VMware Workstation Pro16;Ubuntu 22.04.4
开发和调试工具:gdb;edb;readelf;objdump;Code::Blocks20.03
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。
第一章通过对hello的介绍,简单阐释了程序的P2P过程和O2O过程。并对程序所使用的工具即硬件工具,软件环境和开发调试工具总结。最后列出在整个实验中中间产生文件与解释。
预处理指的是在进行某个任务之前对数据进行一系列的操作,以便于后续任务能够更加高效和准确地完成。预处理的作用主要有以下几点:
数据清洗:清除无效数据、重复数据、缺失数据、异常数据等,保证数据的质量。
数据集成:将多个数据源的数据整合起来,形成一个完整的数据集。
数据转换:将数据从原始格式转换为特定需求的格式,比如将文本数据转换为向量表示。
数据归一化:将不同规格的数据统一到同一规格下,比如将年龄属性归一化到0-1范围内。
特征选择:从全部特征中选出与任务相关的特征,减少特征数量,提高模型性能。
使用指令:gcc -E -o hello.i hello.c
图1.预处理指令输入
图2.文件hello.i内容展示(部分)
预处理后的文件可打开为文本模式,其中代码总共有3091行。经过程序分析,其中代码3078行-3091行为C语言程序main内容。代码内容之前则为加载进入程序中的头文件。
图3.文件hello.i内容解析
本章初略解释预处理概念及其相关作用。并就hello.c程序的预处理步骤相应展示,最后对hello.i预处理结果文件内容进行分析。
编译是将高级程序语言源代码转换为计算机可执行的机器语言的过程,本环节指将程序文本由hello.i编译为hello.s的过程。
它的作用是让计算机能够理解和执行人类能够编写的高级程序语言,而不需要直接操作计算机底层机器语言。相比于解释型语言,编译型语言具有更快的执行速度和更好的性能表现。因为编译器在将代码翻译成机器语言时已经进行了优化,所以执行速度会更快。另外,编译器还可以对代码进行优化,从而使生成的目标代码更加高效,提高程序的运行效率。
使用指令:gcc -S hello.c -o hello.s
图4编译指令输入
图5文件hello.s展示
3.3.1节名称与相应阐释
.file 源文件后缀 .text 代码节后缀
.section.rodata 只读数据后缀 .globl 全局变量后缀
.type 符号函数/数据类型后缀 .size 大小后缀
.string 字符串后缀 .align 指令/数据存放地址对齐后缀
3.3.2函数参数分析
图6.main函数参数
函数中的参数其一为数组,指向字符类型指针,数组存放地址在栈中。经过两次标记处位置将参数传递给printf函数使用。
3.3.3变量参数分析
图6.变量分析
代码声明,main函数作为一个全局函数存在。
图7.变量分析
第一个为局部变量i,编译器会将变量i存放在栈中,如图在-4(%rbp)位置,后续跳转到L3处,进行栈指针减4操作。
第二个为函数数组char *argv[],数组每个元素为字符类型指针,起始地址存放在栈中-32(%rbp)位置。
3.3.4立即数分析
立即数直接展现在代码中,如图展示部分立即数。
3.3.5数组分析
图8.数组分析
对数组的操作,由数组的首地址,加上偏移量。在main中,调用了argv[1]和argv[2],在汇编代码中,每次将%rbp-32的的值即数组首地址传%rax,然后将%rax分别加上偏移量16和8,得到了argv[1]和argv[2],在分别存入对应的寄存器%rsi和%rdx作为第二个参数和第三个参数,之后调用printf函数时使用。
3.3.6赋值算数操作分析
hello.c中赋值操作是for循环中i=0;在汇编代码中使用mov指令实现,mov指令根据操作数的字节大小分为:movb:一个字节movw:“字”movl:“双字”movq:“四字”算数操作\nhello.c中的算术操作是i++,汇编语言addl $1, -4(%rbp)
3.3.8关系分析
Main函数中argc!=4是条件判断语句,进行编译时,在文件中被编译为图示第一条指令,根据条件码判断是否需要跳转。
Main函数中代码i<8作为判断循环条件指令被编译为cmpl $7,-4(%rbp),并设置条件码,为下一步 jle 利用条件码进行跳转L4做准备。
3.3.9转移控制指令
图9,转移指令
汇编语言中先设置条件码,然后根据条件码来进行控制转移。图示第一条,先判断参数是否等于4,正确则不执行if语句,否则跳转执行。图示第二条指令,循环判断是否参数i<8,决定是否继续参与循环跳转。
本章节首先就编译概念与作用进行一个简单阐述,并就函数编译操作(本章为.i->.s转换)展示。在本章中对于编译语言从C语言中包含数据类型及操作,赋值操作,类型转换操作,算术逻辑操作,关系操作,数组/指针/结构操作,控制转移操作,函数操作等进行代码分析。
汇编语言是一种计算机语言,用于编写计算机指令。它是一种较低级别的语言,与高级编程语言相比,它更加接近计算机底层的硬件架构和指令集。汇编语言以符号的形式表示机器指令,程序员可以通过使用这些符号来编写程序。
汇编语言的作用在于,它提供了一种直接控制计算机底层指令执行的方法,使得程序员能够更好地优化程序性能,并且可以更细致地控制计算机资源。而且,由于汇编语言生成的机器代码直接运行在 CPU 上,所以它可以产生非常高效的代码,比如处理大量数据的算法、实时响应的系统和网络通信等领域。另外,学习汇编语言也有助于理解计算机底层原理,有利于提升程序员的编程能力。
使用指令:gcc -c hello.s -o hello.o
图10.汇编指令输入
4.3.1elf
图11.elf
1、ELF头以16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助两届其语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可执行、可重定位或者共享的)、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。
2、.text:已编译程序的机器代码后缀
3、.rodata:只读数据后缀
4、.data:已初始化的全局变量和局部静态变量后缀
5、.bss:未初始化的全局变量和局部静态变量后缀
6、.symtab:符号表,存放函数和全局变量(符号表)信息,不包括局部变量
7、.rel.text:.text节的重定位信息,用于重新修改代码段的指令中的地址信息
8、.rel.data:.data节的重定位信息,用于对被模块使用或定义的全局变量重定位的信息
9、.debug:调试符号表,只有以-g方式调用编译器驱动程序时,才会得到这张表。
10、.line:原始C源程序中的行号和.text节中机器指令之间的映射
11、.strtab节:字符串表,包括.symtab和.debug节中的符号表
12、节头表:每个节的节名、偏移和大小后缀
4.3.2hello.o分析
图12.字节头
除了ELF头外,节头表的是ELF可重定位目标文件中最重要的部分。描述了每个节的节名文件偏移、大小、访问属性、对齐方式等。图中,Name列为节名,Type为类型,Offset为起始地址,Size为大小。
图13.ELF头
Class:64位版本
Data:使用补码表示,且为小端法
Version:版本为1
OS/ABI:操作系统为UNIX – SYSTEM V
TYPE:REL表明这是一个可重定位文件
Machine:64位机器上编译的目标代码为Advanced Micro Devices X86-64
Entry point address:为0x0表示程序的入口地址为0
Start of program headers:为0表示没有程序头表
Start of section headers:节头表的起始位置为1240字节处
Size of section headers:64表示每个表项64个字节
Number of section headers:14表示共14个表
Section header string table index:13为.strtab在节头表中的索引
图14.符号表
Name为符号名称,Value是符号相对于目标节的起始位置偏移,Size为目标大小,Type是类型,数据或函数,Bind表示是本地还是全局。
图15.重定位
hello.o文件中没有.rel.data段,即.data节不需要额外的重定位信息。
对于.rel.text节,Offset为需要重定位的地址与该段首地址的偏移;Info高24位为所引用的符号索引,低8位对应的重定位类型;Type有两种类型,一种是R_X86_64_PC32(R_X86_64_PLT32和R_X86_64_PC32是同一种寻址方式,详见参考文献[4])表示使用的是32位PC相对地址的引用,R_X86_64_32表示使用的是32位绝对地址的引用;Sym. Name为绑定的符号名, Addend为偏移。
由于PC的相对引用和动态链接的特殊,PC的值其实位下一条指令的地址,其值与我们目前想进行重定位的值有4个字节的偏移。Addend减去了4个字节,也就是说第一行指向的其实是.rodata段的首地址,第4行其实是.rodata首地址+0x26。
图16.节组
文件中无节组信息
指令输入:objdump -d -r hello.o
图17.反汇编
通过将反汇编与hello.s比较发现,汇编指令代码几乎相同,反汇编代码除了汇编代码之外,还显示了机器代码,在左侧用16进制表示。机器指令有操作码和操作数组成,和汇编指令一一对应。最左侧为相对地址。
其中跳转指令和函数调用等指令,在反汇编代码中表示为对应地址的偏移,而在hello.s中直接表示为函数名或定义的符号。在反汇编代码中,立即数是16进制显示的,而在hello.s中立即数是以十进制显示的。
本章先就汇编概念与作用进行简单阐释,接着输入指令反汇编操作展示生成ELF可重定位目标文件hello.o,接着通过使用readelf多种指令,设置不同参数。查看ELF信息,最后将文件与第三章hello.S文件比较分析。
链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
链接的作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
指令输入:
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
图18.指令输入
可执行目标文件与可重定位文件稍有不同,ELF头中字段e_entry给出执行程序时的第一条指令的地址,而在可重定位文件中,此字段为0。可执行目标文件多了一个程序头表,也成为段头表,是一个结构数组。还多了一个.init节,用于定义init函数,该函数用来执行可执行目标文件开始执行时的初始化工作。因为可执行目标文件不需要重定位,所以比可重定位目标文件少了两个.rel节。
图19.elf头
查看hello的ELF头:发现hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件,这与hello.o不同。hello中的节的数量为27个。
图19展示字节表
发现刚才提到的27个节的具体信息,在节头表中都有显示,包括大小Size,偏移量Offset,其中Address是程序被载入虚址地址的起始地址。
查看hello的程序头表,首先显示这是一格可执行目标文件,共有12个表项,其中有4个可装入段(Type=LOAD),VirtAddr和PhysAddr分别是虚拟地址和物理地址,值相同。Align是对齐方式,这里4个可装入段都是4K字节对齐。以第一个可装入段为例,表示第0x00000~0x005bf字节,映射到虚拟地址0x400000开头的长度为0x5c0字节的区域,按照0x1000=4KB对齐,具有只读(Flags=R)权限,是只读代码段。
图20.edb打开
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
由图,可以看到虚拟地址空间的起始地址为0x400000。
.inerp段的起始地址为04002e0
.text段的起始地址为0x4010f0,
.rodata段的起始地址为0x402000
图21.部分反汇编
与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中
子程序名:hello!_start 地址:0x00000000004010f0
hello!__libc_csu_init 0x0000000000401270
hello!_init 0x0000000000401000
hello!frame_dummy 0x00000000004011d0
hello!register_tm_clones 0x0000000000401160
hello!main 0x00000000004011d6
hello!printf@plt 0x0000000000401040
hello!atoi@plt 0x0000000000401060
hello!sleep@plt 0x0000000000401080
hello!getchar@plt 0x0000000000401050
hello!exit@plt 0x0000000000401070
hello!__do_global_dtors_aux 0x00000000004011a0
hello!deregister_tm_clones 0x0000000000401130
hello!_fini 0x00000000004012e8
.got的地址为0x403ff0。
本章首先就链接的概念与作用进行简单阐述,接着通过演示汇编操作说解释可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。
进程是计算机中正在运行的程序的实例。每个程序都可以有多个实例,每个实例都是一个独立的进程。进程拥有自己的内存空间、CPU时间和其他操作系统资源。当一个程序被启动时,操作系统会创建一个新的进程,并为该进程分配所需的资源。
进程在计算机中扮演着重要的角色。它们允许多个程序同时运行,而不会相互干扰或崩溃。通过使用进程,操作系统可以确保系统资源得到合理地分配和使用。此外,进程还提供了一种机制,使得程序可以与其他程序进行通信和协作。进程也使得操作系统能够对程序的执行进行控制和管理,例如允许或终止一个进程。
壳(Shell)是计算机操作系统提供给用户与操作系统交互的界面。它可以解释用户输入的命令,并将其转换为操作系统能够理解的形式。其中,Bash是Unix和Linux操作系统中最常用的壳程序之一。
Bash的作用是提供一个命令行环境,使得用户能够直接与操作系统交互,执行各种任务。它允许用户在命令行中输入不同的命令,例如目录管理、文件操作、进程控制等,并通过Shell解释器将这些命令转化成操作系统内核能够识别和执行的指令。
当用户在Bash Shell中输入一个命令时,Shell首先会读取用户输入的命令。然后,它会将该命令传递给Shell解释器,解释器将对命令进行处理,并将命令转换为操作系统内核能理解的格式。接着,Shell将向操作系统内核发出系统调用,请求执行所需的操作。
操作系统内核接收到来自Shell的系统调用请求后,将执行相应的操作并返回结果。Shell会将操作结果输出到用户终端上,以便用户查看。如果命令执行失败或产生错误,Shell会显示相应的错误信息。
进程的创建采用fork函数:pid_t fork(void);创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。
父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。
父进程为shell,在输入./hello的时候,首先shell会对我们输入的命令进行解析,shell会认为时执行当前目录下的可执行文件hello,因此shell会调用fork()创建一个子进程,
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:
删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
设置程序计数器(PC).exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:
1.一个独立的逻辑控制流,提供一个假象,好像我们的进程独占的使用处理器
2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
操作系统所提供的进程抽象:
1.逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
2.上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
3.时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
4.用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
5.上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
hello执行中可能出现的异常:
1.中断:异步发生的。在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。结果就像没有发生过中断一样。
2.陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
3.故障:由错误引起,可能被故障处理程序修正。在执行hello时,可能出现缺页故障。
4.终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
图22.正常运行
图23.ctrl+c
图24.ctrl+z
图25.监视后台
图26.当前暂停程序
图27.树状显示进程
图28.重新运行
图29.杀死进程
本章介绍了进程的概念和作用、shell-bash的处理过程与作用并且着重分析了调用fork创建新进程,调用execve函数执行hello,hello的进程执行过程,以及hello在运行时遇到的异常与信号处理。
1、逻辑地址:程序经过编译后出现在汇编代码中的地址。
2、线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3、虚拟地址:也就是线性地址。
4、物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址
一个逻辑地址由两部分组成:段标识符:段内偏移量。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
索引号就是“段描述符”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
Linux通过分段机制,将逻辑地址转化为线性地址。给定一个完整的逻辑地址[段选择符:段内偏移地址]。首先,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。然后,拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,基地址就知道了。最后,把基地址 + 偏移量,就是要转换的线性地址了。
线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。\n系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。\n\n\nn位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
页表技术虽然能让我们再给出虚拟地址的时候,很大概率通过查找页表来找到内存地址,但是查页表也是访问内存的过程,也很浪费时间。利用局部性原理,像缓存一样,将最近使用过的页表项专门缓存起来。因此出现了TLB(后备转换缓冲器,也叫快表),之后找页表项的时候,先从快表找,找不到在访问内存中的页表项。同理,四级页表能保证页表项的数量少一些。
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
加载并运行 hello 需要以下几个步骤:
1.删除当前进程虚拟地址中已存在的用户区域
2.映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
3.映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域。
4.设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。
处理程序执行如下步骤:
1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
2.检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
3.两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk(读作“break”),它指向堆的顶部。
分配器有两种基本风格:
1.显式分配器:要求应用显式地释放任何分配的块,例如C标准库提供的malloc程序包。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就是放这个块,也被称为垃圾收集器。
分配器简单来说有以下几种实现方式:
本章主要介绍了hello的存储器地址空间,逻辑地址到线性地址、线性地址到物理地址的变换,接着介绍了四级页表下的线性地址到物理地址的变换,分析了hello的内存映射,及缺页故障与缺页中断处理和动态存储分配管理。
1.输入:将hello.c代码从键盘输入。
2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,生成hello.i文件。
3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。
4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。
5.链接(ld):将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程\n序hello,至此可执行hello程序正式诞生。
6.运行:在shell中输入./hello 2021113560 陈禹西 1
7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()函数创建一个子进程。
8.加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口\n后程序开始载入物理内存,然后进入main函数。
9.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源, 顺\n序执行自己的控制逻辑流。
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编\nhello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.