提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
学 号 7203610617
班 级 2036106
学 生 罗家骏
指 导 教 师 史先俊
2022年5月
摘要
提示:这里可以添加本文要记录的大概内容:
本文从最常见不过的的hello程序的“人生”出发,展现了它玩的 P2P(From Program to Process),引申并分析了hello.c从源码,到被预处理、编译、汇编、链接,以及进程管理、存储管理和I/O管理等一系列的过程,系统性地构建了计算机系统知识体系。
关键词:预处理;编译;汇编;链接; 进程管理;存储管理; I/O管理;
提示:以下是本篇文章正文内容,下面案例可供参考
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
(1)P2P过程:
是From Program to Process的简写,也就是从程序到进程。
Program过程是由编译器驱动程序完成的, 即在这一过程中GCC编译器驱动程序读取实现用文本编辑工具或者C语言编辑器编写好的源文件hello.c,并把它编译成一个可执行目标文件hello。这里可大体分为以下四个阶段完成。cpp的预处理,ccl的编译、as的汇编、ld的链接最终成为可执行目标文件hello。
Process过程是Linux系统中通过内置命令行解释器shell加载运行hello程序,为hello程序创建fork进程,并在进程中进行程序执行。
(2)020:
是From Zero-0 to Zero-0的简写。
Hello在开始时不占用系统资源,表达为第一个Zero(0)。
Shell为fork产生的子进程进行execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。进入main执行目标代码,在执行过程中CPU不断从.text和.data节中读取代码和数据,且这一过程中cpu为hello分配时间片执行逻辑控制流。
Hello执行完成后,shell父进程回收hello进程,内核删除为其产生的数据结构。
总结这一过程:hello从开始不占用系统资源到被内核清除痕迹——即表达为From Zero to Zero。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上。
软件环境:Windows10 64位; Vmware 11以上;Ubuntu 16.04 LTS 64位以上。
开发与调试工具:Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc。
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c——源代码
hello.i——hello.c 预处理生成hello.i
hello.s——Hello.i编译生成的汇编文件
hello.o——Hello.s汇编产生的可重定位目标文件
hello——Hello.o链接产生的可执行目标文件
Hello-o-objdump.txt——Hello.o的反汇编文件
Hello-o-elf.txt——Hello.o的ELF文件
Hello-objdump.txt——Hello的反汇编文件
Hello-elf.txt——Hello的ELF文件
1.4 本章小结
本章分析了hello程序的P2P与020的特点,列出了进行实验时的软硬件环境及开发与调试工具,以及在本文中生成的中间结果文件,是后续实验的基础。
第2章 预处理
2.1 预处理的概念与作用
·概念:
预处理是预处理器cpp对一个程序处理的首步,根据以字符#开头的命令修改原始C程序,将.c文件进行初步处理成一个.i文件。操作对象为原始代码中以字符#开头的命令,包括#include的头文件、#define的宏定义,#if、#ifdef、#endif等条件编译。
·作用:
(1)预处理#include。预处理器会读取头文件中用到的库的C程序段,将这段代码直接插入到程序文件中。
(2)预处理#define。会删除“#define”并展开所定义的宏,完成宏替换。
(3)预处理条件编译指令。#if、#ifdef、#endif等条件编译指令可以根据编译器测试的条件来将一段文本包含到程序中或者排除在程序之外。
(4)删除注释:不编译注释内容。
(5)添加行号和文件名标识,以便编译时编译器产生调试用的行号信息。
(6)总结:让编译器在随后对文本进行编译的过程中,更加方便。
2.2在Ubuntu下预处理的命令
应截图,展示预处理过程!
在命令行输入:gcc -E hello.c -o hello.i生成预处理后的hello.i文件。
在这里插入图片描述
图1 预处理命令
2.3 Hello的预处理结果解析
图2 hello.i的首部分
图3 hello.i的尾部分
结果:图2、图3.
解析:hello.i文件变成了3060行,仍然是可读的C代码。预处理器将预处理指令展开,声称在hello.i文件的首部,如图2;hello.i删除了所有注释;尾部保留hello.c中的main函数部分,图3。
2.4 本章小结
本章介绍了预处理的概念、作用,展示预处理过程,并结合预处理之后的程序对预处理结果进行了解析。预处理是计算机对程序进行操作的第一个步骤,在这个过程中预处理器会对.c文件的头文件、宏定义和注释进行操作,最后将初步处理完成的文本保存在.i文件中,为下一阶段的编译作准备。
第3章 编译
3.1 编译的概念与作用
(注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序)
·概念:
编译是编译器(ccl)将文本文件(.i)翻译成包含一个汇编语言程序的文本文件(.s)的过程。
·作用:
编译阶段的作用首先是通过词法分析、语法分析、语义分析三个阶段对源程序进行静态检查,然后是会对代码进行一定程度的保守的优化,最后将高级语言程序代码转化为汇编代码,产生包含一个汇编语言程序的文本文件。
3.2 在Ubuntu下编译的命令
应截图,展示编译过程!
在命令行输入:gcc -S hello.i -o hello.s生成编译后的hello.s文件。
图4 编译的命令
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1 伪指令
以“.”开头的诸多指令,称作汇编指示(或伪指令),伪指令用于指导汇编器和链接器的工作。
.file -------------- C文件声明
.text -------------- 代码段
.globl ------- -----声明全局变量
.align 8 ----------声明对指令或者数据的存放地址进行对齐的方式
.data -------------已初始化的全局和静态C变量
.type -------- -----指明函数类型或对象类型
.size -------------- 声明变量大小
.long .string----- 声明long型、string型数据
.section .rodata-- 指示为.rodata只读数据段
图5 伪指令
3.3.2 数据定义
图6 main函数
(1)Main函数的两个参数 int argc 和 char *argv[] 放在栈中地址-20和-32处的寄存器%edi和%rsi。
(2)main函数的局部变量i ,在汇编中被放在了栈-4(%rbp)的位置,并被赋予了0的初值,然后与7进行比较,参与了循环。
图7 局部变量i
图8 for循环中的局部变量i
(3)字符串
“用法: Hello 学号 姓名 秒数!”
第一个printf传入的输出格式化参数,存放在只读数据段.rodata中,可以 发现字符串被编码成utf-8格式,一个汉字在utf-8编码中占三个字节,一个\ 代表一个字节。
图9 只读数据段.rodata中的字符串
3.3.3 赋值操作
使用数据传送命令,可以进行赋值操作。最简单形式的数据传输类型是MOV类,MOV有movb, movw, movl, movq等操作。分别操作1、2、4、8字节的数据。mov操作的操作源可以是立即数、寄存器、内存。目的操作数可以是:寄存器、内存。即立即数只能作为mov的操作源。
hello.c中涉及到的赋值操作即对i赋初值的操作,使用movl指令将立即数0传到-4(%rbp),i是四字节int型。
图10 对i赋值的操作
3.3.4 算数运算
hello.c对i有自加运算,汇编代码如下:
图11 对i的自加运算
3.3.5 关系运算
hello.c中涉及到的关系操作在13行if判断“argc!=4”,以及for循环中的“i<8”。对应的hello.s中的汇编代码分别如图12和13所示,通过相应的条件跳转实现。
图12 hello.s中“argc!=4”对应的汇编代码
图13 hello.s中“i<8”对应的汇编代码
3.3.6 数组运算
对于数据类型T和整型常数N。T[ N ]若其位置为Xa,那么T[ i ]就被存放在Xa+sizeof(T)i的地址。数组操作出现在循环体for循环中,每次循环中都要访问 argv[1]、argv[2]这两个内存。argv数组中存储的数据类型是char,占据8个字节,argv[ ]首地址为-4(%rbp)。-4(%rbp)+8的地址是argv[1],将其指向的值放入%rsi;-4(%rbp)+16是argv[2]的地址,它指向的值被放入了%rdx。
图14 访问数组元素汇编代码
3.3.7 控制转移
hello.c中涉及到的控制转移在for循环和if语句中,对应的汇编代码如图15,16所示,使用了cmpl和je指令实现条件转移,以达成所需的条件判断和循环跳转。
图15 if语句中的控制转移
图16 for循环中的控制转移
3.3.8函数操作
hello.c中涉及的函数共有5种,即主函数main,函数printf, exit, sleep以及函数getchar。函数操作有:参数传递、函数调用、函数返回。
比如printf函数汇编调用:
图17 printf函数汇编调用
3.4 本章小结
本章简述了在预处理之后编译器进行编译的过程,由.i文件生成.s文件,通过分析hello.s文件中存储的汇编代码,从伪指令、数据定义、赋值操作、算数运算、关系运算、数组运算、控制转移、函数操作等方面对hello.c的编译结果进行了较为详细的解析。
第4章 汇编
4.1 汇编的概念与作用
(注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。)
·概念:
汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在可重定位目标文件.o的过程。
·作用:
将汇编语言翻译成机器语言指令,以便链接后的程序运行。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
在命令行输入gcc -c hello.s -o hello.o
图18 汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.3.1 ELF头
如图19,ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。
图19 ELF头
4.3.2 节头部表
如图20所示,ELF头下面的部分是节头部表,写明了各个节(这里包含.text,.rela.text,.data,.bss…等14个节)的大小,类型,地址以及偏移量的信息。
图20 节头部表
4.3.3 符号表
符号表(.symtab)记录了程序中定义和引用的函数和全局变量的信息,包括符号以及它们的类型、位置等信息。重定位需要引用的符号都在其中声明。
图21 符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
图22 hello.o的反汇编
图23 hello.s
4.4.1 hello.o的反汇编与hello.s对照分析:
(1)函数调用上:
.s文件中是指令+函数名称的形式,反汇编中调用了函数重定位的地址。
(2)分支转移上:
.s文件中指令跳转时使用段的名称;反汇编中直接指令+跳转地址,需要进行重定位。
图24 .s指令跳转
图25 反汇编指令跳转
(3)操作数上:
.s中操作数为10进制,反汇编中操作数为16进制;反汇编中具有操作数大小(如q、l)的后缀,.s中不涉及。
4.4.2 机器语言的构成:
机器语言由二进制的机器指令序列集合构成,机器指令由操作码和操作数组成。
4.4.3 机器语言与汇编语言的映射关系:
反汇编在代码前具有对应的机器代码,故汇编代码与机器代码存在一一对应的映射关系。
4.5 本章小结
本章探究了汇编阶段, 对hello.s进行汇编生成了相应的可重定向目标文件hello.o ,使用readelf工具读取了ELF文件的文件头、程序节、符号表等信息。比较了hello.o对应的反汇编代码和hello.s代码之间的差异。
第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
·概念:
链接是通过链接器将文件中调用的各种函数跟静态库及动态库链接,并将他们打包合并形成可执行目标文件的过程。
·作用:
(1) 链接可以实现将头文件中引用的函数并入到程序中,解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。
(2) 链接,使得程序的分离编译成为可能,可以将一个大型的应用程序分解成为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,就不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
图26 ld链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在命令行输入:readelf -a hello > hello-elf.txt。
Elf头如图所示,与hello.o的ELF格式相对比,发现可执行文件hello的ELF头中类型信息变为EXEC(可执行文件),同时入口点地址和程序头起点被确定,节头数量、字符串表索引节头也发生了变化。
图27 hello-elf.txt
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
打开edb后,将hello可执行文件拖入edb界面,查看左下角Data Dump一栏。
图28 hello的虚拟地址空间
说明:ELF被映射到了0x401000,虚拟地址从0x00401000开始,到0x00401ff0结束。
5.5 链接的重定位过程分析
5.5.1 objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程
在命令行输入:objdump -d -r hello > hello.objdump(得到hello的反汇编文件)。
图29 hello的反汇编
分析hello反汇编与hello.o反汇编的不同:
(1)相比于hello.o,hello反汇编代码中的可重定位条目都替换成了确定的地址,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。
(2)链接后hello增加了printf、sleep、getchar、exit等函数。
5.5.2 结合hello.o的重定位项目,分析hello中对其怎么重定位的
在链接器完成符号解析后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。此时开始进行重定位,由以下两步组成:[2]
(1)重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节,然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
(2)重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。[2]
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
利用edb单步调试,每次跳转的函数依次记录。
程序地址+程序名称:
0000000000401000 <_init>
00000000004010f0 <_start>
00000000004011c0 <__libc_csu_init>
0000000000401125
0000000000401238 <_fini>
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章分析了经过链接生成可执行文件hello的结构,用反汇编阐述了与之前hello.o文件的异同,分析了hello文件的执行流程以及链接过程中重定位的过程,使用edb探索了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。
作用:
(1)用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
(2)提供给应用程序两个假象:
a) 一个独立的逻辑控制流。我们的程序好像是系统中当前运行的唯一程序,处理器好像是无间断的执行我们程序中的指令。
b) 一个私有地址空间。进程使我们感觉好像独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数。
3)如果是内置命令则立即执行。
4)否则调用相应的程序执行。
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在终端输入./hello时shell进行对命令行的解释,因为第一个命令行参数不是内置shell命令,所以shell会fork一个子进程并执行可执行程序hello。新建的子进程几乎和父进程相同;子进程得到与父进程用户级虚拟地址空间相同的但独立一份副本,与父进程任何打开的文件描述符相同的副本。
使用fork()函数来创建一个子进程,fork函数的原型为:pid_t fork(void)
fork函数有以下特点:
·调用一次,返回两次。一次返回至父进程,返回的是子进程的pid;一次返回至子进程返回值为0。
·父进程与子进程并发执行。
·相同但独立的地址空间。子进程创建时,两个进程具有相同的用户栈、本地变量、堆、全局变量、代码;但二者对这行的改变都是相互独立的。
·共享文件。
6.4 Hello的execve过程
使用fork创建进程后,子进程便会使用execve加载并运行hello程序。 execve函数的功能是加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次且从不返回。
execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序。
具体过程:删除已存在的用户区域。映射私有区域。映射共享区域。设置程序计数器。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
系统中每个程序都运行在某个进程的上下文中。上下文是程序正确运行所需要的状态,由系统内核维持。
一个运行多个进程的系统,进程逻辑流的执行可能是交错的。每个进程执行它的流的一部分, 然后被抢占,轮到其他进程执行。一个逻辑流在时间上与另一个重叠,成为并发流。一个进程执行它的控制流的一部分时间叫做时间片。
控制寄存器利用模式位描述了当前进程享有的特权:当设置了模式位时,进程运行在内核模式中,可以执行任何命令,访问任何内存;当没有设置模式位时,进程为用户模式,不允许执行特权指令,不允许直接引用内核区的代码,数据。
在进程执行时,内核可以抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策称为调度。当进程调度一个新的进程运行后,会使用上下文切换来将控制转移到新的进程。
上下文切换的过程:
·保存当前进程的上下文;
·恢复某个先前被抢占进程的被保存的上下文;
·将控制传递给新进程。系统调用、中断可能引起上下文切换。
hello程序执行了sleep系统调用,应发了陷阱异常。此时会从用户模式进入内核模式,使程序休眠一段时间,将控制转给其他进程。当sleep结束后,发送信号给内核,进入内核状态异常处理,此时hello程序得以重新回到用户模式。当执行getchar函数时,会使用read系统调用,产生上下文切换。
6.6 hello的异常与信号处理
6.6.1 异常及处理方式
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常可分为四类:中断、陷阱、故障、终止。
异常原因及处理方式:
中断是由于hello运行过程中遇到来自处理器外部的IO设备的中断,例如键盘。中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。
陷阱是由于hello运行过程中调用了sleep系统调用。陷阱是有意的异常,是执行一条指令后的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障是由错误情况(比如缺页异常)引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果故障处理程序能够修正这个错误,它就将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的 abort例程,abort例程会终止引起故障的应用程序。
终止是由于hello运行时发生错误,通常是一些硬件错误,比如DRAM 或者 SRAM位被损坏时发生的奇偶错误。终止处理程序不会将控制返回给应用程序。
6.6.2 程序运行过程中各命令及运行结果
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结果截屏,说明异常与信号的处理。
运行结果:
(1)正常运行
如图30,程序输出8次hello 学号+姓名
图30正常运行
(2)乱按键盘+回车
如图31所示,会将所按键位输出,直至第一次按enter键,将之后的输入作为shell的命令行输入。
图31 乱按键盘+回车
(3) CTRL+C
如图32所示,按下CTRL+C直接发出SIGINT信号,中断程序。
图32 CTRL+C中断程序
(4) CTRL+Z
按下CTRL+Z翻出SIGTSTP信号,程序停止,指导下一个SIGCONT。
图33 CTRL+Z停止程序
(5)各种命令
a)使用ps查看进程信息
图34 ps查看进程信息
b)使用jobs查看作业状态
图35 jobs查看作业状态
c)使用pstree查看进程树
图36 pstree查看进程树
d)使用fg将后台进程置于前台运行
图37 fg将后台进程置于前台运行
e)使用kill发送信号
图38 kill发送信号
6.7 本章小结
本章介绍了进程、shell的概念,尤其是hello在运行过程中,调用fork函数及execve创建新的进程并执行程序这一系列过程的介绍,并在执行过程中在终端执行各种命令,并对产生的结果进行观察分析。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
(1) 逻辑地址:逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。 逻辑地址往往不同于物理地址(physical address),通过地址翻译器(address translator)或映射函数可以把逻辑地址转化为物理地址。
(2) 线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
(3) 物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。对于hello程序而言,MMU将虚拟内存地址映射到内存中的地址就是hello的物理地址。
(4) 虚拟地址: CPU在寻址的时候,是按照虚拟地址来寻址。CPU通过生成一个虚拟地址(VA)来访问主存,这个虚拟地址被送到内存之前先转换为适当的物理地址。实际上通过反汇编查看到的地址就是hello的虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成:段选择符和段内偏移量。段是对程序逻辑意义上的一种划分,一组完整逻辑意义的程序被划分为一段。段的长度不确定。段描述符用于描述一个段的详细信息。段选择符用于找到对应的段描述符。
段选择符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。TI:0为GDT,1为LDT。Index指出选择描述符表中的哪个条目,RPL请求特权级。
最初8086处理器的寄存器是16位的,为了能够访问更多的地址空间但不改变寄存器和指令的位宽,所以引入段寄存器,8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(即虚拟地址VA)到物理地址(PA)之间的转换通过分页机制完成。
分页机制是对虚拟地址内存空间进行分页。分页是指将内存分割成4KB大小的页(Pages) ,同时将程序段的地址空间按内存页的大小进行划分。
分页模式的基本思想:当任务运行时,当前活跃的执行代码保留在内存中,而程序中当前未使用的部分,将继续保存在磁盘上。当CPU需要执行的当前代码存储在磁盘上时,产生一个缺页错误,引起所需页面的换进(从磁盘载入内存)。通过分页以及页面的换进、换出,一台内存有限的计算机上可以同时运行多个大程序,让人感觉这台机器的内存无限大,因此称为虚拟内存。
页表:页表是一个页表条目(Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址。由页表这一数据结构实现了虚拟地址到物理地址的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
为了消除每次 CPU 产生一个虚拟地址,MMU 就查阅一个PTE带来的时间开销,许多系统都在MMU中包括了一个关于 PTE 的小的缓存,称为翻译后被缓冲器(TLB),TLB的速度快于L1 cache。
TLB通过虚拟地址VPN部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU在读取PTE时会直接通过TLB,如果不命中再从内存中将PTE复制到TLB。同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩 页表大小。core i7使用的是四级页表。
在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。
7.5 三级Cache支持下的物理内存访问
得到物理地址PA后,我们将物理地址进行分割。物理地址(52位)被分割位40位的标记位CT,6位的索引位CI,6位的块偏移位CO。通过CT查找告诉缓存中的对应块,通过CI在块中寻找行,进行组索引,每组8路,对8路的块分别匹配CT(前40位)。如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。若命中,则返回对应块偏移的数据。否则,L1不命中,需要从L2,L3甚至是主存中得到对应的数据。
如果没有匹配成功或者匹配成功但是标志位是0,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
图39 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
当fork函数被shell调用时,并分配给hello一个唯一的PID。为了给hello创建虚拟内存,fork创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello中返回时,hello现在的虚拟内存刚好和调用shell的虚拟内存相同。当这两个进程中的任何一个进行写操作时,写时复制机制会创建新页面。因此也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
(出自深入理解计算机系统-原书第3版,龚奕利,贺莲译)
调用execve加载并运行hello大体分为以下数个步骤:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点_start.
7.8 缺页故障与缺页中断处理
(1)缺页故障
当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的 物理地址不在内存中,因此必须从磁盘中取出时就会发生故障。简单来说,DRAM缓存不命中称为缺页,当MMU翻译虚拟地址时发现在页表中,该页表项的有效位为0,触发缺页中断。
图40 缺页故障
(2)缺页中断处理
缺页异常处理程序选择一个牺牲页将目标页加载到物理内存中,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动导致缺页的指令,这条指令再次发送VA到MMU, MMU就能正常翻译VA了,页面命中。
7.9 动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态存储分配管理由动态内存分配器完成。而动态内存分配器维护着一个被称为堆的虚拟内存区域。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向更高的地址。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片。这些内存片要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有几种风格。
(1)显示空闲链表:要求应用显式地释放任何已分配的块,例如,C语言中的 malloc 和 free。
(2)隐式空闲链表: 应用检测到已分配块不再被程序所使用,就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)。
(3)分离的空闲链表:维护多个空闲链表,将所有可能的块的大小划分为大小类。基本方法有:简化单分离存储、分离适配、伙伴系统。
7.10 本章小结
本章简述了系统对于hello的存储管理,介绍了虚拟地址、物理地址、线性地址、逻辑地址,地址管理中的段式管理与页式管理,进程执行fork与execve的内存映射的内容,存储器层次结构中的三级高速缓存,以及说明了系统应对缺页异常的方法和malloc的内存分配管理机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)read函数:
ssize_t read(int fd, void *buf, size_t nbytes)。
其中fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。
(2)write 函数:
ssize_t read(int fd, void *buf, size_t nbytes)。
其中fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数;若读取失败则返回-1。
(3)close函数:
int close(int fd)。
其中fd为要关闭文件的文件描述符,关闭成功返回0,失败则返回-1。用于关闭一个文件,所需头文件为#include <unistd.h>。
(4)open函数
int open(char *filename,int flags,mode_t mode)。
用于打开文件,或者创建文件,将filename转换成文件描述符。成功时返回文件描述符,失败时返回-1。
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本章小结
本章介绍了 Linux 中 I/O 设备的管理方法,Unix I/O 接口和函数,以及printf 和 getchar 函数的Unix I/O 实现分析。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(一) hello所经历的过程:
- hello.c是最简单的C程序。
- 预处理:预处理器扩展源代码插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏,合并为一个hello.i文件。
- 编译:编译器gcc将hello.i编译成hello.s文件,包含汇编代码。
- 汇编:hello.s经过汇编器as翻译成机器语言指令,并打包形成可重定位目标文件。
- 链接:链接器ld将hello.o需要的各种目标文件与hello.o进行链接,形成可执行目标文件hello
- 使用shell执行hello
- shell使用fork创建子进程,使用execve加载并运行hello程序。
- hello运行过程中,可能要遇到各种异常,收到各种信号,hello可能需要陷入到内核,调用异常处理程序。
- hello调用printf,使用UNIX IO来进行输出
- 结束进程:exit,hello的父进程回收hello,内核也删除它的所有数据。
(二)感悟:
大作业的实验从一个最常见不过的hello程序引申出很多值得我们学习和思考的问题的方面,体现了计算机系统在整体上的统一,很有意义。系统是硬件和软件相互交织的集合体,它们必须共同协作才能达到程序的最终目的。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c——源代码
hello.i——hello.c 预处理生成hello.i
hello.s——Hello.i编译生成的汇编文件
hello.o——Hello.s汇编产生的可重定位目标文件
hello——Hello.o链接产生的可执行目标文件
Hello-o-objdump.txt——Hello.o的反汇编文件
Hello-o-elf.txt——Hello.o的ELF文件
Hello-objdump.txt——Hello的反汇编文件
Hello-elf.txt——Hello的ELF文件
参考文献
[1] 《深入理解计算机系统(原书第3版)》作者:[美]兰德尔 E.布莱恩特(Randal E.·Bryant) 著;龚奕利、贺莲 译
[2] wyf_moraner. 程序人生-CSAPP大作业. 2021.
https://blog.csdn.net/Moraner/article/details/118366473