计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 小卫星
学 号 2021112314
班 级 21TS001
学 生 王嘉乐
指 导 教 师 刘宏伟
计算机科学与技术学院
2023年5月
本文在Linux系统上追踪hello程序的生命历程,探究hello程序从源程序hello.c经过预处理、编译、汇编、链接生成可执行文件的实现原理,分析过程中产生的各个文件的内容与作用。同时介绍了shell的内存管理、IO管理、进程管理等相关知识。通过hello程序来全面地总结梳理CSAPP课程所学的内容。
关键词:hello程序;LINUX系统;预处理;编译;汇编;链接;进程;shell;储存;虚拟内存;I/O。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
hello程序的生命周期是从源程序开始的,程序员通过编辑器编写并保存hello.c文件,运行C预处理器(cpp)将hello.c文件进行预处理,生成hello.i文件,再运行C编译器(ccl)将此hello.c文件翻译为一个可汇编文件hello.s,然后运行汇编器(as)将hello.s文件翻译为一个可重定位的目标文件hello.o,最后运行链接器(ld)将hello.o和各种目标文件链接起来。最终创建出一个可执行目标文件hello。
shell为hello创建进程并加载hello的可执行文件,为其提供了虚拟地址空间等进程上下文,实现了hello的从无到有的过程。hello在运行时会经历诸多的异常与信号,以及对存储器的访问也会涉及诸多机制,以及通过中断和IO端口与外设交互,等等。最终,hello正常退出或收到信号后终止,都会使得操作系统结束hello进程,释放其占用的一切资源,返回shell,如此结束hello的一生。
1.2 环境与工具
硬件环境:AMD Ryzen 5 5500U,16GB内存。
系统环境: 虚拟机:Ubuntu 18.04 LTS,VMWare Workstation 16
工具: 文本编辑器gedit,反汇编工具edb,反汇编工具objdump,编译环境gcc等。
1.3 中间结果
hello.i 预处理后的代码
hello.s 编译后的汇编语言代码
hello.o 可重定位目标文件
hello.elf 可重定位目标elf格式
hello_o.txt hello.o的objdump反汇编结果
hello 可执行文件
hello1.elf 可执行目标文件hello elf格式
hello.txt hello的objdump反汇编结果
1.4 本章小结
本章介绍了hello的p2p以及020的简要过程,展示了完成本次大作业所需的各种环境以及实用工具,列出了过程中生成的各种文件及其作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。
作用:
①处理头文件:比如hello.c的第一行的#include<stdio.h>命令告诉预处理器读取系统有文件stdio.h的内容,并把它直接插入程序文本中。
②处理宏定义:对于#define指令,进行宏替换,对于代码中所有使用宏定义的地方使用符号表示的实际值替换定义的符号
③处理条件编译:根据可能存在的#ifdef来确定程序需要执行的代码段。
④处理特殊符号:例如#error等,预编译程序可以识别一些特殊的符号,并在后续过程中进行合适的替换。
⑤删除c语言源程序中的注释部分。
2.2在Ubuntu下预处理的命令
使用命令行键入cpp hello.c >hello.i
图2.1 在Ubuntu下的预处理
2.3 Hello的预处理结果解析
可以发现,原本的源代码文件只有23行,预处理后的文件为3105行,原本的源代码部分在3092行之后,在这之前是hello引用的所有的头文件stdio.h, unistd.h , stdlib.h内容的展开。而很显然我们发现插入的部分不止有这三个头文件的内容,还出现了其他的头文件,这是以为这三个头文件中同样使用#include命令引入了其他的头文件,这些头文件同样出现在了hello.i文件中。插入的库文件的具体信息如下图所示:
图2.2头文件信息
可以发现在源代码头部出现的注释在预处理之后的源代码部分已经不可见,符合前面提到的在预处理过程中预处理器将删除源代码中的注释部分。
2.4 本章小结
本章节主要介绍了在预处理过程中预处理器是如何工作的,从头文件展开到宏替换,再包括删除注释以及条件替换等,同时展示了在Ubuntu系统下hello.c文件的预处理过程以及预处理的结果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:广义的编译是说将某一种程序设计语言写的程序翻译成等价的另一种语言。此处是指利用编译程序从预处理文本文件(.i)产生汇编程序(.s)的过程。
作用:将输入的高级程序设计语言源程序翻译成以汇编语言或机器语言表示的目标程序作为输出。
3.2 在Ubuntu下编译的命令
使用命令行键入:gcc -S -o hello.s hello.i
图3.1 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1常量与变量处理
源程序中有数字常量、字符常量、局部变量int i、局部变量argc、局部变量argv。
首先是数字变量,我们可以通过观察源代码中使用的数字常量发现数字常量一般储存在.text节,如下图:
图3.2 hello.s中的数字常量
然后是字符常量,可以发现在printf等函数中使用的字符串常量是储存在.rotate段的,具体储存情况如下图:
图3.3 hello.s中的字符常量
图3.4 hello.c中的变量
观察下图中汇编代码,可以看出局部变量i存放在栈中-4(%rbp)中,局部变量argc储存在栈中-20(%rbp)位置,局部变量argv储存在栈中。
图3.5 hello.s中的局部变量argc
图3.6 hello.s中的局部变量
图3.7 hello.s中的条件跳转1
3.3.2关系操作符与控制语句的处理
源程序中if(argc!=4)中的!=关系操作符在编译器的转换后变成了如图3.3中所示的汇编语句。在对argc与4进行比较时,同时使用了cmpl与je指令,若两数相等则跳转至.L2处,否则跳过此部继续向下执行。在此例中可以看出关系操作符和控制语句是借助jX指令来实现的。
图3.8 跳转指令表
在for循环中对于循环变量i的判断,当循环变量大于等于9的时候进行条件跳转。
图3.9 hello.s中的条件跳转2
3.3.3算术运算
在hello.s程序中使用了addq和addl指令,这是加法运算,类似的有:
①加: x=x+y汇编语言是addq y,x
②减: x=x-y 汇编语言是subq y,x
③乘: x=x*y 汇编语言是imulq y,x
④除: z=x/y 汇编语言是movq x, z /n cqto /n idivq y
图3.10 hello.s中的四则运算
3.3.4赋值
对循环变量i在循环中的赋值,每次循环结束的时候都对齐进行+1操作,具体的操作汇编代码如下:
图3.11 hello.s中的赋值操作
3.3.5数组操作
观察汇编代码可以发现argv储存的两个值都存放在栈中,argv[1]的储存地址是-24(%rbp),而argv[1]的储存地址是-16(%rbp),对于数组操作的汇编代码如下截图:
图3.12 hello.s中的数组操作
3.3.6函数调用
在hello.s中出现了多个函数调用的情况,在X86系统中函数储存参数时,第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,其余的参数保存在栈中的某些位置。
main函数:
参数:传入参数argc和argv,其中argv储存在栈中,argc储存在%rdi中
返回:在源代码中最后的返回语句是return 0,因此在汇编代码中最后是将%eax设置为0并返回这一寄存器。汇编代码如下:
图3.13 hello.s中的main函数
printf函数:
参数:第一次调用的时候只传入了字符串参数首地址;for循环中调用的时候传入了 argv[1]和argc[2]的地址。
调用:第一次是满足if条件的时候调用使用了puts函数来优化,第二次是在for循环条件满足的时候调用。
汇编代码如下:
图3.14 hello.s中调用puts函数
图3.15 hello.s中调用printf函数
atoi函数:函数以argv[3]为参数。
sleep函数:函数以atoi函数的返回值为参数,这一参数储存在%edi中,这一函数在for循环的条件下被调用,详细汇编代码如下:
图3.16 atoi函数和sleep函数
exit函数:
参数:传入的参数为1,执行退出命令。
调用:当if条件满足的时候调用这一函数。
代码表示如下:
图3.17 hello.s中exit函数的调用
3.4 本章小结
本章主要介绍了在将修改了的源程序文件转换为汇编程序的时候主要发生的变化,汇编代码文件中主要存在的部分以及源代码中的一些主要的操作对应的汇编代码中的汇编代码的展现形式。总的来说,编译器做的就是在进行词义分析和语义分析之后,判断源代码符合语法要求,之后再将其转换为汇编代码。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编就是将汇编语言转化为机器可直接识别执行的代码文件的过程。
作用:汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
使用命令行键入:gcc -c hello.s -o hello.o。
图4.1 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
使用命令行键入:readelf -a hello.o > hello.elf,在目录下会生成一个.elf文件。文件格式如下图所示:
图4.2 .elf文件格式
分析.elf文件中的内容如下:
①ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
图4.3 hello.o的ELF头
②节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
图4.4 hello.o的节头部表
③重定位节:.rela.text保存的是.text节中需要被修正的信息,任何调用外部函数或者引用全局变量的指令都需要被修正,调用外部函数的指令需要重定位,引用全局变量的指令需要重定位,调用局部函数的指令不需要重定位,在可执行目标文件中不存在重定位信息。在此程序中需要被重定位的是printf、puts、exit、sleep和.rodata中的内容。而.rela.eh_frame节是.eh_frame节重定位信息。
图4.5 hello.o的重定位节
④符号表:符号表(.symtab)存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
图4.6 hello.o的符号表
4.4 Hello.o的结果解析
使用命令行键入:objdump -d -r hello.o >hello_o.txt
图4.7 生成hello.o反汇编文件
图4.8 hello_o.txt文件内容
对比hello.s文件与hello_o.txt文件中的内容,发现二者大体内容相近,在反汇编文件中显示的不仅是汇编代码,还有机器指令码。两者区别有下面三点:
①分支转移:hello.s文件中分支转移是使用段名称进行跳转,而hello.o文件中分支转移是通过地址进行跳转的
②函数调用:hello.s文件中,函数调用call后跟的是函数名称;而在hello.o文件中,call后跟的是下一条指令。因为这些函数都是共享库函数,地址是不确定的,因此call指令将相对地址设置为全0,然后在.rela.text节中为其添加重定位条目,等待链接的进一步确定。
③全局变量:hello.s文件中,全局变量是通过语句:段地址+%rip完成的;对于hello.o的反汇编来说,则是:0+%rip,因为.rodata节中的数据是在运行时确定的,也需要重定位,现在填0占位,并为其在.rela.text节中添加重定位条目。
4.5 本章小结
从本章节中可以看出,汇编这一步骤使得hello程序真正开始从文本状态转化为二进制状态,这并不是简简单单地翻译为真正的机器码,而是生成“可重定位的机器码”,重定位这一机制是为了供链接使用的。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种不同文件的代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
作用:把预编译好了的若干目标文件合并成为一个可执行目标文件,使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用命令行键入: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.ohello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o
图5.1 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
使用命令行键入:readelf -a hello > hello1.elf
图5.2 hello可执行程序的.elf文件
①ELF头:hello与hello.o的ELF头大致相同,不同之处在于hello的类型为EXEC可执行文件。
图5.3 hello的ELF头
②节头部表:节头部表是描述目标文件的节,各节的基本信息均在其中进行了声明,包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息等。
图5.4 hello的节头部表
③重定位节:
图5.5 hello的重定位节
④符号表:
图5.6 hello的符号表
5.4 hello的虚拟地址空间
在图5.3中可以看到如下内容:
PHDR:保存程序头表
INTERP:动态链接器的路径
LOAD:可加载的程序段
DYNAMIN:保存了由动态链接器使用的信息
NOTE:保存辅助信息
GNU_STACK:标志栈是否可执行
GNU_RELRO:指定重定位后需被设置成只读的内存区域
使用edb打开hello,调出Data Dump窗口观察hello加载到虚拟地址的状态,查看每段的信息:
图5.7 在edb中查看hello可执行程序
从虚拟地址0x578a5cb0开始载入,到0x578a5ff0结束,每一节的声明与图5.4中一致。
5.5 链接的重定位过程分析
使用命令行键入:objdump -d -r hello >hello.txt
图5.8 hello的反汇编文件
与之前生成的hello_o.txt文件相比,在hello.txt中增加了许多节。在hello_o.txt中只有一个.txt节,仅有一个main函数。而在hello.txt中有.init、.plt、.text三个节,每个节中都包含大量函数,如puts@plt,printf@plt等。这是由于再库函数的代码链接到程序中后,程序的每个节都变得更加完整。
链接的重定位的过程:重定位节和符号定义链接器将相同类型的节合并,生成ELF节。链接器将运行时的内存地址分配给生成的节,此时程序中每条指令和全局变量都有唯一的运行时地址。要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位,修改.text节和.data节中对每个符号的引用,需要用到在.rel_data和.rel_text节中保存的重定位信息。
图5.9 hello.txt内容部分截图
5.6 hello的执行流程
②开始执行:_start、_libc_start_main
③执行main:_main、_printf、_exit、_sleep、_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
④退出:exit
在edb里打开hello可执行文件,程序地址最先会出现在0x7fe145f7a098处,这里是入口点_dll_start的位置。
图5.10 _dll_start的地址
随后程序经过一系列跳转后进入了_start的位置。
图5.11 _start的位置
随后在间接call指令跳到_lib_start_main的位置处,进行初始化,然后调用main函数。
图5.12 _lib_start_main的位置
然后再经过一系列跳转后,回到main函数处,执行main函数内容。
图5.13 执行main函数内容
最后在完成内容执行后调用exit函数终止进程。
图5.14 调用exit函数终止进程
5.7 Hello的动态链接分析
首先通过观察.elf文件中发生变化的节。
图5.15 .elf文件中的.got.plt节
接着通过观察edb,可以发现dl_init后.got.plt节发生的变化。
图5.16 执行init前的地址内容
图5.17 执行init后的地址内容
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时,通过GOT和过程链接表PLT的协作来解析函数的地址。
5.8 本章小结
本章节介绍了链接的概念以及作用,对hello的elf格式进行了详细分析。介绍了hello的虚拟地址后,又分析了hello的重定位过程、执行流程以及动态链接过程,详细阐述了hello.o是如何链接成为一个可执行文件hello的。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是执行中程序的抽象。
作用:在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:①一个独立的逻辑控制流,提供一个程序独占处理器的假象;②一个私有的地址空间,提供一个程序独占地使用内存系统的假象。
6.2 简述壳Shell-bash的作用与处理流程
在Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程),它的基本功能是解释并运行用户的指令,重复如下处理过程:
①终端进程读取用户由键盘输入的命令行。
②分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
③检查第一个命令行参数是否是一个内置的shell命令,如果不是内部命令,则调用fork( )创建新进程/子进程
④在子进程中,用②中获取的参数,调用execve( )执行指定程序。
⑤如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。
⑥如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 2021112314 王嘉乐 1后,shell会处理该命令,如果判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,但拥有不同的PID。
6.4 Hello的execve过程
exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序。因此,与fork一次调用返回两次不同,在exceve只调用一次并从不返回。当加载可执行目标文件后,exceve调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
6.5 Hello的进程执行
内核为每个进程维持一个上下文,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。在内核调度了一个新的进程运行后,他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
上下文切换:①保存当前进程的上下文,②恢复某个先前被抢占的进程被保存的上下文,③将控制传递给这个新恢复的进程。
一个进程执行它的控制流的一部分的每一时间段叫做进程时间片。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式,处理程序运行在内核模式中,当它返回到应用程序代码是,处理器就把模式从内核模式改回到用户模式。
图6.1 进程的上下文切换
6.6 hello的异常与信号处理
输入随机字符串:从运行结果上看,输入随机字符串并不会影响程序的运行,终端依旧会出现相关输入。
图6.2 输入随机字符串
输入ctrl+c:当向hello程序输入ctrl+c后,会导致中断异常产生SIGINT信号,向子进程发出SIGKILL信号终止并回收,进程终止。
图6.3输入ctrl+c
输入ctrl+z:当向hello程序输入Ctrl-Z后,会导致中断异常产生SIGSTP信号,进程被挂起,与输入Ctrl-C结果不同。
图6.4 输入ctrl+z
此时分别输入ps, jobs, pstree, fg, kill指令进行查看相关信息。
①ps:输入ps后可以看出hello进程并未停止而是被挂起。
图6.5 输入ps
②jobs:输入jobs,验证了hello进程被挂起,是处于停止的状态。
图6.6 输入jobs
③pstree:输入pstree,可以通过进程树来查看所有进程的情况。
图6.7 输入pstree
④fg:输入fg指令使第一个后台作业变成前台作业,这里由于hello是第一个后台作业,所以fg会使得hello回到前台并完成运行。
图6.8 输入fg
输入kill:通过ps指令来查看进程列表得知kill指令成功杀死进程hello。
图6.9 输入kill
6.7本章小结
本章阐述了进程的概念和作用,介绍了shell的一般处理流程,并分析了hello进程的执行过程:创建、加载和终止,以及hello的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
①逻辑地址:逻辑地址是指由程序hello产生的与段相关的偏移地址部分。
②线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是段中的偏移地址加上相应段的基地址就生成了一个线性地址。
③虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
④物理地址:物理地址是加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,即逻辑地址。将内存分为不同的段,每个段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。
7.3 Hello的线性地址到物理地址的变换-页式管理
系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
图7.1 页式管理流程图
7.4 TLB与四级页表支持下的VA到PA的变换
以Core i7地址翻译为例,Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA;如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。
图7.2 使用页表的地址翻译流程
7.5 三级Cache支持下的物理内存访问
MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中条目,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则在下一级cache或是主存中寻找需要的内容,储存到上一级cache后再一次请求读取。
图7.3 储存器的层次结构
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:①删除已存在的用户区域;②映射私有区域:为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的;③映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;④设置程序计数器(PC)指向代码的入口点。
7.8 缺页故障与缺页中断处理
在果程序执行过程中如果遇到了缺页故障,则内核调用缺页处理程序。处理程序会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
在hello程序中使用到printf函数,而printf会调用malloc函数,使用由动态内存分配器动态内存分配机制。
动态内存分配器维护进程虚拟地址空间中的的堆区域,它将堆视作一组不同大小的块的集合来维护,每个块是一段连续的虚拟内存碎片,要么是已分配的,要么是空闲的。空闲块保持空闲直至被应用程序分配,以已分配块保持已分配状态直至被释放。分配器需要一些数据结构维护堆块来区分块边界以及区分已分配块和空闲块,这些可以被标识在块的头部,那么分配器可以将堆组织为一个连续的已分配块和未分配块的序列,未分配的块被称为隐式空闲链表。
通过隐式空闲链表,分配器能够通过对于链表的操作以完成在堆上放置已分配的块、分割空闲块、获取额外内存、合并空闲块等操作,应用程序就能够动态地在堆上分配额外内存空间了。
7.10本章小结
本章阐述了计算机中的虚拟内部管理,介绍了虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式;还介绍了段式以及叶式的管理模式;清楚介绍了进程fork、execve时的内存映射、缺页故障与缺页中断处理;最后介绍了动态存储分配管理。
(第7章 2分)
结论
Hello的一生实则就是我们程序员学习掌握计算机系统全篇内容的过程。
①我们首先使用高级语言——C语言编写了hello的源程序hello.c。
②通过预处理器cpp对源程序进行预处理,从而得到了hello.i的扩展文件。
③通过编译器cd编译,得到了hello.s的汇编文件。
④通过汇编器as汇编,得到了可重定位的目标文件hello.o。
⑤将上一步得到的目标文件与其它可重定位目标文件、动态链接库经过链接器ld链接生成可执行文件hello。
⑥调用fork函数生成子进程;调用execve函数加载运行当前进程的上下文,运行新程序hello。
⑦execve 调用启动加载器,加映射虚拟内存,进入程序入口后,程序载入物理内存从而进入 main 函数。
⑧hello最终被shell回收,内核回收为其创建的所有信息。
感悟:在这一学期的学习后,通过老师上课讲述的知识并结合《深入理解计算机系统》这本教材以及实验课自己动手实践的经历,我深刻理解了现代计算机系统的整体框架,并对底层原理有了大致的认识。再过去,我曾认为程序员只需要能够编写输出正确结果的代码就足够了,通过本次课程的学习,我认识到编写出一个更好的,面向不同对象的代码是一件非常有价值的事。即使我并非计算机专业的学生,在学完本次课程后,我对计算机的热情又增加了一份!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c 原始代码
hello.i 预处理后的代码
hello.s 编译后的汇编语言代码
hello.o 可重定位目标文件
hello.elf 查看可重定位目标elf格式
hello_o.txt 查看hello.o的objdump反汇编结果
hello 可执行文件
hello1.elf 查看可执行目标文件hello elf格式
hello.txt 查看hello的objdump反汇编结果
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] Linux下 可视化 反汇编工具 EDB 基本操作知识_linux中edb_hahalidaxin的博客-CSDN博客
[8]关于引用网络参考文献的写法 - 知乎 (zhihu.com)
[9]RANDALE.BRYANT,DAVIDR.O‘HALLARON.Computer Systems:A Programmer’s Perspective.北京:机械工业出版社,2016.7.
(参考文献0分,缺失 -1分)