程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业  航天与自动化            

学     号  7203610713              

班   级  2036016                 

学       生  郭松言                

指 导 教 师  史先俊                   

计算机科学与技术学院

2022年5月

摘  要

本文以一个简单程序hello从编写到执行,最后终止,被回收的全过程为例,详细介绍了如预处理、编译、汇编、链接生成可执行文件的全过程,并详略得当地分析了各个过程当中计算机的软件和硬件如何有机地配合。通过这样的方式,详细阐述我们的计算机系统是如何对hello进行进程管理、存储管理和I/O管理,通过对hello一生周期的探索,让我们对计算机系统有更深的了解,最终总结出了有关hello运行的总体性过程。

关键词:操作系统;预处理;编译;汇编;进程;I/O ;                            

目  录

第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简介

P2P的过程:                   

预处理: 预处理器(cpp)根据字符#开头的命令,修改原始的程序员用键盘输入的.c后缀的c程序(Program),并把它直接插入程序文本当中,得到了另一个以.i作为文件拓展名的c程序。

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,其包含了一个汇编语言程序,该程序还包含main函数的定义。汇编器(as)将hello.s翻译成积极语言指令,并且把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目文件hello.o中,其是一个二进制文件。链接器负责处理如printf等存在于其他.o目标文件中的文件,将他们与main.o文件合并在一起,修改重定向条目,得到一个hello文件,它是一个可执行文件,可以被加载到内存之中,由系统进行执行。在得到可执行目标文件hello之后,其被存放在磁盘上,我们可以将其文件名输入到shell中,shell将通过fork的方式,生成一个新的子进程(Process),并且在子进程的上下文中加载并运行这个程序文件。

020的过程:

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

1.2 环境与工具

硬件环境:X64 CPU;3.8 GHz;16G RAM;1T Disk

软件环境:Windows10 64位;Vmware 11;Ubuntu 20.04 LTS 64位/

开发与调试工具:gcc,vim,edb,readelf,HexEdit

1.3 中间结果

文件名称

  文件作用

hello.i

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

hello.s

  hello.i编译后的汇编文件

hello.o

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

hello

  链接之后的可执行目标文件

hello.out

  hello反汇编之后的可重定位文件

hello_elf

hello的elf格式文件

Hello_objdump

hello的反汇编文件

Hello_o_objdump

  hello.o的反汇编文件,带有重定位条目标注信息

1.4 本章小结

本章以hello.c为例,主要简单介绍了 hello 的 p2p,020 过程,列出了本次实验信息:环境、中间结果,并且大致简介了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程以及所需要的运行环境和工具,以及产生的中间文件名和作用。


第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理概念:

预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。

2.1.2预处理阶段作用:

1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。

2. 处理条件编译指令

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

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

4.处理特殊符号

预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.1.3总结

预处理阶段程序将所有的#define删除,并展开所有的宏定义;处理所有的条件预编译指令,比如#if, #ifdef, #elif等;处理#include预编译指令,将被包含的文件插入到该预编译指令所在的位置,这个过程是递归进行的;删除所有的注释;添加行号和文件名标识,以便于编译时编译器产生条实用的行号与代码段的映射信息;保留所有的#pragma编译器指令。

当我们无法确定宏定义是否正确,或者包含的头文件是否正确的时候,可以查看预编译后的文件来定位问题所在。

2.2在Ubuntu下预处理的命令

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

2.3 Hello的预处理结果解析

处理前:

处理后:

被展开成3000余行

这是因为源程序hello.c中有如下三条预处理指令:

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

这三条预处理指令告诉预处理器读取系统头文件stdio.h、unistd.h、stdlib.h的内容,并把它们插入到程序文本中,一般包括结构体的定义(typedef struct)、函数声明(extern)、对引用目录的标注(# 28 “/usr/include/stdio.h” 2 3 4)等内容。若源程序中有#define预处理指令还会对相应的符号进行替换,或者其他类型的预处理指令,预处理器也会执行对于的操作。

我们再看源程序的其他部分,预处理得到的hello.i中的main函数和源程序保持一致,因为预处理器不会对其他部分进行修改。

2.4 本章小结

本章主要介绍了预处理的概念与作用,执行预处理的命令,并结合hello.i文件,解析了hello的预处理结果

预处理过程是之后所有操作的基础,是不可或缺的重要过程
第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。

编译的作用就是将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.0汇编指令的介绍

.file:声明源文件

.text:代码节

.section:

.rodata:只读代码段

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

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

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

.type:声明一个符号是数据类型还是函数类型

3.3.1 数据

1.字符串

按照文件顺序,首先是:

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

2.局部变量i:

3.main函数

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

4.各种立即数

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

5.数组:char *argv[]

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

红色方框标注的是:数组argv的存放的位置:-32(%rbp)

蓝色方框标注的是:分别获取argv[1]和argv[2]的地址

3.3.2.全局函数

由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中使用的字符串常量也被存放在数据区。

这段汇编代码说明main函数是全局函数

3.3.3赋值操作

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

movb:一个字节

movw:两个字节

movl:四个字节

movq:八个字节

3.3.4算数操作

hello.c中的算数操作有:i++,由于是i是int类型的,因此汇编代码只用addl就能实现其他的操作有

指令

效果

leaq S,D

D=&S

INC D

D+=1

DEC D

D-=1

NEG D

D=-D

ADD S,D

D=D+S

SUB s,D

D=D-S

3.3.5关系操作

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

  1. i<8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。

3.3.6控制转移指令

汇编语言中首先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:

(1)判断i是否为3,如果i等于3,则不执行if语句,否则执行if语句,对应的汇编代码为

(2)for(i=0;i<8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句中,对应的汇编代码为:

第一个红色方框:首先i赋初值0,然后无条件跳转至判断条件的代码中,即.L3.

第二个红色方框:判断i是否符合循环的条件,符合直接跳转至.L4,也就是循环体的内部.

3.3.7函数操作

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

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

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

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

hello.C涉及的函数操作有:

main函数,printf,exit,sleep ,getchar函数

main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串

exit参数是1,sleep函数参数是atoi(argv[3]),函数的返回值存储在%eax寄存器中。

3.3.8类型转换

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

3.4 本章小结

本章介绍了编译的概念与作用,以及hello.i文件转化为hello.s文件的过程。同时还对其中的汇编代码中C语言的数据和操作进行了解析。
第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中.

汇编的作用:翻译生成机器语言,因为机器语言是计算机能直接识别和执行的一种语言.

4.2 在Ubuntu下汇编的命令

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

4.3 可重定位目标elf格式

4.3.1Elf头

描述生成该文件的系统的字的大小和字节顺序

命令:readelf -h hello.o

以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。根据头文件的信息,可以知道该文件是可重定位目标文件,有13个节。

4.3.2节头部

表描述了不同节的位置和大小,目标文件中每个节都有固定大小的条目。

命令:readelf -S hello.o

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

4.3.3符号表

存放程序中定义和引用的函数和全局变量的信息。命令readelf -s hello.o

name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

4.3.4重定位节

节包含了要在链结中重定位的项目

命令:objdump -r hello.o

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进行对照分析。

Hello.s

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

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

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

4.5 本章小结

本章经历了hello从hello.s到hello.o的汇编过程,查看了hello.o的elf,使用objdump工具得到反汇编代码,和之前的汇编代码进行比较,通过寻找不同之处分析了从汇编语言到机器语言的映射关系 。


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的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.3.1 .ELF Header

命令:readelf -a hello > 5.3helloelf.txt

ELF 头:

  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

  类别:                              ELF64

  数据:                              2 补码,小端序 (little endian)

  Version:                           1 (current)

  OS/ABI:                            UNIX - System V

  ABI 版本:                          0

  类型:                              EXEC (可执行文件)

  系统架构:                          Advanced Micro Devices X86-64

  版本:                              0x1

  入口点地址:               0x4010f0

  程序头起点:          64 (bytes into file)

  Start of section headers:          14200 (bytes into file)

  标志:             0x0

  Size of this header:               64 (bytes)

  Size of program headers:           56 (bytes)

  Number of program headers:         12

  Size of section headers:           64 (bytes)

  Number of section headers:         27

  Section header string table index: 26

hello的文件头和hello.o文件头的不同之处如上述加粗所示,Type类型为EXEC表明hello是一个可执行目标文件,有27个节

5.3.2 Section Headers

节头:

  [号] 名称              类型             地址              偏移量

       大小              全体大小          旗标   链接   信息   对齐

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .interp           PROGBITS         00000000004002e0  000002e0

       000000000000001c  0000000000000000   A       0     0     1

  [ 2] .note.gnu.propert NOTE             0000000000400300  00000300

       0000000000000020  0000000000000000   A       0     0     8

  [ 3] .note.ABI-tag     NOTE             0000000000400320  00000320

       0000000000000020  0000000000000000   A       0     0     4

  [ 4] .hash             HASH             0000000000400340  00000340

       0000000000000038  0000000000000004   A       6     0     8

  [ 5] .gnu.hash         GNU_HASH         0000000000400378  00000378

       000000000000001c  0000000000000000   A       6     0     8

  [ 6] .dynsym           DYNSYM           0000000000400398  00000398

       00000000000000d8  0000000000000018   A       7     1     8

  [ 7] .dynstr           STRTAB           0000000000400470  00000470

       000000000000005c  0000000000000000   A       0     0     1

  [ 8] .gnu.version      VERSYM           00000000004004cc  000004cc

       0000000000000012  0000000000000002   A       6     0     2

  [ 9] .gnu.version_r    VERNEED          00000000004004e0  000004e0

       0000000000000020  0000000000000000   A       7     1     8

  [10] .rela.dyn         RELA             0000000000400500  00000500

       0000000000000030  0000000000000018   A       6     0     8

  [11] .rela.plt         RELA             0000000000400530  00000530

       0000000000000090  0000000000000018  AI       6    21     8

  [12] .init             PROGBITS         0000000000401000  00001000

       000000000000001b  0000000000000000  AX       0     0     4

  [13] .plt              PROGBITS         0000000000401020  00001020

       0000000000000070  0000000000000010  AX       0     0     16

  [14] .plt.sec          PROGBITS         0000000000401090  00001090

       0000000000000060  0000000000000010  AX       0     0     16

  [15] .text             PROGBITS         00000000004010f0  000010f0

       0000000000000145  0000000000000000  AX       0     0     16

  [16] .fini             PROGBITS         0000000000401238  00001238

       000000000000000d  0000000000000000  AX       0     0     4

  [17] .rodata           PROGBITS         0000000000402000  00002000

       000000000000003b  0000000000000000   A       0     0     8

  [18] .eh_frame         PROGBITS         0000000000402040  00002040

       00000000000000fc  0000000000000000   A       0     0     8

  [19] .dynamic          DYNAMIC          0000000000403e50  00002e50

       00000000000001a0  0000000000000010  WA       7     0     8

  [20] .got              PROGBITS         0000000000403ff0  00002ff0

       0000000000000010  0000000000000008  WA       0     0     8

  [21] .got.plt          PROGBITS         0000000000404000  00003000

       0000000000000048  0000000000000008  WA       0     0     8

  [22] .data             PROGBITS         0000000000404048  00003048

       0000000000000004  0000000000000000  WA       0     0     1

  [23] .comment          PROGBITS         0000000000000000  0000304c

       0000000000000023  0000000000000001  MS       0     0     1

  [24] .symtab           SYMTAB           0000000000000000  00003070

       00000000000004c8  0000000000000018          25    30     8

  [25] .strtab           STRTAB           0000000000000000  00003538

       0000000000000158  0000000000000000           0     0     1

  [26] .shstrtab         STRTAB           0000000000000000  00003690

       00000000000000e1  0000000000000000           0     0     1

5.3.3 重定位节.rela.text

重定位节 '.rela.dyn' at offset 0x500 contains 2 entries:

  偏移量          信息           类型           符号值        符号名称 + 加数

000000403ff0  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0

000000403ff8  000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' at offset 0x530 contains 6 entries:

  偏移量          信息           类型           符号值        符号名称 + 加数

000000404018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0

000000404020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

000000404028  000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0

000000404030  000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0

000000404038  000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0

000000404040  000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

5.3.4 符号表.symtab

Symbol table '.dynsym' contains 9 entries:

   Num:    Value          Size Type    Bind   Vis      Ndx Name

     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND

     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)

     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)

     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getchar@GLIBC_2.2.5 (2)

     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND atoi@GLIBC_2.2.5 (2)

     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)

     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.2.5 (2)

5.4 hello的虚拟地址空间

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

通过查看edb,看出hello的虚拟地址空间开始于0x401000,结束与0x401ff0,如下图

根据下图的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x132.

5.5 链接的重定位过程分析

命令:objdump -d -r hello

5.5.1 生成的反汇编代码

Disassembly of section .init:

0000000000401000 <_init>:

  401000: f3 0f 1e fa           endbr64

  401004: 48 83 ec 08           sub    $0x8,%rsp

  401008: 48 8b 05 e9 2f 00 00 mov    0x2fe9(%rip),%rax        # 403ff8 <__gmon_start__>

  40100f: 48 85 c0              test   %rax,%rax

  401012: 74 02                 je     401016 <_init+0x16>

  401014: ff d0                 callq  *%rax

  401016: 48 83 c4 08           add    $0x8,%rsp

  40101a: c3                    retq   

Disassembly of section .plt:

0000000000401020 <.plt>:

  401020: ff 35 e2 2f 00 00     pushq  0x2fe2(%rip)        # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>

  401026: f2 ff 25 e3 2f 00 00 bnd jmpq *0x2fe3(%rip)        # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>

  40102d: 0f 1f 00              nopl   (%rax)

  401030: f3 0f 1e fa           endbr64

  401034: 68 00 00 00 00        pushq  $0x0

  401039: f2 e9 e1 ff ff ff     bnd jmpq 401020 <.plt>

  40103f: 90                    nop

  401040: f3 0f 1e fa           endbr64

  401044: 68 01 00 00 00        pushq  $0x1

  401049: f2 e9 d1 ff ff ff     bnd jmpq 401020 <.plt>

  40104f: 90                    nop

  401050: f3 0f 1e fa           endbr64

  401054: 68 02 00 00 00        pushq  $0x2

  401059: f2 e9 c1 ff ff ff     bnd jmpq 401020 <.plt>

  40105f: 90                    nop

  401060: f3 0f 1e fa           endbr64

  401064: 68 03 00 00 00        pushq  $0x3

  401069: f2 e9 b1 ff ff ff     bnd jmpq 401020 <.plt>

  40106f: 90                    nop

  401070: f3 0f 1e fa           endbr64

  401074: 68 04 00 00 00        pushq  $0x4

  401079: f2 e9 a1 ff ff ff     bnd jmpq 401020 <.plt>

  40107f: 90                    nop

  401080: f3 0f 1e fa           endbr64

  401084: 68 05 00 00 00        pushq  $0x5

  401089: f2 e9 91 ff ff ff     bnd jmpq 401020 <.plt>

  40108f: 90                    nop

Disassembly of section .plt.sec:

0000000000401090 <puts@plt>:

  401090: f3 0f 1e fa           endbr64

  401094: f2 ff 25 7d 2f 00 00 bnd jmpq *0x2f7d(%rip)        # 404018 <puts@GLIBC_2.2.5>

  40109b: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010a0 <printf@plt>:

  4010a0: f3 0f 1e fa           endbr64

  4010a4: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip)        # 404020 <printf@GLIBC_2.2.5>

  4010ab: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010b0 <getchar@plt>:

  4010b0: f3 0f 1e fa           endbr64

  4010b4: f2 ff 25 6d 2f 00 00 bnd jmpq *0x2f6d(%rip)        # 404028 <getchar@GLIBC_2.2.5>

  4010bb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010c0 <atoi@plt>:

  4010c0: f3 0f 1e fa           endbr64

  4010c4: f2 ff 25 65 2f 00 00 bnd jmpq *0x2f65(%rip)        # 404030 <atoi@GLIBC_2.2.5>

  4010cb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010d0 <exit@plt>:

  4010d0: f3 0f 1e fa           endbr64

  4010d4: f2 ff 25 5d 2f 00 00 bnd jmpq *0x2f5d(%rip)        # 404038 <exit@GLIBC_2.2.5>

  4010db: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010e0 <sleep@plt>:

  4010e0: f3 0f 1e fa           endbr64

  4010e4: f2 ff 25 55 2f 00 00 bnd jmpq *0x2f55(%rip)        # 404040 <sleep@GLIBC_2.2.5>

  4010eb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

Disassembly of section .text:

00000000004010f0 <_start>:

  4010f0: f3 0f 1e fa           endbr64

  4010f4: 31 ed                 xor    %ebp,%ebp

  4010f6: 49 89 d1              mov    %rdx,%r9

  4010f9: 5e                    pop    %rsi

  4010fa: 48 89 e2              mov    %rsp,%rdx

  4010fd: 48 83 e4 f0           and    $0xfffffffffffffff0,%rsp

  401101: 50                    push   %rax

  401102: 54                    push   %rsp

  401103: 49 c7 c0 30 12 40 00 mov    $0x401230,%r8

  40110a: 48 c7 c1 c0 11 40 00 mov    $0x4011c0,%rcx

  401111: 48 c7 c7 25 11 40 00 mov    $0x401125,%rdi

  401118: ff 15 d2 2e 00 00     callq  *0x2ed2(%rip)        # 403ff0 <__libc_start_main@GLIBC_2.2.5>

  40111e: f4                    hlt    

  40111f: 90                    nop

0000000000401120 <_dl_relocate_static_pie>:

  401120: f3 0f 1e fa           endbr64

  401124: c3                    retq

0000000000401125 <main>:

  401125: f3 0f 1e fa           endbr64

  401129: 55                    push   %rbp

  40112a: 48 89 e5              mov    %rsp,%rbp

  40112d: 48 83 ec 20           sub    $0x20,%rsp

  401131: 89 7d ec              mov    %edi,-0x14(%rbp)

  401134: 48 89 75 e0           mov    %rsi,-0x20(%rbp)

  401138: 83 7d ec 04           cmpl   $0x4,-0x14(%rbp)

  40113c: 74 16                 je     401154 <main+0x2f>

  40113e: 48 8d 3d c3 0e 00 00 lea    0xec3(%rip),%rdi        # 402008 <_IO_stdin_used+0x8>

  401145: e8 46 ff ff ff        callq  401090 <puts@plt>

  40114a: bf 01 00 00 00        mov    $0x1,%edi

  40114f: e8 7c ff ff ff        callq  4010d0 <exit@plt>

  401154: c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)

  40115b: eb 48                 jmp    4011a5 <main+0x80>

  40115d: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  401161: 48 83 c0 10           add    $0x10,%rax

  401165: 48 8b 10              mov    (%rax),%rdx

  401168: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  40116c: 48 83 c0 08           add    $0x8,%rax

  401170: 48 8b 00              mov    (%rax),%rax

  401173: 48 89 c6              mov    %rax,%rsi

  401176: 48 8d 3d b1 0e 00 00 lea    0xeb1(%rip),%rdi        # 40202e <_IO_stdin_used+0x2e>

  40117d: b8 00 00 00 00        mov    $0x0,%eax

  401182: e8 19 ff ff ff        callq  4010a0 <printf@plt>

  401187: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  40118b: 48 83 c0 18           add    $0x18,%rax

  40118f: 48 8b 00              mov    (%rax),%rax

  401192: 48 89 c7              mov    %rax,%rdi

  401195: e8 26 ff ff ff        callq  4010c0 <atoi@plt>

  40119a: 89 c7                 mov    %eax,%edi

  40119c: e8 3f ff ff ff        callq  4010e0 <sleep@plt>

  4011a1: 83 45 fc 01           addl   $0x1,-0x4(%rbp)

  4011a5: 83 7d fc 07           cmpl   $0x7,-0x4(%rbp)

  4011a9: 7e b2                 jle    40115d <main+0x38>

  4011ab: e8 00 ff ff ff        callq  4010b0 <getchar@plt>

  4011b0: b8 00 00 00 00        mov    $0x0,%eax

  4011b5: c9                    leaveq

  4011b6: c3                    retq   

  4011b7: 66 0f 1f 84 00 00 00 nopw   0x0(%rax,%rax,1)

  4011be: 00 00

00000000004011c0 <__libc_csu_init>:

  4011c0: f3 0f 1e fa           endbr64

  4011c4: 41 57                 push   %r15

  4011c6: 4c 8d 3d 83 2c 00 00 lea    0x2c83(%rip),%r15        # 403e50 <_DYNAMIC>

  4011cd: 41 56                 push   %r14

  4011cf: 49 89 d6              mov    %rdx,%r14

  4011d2: 41 55                 push   %r13

  4011d4: 49 89 f5              mov    %rsi,%r13

  4011d7: 41 54                 push   %r12

  4011d9: 41 89 fc              mov    %edi,%r12d

  4011dc: 55                    push   %rbp

  4011dd: 48 8d 2d 6c 2c 00 00 lea    0x2c6c(%rip),%rbp        # 403e50 <_DYNAMIC>

  4011e4: 53                    push   %rbx

  4011e5: 4c 29 fd              sub    %r15,%rbp

  4011e8: 48 83 ec 08           sub    $0x8,%rsp

  4011ec: e8 0f fe ff ff        callq  401000 <_init>

  4011f1: 48 c1 fd 03           sar    $0x3,%rbp

  4011f5: 74 1f                 je     401216 <__libc_csu_init+0x56>

  4011f7: 31 db                 xor    %ebx,%ebx

  4011f9: 0f 1f 80 00 00 00 00 nopl   0x0(%rax)

  401200: 4c 89 f2              mov    %r14,%rdx

  401203: 4c 89 ee              mov    %r13,%rsi

  401206: 44 89 e7              mov    %r12d,%edi

  401209: 41 ff 14 df           callq  *(%r15,%rbx,8)

  40120d: 48 83 c3 01           add    $0x1,%rbx

  401211: 48 39 dd              cmp    %rbx,%rbp

  401214: 75 ea                 jne    401200 <__libc_csu_init+0x40>

  401216: 48 83 c4 08           add    $0x8,%rsp

  40121a: 5b                    pop    %rbx

  40121b: 5d                    pop    %rbp

  40121c: 41 5c                 pop    %r12

  40121e: 41 5d                 pop    %r13

  401220: 41 5e                 pop    %r14

  401222: 41 5f                 pop    %r15

  401224: c3                    retq   

  401225: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)

  40122c: 00 00 00 00

0000000000401230 <__libc_csu_fini>:

  401230: f3 0f 1e fa           endbr64

  401234: c3                    retq   

Disassembly of section .fini:

0000000000401238 <_fini>:

  401238: f3 0f 1e fa           endbr64

  40123c: 48 83 ec 08           sub    $0x8,%rsp

  401240: 48 83 c4 08           add    $0x8,%rsp

  401244: c3                    retq

5.5.2 hello的反汇编代码中不同节的含义:

5.5.3 hello重定位的过程:

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

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

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

5.5.4 hello的反汇编代码和hello.o反汇编代码的区别

hello的反汇编代码从.init节开始,而hello.o的反汇编代码从.text节开始。

hello的反汇编代码中导入了puts、printf、atoi、getchar、sleep等在主程序中使用过的函数,而hello.o的反汇编代码中不包含这些函数。

hello的反汇编代码中函数的调用方法同hello.s,使用call + 函数名直接调用,而hello.o的反汇编代码使用call指向下一条语句,并未直接调用函数。

5.6 hello的执行流程

子程序名

程序地址

 init;

0x401430

puts@plt;

0x401460

printf@plt;

0x401470

__libc_start_main@plt;

0x401480

 getchar@plt;

0x401490

 exit@plt;

0x4014a0

sleep@plt;

0x4014b0

start;

0x4014d0

main;

0x4014fe

 __libc_csu_init;

0x401580

__libc_csu_fini;

0x4015f0

_fini;

0x4015f4

5.7 Hello的动态链接分析

5.7.1分析

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

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

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

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

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

5.7.2

根据hello ELF文件可知:

[21] .got.plt          PROGBITS         0000000000404000  00003000

       0000000000000048  0000000000000008  WA       0     0     8

GOT表位置在调用dl_init之前0x601008后的16个字节均为0:

调用_start之后发生改变,0x404008后的两个8个字节分别变为:0x7f6f8dc46170、0x7f6f8da34750,其中GOT[O](对应0x404e28)和GOT[1](对应0x7fb06087e168)

包含动态链接器在解析函数地址时会使用的信息。GOT[2](对应0x7fb06066e870)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下: ​

GOT[2]对应部分是共享库模块的入口点,如下:

举例puts函数在调用puts函数前对应GOT条目指向其对应的PLT条目的第二条指令,如图puts@plt指令跳转的地址

调用puts函数前(链接前)PLT函数:

可以看出其对应GOT条目初始时指向其PLT条目的第二条指令的地址。puts函数执行后在查看此处地址:

可以看出其已经动态链接,GOT条目已经改变。

5.8 本章小结

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


6hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

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

为使用者提供操作界面,具备命令行解释器的作用,作为系统和用户之间的接口

流程:

获取命令行用户输入的命令,若是内置命令,执行内置命令

若非内置命令,则为可执行程序

创建子进程,运行该可执行程序

若在前台运行,等待程序在前台运行完毕,回收

若在后台运行,不等待程序运行,运行结束时回收

6.3 Hello的fork进程创建过程

在终端中键入./hello 118030XXXX XX 3,运行的终端程序会对输入的命令行进行解析,因为hello 不是一个内置的shell 命令。所以解析之后终端程序判断./hello 的语义为执行当前目录下的可执行目标文件hello,之后终端程序首先会调用fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。

6.4 Hello的execve过程

子进程调用execve函数,加载并运行hello程序,加载器删除子进程所有的内存段,并且创建新的代码、数据、堆和栈段,它们被初始化为0

6.5 Hello的进程执行

上下文信息:上下文程序正确运行所需要的状态,包括存放在内存中的程序的代码和数据,用户栈、用寄存器、程序计数器、环境变量和打开的文件描述符的集合构成

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,这个模式中,硬件防止特权指令的执行,并对内存和I/O空间的访问操作进行检查.设置模式位时,进程处于内核模式,一切程序都可运行.任务可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件

进程执行分析:Hello起初在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核休眠,并将hello进程从运行队列加入等待队列,定时器开始计时2s,当定时器到时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列, hello进程继续执行。

6.6 hello的异常与信号处理

6.6.1异常和信号异常

(1)异常和信号异常可以分为四类:中断、陷阱、故障、终止,各自的属性如下:

类别

原因

异步\同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常.

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

hello程序出现的异常可能有:

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

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

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

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

在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。

ID

名称

默认行为

相应事件

2

SIGINT

终止

来白键盘的中断

9

SIGKILL

终止

杀死程序(该信号不能被捕获不能被忽略)

11

SIGSEGV

终止

无效的内存引用(段故障)

14

SIGSEGV

终止

来自alarm函数的定时器信号

17

SIGSEGV

忽略

一个子进程停止或者终止

6.6.2键盘上各种操作导致的异常

1.正常执行hello程序的结果:

输入:./hello 7203610713 郭松言 0.5

  1. 按下 ctrl-z 的结果:

输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下:

如图所示用ps命令可以看到,hello进程并没有被回收,此时他的后台 job 号是 1:

调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收:

  1. 下图是按下Ctrl+c的结果,在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业:

用ps查看前台进程组发现没有hello进程。:

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

6.7本章小结

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


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。是hello.o中的相对偏移地址。

线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址。是hello里的虚拟内存地址。

物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。是hello里虚拟内存地址对应的物理地址。

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

逻辑地址=段选择符 + 偏移地址

线性地址 = 段基址 + 偏移地址

逻辑地址到线性地址的变换——段式管理的关键在于;段选择符à段基址

变换在处理器的两种寻址模式——实模式和保护模式下有所不同:

实模式:

段基址 = 段选择符 * 16

线性地址 = 段选择符 * 16 + 偏移地址

保护模式:

保护模式是现代计算机常用的寻址模式。保护模式下,段选择符作为一个索引,到一个称为描述符表的数据结构中读取段基址。此时,16位的段选择符也被划分成了几个部分,予以不同的解释:

TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。

RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态。

高13位(8K个索引):用来确定当前使用的段描述符在描述符表中的位置。

计算机维护一系列表,称为描述符表。描述符表分为三种:全局描述符表(GDT)、局部描述符表(LDT)、中断描述符表(IDT)。描述符表的每个表项,大小为8个字节,称为段描述符。段描述符的内容如下图。

BASE:段基址

Limit:指出这个段有多大。

DPL:描述符的特权级(privilege level),其值从0(最高特权,内核模式)到3(最低特权,用户模式),用于控制对段的访问。

下图详细显示了一个逻辑地址是怎样转换成相应线性地址的,逻辑地址转换为线性地址的一般步骤:

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、把Base + offset,就是要转换的线性地址了。

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

每个页表实际上是一个数组,数组中的每个元素称为页表项(PTE, page table entry),每个页表项负责把虚拟页映射到物理页上。在 DRAM 中,每个进程都有自己的页表,具体如下

因为有一个表可以查询,就会遇到两种情况,一种是命中(Page Hit),另一种则是未命中(Page Fault)。

命中的时候,即访问到页表中蓝色条目的地址时,因为在 DRAM 中有对应的 数据,可以直接访问。

不命中的时候,即访问到 page table 中灰色条目的时候,因为在 DRAM 中 并没有对应的数据,所以需要执行一系列操作(从磁盘复制到 DRAM 中), 具体为:

触发 Page fault,也就是一个异常,Page fault handler 会选择 DRAM 中需要被置换的 page,并把数据从磁盘复制到 DRAM 中,重新执行访问指令,这时候就会是 page hit。复制过程中的等待时间称为 demand paging。

仔细留意上面的页表,会发现有一个条目是 null,也就是没有分配。具体的分配过程(比方说声明了一个大数组),就是让该条目指向虚拟内存(在磁盘上)的某个页,但并不复制到 DRAM,只有当出现 page fault 的时候才需要拷贝数据。

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

TLB:页表常驻内存,地址翻译时MMU需要去内存中访问,为减少这样的开销,MMU中包含了一个关于PTE的小缓存,称为翻译后备缓冲器(TLB)。

多级页表:将页表分为多个层级,以减少内存要求。

CPU将VPN进一步划分为TLBT(TLB标记)和TLBI(TLB索引)。

由TLBI,访问TLB中的某一组,遍历该组中的所有行,若找到一行的tag等于TLBT,且有效位valid为1,,则缓存命中,该行存储的即为PPN;否则缓存不命中,需要到页表中找到被请求的块替换原TLB表项中的块。

多级页表的访问:缓存不命中后,VPN被解释成从低位到高位的等长的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则第四级页表中的该表项存储的是所需要的PPN的值。

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

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

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

因此L1Cashe的物理访存大致过程如下:

(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组

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

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

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

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

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

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

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

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

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

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

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。

缺页中断处理:

CPU 首先把虚拟地址发送给 MMU,MMU 检查缓存,发现没有对应的页,于是触发异常,异常处理器会负责从磁盘中找到对应页面并与缓存/内存中的页进行置换,置换完成后再访问同一地址,并把从页表中得到对应的物理地址,接着 MMU 把物理地址发送给缓存/内存,最后从缓存/内存中得到数据。

7.9动态存储分配管理

1 堆:动态存储的分配管理是由动态内存分配器完成的,动态内存分配器维护着一个进程的虚拟内存区域(称为堆),堆是请求二进制零的区域,它季节再未初始化的数据区域后面,并向上增长。对于每个进程,内核维护一个brk变量,它指向堆顶。

2 堆结构:分配器把堆看成一组大小不同的块的集合来维护。其中每一个块就是一个连续虚拟内存片,它的状态有两种,已分配或者空闲。已分配块显式地保留为应用程序所使用,空闲块可被分配,保持空闲直到显式地被应用程序分配。已分配块保持分配状态,直到被释放。

3 分配器的实现:

1 分配器的分类:

显示分配器:要求应用显式地释放任何已分配块

隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,自动地释 放已分配块

2 隐式空闲链表:

带边界标签的隐式空闲链表将这些信息嵌入块本身,在块的前四个字节的头 部和最后四个字节的尾部中嵌入表示块状态的信息,最低位用来表示块的分配 状态,1表示已分配,0表示未分 配;整个四字节的值用来表示块的大小;在 空闲块进行合并时,通过前一个块的脚部和后一个块的尾部就能知道新释 放的空闲块该如何合并。

3.显式空闲链表:

将堆组织成一个双向空闲链表,在每一个空闲块中,都包含一个pred(前驱) 和succ(后继)指针,释放一个块可以采用后进先出(LIFO)的顺序或者按 照地址顺序来维护链表,前者释放块可以在常数时间内完成,后者需要线性的 时间,但具有更高的内存利用率。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。

在hello 的运行中,它需要与其他进程共享主存,而对于有限的主存空间,如果太多的进程需要太多的内存,这就可能会使程序没有空间可用。而虚拟内存的概念很好地解决了这一点。他是硬件异常,地址翻译,主存,磁盘和内核软件的完美交互,它为每个进程提供了大的私有的地址空间。在这个机制下,hello可以通过malloc从堆中申请内存,也可以通过free释放掉使用结束的内存。可以说,虚拟内存及其管理机制为hello提供了一个自由正确运行的广阔平台。


8hello的IO管理

8.1 Linux的IO设备管理方法

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

设备管理:所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单,低级的应用接口,称为Unix I/O。

这便是Linux的IO设备管理方法。

8.2 简述Unix IO接口及其函数

Unix I/O接口:

1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出,标准错误。

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

4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时执行读操作时触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

5.关闭文件:当应用完成了访问,它就通知内核关闭这个文件,并释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

6.Unix I/O函数:

int open(char* filename,int flags,mode_t mode) :进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

返回:若成功则为新文件描述符,若出错为-1。

7.int close(fd),fd是需要关闭文件的描述符。

返回:若成功则为新文件描述符,若出错为-1。

8.ssize_t read(int fd,void *buf,size_t n),该函数从描述符为fd的当前位置最多赋值n个字节到内存buf的位置,返回值为实际传送的字节数量。返回:若成功则为新文件描述符,若出错为-1。

9.ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

返回:若成功则为新文件描述符,若出错为-1。

8.3 printf的实现分析

分析首先查看printf函数的函数体:

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

接下来是write函数:

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

int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

查看syscall函数体:

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

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

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

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

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

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

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

8.4 getchar的实现分析

getchar 的源代码为:

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

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

8.5本章小结

了解了系统级的IO过程,分析了getchar和printf函数的实现

结论

1.预处理阶段,预处理器将hello.c include的外部的库取出合并到hello.i文件中

2.编译阶段,文本文件hello.i翻译成文本文件hello.s

3.汇编:将.s汇编程序翻译成机器语言指令,hello.s会变成为可重定位目标文件hello.o

4.链接:可重定位目标文件和动态链接库链接,生成可执行目标程序hello

5.运行:在shell中输入./hello 并跟上3个参数1190201015 guyuxiao 2

6.运行程序:shell收到运行./hello.c命令后,调用fork创建子进程,并调用execve启动加载器,加映射虚拟内存,创建新的内存区域,并将hello存入内存与CPU

7.执行指令:CPU为其分配时间片,在一个时间片中,hello有对CPU的控制权,顺序执行自己的代码

8.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址

9.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存

10.异常处理:如果运行途中键入ctr-c指令,则程序停止,如果运行途中键入ctr-z指令,则程序挂起

  1. 结束:shell父进程回收子进程的资源,hello的一生就此终结。

我的感悟:沿着一个简单的HELLO程序从代码到执行进行了一次细致地梳理,才发现在初学时就可以随手敲出来的代码背后有多少智慧的火花和天才的构想。这也是我第一次仔细的研究一个程序的执行,对我以后编写硬件友好的程序有很大的帮助。我想,随着这一次美妙的旅程。增长的不仅是对于计算机的理解,还有对系统这个软件层面上的科技黑箱一个剖析,一次由高级语言到电子信号的切身感受。以后如果还有机会,我会选择更深入的探究计算机的奥秘。


附件

hello.c :hello源代码

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

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

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

Hello:可执行目标程序,分析链接器行为

5.3helloelf.txt:hello的elf格式,分析汇编器和链接器行为

5.5helloobjdump.txt:可执行hello的反汇编,作用是重定位过程分析

elf.txt:hello.o的elf格式,分析汇编器和链接器行为

objdump.txt:hello.o的反汇编,主要是为了分析hello.o


参考文献

[1]  https://blog.51cto.com/u_15443779/4718537

[2]  https://www.cnblogs.com/diaohaiwei/p/5094959.html

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

[4]  程序人生-Hello’s P2P - 码农岛

[5]  深入理解计算机系统原书第3版

[6]  CSAPP大作业程序人生-Hello‘s P2P

[7]  程序人生-Hello’s P2P - 哔哩哔哩

[8]  详解重定位目标文件 - 简书

[9]  HIT-CSAPP大作业_东百月的博客-CSDN博客

[10]  CSAPP 大作业程序人生 - 哔哩哔哩

[11]  https://www.likecs.com/show-203699927.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值