大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L021406
班 级 2003002
学 生 马铭
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文主要通过观察hello.c程序在Linux系统下的生命周期,探讨hello.c源程序的预处理、编译、汇编、链接、生成可执行文件并运行的主要过程。同时结合课本中所学知识详细说明系统是如何实现对hello程序的进程管理,存储管理和I/O管理。通过对hello.c程序的生命周期的探索,让我们对可执行文件的生成和执行以及其它相关的计算机系统的知识有更深的理解。
关键词:预处理,编译,汇编,链接,加载,进程管理,存储管理,I/O管理
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(Program to Process):Hello程序是从一个源程序开始的,通过编辑器创建并保存的文本文件,名为Hello.c;源程序通过预处理器(cpp)将头文件的内容直接插入程序文本,得到Hello.i文本文件,称为预处理阶段;编译器(cc1)将文本文件Hello.i翻译成文本文件Hello.s,包含一个汇编语言程序,此阶段称为编译阶段;接下来的汇编阶段利用汇编器(as)将Hello.s翻译成机器语言指令,打包成一种角可重定位目标程序的格式保存在二进制文件Hello.o中;最后进入链接阶段,连接器(Id)将调用标准库函数的预编译文件例如printf.o合并到Hello.o中,得到Hello可执行文件。Hello被加载到内存中友系统执行。
020(Zero-0 to Zero-0):shell程序通过execve加载并执行hello并映射虚拟内存,进入程序入口后,程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
X64 CPU;2.30GHz;16G RAM;512 GHD Disk
1.2.2 软件环境
Windows11 64位;Vmware 15.5.6;Ubuntu 20.04 LTS 64位
1.2.3 开发工具
Visual Studio 2022 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc;edb1.0.0
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 | 文件作用 |
hello.c | 源程序 |
hello.i | 源程序预处理后的文件 |
hello.s | 源程序编译后的汇编文件 |
hello.o | 汇编文件汇编后的可重定位文件 |
hello | hello.o链接后的可执行文件 |
1.4 本章小结
本章主要介绍了hello.c从源程序到执行的P2P过程,再从执行到消亡的020大致过程,并且对过程中的文件进行了简单介绍,为下述篇章做出概括与铺垫。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在对源程序进行编译之前,通过预处理器(cpp)先对源程序中的预处理命令(主要是以#开头的指令,包括宏定义命令、文件包含命令和条件编译命令 )进行系统文件文本的插入。例如我们最熟悉的#include<stdio.h>就是告诉预处理器读取系统头文件stdio.h。
作用:
1.实现条件编译,实现部分代码的在某些条件下的选择性编译。
2.实现宏定义,在预处理阶段用定义的实际数值将宏替换。
(优点:代码复用,提高性能 缺点:无安全检查,容易出错)
3.实现头文件引用,将头文件的内容复制到源程序中以实现引用,较为便捷,且可以缩小源程序的大小。
4.实现注释,将c文件中的注释从代码中删除。
5.实现特殊符号的使用。如处理#line、#error、#pragma等。
2.2在Ubuntu下预处理的命令
结果如下:
3.3 Hello的编译结果解析
3.3.0c语言源程序截图
3.3.1汇编语言中的数据分析
字符串:字符串只存储在只读存储段上,不可变。
局部变量i:函数中只有一个局部变量,被放在栈中,利用%rbp的偏移量来访问。
主函数传递的参数argc、argv:这两个参数初始时分别存储在%edi和%rsi上,并在主函数中将其压栈,同样通过%rbp的偏移量来访问。其中argc在-20(%rbp)中,而argv在-32(%rbp)中。
从主函数中argc!=4和下面的指令可以判断出:
数组:数组的实现也是通过偏移量完成:
3.3.2汇编语言中的语句分析之赋值
在循环的开始将i赋值为0,是通过下述实现:
在汇编代码中,有时为了传递地址会使用lea指令:
3.3.3汇编语言中的语句分析之运算
i++是通过add函数实现的:
除了c语言中的算数,还有对地址的加减,如sub函数:
3.3.3汇编语言中的语句分析之比较与循环
比较语句有argc!=4和i<8这两个,语句分别为:
而循环则也是通过上述的j**函数进行跳转,实现循环。
3.4 本章小结
本章介绍了.i文件被编译为汇编命令的过程。阐述了c语言中各指令在汇编命令如何完成,加深了我理解高级语言例如c语言到汇编语言之间的转化过程。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编就是汇编器(as)将.s文件中的各种汇编指令翻译成机器语言生成后缀名为.o的可重定位目标文件的过程。
作用:将由源程序得到的高级语言转化成的汇编代码转化成机器能读得懂的机器语言,是程序能够执行的基础。
4.2 在Ubuntu下汇编的命令
结果如下:
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.3.0生成elf文件
在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
在ubuntu下打开.elf文件,并且进行分析:
4.3.1ELF头
ELF头(ELF header)以一个16字节的序列开始,称为魔数。这个数描述了生成该文件的字节顺序和系统的字的大小,在程序执行时会检查魔数是否正确,如果不正确则拒绝加载。ELF头剩下的部分告诉了我们关于文件的信息,如ELF头的大小、目标文件的类型(Type)、机器类型(Machine)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。
4.3.2节头表
节头表提供了每个节的名称、地址、偏移量、全体大小、旗标、链接、信息、对齐、类型、读、写、执行权限以及对其方式。符号表中存放着我们定义和引用的全局变量和函数。同样由于还未进行链接重定位,偏移量Value还都是0。
4.3.3重定位节
保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位。本程序中需要重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
调用局部函数的指令不需要重定位,可执行文件中不包含重定位节。
4.3.4符号表
.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表。.symtab符号表不包含局部变量的条目。同样由于还未进行链接重定位,偏移量Value还都是0。
4.4 Hello.o的结果解析
反汇编命令:objdump -d -r hello.o > hello1.txt
反汇编的结果如下:
与hello.s比较发现以下差别:
- 分支转移:在汇编代码中,分支跳转是直接以.L0 .L1等符号表示;但在反汇编代码中,分支转移通过跳转到以主函数地址为基址的一个偏移地址中,如
jle 38 <main+0x38> 来实现的。
- 函数调用:汇编代码中函数调用时直接个函数名称,而在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令),即用具体的地址表示。
- 访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip)。
- 立即数的表示:汇编代码中的立即数是10进制的,而反汇编代码中的立即数是16进制的。
4.5 本章小结
本章介绍了程序的汇编过程,分析了一个可重定位目标文件中的内容,并对比了汇编与反汇编代码有何异同,加深了从汇编语言如何翻译成机器语言的转换处理的理解。充分理解这一章的内容能够帮助我们理解链接的过程。
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接(linking)通过连接器(ld)是将各种代码,包括标准库代码,和数据片段收集并合成为一个单一文件的过程,这个文件可被加载到内存并执行。
链接的作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
5.2 在Ubuntu下链接的命令
结果如下:
5.3 可执行目标文件hello的格式
5.3.0获得ELF文件
命令:readelf -a hello > hello1.elf 结果如下:
5.3.1ELF头
对比hello.elf我们可以发现以下几点不同:
1.文件的类型(Type)不同,可执行文件的类型不再是REL而是EXEC。
2.,由于库文件的链接,程序的入口点变化,使得main不再是从0x0开始。同理节头的开始位置也发生了变化。
3.节头的数量产了变化,从原来的14变成了27个。
5.3.2节头表
与hello.elf不同,每个节的地址不再是0,这是可执行文件经过了重定位后的结果。
5.3.3重定位节
可以看出与hello.elf已经完全不同了。
5.3.4符号表
多出了.dynym节。这里面存放的是通过动态链接解析出的符号,这里我们解析出的符号是程序引用的头文件中的函数。
5.4 hello的虚拟地址空间
在view下选择memory region,将范围选择0x400000.
使用edb查看可以发现程序是从0x400000开始的,和ELF头中相匹配。
从节头表可以看出程序的入口在0x4010f0,,此时对应.text的起始地址,对应edb中的截图如下:
根据节头表中的各个节的位置信息可以找到各个节在内存中的位置。比如.interp节的起始位置为0x4002e0。通过edb查看如下:
由上图可以看出0x4002e0位置处放的正是动态链接器的路径名。
5.5 链接的重定位过程分析
通过命令objdump -d -r hello > hello2.txt得到hello文件的反汇编代码。
我们通过比较hello和hello.o的反汇编代码,可以得到如下不同:
1.hello.o的反汇编代码的地址从0开始,而hello的反汇编代码从0x400000开始。
这说明hello.o还未实现重定位的过程,每个符号还没有确定的地址,而hello已经实现了重定位,每个符号都有其确定的地址。
2.hello中除了main函数的汇编代码,还有很多其它函数的汇编代码。
其中_init是程序初始化需要执行的代码,.plt是动态链接的过程链接表。由此也可以看出重定位完成。
3.对于跳转,返回指令的地址hello中已经有了明确的数据(PC相对或者是绝对),而hello.o中的地址位置全为0。
由此可以看出链接是会为地址不确定的符号分配一个确定的地址,而在该符号的引用处也将地址改为确定值。
4.在.o文件中分支跳转是通过偏移量实现的,但是在.out文件中分支跳转时采用直接跳转到具体的地址实现的。
hello的重定位过程:
当汇编器生成目标模块时,它并不知道代码和数据最终在内存空间中的地址。它也不知道任何本模块引用的函数或全局变量的地址。所以每当汇编器遇到了一个最终位置未知的对象引用时,它就会生成一个重定位实体,来告知链接器如何在整合可重定位目标文件的时候修改这个引用。代码中的重定位实体放置在.rel.text段中。数据中的重定位实体放置在.rel.data中。下图为ELF重定位实体的格式:
下图为hello.o反汇编代码中的一节,从中可以看出该重定位采用PC相对寻找。
通过查询hello的ELF文件内容得到main函数地址为0x401125,.rodata的地址为0x402008。将数据带入运算可得0xec3。
将结果与hello的反汇编代码比较,发现结果正确。
同理可以得到其他重定位条目。
5.6 hello的执行流程
hello的执行流程:
1.开始执行:_start、_libc_start_main
2.执行main:_main、_printf、_exit、_sleep、_getchar
3.退出:exit
程序名称 | 程序地址 |
_start | 0x4010f0 |
_libc_start_main | 0x4004a0 |
_main | 0x401125 |
_printf | 0x401040 |
_exit | 0x401070 |
_sleep | 0x401080 |
_getchar | 0x401050 |
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
延迟绑定是通过GOT和PLT实现的,根据hello ELF文件可知,GOT起始表位置为0x404000如图:
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用dl_init之后的.got.plt:
可以看到.got.plt的条目已经发生变化。
5.8 本章小结
本章主要介绍了链接的概念和作用,详细介绍了hello.o是如何链接生成一个可执行文件的。同时展示了可执行文件中不同节的内容。最后分析了程序是如何实现的动态链接的。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
进程的作用:向用户提供了一种假象。 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序。
其基本功能是解释并运行用户的指令,重复如下处理过程:
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork( )创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回。
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
当在shell上输入./hello命令时,命令行会首先判断该命令是否为内置命令,如果是内置命令则立即对其进行解释。否则将其看成一个可执行目标文件,再调用fork创建一个新进程并在其中执行。
当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程有不同的PID。并且父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用execve函数在当前子进程的上下文加载并运行一个新的程序。
Execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有出错时才返回调用程序,否则execve调用一次且不返回。
execve加载并运行需要以下步骤:
1.删除已存在的用户区域
2.创建新的区域结构:
A.私有的、写时复制
B.代码和初始化数据映射到.text和.data区
C.bss和栈堆映射到匿名文件 ,栈堆的初始长度0
3.共享对象由动态链接映射到本进程共享区域
4.设置PC,指向代码区域的入口点。Linux根据需要换入代码和数据页面
6.5 Hello的进程执行
1.上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
2.上下文切换:操作系统内核使用一种称为上下文切换的异常控制流来实现多任务。当内核选择一个新的进程运行时,称为内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。同时进行这些操作:
A.保存当前进程上下文.
B.恢复某个先前被抢占的进程被保存的上下文.
C.将控制传递给这个新恢复的进程。.
3.进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
4.用户态和内核态:处理器通常用某个控制寄存器的一个模式位,来提供用户模式/内核模式这一机制的功能。没有设模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
在hello进程执行时,在进程调用execve函数之后,进程会为hello分配内存。一开始hello运行在用户模式下hello就调用了sleep函数,进程陷入内核模式,内核会请求释放当前进程,将hello进程移出运行队列加入等待队列,这时计时器开始计时,内核也进行上下文切换将当前进程的控制权交给其他进程。根据我们选择的时间,会发送一个中断信号,此时又进入内核状态执行中断处理,将hello进程重新加入运行队列,hello就继续执行自己的控制逻辑流。
6.6 hello的异常与信号处理
异常可以分为以下几类:
接下来我们测试hello。
1.正常运行程序:
2.在执行过程中乱按字母:
3.在执行过程中按回车键:
我们会发现按得回车数量等同于在正常执行后按的回车数。回车键在getchar后会得到一个\n的字符。
4.在执行hello时,若使用键盘输入ctrl+c则会使该进程停止,内核发送停止前台进程组的信号。因此未循环8次就退出了。用ps查看前台进程组,可以发现其中并没有hello进程。
5.在执行过程中按ctrl+z作为输入:
在第三条输出使ctrl+z的挂起前台的作业,此时hello进程并没有回收,转到了后台,用ps命令证实。此时他的后台job号是1,所以可以调用fg 1命令将hello进程再调回前台。此时,shell先打印hello的命令行命令,hello则继续运行剩余未完成的内容,继打印了后5条。
6.7本章小结
本章阐述了进程的概念及作用,介绍了shell的一般处理流程及作用,以及fork和execve函数是如何创建新进程、调用进程的。还分析了hello执行过程中会遇到的异常信号及处理方法。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为(段标识符:段内偏移量)。
线性地址:连续的虚拟地址。
虚拟地址:逻辑地址在转换成物理地址之前需要先转换成虚拟地址。不能用来直接访存,需要使用MMU翻译成物理地址。
物理地址:出现CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。索引号对应的是段描述符表的位置,其中的Base字段描述了一个段的开始位置的线性地址。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
首先看段选择符的T1=0还是1,声明当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。使用段选择符中的前13位,在这个数组中查找到对应的段描述符,即可得到它的基地址,基地址Base + offset即要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
在一级页表的地址翻译中,虚拟地址位数为48位,物理地址为52位。在四级页表的情况下,36位的VPN被划分为4个9位的片,每个片被用作一个到页表的偏移量。CR3寄存器包含L1页表的物理地址,VPN1提供一个到L1 PTE的偏移量,这个PTE包含L2页表的基地址,以此类推。
具体实现过程是:CPU 产生虚拟地址传送给至MMU,MMU 使用前 36 位 VPN向TLB匹配。若命中,则得到物理页号与虚拟页偏移(VPO)组合成物理地址。如果 TLB没有命中,就要向页表中查询所需要的物理页号,CR3确定第一级页表的起始地址,最终在第四级页表中查询物理页号,与虚拟页偏移组合成物理地址,并且向TLB中添加条目。若PTE不在物理内存中,则产生缺页故障。
7.5 三级Cache支持下的物理内存访问
L1的Cache有64组,所以组索引位有6位,每组有8个高速缓存行,而每块的大小为64B,所以块偏移也有6位。因此标志位有40位。
1.在得到物理地址后,对8路的块分别匹配CT进行标志位匹配。若匹配成功且块的标志位valid为1,则判定为命中。
2.然后根据块偏移得知我们要的字节在块中的具体位置,然后就可以直接取用该字节内容返回给CPU。
3.如果高速缓存没命中,则要从存储层次结构的下一层去找被要求的块。L1没命中就到L2中找,L2中没命中就到L3找,L3也没没命中就去主存里找。然后将新的块存储在组索引位所指示的组中的一个高速缓存中。
4.映射到的组内有空闲块的,就直接放置,若组内无空闲块,则产生冲突,与最近最少使用的块进行替换。
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个后来进行写操作时,写时复制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
当创建了一个子进程之后,子进程调用execve函数在当前子进程的上下文加载并运行一个新的程序。
Execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有出错时才返回调用程序,否则execve调用一次且不返回。
在书上的第9章我们了解到execve加载并运行需要以下步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
因为程序运行时,不会将所有的数据都加载到内存中,而是放在磁盘上等需要的时候再加载进入内存。如果程序运行需要的数据还未从磁盘上加载进入内存,也就是MMU查找PTE时没有找到对应的地址,此时就会引发缺页故障异常,而缺页故障程序会根据当前内存状态,选择牺牲内存中的一个内存页,然后把磁盘上的物理页加载进入内存中。缺页异常处理完后,会跳转到引发缺页故障的那条指令重新执行。
7.9动态存储分配管理
在C语言中,动态内存时采用显示分配器(malloc,free)实现的,在程序中调用malloc申请内存(在堆中),在分配完的内存前后会额外用一些地址空间用来标记分配的该片内存的大小,再次调用malloc会根据这些表示找到空闲空间进行申请,而调用free则会根据标识释放与之前申请相对应的内存空间。
printf在运行时,是从前往后扫描格式化字符串的,每执行一个格式化字符串就会比较输出的大小,根据这个大小决定是否要调用malloc申请内存。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,分析了计算机中四种地址空间的概念、区别以及如何相互转换。还介绍了hello的四级页表的虚拟地址空间到物理地址的转换,阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.2.1 Unix I/O接口
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置 k。
4.读写文件:一个读操作就是从文件复制 n>0个字节到内存,从当前文件位置k开始,然后将k增加到 k+n,给定一个大小为 m字节的而文件,当 k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件:当应用完成对文件的访问后,会通知内核关闭这个文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
8.2.2 Unix I/O函数:
1.int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
2.int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果.
3. ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4.ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数将buf中的i个元素写到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。直到用户输入回车之后getchar才开始每次都读入一个字符。在用户使用键盘时,键盘接口获得一个键盘扫描码,同时产生中断请求,从而调用键盘中断处理子程序。扫描得到的ASCII码保存到系统的键盘缓冲区的内部。getchar等调用read系统函数,通过系统调用读取按键ascii码,等到输入回车键返回这个字符串。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf和getchar函数的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.编写hello.c的源程序,从此hello.c诞生。此时hello.c仍是一个文本文件,还没有变成二进制文件。
2.对hello.c进行预处理(gcc -E),hello.c变成了hello.i。
3.对hello.i进行编译处理(gcc -S),hello.i变成了hello.s。
4.对hello.s进行汇编处理(gcc -c),hello.s变成了hello.o。此时hello变成了二进制文件。
5.对hello.o进行链接处理,将其与其它可重定位目标文件以及动态链接库进行链接生成可执行目标文件hello。此时hello程序就可以在计算机上执行。
6.在shell命令行上输入./hello 120L021406 马铭 1来运行hello程序。
7.shell首先判断输入命令是否为内置命令。经过检查后发现其不是内置命令,则shell将其当作程序执行。
8.shell调用fork函数创建一个子进程。
9.shell调用execve函数,execve函数会将新创建的子进程的区域结构删除,然后将其映射到hello程序的虚拟内存,然后设置当前进程上下文中的程序计数器,使其指向hello程序的入口点。
10.运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache协同工作,完成对地址的翻译和请求。
11.execve函数并未将hello程序实际加载到内存中。当CPU开始执行hello程序对其进行取指时,发现其对应的页面不在内存中。此时会出现缺页故障这一异常,操作系统通过异常表对缺页处理程序进行间接调用,缺页处理程序实现虚拟内存和物理内存的映射,将缺失的页面加载到内存中。此时CPU重新执行引起故障的指令,指令可以正常执行,程序从而继续向下执行。
12.当hello程序调用sleep函数后进程休眠进入停止状态。而CPU不会等待hello程序休眠结束,而是通过内核进行上下文切换将当前进程的控制权转移到其它进程。当sleep函数调用完成后,内核再次进行上下文切换重新执行hello进程。
13.当hello程序执行printf函数时,会调用malloc函数从堆中申请内存。
14.在hello进程执行时,当在命令行中输入Ctrl-C时,shell会向前台作业发送SIGINT信号,该信号会终止前台作业,即hello程序终止执行。当输入Ctrl-Z时,shell会向前台作业发送SIGTSTP信号,该信号会挂起当前进程,即hello程序停止执行,之后再向其发送SIGCONT信号时,hello程序会继续执行。
15.当hello进程执行完成后,父进程会对子进程进行回收。内核删除为这个进程创建的所有数据结构。
计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。抽象是计算机系统设计与实现的重要基础:文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象,进程是对一个正在运行的程序的抽象,虚拟机是对整个计算机的抽象。
计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度!!!
计算机系统的设计考虑全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。
附件
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello.elf::hello.o生成的elf文件
hello:链接之后的可执行文件
hello.elf::hello生成的elf文件
hello1.txt:hello.o得到的反汇编文件
hello2.txt:hello得到的反汇编文件
参考文献
[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] 深入理解计算机系统原书第3版-文字版.pdf
[8] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[9] https://www.cnblogs.com/pianist/p/3315801.html
[10] https://blog.youkuaiyun.com/drshenlei/article/details/4261909
[11] https://blog.youkuaiyun.com/rabbit_in_android/article/details/49976101