哈工大计算机系统大作业

摘要

  • 本文主要阐述hello程序在Linux系统的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。并结合课本的知识详细阐述我们的计算机系统是如何对hello进行进程管理、存储管理和I/O管理,通过对hello一生周期的探索,让我们对计算机系统有更深的了解。

    关键词:Hello程序;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O ;

**

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:在Linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。

020: shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

文件名称文件作用
hello.ihello.c预处理之后文本文件
hello.shello.i编译后的汇编文件
hello.ohello.s汇编之后的可重定位目标文件
hello链接之后的可执行目标文件
hello.outhello反汇编之后的可重定位文件

1.4 本章小结

本章大致主要简单介绍了 hello 的 p2p,020 过程,列出了本次实验信息:环境、中间结果,并且大致简介了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程。

第2章 预处理

2.1 预处理的概念与作用

预处理概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

命令:gcc hello.c -E -o hello.i

在这里插入图片描述

2.3 Hello的预处理结果解析

在这里插入图片描述

经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,文件的内容增加,且仍为可以阅读的C语言程序文本文件。对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。

2.4 本章小结

本章介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析

3.3.0汇编指令的介绍

在这里插入图片描述
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型

3.3.1数据
1.字符串

程序中有两个字符串,由上图可知,这两个字符串都在只读数据段中,分别如图所示: 在这里插入图片描述在这里插入图片描述在这里插入图片描述
这两个字符串作为printf函数的参数:

2.局部变量i

main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。
在这里插入图片描述

3.main函数

参数 argc 作为用户传给main的参数。也是被放到了堆栈中。

4.各种立即数

立即数直接体现在汇编代码中

5.数组:char *argv[]

hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf
在这里插入图片描述
红色方框标注的是:数组argv的存放的位置:-32(%rbp)
蓝色方框标注的是:分别获取argv[1]和argv[2]的地址

3.3.2.全局函数

在这里插入图片描述
由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中使用的字符串常量也被存放在数据区。
在这里插入图片描述
这段汇编代码说明main函数是全局函数

3.3.3赋值操作

程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型又有好几种不一样的后缀
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节

3.3.4算数操作

hello.c中的算数操作有:i++,由于是i是int类型的,因此汇编代码只用addl就能实现其他的操作有
在这里插入图片描述

3.3.5关系操作

(1)argc!=3;是在一条件语句中的条件判断:argc!=3,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
在这里插入图片描述
(2)i<8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。
在这里插入图片描述

3.3.6控制转移指令

汇编语言中首先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:(1)判断i是否为3,如果i等于3,则不执行if语句,否则执行if语句,对应的汇编代码为
在这里插入图片描述
(2) for(i=0;i<8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句中,对应的汇编代码为:
在这里插入图片描述
第一个红色方框:首先i赋初值0,然后无条件跳转至判断条件的代码中,即.L3.
第二个红色方框:判断i是否符合循环的条件,符合直接跳转至.L4,也就是循环体的内部.

3.3.7函数操作

调用函数时有以下操作:(假设函数P调用函数Q)
(1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始 地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的 地址。
(2)传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回 一个值。
(3) 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回 前,又必须释放这些空间。

hello.C涉及的函数操作有:
main函数,printf,exit,sleep ,getchar函数
main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串
exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。

3.3.8类型转换

hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型其他的类型转换还有int、float、double、short、char之间的转换

3.4 本章小结

本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

gcc hello.s -c -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

(1) ELF Header:用命令:readelf -h hello.o,如图4.3.1ELF Header
ELF Header:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。

根据头文件的信息,可以知道该文件是可重定位目标文件,有13个节。

看图4.3.2中文版更能看清楚
在这里插入图片描述
图4.3.1ELF Header
(2) Section Headers:命令:readelf -S hello.o
Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
在这里插入图片描述
图4.3.1
在这里插入图片描述图4.3.2

(2) Section Headers:命令:readelf -S hello.o

Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
在这里插入图片描述
图4.3.3Section Headers

(3)查看符号表.symtab :命令readelf -s hello.o如图4.3.4
.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
在这里插入图片描述
图4.3.4符号表.symtab

(4)重定位节.rela.text,如图4.3.5
重定位节:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
重定位节.rela.text中各项符号的信息:

Offset:需要被修改的引用节的偏移Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,
symbol:标识被修改引用应该指向的符号,
type:重定位的类型
Type:告知链接器应该如何修改新的应用
Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称。
在这里插入图片描述
图4.3.5

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
在这里插入图片描述
图4.4.1反汇编代码
在这里插入图片描述
图4.4.2 hello.s

通过反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。

(1)分支转移:反汇编的跳转指令用的不是段名称比如.L3,二是用的确定的地址,因为,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

(2)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。

第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.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
在这里插入图片描述

图5.2

5.3 可执行目标文件hello的格式

(1)ELF Header:hello的文件头和hello.o文件头的不同之处如下图标记所示,Type类型为EXEC表明hello是一个可执行目标文件,有25个节
在这里插入图片描述
图5.3.1
(2)节头部表Section Headers:Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
## 标题
图5.3.2
(3)重定位节.rela.text:
在这里插入图片描述
(4) 符号表.symtab
在这里插入图片描述
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0,如图5.4.1
在这里插入图片描述在这里插入图片描述
图5.4.1

根据图5.3.2的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x400550,大小为0x132.
在这里插入图片描述
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello >hello.out,获得hello的反汇编代码.
通过分析hello与hello.o的不同,说明链接的过程。可以发现以下不同的地方:

(1)hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程,如图5.5.1和5.5.2的比较可知.

(2)hello反汇编的代码中多了很多的节以及很多函数的汇编代码,如图5.5.3.所示,这些节都具有一定的功能和含义,如图5.5.5所示列举了一些节的含义。
在这里插入图片描述
图5.5.1 hello.out
在这里插入图片描述
图5.5.2
在这里插入图片描述
图5.5.3
在这里插入图片描述
图5.5.4hello ELF 格式中的 Section Headers Table
在这里插入图片描述
图5.5.5一些节的含义
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt,如图5.5.5所示,它们每个符号的含义如图5.5.6所示
在这里插入图片描述
图5.5.4
图5.5.4
在这里插入图片描述
图5.5.5

(4)重定位过程
重定位过程的地址计算算法如图5.5.6所示
在这里插入图片描述
图5.5.6

举例说明
1.exit()相对地址引用定义exit的重定位条目为r,则
通过图5.5.6的公式可知:
ADDR(s) =0x400530refaddr = ADDR(s) + r.offset=0x400582+0x27=0x4005a9*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)=(0x400530+(-4)-0x4005a9)=-125=(unsigned)(0xffffff83),
经验证,如图所示,符合!!!!!!
在这里插入图片描述

5.6 hello的执行流程

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

根据hello ELF文件可知(图5.7.1),GOT起始表位置为0x601000,如图: ​
在这里插入图片描述
图5.7.1

GOT表位置在调用dl_init之前0x601008后的16个字节均为0:
在这里插入图片描述
图5.7.2GOT表初始前内容

调用_start之后发生改变,0x601008后的两个8个字节分别变为:0x7f6f8dc46170、0x7f6f8da34750,其中GOT[O](对应0x600e28)和GOT[1](对应0x7fb06087e168)包含动态链接器在解析函数地址时会使用的信息。GOT[2](对应0x7fb06066e870)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下: ​
在这里插入图片描述
图5.7.3 GOT表初始后内容

GOT[2]对应部分是共享库模块的入口点,如下:
在这里插入图片描述
图5.7.4共享库模块入口点

举例puts函数在调用puts函数前对应GOT条目指向其对应的PLT条目的第二条指令,如图puts@plt指令跳转的地址
在这里插入图片描述
图5.7.5puts&plt函数
在这里插入图片描述
图5.7.6 调用puts函数前(链接前)PLT函数

可以看出其对应GOT条目初始时指向其PLT条目的第二条指令的地址。puts函数执行后在查看此处地址:
在这里插入图片描述
可以看出其已经动态链接,GOT条目已经改变。

5.8 本章小结

在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。历经艰辛我们的hello可算是诞生了!!

第6章 hello进程管理

6.1 进程的概念与作用概念:

进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情 况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数 据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。

作用:进程为用户提供了以下假象:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

以我们的hello为例,当我们输入 ./hello 1180800811 张瑞豪 1 的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:

(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
在这里插入图片描述
图6.4
除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据 复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用 它的页面调度机制自动将页面从磁盘传送到内存。

6.5 Hello的进程执行

进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:

①逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。

②并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。

③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。

⑤用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。

⑥上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

⑦上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1) 保存以前进程的上下文
2)恢复新恢复进程被保存的上下文,
3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。

现在我们再来看一下hello进程执行,再进程调用execve函数之后,由上面分析可知,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.txt和.data节分配虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出hello 1180800811 张瑞豪,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。如图6.5.1所示
在这里插入图片描述
图6.5.1

当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

整个程序的执行效果如图6.5.2
在这里插入图片描述
图6.5.2

6.6 hello的异常与信号处理

(1)异常和信号异常可以分为四类:中断、陷阱、故障、终止,各自的属性如图6.6.0
在这里插入图片描述
图6.6.0

hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。

故障:在执行hello程序的时候,可能会发生缺页故障。

终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。
在这里插入图片描述
(2)键盘上各种操作导致的异常
1.图6.6.1是正常执行hello程序的结果
在这里插入图片描述
图6.6.1

2.图6.6.2按下 ctrl-z 的结果,输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,如图6.6.3所示用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。如图6.6.4.
在这里插入图片描述
图6.6.2
在这里插入图片描述
图6.6.3
在这里插入图片描述
图6.6.4

3.图6.6.5是按下Ctrl+c的结果,在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,如图6.6.6,用ps查看前台进程组发现没有hello进程。
在这里插入图片描述
图6.6.5
在这里插入图片描述
图6.6.6

4.程序运行过程中按键盘,不停乱按,结果如图6.6.7,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入。
在这里插入图片描述
图6.6.7

6.7本章小结

在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。

虚拟地址:也就是线性地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。

7.2 Intel逻辑地址到线性地址的变换-段式管理

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器,如图7.2.1所示。

索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成,如图7.3.2所示

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
在这里插入图片描述
图7.2.1段选择符
在这里插入图片描述
图7.2.2段描述符

图7.2.3详细显示了一个逻辑地址是怎样转换成相应线性地址的,逻辑地址转换为线性地址的一般步骤:
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
在这里插入图片描述
图7.2.3逻辑地址转换

7.3 Hello的线性地址到物理地址的变换-页式管理

由课本知识点可知,线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。

这里我们不考虑TLB和多级页表,这将在下面探讨。

使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。

虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小时相同的。

任意时刻虚拟页都被分为三个不相交的子集:

未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页

每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如图7.3.1所示,页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在次胖的起始地址。
在这里插入图片描述
图7.3.1页表

通过了解了上述虚拟地址转换为物理地址操作系统所提供的机制,现在我们来看一下到底是如何实现虚拟地址到物理地址的转换。如图7.3.2所示。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

在这里插入图片描述
图7.3.2

7.4 TLB与四级页表支持下的VA到PA的变换在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下: 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小 4KB,一个 PTE 条目8B,共 512 个条目,使 用 9 位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位, 因为 VA 48 位,所以 VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。

如图 7.4,CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
在这里插入图片描述
图7.4

7.5 三级Cache支持下的物理内存访问

我们只讨论Cashe1的物理内存访问,Cashe2,Cashe3原理相同。

由于L1Cashe有64组,所以组索引位s为6,每组有8个高速缓存行,由于每个块的大小为64B,所以块偏移为为6,因此标记位为52-6-6=40位。

因此L1Cashe的物理访存大致过程如下:
(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组

(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。

(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可

(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。如图7.5所示 。
在这里插入图片描述
图7.5

7.6 hello进程fork时的内存映射

当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。

2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。

3)映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
在这里插入图片描述
图7.7加载器是如何映射用户地址空间区域的

7.8 缺页故障与缺页中断处理

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。故障处理具体流程如图7.8所示
在这里插入图片描述
图7.8缺页故障处理流程

7.9动态存储分配管理

动态内存分配器维护者一个进程的虚拟内存区域,成为堆。(如图7.9.1所示),分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。

(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。

(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
在这里插入图片描述
图7.9.1

1.显示分配器的约束条件
①处理任意的请求序列
②立即相应请求
③只使用堆
④对其块(对齐要求)
⑤不修改已分配的块

2.隐式空闲链表
隐式空闲链表区别块的边界、已分配块和空闲块的方法如图7.9.2所示
在这里插入图片描述
图7.9.2
这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。

头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。块的格式如图7.9.3所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。
在这里插入图片描述
图7.9.3隐式空闲链表块的格式

(1)放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。

(2)分割空闲块一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。如图7.9.4所示。
在这里插入图片描述
图7.9.4

(3)获取额外堆内存如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。

(4)合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。如图7.9.5.所示
在这里插入图片描述
图7.9.5

3.显示空闲链表

显示空闲链表是将空闲块组织为某种形式的显示数据结构。如图7.9.6所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。
在这里插入图片描述
图7.9.6

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线 性时间减少到了空闲块数量的线性时间。

一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过 的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界 标记,那么合并也可以在常数时间内完成。

按照地址顺序来维护链表,其中链 表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm

所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:

设备的模型化:文件
设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O 接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。 (3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。

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函数的函数体:

`1.	static int printf(const char *fmt, ...) 
2.	{  
3.	    va_list args;  
4.	    int i;  
5.	    va_start(args, fmt);  
6.	    write(1,printbuf,i=vsprintf(printbuf, fmt, args));  
7.	    va_end(args);  
8.	    return i;  
9.}

printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

接下来是write函数:

1.write: 
2. mov eax, _NR_write 
3. mov ebx, [esp + 4] 
4. mov ecx, [esp + 8] 
5. int INT_VECTOR_SYS_CALL 

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

查看syscall函数体:

1.sys_call:  
2.call save 
3.
4.	    push dword [p_proc_ready]  
5.	     
6.	    sti   
7.		    
8.	    push ecx   
9.	    push ebx  
10.	    call [sys_call_table + eax * 4]  
11.	    add esp, 4 * 3  
12.	  
13.	    mov [esi + EAXREG - P_STACKBASE], eax   
14.	    cli  
15.	    ret

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。

显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

于是我们的打印字符串就显示在了屏幕上。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar 的源代码为:

1. int getchar(void)  
2. {  
3.  static char buf[BUFSIZ];  
4.  static char *bb = buf;  
5.  static int n = 0;  
6.  if(n == 0)  
7.  {  
8.   n = read(0, buf, BUFSIZ);  
9.   bb = buf;  
10.  }  
11.  return(--n >= 0)?(unsigned char) *bb++ : EOF;  
12. } 

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。

getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。

结论

用计算机系统的语言,逐条总结hello所经历的过程。

hello.c:编写c程序,hello.c诞生,它是一个二进制文本文件,hello.c中的每个字符都是用ascall编码表示。

hello.i:hello.c经过预处理阶段变为hello.i。

hello.s:hello.i经过编译阶段变为hello.s。

hello.o:hello.s经过汇编阶段变为hello.o。

hello:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。至此可执行hello程序正式诞生。

运行:在终端输入1180800811 张瑞豪 1。

创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork()函数创建一个子进程。

加载::shell 调用 execve,execve 调用启动加载器,加映射虚拟内 存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。

上下文切换:hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。

动态申请内存:当hello程序执行printf函数是, 会调用 malloc 向动态内存分配器申请堆中的内存。

信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。

终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。

对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

计算机系统的设计思想和实现都是基于抽象实现的。从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。

计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度!!!

计算机系统的设计考虑全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。

附件

在这里插入图片描述

参考文献

[1] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[2] file:///C:/Users/m1777/Desktop/深入理解计算机系统原书第3版-文字版.pdf
[3] https://www.cnblogs.com/pianist/p/3315801.html

Logo

更多推荐