计统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 120L0*****
班   级 200****
学 生 ***    
指 导 教 师 吴锐  
计算机科学与技术学院
2021年5月

摘 要
本论文通过hello.c程序,加上CSAPP课程所学到的知识,在Ubuntu虚拟机Linux系统下进行所有操作,运用Linux系统的工具,分析hello程序的一生。
关键词:CSAPP;Linux;hello。

目 录

第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。
P2P:From Program to Process。编辑完成的hello.c程序先经过cpp预处理器的预处理得hello.i文件,ccl编译器将其编译获得hello.s文件,as汇编器再将其翻译为机器语言指令获得hello.o文件,再经过ld链接器进行链接得可执行文件hello。shell输入执行命令后,进程管理为其fork()一个子进程。即完成了P2P的过程。
020:From Zero to Zero。进程管理给hello进行execve操作,进行mmap操作将其映射到内存中,接着给运行的hello分配时间片来执行逻辑控制流。当程序运行结束后,父进程会回收hello进程,内核删除相关的数据。即完成了020的过程。

1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

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

1.3 中间结果
文件名 文件的作用
hello 链接之后的可执行文件
hello.c 源代码
hello.i 预处理之后的文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的重定位文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的TXT格式
hello1.elf hello的ELF格式
hello2.txt hello的TXT格式

1.4 本章小结
本章简要介绍了hello的P2P,020过程,并列出了大作业的软硬件环境及开发工具,还列出了操作过程中产生的中间结果。

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:预处理主要包括三个方面:1.宏定义;2.文件包含;3.条件编译。
1、预处理宏定义就是将被#define定义的变量替换成定义的值,便于处理C程序中被宏定义的变量;
2、预处理文件包含就是将#include调用的文件取出,然后与C程序合并成同一个文件,这样便于处理C程序中调用其他库的函数;
3、预处理条件编译就是将#ifdef、#else、#endif等条件编译指令形成的代码片段根据条件是否成立缩减成最小片段,从而便于后续C程序条件语句的编译。

2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图2-2预处理
2.3 Hello的预处理结果解析
图2-3预处理结果
分析:可以发现hello.c和hello.i中最大的区别便是一开始的#include那些代码都换成了一大堆实际的函数,而main函数的代码并没有发生改变,这说明预处理只是将那些#include所引用的头文件中的内容(函数、定义变量、定义结构体)都取了出来,并将其与原来的C代码放在一起才形成了现在的hello.i。

2.4 本章小结

本章介绍了预处理的基本概念和作用,以及在Linux中对C文件进行预处理的命令行,同时还分析了从hello.c到hello.i(预处理)到底发生了什么变化。

第3章 编译
3.1 编译的概念与作用

概念:编译就是从源代码(通常为高级语言)到能直接被计算机或虚拟机执行的目标代码(通常为低级语言或机器语言)的翻译过程。
作用:编译的主要作用有词性分析、语法分析、语义分析、源代码优化、目标代码优化
1、词性分析就是将源代码的字符序列分割成一系列单词符号,如array[i]=(i+4)*(2+6);
2、语法分析就是根据词性分析得到的单词符号,生成对应的语法树;
3、语义分析就是判断语义是否合法,但是并不分析对错;
4、源代码优化就是将源代码优化成与环境无关的中间代码;
5、目标代码优化就是将中间代码优化成适合目标环境的代码。

3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3-2编译
3.3 Hello的编译结果解析

3.3.1节名称及其作用
节名称 作用
.file 声明源文件
.text 代码节
.rodata 只读数据段
.string 声明一个字符串
.type 声明一个符号是函数类型还是数据类型
.align 声明对指令或者数据的存放地址进行对齐的方式
.global 声明全局变量
图3-3-1编译结果中的节
3.3.2局部变量
在hello.c文件中的main函数中定义了局部变量i,如图所示:
图3-3-2-1hello.c中i的定义
而在hello.s文件中局部变量i存放在了-4(%rbp)的地址中,如图所示:
图3-3-2-2hello.s中i的定义

3.3.3字符串
在hello.c文件中的mian函数中打印了两个字符串,如图所示:
图3-3-3-1hello.c中字符串的打印

而在hello.s文件中这两个字符串都在只读数据段中,如图所示:
图3-3-3-2hello.s中字符串的声明

并且用以下指令从main函数中第二个参数argv数组中调用对应的字符串给printf函数:
在这里插入图片描述图3-3-3-3hello.s中字符串的调用

3.3.4参数argc和argv数组
在hello.c文件中的main函数传入了两个参数,其中一个是整数,另一个是数组,如图所示:
图3-3-4-1hello.c中参数的传递

而在hello.s文件中作为参数,argc被存放到了-20(%rbp)的位置,argv数组被存放到了-32(%rbp)的位置,如图所示:
图3-3-4-2hello.s中参数的传递

其中argv数组中的元素是指向字符类型的指针,在hello.s中被两次调用,传参给printf函数。
3.3.5函数操作
在hello.c文件中的函数主要有:main函数,printf函数,exit函数,sleep函数和getchar函数。main函数的参数是argc和*argv,printf函数的参数是字符串,exit函数的参数是1,sleep函数的参数是atoi(argv[3])。如图所示:
图3-3-5-1main函数代码

其中main函数为全局函数,在hello.s文件中如图所示:
图3-3-5-2hello.s中main函数的声明

printf函数是将字符串的首地址存入寄存器作为参数传递,并通过call来进行调用,
在hello.s文件中如图所示:
图3-3-5-3hello.s中printf函数的调用

exit函数是将立即数1存入寄存器作为参数传递,并通过call来进行调用,在hello.s文件中如图所示:

图3-3-5-4hello.s中exit函数的调用

sleep函数是将固定位置存储的值存入寄存器作为参数传递,并通过call来进行调用,在hello.s文件中如图所示:

图3-3-5-5hello.s中sleep函数的调用

3.3.6赋值操作
在hello.c文件中的main函数中的赋值操作只有i=i,在hello.s文件是由mov指令来实现的,如图所示:
图3-3-6-1hello.s中的mov指令

其中movl对应的是四个字节,其他还有movb对应一个字节,movw对应两个字节,movq对应八个字节。
3.3.7算术操作
在hello.c文件中的算数操作只有i++,在hello.s文件中由addl指令来实现,如图所示:
图3-3-7hello.s中的addl指令

3.3.8关系操作与控制转移
在hello.c文件中有两处条件判断,如图所示:

在这里插入图片描述图3-3-8-1hello.c中的条件判断

在hello.s文件中两个判断都是先进行比较,然后设置条件码,根据条件码判断是否需要跳转,如图所示:
在这里插入图片描述
在这里插入图片描述

图3-3-8-2hello.s中的条件转移

3.3.9类型转换
在hello.c中使用atoi(argv[3])将字符串转为了整型变量。其他的类型转换还有int、float、double、short、char之间的转换,在hello.s中如图所示:
图3-3-9hello.c中的类型转换

3.4 本章小结

本章主要介绍了编译器处理C语言程序的基本过程,并对生成的汇编程序中涉及到的C语言各种数据类型和各类操作做了说明。

第4章 汇编
4.1 汇编的概念与作用

概念:汇编是指用汇编器将汇编语言翻译成机器语言的过程。
作用:
1、将汇编指令翻译成可重定位的二进制目标文件;
2、生成符号表;
3、生成各个段的section;
4.2 在Ubuntu下汇编的命令
命令:gcc -C hello.s -o hello.o
图4-2汇编命令

4.3 可重定位目标elf格式
生成hello.o文件的elf格式命令:readelf -a hello.o > hello.elf

图4-3生成elf格式命令

4.3.1 ELF Header
命令:readelf -h hello.o
内容:ELF Header以一个16字节的Magic序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
图4-3-1表头信息

4.3.2 Section Headers
命令:readelf -S hello.o
内容:目标文件中的每个节都有一个固定的条目体现在节头中,指明了各个节的信息,包括名称、类型、起始地址和偏移量。
图4-3-2节头信息

4.3.3重定位节
命令:readelf -r hello.o
内容:重定位节保存的是.text节中需要被修正的信息(任何调用外部函数或者引用全局变量的指令都需要被修正),调用外部函数的指令和引用全局变量的指令需要重定位,调用局部函数的指令不需要重定位。
图4-3-3可重定位节信息

4.3.4 符号表
命令:readelf -s hello.o
内容:存放定义和引用的函数和全局变量的信息,name为名称,对应可重定位目标模块;value为起始位置偏移;size为目标大小;Bind表示是本地的还是全局的;type表示类型,要么是函数要么是数据。
图4-3-4符号表信息

4.4 Hello.o的结果解析
命令:objdump -d -r hello.o>hello1.txt
图4-4hello.o的结果

结果解析:与hello.s的差异
4.4.1分支转移
hello.s:
图4-4-1-1hello.s中的分支转移

hello1.txt:
图4-4-1-2hello.o中的分支转移

反汇编指令用确定的地址,hello.s用段符号如L1。
4.4.2 函数调用
hello.s:

图4-4-2-1hello.s中的函数调用

hello1.txt:
图4-4-2-2hello.o中的函数调用

hello.s文件中函数调用直接使用函数名称,反汇编文件中call后是偏移量。

4.5 本章小结

本章对应的主要是hello.s汇编到hello.o的过程。在本章中,我们查看了hello.o的可重定位目标文件的格式,使用反汇编查看hello.o经过反汇编过程生成的代码并且把它与hello.s进行比较,分析和阐述了从汇编语言进一步翻译成为机器语言的汇编过程。

第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-2链接命令

5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
5.3.1 ELF Header
变化:hello是一个可执行目标文件,并且有27个节。
图5-3-1表头信息

5.3.2 Section Headers
变化:对Hello中所有节的信息进行了声明,包括大小size和偏移量offset,根据里面的信息可以定位各个节所占的区间,地址为被加载到虚拟地址的初始地址。

在这里插入图片描述
图5-3-2节头信息

5.3.3重定位节
图5-3-3重定位节信息

5.3.4符号表
在这里插入图片描述
在这里插入图片描述
图5-3-4符号表信息

5.4 hello的虚拟地址空间
hello虚拟地址起始于0x400000,结束于0x400ff0,如图所示:
在这里插入图片描述

图5-4-1hello虚拟地址

根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145。
图5-4-2.txt节虚拟地址

5.5 链接的重定位过程分析

命令:objdump -d -r hello > hello2.txt
分析:经过对hello和hello.o的比较分析,可以看出hello2.txt中多了许多节(.init,.plt),hello1.txt中只有一个.text节,同时还能发现hello反汇编的代码有明确的虚拟地址而hello.o中虚拟地址为默认的0x000000,如图所示:
在这里插入图片描述
在这里插入图片描述
图5-5重定位结果

过程:重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
5.6 hello的执行流程
流程:
1、ld-2.27.so!_dl_start—
2、ld-2.27.so!_dl_init—
3、hello!_start—
4、hello!_init—
5、hello!main—
6、hello!puts@plt–
7、hello!exit@plt–
8、hello!printf@plt–
9、hello!sleep@plt–
10、hello!getchar@plt–
11、sleep@plt—

5.7 Hello的动态链接分析
动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。延迟绑定通过两个数据结构之间简洁但又有些复杂的交互来实现,即过程链接表(PLT)和全局偏移量表(GOT)。
过程链接表(PLT):PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。每个条目都负责调用一个具体的函数。
全局偏移量表(GOT):GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在ld-2.27.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
调用dl_init前的.got.plt:
在这里插入图片描述

‘调用dl_init后的.got.plt:
在这里插入图片描述

5.8 本章小结

本章介绍了链接的概念和作用,阐述hello.o如何链接成为可执行文件,介绍了ELF格式和各个节的含义。

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程为用户提供了这样的假象,我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断地执行我们程序中地指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell俗称壳,是指"为使用者提供操作界面"的软件。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令。它作为用户操作系统与调用其他软件的工具。
处理流程:
1、从终端读入输入的命令。
2、将输入字符串切分,分析输入内容,解析命令和参数。
3、如果命令为内置命令则立即执行,如果不是内置命令则用fork()创建新的进程调用相应的程序执行。
4、在子进程中,用步骤2获取的参数,调用execve()执行指定程序。
5、在程序执行期间始终接受键盘输入信号,并对输入信号做相应处理。

6.3 Hello的fork进程创建过程
在终端中输入如图所示的命令之后,shell会处理该命令,判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是虚拟地址独立、PID也不相同的一份副本。
图6-3fork进程创建

6.4 Hello的execve过程
子进程创建后,shell调用execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。之后当出现错误时,例如找不到hello,execve才会返回到调用程序。
详细过程:
1、删除当前虚拟地址中已存在的用户区域。
2、为新程序建立新的区域结构,这些区域结构是私有的,虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区,bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3、如果hello与共享对象链接,那么这些对象都被动态链接到这个程序,然后映射到用户虚拟地址空间中的共享区域。
4、设置程序计数器,使之指向代码区域的入口点,下次调用这个进程时,从这个入口点开始执行。
6.5 Hello的进程执行
6.5.1上下文信息:
概念:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象构成。
6.5.2时间片:

概念:一个进程执行它的控制流的一部分的每一个时间段。
6.5.3调度:

概念:在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。
6.5.4用户态:
概念:进程运行在用户模式中时,不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个I/O操作,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
6.5.5核心态:
概念:进程运行在内核模式中时,可以执行指令集中的任何指令,并且可以访问内存中的任意位置。
6.5.6用户态与核心态转换:
概念:程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再改回用户态。
6.5.7上下文切换:
概念:hello执行时存在逻辑控制流,多个进程的逻辑控制流在时间上可以交错,表现为交替运行。进程控制权的交换需要上下文切换。操作系统内核使用一种成为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。
6.6 hello的异常与信号处理
异常种类:中断、陷阱、故障、终止。
图6-6异常种类及原因

信号种类:SIGSTP、SIGCONT、SIGKILL、SIFGINT等。
6.6.1正常执行
图6-6-1正常执行结果

6.6.2按下CTRL-Z
图6-6-2-1按下CTRL-Z结果

按下ctrl+z将进程挂起,hello进程没有回收,而是运行在后台,使用ps命令可以看到。调用fg 1将其调到前台,首先打印命令行命令,然后把剩余info输出。
在这里插入图片描述
图6-6-2-1输出剩余info

6.6.3按下CTRL-C
图6-6-3-1按下CTRL-C结果

按下ctrl+c会导致内核发送SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组没有hello进程。
图6-6-3-2查看进程组结果

6.6.4不断乱按
图6-6-4不断乱按结果

程序运行时乱按键盘,会发现屏幕的输入缓存到stdin,并随着printf指令被输出到结果。

6.7本章小结
本章介绍了进程的概念和作用,结合fork和execve函数说明了hello进程的执行过程,之后分析了进程执行过程中异常和信号的处理问题。至此,可执行目标文件成功被加载至内存并执行。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是由程序产生的与段相关的偏移地址部分。
线性地址:逻辑地址经过段机制后转化为线性地址,为(描述符:偏移量)的组合形式。分页机制中线性地址作为输入。
虚拟地址:即逻辑地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有唯一的物理地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
图7-1:虚拟地址翻译

7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是实现逻辑地址到线性地址转换机制的基础,段的特征有段基址、段限长、段属性。这三个特征存储在段描述符中,用以实现从逻辑地址到线性地址的转换。段描述符存储在段描述符表中,通常,我们使用段选择符定位段描述符在这个表中的位置。每个逻辑地址由16位的段选择符和32位的偏移量组成。
段基址规定了线性地址空间中段的开始地址。在保护模式下,段基址长32位。因为基址长度和寻址地址的长度相同,所以段基址可以是0-4GB范围内的任意地址。
和一个段有关的信息需要8个字节来描述,这就是段描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里所有的描述符都在一起集中存放,这就构成了一个描述符表,描述符表分为两种,GDT和LDT。
一些全局的段描述符,就放在"全局段描述符表(GDT)"中,一些局部的,例如每个进程自己的段描述符,就放在的"局部段描述符表(LDT)"中。
介绍一个完整的变换过程,给出一个完整的逻辑地址。首先看段选择符判断当前转换时GDT中的段还是LDT中的段,再根据相应寄存器得到其地址和大小。之后拿出段选择符中的前13位,在对应地址中查找到对应的段描述符,这样就知道了基址。根据基址和偏移量结合,就得到了所求的线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理
分页机制是实现虚拟存储的关键,位于线性地址与物理地址的变换之间设置。虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传输单元。VM系统通过将虚拟内存分割为称为虚拟页为大小固定的块来处理这个问题。每个虚拟页的大小固定。类似地,物理内存被分割为物理页,大小与虚拟页相同。
同任何缓存一样,虚拟内存系统必须用某种方法来判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM,替换这个牺牲页。
页表是一个存放在物理内存中的数据结构,将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时读取页表。操作系统负责维护页表中的内容,以及再磁盘与DRAM之间来回传送页。
内存分页管理的基本原理是将整个内存区域划分成固定大小的内存页面。程序申请使用内存时就以内存页位单位进行分配。转换通过两个表,页目录表PDE(也叫一级目录)和二级页表PTE。进程的虚拟地址需要首先通过其局部段描述符变换为CPU整个线性地址空间中的地址,然后再使用页目录表和页表PTE映射到实际物理地址上。

7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。为了降低时间开销,MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块,下述为一个从TLB中获取物理地址的过程:
1、CPU产生一个虚拟地址。
2、MMU从TLB中取出相应的PTE。
3、MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
4、高速缓存/主存将所请求的数据字返回给CPU。
图7-4含TLB的虚拟地址翻译流程

Inter Core i7实现支持48位虚拟地址空间和52位物理地址空间,使用4KB的页。X64 CPU上的PTE为64位,所以每个页表一共有512个条目。512个PTE条目需要9位VPN定位。再四级页表的条件下,一共需要36位VPN,因为虚拟地址空间是48位,故低12位是VPO。TLB四路组联,共有16组,需要4位TLBI,故VPN的低4位是TLBI,高32位是TLBT。
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT+TLBI向TLB中匹配,如果命中,则得到40位PPN+12位VPO组合成52位物理地址PA。如果没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,9位VPN1确定在第一级页表中的偏移量,查询出第一部分PTE,以此类推最终在四级页表都访问完后获得PPN,与VPO结合获得PA,并向TLB中更新。

7.5 三级Cache支持下的物理内存访问
得到物理地址PA后,通过其访问物理内存,物理地址由CI(组索引)、CT(标记位)、CO(偏移量)组成。首先使用CI进行组索引,每组8路,对8路的块分别匹配标记位CT,如果匹配成功且块的有效位为1则命中,根据数据偏移量CO取出数据返回。如果没有匹配成功则不命中,向下一级缓存中查询数据,顺序是L1缓存到L2缓存到L3缓存到主存。查询到数据后,放置策略是如果映射到的组有空闲块则直接放置,否则产生冲突,采用最近最少使用策略驱逐块并替换新块进入。
下图给出了三级Cache的大致构造:
图7-5三级Cache构造

7.6 hello进程fork时的内存映射
概念介绍:
1、mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
2、vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
Shell通过fork函数为hello创建新进程,当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、vm_area_struc和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
在用fork创建虚拟内存的时候,要经历以下步骤:
1、创建当前进程的mm_struct,vm_area_struct和页表的原样副本。
2、两个进程的每个页面都标记为只读页面。
3、两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。

7.7 hello进程execve时的内存映射

hello调用execve后,execve在当前进程中加载并运行包含在可执行目标文件中的程序,用hello程序有效地代替了当前程序。当加载并运行可执行目标文件时,需要以下几个步骤:
1、删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2、映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3、映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。下图给出了私有区域和共享区域在内存映射时的位置。
图7-7内存映射位置

7.8 缺页故障与缺页中断处理
物理内存缓存不命中称为缺页。假设CPU引用了磁盘上的一个字,而这个字所属的虚拟页并没有缓存在DRAM中。地址翻译硬件会从内存中读取虚拟页对应的页表,说明这个虚拟页没有被缓存,触发一个缺页故障。
这个异常导致控制转移到内核的缺页处理程序,处理程序首先判断虚拟地址A是否合法,如果不合法则触发段错误终止进程。如果合法则判断试图进行的内存访问是否合法,如果不合法则出发保护异常终止进程。如果合法则根据页式管理的规则,选择一个牺牲页,用新页替换掉,更新页表并再次触发地址翻译硬件进行翻译。

7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种:显式分配器和隐式分配器。显式分配器要求应用显式地释放人设已分配地块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾回收器,而自动释放未使用的已经分配的块的过程叫做垃圾收集。
malloc使用的是显式分配器,通过free函数释放已分配的块。
下面分别介绍两种分配器:
1、隐式空闲链表分配器。我们可以将堆组织为一个连续的已分配块和空闲块的序列,空闲块是通过头部中的大小字段隐含地连接着的,这种结构为隐式空闲表。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块地集合。一个块是由一个字的头部、有效载荷、可能的填充和一个字的脚部,其中脚部就是头部的一个副本。头部编码了这个块的大小以及这个块是已分配还是空闲的。分配器就可以通过检查它的头部和脚部,判断前后块的起始位置和状态。
2、显示空闲链表分配器。将堆组成一个双向空闲链表,在每个空闲块中,都包含一个pred和succ指针。一种方法是用后进先出(LIFO)的顺序来维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用。一般而言,显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在的提高了内部碎片的程度。

7.10本章小结
本章介绍了hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA 到 PA 的变换,以及进程fork和execve内存映射的内容,还有缺页问题和动态存储分配管理的问题。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法

设备的模型化:文件
文件的类型:
1、普通文件(regular file):包含任意数据的文件。
2、目录(directory)(文件夹):包含一组链接的文件,每个链接都将一个文件名映射到一个文件。
3、套接字(socket):用来与另一个进程进行跨网络通信的文件
4、命名通道
5、符号链接
6、字符和块设备
设备管理:unix io接口
操作:
1、打开和关闭文件
2、读取和写入文件
3、改变当前文件的位置
8.2 简述Unix IO接口及其函数
8.2.1打开文件:open函数
操作:一个应用程序要求通过内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个标识符。
说明:进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的。open将filename转换为一个文件描述符,并且返回描述符数字。
8.2.2关闭文件:close函数
操作:当应用程序完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
说明:进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。
8.2.3读取文件:read函数
说明:应用程序通过read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误;返回值0表示EOF;否则,返回值表示的是实际传扫的字节数量。
8.2.4写入文件:write函数
说明:应用程序通过write函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.2.5改变文件位置:lseek函数
说明:文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

8.3 printf的实现分析
8.3.1printf函数:
图8-3-1 printf函数代码

分析:其中,va_list是一个字符指针数据类型,代码中的赋值表示省略参数中的第一个参数,arg变量定位到了第二个参数,也就是第一个格式串。
8.3.2vsprintf函数:
图8-3-2 vsprintf函数代码

分析:vsprintf函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
8.3.3write函数:
图8-3-3 write函数汇编代码

分析:write函数是将buf中的i个元素写到终端的函数。int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。所以它先将参数传入寄存器中之后调用。
8.3.4syscall的实现:
图8-3-4 syscall的实现

分析:syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中储存的是字节的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法、Unix IO接口及其函数,并分析了printf函数和getchar函数。
(第8章1分)
结论
hello程序的过程可总结如下:
1、编写代码:用高级语言写.c文件
2、预处理:从.c生成.i文件,将.c中调用的外部库展开合并到.i中
3、编译:由.i生成.s汇编文件
4、汇编:将.s文件翻译为机器语言指令,并打包成可重定位目标程序hello.o
5、链接:将.o可重定位目标文件和动态链接库链接成可执行目标程序hello
6、运行:在shell中输入命令
7、创建子进程:shell嗲用fork为程序创建子进程
8、加载:shell调用execve函数,将hello程序加载到该子进程,映射虚拟内存
9、执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流
10、访问内存:MMU将虚拟内存地址映射成物理内存地址,CPU通过其来访问
11、动态内存分配:根据需要申请动态内存
12、信号:shell的信号处理函数可以接受程序的异常和用户的请求
13、终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构
至此,hello运行结束。
感悟:hello是一个非常渺小的程序,hello的一生也注定是短暂的一生,然而正是这转瞬即逝的一段旅程,涉及到了计算机系统方方面面的内容,这些内容是计算机运行程序最本质、最核心的方式。

附件
文件名 文件的作用
hello 链接之后的可执行文件
hello.c 源代码
hello.i 预处理之后的文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的重定位文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的TXT格式
hello1.elf hello的ELF格式
hello2.txt hello的TXT格式

参考文献

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值