摘  要

本文分析了hello程序的整个运行生命周期。首先编写hello.c源程序,之后运行C预处理器对其进行预处理,得到hello.i文件,运行C编译器将翻译生成汇编语言文件hello.s,然后运行汇编器将其翻译成一个可重定位目标文件hello.o,最后运行链接器程序将hello.o和系统目标文件组合起来,就可以创建一个可执行目标文件hello。在shell接收到输入的./hello的指令后开始调用fork函数创建进程,execve加载hello进入内存,由CPU控制程序逻辑流的运行,中断,上下文切换和异常的处理,最后结束进程并由父进程进行回收。

关键词:预处理;编译;汇编;链接;进程;存储管理;IO管理                         

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

7.4 TLB与四级页表支持下的VA到PA的变换

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

第1章 概述

1.1 Hello简介

1、首先用键盘输入,通过编译器编写得到hello,c的源程序。

2、然后运行C预处理器,即cpp,将源程序hello.c进行预处理得到hello.i文件。

3、再运行C编译器,即ccl,将其翻译生成hello.s文件。

4、再运行汇编器,即as,翻译得到可重定位文件hello.o。

5、最后运行链接器,即ld,将hello.o文件与系统目标文件结合,得到可执行目标程序hello。

(如图1.1.1所示)

(图1.1.1)

1.2 环境与工具

软件环境:Windows10 64位;Ubantu20/04 LTS 64位。

硬件环境:X64 CPU;2GHZ;4GRAM;256Disk。

工具:Visual Studio 2019;gdb;objdump。

1.3 中间结果

hello.c     源程序

hello.i     预处理后的文件

hello.s     编译后的汇编文件

hello.o     汇编后的可重定位文件

hello  链接后的可执行文件

hello.elf  hello.o的ELF格式文件

helloobj.txt  hello.o的反汇编文件

hello2.txt  hello.o的反汇编代码

helloelf.txt  hello的txt格式文件

1.4 本章小结

本章大体上介绍了hello程序的一生,以及实验过程中所需要的软硬件工具、实验的大体流程思路等。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。

作用:预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换。通过预处理可以最终得到 .i文件。

2.2在Ubuntu下预处理的命令

预处理的命令是:gcc -E -o hello.i hello.c。(结果如图2.2.1、图2.2.2)

(图2.2.1)

(图2.2.2)

2.3 Hello的预处理结果解析

解析结果的部分代码如图2.3.1所示。

(图2.3.1)

查看代码我们可以发现,hello.c经过预处理后的hello.i的内容有了明显增多。从短短的几行代码变成了三千多行的代码。我们也可以看到,hello.i中的宏进行了宏展开,头文件中的内容被包含进该文件中。同时代码中有#define命令还会对相应的符号进行替换。

2.4 本章小结

本章中我们了解了预处理的相关知识,并通过操作虚拟机查看了经过预处理得到的hello.i文件中的相关代码内容,更加切实深刻地感受到了预处理地作用。

第3章 编译

3.1 编译的概念与作用

概念:编译器将.i文件翻译成包含汇编语言程序的文本文件.s。

作用:将源程序翻译成为目标程序,并且进行语法和词法的分析。

3.2 在Ubuntu下编译的命令

编译的命令为:gcc -S hello.i -o hello.s(结果如图3.2.1、3.2.2)

(图3.2.1)

(图3.2.2)

3.3 Hello的编译结果解析

1数据

(1.1)字符串

通过查看hello.s中的内容我们可以知道程序中一共有两个字符串,并 且它们都在只读数据段中。如图3.3.1所示:

(图3.3.1)

hello.c中的数组是主函数的第二个参数,其每个元素都是指向字符类 型的指针。起始的地址存放在栈-32(%rbp)中,如图3.3.2:

(图3.3.2)

这个数组被两次调用并将参数传递给printf。两个字符串作为printf的参数,结果如图3.3.3所示:

(图3.3.3)

(1.2)局部变量

从hello.c的main函数可知其生命了一个局部变量i,在编译时,变量i会被存放在栈中,在本程序中,变量i被放到了位置-4(%rbp)。结果如图3.3.4所示:

(图3.3.4)

(1.3)参数argc

argc作为被参数到main中的参数也被存放到了栈中。

(1.4)数组char *argv[]

char *argv[]是作为main函数的第二个参数,数组的每个元素都是一 个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,      被两次调用传给printf。

  1. 全局变量

(图3.3.5)

如图3.3.5所示,只有main函数是一个全局变量且是一个全局函数。

  1. 算数操作

在hello.c中的算数操作是i++,对应的汇编语言为:-4(%rbp),如图3.3.6所示:

(图3.3.6)

  1. 赋值操作

在hello.c中的赋值操作为i=0,汇编代码如图3.3.7所示:

(图3.3.7)

在汇编代码中,我们可以用,mov指令实现赋值操作,具体指令如图3.3.8所示:

(图3.3.8)

  1. 关系操作

hello.c中argc!=4;是条件判断语句,进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),在比较之后还设置了条件码,根据条件码判断是否需要跳转。如图3.3.9。

(图3.3.9)

hello.c中i<8,作为判断循环条件指令被编译为cmpl $7,-4(%rbp),并设置条件码,为下一步jle利用条件码进行跳转做准备。如图3.3.10。

(图3.3.10)

  1. 控制转移指令

判断argc是否等于4,如果argc等于4,则不执行if语句,否则执行if语句,对应的汇编代码如图3.3.11:

(图3.3.11)

每次for循环都要检查i的值是否超过8,代码如图3.3.12。

(图3.3.12)

  1. 函数操作

hello.c中的函数操作:

main函数:参数是int argc,char *argv[]

printf函数:参数是argv[1],argv[2]

exit函数:参数是1

sleep函数:参数是atoi(argv[3])

getchar函数:无参数

调用函数时有以下操作:(假设函数P调用函数Q)

①传递控制:调用过程Q的时候,程序计数器(%rip)必须设置为函数Q代码的起始地址,然后在返回时,要把程序计数器(%rip)设置为P中调用Q后面那条指令的地址。

②传递数据:函数P必须能够向函数Q提供一个或多个参数Q必须能够向P中返回一个值。

③分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

(8)类型转换

hello.c中atoi(argv[3])将字符串类型转换为整型。int、float、double、short、 char等类型可以进行相互转化。

3.4 本章小结

本章主要介绍了编译器如何处理C语言,编译器从C语言的数据、赋值、类型转换、算术操作、逻辑操作等不同的方面进行了分析。理解这些编译的机制能偶让我们的反响工程的能力得到提升,阅读汇编代码更加轻松、准确。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是编译后的文件到生成机器语言二进制程序的过程,即.s到.o的过程。

作用:将汇编语言翻译成机器语言,使其能够在链接后被机器识别并执行。

4.2 在Ubuntu下汇编的命令

命令为:gcc -c -o hello.o hello.s。

4.3 可重定位目标elf格式

使用生成hello.o的elf格式命令:readelf -a hello.o> hello.elf。可以得到hello.elf文件。

查看hello.elf的内容。

  1. ELF头

ELF头开始的16个字节的序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分含有帮助链接器语法分析和解释目标文件的信息,包括目标文件类型、系统架构、机器类型节头部表等内容。目标文件的每个节都有一个固定大小的条目。

  1. 节头

包含各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对其等信息。

  1. 重定位节

这部分保存了.text节中需要被修正的信息。hello程序中需要被重定位的内容有printf、puts、exit、sleep、sleepseces、getchar、rodata中的.L0和.L1。

  1. 符号表

符号表存放的是在程序中定义和引用的函数和全局变量的信息,在每个重定位文件的.symtab中都有一张符号表。和编译器中的符号表不同的是,此符号表不包含局部变量的条目。

4.4 Hello.o的结果解析

命令为:objdump -d -r hello.o>helloobj.txt

分析与hello.s的差异:

  1. 函数调用与重定位条目对应

helloobj中的call后面不再是函数的名称,而是一条重定位条目指引的信息,不同于汇编文件中的直接加文件名。

hello.s:

helloobj.txt:

  1. 分支转移

helloobj中跳转指令使用的是确定的地址,不同于汇编代码中使用段名称。

hello.s:

helloobj.txt:

4.5 本章小结

本章讲述了hello.s汇编到hello.o的过程,此过程中,我们分析了hello.o的可重定位目标文件以及hello.o经过反汇编生成的代码并与hello.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.o hello.o

/usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

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

先用指令将hello转化为可查看到的helloelf.txt格式。

  1. ELF头

ELF头以一个16字节序列开始,描述生成文件的系统的字和字节顺序。剩下的部分含有帮助链接器语法分析和解释目标文件的信息,包括目标文件类型、系统架构、机器类型节头部表等内容。目标文件的每个节都有一个固定大小的条目。

从图中可以看出节头的数目为27个。

  1. 节头

与hello.o的节头相比增加了若干条目,此节头中一共有27个节的信息,而hello.o的节头只有13个节的信息。此外。所有节被分配了运行时的地址。

  1. 程序头表

可执行文件或共享目标文件的程序头是一个结构数组。每种结构都描述了系统准备程序执行所需的段或者其他信息。

  1. 重定位节

  1. 符号表

可执行目标文件的符号表表项数目明显多于可重定位目标文件的表项数目,一共有51个。一方面,可执行文件中加入了和调试、加载、动态链接相关的节,另一方面,链接器对可重定位目标文件中的符号惊醒了进一步的解析,加入了若干系统调用。

  1. 动态符号表

用于保存与动态链接相关的导入导出符号,但是不包括模块内部的符号。

5.4 hello的虚拟地址空间

使用edb加载hello:

在Data Dump处可以得知hello的虚拟地址空间始于0x401000。

根据之前的Section Header部分,在edb中可以找到各个节的信息,例如.text节,虚拟地址始于0x4010f0。

5.5 链接的重定位过程分析

命令:objdump -d -r hello>hello1.txt。

通过比较分析,发现hello与hello.o主要有以下不同。

  1. hello反汇编代码有确定的虚拟地址,已经完成了重定位。而hello.o虚拟地址为0,未完成重定位。

hello:

hello.o:

  1. hello的反汇编代码中增加了很多节以及函数的汇编代码。

  1. hello的重定位过程

重定位节和符号定义链接器将所有类型相同的节合并在一起,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,以及赋给输入模块定义的每个和符号,当这一步完成时,程序中每个指令和全局变量都有唯一运行时的地址。重定位节中的符号引用这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖与可重定位目标模块中重定位条目这一数据结构。

重定位条目当编译器遇到对最终位置位置的目标引用时,会生成一个重定位条目,代码的重定位条目放在.rela.text中。

5.6 hello的执行流程

使用edb执行hello。

hello的执行流程大致分为:开始执行部分:_start、libc_start_main,执行main部分:_main、_printf、_exit、_sleep、_getchar;推出部分:exit。

程序名称                                    地址

ld-2.27.so!_dl_start                        0x7fb85a93aea0

ld-2.27.so!_dl_init                         0x7f9612138630

hello!_start                               0x400582

lib-2.27.so!__libc_start_main                0x7f9611d58ab0

hello!puts@plt                            0x4004f0

hello!exit@plt                            0x400530

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过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文件,可以看到GOT和PLT节的起始地址:

GOT之前:

GOT之后:

PLT之前:

PLT之后:

5.8 本章小结

本章主要介绍了链接的概念与应用,简要的分析了可执行文件的ELF格式,hello的虚拟地址空间和执行流程。同时还详细地分析了静态链接的重定位过程以及动态链接的过程。

6 hello进程管理

6.1 进程的概念与作用

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

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

6.2 简述壳Shell-bash的作用与处理流程

shell是指“为使用者提供操作界面”的软件(命令解析器)。作为命令语言,它交互式解释和执行用户输入的命令,或者自动地解释和执行预先设定好的一连串的命令,然后调用相应的应用程序,代表用户控制操作系统中的任务。

shell的处理流程大致如下:

(1)从终端读入输入的命令;

(2)将输入字符串切分获得所有的参数;

(3)如果是内置命令则立即执行;

(4)否则调用相应的程序执行;

(5)shell 应该接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

在终端中输入./hello 120L020816郭仁恺 1命令后,shell会处理该命令,判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是虚拟地址独立、PID也不相同的一份副本。

6.4 Hello的execve过程

exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。fork函数创建子进程后,子进程会调用exceve函数加载新的hello程序,这需要先删除已存在的用户区域,再创建新的代码、数据、堆和栈段,如果hello程序与共享对象链接,比如标准C库libc.so,还需要映射共享区域,最后设置当前进程的上下文中的程序计数器(PC),使程序计数器指向代码区域的入口。

execve在调用成功的情况下不会返回,只有当出现错误时,execve才会返回到调用程序。

6.5 Hello的进程执行

上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。一个进程执行它的控制流的一部分的每一时间段叫做时间片。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

在进程调用execve函数以后,进程已经为hello程序分配了新的虚拟地址空间。hello在开始运行时运行在用户模式下,输出“hello 120L020816 郭仁恺”后,它调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式,再次输出相同的字符串。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。

如图为内核模式与用户模式的切换。

6.6 hello的异常与信号处理

1、正常运行状态

hello会先从键盘读入“120L020816 郭仁恺 1”,其中最后一个数字是用户指定的秒数,表示每经过1秒,hello程序会输出一串“Hello 120L020816郭仁恺”字符串。结果如图:

  1. Ctrl+Z

按下Ctrl+Z时,进程收到SIGSTP信号,此时前台作业被挂起,并打印了相关的挂起信息,但hello进程还没有被回收,而是在后台运行。输入ps命令,可以查看此时正在运行的进程,发现hello仍然在后台运行,其PID是2718。结果如图所示。

输入jobs命令,查看进程的job号。通过图片可知hello的job号为1.输入fg 1,可以见hello进程重新调回前台运行。

  1. Ctrl+c

按下Ctrl+C时,内核发送一个SIGINT信号到前台进程组的每个进程,终止前台作业。进程收到SIGINT信号,结束hello。输入ps命令,已经查询不到hello的PID,输入jobs也同样查询不到hello。

结果如图所示:

  1. 随便按

乱按的输入被缓存到缓冲区并且输出,但是并不会影响进程的执行。

6.7本章小结

本章主要介绍的进程的概念和作用,以及shell-bash的处理过程。本章以hello为例,详细说明了fork进程创建过程,execve过程,hello的进程执行以及一些异常与信号处理。我们可以看到,每种信号都有对应的不同的处理机制,对不同的shell命令,hello也有不同的响应结果。正是这些精心设计的机制,支持着hello以及其他所有程序得以顺利、高效地在计算机上运行。

7 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。在第3章编译部分中,生成的hello.s文件中所表示的地址就是逻辑地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为段标识符:段内偏移量。

线性地址:逻辑地址到物理地址变换之间的中间层。程序hello的代码在编译后会产生逻辑地址。然后正如第4章汇编部分中所叙述的那样,对编译后的代码再进行汇编得到hello.o文件,对hello.o进行反汇编得到hello1.s文件。反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。

虚拟地址:即线性地址。对于进程来说,使用的都是虚拟地址。

物理地址:放在寻址总线上的地址。在hello的运行中,访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。

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

逻辑地址由段选择符和偏移量组成,段标识符是一个16位长的字段,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。

段式管理以段为单位分配内存,每段分配一个连续的内存区,由于各段长度不等,所以这些存储区的大小不一。段式管理的内存分配与释放在作业或进程的执行过程中动态进行。

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

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应的页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。一个n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),和一个n-p位的虚拟页号(VPN)。MMU利用VPN选择适当的PTE,根据PTE,可以得知虚拟页的信息。如果虚拟页已缓存,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。如果虚拟页未缓存,会触发缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

7.4 TLB与四级页表支持下的VA到PA的变换

CPU每产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,会要求从内存多取一次数据,代价是几十到几百个周期。许多系统试图消除这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)。

CPU产生虚拟地址VA后,VA被传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU会查询页表。CR3寄存器包含L1页表的物理地址,用于确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。

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

获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。

7.6 hello进程fork时的内存映射

在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。同时为这个新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。

7.7 hello进程execve时的内存映射

execve的加载步骤在6.4节中已经给出,如下:

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

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

(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

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

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。

缺页中断处理:通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

7.9动态存储分配管理

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

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

2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块,也叫垃圾收集器。

隐式空闲链表:

带边界标记的隐式空闲链表的每个块由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成。在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的,分配器可以通过遍历堆中所有的块间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配、最佳适配。分配完后可以分割空闲块减少内部碎片,其中分配器在面对释放一个已分配块时也可以合并空闲块。

显示空闲链表:

显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里。例如堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱指针和一个后继指针。在显式空闲链表中,可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处;也可以采用按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,这时,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章介绍了存储器地址空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello 进程fork时和execve 时的内存映射、缺页故障与缺页中断处理、包括隐式空闲链表和显式空闲链表的动态存储分配管理。

8 hello的IO管理

8.1 Linux的IO设备管理方法

Linux将文件所有的I/O设备都模型化为文件,甚至内核也被映射为文件。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。Linux就是基于Unix I/O实现对设备的管理。

设备的模型化:文件

设备管理:unix io接口

这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等

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

在形参列表里有这么一个token:... 这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。

再进一步查看windows系统下的vsprintf函数体:

va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。

再看一看vprintf函数:

其中vsprintf(buf, fmt, arg)函数能够返回我们想要打印的字符串的长度并对我们的格式化字符串进行解析。当获取到字符串的长度后,我们便能够将字符串打印出来。

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

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

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

8.4 getchar的实现分析

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

getchar函数是stdio.h中的库函数,它的作用是从stdin流中读入一个字符。函数的返回值是用户输入的第一个字符的ASCII码,如果出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓冲区中,后续如果还调用getchar函数的话,getchar就可以自动读取缓冲区中保留的字符,而无需等待键盘的任何输入。

在hello.c程序的末尾,main函数在返回前,就使用了一个getchar( )函数,这里的函数作用是将键盘缓冲区中剩余的字符取出。由于这里的getchar函数仅仅只是取出字符,而没有将其赋值给其他任何变量,所以其作用相当于清除缓冲区剩余的字符。

8.5本章小结

本章介绍了Linux的I/O设备管理方法、Unix的接口及其函数,了解了linux的I/O设备管理机制,开、关、读、写、转移文件的接口及相关函数,以及printf和getchar函数的实现方法以及操作过程。

结论

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

(1)编写hello.c源文件;

(2)对hello.c执行预处理得到hello.i;

(3)对hello.i执行编译得到hello.s;

(4)对hello.s执行汇编得到hello.o;

(5)将hello.o和动态链接库进行链接成为可执行程序hello;

(6)./hello运行程序,此时shell调用fork()函数创建子进程并在子进程中执行execve()函数加载程序到虚拟内存,MMU将程序中间的虚拟地址翻译成为实际的物理地址,此后执行逻辑控制流,正常结束程序或者出现异常,调用信号处理函数进行处理,此后进程结束,占用资源将被释放和回收。

计算机的每一步设计都是前人深思熟虑,历经迭代,经过严格规定处理正常情况或异常情况之后的结果,其知识的深度与广度每一寸都凝聚着前人的心血。

附件

hello.c     源程序

hello.i     预处理后的文件

hello.s     编译后的汇编文件

hello.o     汇编后的可重定位文件

hello  链接后的可执行文件

hello.elf  hello.o的ELF格式文件

helloobj.txt  hello

参考文献

[1] [美]布赖恩特(Bryant,R.E.).深入了解计算机系统.机械工业出版社,2016

[2]  [美]派特、派特尔.计算机系统概论.机械工业出版社,2017

[3] https://blog.csdn.net/oceanstudy123/article/details/106024041

[4]  linux sda sda1区别,linux–/dev/sda和/dev/sda1之间的区别_小狐狸与小道士的博客-CSDN博客

[5]  https://www.cnblogs.com/pianist/p/3315801.html

2022哈工大计算机系统大作业相关推荐

  1. 2022哈工大计算机系统大作业——程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 人工智能(未来技术) 学   号 120L020301 班   级 2036011 学       生 张思远 ...

  2. 哈工大计算机系统大作业 程序人生-Hello’s P2P 2022

    2022哈工大计算机系统大作业 目录 摘 要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubun ...

  3. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  4. 哈工大计算机系统大作业 程序人生-Hello‘s P2P

    哈工大计算机系统大作业 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2 在Ubuntu下预处理的 ...

  5. 程序人生-哈工大计算机系统大作业2022春

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 7203610401 班    级 2036012 学       生 王元辰 指 导 ...

  6. 哈工大2022秋计算机系统大作业-程序人生(Hello‘s P2P)

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学    号 班    级 学       生 指 导 教 师 刘宏伟 计算机科学与技术学院 ...

  7. 哈工大2022春计算机系统大作业:程序人生-Hello‘s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学   号 120L021305 班   级 2003002 学       生 李一凡 指 导 教 ...

  8. 哈工大计算机系统大作业2022春

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术学院 学    号 7203610110 班    级 2036011 学       生 王 ...

  9. 2022春季学期哈工大计算机系统大作业

    计算机系统 计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022013 班   级 2003007 学       生 王炜栋 ...

  10. 2022春哈工大计算机系统大作业——hello的程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 班   级 学       生 指 导 教 师 计算机科学与技术学院 2021年5月 摘 ...

最新文章

  1. .Net 基础new关键字的用法
  2. 关于MyEclipse连接SQLServer和Mariadbsql
  3. 杭电1430康托 bfs(java)
  4. 有没有一种让人很爽的学习方法?
  5. SAP ABAP和Linux系统里如何检查网络传输的数据量
  6. 《构建之法》第十三章学习总结
  7. restful是什么
  8. dw写出来的php乱码,解决DW中的乱码问题
  9. DELPHI正则表达式
  10. [原]浅谈几种服务器端模型——反应堆模式(epoll 简介) - _Boz - 博客园
  11. SQL根据下标,返回split分割后字符串
  12. Android15_ListView分页
  13. Spss-kmeans聚类分析操作
  14. 垃圾回收器ZGC应用分析总结
  15. 如何正确的去学习C语言
  16. BufferedWriter的write(int c)方法
  17. 华为eNSP企业综合组网(《路由与交换技术》课程综合实验)
  18. BugKu-Misc-赛博朋克
  19. 一个草根站长的逆袭之旅
  20. 《西南联大行思录》之感想20180911

热门文章

  1. NAS 和 SAN 服务器概述
  2. 对接微信支付退款证书之坑
  3. if 条件结构与switch条件选择结构
  4. QuickTime不支持播放HEVC编码mp4/mov视频
  5. 如何配置一台电脑。(一次解决电脑基础知识和如何选择合适电脑)
  6. gst-rtsp-server 开源包的说明
  7. php布尔运算,布尔逻辑运算符有哪些?
  8. 电脑连上Jink后打开Altium Designer弹出Failed to measure TotalIRLen(6)
  9. 【转】 笔记本散热维护
  10. python 分析泰坦尼克号生还率_泰坦尼克号上谁的生存几率更大?Kaggle 金牌得主 Python 数据挖掘框架发现真相...