CSAPP 2019大作业

目 录

第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:hello.c经过cpp预处理、ccl编译、as汇编以及ld链接四个过程生成二进制可执行目标程序,然后由shell通过fork为其创建一个新进程。
020:shell通过execve加载并执行hello,为其映射虚拟内存,在进入程序入口后分配并载入物理内存,然后进入main函数开始执行相关代码,CPU为其分配时间片执行逻辑控制流。进程结束后,shell进行回收,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk
软件环境:Windows10 Home 64位; Vmware 15;Ubuntu 16.04 LTS 64位
开发与调试工具:gcc;gedit;readelf;gdb;codeblocks
1.3 中间结果
文件 作用
hello.c 源代码
hello.i 预处理生成的文本文件
hello.s hello.i编译生成的汇编文件
hello.o hello.s汇编生成的可重定位目标文件
hello 链接后的可执行文件

1.4 本章小结
本章简单描述了hello 的P2P、020过程,介绍了环境与工具以及过程中生成的中间产物文件及其各自的作用。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器cpp根据以#开头的命令进行程序的修改,进行调用库,处理程序中的宏定义,以及删除程序中的注释等操作。
预处理的作用:预处理的作用是让编译器在随后对文本进行编译的过程中更加方便简洁,减少编译器的工作。
2.2在Ubuntu下预处理的命令
命令:GGC -E hello.c -o hello.i在这里插入图片描述

图2.2 预处理结果
2.3 Hello的预处理结果解析
hello.i从hello.c 的527字节增加到66,088字节,查看hello.i发现main函数的位置跳到了文件的最后部分,原C文件中的注释在.i文件中也都被删除了。而在main函数之前,预处理器cpp读取头文件stdio.h 、stdlib.h 、和unistd.h中的内容,三个系统头文件依次展开。
2.4 本章小结
本章介绍了hello.c的预处理过程,并通过对修改后的hello.i文件进行结果分析,介绍了预处理器cpp读取系统头文件中的内容并把它插入程序文本,删除注释,调用库,处理程序中的宏定义等一系列过程。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:编译的过程就是编译器ccl对hello.i文件进行一系列处理的过程。在编译阶段中,编译器首先检查代码的规范性和合理性,之后分析代码的语义和语法,确定代码的工作目标,检查无误后,编译器再将hello.i翻译成hello.s文件即生成汇编代码。
编译的作用:在编译阶段编译器会对代码进行一系列的检查,如果出现错误则会直接反馈回来造成编译的失败。其次,汇编语言位不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
命令:gcc -S heelo.i -o hello.s

在这里插入图片描述图3.2 编译结果
3.3 Hello的编译结果解析
在这里插入图片描述
图3.3 hello.s 的部分内容
.file:源文件名
.text:代码段
.section .rodata:下面是.rodata节
.align:对齐方式
.globl:全局变量
.type:指定是对象类型或是函数类型
.string:字符串

3.3.1 数据
hello.s中C语言的数据类型主要有:全局变量,局部变量,指针数组
①int argc:argc是函数传入的第一个int型参数,存储在%edi中
②int i;局部变量,根据movl $0, -4(%rbp)操作可知i的数据类型占用了4字节的栈空间。
③常量:在hello.s中如4和8的常量以立即数的形式出现。
④字符串:argv[1]和argv[2]都声明在.rodata只能读数据段中,并且给出了字符串的编码。

3.3.2 赋值
对局部变量i的赋值:使用movl语句:movl $0, %eax
3.3.3算数操作
对i++算数操作的编译:使用addl语句:addl $1, -4(%rbp)
3.3.4 关系操作
①i<8关系操作编译为:
在这里插入图片描述
②argc!=4关系操作编译为:
在这里插入图片描述

3.3.5 数组/指针/结构操作
指针数组:char argv[]:在argv数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串且通过(%rax)和%rax+8分别可得上述字符串。其中char 数据类型占8个字节。
在这里插入图片描述
图3.3.5 数组结构解析
3.3.6 控制转移
①if(argc!=4) 。当argc不等于4时进行跳转。cmpl语句比较 -20(%rbp)和-4,设置条件码,判断ZF零标志,如果最近的操作得出的结果为0,则跳到.L2中,否则顺序执行下一条语句。
②for(i=0;i<8;i++) :for循环里面的比较和转移过程, for循环的控制时比较cmpl $7, -4(%rbp) ,当i大于7时跳出循环,否则进入.L4循环体内部执行。
3.3.7 函数操作
①main函数:
参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数调用
函数返回:设置%eax为0并且返回,对应return 0 。
②printf函数:
参数传递:call put@PLT时只传入了字符串参数首地址;for循环中call printf@PTL时传入了 argv[1]和argc[2]的地址。
函数调用:for循环中被调用
③exit函数:
参数传递:传入的参数为1,再执行退出命令
函数调用:if判断条件满足后被调用
④sleep函数:
参数传递:传递控制call sleep@PTL
函数调用:for循环下被调用
⑤getchar函数
传递控制:call getchar@PLT
函数调用:在main中被调用
3.4 本章小结
本章主要介绍了编译器ccl是如何将.i文件编译成汇编代码的。编译的过程就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码,在这个过程中,编译器会对代码进行隐式的优化,并且会将原代码中用到的跳转、循环等操作操作用控制转移等方法进行解析,最后生成汇编代码保存在hello.s文件中。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器as将hello.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。
汇编的作用:汇编的作用是将hello.s中的汇编代码翻译成可以供机器执行的二进制代码。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
在这里插入图片描述
图4.2 汇编结果
4.3 可重定位目标elf格式
① ELF头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接器语法分析和解释目标文件的信息。
在这里插入图片描述
图4.3.1 ELF头信息

②节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
在这里插入图片描述
图4.3.2 ELF节头部信息

③当汇编器生成一个目标模块时,其并不知道数据和代码最终将放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。因此,无汇编器论何时遇到对最终位置未知的目标引用,都会生成一个重定位条目来指示链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

ELF重定位条目的数据结构如下:
typedef struct
{
long offset; /需要被修改的引用的节偏移/
long type:32, /重定位类型/
symbol:32; /标识被修改引用应该指向的符号/
long attend; /符号常数,对修改引用的值做偏移调整/
}Elf64_Rela;

两种最基本的重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。
在这里插入图片描述
图4.3.3 ELF重定位条目信息

④.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
在这里插入图片描述
图4.3.4 ELF符号表信息
4.4 Hello.o的结果解析
反汇编命令:objdump -d -r hello.o
对比hello.s文件和反汇编代码,主要有以下的差别:
①操作数:hello.s中的操作数为十进制,hello.o反汇编代码中的操作数是十六进制。
②分支转移:跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。
③函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。由于函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
④全局变量的访问:在hello.s文件中,对于.rodata等全局变量的访问为$.LC0,而在反汇编代码中是$0x0。由于变量的地址在程序运行时才能确定,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
在这里插入图片描述在这里插入图片描述
图4.4.1 反汇编结果
在这里插入图片描述在这里插入图片描述
图4.4.2 hello.s内容
4.5 本章小结
本章主要介绍了汇编过程。汇编器as通过汇编操作将汇编代码处理成机器可以看懂的机器语言即二进制代码。二进制代码较汇编代码来说,虽然可读性变得比较差,但是在执行效率方面有了非常大的提升,汇编代码虽然已经在原来的文本的基础上进行了优化,但是还是存在着一些字符等不能够直接处理的数据。但是二进制代码中,已经将所有的指令、函数名字等量变成了相应的存储地址,这样机器就可以直接读取这些代码并执行。hello.o可重定位目标文件为后面的链接提供了充足的准备条件。通过介绍hello.s和反汇编代码的区别,使我们更为深刻地理解汇编语言到机器语言实现地转变和这一过程中为链接阶段所做的准备。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是通过链接器ld将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:链接能将程序封装为很多个模块以供我们直接调用,如标准C库中的printf函数:printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个函数必须通过链接器将该文件合并到hello.o程序中,结果得到可执行目标文件hello,它可以被加载到内存中,由系统执行。
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的格式
可执行目标文件hello的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
在这里插入图片描述在这里插入图片描述
图5.3 hello 节头部信息
5.4 hello的虚拟地址空间
edb图中第一行为ELF头信息,也就是ELF文件最开始存的数据。对比后可以发现通过这两种方式得到的信息是完全相同的。再来说一下这个信息的含义,我们看到右侧的ELF头下面有很多文字注释。
在这里插入图片描述在这里插入图片描述
图5.4 hello和hello.o反汇编代码对比分析
5.5 链接的重定位过程分析
反汇编命令:objdump -d -r hello
hello与hello.o主要有以下的不同:
①链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。
②增加的节:hello中增加了.init和.plt节以及一些节中定义的函数。
③函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
④地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata和sleepsecs等全局变量的访问,是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

根据上述hello和hello.o的不同,分析链接的过程:链接器ld将各个目标文件组装在一起,即将.o文件中的各个函数段按照一定规则累积在一起。

根据hello.o中的重定位项目,分析hello重定位过程:
重定位过程合并输入模块并为每个符号分配运行时地址,主要分以下两个步骤:
①重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,包括hello.o在内的所有可重定位目标文件中的.data节被全部合并成一个节,这个节成为输出的可执行目标文件hello中的.data节。然后,连接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中每条指令和包括.rodata、sleepsecs等全局变量都有唯一的运行时内存地址了。
②重定位节中的符号引用。链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。
在这里插入图片描述
图5.5 反汇编结果部分内容
5.6 hello的执行流程
0x400430 init;
0x400460 puts@plt;
0x400470 printf@plt;
0x400480 __libc_start_main@plt;
0x400490 getchar@plt;
0x4004a0 exit@plt;
0x4004b0 sleep@plt;
0x4004d0 _start;
0x4004fe main;
0x400580 __libc_csu_init;
0x4005f0 __libc_csu_fini;
0x4005f4 _fini;
5.7 Hello的动态链接分析

图5.7 _dl_init 前(上图)后(下图)
在执行函数dl_init的前后,地址0x600ff0中的值由0发生了变化。这说明这个表中的信息是在程序执行的过程中动态的链接进来的。也就是说,我们在之前重定位等一系列工作中,用到的地址都是虚拟地址,而我们需要的真实的地址信息会在程序执行的过程中用动态链接的方式加入到程序中。当我们每次从PLT表中查看数据的时候,会首先根据PLT表访问GOT表,得到了真实地址之后再进行操作。
5.8 本章小结
本章主要介绍了链接的过程。链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。通过查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,更好地掌握了链接尤其是重定位的过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:狭义上:进程是一个执行中程序的示例。广义上:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
进程的作用:
①每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
②进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash的作用:
①Shell实际上是一个命令解释器,它将由用户输入的命令进行解释并把它们送到内核。Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,如循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果
②shell的处理流程
shell首先从界面中读取用户的输入并并转化为对应的参数,再检查命令是否是内部命令,若不是再检查是否是一个应用程序或购买的商业程序,之后shell在搜索路径里寻找这些应用程序。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
6.3 Hello的fork进程创建过程
fork函数的机制为:父进程通过调用fork函数创建一个新的子进程。新创建的子进程几乎但不完全与子进程相同。在创建子进程的过程中,内核会将父进程的代码、数据段、堆、共享库以及用户栈这些信息全部复制给子进程,同时子进程还可以读父进程打开的副本。唯一的不同就是他们的PID,这说明虽然父进程与子进程所用到的信息几乎是完全相同的,但是这两个程序却是相互独立的,各自有自己独有的用户栈等信息。
fork函数虽然只会被调用一次,但是在返回的时候却有两次。在父进程中,fork函数返回子进程的PID;在子进程中,fork函数返回0。这就提供了一种用fork函数的返回值来区分父进程和子进程的方法。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件Hello并带列表argv和环境变量列表envp。只有当出现错误时才会返回到调用程序。在execve加载了Hello之后,它调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(int argc , char **argv , char *envp);

结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,主要分以下几个步骤:
①删除已存在的用户区域。
②映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
③映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
进程调度的过程:在Hello执行的某些时刻如sleep函数,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。当内核调度了一个新的进程运行后,它就抢占Hello进程,并且使用上下文切换机制来将控制转移到新的进程。
Hello进程初始运行在用户模式中,直到Hello进程中的sleep系统调用,它显式地请求让Hello进程休眠,内核可以决定执行上下文切换,进入到内核模式。当定时器2.5后中断时,内核就能判断当前Hello休眠运行了足够长的时间,切换回用户模式。具体如下:
时间进程hello其他进程sleep中断从sleep返回用户模式内核模式用户模式上下文切换上下文切换
6.6 hello的异常与信号处理
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

hello执行过程中会出现的异常:
中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT
终止:信号SIGINT,默认行为是 终止

各命令情况截图如下:

  1. hello运行时什么都不按。程序执行完后,进程被回收。再按回车键,结束。
    在这里插入图片描述
    图6.6.1 正常运行

2.运行过程中按Ctrl+C。父进程收到SIGINT信号,终止hello进程,并且回收hello进程。
在这里插入图片描述
图6.6.2 运行时按Ctrl+C

3.运行时乱按。如图6-6,发现乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入。
在这里插入图片描述
图6.6.3 hello运行时乱按

4.按下Ctrl+Z后运行ps命令。按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程(包括僵死进程)。
在这里插入图片描述
图6.6.4 按下Ctrl+Z后运行ps命令

  1. 按下Ctrl+Z后运行jobs命令。jobs命令列出 当前shell环境中已启动的任务状态。
    在这里插入图片描述
    图6.6.5 按下Ctrl+Z后运行jobs命令

6.按下Ctrl+Z后运行pstree命令。pstree命令是以树状图显示进程间的关系。
在这里插入图片描述
图6.6.6 按下Ctrl+Z后运行pstree命令部分截图

7.fg命令将进程调到前台。
在这里插入图片描述
图6.6.7 fg命令

8.kill发送信号给一个进程或多个进程。通过kill -9 32309杀死pid为32309的进程。
在这里插入图片描述
图6.6.8 kill命令

6.7本章小结
本章主要介绍了hello进程的执行过程: hello的创建、加载和终止,以及如何利用shell这个平台来对进程进行监理调用或发送信号等一系列操作。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:又称相对地址,是程序运行由CPU产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。
物理地址:即程序运行时加载到内存地址寄存器中的地址,内存单元的真正地址。
线性地址:是经过段机制转化之后用于描述程序分页信息的地址,是对程序运行区块的一个抽象映射。
虚拟地址:与线性地址相似,是对程序运行区块的相对映射。对hello这个程序而言,虚拟地址是在物理地址上运行的,但是对于CPU而言,CPU看到的hello运行的地址是逻辑地址,在具体操作的过程中,CPU会将逻辑地址转换成线性地址再变成物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图所示:

图7.2 逻辑地址
索引号是“段描述符(segment descriptor)”的索引,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成。其中Base字段,它描述了一个段的开始位置的线性地址,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中,由段选择符中的T1字段表示选择使用哪个,=0,表示用GDT=1表示用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
具体的转换步骤如下:
①给定一个完整的逻辑地址[段选择符:段内偏移地址。
②看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。可以得到一个数组。
③取出段选择符中前13位,在数组中查找到对应的段描述符,得到Base,也就是基地址。
④线性地址 = Base + offset。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的转换是通过“页”完成的。所谓的页就是线性地址被分为以固定长度为单位的组。
Linux 系统拥有自己的虚拟内存系统,Linux 将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在,因此不需要记录。内核为hello进程维护一个段的任务结构,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd 指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct 的链表,一个链表条目对应一个段,所以链表相连指出了 hello 进程虚拟内存中的所有段。
CPU芯片上有一个专门的硬件叫做内存管理单元(MMU),它的功能就是动态的将虚拟地址翻译成物理地址的。
其工作原理如下图:

图7.3 MMU工作原理
上图中,N为的虚拟地址包含两个部分,一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE(页表条目)。接下来在对应的PTE中获得PPN(物理页号),将PPN与VPO串联起来,就得到了相应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
变换过程如下图:

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

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 中添加条目。

7.5 三级Cache支持下的物理内存访问
在7.4节中我们已经获得了物理地址VA,下面继续利用7.4中的图的右侧部分进行说明:使用CI进行组索引,每组8路,对8路的块分别匹配 CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。 如果没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
7.6 hello进程fork时的内存映射
在7.3节中提及了mm_struct和vm_area_struct这两个标记符,这里进行具体介绍:
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。

hello进程在用fork创建内存时主要分以下三个步骤:
①创建当前进程的mm_struct,vm_area_struct和页表的原样副本。
②两个进程的每个页面都标记为只读页面。
③两个进程的每个vm_area_struct都标记为私有以保证只能在写入时复制。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。
加载并运行hello分为以下四个步骤:
①删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
③映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
由于页表只相当于磁盘的一个缓存,因此不可能保存磁盘中全部的信息,所以会产生有些信息的查询出现查询失败的情况,即缺页。
对于一个访问虚拟内存的指令来说,如果发生了缺页,CPU就会触发缺页异常。缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页作为新的储存页。在找到要存储的之后,内核会从磁盘中将需要访问的内存,并将PTE中的信息更新,这样就成功的将一个物理地址缓存在了页表中。当异常处理返回的时候,CPU会重新执行访问虚拟内存的操作,这个时候就可以正常的访问,不会发生缺页现象了。
7.9动态存储分配管理
动态内存分配器的基本原理:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的已分配或空闲的虚拟内存片。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲直到它显式地被应用所分配。一个已分配的块保持已分配状态直到它被应用程序显式执行释放或内存分配器自身隐式执行释放。分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么 就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。

隐式空闲链表分配器原理:
隐式空闲链表分为两种形式,具体如下:

  1. 如下图。头部一共四个字节,前三个字节存储的是块的大小,最后一个字节存储的是当前这个块是空闲块还是已分配的块,0代表空闲块,1代表已分配的块。中间的有效载荷就是用于存放已分配的块中的信息用的。最后的填充部分是为了地址对齐等一些要求用的。

图7.9.1隐式空闲链表结构1

2.如下图。第二种结构与第一种结构不同的是在最后多了一个与头部相同的结构,即脚部。这个新的结构的作用就是为了在空闲块合并的时候比较方便高效。因为如果利用之前的结构,在合并前面的空闲块的时候,由于我们不知道前面的块的大小,所以我们不能获得前面块的起始位置,这样就只能从链表的开始来找一遍。有了脚部,我们就可以利用脚部中存储的信息来获得前一个块中的地址。

图7.9.2 隐式空闲链表结构2

显式空闲链表基本原理:
如下图,通过与隐式结构对比,不同的是显示结构在空闲块中增加了8个字节,分别保存当前空闲块的前驱空闲块的地址和后继空闲块的地址。也就是说,显式的结构比隐式结构多维护了一个链表,就是空闲块的链表。这样做的好处就是在我们在malloc的时候,隐式的方法是要遍历所有的块,包括空闲块了分配块。但是显式的结构只需要在空闲块中维护的链表检索就可以了,这样降低了在malloc时候的复杂度。
关于空闲块的维护方式一共有两种,一种是后进先出的方式,另一种是按照地址的方式。按照地址维护很好理解,与隐式的结构大致相同。后进先出的方式的思想是,当一个分配块被free之后,将这个块放到链表的最开头,这样在malloc的时候会首先看一下最后被free的块是否符合要求。这样的好处是释放一个块的时候比较高效,直接放在头部就可以。

图7.9.3 显示空闲链表(左空闲块,右已分配块)
7.10本章小结
本章主要介绍了储存器的地址空间(包括虚拟地址、物理地址、线性地址、逻辑地址)以及进程fork和execve时的内存映射的内容,同时描述了TLB和四级页表支持下VA到PA的转换,以及得到了PA后三级cache下的物理内存的访问过程,阐述了系统应对缺页异常的措施,最后介绍了malloc的内存分配管理机制。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个linux文件就是一个m个字节的序列:

B0 , B1 , … , Bk , … , Bm-1
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:
打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置:文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。
关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中

Unix IO函数:

  1. open()函数
    功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
    函数原型: int open(const char *pathname,int flags,int perms)
    参数:pathname:被打开的文件名
    flags:文件打开方式,
    返回值:成功:返回文件描述符;失败:返回-1

  2. close()函数
    功能描述:用于关闭一个被打开的的文件
    函数原型:int close(int fd)
    参数:fd文件描述符
    函数返回值:0成功,-1出错

  3. read()函数
    功能描述: 从文件读取数据。
    函数原型:ssize_t read(int fd, void *buf, size_t count);
    参数:fd:将要读取数据的文件描述词。
    buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。
    count: 表示调用一次
    read操作,应该读多少数量的字符。
    返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

  4. write()函数
    功能描述: 向文件写入数据。
    函数原型:ssize_t write(int fd, void *buf, size_t count);
    返回值:写入文件的字节数(成功);-1(出错)

  5. lseek()函数
    功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
    函数原型:off_t lseek(int fd, off_t offset,int whence);
    参数:fd;文件描述符。
    offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
    返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析
printf需要做的事情是:接受一fmt的格式,然后将匹配到的参数按照fmt格式输出。由下面的代码可知printf函数主要调用了vsprintf和write函数。

图8.3.1 printf函数

由下图vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
在这里插入图片描述
图8.3.2 vsprintf函数
Printf的运行过程:从vsprintf生成显示信息,显示信息传送到write系统函数,write函数再陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序。从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成ASCII码,保存到系统的键盘缓冲区之中。

图8.4 getchar函数

getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时,程序就等待用户进行按键输入,用户输入的字符被存放在键盘缓冲区中。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
如果buf长度为0,getchar会调用read函数,将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度;如果buf长度不为0,则直接将保存的buf中的最前面的元素返回。、

8.5本章小结
本章节介绍Linux的I/O设备管理机制以及Unix IO和Unix IO函数,深入分析了printf函数和getchar函数的实现方法以及操作过程。

(第8章1分)
结论
hello的“一生”:
1.hello被IO设备编写,以文件的方式储存在主存中。
2.hello.c经过预处理生成hello.i文件
3.编译器将hello.i编译为汇编代码并保存在hello.s汇编文件中
4.汇编器将hello.s汇编成可重定位目标文件hello.o
5.链接器将hello.o和外部文件进行链接,生成可执行文件hello
6.在shell中输入运行命令后,内核会其分配好运行程序所需要的堆、用户栈、虚拟内存等一系列信息并通过exceve加载并运行hello
7.在一个时间片中,hello具有自己的CPU资源,顺序执行逻辑控制流
8. 当我们需要从外部对hello程序进行操控的时候,只需要在键盘上给一个相应的信号,系统就会按照相应的指令来执行
9.hello的VA通过TLB和页表翻译为PA。当hello需要访问磁盘中的信息的时候, CPU找到VA并利用MMU将其翻译成可以看懂的地址。
10.三级cache 支持下的hello物理地址访问
11.hello在运行过程中会有异常和信号等
12.printf会调用malloc通过动态内存分配器申请堆中的内存
13.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构
14. 当hello与计算机系统携手完成以上工作并解决相应问题后,hello就伴随着shell的进程回收结束了它短暂的一生。

深切感悟:通过这次大作业的完成,我加深了对计算机系统的深入理解,对所学的理论知识有了进一步的理解与掌握。

创新理念:个人认为计算机系统应该向松散耦合的方向发展,在未来嵌入式系统应被更多的采用。

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

附件
文件 作用
hello.c 源代码
hello.i 预处理生成的文本文件
hello.s hello.i编译生成的汇编文件
hello.o hello.s汇编生成的可重定位目标文件
hello 链接后的可执行文件
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] https://baike.baidu.com/item/getchar/919709?fr=aladdin
[3] .https://blog.csdn.net/qq_35144795/article/details/77580913
[4] https://blog.csdn.net/weixin_43821874/article/details/86485888
[5] https://blog.csdn.net/weixin_30586257/article/details/95556967
(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值