计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业    人工智能未来技术     

摘  要

HelloWorld这个简单的程序蕴含着计算机系统的许多奇妙的知识、神奇的机制,尽管看上去简单,但它是无数程序们的天才的创造、思想的结晶。本文通过逐步分析hello C源代码的预处理、编译、汇编、链接生成可执行程序hello的过程,以及Hello的进程管理、存储管理、IO管理,展示了其中的流程机制,回顾了计算机系统中的所学知识。

关键词:计算机系统;编译系统;进程管理;存储管理;IO管理  

第1章 概述

1.1 Hello简介

P2P(From Program to Process)

程序的编译经历了预处理、编译、汇编、链接四个阶段,生成了可执行程序,其过程为:

·预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。得到以.i作为文件扩展名的C程序。

·编译阶段。编译器(ccl)将文本文件Hello.i翻译成文本文件Hello.s,包含一个汇编语言程序。由编译器把高级语言代码翻译为汇编语言。

·汇编阶段。汇编器(as)将Hello.s翻译成机器语言指令,将结果保存于Hello.o文件。

·链接阶段。由链接器把多个.o文件合并,得到Hello可执行目标文件。

在生成可执行文件后,shell加载并运行这个文件,最开始只有shell进程在运行等待命令行输入,当我们让shell运行Hello程序时,操作系统保存shell进程的上下文,由系统调用fork()创建一个新的Hello进程。

020(From Zero-0 to Zero-0)

创建进程后,通过调用execve函数来调用加载器将程序复制到内存并运行,实现加载。通过系统调用mmap将内存映射到文件。由于进程是轮流使用处理器的,这由进程的逻辑流决定,一个进程执行它的控制流的一部分的每一时间段叫做时间片,CPU为进程分配时间片,在程序执行完之后,Hello进程终止,被shell父进程回收,内核将子进程的退出状态传递给父进程,然后抛弃已终止的Hello进程,Hello进程将不存在。完成O2O。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:X64 CPU(AMD Ryzen 7 4800H 2.9GHz) ;16G RAM;476.92GHD Disk

软件环境:Windows 10 64位;  VMware® Workstation 15 Pro;  Ubuntu 18.04.6 LTS

开发与调试工具:GCC编译器,Code Block IDE,  EDB

1.3 中间结果

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

Hello.i 预处理后的文本文件

Hello.s 编译之后产生的汇编文件

Hello.o 可重定位的目标文件

Hello 可执行文件Hello

Hello.objdump Hello的反汇编文件

1.4 本章小结

本章以程序Hello为引子,引出了计算机系统中的软硬件相关的各个章节的知识,带我们总览了计算机系统的工作原理与程序编译运行的过程,起到了抛砖引玉的作用。

第2章 预处理

2.1 预处理的概念与作用

预处理即预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。结果得到了另一个C程序,通常以.i作为文件扩展名。

预处理的作用:用于将所有的#include头文件以及宏定义展开并替换原来的以#开头的命令,得到修改之后的.i文件。

2.2在Ubuntu下预处理的命令

 计算机系统_第1张图片

                                                    图 2.1     预处理命令

在Ubuntu下的预处理命令为:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

在经过预处理之后,hello.c文件生成hello.i文件,程序文本由.c文件的18行变为.i文件的3105行,其中main函数在.i程序文本的最后。之所以会产生这种现象的原因是因为在预处理阶段,预处理器读取了系统头文件#include、#include、#include的内容,并将内容插入到程序文本之中。#include<>表示引用的是编译器的类库路径里面的头文件。

2.4 本章小结

在本章中我们实现预处理源程序文件hello.c生成hello.i文件,我们在Ubuntu下使用预处理命令实现生成hello.i文件。通过查看程序文本的内容,我们发现了在预处理的过程中,预处理器读取了源程序文件hello.c中的系统头文件stdio.h、unistd.h、stdlib.h中的内容,并将它们直接插入到了程序文本之中,得到hello.i。

第3章 编译

3.1 编译的概念与作用

概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。编译也即把高级语言代码编译为汇编代码。

编译的作用:将经过预处理之后的程序转换成汇编代码,得到.s文件。

3.2 在Ubuntu下编译的命令

计算机系统_第2张图片

                                        图3.1   在Ubuntu下编译的命令 

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

3.3 Hello的编译结果解析

3.3.1伪指令部分

首先,在hello.s文件开头中以“.”开头的行都是指导汇编器和链接器工作的伪指令。

计算机系统_第3张图片

                                           图3.2  hello.s文本内容的开头

·“.file “hello.c”表示源文件名为“hello.c”

·“.text”表示text段,在内存中被映射为只读

·“.section .rodata”为只读段,不可执行

·“.aling ”的作用在于对于指令或者数据的存放地址进行对齐。

·“.string”定义了字符串,图中可以两个字符串分别用.LC0和.LC1标识,存放在只读段中

·“.globe main”定义了函数符号,.globe告诉汇编器,这个main符号要被链接器用到,要在目标文件的符号表中标记”main”为一个全局符号

·“.type main,@function”定义了main函数

3.3.2数据部分

  1. 常量

             计算机系统_第4张图片

                                           图3.3 hello.c中的部分源代码 

由hello.c源程序代码可知程序中的printf()操作中的参数为字符串常量“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”。在hello.s文件的汇编语言中我们可以看到这两个字符串常量分别由.LC0和.LC1标识,如图3.4所示。

                                      图3.4 hello.s文件中的字符串常量标识.LC0和.LC1

2、变量

由于在main函数中定义的变量是局部变量,则 argc和 *argv[]以及i为都为局部变量;

变量i:

                                    计算机系统_第5张图片

                                                             图3.5 hello.c 代码

       在hello.c中我们可以看到整型变量i用于main()函数中for循环的记数,我们在hello.s中可以找到这     部分代码对应的汇编语言如下图3.6所示;

                                        计算机系统_第6张图片

                                                  图3.6 for循环对应的汇编代码

由图3.6所示hello.s蓝框内汇编内容可以看出,局部变量i被存储于%rbp-4的内存地址处,初始值为0。接下来jmp指令无条件直接跳转到标号“.L3”处,将i的大小与7进行比较,如果i的大小小于等于7,则跳转到.L4处进行for循环内的操作,如果大于7则终止循环。

变量argc:

                                      计算机系统_第7张图片

                                                      图3.7   hello.c部分代码

  在hello.c代码中我们可以看到在main函数中局部变量argc在if()语句中与4进行了比较,我们可以在hello.s中找到对应的汇编语句,从而判断出局部变量argc的位置;

                                             

                                                     图3.8   hello.s中的比较操作

由如图3.8所示的汇编语句,我们可以看到cmpl指令将%rbp-20中的值与4进行了比较,如果相当则跳转到.L2执行for循环语句中的内容,则我们可以判断出局部变量argc的一开始被存放寄存器%rdi处,然后通过movl指令将数 据传送到(%rbp-20)的地址处。

同理可以判断出局部变量*argv[]的首地址被存放于寄存器%rsi处,之后movq指令将数据传送到%rbp-32地址处。

3.3.3操作部分

  1. 赋值操作

在hello.s中的汇编语句中我们可以看到运用数据传送指令MOV类实现了赋值操作,如下所示:

                                                           movl   $0,%eax

该条汇编语句将数值0传送给%eax寄存器,对应hello.c中的对于局部变量i赋初值0;

 2、算数操作

在hello.c代码中我们可以看到我们在for循环中对于局部变量i在每一次循环中进行了+1操作,如下图3.10中所示:

                                   

                                                         图3.9  for循环i+1

我们找到其在hello.s中所对应的汇编语句如下:

                                              addl    $1,-4(%rbp)

则addl指令实现了双字加法,对于储存在%rbp-4地址处的i的值进行+1,然后继续执行下一条指令;

3、控制转移

在hello.c代码中我们可以看到使用到了if条件判断语句,如下所示:

                             

                                                 图3.10 if判断语句 

 则我们在hello.s中找到对应的汇编语句,如下:

                                   计算机系统_第8张图片

                                            图3.11对应汇编语句

        可以看到使用指令cmpl将argc的值与4进行了比较,je指令表明如果相等的话,则跳转到.L2处继续 执行,也即对应于hello.c中的执行for循环中的内容。hello.c中的if条件判断语句在汇编语言中通过je判断cmpl产生的条件码进行跳转来实现;同理可以判断for循环中的循环条件语句;

4、数组/指针操作

由于在hello.c程序的main函数中定义了char *argv[]参数,即元素类型为字符指针的数组,数组中的每一个元素都是字符指针。在hello.s中我们可以看到汇编代码使用首地址+偏移量的方式来访问数组元素。Argv[]数组首地址的值为%rbp-32,先将值传送给寄存器%rax,之后通过+偏移量的方式实现对于数组元素的取用与操作,如下所示;

                                       计算机系统_第9张图片

                                               图3.12   hello.s中的数组取用操作

5、函数操作

函数调用:

在hello.c程序代码中,我们可以看到printf()、exit()、sleep()、getchar()等函数操作;在hello.s中,我们可以找到对应的汇编语句,使用call来调用相应的函数,如下图3.13中画蓝色方框部分所示:

参数传递:

        在hello.s中的汇编语句中我们可以看出,参数传递通过寄存器实现。

                                            

                                                图3.13   hello.s中的函数调用 

3.4 本章小结

本章介绍了编译的概念和作用,同时,通过分析汇编产生的hello.s文件中的汇编语句,解释了编译器如何处理C语言中的各种数据以及如何执行各种操作。

第4章 汇编

4.1 汇编的概念与作用

汇编:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,由汇编器把汇编代码编译成二进制代码,生成hello.o文件。

4.2 在Ubuntu下汇编的命令

计算机系统_第10张图片                                                       图4.1 Ubuntu下的汇编命令 

在Ubuntu下的编译命令为:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

 

图4.2,由命令readelf -a hello.o > helloelf.txt得到helloefl.txt文件,便于查看可重定位目标elf的格式。

我们知道,ELF可重定位目标文件的典型格式为:

4.3.1 ELF头

ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成改文件的系统的字的大小和字节顺序,如下图4.3所示。

图4.3,该Magic魔数用来指名该文件是一个ELF目标文件。其中第一个字节7f是一个固定的数,后面的三个字节分别对应于’E’’L’’F’的ASCII表示。

ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移以及节头部表中条目的大小和数量,如下图所示。

      计算机系统_第11张图片 

                                                        图4.4  ELF头信息

其内容如下:

·“类别 ELF64”表示文件类型是64位的ELF的格式。

·“数据:2补码,小端序”表示文件中的数据是按照小端存储格式存储的。

·“Version:1(current)”表示当前ELF文件头版本号为1。

·“OS/ABI:    UNIX - System V”指出操作系统类型。

·“ABI 版本:0”表明ABI版本号,为0。

·“类型:REL(可重定位文件)”表示文件类型为可重定位文件。

·“系统架构 :Advanced Micro Devices x86 - 64”表示了机器平台的类型。

·“版本 :0x1”表示当前目标文件的版本号。

·“入口点地址: 0x0 ”因为不是可运行的程序,所以入口点地址为0.

·“程序头起点:0(byte into file)”目标文件没有程序头起点。

·“size of this header:64(bytes)”表明头文件的大小是64字节

·“start of section headers:1160(bytes into file)”表明节头部表的文 件偏移为1160字节。

·“Number of section headers”表示节头部表中条目的数量。

4.3.2节区

                 计算机系统_第12张图片

                                                              图4.5 节区内容

         

如上图所示是夹在ELF头和节头部表之间的节区,其内容解释如下:

·.text:已编译程序的机器代码。

·.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。

·.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。

·.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化的变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0.

·.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。.symtab符号表不包含局部变量的条目。

·.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。

·.Rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量的地址或者外部定义函数的地址,都需要被修改。

·.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才能得到这张表。

·.line:原始C源程序中的行号和.text节中机器指令之间的映射。

·.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串的序列。

4.3.3 符号表

              计算机系统_第13张图片

                                                            图4.6符号表

如上图所示,我们可以从条目信息中的Bind是“LOCAL”还是“GLOBAL”判断符号是本地的还是全局的。从values列都为0可以看出它们的偏移量都为0。从Size列可以看出目标的字节大小。从Type可以判断出它们是字节还是函数。Ndx列中Ndx=1表示.text节,Ndx=3表示.data节。举例来说,第10个条目为全局符号main定义的条目,它是一个位于.text节中的偏移量为0处的142字节函数。此外符号表还显示出了外部库函数puts()、exit()、printf()、atoi()、sleep()、getchar()的条目,它们是外部库函数,需要在链接之后进行确定。

4.4 Hello.o的结果解析

                                                  图4.7 objdump操作  

如上图4.7所示在linux系统下进行操作,得到下图所示hello.o的反汇编内容:

         计算机系统_第14张图片

                                 图4.8 hello.o反汇编生成文件内容

分析hello.o反汇编与hello.s的差别:

1、立即数的表示的不同:在hello.s中,操作数为十进制,而在hello.o反汇编代码中,操作数是十六进制。

2、条件分支转移语句的不同:在hello.s中,跳转指令的操作数为段名称,例如”.L2”。

而在hello.o反汇编代码中,跳转指令之后的是相对偏移地址。例如34

3、函数调用语句的不同:在hello.s中我们可以看到,函数调用为call后面加上所要调用函数的函数名,而在hello.o反汇编代码中,call后面的形式例如call 21,由于hello.c中调用的这些函数是共享库中的函数,需要通过动态链接器来为函数确定运行时的执行地址,在汇编为机器语言时,对于这些函数,在.rela.txt节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章介绍了汇编的概念和作用,借助于helloelf.txt文件分析了ELF可重定位目标文件的格式,最后通过objdump生成hello.o的反汇编文件,与hello.s进行比较,阐述了机器语言与汇编语言中的不一致。

5链接

5.1 链接的概念与作用

链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

链接的作用:由链接器(ld)将多个.o文件合并为可执行文件hello。

5.2 在Ubuntu下链接的命令

                                                 图5.1 在Ubuntu下链接的命令 

 图5.1 在Ubuntu下链接的命令 

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

图5.2 使用命令readelf -a hello > helloldelf.txt查看可执行目标文件hello的ELF格式,并将结果重定向到helloldelf.txt便于查看分析。

helloldelf.txt内容如下图所示:

5.3.1 ELF头

计算机系统_第15张图片 计算机系统_第16张图片

                   图5.3 hello.o ELF头                                                 图5.4 hello ELF头

 

同第四章对于ELF的描述相同,ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或共享的)、机器类型(如x86-64)、节头部表的文件偏移、以及节头部表中条目的大小和数量。        

        与第四章的hello.o的ELF格式的不同之处为,hello的ELF头中入口点的地址不为0x0,而是0x401090,同时程序头起点为为64,此外,section of headers:由1160变为13616,size of program headers由0字节变为56字节,Number of program headers由0变为10,Number of section headers由13变为25,Section header string table index由12变为了24,如图所示,图5.5为hello.o的ELF头,图5.6为hello的ELF头。

计算机系统_第17张图片计算机系统_第18张图片

图5.5 hello.o的ELF节头                                                               图5.6hello的ELF节头

5.3.2 节头表

通过对于hello.o与hello的ELF格式比较可知,hello的ELF中节头的条目由13变为了25,在这里节头描述了各个节的属性,包括大小和偏移量,则由这些信息可以定位到各节所占的区间。如图所示,图5.5为hello.o的ELF格式的节头,图5.6为hello的ELF格式的节头。

5.3.3 程序头

计算机系统_第19张图片

                             图5.7 hello的ELF格式文件中的程序头内容 

如图所示, 程序头中的各段组成了最终在内存中执行的程序,还提供了各段在虚拟地址空间和物理地址空间中的大小、位置、标志、访问授权和对齐等方面的信息,其各段语义如下:

·PHDR保存了程序头表

·INTERP指定程序从可行性文件映射到内存之后,必须调用的解释器,它是通过链接其他库来满足未解析的引用,用于在虚拟地址空间中插入程序运行所需的动态库。

·LOAD表示从一个需要从二进制文件映射到虚拟地址空间的段,其中保存了常量数据(如字符串),程序目标代码等信息。

·DYNAMIC段保存了由动态链接器使用的信息。

5.3.4 重定位节

计算机系统_第20张图片

                                                  图5.8 hello的ELF重定位节内容 

 

5.4 hello的虚拟地址空间

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

虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。在Linux中,地址空间最上面的区域是保留给操作系统中的代码和数据的,这对所有的进程来说都是一样的。地址空间的底部区域存放用户进程定义的代码和数据。

使用edb加载hello程序,查看反汇编内容如下图所示:

计算机系统_第21张图片

                                        图5.9 hello程序在edb下的反汇编显示 

从edb的DataDump窗口下面我们可以看到hello程序中虚拟地址空间的各段的信息:

 如上图我们可以看到,段的虚拟地址空间从0x40100到0x401ff0。与5.3中的内容进行对照分析,从节头部表中我们可以获得各个节的偏移量的信息,得到各个节在虚拟地址中所对应的地址。

 以节头表中的.text为例,我们知道.text是已经编译程序的机器代码,我们可以从节头表中的条目看到它的地址从0x401090开始,这与ELF头中表明的入口点位于0x401090相同,我们在edb中的Data Dump中找到对应的地址如下:

从节头表的条目中我们可以看到.text的大小为0x00132,通过计算我们判断出.text的虚拟内存分配为0x401090到0x4011c2;

5.5 链接的重定位过程分析

 

                                        图5.10  linux下的objdump指令  

在linux中键入如上命令,生成hello的汇编代码保存于hellold.txt文件中,便于进行查看;同理,生成hello.o的汇编代码保存于hello_old.txt文件中,便于进行查看。通过比对,hello与hello.o的不同之处在于:

  1. hello程序经过objdump之后生成的汇编代码由于经过了链接,引入了其他库函数,而hello.o程序经过objdump之后生成的汇编代码中只包含main()函数的汇编代码,如下图5.11和图5.12所示; 

计算机系统_第22张图片计算机系统_第23张图片

 图5.11 hello程序的汇编代码                                        图5.12hello.o程序的汇编代码

 2、hello中的汇编代码以虚拟内存进行标记,从0x401000开始,hello.o的汇编代码从0x0开始标记,分别如下图5.13和图5.14所示:

       计算机系统_第24张图片

                                             图5.13  hello程序的汇编内容

                

                                                图5.14  hello.o的汇编内容

重定位分析:

首先对hello.o中的重定位条目进行查看,重定位条目中包含需要被修改的引用的节偏移、重定位类型、偏移调整等信息,如下图所示;

计算机系统_第25张图片

                                              图5.15 hello.o重定位条目 

 由书上表述易知,ELF有两种最基本的重定位类型,分别为R_X86_64_PC32和R_X86_64_32,根据这两种重定位类型,我们可以判断出重定位,以这个条目的表述为例:

表明需要重定位的位置在.text中偏移量为0x18的地方。

 

在hello.o的汇编代码中可以找到对应的语句。由于汇编器在生成一个目标模块时,它并不知道数据和代码最终将会放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

5.6 hello的执行流程

  1. call ld-2.27.so! dl start

 

 

2、call ld-2.27.so! dl init

 

3、call hello!puts@plt

4、call hello!exit@plt

 

5、call hello! init

 

6、call hello!getchar@plt

 

7、call hello!sleep@plt

 

8、call hello!printf@plt       

  

5.7 Hello的动态链接分析

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

如图所示,从hello的ELF文件节头表中可以找到这两行表述,我们知道,延迟绑定通过GOT和PLT这两个数据结构之间的交互实现,其中GOT是数据段的一部分,PLT是代码段的一部分。PLT与GOT在程序运行时解析函数的地址,实现函数的动态链接。

GOT:ELF格式的共享库使用PIC技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC在代码中的跳转和分支指令不适用绝对地址。PIC在ELF可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表GOT。

PLT:过程链接表PLT用于把位置独立的函数调用重定向到绝对位置。通过PLT动态链接的程序支持惰性绑定模式。每个动态链接的程序和共享库都有一个PLT,PLT表的每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。程序对某个函数的访问被调整为对PLT入口的访问。

由上图中所示的节头部表中的.plt条目中的内容可知,存储PLT的.plt的开始地址为0x401020。我们可以从hello的反汇编代码中查看PLT表的项所对应的函数。

                                       计算机系统_第26张图片 

                                                     图5.16  PLT表项所对应的函数

 全局偏移量GOT是一个数组,每个条目为8字节地址,其中GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GOT[2]为动态链接器在1d-linux.so模块的入口点。GOT[]数组中的条目对应于被调用到的函数,其地址在运行时将被解析。每个条目都有一个相匹配的PLT表的条目。由节头表中的 内容信息可知,存储GOT的.got.plt的起始地址为0x404000,如下图所示。

我们在edb Data Dump中找到这些地址,例如.got.plt的起始地址0x404000如下图所示:

 除了PLT[0]之外,每个PLT对应的GOT条目初始时都指向这个PLT的第二条指令,由hello的汇编代码可见,PLT[1]对应于地址0x404018中的GOT条目中的内容,0x404018对应的值又为0x401036,对应于PLT[1]的第二条指令。在函数调用的过程中,所对应的GOT条目会被动态链接器修改。 

由图可见条目GOT[1]0x404008与GOT[2]0x404010中初始内容为0

计算机系统_第27张图片 

 

使用edb进行调试,我们可以发现当调试到dl_start函数正常返回之后,GOT[1]和GOT[2]中的内容被修改为正常值。

在函数被第一次调用的,动态链接器会确定puts的运行位置,修改相应的GOT条目,实现动态链接,之后对于函数的调用可以直接使用GOT条目中的值进行跳转。

5.8 本章小结

本章在开头部分介绍了链接的概念与作用,分析了ELF文件的经典格式,包括ELF头,节头表里的内容以及里面的参数,利用edb产生汇编代码,通过ELF文件中的内容确定PLT和GOT表在虚拟地址空间中的起始位置和内容,分析静态链接重定位以及动态链接的过程。

6hello进程管理

6.1 进程的概念与作用

进程的概念:进程是对一个正在执行的程序的一种抽象。进程的经典定义就是一个执行中的程序的实例。

进程的作用:进程帮助我们在现代系统上运行一个程序时,得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样,我们的程序好像是独占地使用处理器和内存处理器就好像是无间断地一条接一条地执行我们的程序中的指令。进程也提供一个假象,好像我们程序中的代码和数据是内存中的唯一对象。

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

在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件,接收用户命令,之后调用相应的应用程序。我们也可以将它理解为是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。

6.3 Hello的fork进程创建过程

     

       如图在shell中输入命令“./hello 7203610603 林鑫 6 ”,通过shell输入一个可执行目标文件的名字,在程序运行时,shell就会调用系统函数fork()创建一个新的进程,然后在这个新进程的上下文中运行它们的代码或者其他应用程序。

6.4 Hello的execve过程

当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程通过execve系统掉用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。知道CPU引用了一个被映射的虚拟页时才会进行复制。此时,操作系统利用它的页面调度机制自动从磁盘传送到内存。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种进程被叫做调度,是由内核中称为调度器的代码处理的,当内核选择一个新的进程运行时,我们说内核调度了这个进程。

内核模式和用户模式:为了使操作系统提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。

处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集合中的任何指令,并且可以访问系统中的任何位置。

没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许特权指令,比如停止处理器,改变位模式,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中的内核区内的代码和数据。

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理器程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

Linux提供一种机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构的内容。/proc文件系统将许多内核数据的内容输出为一个用户程序 可以读的文本文件的层次结构。

6.6 hello的异常与信号处理

6.6.1

会出现四种异常类别信号:中断(interrupt)、陷阱(trap)、故障(fault)、终止(abort)。

中断的处理如下图所示:

计算机系统_第28张图片

 

 陷阱的处理如下图所示:

计算机系统_第29张图片

 故障的处理如下图所示:

计算机系统_第30张图片

终止是不可恢复的错误。终止的处理如下图所示:

计算机系统_第31张图片 

6.6.2可能产生的信号及处理方法

  1. 程序运行过程中不停乱按,包括回车。

 计算机系统_第32张图片

如图所示,在程序运行过程中不停乱按,输入的字符串存储在缓冲区,如果不停乱按并输入回车,程序读入信息,将之作为输入shell的命令。 

 2、Ctrl+Z

计算机系统_第33张图片

键入CTRL+Z命令,会发送SIGSTP信号给前台进程组的每一个进程,hello进程停止。

输入ps命令可以查看当前所有进程以及它们的PID号,如下图所示。 

                         

 

输入jobs命令可以查看当前的作业情况,如图所示。

输入pstree命令将所有进程以树状图形式显示,如下图所示。

计算机系统_第34张图片 

使用fg命令使得进程继续在前台运行。

                          

使用kill命令可以杀死当前进程。例如命令kill -9 PID号为向PID号对应的进程发送SIGKILL信号,杀死该进程。在这一次的程序运行中,hello的进程分配的PID号为8156,则发送命令kill -9 8156杀死hello进程,如图所示。 

                计算机系统_第35张图片

3、Ctrl+C

输入Ctrl+C命令将发送SIGINT信号给前台进程组的每个进程,终止前台hello进程并回收,如下图所示。

计算机系统_第36张图片

 

 

6.7本章小结

本章在一开始介绍了进程的概念和作用,叙述了shell的工作原理和过程,分析fork()和execve()在运行hello时的作用。在shell下执行hello进程,并通过操作分析hello对于异常信号的处理过程。

 

 

7hello的存储管理

7.1 hello的存储器地址空间

·逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。

·线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,再加上基地址就是线性地址。

·虚拟地址:虚拟地址并不真实存在于计算机中。每个进程都分配有自己的虚拟空间,而且只能访问自己被分配使用的空间。理论上,虚拟空间受物理内存大小的限制,如给有4GB内存,那么虚拟地址空间的地址范围就应该是0x00000000~0xFFFFFFFF。每个进程都有自己独立的虚拟地址空间。这样每个进程都能访问自己的地址空间,这样做到了有效的隔离。

·物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元都分配一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。

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

段式管理,是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体,程序员需要知道并使用它。它的产生是与程序的模块化直接相关。段式管理是通过段表进行的,它包括段号和段名、段起点、装入位、段的长度等。

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

页式管理就是把内存物理空间划分成大小相等的若干区域,一个区域称为一块。把逻辑地址空间划分为大小相等的若干页,页的大小与块大小相等。

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

在任何既使用虚拟内存又使用SRAM高速缓存的系统中,都有应该使用虚拟地址还是使用物理地址来访问SRAM高速缓存的问题。使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为很简单的事情。而且,高速缓存无需处理保护问题,因为访问权限的检查是地址翻译的一部分。

如下图所示,展示了一个物理寻址的高速缓存如何和虚拟内存结合起来。主要的思路是地址翻译发生在高速缓存查找之前。页表条目可以缓存,就像其他的数据字一样。

计算机系统_第37张图片

                                    图7.1 物理寻址高速缓存与虚拟内存结合过程 

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

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常由高度的相联度。如下图所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2^t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

                        

 下图左图7.2展示了当TLB命中时(通常情况)所包括的步骤。这里的关键点是,所有的地址翻译都是在芯片上的MMU中执行的,因此非常快。

第一步:CPU产生一个虚拟地址

第二步和第三步:MMU从TLB中取出相应的PTE

第四步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓 存/主存。

第五步:高速缓存/主存将所请求的数据字返回给CPU。

当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,如下图右图7.3所示。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

计算机系统_第38张图片计算机系统_第39张图片

 

                        图7.2当TLB命中时                                           图7.3当TLB不命中时

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

为了保证可管理性,我们做出如下假设:

·内存是按字节寻址的。

·内存访问是针对1字节的字的

·虚拟地址是14位长的

·物理地址是12位长的

·页面大小是64字节的

·TLB是四路组相联的,总共有16个条目。

·L1 d-cache是物理寻址、直接映射的,行大小为4字节,总共16组。

下图7.3为虚拟地址和物理地址的格式。因为每个页面是2^8=64字节,所以虚拟地址和物理地址的低6位分别作为VPO和PPO。虚拟地址的高8位作为VPN。物理地址的高6位作为PPN。

计算机系统_第40张图片

                                  图7.3虚拟地址和物理地址的格式 

 

·TLB是利用VPN的位进行虚拟寻址的。因为TLB有4个组,所以VPN的低2位就作为组索引(TLBI)。VPN中剩下的高6位作为标记(TLBT),用来区别可能映射到同一个TLB组的不同VPN。

·这个页表是一个单级设计,一共有256个页表条目(PTE)。然而,我们只对这些条目中的开头16个感兴趣。我们用索引它的VPN来标识每个PTE,不过这些VPN并不是页表的一部分,也不储存在内存中。

·直接映射的缓存是通过物理地址中的字段来寻址的。因为每个块都是4字节,所以物理地址的低2位作为块偏移(CO) 。因为有16组,所以接下来的4位就用来表示组索引(CI)。剩下的6位作为标记。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

虚拟内存和内存映射在将程序加载到内存的过程中扮演着关键的角色。在hello进程中程序执行了execve调用,execve函数在当前进程中加载并运行可执行目标文件中的程序,加载并运行包括以下几个步骤:

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

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

计算机系统_第41张图片

                                            图7.4 私有区域的不同映射

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

·程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页。

 

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。下图7.5展示了在缺页之前我们的示例页表的状态。CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位判断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反应VP4不再缓存在主存中这一事实。

计算机系统_第42张图片

                                           图7.5 缺页之前示例页表的状态 

接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。下图展示了在缺页之后示例页表中的状态。

计算机系统_第43张图片

                                                  图7.6缺页之后示例页表的状态

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

虽然可以使用低级的mmap和mumap函数来创建和删除虚拟内存的区域,不过使用动态内存分配器更方便,也有更好的可移植性。

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

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

C标准库提供了一个称为malloc程序包的显式分配器。程序通过调用malloc函数来从堆中分配块。

malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。实际中,对齐依赖于编译代码在32位模式(gcc-m32)还是64位模式(默认)中运行。在32位模式中,malloc返回的块的地址总是8的倍数。在64位模式中,改地址总是16的倍数。

如果malloc遇到问题,那么它就返回NULL,并设置errno。malloc不初始化它返回的内存。malloc用于显式地分配和释放堆内存。

7.10本章小结

本章讲解了有关hello内存管理的有关内容,包括TLB、段式管理、页式管理缺页故障与却页中断处理、动态存储分配管理、execve()和fork()的内存映射等内容。

8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

输入/输出(I/O)是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。

8.2 简述Unix IO接口及其函数

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

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

 1、进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的。

计算机系统_第44张图片

Open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

2、进程通过调用close函数关闭一个打开的文件。

3、应用程序分别调用read和write函数来执行输入和输出。 

计算机系统_第45张图片

8.3 printf的实现分析

printf函数的函数体如下

int printf(const char *format, ...)

{

int i;

char buf[256];

va_list arg = (va_list)((char*)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

 write(buf, i);

return i;

}

在形参列表里由这么一个token:....这是可变形参的一种写法,用于表示当形参个数不确定时的情况。

va_list是一个字符指针,在上面给va_list的赋值语句中,((char*)(&format)+4)表示...中的第一个参数,这是因为在C语言中,参数压栈的方向为从右向左,在调用函数printf的过程中,从最右边的参数入栈。指针*format指向(const char *format)中的第一个元素,它在栈上有分配相应的位置。之后用vsprintf函数接收确定输出格式的格式字符串format。用格式字符串对个数变化的参数进行格式化,产生格式化输出。最后调用write()函数执行写操作,将buf中的i个元素值写到终端。

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

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

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

8.4 getchar的实现分析

程序执行到getchar函数时,从输入缓冲区中寻找字符,如果在输入缓冲区中没有字符,则等待用户输入字符。我们在使用键盘输入字符的时候,首先输入进入缓冲区,getchar()函数从缓冲区中提取字符,每次只能提取一个字符。

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

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux中的I/O设备管理方法,简单叙述了Unix IO的接口及其函数,最后进行了printf以及getchar函数的实现分析。

结论

Hello所经历的历程:

  1. hello.c经过预编译处理之后生成hello.i文本文件;
  2. hello.i经过编译之后,生成汇编代码hello.s
  3. hello.s经过汇编处理之后,得到二进制可重定位的目标文件hello.o。
  4. hello.o经过链接器链接处理之后生成可执行hello程序。
  5. 在hello运行的时候,父进程会调用系统函数fork()创建子进程,调用execve函数加载并执行程序。
  6. 你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
  7. hello在运行的过程中会进行库函数的调用以实现相应的功能,如printf、getchar函数等。
  8. hello进程在运行完之后会被父进程所回收,内核会从系统中删除掉所有它的痕迹,所谓“赤条条来去无牵挂”。

CSAPP深入理解计算机系统在经典的下下x86架构机器上运行Linux操作系统,采用C语言编程,向我们解释了所有计算机系统的本质概念,并向我们展示这些概念时如何实实在在地影响应用程序的正确性、性能和实用性的。

通过对这门课的学习,我开始了解计算机是如何运作的,以及当程序出现故障时该如何修复,同时我也学会了如何更好地利用操作系统和系统软件提供的供能,对各种操作条件和运行时参数都能正确操作,同时,我也了解了编译器、计算机体系结构、操作系统、嵌入式系统、网络互联和网络安全方面的相关知识。

附件

列出所有的中间产物的文件名,并予以说明起作用。

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

hello.s 编译之后产生的汇编文件

hello.o 可重定位的目标文件

hello 可执行文件Hello

hello.objdump Hello的反汇编文件

hellold.txt     hello的ELF文件

hello_old.txt hello.o的ELF文件

参考文献

[1]  深入理解计算机系统.

[2]  现代x86汇编语言程序设计

[3]  汇编语言基于下x86处理器

[4]  编译系统透视

 

 

你可能感兴趣的:(p2p,网络协议,网络)