2021-06-20

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1190202110
班   级 1903002
学 生 田雪洋    
指 导 教 师 郑贵滨

计算机科学与技术学院
2021年6月
摘 要
本篇以hello.c为例,介绍了程序从代码到可执行程序的一步步处理,即预处理,编译,汇编,链接的过程。并且分析了这个过程中各个阶段的内容和机制。此外,还介绍了与之相关的内存分配,IO管理等过程。
关键词:预处理,编译,汇编,链接,内存管理,IO管理,进程,shell;

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
7.1 HELLO的存储器地址空间 - 11 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级CACHE支持下的物理内存访问 - 11 -
7.6 HELLO进程FORK时的内存映射 - 11 -
7.7 HELLO进程EXECVE时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:Program to Process。对hello.c进行预处理,编译,汇编,链接,最后生成可执行文件,可执行文件在shell中输入启动命令后,shell为其fork产生一个子进程,然后hello便从program变成了process
020:在执行完P2P之后,shell调用execve后,会映射到虚拟内存,这个过程先删除当前虚拟地址中保存的数据结构并为hello创建新的区域,在hello开始执行程序后载入物理内存,然后进入main函数执行hello的具体c代码,将内容输出到屏幕。程序执行结束后,回收hello进程,并回收其内存空间。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上,Ubuntu 16.04 LTS 64位/优麒麟 64位,notepad++
开发与调试工具:gdb,gcc,vim,edb,readelf

1.3 中间结果
文件名称 文件作用
hello.i hello.c预处理之后的文件
hello.s hello.i编译后的文件
hello.o hello,s汇编之后的可重定位文件
Hello 可执行文件
hello.out hello反汇编之后的可重定位文件

1.4 本章小结
本章简要地介绍了实验的环境和工具,中间结果。并详细的介绍了hello p2p,020的过程。

第2章 预处理
2.1 预处理的概念与作用
预处理的作用:预处理是预处理器根据以字符#开头的命令,修改原始的C程序,得到以.i为文件扩展名的另一个C程序的过程。

                          图2-1

2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
结果如下:

                    图2-2.1

生成文件如下:

图2-2.2
2.3 Hello的预处理结果解析
Hello.c的源程序如下:

                       图2-3.1

经过预处理后的hello.i程序的一部分如下:

                         图2-3.2

通过上面两个图对比,我们可以看到,经过预处理,hello.i比hello.c的源程序多出了许多内容,经过预处理,GCC对源文件中的宏定义进行了展开,引入了相关头文件的内容,插入了各种引用的库函数的代码,而原本的main函数放在了最后,而上图hello.i图片展示的正是最后的main函数部分,而这部分和源代码保持相同。
2.4 本章小结
第二章主要介绍了预处理的概念,功能,作用,并且以hello.c文件为例,展示了其经过预处理生成的hello.i文件,并对比了两个文件的异同。

第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(ccl)将文本文件.i翻译成汇编语言文件.s的过程。

                         图3-1.1

3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s

结果如下:

图3-2.1

                      图3-2.2

3.3 Hello的编译结果解析
编译结果如下:

                        图3-3.1

对于上面的内容,一些重要的声明如下:
.file 源文件
.text 代码段
.data 数据段
.section .rodata 只读数据,.rodata节
.globl 全局变量
.size 大小
.type 类型
.align指令或数据的存放地址进行对齐的方式

3.3.1数据

1.字符串

                        图3-3.2

2.局部变量i

                       图3-3.3

Main函数将局部变量i放到栈上,其位置在%rbp-4的位置,并且初始化为0

3.main函数

                       图3-3.4

main函数被存在了.txt中 .globl表明这是一个全局函数,并且在.type中被定义成了@function,即函数类型

4.各种立即数

             图3-3.5

所有的立即数的数值都直接体现在了汇编代码中。

5.全局变量

                          图3-3.6

全局变量sleepsecs存储在.data节,被初始化为2,大小为4个字节

3.3.2跳转操作

                         图3-3.7

                       图3-3.8

                       图3-3.9

该函数中一共有三次跳转操作,第一个图中和第二个图中的跳转对应于c程序中的if(argc!=3),第三个图中的跳转对应循环判断条件i<10。

3.3.3数组操作

                   图3-3.10

main函数的第二个参数为数组,该数组的元素是指向字符串的指针。将argv数组的首地址保存在寄存器%rsi中,然后将其保存到了栈上,对于数组的访问,使用数组头指针位置加上偏移量(在本程序中为:agrv0+8i)访问数组中的每一个元素。

               图3-3.11

依次读出数组中的三个量,argv0,argv2,argv3

3.3.4函数调用
getchar()函数的调用

                图3-3.12

puts函数的调用

               图3-3.13

printf函数的调用

                 图3-3.14

Sleep函数的调用

           图3-3.15

exit函数的调用

             图3-3.16

3.3.5算数操作
该算数操作对应于原函数中for循环,对于循环变量i进行++i的操作

             图3-1.17

该算数操作是将%rsp-32的指针由加了16,即使它指向了argv[2]

              图3-3.18

3.3.6关系操作

本程序中的关系操作主要是cmp,通过该汇编指令,设置条件码CF,ZF,SF,OF,为后面的跳转操作做准备。

             图3-3.19

3.4 本章小结
本章主要介绍了编译的概念,作用,并且详细分析了hello.s中的所有汇编指令,加深了对于汇编指令的理解和对编译过程的理解。

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。.o是可重定位目标程序格式的文件。
作用:将汇编语言翻译为机器语言指令,使其在链接阶段能够直接被计算机执行。
4.2 在Ubuntu下汇编的命令
汇编的指令:gcc –c –o hello.o hello.s

结果如下:

                  图4-2.1

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

(1) ELF头
ELF头以一个16字节的序列开始(图中的Magic行),该序列描述了生成该文件的系统的字的大小和字节顺序。ELF头的其他剩余部分包含帮助链接器语法分析和解释目标文件的信息。比如下图中的类别,数据(是否是补码,大端序还是小端序),版本,操作系统类型(如图,本机是UNIX – System V),类型(如可重定位,可执行,或者共享的,本图中是可重定位),机器类型(即本图中的系统架构:Advanced Micro Devices X86-64),节头部表的偏移量(本图中为1232bytes),以及节头部表中条目的大小和数量(本图中是13个,大小为664bytes)。

                      图4-3.1

(2) 节头:显示了各个节的名字、类型、位置、大小等信息。

                   图4-3.2

如图所示,节头(section header)一共有14个,从0x4d0开始,在[Nr]这一行指明了各行包含的信息,名字,类型,地址,偏移量,大小等信息。图中地址那一列为0,是因为这是个重定位文件。

(3)符号表.symtab:
.symtab节是一张符号表,它存放着程序中定义和引用的函数和全局变量的信息。

                   图4-3.3

如上图所示,Name是字符串表中的字节偏移,指向符号的以null结尾的字符串的名字。Value是符号的地址,但对于重定位的模块来说,value是距定义目标的节的起始位置的偏移(图中全为0)。Size是目标的大小。Type表示的函数或者数据。Bind表示符号是本地的还是全局的,Ndx表示它存放在哪个节中。

(4).rela.text节
该节存放的是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改的这些位置。

                      图4-3.4

如图所示:偏移量就是需要 被修改的引用节的偏移量大小。信息包含symbol和type两个部分,symbol表示被修 改引用应该 指向的符号,在前四个字节,type重定位的类型在后四个字节。类型就是让 链接 器如何修改新的应用。符号值,一个有符号常数,重定位后要使用它 对被修改的引用进行再次偏移。符号名称,就是重定位 的目标的名称
4.4 Hello.o的结果解析
objdump -d -r hello.o 产生的反汇编代码如下所示:

                       图4-4.1

hello.s的代码如下所示:

                   图4-4.2

通过上面两图对比,我们发现,汇编代码部分没有变化,但是objdump -d -r hello.o 产生的 除了汇编代码外还有机 器代码,机器语言是用 二进制代码表示的计算机能直接识别 和执行的一种机器指令 的集合,而每一条机器指令由操作码和操作数组成。但是机器码难以 阅读和记忆,所以人们便发明了汇编语言来方便记忆和阅读,因此,汇编语言和机器语言有着一一 对应的映射关系,所以汇编语言也是底层的机器可以识别的语言。
下面就图中的不同进行分析:
(1) 分支转移。
在objdump -d -r hello.o 产生的汇编代码中,分支跳转是使用的main首地址+偏移量的方式,而hello.s是直接告诉要跳转到的目标位置,如.L1,.L2之类的。
(2) 函数调用
在objdump -d -r hello.o 产生的汇编代码中,函数跳转是使用的main首地址+偏移量的方式,并且还在下面标定是否是重定位,并在.rela.text节中为其添加重定位条目,而hello.s里面是直接call函数名称。
4.5 本章小结
本章介绍了汇编的概念与作用,并且详细分析了汇编文件的结构,最后比较了其和.s文件的不同点。
第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-2.1

5.3 可执行目标文件hello的格式
对可执行文件进行链接,然后重新执行第四章中的相关命令,得到结果如下:
(1) ELF头
和前面相比,重定位后的elf头中,类型变为了EXEC(可执行文件),程序的入口点地址也发生了变化,下面的节头大小,数目,开始地址都发生了变化。

                     图5-3.1

(3) 节头:

                  图5-3.2

如图所示,节头(section header)一共有14个,从0x3778开始,在[Nr]这一行指明了各行包含的信息,名字,类型,地址,偏移量,大小等信息。图中地址那一列为0,是因为这是个重定位文件。经过重定位后,地址,偏移量等信息不再是0,而是变成了程序被载入到虚拟地址的起始地址。

(3)符号表.symtab:
.symtab节是一张符号表,它存放着程序中定义和引用的函数和全局变量的信息。

                       图5-3.3

                     图5-3.4

                   图5-3.5

如上图所示,Name是字符串表中的字节偏移,指向符号的以null结尾的字符串的名字。Value是符号的地址,但对于重定位的模块来说,value是距定义目标的节的起始位置的偏移(图中全为0)。Size是目标的大小。Type表示的函数或者数据。Bind表示符号是本地的还是全局的,Ndx表示它存放在哪个节中。

(4).rela.text节
该节存放的是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改的这些位置。
图5-3.6
如图所示:偏移量就是需要被修改的引用节的偏移量大小。信息包含symbol和type两个部分,symbol表示被修改引用应该指向的符号,在前四个字节,type重定位的类型在后四个字节。类型就是让链接器如何修改新的应用。符号值,一个有符号常数,重定位后要使用它对被修改的引用进行再次偏移。符号名称,就是重定位的目标的名称

5.4 hello的虚拟地址空间
通过查看edb,可以看出hello的虚拟地址空间开始于0x400000,结束于0x401000,如下图所示

                      图5-4.1

根据前面的节头部表,可以通过edb找到各个节的信息。如下图所示

                       图5-4.2

5.5 链接的重定位过程分析

通过objdump -d -r hello运行结束后的结果如下:

                图5-5.1

                    图5-5.2

                 图5-5.3

              图5-5.4

                 图5-5.5

                图5-5.6

与之前的文件进行比较,发现hello的反汇编代码增加了.plt , .init , .fini节,并且库函数的代码全部已经链接到程序中,程序的各个节变得更加完整。程序中的函数和其他需要重定位的内容部分都已全部完成,可以直接跳转到相应的地址处。
重定位的过程:

  1. 重定位节和符号定义:链接器将所有类型相同的节合并为同一类型的新的聚合节。然后链接器将运 行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个 符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行 时内存地址了,
  2. 重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行 时地址。
    5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
0x401000 _init
0x401020 .plt
0x401030 puts@plt
0x401040 printf@plt
0x401050 getchar@plt
0x401070 exit@plt
0x401080 sleep@plt
0x401090 _start>
0x4010c0 _dl_relocate_static_pie
0x4010c1 main
0x401150 __libc_csu_init
0x4011b0 __libc_csu_fini
0x4011b4 _fini
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
程序调用由共享库定义的函数时,编译器无法预测这个函数的运行时的地址。所以GNU通过延迟绑定来将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过GOT和过程链接表来实现的。若一个目标模块调用定义在共享库中的任何函数,那么他就有自己的GOT和PLT。GOT是数据的端的一部分,而PLT是代码段的一部分。
GOT表位置在调用dl_init之前的位置和字节如下图所示:

                 图5-7.1

调用后的GOT表改变成如下图所示:

        图5-7.2

通过上面两张图的对比在执行do_int前和执行do_int后,global_offset从全0的状态,变成了相应的偏移量值。此时,GOT中的条目就包含着目标的正确的绝对地址。

5.8 本章小结
本章简要的介绍了链接的相关过程,并且通过对hello文件的链接,详细的分析了hello的虚拟地址空间,动态链接,静态链接,重定位的过程。通过本节,对链接的理解更加深刻。

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个执行中程序的实例。
进程的作用:

  1. 一个独立的逻辑控制流。它提供了一个假象,好像我们的程序独占地使用处理器。

  2. 一个私有的地址空间,它提供了一个假象,好像我们的程序独占地使用内存系统。
    6.2 简述壳Shell-bash的作用与处理流程
    作用:Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序,是命令行解释器,以用户态方式运行的终端进程,其基本功能是解释并运行用户的指令。
    处理流程:

                   图6-2.1
    

上述过程会被重复执行。
6.3 Hello的fork进程创建过程
Linux终端通过调用fork函数创建一个子进程,并在父进程中返回子进程的PID,在子进程中返回0.子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码和数据段,堆,共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。这就意味着,当父进程调用fork时,子进程可以读写父进程中任何打开的文件。并且父进程和子进程是并发运行的独立进程。
Hello的fork进程创建过程和上文叙述的相同。
6.4 Hello的execve过程
execve函数的功能是在当前进程的上下文加载并运行一个新程序。fork函数创建子进程后,子进程调用execve函数在当前进程的上下文加载并运行一个新程序hello。execve 调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello 程序,加载器删除子进程现有的用户虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC 指向_start 地址,_start 最终调用hello中的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行

6.6 hello的异常与信号处理
1.异常的分类:

                       图6.6.1
  1. hello中可能出现的异常
    故障,终止,陷阱,中断这四种异常均有可能出现。

  2. 键盘上各种操作导致的异常
    (1) ctrl+z
    执行ctrl+z命令后,系统发送SIGSTP信号到前台进程组中的每个进程,前台作业挂起,就是停止。

               图6-6.2
    

之后执行jobs命令可以看到作业组

              图6-6.3

然后执行fg命令,进程接着进行

图6-6.4

(2) ctrl+c
内核发送一个SIGINT信号到前台进程组中的每一个进程,终止前台进程。

图6-6.5

(3) 过程中乱按

       在大量的异常输入后,会导致程序终止。

图6-6.6

6.7本章小结
本章简要地介绍了进程的定义作用,信号的定义作用,shell的定义作用,fork,execve函数的功能和执行hello的过程,以及hello的进程执行过程。

第7章 hello的存储管理
7.1 hello的存储器地址空间

7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址一共有48位。如图所示,前16位是段选择符,段选择符又有三部分组成,分别是,索引(描述符表中的位置),TI(若TI=0,描述附表是全局描述符表,若TI=1,描述符表是局部描述符表)和RPL(端的级别)

            图7-2.1

逻辑地址转换为线性地址的整体过程如下图所示:

                     图7-2.2

用语言描述具体步骤如下:

  1. 首先根据逻辑地址的TI确定出是GDT(全局描述符表)还是LDT(局部描述符表)。

  2. 根据索引来从上面确定的表中获得32位段基址。

  3. 根据32位段基址和逻辑地址的高32位的段内偏移量一起结合确定32位线性地址。
    7.3 Hello的线性地址到物理地址的变换-页式管理
    线性地址(即虚拟地址)到物理地址的变换是通过页表实现的。

                图7-2.3
    

如图所示,页表基址寄存器指示着页表的首地址,在虚拟地址转换为物理地址时,虚拟地址的低p位直接转换为物理地址的低p位。虚拟地址的剩下的位数(VPN)为其物理地址剩下的位数(PPN)在页表中的偏移量,通过该偏移量和基地址寄存器中存储的页表的首地址,可以确定PPN所在的行,若该行的有效位为1,则将物理页号中的内容即是PPN,和刚刚确定的PPO一起组成物理地址。若有效位为零,则触发缺页异常,系统会去虚拟内存(磁盘)中去找相应的物理页号,然后写入物理内存,并把有效位更新为1,然后重复上面的过程。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。

                     图7-4.1

(图片来源于PPT)
36位VPN被划分成了四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2 PTE的偏移量,依次类推。在每一级页表的地址翻译中,都执行7.3中所描述的步骤。

7.5 三级Cache支持下的物理内存访问
如下图所示,为Core i7中的三级cache的模式图。在通过7.4获得物理内存后。从物理内存中分别获得组索引,块偏移,以及标记位。然后在L1中寻找,若在L1中找到,则直接读取数据,若L1中不存在,则依次到L2,L3主存中进行寻找,直到找到,然后将其读入L1,并将数据读出。

              图7-5.1

7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:

            图7-6.1

在下一次调度这个进程时,将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。下图为内存映射图。

                   图7-6.2

7.8 缺页故障与缺页中断处理
缺页故障:当要读取的某个页不在内存中时,即发生不命中时,便出现了缺页故障。
缺页中断处理:
(1)首先判断虚拟地址是否合法。缺页处理程序将搜索区域结构的链表,把A和每个区域结构中的 vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在下图中标识为“1”。
(2)判断进程是否有读、写或者执行这个区域内页面的权限。例如,这个缺页是否由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在下图中标识为“2”。
3)处理缺页。选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送到MMU。这次,MMU就能正常地翻译地址,而不会再产生缺页中断了。

                       图7-8

7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。堆的结构如下图所示。
图7-9

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器的类型:
1.显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供―种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放–个块。
2.隐式分配器(implicit allocator),分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collec-tor),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。

分配器的要求:

  1. 处理任意请求序列

  2. 立即响应请求

  3. 只使用堆

  4. 对齐块

  5. 不修改已分配的块
    下面将以一种叫做隐式空闲链表的简单空闲块组织结构来简要地介绍分配器的工作情况。
    一种简单的隐式空闲链表中的每一个块的结构如下图所示:

                  图7-9.2
    

在这样的块儿中,一个字大小的头部的高29位指示了块的大小,最低位表示该块是空闲的还是已分配的。然后是有效载荷部分和填充,这部分只有已分配的块中有。对于未分配的块,此部分为空。
若干个这样的块便链接成了隐式空闲链表,如下图所示:

                    图7-9.3

下面介绍对隐式空闲链表的简单操作。

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

  2. 分割空闲块。从空闲块中分配一部分给分配块,将空闲块分割。

  3. 获取额外的堆内存。如果隐式空闲链表中没有一个足够大的空闲块放置分配块,那么分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。

  4. 合并空闲块。分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻。这些邻接的空闲块可能引起一种现象,叫做假碎片(fault fragmentation),就是有许多可用的空闲块被切割成为小的、无法使用的空闲块。如下图所示:

                    图7-9.4
    

所以就要对邻接的空闲块进行合并。合并很简单,在常数时间内就可以完成。
7.10本章小结
本章主要以hello为例,介绍了存储器的虚拟地址,物理地址,线性地址,逻辑地址。然后介绍了Intel逻辑地址到线性地址的变换,Hello的线性地址到物理地址的变换,多级页表,缺页处理,以及动态分配内存。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
-个Linux文件就是一个m个字节的序列:
B0,B1,…,Bk ,…,Bm - 1
所有的I/О设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
UnixI/O接口:

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  2. Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
  3. 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。
  4. 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k十n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
    类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  5. 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
    Unix I/O函数:
  6. int open(char *filename, int flags , mode_t mode)
    open函数打开一个已经存在的文件或创建一个新文件。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
  7. int close(int fd)
    关闭已打开的文件
  8. ssize_t read(int fd ,void *buf , size_t n)
    read 函数从描述符fd的当前文件位置赋值最多n个字节到内存位置 buf。
    4.ssize_t write(int fd, const void * buf, size_t n)
    write函数从内存中buf位置复制至多n个字节到描述符为fd的当前文件位置。
    8.3 printf的实现分析
    printf函数的函数体如下所示:
  9. int printf(const char *fmt, …)
  10. {
  11. int i;
  12. char buf[256];
  13.   va_list arg = (va_list)((char*)(&fmt) + 4);  
    
  14.   i = vsprintf(buf, fmt, arg);  
    
  15.   write(buf, i);  
    
  16.  return i;  
    
  17. }  
    

首先va_list arg是一个字符指针,后面的(char*)(&fmt) + 4是…中第一个参数
printf函数主要调用了vsprintf函数和write函数来实现功能。
vsprintf的函数体如下:

  1. int vsprintf(char *buf, const char *fmt, va_list args)
  2. {   
    
  3.  char* p;   
    
  4.  char tmp[256];   
    
  5.  va_list p_next_arg = args;   
    
  6.  for (p=buf;*fmt;fmt++) {   
    
  7.  if (*fmt != '%') {   
    
  8.  *p++ = *fmt;   
    
  9. continue;   
    
  10. }   
    
  11. fmt++;   
    
  12. switch (*fmt) {   
    
  13. case 'x':   
    
  14. itoa(tmp, *((int*)p_next_arg));   
    
  15. strcpy(p, tmp);   
    
  16. p_next_arg += 4;   
    
  17. p += strlen(tmp);   
    
  18. break;   
    
  19. case 's':   
    
  20. break;   
    
  21. default:   
    
  22. break;   
    
  23. }   
    
  24. }   
    
  25. return (p - buf);   
    
  26. }

vsprintf函数的功能是格式化。他接受确定输出格式的字符串fmt,用格式字符串进行参数格式化,产生想要的输出形式。
最后,总结一下printf函数的执行过程:
1.从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
2.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
3.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数的源码如下:

  1. int getchar(void)
    1. {
    1. static char buf[BUFSIZ];
    1. static char *bb = buf;
    1. static int n = 0;
    1. if(n == 0)
    1. {
    1. n = read(0, buf, BUFSIZ);
    1. bb = buf;
    1. }
    1. return(–n >= 0)?(unsigned char) *bb++ : EOF;
    1. }
      当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从输入流中读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。
      异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
      getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
      8.5本章小结
      本章简要地介绍了一些I/O的相关知识,列出了一些Unix I/O的函数,并且分析了printf函数和getchar函数。
      结论
      用计算机系统的语言,逐条总结hello所经历的过程。
  2. 编写hello.c的源程序
  3. 预处理:从hello.c生成hello.i文件
  4. 编译:从hello.i生成hello.s文件
  5. 汇编:从hello.s生成hello.o文件
  6. 链接:将hello.o经重定位和动态链接生成可执行文件hello
  7. 运行:在shell中输入命令
  8. 创建子进程:shell调用fork()函数创建hello的子进程
  9. 加载:shell 调用 execve()函数,execve()函数 调用启动加载器,映射到虚拟内存,程序开始加载到物理内存,然后进入 main 函数。
  10. 执行命令:hello顺序执行自己的逻辑控制流
  11. 动态内存分配:通过malloc函数申请动态内存
  12. 信号:Ctrl+c,发送SIGINT信号给进程并终止前台作业。Ctrl+z时,发送SIGTSTP信号给进程,并将前台作业停止挂起。
  13. 终止:子进程结束后,父进程回收子进程,并回收内存空间。

感悟:一个简简单单的hello.c的程序,背后却要经历那么复杂和漫长的一生。
原来一个简简单单的hello程序背后却是那么的不简单,不平凡。计算机系统的设计者真是巧妙构思,帮助我们简化了程序运行的过程,让计算机成为简单易用的工具。为他们点赞!

  1. 附件

文件名称 文件作用
hello.i hello.c预处理之后的文件
hello.s hello.i编译后的文件
hello.o hello,s汇编之后的可重定位文件
Hello 可执行文件
hello.out hello反汇编之后的可重定位文件

参考文献
[1] 段页式访存——逻辑地址到线性地址的转换 - 简书 (jianshu.com)
[2]深入理解计算机系统原书第三版
[3] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值