hello的一生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
计算机科学与技术学院
2019年12月
摘 要
本论文利用程序员编写的第一个程序hello来阐述一个程序从生成可执行文件到最后产生输出结构涉及的计算机系统思想和结构,重点描述了hello.c编译过程中编译器执行的操作以及动态链接的实现方式,具体涉及符号解析以及各个节的重定位。进一步分析了shell是如何把可执行文件加载,如何利用异常和异常处理进行系统调用和进程管理,最后简单阐述了Unix IO的实现原理

关键词:编译; 链接; 进程; 异常处理; 函数调用; 内存管理; 内存映射;

目 录

第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简介
P2P:(form program to progress)

  1. 预处理:hello.c->hello.i,预处理器执行预处理命令,将宏定义替换,将头 文件中包含的文件插入hello.i中;
  2. 编译:hello.i->hello.s,汇编器将hello.i中的c代码转换成汇编代码
  3. 汇编:汇编器将.s文件中的内容打包处理翻译成机器语言生成.o文件
  4. 链接:加hello.o模块与动态库中的模块链接在一起生成可执行文件hello

O2O:用户通过shell执行可执行目标程序hello,并且同时为该程序映射出虚拟内存。当该程序的进程开始运行时,系统会为其分配并且载入物理内存。在进程运行过程中,shell会调用fork函数,生成一个子进程,并在这个子进程中调用execve函数加载hello程序。然后程序会跳转到_start地址,最终调用hello的main函数。当hello的进程结束时,shell会将其的内存空间回收。
1.2 环境与工具
1.Ubuntu 18.04
2.Gcc
3.i7+8750H 24GB ram
1.3 中间结果
文件名 作用
hello.i Hello.c预处理的结果
hello.s Hello.i编译后的产物
hello.o Hello.s汇编后的产物
hello-obj.s Hello程序反汇编文件
hello_readelf Hello程序整体结构文件
hello Hello可执行文件

1.4 本章小结
简单描述了本论问将要探讨的问题并给出了中间产物
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
1.预处理的定义:预处理也称预编译,是指在编译器进行编译的第一遍扫描之 前所作的工作,预处理指令指示那些在程序正式开始编译之前需要由编译器 进行的操作,可以放在程序中的位置。
2.预处理的作用:在编译器编译之前完成如宏定义(#Define)、头文件包含 (#include)、条件编译(#ifend)等预编译指令,从而简化程序的阅读、修 改、移植和调试,也利于模块化程序的设计
2.2在Ubuntu下预处理的命令
Linux>gcc -E hello.c -o hello.i

图1.
2.3 Hello的预处理结果解析

图2.
由图2可以看出以#开头的预处理指令发生了变化,插入了很多新的代码,剩下的与原来基本一致,增加的代码为三个头文件的源码,其中包括:
(1)运行库在计算机内的位置:

图3.
(2)定义和引用的变量:

图4.
(3)声明的函数:

图5.
Main函数的部分代码为发生变化是因为我们没有在里面使用宏定义等预处理指令,也就不需要进行替换。
2.4 本章小结
简单地说明了预处理的概念和作用,分析了Linux下使用gcc对c文件进行预处理的过程,分析了hello.c预处理的结果
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:将高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的翻译程序。
作用:将预处理后的.i文件中的c代码转换成汇编代码
3.2 在Ubuntu下编译的命令
gcc -S -o hello.s hello.o或者 gcc -S -o hello.s hello.o

图6.
3.3 Hello的编译结果解析
3.3.1 处理变量:
(1)全局变量

图7.
如图7所示,Sleepsecs描述为全局变量,类型为object存放在.data节中,大小为4字节,初始化的值为2,对齐方式为4字节对齐。
(2)局部变量
如图8所示,main函数中的局部变量i是由函数运行时的栈管理的,通过i在栈中的偏移量来寻址,利用%rbp中的值进行寻址。

图8.

如图9所示,%edi和%rsi存储了传入命令行的参数:argc和argv,也被存入栈帧中进行管理。

图9.
3.3.2字符串常量:
如图10所示,printf中的fomant字符串被提取到程序开头,存放在.rodata中,利用与%rip的相对位置进行寻址,.L0和.LC1为偏移量,如图11所示。

图10.

图11.
3.3.3 数组、指针、结构体:
数组与结构体类似,成员都是利用内部的偏移来进行寻址,指针利用寻址操作取出指针所指向的类型的值。
3.3.4 赋值操作:
(1)全局变量的初始化,如果赋初值不为0,则存放在.data段,若初值为0,则存放在.bbs段,若不赋值则存放在.COMMON段。
(2)非静态局部变量的初始化,通过传送指令mov实现,如图12所示,i被初始化为0。

图12.
3.3.5 类型转换:
生成的汇编文件已直接完成类型转换,本程序中sleepsecs为int类型初始化为2.5,是一种隐式转化,在.s文件中的头部可以看到,初始化的值为2,遵循从浮点数数想整型转换时向0舍入的规则,具体细节如图13所示。

图13.
3.3.6 算术操作和逻辑操作:
本程序中只有一次算术操作,i++,由图14所示指令完成:

图14.
(1)在汇编语言中,算术操作加、减、乘、除都是通过一系列的指令完成:
ADD 加法.
  ADC 带进位加法.
  INC 加 1.
  AAA 加法的ASCII码调整.
  DAA 加法的十进制调整.
  SUB 减法.
  SBB 带借位减法.
  DEC 减 1.
  NEC 求反(以 0 减之).
  CMP 比较.(两操作数作减法,仅修改标志位,不回送结果).
  AAS 减法的ASCII码调整.
  DAS 减法的十进制调整.
  MUL 无符号乘法.
  IMUL 整数乘法.
  以上两条,结果回送AH和AL(字节运算),或DX和AX(字运算),
  AAM 乘法的ASCII码调整.
  DIV 无符号除法.
  IDIV 整数除法.
(3)移位和循环指令如下:
SHL 左移
SHR 右移
SAL 算术左移
SAR 算术右移
ROL 循环左移
ROR 循环右移
RCL 带进位的循环左移
RCR 带进位的循环右移
SHLD 双精度左移
SHRD 双精度右移
3.3.7 关系操作与控制语句:
(1)关系操作通过cmp语句实现,在本程序中,存在两处关系操作
1.argc!=3,通过指令cmpl $3, -20(%rbp)实现,其中-4(%rbp)为argc在函数栈帧中的地址。
2.i<10,通过指令cmpl $9, -4(%rbp)实现,其中 -4(%rbp)为i在函数栈帧中的地址。
(2)控制语句,本程序存在两处条件跳转,都是利用条件跳转指令实现的控制转移,for循环的跳出如图15所示,通过cmp判断i与9的比值大小,如果小于等于就跳转到.L4继续执行循环迭代,否则跳转到另一过程调用,其本质上是把当前的程序计数器的值压栈,并修改控制流。否则下个执行的指令是另一个分支,也就是接下来的getchar语句。

图15.
3.3.7 函数操作:
(1)参数传递:
1.main函数接收的是来自命令行的参数,由于64位系统前六个参数是直接传 给寄存器的,所以argc的值存储在%edi,argv为字符串指针,其传送的是地 址,存储在%rsi中。
2.printf的参数存储处在%rdi中,传送的是要打印的字符串在虚拟内存中的地 址,利用pc相对寻址实现地址的读取
3.exit的参数存储再%edi中
4.sleep的参数存储在%edi中,也是利用pc相对寻址

图16.

图17.
(2)函数调用与函数返回

  1. 函数调用通过call指令实现,call将当前%rip的值压入栈中,作为将来的返回地址,将要调用的函数的地址设置为程序计数器的值,实现调用
  2. 函数返回通过ret指令实现,其相当于pop %rip,将栈底的返回地址弹出,设置成程序计数器的值实现返回调用者函数

图18

3.4 本章小结
简单阐述了编译的概念和作用,着重分析了从c程序到汇编程序中,变量和 常量的位置以及初始化、函数调用的过程,各种变量的引用方式,控制语句的 和关系操作的实现。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
1.概念:把汇编语言翻译成机器语言,同时把指令打包成可重定位目标程序的 格式,生成.o文件
2.将.s文件中的汇编语言也即汇编指示转化成机器可以理解的二进制文件
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s 或者gcc -c -o hello.o hello.c

图19.
4.3 可重定位目标elf格式

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

图20.
2.节头部表
描述了不同节的位置和大小把汇编语言翻译成机器语言,同时把指令打包成可重定位目标程序的格式,生成.o文件

图20.
3.重定位节

3.1.rel.txt
.txt段中需要修改的位置的列表,调用的外部函数或者引用全局变量的指 令都需要修改。本程序的代码段重定位信息标出了引用的库的函数调 用、 引用的全局变量和只读段数据常量类型表示寻址方式,偏移量 表示其在代 码段中引用的位置距代码段起始位置处的偏移量,其中引用的全局变量 sleepsecs和.rodata中的printf的格式串都是采用32位pc相对寻址方式。

图21.
r_addend:
此成员指定常量加数,用于计算将存储在可重定位字段中的值。
R_X86_64_PC3:重定位一个使用32位PC相对地址的引用。在指令中编码的 32位值加上PC的当前运行时值,得到有效地址。
R_X86_64_32:重定位一个使用32位PC绝对地址的引用。直接使用在指令中 编码的32位值作为有效地址
3.2.rel.data
被模块引用或者定义的所有全局变量的重定位信息,一般而言,任何已经初始 化的全局变量,如果它的初始值是一个全局变量的地址或者外部定义函数的地 址,都需要修改。
4.符号表.symtab
它存放在程序中定义和引用的函数和全局变量的信息。如图22所示,本程序调用的puts等函数都未在本模块中定义,引用的是库中的函数。而sleepsecs和main都是本模块已经定义的,前者再.data段,后者在.txt段

图22.
4.4 Hello.o的结果解析
1.函数名称不再仅仅是一个符号,分配了地址

图23.
2.函数调用的地址采用了相对寻址,暂时用0占位

图24.
3.分支转移中的跳转偏移从符号到具体的偏移量

图25.
4.原来的各个段消失了被整合成一个main函数
5.相对寻址暂时用 0x0(%rip)替代

图26.
linux>objdump -d hello.o >hello-obj.s:(蓝色表示函数调用,红色表示对数据段值的引用)

图27
4.5 本章小结
简述了汇编的概念以及作用,分析了可重定位文件的格式及各个节存储的信息,重点分析了可重定位条目的含义,比较了汇编前后.s文件和.o文件之间的差别(汇编指令和机器指令的差别)。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
1.链接的概念:将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
2.链接的作用:链接使得分离编译成为可能,我们可以将大型的应用程序分解为小块的、好管理的模块,可以独立地修改和编译这些模块。
5.2 在Ubuntu下链接的命令
ld -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 /usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello

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

  1. ELF头:

图29.
2.节头:

图30.

  1. .text节是保存了程序代码指令的代码节。一段可执行程序,存在Phdr,.text就会存在于text段中。由于.text节保存了程序代码,因此节的类型为SHT_PROGBITS。

2).rodata 保存只读数据。类型PROGBITS。

3).plt 过程链接表(Procedure Linkage Table),包含动态链接器调用从共享库导入的函数所必须的相关代码。存在于text段中,类型PROGBITS。

4).bss节保存未初始化全局数据,是data的一部分。程序加载时数据被初始化成0,在程序执行期间可以赋值,未保存实际数据,类型NOBITS。

5).got节保存全局偏移表。它和.plt节一起提供了对导入的共享库函数访问的入口。由动态链接器在运行时进行修改。如果攻击者获得堆或者.bss漏洞的一个指针大小写原语,就可以对该节任意修改。类型PROGBITS。

6).dynsym节保存共享库导入的动态符号信息,该节在text段中,类型DYNSYM。

7).dynstr保存动态符号字符串表,存放一系列字符串,代表了符号的名称,以空字符作为终止符。

8).rel节保存重定位信息,类型SHT_REL。

9).hash节,也称为.gnu.hash,保存一个查找符号散列表。

10).symtab节,保存了ElfN_Sym类型的符号信息,类型SYMTAB。

10)strtab节,保存符号字符串表,表中内容被.symtab的ElfN_Sym结构中的st_name条目引用。类型SHT_SYMTAB。

11).shstrtab节,保存节头字符串表,以空字符终止的字符串集合,保存了每个节节名,如.text,.data等。有个e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。这节的类型是SYMTAB。

12).ctors和.dtors节,前者构造器,后者析构器,指向构造函数和析构函数的函数指针,构造函数是在main函数执行前需要执行的代码,析构是main函数之后需要执行的代码。

3.程序头表

图31.
1)INTERP,将信息二号位置存放在一个NULL为终止符的字符串中,对程序解释器位置的描述。

2)PHDR段保存了程序头表本身的位置和大小,Phdr表保存了所有的Phdr对文件(以及内存镜像)中段的描述信息。

3)LOAD,一个可执行文件只是一个LOAD类型的段。这类程序头描述的是可装载的段。一般一个需要动态链接的ELF可执行文件通常由两个可装载的段:存放程序代码的text段,存放全局变量和动态链接的data段。上面两个段会被映射到内存,根据p_align中存放的值在内存中对齐。

4)PT_DYNAMIC包含动态链接器所必有的一些信息,包含了一些标记值和指针。

5)PT_NOTE,保存了与特定供应商或者系统相关的附加信息。

5.4 hello的虚拟地址空间
1.ELF可执行文件在虚拟内存中的映射如下:

图32.
2. 本进程的虚拟地址空间段
1)PHDR段 起始地址 0x400040 大小0x1c0

2)INTERP段 起始地址 0x400200 大小0x1c

3)LOAD段 起始地址 0x400000 大小0x81c

4)LOAD段 起始地址 0x600e00 大小0x258

5)DYNAMIC段 起始地址 0x600e10 大小0x1e0

6)NOTE段 起始地址 0x40021c 大小 0x20

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

5.5 链接的重定位过程分析
1)main函数的地址不再是从0开始
2)对于.rodata段的数据的引用,在指令里给出了具体的偏移量,如图33蓝色 方框所示。
3)函数调用给出了具体的函数地址,不再是用0占位,如图33棕色方框所示。

图33.
3)引用的全局类型的函数的具体代码被添加进来,如图36所示

图34.
6)具体的重定位解析:
以对rodata段的引用做分析,hello.o文件的.rela.txt节的引用方式如图35所示,采用是pc32位相对寻址,距.txt起始处为0x18处开始引用,也即是说引用的地方位于main+0x18处,而定义的地方在.rodata处,查看链接后hello可执行文件的.rodata段的位置为4006f0,因此符号定义在4006f4处,当前引用处的rip值为400603,因此偏移量为0xf1,如如36所示。

图35.

图36.
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名称 程序地址
ld-2.27.so!_dl_start 0x7f5d6118fea0
ld-2.27.so!_dl_init 0x7f5d6119e630
libc-2.27.so!__libc_start_main 0x00007f0b4708
hello!_start 0x400500
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0

5.7 Hello的动态链接分析
.got.plt段是函数的绝对地址;.plt段是为了延迟绑定而创建的中间层,程序调用动态加载其把函数的地址填充到got.plt段中去。
查看hello的节头表可知重定位表的地址,我们利用edb跟踪其内容变化

图37.
1)调用dl_init前:

图38.

  1. GOT内有16个为0的字节
  2. 调用的库函数地址如下图所示:

图39.
2)调用dl_init后:

  1. GOT处原先为0的16个字节用地址填充了,其中第二个8字节填充的便是puts的函数实际地址

图40.
2. 调用库函数时为实际地址:

图41.
从调用库函数计算为实际地址可以看出,本程序已经链接到了动态库。
5.8 本章小结
简述了链接的概念和作用,分析了可执行文件的格式及其各个段在虚拟地址空间中的映射,重点分析了链接的重定位过程以及动态链接的实现方式。
(以下格式自行编排,编辑时删除)
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
1.进程的概念:进程是计算机中关于某数据集合上的一次运行活动。
2.进程的作用:为系统进行资源分配和调度提供了基本单位,是操作系统结构的基础。
6.2 简述壳Shell-bash的作用与处理流程
1)shell的作用:Shell是一种为使用者提供操作界面的软件,具备命令行解释器的作用,作为系统和用户之间的接口。
2)处理流程:
1.获取命令行用户输入的命令
1.1 内置命令,执行内置命令
1.2 非内置命令,则为一个可执行程序
1.2.1创建子进程,运行加载该可执行程序
1.2.1.1 前台运行,等待程序在前台运行完毕,并回收
1.2.1.2 后台运行,不等待程序运行,运行结束时回收
6.3 Hello的fork进程创建过程
在命令行中输入./hello 学号 姓名 ,shell识别出这个不是一个内置命令,便会试图加载当前文件下的hell程序,shell便调用fork创建一个新的子进程,新的进程获得和shell程序一模一样但独立的副本,同时这个新的子进程获得一个属于自己的pid,fork调返回两次,shell返回子进程pid,子进程返回0。
6.4 Hello的execve过程
Fork出来的子进程调用exceve函数,将命令行中的程序名称、学号、姓名作为argv参数,exceve调用一次不会返回,除非发生错误。具体实现通过下面的步骤实现(来源:csapp深入理解计算机系统):
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

  1. 上下文信息:操作系统使用一种上下文的较高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文,它包括通用目的寄存器、浮点寄存器、程序计数器、用户栈和各种内核数据结构(比如描述地址空间的页表)、包含有关当前进程信息的进程表,以及包含进程已经打开文件的信息的文件表
  2. 进程时间片一个程序执行它的控制流额一部分的每一时间段叫做时间片
  3. 进程调度:内核抢占当前进程,保存其上下文信息,并重新开始一个先前被抢占了的进程,由内核中的调度器完成。
  4. 用户态与内核态的栈换:处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能
  5. Hello的进程执行,在调用sleep函数之前,进程的时间片处于用户模式,调用sleep函数之后,它显示地请求让进程休眠,此时进程陷入内核模式,由内核决定将控制转移给其他进程,当sleep的计时结束之后,内核又重新调度hello程序,将控制转移给它,时间片又回到用户模式。当调用getchar函数之后,getchar函数会调用read,再次陷入内核模式,等待键盘的输入,内核再次调度其他进程,并发地执行其他程序,当键盘输入完成,内核重新调度hello进程。

调用sleep:

调用getchar:

6.6 hello的异常与信号处理
1)异常类别:

2)信号种类:

3)hello程序中的信号及异常:
1.正常终止:
程序在终止后被后回收,输入ps命令可以看到不存在hello进程

  1. 随便乱按:
    程序正常输出,键盘的并未中断hello程序的执行

  2. Crtl+c:
    Hello程序收到终止信号,程序会被终止,hello停止输出,ps查看进程已不存在,说明已经被回收

  3. Crt+z:
    Hello收到中止信号,Hello进程被停止,jobs命令显示其为停止状态,输入bg1或者fg1,拉进程收到SIGCONT信号,hello进程将再度开始运行

6.7本章小结
简述了进程的定义和作用、shell的机制和处理流程,分析了hello程序执行时fork和exceve系统调用的过程,具体分析了hello进程运行时对于异常和信号的处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1)逻辑地址:源程序里使用的地址,访问内存指令给出的地址即为逻辑地址,也叫相对地址。
2)线性地址:线性地址是逻辑地址到物理地址变换之间的一个中间产物。在分段部件中,逻辑地址是段中的偏移地址,然后加上段的基址地址,就是线性地址,类似于结构体中成员变量的引用。
3)物理地址:计算机主存被组织为M个连续的字节大小单元的数组,物理地址就相当于主存数组下标索引。
4)虚拟地址:每一个进程都有一个自己的虚拟地址,可执行程序中代码段访问的地址都是虚拟地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
在段式管理系统中,整个进程的地址空间是二维的,即线性地址由段好+段内偏移地址两部分组成,为了完成进程逻辑地址到物理地址的映射,处理器到内存中查找段表,用段号匹配具体条目从而得到段的首地址,加上段内地址得到实际的物理地址,这个过程由处理器硬件完成,每个进程都有属于自己的段表。

7.3 Hello的线性地址到物理地址的变换-页式管理
1)在页式管理中地址空间是分页处理的,从虚拟地址空间到物理地址的转换是通过将虚拟空间分割成大小相同的块称为页,物理地址也采用同样的分割方式,将虚拟页的页号与物理内存中的页号进行映射,而业内的偏移都是一样的。
2)CPU中的CR3寄存器存储页表的首地址,页表里的每一个条目阐述了虚拟页与物理页的映射关系。VA=VPN(虚拟页号)+VPO(虚拟页偏移),通过页表条目找到对应的物理页号,VPN->PPN,PA=PPN+VPN,由此得到了正确的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换
1)TLB被称为翻译后备器,是PTE也即页表条目的缓存,TLB的相连度很高,根据程序运行的空间局部性可知,从TLB中查找页表条目能够减少很多的查询时间,相比起在主存中查找页表条目更加高效。
2)四级页表,由于页表是存储在物理内存中的,当地址位数变多之后,光页表条目就会占用大量的物理内存,而程序运行的一小段时间内并不需要那么多页表,因此多级页表可以将常用的页表条目存储在主存中,一级页表条目始终存储在主存中,而二级页表只有最常用的存储在主存中,以48位VA为例12位作为业内偏移,剩下的36位分成4份分别作为,该虚拟页在每一级页表中的偏移量。第一、第二、第三级的页表条目存储的都是下一级页表条目的首地址,最后一级页表条目存储的是该虚拟页对应的物理页页号,如下图所示:
3) TLB与四级页表支持下的VA到PA的变换
在四级页表转换的基础上,cpu首先在TLB中存储,TLB得到VPN,分成两部分4位的TLBI和32位的TLBT,TLB是一个16组、每组四个条目的缓冲区,TLBI作为组索引,TLBT做为标志位去匹配每一行。如果匹配则命中,直接计算出物理地址;不匹配则不命中,再一级一级地在内存中找到对应的最后以及页表条目,计算出相应的物理地址。具体实现如下图所示:

7.5 三级Cache支持下的物理内存访问
Cache被组织成一组组多路相连的缓存块,每一行可以存储B个字节的数值,CPU拿物理地址向cache索要数据,物理地址被分成3部分:CT(标记位)、CI(组索引)、CO(块偏移),如果L1中对应行标记位匹配,并且valid有效值为1,那么直接取出数据,否则为不命中,需要向下一层缓存中取值,直到命中为止,如果cacheL3也不命中,那么需要从主存中取出数据,再一级一级地返回,最终将数据交给cpu,具体结构如下图所示:

7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个后来进行写操作时,写时复制机制就会创建新的页面,因此每个进程也就保持了私有地址空间的概念。写时复制是针对进程映射的私有区域的机制,其具体实现方式如下图所示:

7.7 hello进程execve时的内存映射
1)Exceve函数加载并执行a.out文件需要下面几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
2)具体的内存映射如下图所示:

7.8 缺页故障与缺页中断处理
某条指令发出数据请求,当MMU查找到所需虚拟页的页表条目,发现该页未被缓存在主存中,触发一个缺页故障,进程陷入内核模式,缺页中断处理程序将会从主存中选择一个页面进行牺牲,如果其已被修改过需要写回磁盘将其换初,再从磁盘中读取所需虚拟页的数据,将其缓存到主存中,内核更新对应的页表条目,完成一次按需的页面调度,最后将控制返回给进程的用户模式,再次执行引发缺页异常的指令,指令控制流如下图所示:

7.9动态存储分配管理
1)堆:动态存储的分配管理是由动态内存分配器完成的,动态内存分配器维护着一个进程的虚拟内存区域(称为堆),堆是请求二进制零的区域,它季节再未初始化的数据区域后面,并向上增长。对于每个进程,内核维护一个brk变量,它指向堆顶。
2)堆结构:分配器把堆看成一组大小不同的块的集合来维护。其中每一个块就是一个连续虚拟内存片,它的状态有两种,已分配或者空闲。已分配块显式地保留为应用程序所使用,空闲块可被分配,保持空闲直到显式地被应用程序分配。已分配块保持分配状态,直到被释放。
3)分配器的实现
1.分配器的分类
显示分配器:要求应用显式地释放任何已分配块
隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,自动地释 放已分配块
2.隐式空闲链表:
分配器需要一些数据结构来区分块的边界,带边界标签的隐式空闲链表将这 些信息嵌入块本身,在块的前四个字节的头部和最后四个字节的尾部中嵌入表 示块状态的信息,最低位用来表示块的分配状态,1表示已分配,0表示未分 配;整个四字节的值用来表示块的大小;在空闲块进行合并时,通过前一个块的 脚步和后一个块的尾部就能知道新释放的空闲块该如何合并。如下图所示:

3.显示的空闲链表
将堆组织成一个双向空闲链表,在每一个空闲块中,都包含一个pred(前驱) 和succ(后继)指针,释放一个块可以采用后进先出(LIFO)的顺序或者按 照地址顺序来维护链表,前者释放块可以在7.10本章小结
(以下格式自行编排,编辑时删除)
常数时间内完成,后者需要线性的 时间,但具有更高的内存利用率。具体结构如下图所示:

7.10本章小结
讨论了各种地址空间的管理方式,分析了TLB与四级页表支持下VA与PA的转换,简述了cache支持下的物理内存访问机制、hello进程中fork和exceve时新进程的映射方式、hello进程的缺页异常与处理以及动态存储分配及管理。
(以下格式自行编排,编辑时删除)
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
1)Linux下所有的IO设备被模型化为文件,因此设备的输入与输出被简单地看成文件的读和写,内核给出一个简单低级的应用接口,也就称之为Unix I/O。
设备的模型化:文件
2)文件类型:1.普通文件 2.目录(包含一组链接的文件)3.套接字(网络通信 文件)4.命名通道5.符号链接 6.字符和块设备
3)设备管理:unix io接口

8.2 简述Unix IO接口及其函数
Unix IO接口通用操作
1)打开文件:应用程序请求内核打开一个文件,表示其对I/O设备的访问,内核返回一个非负整数作为描述符。
2)Linux为每个进程打开的标准文件:标准输入、标准输出、标准错误。
3)改变当前的文件位置:应用程序打开的文件,内核维护着该文件中的位置K,初始的值为0,表示从文件开始处的字节偏移量块,调用seek可以改变位置K。
4)读写文件:读操作从文件中复制需要的字节(size大小)到内存中,从K处到K+size处,当K大于文件大小时会触发EOF。写操作与读操作基本一致,但是会隐式地更新K的值。
5)关闭文件:内核不再为这个文件维护描述其状态的数据结构,将其从内存中释放,并把描述符返回到可用的描述符池中。

接口函数:
1)int open(char* filename, int flags,mode_t mode):open函数将filename(要打开的文件名称)转化成一个文件描述符并返回,描述符的选取策略永远是进程的可用池里数值的最小值,flags表明访问方式,mode表明访问权限。
2)ssize_t read(int fd ,void *buf,size_t n):read函数从描述读fd对应文件的当前位置处读取n个字节赋值给buf所指向的内存。返回值为-1表示错误,为0表示EOF 已读到文件末尾,正常读取为实际读取字节数量。
3)int write(int fd,void *buf,size_t n):wirte函数与read基本一致,但操作为向fd对应文件写入buf指向内存开始最多n个字节的值,并且隐式地更新K值(当前的文件位置)。
4)int cose(fd):关闭fd对应的文件,内核不再维护描述相应文件状态的数据结构。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回.
8.5本章小结
简述了系统级的IO,完成了hello最后的输出,分析了getchar和printf函数的具体实现。
(第8章1分)

结论
1)hello的历程

  1. 编写c语言,生成hello.c。
  2. 经过预处理,替换掉宏定义等预处理指令,生成.i文件。
  3. 编译:处理全局变量的初始化以及赋值语句、控制语句、函数调用的具体实现,生成.s文件。
  4. 汇编:将.s文件中的各个段打包处理,为全局符号的引用提出解决方案,为接下来的链接做准备,生成.o可重定位文件。
  5. 链接:将hello.o与库链接,实现hello.o中所有符号的解析,并将库中成员模块和hello.o的各个节重新定位,整合到一个可执行文件中,生成hello。
  6. 加载和执行:shell调用fork为hello创建一个新的进程,新进程调用exceve函数加载hello程序,并未其创建新的虚拟内存映射。
  7. 字符串输出调用系统级IO,通过对文件的读写完成IO设备的输入与输出。
    2)感悟:
    一个看似十分简单的hello程序无论是可执行文件的生成还是最后的结果输出都不是一件简单的事情,需要系统各个层级的配合。虚拟地址空间的概念对于hello的正常运行十分重要,它让一切变得简单,每个进程有通用的映射模式,彼此之间互不干扰。为了保护系统内部的操作不被滥用,产生了用户模式和内核模式两种位模式,不仅保护了系统的正常运行,同时为应用程序和系统之间提供了一种媒介。

(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名 作用
hello.i Hello.c预处理的结果
hello.s Hello.i编译后的产物
hello.o Hello.s汇编后的产物
hello-obj.s Hello程序反汇编文件
hello_readelf Hello程序整体结构文件
hello Hello可执行文件

(附件https://blog.csdn.net/roger_ranger/article/details/784738860分,缺失 -1分)

参考文献
1.汇编语言算术运算指令
https://kaoshi.china.com/ncre/learning/761240-1.htm
2. 移位和循环指令
https://blog.csdn.net/weixin_44214735/article/details/99614632
3.ELF文件类型 ELF程序头 ELF节头 ELF符号
https://blog.csdn.net/youyou519/article/details/82659007
4.Linux内核:IO设备的抽象管理方式
https://blog.csdn.net/roger_ranger/article/details/78473886
5.深入理解计算机系统第三版

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值