程序人生-Hello’s P2P

计算机系统

大作业

 

题     目  程序人生-Hello’s P2P 

专       业         计算学部          

学     号       120L020730        

班     级        2003005       

学       生          原松涛        

指 导 教 师            吴锐         

计算机科学与技术学院

2021年5月

摘  要

本文通过分析Hello程序从代码编辑器到运行进程的过程,对计算机系统编译源文件、运行进程等机制进行较深入的分析和介绍。在Ubuntu环境下运用自己所学知识和各种工具对该程序的整个生命周期进行了系统的分析,体会无数计算机系统机制设计者的思想精华。

关键词:计算机系统;底层实现;Linux;进程                           

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

1.2 环境与工具..................................................................................................... - 4 -

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 4 -

第2章 预处理......................................................................................................... - 5 -

2.1 预处理的概念与作用..................................................................................... - 5 -

2.2在Ubuntu下预处理的命令.......................................................................... - 5 -

2.3 Hello的预处理结果解析.............................................................................. - 5 -

2.4 本章小结......................................................................................................... - 5 -

第3章 编译............................................................................................................. - 6 -

3.1 编译的概念与作用......................................................................................... - 6 -

3.2 在Ubuntu下编译的命令............................................................................. - 6 -

3.3 Hello的编译结果解析.................................................................................. - 6 -

3.4 本章小结......................................................................................................... - 6 -

第4章 汇编............................................................................................................. - 7 -

4.1 汇编的概念与作用......................................................................................... - 7 -

4.2 在Ubuntu下汇编的命令............................................................................. - 7 -

4.3 可重定位目标elf格式................................................................................. - 7 -

4.4 Hello.o的结果解析...................................................................................... - 7 -

4.5 本章小结......................................................................................................... - 7 -

第5章 链接............................................................................................................. - 8 -

5.1 链接的概念与作用......................................................................................... - 8 -

5.2 在Ubuntu下链接的命令............................................................................. - 8 -

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

5.4 hello的虚拟地址空间.................................................................................. - 8 -

5.5 链接的重定位过程分析................................................................................. - 8 -

5.6 hello的执行流程.......................................................................................... - 8 -

5.7 Hello的动态链接分析.................................................................................. - 8 -

5.8 本章小结......................................................................................................... - 9 -

第6章 hello进程管理................................................................................... - 10 -

6.1 进程的概念与作用....................................................................................... - 10 -

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

6.3 Hello的fork进程创建过程..................................................................... - 10 -

6.4 Hello的execve过程................................................................................. - 10 -

6.5 Hello的进程执行........................................................................................ - 10 -

6.6 hello的异常与信号处理............................................................................ - 10 -

6.7本章小结....................................................................................................... - 10 -

第7章 hello的存储管理................................................................................ - 11 -

7.1 hello的存储器地址空间............................................................................ - 11 -

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

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

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

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

7.6 hello进程fork时的内存映射.................................................................. - 11 -

7.7 hello进程execve时的内存映射.............................................................. - 11 -

7.8 缺页故障与缺页中断处理........................................................................... - 11 -

7.9动态存储分配管理....................................................................................... - 11 -

7.10本章小结..................................................................................................... - 12 -

第8章 hello的IO管理................................................................................. - 13 -

8.1 Linux的IO设备管理方法.......................................................................... - 13 -

8.2 简述Unix IO接口及其函数....................................................................... - 13 -

8.3 printf的实现分析........................................................................................ - 13 -

8.4 getchar的实现分析.................................................................................... - 13 -

8.5本章小结....................................................................................................... - 13 -

结论......................................................................................................................... - 14 -

附件......................................................................................................................... - 15 -

参考文献................................................................................................................. - 16 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

      1. P2P

就是From Program to Process的整个过程。

首先运行cpp预处理生成hello.i,然后运行ccl进行翻译生成hello.s,再运行as将其翻译成一个可重定位目标文件hello.o,然后运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello,最后在bash下输入相关命令后即可调用fork函数为可执行文件创建子进程。

1.1.2O2O

就是From Zero-0 to Zero-0。

在bash调用fork函数后,还会调用execve函数映射虚拟内存,即mmp;然后载入物理内存,进入到main函数当中执行目标的代码。程序运行完成后,bash回收子进程,内核删除相关数据痕迹。

1.2 环境与工具

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

1.2.1硬件环境

X64 CPU;2.3GHz;16GRAM;512GHD

1.2.2软件环境

Windows 10 64位;Vmware16.2.3;Ubuntu20.04

1.2.3开发与调试工具

CodeBlocks 64位

gcc/as/ld/vim/edb/readelf/gedit

1.3 中间结果

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

文件的名字

文件的作用

hello.i

预处理后文件

Hello.c

源程序

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标执行文件

hello

链接后的可执行文件

elf.txt

hello.o的elf

elf.txt

hello的elf

1.4 本章小结

    对hello进行了简要的介绍,主要介绍了P2P过程,并且介绍了分析过程中的软硬件环境以及所使用的工具,是文章的基础部分。

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

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

2.1.2预处理的作用

按常见的代码部分分类:

头文件(Header files):

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

宏(Macros):

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

条件编译(Conditionals):

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

此外,还有Diagnostics,Line Control,Pragmas等会被cpp处理

2.2在Ubuntu下预处理的命令

预处理命令:

gcc -E hello.c -o hello.i

 

图表 1

2.3 Hello的预处理结果解析

打开hello.i

 

图表 2

明显发现文件增大,行数显著增加。

 

图表 3

源代码部分保留在文件末尾位置

对原程序中的宏进行了宏展开,头文件中的内容被包含进该文件中。

2.4 本章小结

本章介绍了预处理的概念和作用,再ubuntu中实际对hello.c进行了预处理,然后查看并分析了处理后的hello.i文件,直观地进行了理解。

第3章 编译

3.1 编译的概念与作用

3.1.1概念

编译器ccl hello.i 翻译成文本文件 hello.s,hello.s由汇编代码构成

3.1.2作用

以高级程序设计语言书写的源程序作为输入,以汇编语言或机器语言表示的目标程序作为输出。 除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化等功能.

3.2 在Ubuntu下编译的命令

命令: gcc -S hello.i -o hello.s

 

图表 4

3.3 Hello的编译结果解析

3.3.1 汇编指令

图表 5

.file:声明源文件

.text:指示为代码段

.section .rodata:只读代码段

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

.string:声明一个字符串

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

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

3.3.2数据

3.3.2.1字符串

 

图表 6

3.3.2.2局部变量i

 

图表 7

3.3.2.3argc

主函数的参数

3.3.2.4数组:char *argv[]

 

图表 8

3.3.2.5整数和字符串常量

直接体现在汇编代码中

3.3.3算术操作

 

图表 9

算术操作的机器语言实现如上。

本代码中为i++的实现

 

图表 10

3.3.4关系操作和控制转移指令

 3.3.4.1

 

图表 11

对应argc!=4

判断argc是否等于4,如果argc等于4,则不执行if语句,否则执行if语句

3.3.4.2

 

图表 12

对应i<8

 

图表 13

for循环的条件控制语句

3.3.5赋值

主要使用mov指令来实现

3.3.6函数操作

第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。

3.3.1.1main函数

传入参数argc和argv[],设置%eax为0并且返回,对应return 0

 

图表 14

3.3.1.2printf函数

 

图表 15

 

图表 16

3.3.1.3exit函数

  call exit@PLT

3.3.1.4sleep函数

  call sleep@PLT

3.3.1.5getchar函数

  call getchar@PLT

3.3.7 类型转换

hello.c中涉及类型转换atoi

3.4 本章小结

介绍了编译的概念以及过程,通过分析hello.s介绍了汇编代码的相关实现。经过了这一章节,我们可以将汇编语言翻译成c语言。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标二进制文件文件中。此时的hello.o还未进行链接,所以不可直接运行。

4.2 在Ubuntu下汇编的命令

gcc -c -no-pie -fno-PIC hello.s -o hello.o

 

图表 17

4.3 可重定位目标elf格式

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

4.3.1 readelf命令

readelf -a hello.o > ./elf.txt

 

图表 18

4.3.2ELF Header

Magic 描述了生成该文件的系统的字的大小和字节顺序,还包含了ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。

 

图表 19

4.3.3Section Headers

 

图表 20

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

4.3.4查看符号表.symtab

 

图表 21

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

4.3.5重定位节.rela.text

 

一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

重定位节.rela.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进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

 

图表 22

 

图表 23

发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码

在数的表示上,hello.s中的操作数表现为十进制,而hello.o反汇编代码中的操作数为十六进制。

在控制转移上,hello.s使用.L2和.LC1等段名称进行跳转,而反汇编代码使用目标代码的虚拟地址跳转。

在重定位信息的处理上,汇编代码直接访问了.rodata节的数据,直接按函数名调用了函数,而反汇编代码中二者均未填入真实地址,需要进行重定位

4.5 本章小结

进一步对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了hello.o的ELF格式,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。hello.c已经成为了能够被CPU理解的指令。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

ld-ohello-dynamic-linker/lib64/ld-linux-x86-64.so.2/usr/lib/x86_64-linux-gnu/crt1.o/usr/lib/x86_64-linux-gnu/crti.ohello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o

 

图表 24

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

5.3.1ELF Header

 

图表 25

hello文件的类型是EXEC,表明hello是一个可执行目标文件

5.3.2 Section Headers

 

图表 26

对 hello中所有的节信息进行了声明,包括大小Size以及在程序中的偏移量 Offset,凭借 Section Headers 中的信息使得我们可以通过 HexEdit 定位各个节所占的区间(起始位置,大小)。

5.3.3重定位节

 

图表 27

5.3.4动态符号表

 

图表 28

5.3.5符号表

 

图表 29

5.3.6程序头部表

 

图表 30

描述了文件的布局情况

5.4 hello的虚拟地址空间

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

  

 

图表 31

 

图表 32

通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0

根据节头部表,可以通过edb找到各个节的信息

.text为例

 

图表 33

开始于0x4010f0

5.5 链接的重定位过程分析

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

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

 

图表 34

 

图表 35

 

图表 36

链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。

hello中增加了.init和.plt节,和一些节中定义的函数。

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

Hello实现了调用函数时的重定位,因此在调用函数和在跳转时调用的地址已经是函数确切的虚拟地址。

链接的过程:

链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址。

5.6 hello的执行流程

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

 

图表 37

 

图表 38

5.7 Hello的动态链接分析

  

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

 

图表 39

 

图表 40

plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。

 

图表 41

 

图表 42

5.8 本章小结

主要介绍了链接的概念与作用,描述了hello.o链接成为一个可执行目标文件的过程,介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。在这个过程中运用了edb这一工具,通过对比的手法直观的进行了展现。

第6章 hello进程管理

6.1 进程的概念与作用

进程是一个执行中的程序的实例,系统中的每个程序都运行在某个进程的上下文中。进程不只是程序代码,还包括当前活动。

作用:进程提供给应用程序关键抽象:

一个独立的逻辑控制流

一个私有的地址空间

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

shell是用户级的应用程序,代表用户控制操作系统中的任务。而bashshell的一种

处理流程:

1从终端读入键盘的输入。

2分析字符串,获取命令行参数。

3检查第是否是一个内置的shell命令,如果是则立即执行。如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。

4重复23

6.3 Hello的fork进程创建过程

解析输入时,发现不是一个内置命令,于是将其判定为可执行程序。shell为它fork一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。

6.4 Hello的execve过程

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

2.映射私有区域。为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。

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

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

6.5 Hello的进程执行

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

hello进程的执行是依赖于进程所提供的抽象的基础上,下面为操作系统所提供的的进程抽象:

6.5.1 逻辑控制流和时间片:

一系列程序计数器 PC的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

一个进程执行它的控制流的一部分的每一时间段叫做时间片。

6.5.3 上下文:

上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

当内核选择一个新的进程运行时,则内核调度上下文切换

6.5.4 调度的过程:

在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。

6.5.5 用户态与核心态转换:

为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。

核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,确保了系统的安全性。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

 

图表 43

执行过程中可能收到SIGINTSIGTSTP等;在hello结束或停止时会发出SIGCHLD信号。

Ctrl+C:进程收到 SIGINT 信号,结束 hello。

 

图表 44

中途乱按:将屏幕的输入缓存到缓冲区。

 

图表 45

Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是4479;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台,kill终止进程。

 

图表 46

 

图表 47

6.7本章小结

介绍了进程管理相关内容,了解了hello进程的执行过程,了解了异常的情况以及相关处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

7.1.1 逻辑地址

逻辑地址(Logical Address)是指程序经过编译后出现在汇编代码中的地址。在这里指的是hello.o中的内容。

7.1.2 线性地址

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。

7.1.3 虚拟地址

虚拟地址强调程序拿到的地址并不是真实的物理地址,而是一个虚拟的地址(由逻辑地址表示),需要经过到线性地址再到物理地址的变换。

7.1.4 物理地址

放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。

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

在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。

一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。

给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。

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

CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offsetm, VPO)和一个(n-p)位的虚拟页号(Virtual Page Number, VPN)。MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号(Physical Page Number, PPN) 和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移( Physical Page Offset,PPO)和VPO是相同的。

当页命中时,CPU硬件执行的步骤:

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

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

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

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

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

当缺页时,需要硬件和操作系统协作完成:

        第1~3步:同上。

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

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

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

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

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

7.4.1 翻译后备缓冲器

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

7.4.2 多级页表:

 

图表 48

7.4.3 VA到PA的变换

处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘——写回策略)。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。

在多级页表的情况下,就是不断通过索引 – 地址 – 索引 - 地址重复四次进行寻找。

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

CPU发送一条虚拟地址,随后MMU按照7.4所述的操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CI(组索引),CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去L2,L3,主存判断是否命中,命中时将数据传给CPU同时更新各级cache的储存。

 

图表 49

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

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

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

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

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

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:

 

图表 50

假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

1.判断虚拟地址A是否合法,就是判断A是否在某个区域结构定义的区域内。缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_ start 和vm_ end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。

2.判断试图进行的内存访问是否合法。就是判断进程是否有读、写或者执行这个区域内页面的权限。例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。

3.如果以上两点均不成立,那么内核知道这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"), 它指向堆的顶部。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

显式分配器(explicit allocator), 要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。

隐式分配器(implicit allocator), 另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbagecollec-tor),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection) 。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.10本章小结

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

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux将文件所有的I/O设备都模型化为字节序列,即文件。Linux提供了Unix IO接口函数,Linux基于Unix I/O实现对设备的管理。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O 函数:

  1. int open(char* filename,int flags,mode_t mode),进程通过调用 open 函数来打开一个存在的文件或是创建一个新文件。

open函数将filename 转换为一个文件描述符(用数字表示),返回的数字总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问该文件,mode给该文件赋予更多的权限选项。

  1. int close(fd),进程通过调用close函数关闭一个打开的文件

fd是需要关闭的文件的描述符,close返回操作结果。

  1. ssize_t read(int fd,void *buf,size_t n) 输入

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

  1. ssize_t wirte(int fd,const void *buf,size_t n) 输出

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

8.3 printf的实现分析

首先是printf函数的函数体

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

{

int i;

char buf[256];

  

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

     i = vsprintf(buf, fmt, arg);

     write(buf, i);

  

     return i;

 }

int vsprintf(char *buf, const char *fmt, va_list args)

   {

    char* p;

    char tmp[256];

    va_list p_next_arg = args;

  

    for (p=buf;*fmt;fmt++) {

    if (*fmt != '%') {

    *p++ = *fmt;

    continue;

    }

  

    fmt++;

  

    switch (*fmt) {

    case 'x':

    itoa(tmp, *((int*)p_next_arg));

    strcpy(p, tmp);

    p_next_arg += 4;

    p += strlen(tmp);

    break;

    case 's':

    break;

    default:

    break;

    }

    }

  

    return (p - buf);

   }

  1. vsprintf函数将所有的参数内容格式化之后存入buf,返回格式化数组的长度。
  2. write函数将buf中的i个元素写到终端。

提示:

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

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

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传

8.4 getchar的实现分析

当程序调用getchar(),程序等待用户按键。,用户输入的字符被存放在键盘缓冲区中直到用户按回车(回车也在缓冲区中)

当按下的字符为回车时,中断处理程序将结束getchar并返回读入的第一个字符。

提示:

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

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

8.5本章小结

从hello的IO管理介绍了Linux的IO设备管理方法,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.bash进程调用fork函数,生成子进程;

6.execve函数加载运行当前进程的上下文中加载并运行新程序hello

7.hello最终被shell父进程回收,内核会收回为其创建的所有信息

让我不得不感慨,计算机系统的设计与实现凝聚了多少先辈的伟大智慧,计算机系统是如此的每秒,通过课程的学习,我们应该学习过往经验,让自己的代码更加高效安全。不仅如此,通过对计算机系统的学习,让我们不再停留在应用层面,而是能对他的底层有了深刻理解。

附件

文件的名字

文件的作用

hello.i

预处理后文件

Hello.c

源程序

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标执行文件

hello

链接后的可执行文件

elf.txt

hello.o的elf

elf.txt

hello的elf

参考文献

[1]  [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)

[2]  Bryant,R.E. 深入理解计算机系统 2016机械工业出版社

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值