csapp hit hello的一生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术

计算机科学与技术学院
2019年12月
摘 要
文章从一个小小的hello实例出发,描写了它经历了预处理,编译,汇编,链接成一个可执行目标程序,再在shell中运行它,经历fork创建子进程,execve加载到内存,mmap映射虚拟内存开始运行,在进程管理,内存管理I/O管理的共同调控下,完成执行,最终终止回收的过程。但是麻雀虽小,五脏俱全,滴水见大海,我们仍然能从简单的几行代码中了解到计算机系统的整体与处理运行的流程,有所感悟。

关键词:计算机系统;helloworld;Ubuntu系统;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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(From Program to Process):
Hello.c(program)文件通过预处理,编译,汇编,链接,生成可执行文件hello,随后在Bash里,OS为该文件fork出子进程(process),并将文件execve入内存,再进行mmap映射,在分配的时间片得以执行,在Hardware上运行。完成P2P的过程。

020(From Zero-0 to Zero-0):
开始的0代表在内存中不存在,在相继fork,execve,mmap映射后,hello文件在内存中以进程的形式存在,执行。在I/O管理,信号处理程序等的作用下,在stdout上打印出字符串,返回。进程终止,被shell回收,不在占用内存空间,也是020的过程。

1.2 环境与工具
硬件环境:Inter Core i7;2.8GHz;16G RAM;1T HD Disk
软件环境:macOS Catalina;Parallels Desktop14;Ubuntu 18.04
开发工具: GDB,OBJDUMP

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

文件名字 文件作用
hello.c 源代码
hello.i 预处理后的文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标程序
hello 链接后的可执行目标程序

1.4 本章小结
本章主要纵观了hello程序运行从开始到结束的大致流程,和相关的主要参与者,对程序的运行有了初步的了解和认知。
此外,列出来相应的环境和工具。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理是对于c语言程序进行一个初步整理的过程,处理掉了所有的宏定义,删除“#define”并展开所定义的宏:
1.处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等
2.插入头文件到“#include”处,可以递归方式进行处理
3.删除所有的注释“//”和“/* */”
4.添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
5.保留所有#pragma编译指令(编译器需要用)

预处理作用:
预处理可以在在将c程序转化为s的汇编程序之前对于宏定义处理,方便后续的代码转化,并且对于在汇编中无用的注释进行处理,删去无用部分对后续操作做准备。
2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

预处理后,文件变成了三千多行的文本文件,前面为头文件<stdio.h> <unistd.h> <stdlib.h> 的内容被复制插入进代码中,消除include宏,随后是原本的代码。可以看到,预处理后的文件更加完整和清晰,也没有了多余的注释等无用的部分。

2.4 本章小结
通过动手实践预处理,并分析其i文件的内容。了解了预处理的大致过程,也分析明确了预处理的重要性。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:
编译过程就是将预处理后得到的预处理文件(如hello.i)进行 词法分析、语法分析、语义分析、优化后,生成汇编代码文件

编译的作用:
编译后生成的.s汇编文本文件更接近于程序能够理解的二进制.o文件,是对于程序向.o处理过程中的进一步简化过程。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析
3.3.1数据
1.int sleepsecs = 2.5 发生类型转换,截断后为2,存储在data段。

2.int i 局部变量,存储在堆栈中。
 
3.字符串常量,存储在rodata段。

3.3.2赋值
如3.3.1中图示,全局变量sleepsecs赋初值在data段中直接体现,而局部变量的初值,由mov指令将数据转移到相应的堆栈中的位置。

3.3.3类型转换

向整形变量赋浮点数初值,发生类型转换,小数点后数字被截断,直接赋值为2。

3.3.4 sizeof
Sizeof相当于一条宏命令,是一种特殊的编译预处理。

3.3.5算术操作

对于i的自增,使用了add指令来完成对堆栈中的局部变量加一的操作。

3.3.6逻辑/位操作
常见的逻辑/位操作有:逻辑&& || ! 位 & | ~ ^ 移位>> << 复合操作如 “|=” 或“<<=”等

3.3.7关系操作
对于代码中的两处关系

汇编中翻译如下:
   
都采用cmp + jXX组合来实现。

3.3.8数组/指针/结构操作

对于argv字符串数组,先在rax中取出首地址,再加上偏移量8和16,获得两个参数,数组元素。

3.3.9控制转移

借用关系的例子,如果argc不等于3,就执行if中的语句,否则跳转;如果i<=9,就跳入循环体继续执行操作。

3.3.10函数操作
函数调用前先传递参数,前6个参数使用寄存器rdi,rsi,rdx,rcx,r8,r9。后面的参数逆序入栈传递。

  1. main(int argc,char *argv)
    被系统启动函数__libc_start_main调用。

  2. printf(“xxx”,xxx)
    直接打印字符串的优化为puts()

传递参数使用上述的特定寄存器

  1. exit(1)

  2. sleep(sleepsecs)

  3. getchar()
    无参数直接call。

3.4 本章小结
通过对编译后的汇编代码的解析,了解了程序的大致架构,以及相应语句的汇编实现,基本数据的传递,对程序以及编译的过程有了更加深入的理解。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。
作用:
将汇编代码转变为机器指令,生成目标文件。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式
1.ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,节头部表中条目的大小和数量等。

2.节头表
记录了每个节的名称、类型、属性(读写权限)、在ELF文件中所占的长度、对齐方式和偏移量

3.重定位节
重定位条目告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。如图,偏移量是需要被修改的引用的节偏移,符号标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用,加数是一个有符号常数,一些类型的重定位要用它对被修改引用的值做偏移调整。

每个重定位条目类似该结构体

offset为偏移量,addend为常量加数,type对应的是重定位类型(相对或绝对地址引用),最后的symbol代表对应符号表的符号。

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

4.4 Hello.o的结果解析

汇编语言:基本一致。
立即数:汇编语言使用十进制,机器语言使用十六进制。
分支转移:汇编语言用的是标记名,机器语言使用偏移量跳转。
函数调用:汇编语言使用函数名,机器语言再没有链接前先用0占位,根据重定位条目填写。
全局变量:汇编语言使用变量名,机器语言在链接前也是用0占位,链接时根据重定位条目计算地址。

4.5 本章小结
汇编是将汇编语言翻译成二进制机器语言的过程,对汇编过程的解析能让人对机器级的操作更加熟悉,明确更加底层的机器级语言对于各个情况的处理方式,二进制文件的形成与执行。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件,将可重定位文件整合成可执行文件的过程。
作用:
将函数库中相应的代码组合到目标文件中。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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

5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

  1. ELF头

  2. 节头表

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

.rodata 保存只读数据。类型SHT_PROGBITS。

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

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

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

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

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

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

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

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

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

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

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

  1. 程序头

可以看出,程序包含八个段。
1.PHDR: 指定程序头表在文件及程序内存映像中的位置和大小。
2.INTERP: 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小。对于动态可执行文件,必须设置此类型。
3.LOAD: 指定可装入段,通过p_filesz和p_memsz进行描述。文件中的字节会映射到内存段的起始位置。
4.DYNAMIC: 指定动态链接信息。
5.NOTE: 指定辅助信息的位置和大小。
6.GNU_STACK: 权限标志,标志栈是否是可执行的。
7.GNU_RELRO: 指定在重定位结束之后那些内存区域是需要设置只读。
5.4 hello的虚拟地址空间

1.PHDR段 地址0x400040 大小0x1c0

	 

2.INTERP 地址0x400200 大小0x1c

3.LOAD 地址 0x400000大小0x82c

4.LOAD 地址 0x600e00 大小0x258
	 

分析可知,程序头中各个段的位置对应着程序加载入内存都个个段的位置,其中装载段LOAD1和LAOD2分别对应代码段和数据段。

5.5 链接的重定位过程分析
区别

  1. 不再只有函数main,其他调用的库函数也被链接进来。

2.原来占位的0,全部被解析成了确定的地址,被定位在代码中不同的位置。

3.地址不再从0开始,按区域有着不同的虚拟地址。
重定位过程:
根据重定位条目,按照公式:

  1. 相对寻址 (unsigned)(ADDR(r.symbol)+r.addend - refaddr)
  2. 绝对寻址 (unsigned)(ADDR(r.symbol)+r.addend)
    进行计算,其中 refaddr = ADDR(s) + r.offset
    5.6 hello的执行流程
    使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
    程序名称 程序地址
    ld-2.27.so!_dl_start 0x7f5d6118fea0
    ld-2.27.so!_dl_init 0x7f5d6119e630
    hello!_start 0x400500
    hello!puts@plt 0x4004b0
    hello!exit@plt 0x4004e0

5.7 Hello的动态链接分析
程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定的技术解决这个问题,将过程地址的延迟绑定推迟到第一次调用该过程时。
延迟绑定要用到全局偏移量表(GOT)和过程链接表(PLT)两个数据结构。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。

过程分析:

dl_start前

dl_start后

GOT[2]出现共享模块入口

第一次调用,跳转到 hello!printf@plt+6

第二次之后,跳转到libc – 2.27.so!printf,绑定成功。
5.8 本章小结
本章着重了解了关于链接的相关知识,从链接前后的elf的分析,符号解析和重定位的深入。第一次从内存中观察到了链接的过程,了解了动态链接的延迟绑定机制。强化了对edb的使用,理解了链接中重要的重定位过程的大致流程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是一个执行中程序的实例。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:
进程的概念为我们提供这样一种假象,就好像我们的程序是系统中当前运行的唯一程序一样,我们的程序好像是独占地使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳,是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序。
1.功能:
命令解释。Linux系统中的所有可执行文件都可以作为Shell命令来执行。
2.处理流程:
1)当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行。
2)若是外部命令或应用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

6.3 Hello的fork进程创建过程
在终端中输入./hello 1180300929 王一轩,shell判断它不是内置命令,于是会加载并运行当前目录下的可执行文件hello。
此时shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程有不同的pid。fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0.父进程与子进程是并发运行的独立进程。
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。

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

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

4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。

6.5 Hello的进程执行
系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能。设置了模式位时,进程就运行在内核模式中,该进程可以执行指令集中的任何指令,可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式中的进程不允许执行特权指令。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定叫做调度。
程序在执行sleep函数时,sleep系统调用显式地请求让调用进程休眠,调度器抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。sleep的倒计时结束后,控制会回到hello进程中。程序调用getchar()时,内核可以执行上下文切换,将控制转移到其他进程。getchar()的数据传输结束之后,引发一个中断信号,控制回到hello进程中。
6.6 hello的异常与信号处理
异常分为中断,陷阱,故障,终止。
常用的信号:SIGCHLD,SIGINT,SIGSTOP……

  1. 正常退出

每次打印等待2秒,最后运行getchar

  1. 不停乱按

乱按会将输入的内容存到缓冲区,作为接下来的命令行输入。

  1. Ctrl+c

输入ctrl+c,shell向前台进程发送SIGINT,子进程终止后被回收。

  1. Ctrl+z

输入ctrl+z,shell向前台进程发送SIGSTOP,停止运行,fg后继续运行。

先后输出10个。

6.7本章小结
本章主要研究可执行文件是如何从程序变成进程的,通过探究shell的fork和execve,加载运行后的进程管理和信号处理,对进程有了更加深入的理解,也对shell的进程管理更加熟悉。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。是hello.o中的相对偏移地址。
线性地址:
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:
程序访问存储器所使用的逻辑地址称为虚拟地址。是hello里的虚拟内存地址。
物理地址:
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。是hello里虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1.基本原理:
在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在段式存储管理系统中,为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。
在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。
总体分析:
段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。
程序通过分段划分为多个模块,如代码段、数据段、共享段:
–可以分别编写和编译
–可以针对不同类型的段采取不同的保护
–可以按段为单位来进行共享,包括通过动态链接进行代码共享
这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。
总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。

2.段式管理的数据结构:
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。

  • 进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。在系统中为每个进程建立一张段映射表,如图:

  • 系统段表:系统所有占用段(已经分配的段)。

  • 空闲段表:内存中所有空闲段,可以结合到系统段表中。
    3.段式管理的地址变换
    在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。
    这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。

7.3 Hello的线性地址到物理地址的变换-页式管理
1.基本原理
将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是虚拟页号(VPN),后一部分为虚拟页偏移量(VPO):

页式管理方式的优点是:
1)没有外碎片
2)一个程序不必连续存放。
3)便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。
缺点是:要求程序全部装入内存,没有足够的内存,程序就不能执行。

2.页式管理的数据结构
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。为了完成上述的功能,—个页式系统中,一般要采用页表这一数据结构。
页表:

页表将虚拟内存映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。

3.页式管理地址变换
MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址被划分成4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1<=i<=4.第j级页表中的每个PTE,1<=j<=3,都指向第j+1级的某个页表的基址。第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE。将得到的PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。

7.5 三级Cache支持下的物理内存访问
L1 d-cache的结构如图:通过6-11位的组索引找到对应的组,将组中每一行的tag与CT比较,若标记位匹配且有效位为1,说明命中,根据0-5位的块偏移取出数据,如果没有匹配成功,则向下一级缓存中查找数据。取回数据后,如果有空闲块则放置在空闲块中,否则根据替换策略选择牺牲块。

7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的pid。为了给这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。

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

7.8 缺页故障与缺页中断处理
1.缺页故障
进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。

2.缺页中断处理
缺页异常处理程序选择一个牺牲页,将该位置换入所需的页面,再将导致缺页的指令重新启动:页命中。

左:缺页中断处理前 右:缺页中断处理后

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

  1. 隐式空闲链表:
    空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。

(1)放置策略:首次适配、下一次适配、最佳适配。
首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。

(2)合并策略:立即合并、推迟合并。
立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。

在每个块的结尾添加一个脚部,分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,从而使得对前面块的合并能够在常数时间之内进行。

  1. 显式空闲链表:
    每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。
    空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。

  2. 分离的空闲链表
    维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。

7.10本章小结
本章讨论了关于进程的存储问题,包括段式or页式存储,页表的存储管理,虚拟地址物理地址的转换,进程的加载时的内存映射,缺页故障和处理,动态内存分配。对程序运行时OS对内存的相关管理,机制对进程运行的实现有了一定的理解。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
I/O接口操作
1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
IO函数:
1.int open(char *filename, int flags, mode_t mode)
进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,而且返回描述符数字。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。
2.int close(int fd)
进程通过调用close函数关闭一个打开的文件。
3.ssize_t read(int fd, void *buf, size_t n)
应用程序通过调用read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示的是实际传送的字节数量。
4.ssize_t write(int fd, const void *buf, size_t n)
应用程序通过调用write函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
1.printf函数

其中,…代表可变形参。
typedef char* val_list,arg表示第一个参数

2.vsprintf函数

作用:格式化输出字符串fmt,产生格式化输出,返回打印的个数。

3.write函数

先传参数,再调用sys_call,syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
  1. 字符显示驱动子程序:
    从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息),显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
getchar函数
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(–n >= 0)?(unsigned char) *bb++ : EOF;
}

getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。返回时返回buf的第一个元素,除非n<0。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
对Linux I/O的解读,printf函数的详解和getchar函数的分析,明白了I/O文件之间的巧妙抽象表示。了解了进程的I/O调用的过程。
(第8章1分)

结论
用计算机系统的语言,逐条总结hello所经历的过程。

  1. 通过编辑器编辑出hello.c文件。
  2. 预处理器cpp将c文件的宏展开为i文件。
  3. 编辑器将i文件编译成汇编语言的s文件。
  4. 汇编器将汇编语言翻译成二进制机器语言,生成二进制可重定位目标程序,o文件。
  5. 链接器将所引用的目标文件符号解析,重定位后完全链接成可执行的目标文件hello。
  6. 在shell中输入命令行指令,运行hello程序。
  7. 不是内置指令,shell fork一个子进程。
  8. 对子进程用命令行分析的参数execve加载,mmap映射虚拟内存。
  9. 改变PC到_start,最后开始执行main函数
  10. hello进程逐条执行调用系统函数。
  11. 配合I/O接口到字符显示驱动子程序,将内容打印在屏幕。
  12. return或exit后子进程终止被回收

对计算机系统的设计与实现的深切感悟:
短短几行代码,却反映了现代计算机在执行的整体流程。硬件软件各个部分紧密结合,巧妙而又高效。想要做到真正的深入理解计算机系统,还长路漫漫,需要不断的学习与总结。想要在整个计算机体系上有所创新,还应该分步骤去学习其各个部分的精巧设计,精进术业。希望在以后的课程中能更加深刻 的探究其中的奥秘,做到深入理解计算机系统。
(结论0分,缺少 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源码
hello.i 预处理后的文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标程序格式
hello 链接之后生成的可执行目标文件
elf.txt hello.o readelf 的重定向
obj.txt hello.o objdump 的重定向
elf2.txt hello可执行目标文件 readelf 的重定向
obj2.txt hello可执行目标文件 objdump 的重定向
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
【1】深入理解计算机系统(第三版)
【2】内核必须懂(七): Linux四级页表(x64)
https://cloud.tencent.com/developer/article/1421792
【3】进程管理—进程描述符(task_struct)
https://www.cnblogs.com/feng9exe/p/6883614.html
【4】调研task_struct结构体
https://www.cnblogs.com/cuckoo-/p/10966158.html
【5】printf 函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
【6】LINUX 逻辑地址、线性地址、物理地址和虚拟地址
https://www.cnblogs.com/zengkefu/p/5452792.html
【7】ELF(七)可重定位目标,部分摘自深入理解操作系统,深入理解linux内核
https://blog.csdn.net/ylcangel/article/details/18188921

(参考文献0分,确实 -1分)

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值