Hello的一生(从程序到进程)

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算机类

学   号 1190201717

班   级 1903011

学 生 张志强

指 导 教 师 史先俊

计算机科学与技术学院

2021年5月

摘 要

本文将运用计算机系统中的知识分析一个程序(hello.c)从一个磁盘当中的程序到在操作系统上运行的进程,基本上涉及到程序从出生到死亡的各个阶段即P2P——从程序到进程。

关键词:从程序到进程;P2P;O2O;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O ;

目 录

第1章 概述
1.1 Hello简介
(1) P2P:

首先,在一个磁盘文件里编写代码,得到一个源程序hello.c;调用C预处理器对该程序进行宏替换,得到hello.i文件;然后调用C编译器对hello.i文件进行处理,得到hello.s这个汇编语言文件;接着调用汇编器处理可重定向的目标文件hello.o文件;最后通过链接器链接,得到可执行文件。然后在Bash里启动这个程序,这时shell会调用fork函数创建一个进程,然后调用execve函数加载这个程序并运行。

(2) O2O:

父进程调用fork函数创建一个子进程,为其创建相应的数据结构、创建虚拟内存并分配唯一的PID,然后调用execve函数将可执行程序进行内存映射并设置程序计数器。当程序退出后,由父进程或祖父进程来回收该进程,彻底删除掉它的痕迹。

1.2 环境与工具
硬件环境:

(1) 处理器:Intel® Core™ i5-9300H CPU @ 2.40GHz 2.40 GHz

(2) RAM:8GB

(3) 系统类型:64 位操作系统, 基于 x64 的处理器

(4) Window版本:Windows 10 家庭中文版

软件:

Gcc、edb、readelf、VSCode、vim、VMware、ubuntu-19.04桌面版等

1.3 中间结果
文件名

文件作用

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

Hello.o

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

hello

链接后的可执行目标文件

hello.out

反汇编后的可重定位文件

1.4 本章小结
本章首先简要介绍了hello.c的P2P与O2O,然后对整篇论文写作过程中对hello文件操作的所有需要的环境与工具进行了大致的列举,最后对实验过程中用到的所有中间文件及其作用和使用时期以一个表格的形式进行了大致的展现。

第2章 预处理
2.1 预处理的概念与作用
(1) 概念:

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

(2) 作用:

  1. 宏定义相关。预处理程序中的#define 标识符文本,预处理工作也叫做宏展开:将宏名替换为文本(这个文本可以是字符串、可以是代码等)。

  2. 文件包含相关。预处理程序中的#include,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

  3. 条件编译相关。根据#if以及#endif和#ifdef以及#ifndef来判断执行编译的条件。

2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i(或gcc -E hello.c -o hello.i)

图1:预处理生成hello.i

2.3 Hello的预处理结果解析

图2:源程序在hello.i文件里的对应位置

经过预处理后的hello.i文件变成了3000多行,原来hello.c的程序出现在3000多行。在这之前出现的是头文件 stdio.h unistd.h stdlib.h 的依次展开。 以 stdio.h 的展开为例: stdio.h 是标准库文件, cpp 到 Ubuntu 中默认的环境变量下寻找 stdio.h,打开文件/usr/include/stdio.h ,发现其中依然使用了#define 语句,cpp 对 stdio 中的define 宏定义递归展开。 所以最终的.i 文件中是没有#define 的; 发现其中使用了大量的#ifdef #ifndef 条件编译的语句, cpp 会对条件值进行判断来决定是否执行包含其中的逻辑。预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。

2.4 本章小结
本章主要介绍了预处理的概念和应用功能,以及Ubuntu下预处理的两个指令,同时具体到我们的hello.c文件的预处理结果hello.i文本文件解析,详细了解了预处理的内涵。我们发现预处理主要由预处理器完成,这一阶段一共完成4件事:头文件的展开;宏替换;去掉注释;条件编译。

第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

(1) 编译的概念:

编译就是将高级程序语言的源文件经过词法分析、语法分析、语义分析以及经过一系列优化后生成汇编代码的过程。具体到我们实验,就是将预处理得到的ASCII码的中间文件hello.i翻译成ASCII汇编语言文件hello.s的过程。

(2) 编译的作用:

编译的目的是将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤。它包括以下几个部分。

  1.   词法分析。对输入的字符串进行分析和分割,形成所使用的源程序语言所允许的记号(token),同时标注不规范记号,产生错误提示信息。
    
  2.   语法分析。分析词法分析得到的记号序列,并按一定规则识别并生成中间表示形式,以及符号表。同时将不符合语法规则的记号识别出其位置并产生错误提示语句。
    
  3.   语义分析。即静态语法检查,分析语法分析过程中产生的中间表示形式和符号表,以检查源程序的语义是否与源语言的静态语义属性相符合。
    
  4.   代码优化。将中间表示形式进行分析并转换为功能等价但是运行时间更短或占用资源更少的等价中间代码。
    
  5.   生成目标代码。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
    

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

图3:编译hello.i文件

3.3 Hello的编译结果解析
(1) 汇编指令:

.file:声明源文件

.text:代码节

.section:

.rodata:只读代码段

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

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

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

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

(2) 数据:

  1.   字符串:
    

图4:字符串数据

如上图:我们发现在.i文件里有两个字符串,对应于源程序里printf中的两个格式串,其中汉字是使用UTF-8来编码的。注意:这两个字符串都是在.rodata节当中的只读字符串。

  1.   局部变量i:
    

Main函数里声明了一个局部变量i,对于局部变量,编译器将其放到栈中,这样就可以随着方法栈帧的回收而回收了。如下图,通过基指针来访问该变量:

图5:局部变量的访问

  1.   argc:
    

通过分析发现,argc也是保存在栈当中的。(通过寄存器传参,保存在栈中)

图6:argc的保存

  1.   各种立即数:
    

立即数直接体现在汇编代码中,以$的形式出现。

图7:立即数的形式

  1.   数组:
    

首先,argv[]的首地址通过寄存器传参,且被保存在了-32(%rbp)当中:

图8:argv首地址的保存

然后,对数组元素的访问使用的是:首地址+偏移量的方式:

图9:对数组元素的访问

(3) 赋值操作:

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

movb:一个字节

movw:两个字节

movl:四个字节

movq:八个字节

(4) 算数操作:

本程序中只涉及到几种简单的算数操作:add、sub等

(5) 关系操作:

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

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

(6) 控制转移:

首先进行控制转移的前提条件是:已经设置了条件寄存器。

在本程序中:

  1. 对输入参数个数的判断:

图10:对输入参数个数的判断

  1. For循环:

图11:for循环

(7) 函数操作:

函数调用大致涉及到以下几步(以函数P调用函数Q为例):

  1.   通过寄存器和栈来传递参数
    
  2.   设置rip为Q的第一条指令的地址
    
  3.   Q为局部变量分配空间
    
  4.   函数Q执行
    
  5.   函数Q通过寄存器设置返回值,并回收栈帧
    
  6.   将程序计数器设置为P调用Q这条指令的下一条指令
    

(8) 类型转换:

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

3.4 本章小结
本章主要详细介绍了编译的概念与作用,以及在Ubuntu下编译的指令,最后我们具体到对hello.c源文件的编译文件hello.s进行数据类型(主要包括整数,字符串,数组)和操作(赋值操作,类型转换,算术操作,关系操作,指针数组结构操作以及控制转移和函数操作)的细致分析和探讨。

第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

(1) 汇编的概念:

汇编器as将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件也是可重定位目标文件,是二进制文件。

(2) 汇编的作用:

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

4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -c -o hello.o

图12:汇编生成.o文件

4.3 可重定位目标elf格式
(1) ELF头:

使用命令:readelf -h hello.o查看ELF头,如下图:

图13:ELF头

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

(2) 节头:

使用命令readelf -S hello.o查看节头,如下图:

图14:ELF节头

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

(3) 符号表:

使用readelf -s hello.o命令来查看符号表:

图15:符号表

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

(4) 各个节的内容:

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

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

. data : 已初始化的全局和静态C变量

. bss : 未初始化的全局和静态C变量

. symtab :一个符号表,它存放在程序中定义和引用的函数和全局变量的信息

. debug : 一个调试符号表,其条目时程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。

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

. strtab : 一个字符串表,其内容包括 .symtab 和 .debug节中的符号表,以及节头部中的节名字。

.rel.text:一个.text节中位置的列表,当链接器将这个文件和其它文件组合后会修改这些位置。

.rel.data:被模块引用或定义的所有全局变量的重定位信息。

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

图16:生成反汇编代码

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

  1.   数的表示:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
    
  2.   分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,也即间接地址。
    
  3.   函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
    

4.5 本章小结
本章对汇编结果进行了详尽的介绍。与我们的hello.o文件相结合,介绍了汇编的概念与作用,以及在Ubuntu下汇编的命令。同时本章主要部分在于对可重定位目标elf格式进行了详细的分析,侧重点在重定位项目上。同时对hello.o文件进行反汇编,将hello.asm(我的反汇编文件)与之前生成的hello.s文件进行了对比。使得我们对该内容有了更加深入地理解。

第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。

(1) 概念:

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

(2) 作用:

链接使得分离编译(seperate compila)成为可能,更便于我们维护管理,我们可以独立的修改和编译我们需要修改的小的模块。链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

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

图17:链接生成可执行文件

5.3 可执行目标文件hello的格式
输出到文件:readelf -a hello > hello1.elf

elf头:

图18:ELF头

节头:

图19:elf文件的节头

描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

5.4 hello的虚拟地址空间
使用edb加载hello, Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序。查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。在下面可以看出,程序包含PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO几个部分,如下图所示。

图20:Data Dump视图

其中PHDR 保存程序头表。INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可执行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。

图21:Linux的虚拟内存空间

5.5 链接的重定位过程分析
分析hello与hello.o的不同:

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

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

3.函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4.地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

链接的过程:

要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位(确定地址),修改.text节和.data节中对每个符号的引用(地址),而这些需要用到在.rel_data和.rel_text节中保存的重定位信息。

5.6 hello的执行流程
401000 <_init>

401020 <.plt>

401030 <puts@plt>

401040 <printf@plt>

401050 <getchar@plt>

401060 <atoi@plt>

401070 <exit@plt>

401080 <sleep@plt>

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1 <main>

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

5.7 Hello的动态链接分析
1)对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。

2)附上dl_init函数调用前后GOT信息变化截图

图22:dl_init函数调用前

图23:dl_init函数调用后

3)我们进一步发现,改变的是:从地址0x6001008处,由00 00 00 00 00 00变为了70 01 70 ff fb 7e。由00 00 00 00 00 00变为80 e6 4e ff fb 7e。由于机器为小端,则这两处改编成的地址应该是0x7e fb ff 70 01 70和0x7e fb ff 4e e6 80。

4)在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结
本章结合实验中的hello可执行程序依此介绍了链接的概念及作用,在Ubuntu下链接的命令行;并对hello的elf格式进行了详细的分析对比,同时注意到了hello的虚拟地址空间知识;并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程;遍历了整个hello的执行过程,在最后对hello进行了动态链接分析。

第6章 hello进程管理
6.1 进程的概念与作用
(1) 概念:

进程是执行中程序的实例。

(2) 作用:

在现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。

6.2 简述壳Shell-bash的作用与处理流程
(1) 作用:

Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作。

(2) 处理流程:

  1.   将用户输入的命令行进行解析,分析是否是内置命令;
    
  2.   若是内置命令,直接执行;若不是内置命令,判断是否为可执行文件,若是则bash在初始子进程的上下文中加载和运行它;
    
  3.   在前台或后台运行程序;
    
  4.   接收SIGCHLD信号,回收子进程。
    

6.3 Hello的fork进程创建过程
根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行新程序hello。函数原型为:int exeve(const char *filename, const char *argv[], const char *envp[]);如果成功,则不返回;如果错误,则返回-1。在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给hello的主函数(即main函数),该函数有以下原型:int main(int argv, char **argv, char **envp)或者等价的int main(int argc, char *argv[], char *envp)。

详细过程如下:

  1. 删除已存在的用户区域。

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

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

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

6.5 Hello的进程执行
(1) 时间片:

一个进程执行他的控制流的一部分的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)。

(2) 进程的上下文切换:

调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。

上下文切换:在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程。

1)保存当前进程的上下文

2)恢复某个先前被强占的进程被保存的上下文

3)将控制传递给这个新恢复的进程

用户的上下文信息由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

(3) 用户态和内核态的转换:

为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理
异常类型:

  1.  中断 来自I/O设备的信号 异步 总是返回到下一条指令
    
  2.  陷阱 有意的异常 同步 总是返回到下一条指令
    
  3.  故障 潜在可恢复的错误 同步 可能返回到当前指令
    
  4.   终止 不可恢复的错误 同步 不会返回
    

处理方式:

图24:中断处理

图25:陷阱处理

图26:故障处理

图27:终止处理

信号处理:

  1.   Ctrl+z:
    

图28:Ctrl+z

按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是11810;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台继续运行。

  1.   Ctrl+c:
    

图29:Ctrl+c

进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。

  1.   中途乱输:
    

图30:中途乱输

可以发现中途乱输的内容其实是被缓存在了输入缓冲区当中。

  1.   Kill
    

图31:Kill命令

挂起的进程被终止,在ps中无法查到到其PID。

6.7本章小结
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。

  1.   在硬件层,异常是由处理器中的事件触发的控制流中的突变
    
  2.   在操作系统层,内核用ECF提供进程的基本概念。
    
  3.   在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待他们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号。
    
  4.   最后在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数分支到另一个函数。
    

同时还有四种不同类型的异常:中断,故障,终止和陷阱。

第7章 hello的存储管理
7.1 hello的存储器地址空间

  1.   逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
    
  2.   线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
    
  3.   虚拟地址:也就是线性地址。
    
  4.   物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
    

7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。

索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。

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

GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

给定一个完整的逻辑地址段选择符+段内偏移地址,

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

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

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

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

图32:页表

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

图33:变换过程

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

图34:VA到PA的变换过程

如图 7,CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

图35:cache支持下的物理内存访问

大致过程如下:

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

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

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

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

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

7.7 hello进程execve时的内存映射
步骤:

  1.   删除已存在的用户区域。
    
  2.   映射私有区域
    
  3.   映射共享区域
    
  4.   设置程序计数器(PC)
    

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

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

图36:故障处理流程

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

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

(1)带边界标签的隐式空闲链表分配器管理:

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。

隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。

当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。

(2)显示空间链表管理:

显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。

显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。

7.10本章小结
通过本章,我们认识到以下几点事实:

1)虚拟内存是对主存的一个抽象。支持虚拟内存的处理器通过使用一种叫做虚拟内存寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。

2)虚拟内存提供三个功能:简化了内存保护;简化了内存管理;在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间的内容。

3)地址翻译的过程必须和系统中的所有的硬件缓存的操作集成在一起。

4)内存映射为共享数据、创建进程以及加载程序提供了一种高效的机制。

5)动态内存分配器直接操作内存,无需类型系统的很多帮助。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数
Unix I/O 接口:

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

(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。

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

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

(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。

Unix I/O 函数:

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

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

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

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

8.3 printf的实现分析

图37:printf实现

其中,vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

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

8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。

结论
1)全文总结:

1,hello.c经过预编译,拓展得到hello.i文本文件

2,hello.i经过编译,得到汇编代码hello.s汇编文件

3,hello.s经过汇编,得到二进制可重定位目标文件hello.o

4,hello.o经过链接,生成了可执行文件hello

5,bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello

6,hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。

7,hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关

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

2)感悟:

  1. 作为当代计算机专业的大学生,我们不仅要懂得上层实现,还要学习底层知识。

  2. 要培养全局意识、系统观念,了解每一个过程。

  3. 学习时要善于总结,将知识都融会贯通。

附件
文件名

文件作用

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

Hello.o

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

hello

链接后的可执行目标文件

hello.out

反汇编后的可重定位文件

hello1.elf

ELF格式文件

参考文献

  1. 布莱恩特.深入理解计算机系统.第三版.机械工业出版社

  2. 博客:https://blog.csdn.net/gzn00417/article/details/103792824

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值