本文通过展示hello程序的生命周期,深刻揭示了与计算机系统相关的概念、理论、技术与实现,详述了计算机的内部机制。Hello的一生可划分为2个阶段:
- 编译阶段:由高级语言程序编译为可执行程序
- 运行阶段:结合进程管理、存储管理、I/O管理走完它的一生
关键词:预处理;编译;汇编;链接;进程;内存管理;I/O管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:hello.c经cpp、ccl、as、ld处理转换得可执行目标文件hello,在shell中键入./hello 1190200525 刘祥龙,shell1为其fork一个子进程;如此便完成了hello从程序到进程的转变。
O2O:shell调用execve,execve调用启动加载器,映射虚拟内存,设置当前进程上下文中的程序计数器,使之指向程序入口地址;进入程序入口程序后开始载入物理内存,然后进入 main函数执行目标代码。程序运行结束后父进程回收hello,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
·硬件:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
·软件:Windows10 64位,Ubuntu 20.04 LTS 64位
·开发工具:Visual Studio Code,GCC,objdump,EDB,readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i:预处理后的文本文件
hello.s:汇编后的文本文件
hello.o:编译后的可重定位文件
hello.elf:hello.o的elf文件
hello.txt:hello的反汇编代码文本
linkhello.elf:hello的elf文件
linkhello.txt:hello的反汇编代码文本
hello:最终的可执行目标文件
1.4 本章小结
本章主要介绍了hello的P2P与O2O过程,并说明、展示了实验的软硬件平台与中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理阶段,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
C语言目前提供了3种预处理功能:
·宏定义处理
·文件包含处理
·条件编译处理
作用:
(1)用实际值替换符号常量
(2)读取系统头文件的内容,并将其直接插入到程序文本中
(3)处理条件编辑指令,决定需要编译的代码
此过程不会对程序的源代码进行解析,但也会删除程序中的注释及多余的空白字符。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
经预处理后,cpp将源文件hello.c翻译为文本文件hello.i
使用cat指令抓取文件内容后发现:文件内容增加,进行了宏展开,并将头文件中的内容插入到文本文件中;此时仍为可阅读的C语言文本。
2.4 本章小结
本章主要介绍了C语言文件编译的预处理过程;从预处理的概念、作用及预处理结果这3大方面进行介绍。
第3章 编译
3.1 编译的概念与作用
概念:
编译器(ccl)将文本文件hello.i翻译为文本文件hello.s,其包含一个汇编语言程序。
作用:
- (主要功能)将源程序翻译为汇编语言程序。
- 进行语法分析、词法分析及目标代码生成。
- 进行目标程序优化,提高程序性能。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.0 指令及其内容
3.3.1 数据
·整型:
(1)已初始化的全局变量sleepsecs存储在.data节,设置为long型,且值为2
- 立即数:在汇编代码中直接以“$”的形式标记
- Main函数参数argc:存储于栈空间中
·字符串:
以全局变量的形式保存,存储于.rodata节
·局部变量:
局部变量i保存在-4(%rbp)中,即栈中
·全局变量:
3.3.2 赋值
使用mov语句给局部变量i赋值
3.3.3 类型转换
对全局变量sleepsecs的值进行隐式的类型转换(由2.5重新赋值为2)
3.3.4 算术操作
指令 | 功能 |
lea A,B | B=&A |
add A,B | B+=A |
sub A,B | B-=A |
mul A,B | B*=A |
inc A | A++ |
dec A | A-=1 |
neg A | A=-A |
3.3.5 关系操作
使用cmp语句进行关系比较
jmp的条件跳转语句也可进行关系比较
3.3.6 逻辑操作
汇编语言中的逻辑操作由算术与赋值操作实现
3.3.7 数组/指针操作
(1)数组:利用在栈帧中位置,通过(%rax)和%rax+8,分别得到argv[1]和argc[2]两个字符串
- 指针:使用mov指令获得argv[0]指针的地址
3.3.8 控制转移
使用cmp语句设置条件码,根据条件码利用jmp语句进行跳转
3.3.9 函数操作
使用call指令调用函数
3.4 本章小结
介绍了编译的概念与作用,着重分析介绍了汇编指令,并就hello.s中的语句进行了分析。展示了编译器是如何根据C语言不同的数据类型与操作将.i文件翻译为.s文件。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存到目标文件hello.o中(二进制文件)
作用:将汇编代码转换为机器指令,使其在链接后能被机器识别并执行
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello.elf
·elf头:
命令:readelf -h hello.o
Elf头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。Elf头剩余部分包含帮助链接器语法分析和解释目标文件的信息。其中包含elf头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小及数量。
·节头部表
命令:readelf -S hello.o
节头部表记录各节名称、类型、地址、偏移量、大小、全体大小、旗帜、链接、信息、对齐等信息。
·符号表
命令:readelf -s hello.o
符号表存放在程序中定义和引用的函数及全局变量的信息。每个可重定位目标文件在.symtab这都有一张符号表,但其不包含局部变量的条目。
·重定位节
命令:readelf -r hello.o
重定位节.rela.text:
一个.text节中位置的列表。当链接器把这个目标文件和其他文件组合时需要修改这些位置。一般而言,任何调用外部函数或引用全局变量的指令都需要修改,而调用本地函数的指令不需要修改。
偏移量:相对节头的偏移
信息:在符号表中的标号及类型
类型:重定位方式
此程序中需要被重定位的是printf, puts, exit, sleepsecs, getchar, sleep, rodata中的.L0及.L1
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello.txt
·hello.o的反汇编代码:
·hello.s
hello.o与hello.s代码在整体上区别不大;汇编代码及反汇编代码的差异主要体现在以下3个方面:
- 访问全局变量:汇编代码使用.LC0(%rip)访问全局变量,而汇编代码使用0x0(%rip)(因反汇编代码需要重定位)
- 分支转移:汇编代码的分支跳转直接以助记符的形式表示,而反汇编中直接跳转到确定的地址
- 函数跳转:汇编代码中的函数跳转形式为:call+函数名;而反汇编代码中call后直接指向下一条指令,且以main+偏移量的方式进行跳转。
分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
4.5 本章小结
介绍了汇编阶段的概念及作用,展示了hello.s到hello.o的汇编过程,介绍如何使用readelf工具观测、分析elf文件;利用objdump得到hello的反汇编代码,并与其汇编代码进行比较。充分展示了汇编代码与反汇编代码的异同。
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可执行于编译时、加载时、运行时。在现代系统中,链接由叫链接器的程序自动执行。
作用:
(1)使得分离编译成为可能。不必再将一个大型的应用程序组织为一个巨大的源文件,而是可以将其分解为更小、更好管理的模块,可以独立的修改、编译这些模块。
(2)提高效率。当需要改变模块中的一个时,只需简单的重新编译它,并重新链接应用,而不必重新编译其他文件
(3)可利用共享库
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.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello > linkhello.elf
ELF头:
相较于可重定位目标文件的elf头:
·类型变为可执行文件
·可执行目标文件的elf头中节头数量增长至27
·程序头大小变为56
·指明程序入口点地址
文件头信息:
·.ref节已完成重定位,偏移量offset已给出,虚拟地址的起始地址已给出
重定位节:
·完成重定位
符号表:
·条目增加至51条
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
通过data dump查看加载到虚拟地址的程序代码知hello的虚拟地址空间开始于0x400000,结束于0x400ff0
与5.3对照知edb中观察到的每个节的地址与程序头中所对应的address均相同。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令:objdump -d -r hello > linkhello.txt
Hello反汇编
Hello.o反汇编
Hello反汇编
·不同点:
- 指令地址不同;hello反汇编代码具有确定的虚拟地址,而hello.o的反汇编代码不具有确定的虚拟地址。(hello中函数调用、跳转的地址均变成虚拟地址)
- Hello中增加了.init与.plt节及相应的部分函数
- Hello中无重定位条目,而hello.o反汇编代码中含重定位条目
- Hello中对全局变量的访问经过重定位,而hello.o中对全局变量的访问是$0x0及0(%rip)。
·链接过程:
链接器:(1)符号解析(2)重定位
静态链接:目标文件直接链接进入可执行程序
动态链接:在程序启动后才动态加载目标文件
·hello重定位详述:(合并输入模块,并为每个符号分配运行时地址)
- 重定位节和符号定义:
- 链接器将所有相同类型的节合并为同一类型的新的聚合节
- 链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号
- 此步骤完成后,hello程序中的每条指令和全局变量都有唯一的运行时内存地址
- 重定位节中的符号引用
链接器依据重定位条目修改代码节和数据节中对每个符号的引用,使它们指向正确的运行时地址。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子程序名 | 地址 |
_dl_init | 0x7f944cc65c10 |
hello!_start+0 | 0x00000000004010d0 |
libc-2.31.so!__libc_start_main+0 | 0x00007f944ca71fc0 |
libc-2.31.so!__cxa_atexit+0 | 0x00007f944ca94f60 |
hello!__libc_csu_init+0 | 0x0000000000401190 |
libc-2.31.so!_setjmp+0 | 0x00007f944ca90e00 |
hello!main+0 | 0x0000000000401105 |
hello!puts@plt+0 | 0x0000000000401030 |
hello!exit@plt+0 | 0x0000000000401060 |
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
dl_init前:
dl_init后:
·在进行动态链接前先进行静态链接,生成部分链接的可执行目标文件hello,此文件中有对定义在共享库中的例程和数据的未解析的引用
·在加载时,加载器将部分链接的可执行文件映射到内存
·调用动态链接器,通过加载共享库和重定位程序中的引用来完成链接任务。
被编译为位置无关代码的共享库可以加载到任何地方,也可在运行时被多个进程共享。为了加载、链接和访问共享库的函数和数据,应用程序也可在运行时使用动态链接器。
5.8 本章小结
详细介绍了链接的概念及作用,展示并分析了hello的elf文件。使用edb查看了hello的虚拟地址空间,详述了链接过程,尤其是重定位过程;最后展示了hello的执行流程及动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是又程序正确运行所需的状态组成。此状态包括存放在内存中程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量及打开文件描述符的集合。
作用:
·一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器
·一个私有的地址空间,提供一个假象,好像我们的程序独占的使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
定义:交互型的应用级程序,代表用户运行其他程序;shell执行一系列读、求值步骤,然后终止。
功能:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。
处理流程:
·从终端读入输入的命令。
·将输入字符串切分获得所有的参数
·如果是内置命令则立即执行;否则调用相应的程序、为其分配子进程执行
·shell 应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
创建:父进程通过调用fork函数创建一个新的、处于运行状态的子进程
·子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(包括代码和数据段、堆、共享库及用户栈)
·子进程获得与父进程任何打开文件描述符相同的副本(共享文件)
·子进程有不同于父进程的PID
·父子进程并发执行、具有相同但是独立的地址空间
·fork函数调用一次返回两次(一次返回父进程,一次返回到新创建的子进程)
hello的fork过程如下:
·在shell输入:./hello 1190200525刘祥龙
·由于./hello不是shell内置命令,而是经过编译系统的可执行文件;故shell为当前进程fork一个子进程
·将hello载入内存,开始执行
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
·创建子进程后,execve函数在当前进程的上下文中加载并运行一个新程序,此即hello程序。
·hello程序通过调用execve函数调用加载器
·加载器将可执行目标文件hello的代码和数据从磁盘复制到内存,然后通过跳转到程序的第一条指令或入口点来运行该程序。
(具体而言:
(1)加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆 和栈段。
(2)新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行 文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。
(3)最后加载器设置 PC 指向_start 地址,_start 最终调用 hello 中的 main 函数。
)
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
·进程上下文信息:
上下文就是内核重新启动一个先前被抢占了的进程所需的状态。此状态包含存放在内存中的程序的代码及数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、打开文件描述符的集合。
·调度:
在程序运行的某系时刻,内核决定抢占当前进程,并重新开始一个先前被抢占进程。
在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
·上下文切换:
- 保存当前上下文
- 恢复先前某个被抢占的进程被保存的上下文
- 将控制传递给中国新恢复的进程
·时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
·用户模式与内核模式:
处理器用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令及它可以访问的地址空间范围。
当设置了模式位时,进程就运行在内核模式中。
未设置模式位时,进程就运行在用户模式中。
·hello的进程执行过程:
最初hello运行在用户模式下,输出“Hello 1190200525 刘祥龙”
hello调用sleep函数、陷入内核模式、处理休眠请求(定时器开始计时),内核将控制通过上下文切换机制转移给其他进程(进入用户模式)。
定时器发送中断信号,进入内核模式,内核中中断处理程序处理中断,并再次进行上下文切换,将控制返回给hello。
在用户状态下,继续执行hello程序。
(hello在调用getchar函数时,也会执行上下文切换)
总而言之:
在hello的运行过程中,CPU不断切换上下文,使运行过程被切分成时间片,hello将与其他进程交替占用CPU,以实现进程的调度。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello程序执行过程可能出现的异常一共有四种:中断、陷阱、故障、终止。
·中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。
hello程序执行过程中可能会出现外部I/O设备引起的异常
·陷阱:有意的异常,同步发生,总是返回到下一条指令。
hello调用getchar函数时会触发此异常
·故障:潜在可恢复的错误,同步发生,可能返回到当前指令或终止。
hello程序运行时可能会出现缺页异常的情况
·终止:不可恢复的错误,同步发生,不会返回。
hello程序运行过程中硬件系统发生的一些错误可能会导致终止
- 正常执行hello程序:
- Ctrl+c 终止:
键入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,(默认)终止前台作业
- Ctrl+z 暂停:
键入Ctrl+z将(默认)挂起前台的作业,hello进程此时并没有被回收,而是运行在后台
- 程序运行过程中乱按键盘:
无关输入被缓存到stdin,并随着printf指令被输出。
(5)输入ps打印当前进程的状态
(6)输入jobs列出当前作业
- 输入pstree打印进程树
- 输入fg 1,继续执行前台进程1
- 输入kill,杀死hello
6.7本章小结
详细介绍了进程的概念、功能与作用,并展示了shell的处理流程。与此同时,详述hello程序调用fork创建新进程、调用execve函数加载、执行hello的过程。着重展示了hello的进程执行过程及hello的异常及信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
·逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。表示为 [段标识符:段内偏移量]。hello.o里面的相对偏移地址即是逻辑地址。(又称相对地址)
·线性地址:逻辑地址经过段机制转化后为线性地址,用于描述程序分页信息的地址。(是逻辑地址与物理地址间转化过程中的一个环节)以 hello 为例,线性地址表明 hello 应该在内存的哪些块上运行。
·虚拟地址:为了方便加载与链接,将物理地址映射为虚拟地址。虚拟内存为每个进程提供了一致的地址空间,即虚拟地址空间,CPU在虚拟地址空间上的生成的地址即为虚拟地址。比如在将hello.o链接的时候hello.o文件中的main函数的地址即是虚拟地址。
·物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址(又称绝对地址);是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址直接成为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址实际是由48位组成的,前16位包括段选择符,后32位为段内偏移量;段选择符前13位为索引,通过索引(依据TI取值)在描述符表(GDT:全局描述符表;LDT:局部描述符表)中找寻到32位段基地址,段基地址与段内偏移量相加即得线性地址。(32位)
7.3 Hello的线性地址到物理地址的变换-页式管理
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),和一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,(eg.VPN 0选择PTE 0,VPN 1选择PTE 1)。根据PTE中有效位的信息可知虚拟页是否被缓存,如果虚拟页已缓存,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。(此处VPO和PPO相同)。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,VA传送给 MMU,MMU使用前36位VPN作为TLBT(TLB标记)+TLBI(TLB索引)访问、查询TLB中是否缓存有相应的PTE;如果TLB命中,则得到 PPN,与VPO组合获得PA。 如果 TLB未命中,MMU 向页表中查询,CR3包含L1页表的物理地址,VPN1提供一个L1 PTE的偏移量,这个PTE包含L2页表的基地址,VPN2提供一个L2 PTE的偏移量,以此类推,最终在第四级页表中查询到PPN的值,与VPO组合获得PA,并且向TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则将引发缺页故障。
7.5 三级Cache支持下的物理内存访问
·CPU产生虚拟地址VA。
·MMU使用VPN在TLB中找寻PTE,若TLB命中,得到PPN与VPO组合得到PA;若TLB不命中,利用VPN的多级页表机制到内存中找到对应的物理页号PPN,与VPO组合得到PA。
·PA分为PPN和PPO两部分。利用PPO,将其分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN即是缓存组标志CT,利用CI进行组索引,将CT与标志位进行比较,若相同(即命中)则把偏移量CO处的数据字节传给MMU,随后将其传给CPU;不命中时则访问下一级缓存。
·先访问一级缓存,不命中时访问二级缓存,再不命中访问三级缓存,再不命中访问主存,如果主存缺页则访问硬盘。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并为它分配一个唯一的PID。为给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
·删除已存在的用户区域。
·映射私有区域。 为新程序的代码、数据、bss和栈区域创建新的结构区域。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text区和.data区。Bss区域是请求二进制零的,初始长度为0。
·映射共享区域。 若hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
·设置PC。使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:(即DRAM缓存不命中)
当指令引用一个相应的虚拟地址,而与该地址相对应的物理页面不在内存中,会触发缺页异常。
缺页异常将调用内核中的缺页异常处理程序,将执行以下操作:
·判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。
·判断内存访问是否合法,若访问是不合法的,那么缺页处理程序会触发一个保护异常,终止这个进程。
·若操作合法,选择一个牺牲页面,如果这个牺牲页面未被修改过,那么就将它交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
方法:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆;分配器将堆视为是一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。
已分配的块显式的保留为供应用程序使用。空闲块可用来分配。一个已分配的块保存已分配的状态,直至它被释放。一个空闲的块将保持空闲,直到它显式的被应用所分配。
策略:
·显式分配器:要求应用显式地释放任何已分配的块
·隐式分配器:分配器检测到一个已分配块何时不再被程序所使用,而自动释放这个块。
显式分配器约束条件:
·处理任意请求序列
·立即响应请求
·只使用堆
·对齐块
·不修改已分配的块
目标:
·最大化吞吐率
·最大化内存利用率
堆中块组织形式:
·隐式空闲链表:
空闲块通过头部中的大小字段隐含的连接着的。分配器可以通过遍历堆中所有的块,从而间接的遍历整个空闲块的集合;需要以特殊标记结束块。
块放置策略:
- 首次适配 (2)下一次适配 (3)最佳适配
块合并策略:
(1)立即合并 (2)推迟合并
·显式空闲链表:
将空闲块组织为某种形式的显式数据结构。将堆组织成一个双向空闲链表,在每个空闲块中,都包含一个先驱与后继指针。
块排序策略:
- 用后进先出的顺序维护链表 (2)按地址顺序维护链表
空闲链表分离策略:
(1)简单分离存储 (2)分离适配 (3)伙伴系统
7.10本章小结
从以下几个方面介绍了 hello 的存储管理:
- 存储器的4种地址空间、
- 逻辑地址、虚拟地址、物理地址的转换流程及管理模式(段式管理、页式管理)
- 基于TLB的地址翻译、物理内存访问
- hello 进程 fork 时及execve 时的内存映射
- 缺页故障与缺页中断处理的流程
- 动态存储分配管理
其中详细展示了(3)(6)模块的内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
一个Linux文件就是一个m字节的序列:
所有的I/O设备(例如网络、磁盘、终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
设备管理:unix io接口
将设备优雅的映射为文件,允许Linux内核引出一个简单、低级的应用接口
8.2 简述Unix IO接口及其函数
Unix IO接口使得所有的输入、输出都能以一种统一且一致的方式来执行:
·打开文件
·shell创建的每个进程开始时都有3个打开的文件:标准输入、标准输出、标准错误
·改变当前的文件位置
·读写文件
·关闭文件
相关函数:
·进程通过调用open函数来打开一个已存在的文件或创建一个新文件:
Open函数将filename转换为一个文件描述符,并返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。Flags参数指明了进程打算如何访问这个文件。
·接下来,下面的代码片段创建一个新文件,该文件的拥有者有读写权限,而所有其他的用户都有读权限:
umask(DEF_UMASK);
fd = Open(“foo.txt”, O_CREAT|O_TRUNC|O_WRONLY, DEF_MODE);
·最后,进程通过调用close函数关闭一个打开的文件。
int close(int fd);
·应用程序分别调用read、write函数来执行输入、输出。
·通过调用rio_readn和rio_writen函数,应用程序可以在内存和文件之间直接传送数据。
#include “csapp.h”
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
返回:若成功则为传送的字节数,若为EOF则为0(只对rio_readn而言),若出错则为-1
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
·从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
·字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
·显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
Printf函数代码如下:
Printf函数接受一个格式字符串 fmt,之后是一个变参列表。后面每一个参数均
对应这格式字符串中的一个格式符。Printf函数分别调用两个函数:vsprintf 和 write。
vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。在printf中调用系统函数write(buf,i)将长度为i的buf输出。
故printf 的实现就是:用参数匹配格式字符串,然后用 vsprintf 将结果字符串整理,最后用 write 函数输出。
vsprintf函数:
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar函数源代码:
getchar函数调用系统函数read,该函数将整个缓冲区读入到buf中,后取出缓冲区内第一个字符作为结果。
8.5本章小结
本章主要介绍了IO设备管理方法、Unix I/O接口及其函数;同时也对printf函数与getchar函数的实现进行了简要分析.
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello的一生曾这样走过:
·编程:在codeblocks或virtual studio环境下编写、开发hello的C语言代码,生成hello.c文件
·预处理:cpp根据以字符#开头的命令,修改原始C程序;读取并将系统头文件的内容直接插入程序文本、生成hello.i文件
·编译:ccl将.i文件翻译为.s文件,其包含一个汇编语言程序
·汇编:as将hello.s文件翻译为机器语言指令,将这些指令打包装入可重定位目标程序hello.o
·链接:ld将可重定位目标文件hello.o与动态链接库链接成可执行目标文件hello,hello可被加载到内存中由内存执行。
至此,Hello经过编译系统,实现了从高级语言程序到可执行语言程序的转变。
接下来,hello将开启新的篇章。
·运行:在shell上键入:./hello 1190200525 刘祥龙
·创建子进程:由于在终端键入的非shell内置命令,故将调用fork函数创建一个子进程。
·加载:shell调用execve,execve调用启动加载器,映射虚拟内存,设置当前进程上下文中的程序计数器,使之指向程序入口地址;进入程序入口程序后开始载入物理内存,然后进入 main函数。
·访存与缺页:CPU生成虚拟地址,CPU上的MMU将联合TLB将虚拟地址翻译为物理地址,此时可能会触发缺页故障,将调用异常处理程序。
·执行指令:在一个时间片内进程享有CPU资源,可执行自己的逻辑控制流
·上下文切换:在hello的运行过程中,CPU不断切换上下文,使运行过程被切分成时间片,hello将与其他进程交替占用CPU,以实现进程的调度。
·信号处理:键入Ctrlz挂起当前进程,键入Ctrlc终止当前进程。
·动态内存申请:当hello程序执行printf函数时,将调用 malloc函数向动态内存分配器申请分配堆中的内存。
·结束:当子进程hello执行完成时,内核安排父进程回收子进程,将子进程退出状态传递给父进程。内核删除为此进程创建的所有数据结构。
至此,hello走到了生命的尽头,结束了它波澜壮阔的一生。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i:预处理后的文本文件
hello.s:汇编后的文本文件
hello.o:编译后的可重定位文件
hello.elf:hello.o的elf文件
hello.txt:hello的反汇编代码文本
linkhello.elf:hello的elf文件
linkhello.txt:hello的反汇编代码文本
hello:最终的可执行目标文件
为完成本次大作业你翻阅的书籍与网站等
- https://blog.csdn.net/weixin_36277197/article/details/116987253? C语言中编译预处理命令的作用有哪些,C语言系列——预处理命令
- https://blog.csdn.net/Hanani_Jia/article/details/81735517? gcc编译程序四个阶段 预处理、编译、汇编、链接
- https://blog.csdn.net/qq_14892521/article/details/103460609? 连接过程详述
- https://blog.csdn.net/qq_42192672/article/details/82937667? 初识edb debugger
- https://blog.csdn.net/genghaihua/article/details/89450057? 物理地址和逻辑地址
- https://blog.csdn.net/mzjmzjmzjmzj/article/details/84713351? 通俗理解CPU中物理地址、逻辑地址、线性地址、虚拟地址、有效地址的区别
- https://blog.csdn.net/Pipcie/article/details/105670156? 段页式访存——逻辑地址到线性地址的转换
- 兰德尔E.布莱恩特,大卫R.奥哈拉伦 深入理解计算机系统(第三版)