计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能模块(2+x)
学 号 2021113169
班 级 21Wl025
学 生 李文豪
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
本文按照哈尔滨工业大学计算机系统课程大作业要求,以hello程序为例,从预处理编译开始,到存储管理结束,使用各种工具在linux环境下对hello程序进行分析,详细的分析了hello P2P(程序到进程),O2O(程序开始从磁盘加载进入内存到程序结束退出内存)的整个过程,分析了其中各种的处理方式,可以很好的帮助理解程序的整个运作过程,加深对与计算机系统的理解,从而提升自己的学业水平。
关键词:计算机系统;P2P;系统管理;
目 录
第1章 概述
1.1 Hello简介
1.1.1 P2P(From Program to Process)过程:
hello的生命周期是从一个高级C语言程序开始的,分为四个阶段:首先经过预处理器cpp进行预处理,生成文本文件hello.i,然后经过编译器ccl生成hello.s汇编程序,接着经过汇编器as生成hello.o文件,最后经过链接器ld将其与引用到的库函数链接,生成可执行文件hello。再通过系统创建一个新进程并且把程序内容加载,实现有程序到进程的转化。
1.1.2 O2O(From Zero-0 to Zero-0)过程:
程序运行前,shell调用execve函数将hello程序加载到相应的上下文中,将程序内容载入物理内存。然后调用main函数。程序运行结束后,父进程回收进程,释放虚拟内存空间,删除相关内容。这就是hello.c的O2O过程。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;VMware Workstation Pro15.5.1;Ubuntu 20.04.4
开发和调试工具:gdb;edb;readelf;objdump;Code::Blocks20.03
1.3 中间结果
文件名 | 文件作用 |
hello.c | 源代码 |
hello.i | hello.c预处理后的文件 |
hello.s | hello.i编译后的文件 |
hello.o | hello.s汇编后的文件 |
hello.o.objdump | hello.o反汇编的文件 |
hello | hello.o链接的文件 |
hello.objdump | hello反汇编的文件 |
elf.txt | hello.o用readelf -a hello.o指令生成的文件 |
hello1.elf | hello用readelf -a hello指令生成的文件 |
1.4 本章小结
本章根据hello的自白,概括介绍了hello的P2P和O2O的过程,介绍了本实验用到的硬软件环境和开发调试工具,并对下文所产生的文件进行了总汇。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。[1]
2.1.2 预处理的作用
当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2在Ubuntu下预处理的命令
预处理有多种命令,如gcc –E hello.c –o hello.i和cpp hello.c > hello.i,本次实际操作中使用的方式为第二种方式,如图所示。
2.3 Hello的预处理结果解析
经过预处理,原hello.c文件中的预处理命令如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等消失,被转换为文件实际地址或其他方式,并对转换后引入的新预处理命令再次递归处理。
由于该步骤会不断递归进行,预处理后得到的.i文本往往会变得很长。在预处理过程结束后得到的hello.i文件中,文本行数有着明显的巨量提升,达到了3060行。
2.4 本章小结
本章对预处理的概念与作用进行了具体描述,并展示了hello.c在Ubuntu下的预处理过程,以及对hello.i文本进行了简要解析。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。整个编译过程,实质就是是将高级语言翻译为机器语言的过程。
3.2.2 编译的作用
编译而成的汇编语言程序中,每一条汇编语言都对应着低级机器语言指令。汇编语言为不同高级语言的不同编译器提供了通用的输出语言,这使得程序在各种情况下都能顺利翻译。
3.2 在Ubuntu下编译的命令
使用gcc编译的命令为:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1 字符串常量
printf中的格式化字符串被认为是字符串常量,被放在.rodata段中,定义为string类型,分别对应着hello.c文件中两个printf中的字符串,globl项说明了该常量作用范围为main函数内。
3.3.2 局部变量
hello.c的main函数中定义了一个局部变量i,该变量被保存在栈中,在之后的汇编代码中,通过-4(%rbp)完成对i的引用。
3.3.3 整形常量
在汇编代码中,整形常量以立即数的形式进行表示,如图。
3.3.4 参数传递
main函数的参数保存如图所示,21行代码将栈指针减少32位,然后分别将%rdi和%rsi的值存入栈中,由此可得,%rbp-20和%rbp-32的位置分别存了argc和argv数组的值。
printf函数的参数的传递如图所示,分别将第一、第二、第三个参数依次保存在%rdi,%rsi,%rdx中,实现参数传递。
3.3.5 赋值
hello.c中的赋值操作为在循环中将i赋值为0,在汇编代码中赋值往往通过mov来实现,如图即为对应汇编代码。
3.3.6 算术操作
hello.c中涉及到的算术操作为i++,在汇编语言中可通过add来实现,如图即为对应汇编代码。
3.3.7 关系操作
hello.c中涉及到的关系操作为!=和<。其中!=操作通过cmp进行条件码设置后使用je来实现判断,如图即为对应汇编代码。
而hello.c中涉及到的<操作为i<8,在实际编译过程中被优化为i<=7,通过cmp进行条件码设置后使用jle来实现判断,如图即为对应汇编代码。
3.3.8 数组操作
hello.c中的数组操作为在函数传参时使用到的argv数组,汇编语言通过在数组地址上进行偏移得到对应所需值,如图即为对应汇编代码。
3.3.9 函数调用
函数调用在汇编语言中通过call来实现,如图即为hello.c中部分函数调用过程对应汇编代码。
3.3.10 函数返回
函数的返回值通过%rax来进行传递,如调用atoi函数后其返回值保存在%rax中,之后又作为sleep函数的参数进行传递传递,如图即为对应汇编代码。
3.3.11 控制转移
hello.c中涉及到的控制转移为if和for。其中if通过上文!=关系操作来进行判断,通过je跳转到关系不成立的下一条指令,如图即为对应汇编代码。
for则较为复杂,首先对i进行赋值,然后跳转到条件判断处进行判断,若满足条件则再次跳转到内容代码处,之后循环操作,其中所涉及内容上文均有提及,整体汇编代码如图所示。
3.4 本章小结
本章主要对编译操作进行了详细解析,首先介绍了编译的作用与概念,之后展示了在ubuntu环境下的编译操作,最后对编译后得到的.s文件内容进行了详细分析。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编器(as)将汇编程序翻译为机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中
4.1.2 汇编的作用
生成机器指令,方便机器直接分析。
4.2 在Ubuntu下汇编的命令
汇编的命令有多种,如as hello.s -o hello.o 和gcc -c hello.s -o hello.o,图中为使用第二种进行汇编后得到的结果。
4.3 可重定位目标elf格式
4.3.1 ELF头
ELF头以16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助两届其语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可执行、可重定位或者共享的)、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。如图即为hello.o的ELF头内容。
4.3.2 节表头
内容为每个节的节名、偏移和大小。如图即为hello.o的节表头内容。
4.3.3 .rela.text段
一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。如图即为hello.o的.rela.text内容。
4.4 Hello.o的结果解析
执行命令objdump -d -r hello.o > hello.o.objdump得到反汇编文本,下面进行对比分析。
4.4.1 分支跳转
在反汇编文件中,跳转是用地址的偏移量来间接寻址的,可看出汇编语言在汇编成机器语言之后跳转地址会被替换为确定的地址。
4.4.2 函数调用
函数调用与分支跳转类似,在.o文件中函数名被替换成当前位置的下一条指令,以便静态链接或者动态链接后确定其具体的位置。
4.4.3 立即数表示
在.s文件汇编之后,十进制数被转换为二进制数,因此在反汇编之后的文本中立即数的表达方式均为16进制。
4.5 本章小结
在本章中介绍了汇编的概念与作用,使hello.s经汇编器被汇编为hello.o,并对其ELF格式进行研究,最后通过反汇编观察并分析汇编过程中产生的特点与变化。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
5.1.2 链接的作用
链接使得分离编译,一个大的应用程序可以被分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
5.2 在Ubuntu下链接的命令
链接的命令较为复杂,为ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello。如图即为链接结果。
5.3 可执行目标文件hello的格式
使用命令readelf -a hello > hello1.elf得到各段基本信息,大部分内容与上文所提到的elf格式内容基本相同,下面主要介绍ELF头,节表头和程序头表三个主要内容。
5.3.1 ELF头
该部分内容与hello.o的ELF头内容除类型外基本一致,包括ELF头的大小、目标文件的类型(如可执行、可重定位或者共享的)、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。如图即为hello的ELF头内容。
5.3.2 节表头
内容为每个节的节名、偏移和大小。其中.text段(存放程序的段)的地址与ELF头中程序入口地址一致。
5.3.3 程序头表
程序头中包括所指向段的类型、其在ELF文件中的偏移地址、大小,映射到内存的虚拟地址信息,段的读写权限等等。如图即为hello的程序头表。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。如图所示,可以找到程序入口点地址0x4010f0,与5.3显示信息一致。此外,.rodata段的起始地址为0x402000,也可以在edb中找到。
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > hello.objdump进行反汇编。
5.5.1 分支跳转
在.o文件中,分支跳转是采用偏移量的方式进行间接跳转的,而在.out文件中,分支跳转时直接跳转到具体的地址实,可看出链接时进行了重定位操作。
5.5.2 函数调用
在.o文件中函数的调用直接用当前的下一个地址占位,而在.out文件中是跳转到函数的地址处,可看出链接时进行了重定位操作。
5.5.3 数据访问
在.o文件中,对于rodata中的数据(全局变量,字符串常量)的访问是用0(%rip)的形式占位标记的。而在out文件中直接根据给出了具体的偏移量,可看出链接时进行了重定位操作。
5.6 hello的执行流程
Shell通过调用加载器的操作系统代码来运行。在程序头部表的引导下,加载器将hello可执行文件的片复制到代码段和数据段。随后,加载器跳转到程序的入口点,这里就是_start的地址,随后首先加载库函数,最后开始执行main函数。
子程序名 | 地址 |
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 |
5.7 Hello的动态链接分析
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时。out文件中,调用共享库的函数,都是通过PLT进行调用的,而程序中都是调用【函数名@plt】的函数,而这些函数都会指向同一个地址。
所有的调用共享库最终都会到0x401026这里,而这里的jmp语句是跳转到0x404010内存所存储的地址处,在dl_init前,这里的地址是空的。
在dl_init后,0x404010的值会被改变,而这个地址指向的是ld-2.33.so动态库中的一个函数地址,该函数会根据PLT压入的值调用对应的函数。
5.8 本章小结
本章介绍了链接的概念与作用,将hello.o经过链接得到hello.out文件,查看该文件的ELF并进行分析。之后通过edb反汇编了解hello的虚拟地址空间,分析了链接中的重定位过程,并找到了该程序运行的详细执行流程,最后对动态链接行为进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
一个执行中程序的实例。
6.1.2 进程的作用
文件在被创建的其对应的进程的上下文中运行。让程序可以对由程序变量表示的内部程序状态中的变化做出反应。
6.2 简述壳Shell-bash的作用与处理流程
在计算机科学中,Shell俗称壳(用来区别于核),是指"为使用者提供操作界面"的软件(命令解析器)。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。
shell负责确保用户在命令提示符后输入的命令被正确执行。其功能包括:
(1) 读取输入并解析命令行
(2) 替换特别字符,比如通配符和历史命令符
(3) 设置管道、重定向和后台处理
(4) 处理信号
(5) 程式执行的相关设置
处理流程:
1. Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符 号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令 的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程 回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command) 标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割 符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有从处理的结果中用到的注释删除,並且按照下面的顺序实行命 令的检查:
A.内建的命令
B. shell函数(由用户自己定义的)
C.可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。
6.3 Hello的fork进程创建过程
父进程通过调用该fork()函数创建一个新的子进程。shell判断输入字符串“./hello”不是内置指令,则执行执行fork系统调用,shell的进程将分化成两个进程父进程和子进程,此时hello还未成为进程。
6.4 Hello的execve过程
execve函数的作用是在当前进程的上下文中加载并运行一个新程序。在子进程中,shell执行execve系统调用,将hello.out装载入内存,覆盖了原先的内存信息,此时,hello正式成为进程,下一步开始执行。
6.5 Hello的进程执行
在Linux系统中,进程调度采用的是抢占式多任务处理,分为就绪态,运行态,等待态三种状态,对于每个核来讲,同一时间内处于运行态的进程只能有一个,当运行态中的进程用光了分配给他的时间片后,会回到就绪态与其优先级相同的队列队尾,然后加载就绪态中优先级最高的队列并处于队首的进程进入运行态执行,如果程序在运行过程中执行了sleep这样的等待函数就会从运行态退出进入等待态,等待结束后再进入就绪态排队。
进程状态的切换是利用上下文切换机制实现的,上下文切换本质是异常处理机制,运行态的程序由于某种原因(时间片用完,需要等待等等)引发了该异常,进入内核态,在内核态中完成了对进程的状态改变以及决定了下一个进入运行态的程序,并加载入CPU执行。
在Hello中,程序加载成功后进入就绪态等待进入运行态,进入运行态后打印相应的字符串,打印完成后会调用sleep,因而进入等待态,休眠结束后进入就绪态等待进入运行态,依次循环,直到for循环结束,结束后会因为需要等待用户输入再次进入等待态等待用户执行,用户执行完后,该进程经过一系列退出操作后,会发出SIGCHLD信号,等待被shell回收。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 可能出现的异常分析
异常和信号异常分为四类:中断、陷阱、故障、终止。hello执行中这些异常均可能出现。
中断:在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。结果就像没有发生过中断一样。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:由错误引起,可能被故障处理程序修正。在执行hello时,可能出现缺页故障。
终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。
6.6.1 运行时输入回车
通过上文分析,可以得出该种操作会导致中断。
6.6.2 运行时输入Ctrl+C
Ctrl+C会发送一个SIGINT信号给hello,而SIGINT信号的默认操作是终止进程,在hello中没有对SIGINT信号进行捕捉所以会执行默认操作终止进程。
6.6.3运行时输入Ctrl+Z
按Ctrl+Z,会发送SIGTSTP信号,该信号默认操作是暂停进程,即让进程处于等待态。
在该种状态下,可以继续运行其他指令,如图即为依次运行ps,jobs,pstree。
通过输入fg,可以使停止的进程收到SIGCONT信号,重新在前台运行。
通过输入kill,-9 5878,即给进程9974发送9号信号SIGKILL,可以杀死进程。
6.7本章小结
本章首先介绍了进程的概念与作用,简述了壳Shell-bash的作用与处理流程,以及hello进程的各部分详细流程,并在最后探究了可能出现的异常与信号。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序编译后出现在汇编代码中的地址,用来指定一个操作数或一条指令的地址,由段标识符加上偏移量表示。
线性地址:地址空间的整数是连续的。
虚拟地址:为每一个进程提供的私有的、大的和一致的地址空间。
物理地址:计算机系统的主存被组织成一个由连续字节大小的单元的数组。为每个字节分配唯一的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在汇编代码中我们看到的地址只是一个段内偏移量,需要加上DS数据段的基地址才能构成线性地址,在x86保护模式下,段描述符占8个字节无法放在段寄存器(2个字节)中,所以把段描述符整理成一张表(GDT),放在了内存中,而段寄存器中存放改段描述符的索引值,而需要的时候根据这个索引值取内存中找,找到后,基地址+偏移量便得到了线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页表基址寄存器指向当前页表。虚拟地址包含两部分,一是虚拟页面偏移,另一个是虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中物理页号和虚拟地址中的VPO串联起来,得到了对应的物理地址。物理页面偏移和VPO是相同的。
CPU硬件执行的步骤如下:
1.处理器生成一个虚拟地址,并把它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.MMU构造物理地址,并把它传送给高速缓存/主存
5.高速缓存/主存返回所请求的数据字给处理器
7.4 TLB与四级页表支持下的VA到PA的变换
MMU把虚拟地址(VA)转化成物理地址(PA)是通过查询页表(PTE)实现的,PTE中存储了虚拟页到物理页的映射关系,PTE是常驻内存的一个表,如果CPU每次查询都去访问内存访问PTE的话速度太慢,所以引入了TLB(与Cache类似),TLB是MMU中一个小的具有较高相联度的缓存,其运行机理类似于Cache,只不过存储的只是PTE而已,通过这样的缓存极大的提高了PTE的访问效率,但对于地址空间位64位的系统来讲,PTE占用了非常多的内存,于是引入了多级页表,第一级页表常驻内存,而其它的级数只在用到的时候创建放入内存中,这样极大的减少了内存的需要。
在访问时,MMU通过把根据虚拟地址查表一级PTE,PTE根据虚拟地址指向下一级页表,下一级页表又根据虚拟地址指向下下级页表,到第四级页表时查询得到具体的物理页号(PPN),根据PPN和VPO(虚拟页面偏移量与物理页面偏移量PPO相同),就可以访问到具体的物理内存了。
7.5 三级Cache支持下的物理内存访问
在三级Cache支持下,CPU访问物理内存首先会去Cache L1中找需要访问的内存是否已被缓存,是否有效,如果已被缓存且有效就称为Cache命中,直接就对其进行读写即可,如果Cache L1没有找到,就去L2中寻找,如果还是没有就去L3中寻找,如果依旧没有,这时才会访问内存,把需要访问的内存附近的整一Cache line都加载进入Cache L3,L2,L1中,然后再进行读写操作。
7.6 hello进程fork时的内存映射
当调用fork被Hello这个进程调用时,内核为该新进程创建了相应的数据结构并分配给它一个唯一的PID。他会创建当前hello进程的mm_struct、区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
在执行shell执行execve时,会删除当前进程虚拟地址的用户部分中的已存在的区域结构,为新程序的代码、数据、bss和栈区域创建新的区域结构,再银蛇共享区域,最后设置程序计数器,使其指向代码区域的入口(_start函数)。
7.8 缺页故障与缺页中断处理
因为程序运行时,不会将所有的数据都加载到内存中,而是放在磁盘上等需要的时候再加载进入内存。如果程序运行需要的数据还未从磁盘上加载进入内存,也就是MMU查找PTE时没有找到对应的地址,此时就会引发缺页故障异常,而缺页故障程序会根据当前内存状态,选择牺牲内存中的一个内存页,然后把磁盘上的物理页加载进入内存中。缺页异常处理完后,会跳转到引发缺页故障的那条指令重新执行。
7.9本章小结
本章对hello程序运行时虚拟地址的变化进行分析,解析了hello应用程序的虚拟存储地址空间,分析了虚拟地址,线性地址和虚拟物理线性地址之间的互相转换,页表的命中与不页表的命中,使用动态快表缓存作为页表的高速缓存以及如何加速页表,动态内存管理的操作,fork时的动态内存中断与映射、execve时的动态内存中断与映射、缺页的中断与缺页映射和中断的处理。
结论
程序员通过键盘输入保存在磁盘上的hello.c文件,经过预处理,编译,汇编,链接一系列过程,一步步地被加工为一个完整的可执行文件。之后,在shell加载hello,先执行fork,调再用execve,使hello成为了独立的进程完成了P2P的整个过程。
在hello执行完毕之后,如果函数内部没有调用exit,__lib_start_main函数会帮我们调用exit退出,在退出后会给shell发送一个SIGCHLD信号,shell收到这个信息后会释放之前用于存储hello信息的一些内存空间,使hello完成了O2O的过程。
通过本次大作业,我系统的认识了整个计算机系统结构,以一种从未有过的视角去看待程序的运行过程。对于一直以来从未关注过程序在底层是如何运行的我来说,这门课程是不容易的,从平时的上课,到每一次的实验,再到本次大作业,每一项都耗费了我很大的精力。但当我一步步完成这一项项任务后,我发现我已对于计算机系统已经有了较为具体的了解,知道了程序除了复杂度还能有更丰富多样的优化算法,清楚了哪怕是一个简单的hello的背后都蕴含着无比丰富的信息。总之,这门课让我受益颇丰,我相信这门课带给我的知识可以让我在未来受益终生。
附件
文件名 | 文件作用 |
hello.c | 源代码 |
hello.i | hello.c预处理后的文件 |
hello.s | hello.i编译后的文件 |
hello.o | hello.s汇编后的文件 |
hello.o.objdump | hello.o反汇编的文件 |
hello | hello.o链接的文件 |
hello.objdump | hello反汇编的文件 |
elf.txt | hello.o用readelf -a hello.o指令生成的文件 |
hello1.elf | hello用readelf -a hello指令生成的文件 |
参考文献
- 袁春风. 计算机系统基础. 北京:机械工业出版社,2018.7(2019.8重印)
[2] Randal E. Bryant;David R. O’Hallaron. 深入理解计算机系统. 北京:机械工业出版社,2016.7(2019.3重印)
[3] 一个简单程序从编译、链接、装载(执行)的过程-静态链接 - 知乎 (zhihu.com)
[4] 编译和链接的过程_douguailove的博客-CSDN博客_编译过程
[5] (1条消息) readelf命令使用说明_木虫下的博客-CSDN博客_readelf
[6] 【转】linux汇编.section .text .data 与.global - 比较懒 - 博客园 (cnblogs.com)