程序人生-Hello’s P2P

计算机系统

 

大作业

题     目  程序人生-Hello’s P2P 

专       业   未来技术(人工智能)

学     号    7203610830          

班     级    2036015             

学       生    王一鸣           

指 导 教 师     史先俊            

计算机科学与技术学院

2022年5月

摘  要

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

关键词:关键词:预处理 编译 汇编 链接 进程管理 异常与信号 虚拟内存 存储地址翻译 I/O管理                           

目  录

第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程序的生命周期是从一个高级C语言程序开始的, 因为这种形式能够被人读懂。然而,为了在系统上运行 hello.c程序每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件,在这里,GCC编译器驱动程序读取源程序文件 hello.c, 并把它翻译成一个可执行目标文件hello。之后用户通过shell键入./hello命令开始执行hello程序,shell通过fork函数创建一个子进程,再由子进程执行execve函数加载hello。以上就是hello从源程序到一个被执行的进程的P2P(program to process)过程了。

在execve执行hello程序后,内核为hello进程映射虚拟内存。在hello进入程序入口之后,hello相关的数据被内核加载到物理内存中,hello程序开始正式被执行。内核还需要为hello分配时间片、逻辑控制流。最后,当hello程序运行结束,终止成为僵死进程后,由shell回收hello进程,在系统中删除与hello有关的数据内容。这便是hello的020(From Zero-0 to Zero-0)过程。

1.2 环境与工具

硬件环境:Intel Core i7 8750H CPU、3.0GHz、16G RAM、512G HD Disk

软件环境:Windows11 64位、Vmware15.9、Ubuntu 20.04

开发与调试工具:Codeblocks20.4、Visual Studio2022、gcc、Objdump、edb等

1.3 中间结果

文件名

文件作用

hello.c

程序的源代码                   

hello.i

hello.c文件预处理后的文本文件

hello.s

hello.i文件编译后的汇编文件

hello.o

hello.s汇编后的可重定位目标文件

hello

hello.s链接后的可执行文件

1.4 本章小结

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

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理器cpp根据以字符#开头的命令,修改原始的C程序。比如hello.c 中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。或是比如对宏#define PI 3.14,就将程序中的所有PI替换成3.14。预处理的结果就得到了另一个C程序,通常是以.i作为文件扩展名。

图2.1-1 hello.c中包含的头文件

预处理的作用:

1.实现条件编译,通过预处理可以实现部分代码的在某些条件下的选择性编译。如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

2.实现宏定义,在预处理阶段用定义的实际数值将宏替换。

3.实现头文件引用,将头文件的内容复制到源程序中以实现引用。如#include "FileName"或者#include 等。该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。

4.实现注释,将c文件中的注释从代码中删除。

5.实现特殊符号的使用。如处理#line、#error、#pragma以及#等。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

预处理命令:gcc hello.c -E -o hello.i

hello.c进行预处理结果如下:

图2.2-1 Ubuntu下的预处理命令

2.3 Hello的预处理结果解析

       在运行2.2的预处理命令后,我们可以看到shell当前加载的目录下产生了新文件hello.i

图2.3-1 生成的hello.i文件

打开hello.i文件后,我们可以看到,hello.i的内容较hello.c多了很多,这是因为预处理后,包含的头文件中的内容被复制在了.i文件中,同时我们还可以发现,.c文件中的注释行被删除了。

图2.3-2 hello.c与hello.i内容对比

2.4 本章小结

介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。展示如何在ubuntu下用gcc对.c文件进行预处理,为文件后续的操作打下基础。

第3章 编译

3.1 编译的概念与作用

编译的概念:在编译阶段,编译器检查是否有语法错误,检查无误后,编译器将.i文件翻译成.s文件,它包含一个汇编语言程序,该程序包含main函数的定义。

编译的作用:汇编语言程序为不同高级语言的不同编译器提供了通用的输出语言,为低级机器语言。编译将高级语言转化为汇编语言能够检测代码的正确性,并为接下来将汇编语言生成机器可识别的机器码做准备。

3.2 在Ubuntu下编译的命令

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

图3.2-1 Ubuntu下编译的命令

3.3 Hello的编译结果解析

图 3.3‑0‑1 hello的编译结果(1)

 

图 3.3-0‑2 hello的编译结果(2)

得到的汇编代码如上图3.3.0-1与3.3-2所示。

3.3.1初始伪指令与主函数开头

图3.3.1-1 伪指令与主函数开头的汇编语言代码

所有以’.’开头的行都是指导汇编器和链接器工作的伪指令。

.file:声明源文件

.text:代码节

.section:    .rodata:只读代码段

.align 8:数据或者指令的地址对齐方式

.string:声明一个字符串(.LC0,.LC1)

.global:声明全局变量(main)

.type:声明一个符号是数据类型还是函数类型(此处@function表示main为函数类型)

3.3.2 数据:

(1)字符串:

程序中有两个字符串,两个字符串都在只读数据段中,如上图3.3.1-1中的.LC0,.LC1所示,两个字符串作为printf的参数。字符串被放在只读数据段中。

(2)局部变量i

Main函数声明了局部变量i,编译器进行编译时将变量放置在堆栈中,通过rbp的相对偏移来访问。

图3.3.2‑1 局部变量i的相对偏移访问

3)形参有符号数argc、字符数组首地址*argv

       如图3.3.2-2,符号数argc和字符型数组指针argv,根据寄存器使用规则,这两个参数分别通过%edi和%rsi传递。通过rbp的偏移量来访问。在程序最开始,为main函数建立栈帧,并完成参数传递。Argc存放在-20(%rbp),argv作为main函数的参数,数组的元素都是指向字符类型的指针,起始地址存放在栈中-32(%rbp)的位置。

图3.3.2‑2 形参argc、*argv的汇编表示

(4)数组:

c代码中的数组访问有argv[1]、argv[2]、argv[3],在汇编代码中访问这三个量是通过数组首地址加偏移量的方式实现的,具体汇编代码如图3.3.2-3所示。

图3.3.2‑3数组元素访问的汇编表示

3.3.3 操作

(1)关系判断操作(条件转移):

argc!=4;是在一条件语句中的条件判断argc是否为4,进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),如图3.3.3-1所示,同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。

图3.3.3‑1 argc !=4关系判断操作的汇编代码

       i<8;在hello.c作为判断循环条件,被编译为:cmpl $7,-4(%rbp),如图3.3.3-2所示,计算 i与7是否相等,然后设置条件码,为下一步利用jle条件码进行跳转做准备,若不相等,则跳转到.L4,相等则运行下面的部分。

图3.3.3‑2 i<8关系判断操作的汇编代码

(2)赋值操作:

在原本c程序中的赋值操作只有i=0这个赋值语句。在汇编中是通过mov立即数的方式体现的,movb:一个字节;movw:两个字节;movl:四个字节;movq:八个字节如图3.3.3-3:

                                                

图3.3.3‑3 i=0赋值操作的汇编代码

在汇编器处理一些c语言代码的过程中也会产生一些赋值操作,还有通过以lea(地址传递)来赋值的方式,如图3.3.3-4所示:

图3.3.3‑4 lea地址传递赋值操作的汇编代码

(3)算数操作

       hello.c中进行了i++的算术操作,i为int类型。在汇编代码中是通过add来实现的,如图3.3.3-5所示:

图3.3.3‑5 i++算数操作的汇编代码

(4)类型转换

Hello.c中的sleep(atoi(argv[3]));语句存在atoi()的类型转换,在hello.c中用atoi将字符串转化成int型,在hello.s中用call语句调用atoi函数强制处理该类型转换,如图3.3.-6所示。

图3.3.3‑5 atoi类型转换的汇编操作的汇编代码

5)函数操作:

在hello.c中多处涉及函数操作,调用另一个函数来执行当前任务,如printf、atoi、getchar、sleep、exit,在hello.s中对此的处理均是先完成参数传递(有入口参数的情况下),然后在用call语句转到相应函数的入口处继续执行。

printf:%rdi传递:

图3.3.3‑6 printf函数的汇编操作的汇编代码

Exit:%edi传递:

图3.3.3‑7 exit函数的汇编操作的汇编代码

Atoi:用%rdi:

图3.3.3‑8 atoi函数的汇编操作的汇编代码

Sleep:用%edi传递:

图3.3.3‑9 sleep函数的汇编操作的汇编代码

getchar:(无传递参数)

图3.3.3‑10 getchar函数的汇编操作的汇编代码

函数返回:返回时,若有返回值,在返回之前会将返回值存放到%eax中,否则直接利用ret返回。在返回之前要恢复栈帧,删除被调用函数的栈帧,恢复调用函数的栈帧。函数返回时一个非常重要的指令为leave指令,leave指令的作用就是将被调用函数的栈帧抹除,实现过程如下图所示:

图3.3.3‑11函数返回操作的汇编代码

3.4 本章小结

本章介绍了.i文件被编译为汇编代码的过程。编译器通过词法分析和语法分析,来检查原始代码有没有错误,在确认没有错误之后,编译器会按照一定的规范生成与原始代码等价的汇编代码,在这个过程中,编译器可能会按照自己的理解,对原始代码结构和数据做出调整。

第4章 汇编

4.1 汇编的概念与作用

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

       作用:将.s文件生成.o格式机器码,使其能被链接器ld链接生成可执行文件。

4.2 在Ubuntu下汇编的命令

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

4.2‑1 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

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

4.3.1 ELF格式的具体内容

图4.3.1‑1 ELF格式的具体内容

4.3.2 ELF

打开ELF 的指令:readelf -a hello.o

打开之后,可以看到ELF文件从ELF头开始,如下图4.3.2-1所示,ELF头以一个16个字节的序列开始,这个序列描述了生成该文件的系统的字大小和字节顺序。在该ELF头中显示,以小端码机器存储,文件类型为可重定位目标文件。

4.3.2‑1 hello.o的ELF头内容

       ELF头中的Magic魔数是一个定值,在程序执行时会检查魔数是否正确,如果不正确则拒绝加载。ELF头告诉了我们文件的基本信息,类别是ELF64,文件中的数据是按照2的补码储存的,小端序,文件的类型是可重定位文件,节头大小为64字节,节头的数量为14个。

       4.3.3 节头表

节头表告诉了我们每个节的大小、名称、类型、读、写、执行权限以及对其方式。由于我们的程序还未进行链接,因此每个节的起始位置都是0,在链接后会为每个节进行重定位以获得起始位置。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,       

4.3.3‑1 节头表的内容

4.3.4 符号表

存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。同样由于还未进行链接重定位,偏移量Value还都是0。结果如下图4.3.4-1所示。

图4.3.4‑1 符号表的内容

4.3.5 重定位节

       

图4.3.5‑1 重定位节的内容

重定位节包含了.text文件中需要重定位的信息,在链接器将目标文件与其它文件进行链接时需要修改这些信息,可执行文件中不包含重定位节。

Offset:需要被修改的引用节的偏移Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,

symbol:标识被修改引用应该指向的符号,

type:重定位的类型

Type:告知链接器应该如何修改新的应用

Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整

Name:重定向到的目标的名称。

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o 

分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

4.4‑1 hello.o反汇编的内容

反汇编代码和汇编代码在指令格式上非常相似,但在以下几个方面存在不同:

1.立即数的引用不同,在反汇编中立即数是十六进制的,而汇编代码则是十进制。

2.反汇编的语言mov、add、push后无其他字符如b、w等。

3.子程序的调用不同,反汇编代码中子程序的调用是通过对主函数地址的相对偏移进行的,而在汇编代码中则是通过call直接加上函数名的方法进行的。

4. 对全局变量的访问方式:反汇编语言通过pc相对寻址,通过rip+x的值进行访问,未重定位,故用0占位。在汇编语言中,通过.LC0+rip进行访问。

5.分支跳转不同,在反汇编代码中,分支转移是通过跳转到以主函数地址为基址的一个偏移地址中,而在汇编代码中则是通过.L4、.L3这样分块的方式来跳转的。

综上所述,反汇编代码与汇编代码在指令上是一一对应的关系,只有在一些特殊的指令需要有引用的转化,其他地方几乎完全一致。

4.5 本章小结

本章介绍了编译过程,该过程将汇编语言翻译成机器语言,以供计算机识别并执行相关指令,得到了.o文件,.o文件是可重定位目标程序,可以通过readelf来查看其信息,剖析了这些内容在接下来的链接过程中能起什么作用,并对比了汇编与反汇编代码有何异同。但是它还不是可执行程序,需要进一步的链接来生成可执行程序,即从hello.o到hello。

第5章 链接

5.1 链接的概念与作用

链接的概念:链接是指在电子计算机程序的各模块之间传递参数和控制命令,并把它们组成一个可执行的整体的过程。链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存中并执行。链接可以执行于编译时、也就是源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时。也就是由应用程序来执行。总之是把多个文件拼接合并成一个可执行文件。

链接的作用:链接可以在编译、汇编、加载和运行时执行。链接方便了模块化编程。链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。

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‑1 Ubuntu下进行链接的命令

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

5.3.1 ELF

 

图5.3.1‑1 hello.o的ELF头内容

可以看到,可执行文件的ELF头与可重定位目标文件的ELF头有以下几个不同:

1.文件的类型不同,可执行文件的类型不再是REL而是EXEC。

2.程序的入口点不一样,因为连接上了库文件,使得main函数不再是从0x0开始。同理节头的开始位置也发生了变化。

3.节头的数量产生了变化。

5.3.2 节头表

图5.3.2‑1 hello.o的节头表内容(部分)

节头表对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息我们就可以用HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。可以看到与hello.o不同,在可执行文件中经过重定位每个节的地址不再是0,而是根据自身大小加上对齐规则计算的偏移量。

5.3.3 符号表

Hello的符号表如下图5.3.3-1所示

图5.3.3‑1 hello.o的符号内容(部分)

通过观察我们可以发现,在可执行文件中多出了.dynym节。这里面存放的是通过动态链接解析出的符号,这里我们解析出的符号是程序引用的头文件中的函数。

5.3.4 重定位节

图5.3.4‑1 hello.o的重定位节内容(部分)

可以看出,由于链接了别的头文件,重定位节的偏移量与hello.o已经完全不一样了。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。 

 根据节头表的信息,我们可以知道ELF是从0x400000开始的,如图5.4-1所示。查看edb,可以看出hello的虚拟地址空间开始于0x400000,结束于0x40ff0  

5.4.‑1 elf的虚拟地址空间

通过节头部表,找到edb的各个节的信息,如.text,虚拟地址开始于0x4010f0,字节偏移为0x10f0

图5.4.‑2 .text的虚拟地址空间

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

首先,可以看到的不同是文件中多了很多函数,一些在hello.o中没有的函数,这些都是在hello.c中没有定义却直接使用的函数,这些函数定义在共享库中,在链接时完成了符号解析和重定位,如printf、sleep等链接的时候将头文件链接到了可执行文件中。

其次,在hello.o中call、jmp指令后紧跟着的是相对地址,而hello中紧跟的是虚拟内存的确定地址,原因在于链接器完成了重定位过程,可以确定运行时的地址。在hello.o反汇编代码中出现以main加上相对偏移的跳转已经全部被重写计算,这是因为在重定位后main函数有了全新的地址,使得这个计算成为可能。同时对子函数的call引用也在重定位后重写计算来了。

 

5.5 链接的重定位过程分析

   综上所述,重定位的大体过程是链接器ld将所有链接文件中相同的节合并,并按照要求计算新的偏移地址赋值给新的节。同时链接器按链接指令的顺序搜索符号表,查找符号引用。

hello重定位的过程:

(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

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

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

从加载hello到_start,到call main,以及程序终止的所有过程记录如下表,包括跳转的各个子程序名和程序地址。

程序名称

地址

ld-2.32.so!_dl_start

0x7f96ed2e1ea0

ld-2.32.so!_dl_init

0x7f96ed2f0630

hello! _start

0x400500

libc-2.32.so! __libc_start_main

0x7fbdf0cccab0

hello!puts@plt

0x401030

hello!exit@plt

0x401060

hello!printf@plt

0x401050

hello!sleep@plt

0x401070

hello!getc@plt

0x401080

libc-2.32.so!exit

0x7f66b7b9e120

5.7 Hello的动态链接分析

在程序中动态链接是通过延迟绑定来实现的,延迟绑定的实现依赖全局偏移量表GOT和过程连接表PLT实现。GOT是数据段的一部分,PLT是代码段的一部分。

PLT数组中每个条目时16字节,PTL[0]是一个特殊的条目,他跳转到动态链接器中。每个可被执行程序调用的库函数都有自己的PLT条目。PLT[1]调用_libc_start_main函数负责初始化。GOT数组中每个条目八个字节。GOT[0]和GOT[1]中包含动态链接器解析地址时会用的信息,GOT[2]时动态练级去在ld-linux.so模块的入口点。其余的每一个。

通过5.3.2的节头表我们可以找到.GOT.PLT的地址为0000000000404000

图5.7-1  节头表中.GOT.PLT的地址

       在运行dl_start和dl_init之前,GOT.PLT表如图5.7-2所示

图5.7-2  运行dl_start之前.GOT.PLT表

调用dl_start之后,.GOT.PLT表变为图5.7-3所示。

图5.7-3  运行dl_start之后.GOT.PLT表

动态链接是一项有趣的技术。考虑一个简单的事实,printf,getchar这样的函数实在使用的太过频繁,因此如果每个程序链接时都要将这些代码链接进去的话,一份可执行目标文件就会有一份printf的代码,这是对内存的极大浪费。为了遏制这种浪费,对于这些使用频繁的代码,系统会在可重定位目标文件链接时仅仅创建两个辅助用的数据结构,而直到程序被加载到内存中执行的时候,才会通过这些辅助的数据结构动态的将printf的代码重定位给程序执行。即是说,直到程序加载到内存中运行时,它才知晓所要执行的代码被放在了内存中的哪个位置。这种技术被称为延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。而那两个辅助的数据结构分别是过程链接表(PLT)和全局偏移量表(GOT),前者存放在代码段,后者存放在数据段。

5.8 本章小结

本章介绍了程序链接已经加载动态库的过程。这是程序生成可执行文件的最后一步,也是将大型程序项目分解成小模块的关键所在。本章通过可执行文件的程序头来分析重定位的过程,并解析了一个程序运行的全过程。最后简单介绍了动态链接这一现代计算机中极为重要的部分是怎么运作的。

第6章 hello进程管理

6.1 进程的概念与作用

进程就是一个执行中程序的实例.每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程。其作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象.进程也被称为计算机科学中最伟大的创新。

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

shell的作用:shell作为UNIX的一个重要组成部分,是它的外壳。也是用户与UNIX系统的交互作用界面。Shell是一个命令解释程序。除此,它还是一个高级程序设计语言。用shell编写的程序称为shell过程。shell的一项主要功能是在交互方式下解释从命令行输入的命令。shell的另一项重要功能是制定用户环境,这通常在shell的初始化文件中完成。shell还能用作解释性的编程语言。

处理流程:

1、从终端读入输入的命令。

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

3、如果是内置命令则立即执行

4、否则调用相应的程序执行

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

6.3 Hello的fork进程创建过程

首先我们需要打开shell,使用”./hello 7203610830 王一鸣 1”命令来运行hello程序。 由于我们输入的不是一条内置命令,因此为了执行我们的命令,shell会通过fork函数创建一个子进程。这样通过shell,我们的hello子进程就被创建了。如图6.3-1所示。

6.3-1  通过fork函数创建进程

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

6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。

execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。利用虚拟内存部分的知识,我们还能知道execve在运行时需要以下几个步骤:1.删除已经存在的用户区域。2.映射私有区域。3.映射共享区域。4.设置程序计数器。

6.4  通过execve函数创建进程

6.5 Hello的进程执行

逻辑控制流:即使在系统中通常有许多其他程序正在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果使用调试器单步调试执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流。

并发流:系统为每个程序都提供了一种只有它一个程序在运行的假象,但是实际情况却不是这样的,系统中很有很多其他程序在运行,比如我现在打字的word和我的虚拟机就是两个程序,它们都在运行。那么处理器是如何执行它们的,以至于让它们看起来都在不间断的一直运行呢?答案就是并发,如图6.5-1,处理器分时间段执行进程A、B、C,这个转换的时间非常短,所以看起来就好像每个进程都在持续不断的在运行。多个逻辑控制流并发执行的一半现象被称为并发。一个进程和其他进程轮流运行的概念成为多任务,一个进程执行它的控制流的每一时间段就成为时间片。如图6.5-1中进程A就由两个时间片组成。

6.5-1  进程时间片示意图

       内核模式和用户模式:内核模式和用户模式不是两个进程,而是一个进程的不同模式,由一个模式位来控制,当设置了模式位时,进程就运行在内核模式中,这时候这个进程就可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。运行程序代码的进程一开始是处于用户模式,只有当发生中断、故障或者陷入系统调用这样的异常时,转而去执行异常处理程序,这时进程才会变为内核模式。当它返回到应用程序代码时,处理器就把模式从内核模式改为用户模式。

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

上下文切换:内核为每个进程维持一个上下文,上下文就是在进程执行的某些时刻,内核可以决定枪战当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度。系统调用和中断也可能引发上下文切换。

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

图6.5-2  hello的工作机制示意图

6.6 hello的异常与信号处理

6.6.1 异常与信号的简述

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

异常可以分为如下四类:中断、陷阱、故障和终止。异常的同步异步指的是异常的发生和程序的关系。比如从键盘输入crtl+c作为异步异常与程序的执行没有关系。而缺页异常这样的同步异常是随着程序的执行产生的。

在hello程序执行的时候如果从键盘键入ctrl+c这样就会受到SIGINT的终止信号,其他来自键盘的信号也类似。在hello结束的时候会向shell发送SIGCHLD信号告诉shell自己运行结束了。

6.6.2 通过键盘向hello发送信号

(1)乱按

乱按后,运行结果如图6.6.2-1所示

6.6.2-1  乱按后hello运行结果图

可以看到,虽然乱按不会影响程序的运行,但是会在程序运行结束后对shell发送许多无效指令,不过要注意因为hello程序最后有一个getchar,因此第一个乱按的指令被getchar给读走了,不会成为发送给shell的无效指令。因为我们可以判断,我们乱按的内容被放入缓冲区,等待程序执行结束被shell当作命令读走。

(2)Crtl+z

输入Crtl+z,结果如图6.6.2-2所示

图6.6.2-2  Crtl+z后hello运行结果图

可以看到输入ctrl+z后程序被放入后台并暂停运行。

(3)Crtl+C

我们按动Crtl+c之后,我们可以看到如图6.6.2-3所示的结果

图6.6.2-3  Crtl+c后hello运行结果图

       输入ctrl+c后程序直接结束运行,回到shell等待输入下一条指令。

       (4)ps命令

       在hello程序已经被Crtl+z暂停后,我们键入ps命令,可以看到如图6.6.2-4的结果。通过ps指令我们可以看到当前在运行的进程及其pid。

图6.6.2-4  ps命令后hello运行结果图

(5)jobs命令

如图6.6.2-5所示,通过jobs命令我们可以看到所有在执行的命令。

图6.6.2-5  jobs命令后hello运行结果图

       (6)pstree命令

如图6.6.2-6所示,通过pstree我们可以看到所有进程之间的父子关系,可以看到我们的hello进程是shell(bash)创建的进程。

图6.6.2-6  pstree命令后hello运行结果图

       (7)fg命令

如图6.6.2-7所示,通过执行fg命令我们可以让暂停的进程重新开始工作。

图6.6.2-7  fg命令后hello运行结果图

       (8)kill命令

如图6.6.2-8所示,可以看到kill成功的杀死了一个进程,fg无法将其唤醒。

图6.6.2-8  kill命令后hello运行结果图

6.7本章小结

可以说现代计算机离不开进程的概念,只有充分了解进程的创建与异常流控制,才可能成为一个优秀的程序员。这章我们讲述了一个进程是怎么在计算机中被创建的,一个程序是怎么通过子进程被执行的,这是P2P中的最后一步process。这一章我们还介绍了异常与信号,并实际对hello用各种信号进行测试,来了解常用信号的用途。

一个系统中有成百上千个程序在同时运行,那么如何管理它们,让它们既能互不影响的运行,又能在必要的时候进行通信就是一个很重要的问题。一个系统要能够有效的运行,它必须建立一个简单但有效的模型。计算机系统为了解决这个问题,提供了两个抽象:进程让每个程序都以为只有它自己在运行,虚拟内存让每个程序都以为它自己在独占整个内存空间。这两个抽象使得计算机系统能够对每个程序都够以一致的方式去管理。多任务就通过进程之间快速的切换来实现,程序之间的影响就通过进程之间的通信——信号来实现。

第7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址(physical address)用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相相应。是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

逻辑地址(Logical Address)是指由程式产生的和段相关的偏移地址部分。表示为 [段标识符:段内偏移量]。

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

虚拟地址(Virtual Address)虚拟内存为每个程序提供了一个大的、一致的和私有的地址空间。其每个字节对应的地址成为虚拟地址。虚拟地址包括 VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB 索引)、TLBT(TLB 标记)。

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

逻辑地址空间表示:段地址:偏移地址。段地址+偏移地址=线性地址

在实模式下:逻辑地址 CS:EA=CS*16+EA 物理地址

在保护模式下:以段描述符作为下标,到 GDT/LDT 表查表获得段地址,

段内偏移量是在链接后就已经得到的 32 位地址,因此要想由逻辑地址得到线性地址,需要根据逻辑地址的前 16 位获得段地址,这 16 位存放在段寄存器中。

段寄存器(16 位):用于存放段选择符。CS(代码段):程序代码所在段;SS(栈段):栈区所在段;DS(数据段):全局静态数据区所在段;其他三个段寄存器 ES、GS 和 FS 可指向任意数据段。

段选择符中字段的含义如图7.2-1所示:

图7.2-1  段选择符中字段的含义

其中 CS 寄存器中的 RPL 字段表示 CPU 的当前特权级

TI=0,选择全局描述符表(GDT);TI=1,选择局部描述符表(LDT);RPL=00 为第 0 级,位于最高级的内核态;RPL=11 为第 3 级,位于最低级的用户态。高 13 位-8K 个索引用来确定当前使用的段描述符在描述符表中的位置。

图7.2-2  段描述符示意图

段描述符是一种数据结构,等价于段表项,分为两类。一类是用户的代码段和数据段描述符,一类是系统控制段描述符。

描述符表:实际上为段表,由段描述符(段表项构成)分为三种类型:

全局描述符表 GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状态段)等都属于 GDT 中描述的段

局部描述符表 LDT:存放某任务(即用户进程)专用的描述符

中断描述符表 IDT:包含 256 个中断门、陷阱门和任务门描述符

下图展示了逻辑地址到线性地址的转化过程:

图7.2-3 逻辑地址到线性地址的转化过程

首先根据段选择符的 TI 部分判断需要用到的段选择符表是全局描述符表还是局部描述符表,随后根据段选择符的高 13 位的索引(描述符表偏移)到对应的描述符表中找到对应的偏移量的段描述符,从中取出 32 位的段基址地址,将 32 位的段基址地址与 32 位的段内偏移量相加得到 32 位的线性地址。

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

Linux下,虚拟地址到物理地址的转化与翻译是依靠页式管理来实现的,虚拟内存作为内存管理的工具。概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。磁盘上数组的内容被缓存在物理内存中(DRAM cache)这些内存块被称为页 (每个页面的大小为P = 2p字节)。

   而分页机制的作用就是通过将虚拟和物理内存分页,并且通过MMU建立起相应的映射关系,可以充分利用内存资源,便于管理。一般来说一个页面的标准大小是4KB,有时可以达到4MB。而且虚拟页面作为磁盘内容的缓存,有以下的特点:DRAM缓存为全相联,任何虚拟页都可以放置在任何物理页中需要一个更大的映射函数,不同于硬件对SRAM缓存更复杂精密的替换算法太复杂且无限制以致无法在硬件上实现DRAM缓存总是使用写回,而不是直写。

    虚拟页面地集合被分为三个不相交的子集:已缓存、未缓存和未分配。

图7.3-1 虚拟页面的示意图

页表实现从虚拟页到物理页的映射,依靠的是页表,页表就是是一个页表条目 (Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址。这个页表是常驻与主存中的。

图7.3-2 页表的示意图

下图展示了页式管理中虚拟地址到物理地址的转换:       

图7.3-3 页式管理中虚拟地址到物理地址的转换示意图

下图a展示了当页面命中时,CPU硬件执行的步骤:

第1步:处理器生成一个虚拟地址,并把它传送给MMU;

第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它;

第3步:高速缓存/主存向MMU返回PTE;

第4步:MMU构造物理地址,并把它传送给高速缓存/主存;

第5步:高速缓存/主存返回所请求的数据字给处理器

处理缺页如图b所示:

第1~3步:和图a中的第1步到第3步相同;

第4步:PTE中的有效位是零,所以MMU触发了一次异常,传给CPU中的控制到操作系统内核中的缺页异常处理程序;

第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘;

第6步:缺页处理程序页面调入新的页面,并更新内存中的PTE;

第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了图b中的步骤之后,主存就会将所请求字返回给处理器。

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

为了消除每次 CPU 产生一个虚拟地址,MMU 就查阅一个 PTE 带来的时间开销,许多系统都在 MMU 中包括了一个关于 PTE 的小的缓存,称为翻译后被缓冲器(TLB),TLB 的速度快于 L1 cache。

图7.4-1  TLB索引示意图

TLB 通过虚拟地址 VPN 部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU 在读取 PTE 时会直接通过 TLB,如果不命中再从内存中将PTE 复制到 TLB。同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩页表大小。core i7 使用的是四级页表。

图7.4-2  页表工作机制示意图

在四级页表层次结构的地址翻译中,虚拟地址被划分为 4 个 VPN 和 1 个 VPO。每个 VPNi 都是一个到第 i 级页表的索引,第 j 级页表中的每个 PTE 都指向第 j+1级某个页表的基址,第四级页表中的每个 PTE 包含某个物理页面的 PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN 之前,MMU 必须访问四个PTE。

图7.4-3  四级页表工作机制示意图

综上,在四级页表下,MMU 根据虚拟地址不同段的数字通过 TLB 快速访问得到下一级页表的索引或者得到第四级页表中的物理页表然后与 VPO 组合,得到物理地址(PA)。

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

Core i7的内存系统如图7.5.1所示。

7.5-1  Core i7内存系统示意图

首先,根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为 1,若有,则说明命中,从这一行对应物理地址 b 位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级 cache,访问的原理与L1相同,若是三级 cache 都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。

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

7.6 hello进程fork时的内存映射

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。

图7.6  hello进程fork时的内存映射

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 缺页故障与缺页中断处理

DRAM缓存不命中称为缺页,即虚拟内存中的字不在物理内存中。CPU引用了虚拟页的一个字,地址翻译硬件从内存中读取了该虚拟页对应的页表条目,从有效位推断出该页未被缓存,这样就触发了一个缺页异常,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,把要缓存的页缓存到牺牲 页的位置。如果这个牺牲页被修改过,就把它交换出去。当缺页处理程序返回时, CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。

下图体现对VP3的引用不命中,从而触发缺页。

图7.8-1  对VP3的引用不命中从而触发缺页

缺页之后,缺页处理程序选择VP4作为牺牲页,并从磁盘上用VP3的副本取代它。在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器分为两种基本风格:显式分配器、隐式分配器。

1. 显式分配器:要求应用显式地释放任何已分配的块。

2. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这 个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

带边界标签的隐式空闲链表分配器原理:

图7.9-1  带边界标签的隐式空闲链表分配器原理

每个块增加四字节的头部和四字节的脚部保存块大小和是否分配信息,可以在 常数时间访问到每个块的下一个和前一个块,使空闲块的合并也变为常数时间,而且可以遍历整个链表。隐式空闲链表即为,利用边界标签区分已分配块和未分配块,根据不同的分配策略(首次适配、下一次适配、最佳适配),遍历整个链表,一旦找到符合要求的空闲块,就把它的已分配位设置为1,返回这个块的指针。隐式空闲链表并不是真正的链表,而是"隐式"地把空闲块连接了起来(中间夹杂着已分配块)。

显式空闲链表的基本原理:

图7.9-2  显式空闲链表的基本原理

因为隐式空闲链表每次查找空闲快都需要线性地遍历整个链表,而其中的已分配块显然是不需要遍历的,所以浪费了大量时间,一种更好的方式是把空闲块组织成一个双向链表,每个空闲块中包含一个 pred 和 succ 指针,指向它的前驱和后继,在申请空闲块时,就不需要遍历整个堆,只需要利用指针,在空闲链表中遍历空闲块即可。一旦空闲块被分配,它的前驱和后继指针就不再有效,变成了有效载荷的一部分。显式空闲链表的已分配块与隐式空闲链表的堆块的格式相同。

7.10本章小结

本章介绍了hello的存储管理机制。讨论了虚拟地址、线性地址、物理地址,介绍了段式管理与页式管理、VA 到 PA 的变换、物理内存访问,以及 hello 进程 fork 、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等。需要理解存储器层次结构,这部分内容十分重要,同时也很难以理解,需要我们花费较长时间去消化,但是这是值得的,因为它对应用程序的性能有着巨大的影响。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

文件的类型有:

1. 普通文件:包含任何数据,分两类

i. 文本文件:只含有 ASCII 码或 Unicode 字符的文件

ii. 二进制文件:所有其他文件

2. 目录:包含一组链接的文件。每个链接都将一个文件名映射到一个文件

3. 套接字:用于与另一个进程进行跨网络通信的文件

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O 接口的几种操作:

1. 打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。

2. shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。

3. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作显式地设置文件的当前位置为k。

4. 读写文件:一个读操作就是从文件复制 n>0个字节到内存,从当前文件位置k 开始,然后将k增加到 k+n。给定一个大小为m字节的文件,当 k>=m时执行读操作会出发一个称为 EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF 符号。

5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终 止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O 函数:

 1. int open(char *filename, int flags, mode_t mode);

open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。

 2. int close(int fd);

关闭一个打开的文件。

 3. ssize_t read(int fd, void *buf, size_t n);

read 函数从描述符为 fd 的当前文件位置赋值最多n个字节到内存位置 buf。返回值-1表示一个错误,0表示 EOF,否则返回值表示的是实际传送的字节数量。

4. ssize_t write(int fd, const void *buf,size_t);

write 函数从内存位置 buf 复制至多n个字节到描述符fd的当前文件位置。

8.3 printf的实现分析

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

先找到 printf 的函数定义:

图8.3-1 printf的函数定义

其中 va_start()和 va_end 是获取可变长度参数的函数,任何可变长度的变元被访问之前,必须先用 va_start()初始化变元指针 argptr。初始化 argptr 后,经过对va_arg()的调用,以作为下一个参数类型的参数类型,返回参数。最后取完所有参数并从函数返回之前。必须调用 va_end()。由此确保堆栈的正确恢复。然后,printf 调用了 write 函数,这是 Unix I/O 函数,用以在屏幕输出长度为 i 的在 printbuf 位置的字节。这里 i = vsprintf(printbuf, fmt, args),,所以关键在于 vsprintf 函数。

图8.3-2 vsprintf的函数定义

vsprintf的功能就是将 printf 的参数按照各种各种格式进行分析,将要输出的字符串存在buf中,最终返回要输出的字符串的长度。

接着就轮到write系统函数了,在Linux下,write函数的第一个参数为fd,也就是描述符,而1代表的就是标准输出。查看write函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后执行int INT_VECTOR_SYS_CALL,代表通过系统调用 syscall,syscall 将寄存器中的字节通过总线复制到显卡的显存中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。由此 write 函数显示一个已格式化的字符串。

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

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

8.4 getchar的实现分析

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

再看getchar的代码:

int getchar(void)

{

static char buf[BUFSIZ];

static char* bb=buf;

static int n=0;

if(n==0)

{

n=read(0,buf,BUFSIZ);

bb=buf;

}

return(--n>=0)?(unsigned char)*bb++:EOF;

}

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

可以看到,getchar 调用了 read 函数,read 函数也通过 sys_call 调用内核中的系统函数,将读取存储在键盘缓冲区中的 ASCII 码,直到读到回车符,然后返回整个字符串,getchar 函数只从中读取第一个字符,其他的字符被缓存在输入缓冲区。

8.5本章小结

本章主要讲述了Linux的I/O设备管理方法,Unix I/O接口及其函数,以及printf函数实现的分析和getchar函数的实现。

结论

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

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

hello所经历的过程:

编写:通过编辑器输入hello.c的C语言代码

预处理:预处理器对hello.c处理生成hello.i文件

编译:编译器编译hello.i将其转化成汇编语言描述的hello.s文件

汇编:汇编器将hello.s文件翻译成可重定位文件hello.o

链接:链接器将hello.o和其他目标文件进行链接,生成可执行文件hello

运行:在shell中输入./hello 7203610830 王一鸣,开始运行hello程序

创建新进程:shell为hello程序fork一个新进程

加载:在新进程中调用execve函数,将hello程序映射到虚拟内存中

执行:内核调度该进程执行,进行虚拟地址的翻译,此时会发生缺页,  开始加载hello代码和数据到对应的物理页中,然后开始执行。

信号处理:在hello进程运行中,按下ctrl+z、ctrl+c等将会发送信号给hello,进而调用信号处理程序进行处理。

终止:输出完8遍对应的字符串后,执行getchar,等待用户输入,输入字符按下回车后,hello进程终止。

回收:hello进程终止后发送SIGCHLD信号给shell,shell将其退出状态 进行回收,最后内核从系统中删除hello所有的信息

感悟:即使是一个再简单不过的程序,它的运行也包含了众多操作,需要软硬件配合、内核与操作系统协作。计算机系统的设计与实现蕴含了多年以来众多技术人员的经验与智慧,通过不断地完善发展,才有了现在比较完备的体系。

计算机系统是一个非常精细的系统,比如流水线精确到每个几微秒周期;它还是非常巧妙的,比如引入了缓存的概念。我不由赞叹计算机系统的设计者的高超智慧。在这门课我学到了很多东西,感谢一学期以来老师的辛勤付出以及耐心答疑。路漫漫其修远兮,就像hello一样,我只是刚刚经过了预处理,后面的路还长呢~

附件

hello.c

hello源代码

hello.i

预处理之后的文本文件

hello.s

hello的汇编代码

hello1.s

hello.o的反汇编代码

hello2.s

hello的反汇编代码

hello.o

hello的可重定位文件

hello

hello的可执行文件

hello.elf

hello的elf文件

hello1.elf

hello.o的elf文件

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]    大卫R.奥哈拉伦,兰德尔E.布莱恩特. 深入理解计算机系统[M]. 机械工业出版社,2016.

[2]   jiangxt211. C预处理.

Available at C预处理_jiangxt211的博客-CSDN博客

[3] https://blog.csdn.net/weixin_45406155/article/details/103775420

[4] https://blog.csdn.net/alanwalker1/article/details/103848576

[5] https://blog.csdn.net/weixin_44819348/article/details/103772429

(参考文献0分,缺失 -1分)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值