csapp大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 1190201313
班   级 1903006
学 生 唐亚锋    
指 导 教 师 史先俊

计算机科学与技术学院
2021年5月
摘 要
结合深入理解计算机系统书本,计算机底层知识,以及现代工具诸如vscode,vs2019,edb,gdb,objdump等等工具,我们在理论指导的基础上,进行了实验测试,从头到尾体验了hello程序完整的一生,P2P与020。通过编写这个大作业,可以把原来分散的知识串联起来,用俯视的角度,观察整个程序的全过程,对于复习很有帮助。同时我们对于计算机底层的一些表示也进行了分析与比较,有了一个较为透彻的掌握
关键词:计算机系统,链接,I/O,编译原理,进程,异常,计算机体系结构;

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

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 7 -
2.4 本章小结 - 7 -
第3章 编译 - 9 -
3.1 编译的概念与作用 - 9 -
3.2 在UBUNTU下编译的命令 - 11 -
3.3 HELLO的编译结果解析 - 12 -
3.4 本章小结 - 16 -
第4章 汇编 - 17 -
4.1 汇编的概念与作用 - 17 -
4.2 在UBUNTU下汇编的命令 - 17 -
4.3 可重定位目标ELF格式 - 17 -
4.4 HELLO.O的结果解析 - 21 -
4.5 本章小结 - 23 -
第5章 链接 - 24 -
5.1 链接的概念与作用 - 24 -
5.2 在UBUNTU下链接的命令 - 24 -
5.3 可执行目标文件HELLO的格式 - 25 -
5.4 HELLO的虚拟地址空间 - 26 -
5.5 链接的重定位过程分析 - 28 -
5.6 HELLO的执行流程 - 30 -
5.7 HELLO的动态链接分析 - 32 -
5.8 本章小结 - 32 -
第6章 HELLO进程管理 - 34 -
6.1 进程的概念与作用 - 34 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 34 -
6.3 HELLO的FORK进程创建过程 - 34 -
6.4 HELLO的EXECVE过程 - 35 -
6.5 HELLO的进程执行 - 35 -
6.6 HELLO的异常与信号处理 - 36 -
6.7本章小结 - 38 -
第7章 HELLO的存储管理 - 39 -
7.1 HELLO的存储器地址空间 - 39 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 39 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 40 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 40 -
7.5 三级CACHE支持下的物理内存访问 - 41 -
7.6 HELLO进程FORK时的内存映射 - 41 -
7.7 HELLO进程EXECVE时的内存映射 - 42 -
7.8 缺页故障与缺页中断处理 - 42 -
7.9动态存储分配管理 - 43 -
7.10本章小结 - 45 -
第8章 HELLO的IO管理 - 46 -
8.1 LINUX的IO设备管理方法 - 46 -
8.2 简述UNIX IO接口及其函数 - 46 -
8.3 PRINTF的实现分析 - 47 -
8.4 GETCHAR的实现分析 - 47 -
8.5本章小结 - 47 -
结论 - 48 -
附件 - 49 -
参考文献 - 50 -

第1章 概述
1.1 Hello简介
Hello的P2P(From Program to Process)过程:
hello.c 是使用c语言,在ide或者txt中编写的文本文件,然后使用cpp进行预处理,把#include的文件写入文件,并且进行宏替换,将C的源程序hello.c翻译成一个ASCII码的中间文件,生成hello.i。然后通过编译器ccl,将中间文件翻译成一个ASCII汇编语言文件,生成汇编程序hello.s。然后通过汇编器as汇编成可重定位目标文件hello.o。最后通过链接器ld链接生成hello可执行文件。然后会在shell中fork进程,最终完成了P2P过程。
Hello的O2O(From Zero-0 to Zero-0)过程:
Hello的O2O过程:shell执行可执行目标文件,对进程进行管理,然后对储存进行管理,映射虚拟内存,分配物理内存,最后将hello的结果输出的显示器上,结束进程,释放内存空间。
1.2 环境与工具
硬件环境:Intel(R)Core(TM)i7-9750 CPU, 2.9GHz;8GB(RAM);X64CPU, 1T SSD固态。
软件环境:Windows10 64 位;VirtualBox;Ubuntu 20.04;
工具:VS;codeblocks;vscode,gdb,edb;Objdump;Hex Editor Neo,CPUZ。
1.3 中间结果
1、hello.i:经过预处理的文件,进行了宏替换以及将#include的内容添加了进去。
2、hello.s:经过编译生成的汇编文件。
3、hello.o:汇编生成的可重定位的文件。
4、hello:链接器产生的可执行目标文件,用于分析链接的过程。
5、hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
6、hellold.txt:hello的反汇编文件,用于分析可执行目标文件hello。
7、helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
8、helloldelf.txt:hello的ELF格式,用于分析可执行目标文件hello。
1.4 本章小结
本章主要是对hello程序从020,以及p2p的过程简介。主要介绍了预处理、编译、汇编、链接的过程,以及各步骤功能的大致介绍。我们还介绍了进程在执行时shell和操作系统的行为。本章节从一个大的全局层面来概括一个程序的出生到结束,并且列举了本次作业中全部中间结果。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理概念:
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
预处理作用:
最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase) ,通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令 (preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
命令:

  1. gcc hello.c -E -o hello.i
    预处理截图如下所示:

图2.2.1预处理命令截图
文件建立结果如下:

图2.2.2文件目录
2.3 Hello的预处理结果解析
.c源文件中含有以下三个库:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
进行预处理后,这些库中的内容会复制进来,如下图所示:

图2.3.1文件目录以及hello.i
2.4 本章小结
本章讲述了hello程序生命周期中预处理阶段,预处理器以#开头的预处理命令,完成将对应的头文件插入程序文本中等任务。由于代码中没有宏常量,因此没有进行宏替换等任务。生成的hello.i仍旧是文本文件,将被传递给编译器进行下一阶段。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:
编译之前,C语言编译器会进行词法分析、语法分析(-fsyntax-only),接着会把源代码翻译成中间语言,即汇编语言。
编译的作用:
进行词法分析和语法分析。并且会进行代码优化,生成汇编代码。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
命令如下所示:

目录如下:

图3.1.1文件目录
Hello.c截图:

图3.1.2hello.c
Hello.i截图:

图3.1.3 hello.i
3.2 在Ubuntu下编译的命令
命令:

图3.2.1 命令
文件目录如下所示:

图3.2.2 文件目录
Hello.s展示如下:

图3.2.3 hello.s
3.3 Hello的编译结果解析
汇编程序文件内容:
.file 声明源文件
.text 声明代码段
.data 声明数据段
.section .rodata 只读数据,rodata节
.globl 全局变量
.size 声明大小
.type 指定类型
.align 声明对指令或数据的存放地址进行对齐的方式

3.3.1 数据类型
1、局部变量:
int i作为局部变量,并不占用文件的实际节的空间,一般通过栈或寄存器存储。对其的访问,也是对栈或者寄存器的访问。如图,我们看到局部变量放在了寄存器-4(%rbp)中。

图3.3.1 局部变量

字符串:LC0和LC1存储了程序打印的两个字符串,形成全局变量。

图3.3.2 字符串存储
Argv数组:从L4中调用-32(%rbp)看出这是在调用argv数组的四个参数。

图3.3.3传参
其他数字以立即数形式出现。
3.3.2 赋值
赋值语句中,以i=0为例。实现方式就是movl,如图所示。因为是int,因此是32位,用的是l,截图如下:

图3.3.4赋值

3.3.3 算术操作
例如i++,从汇编角度看,使用addl实现,如图所示。使用l的原因也是因为计算了32位。

图3.3.5算术操作

3.3.4 关系操作
1、比较参数argc是不是为4,如图所示。如果等于则跳到L2。

图3.3.6关系操作

2、比较i是否满足<8,如图所示。如果小于等于7则跳到L4。

图 3.3.6 比较i和8

3.3.5 数组/指针/结构操作
1、数组有argv[],存储着命令行输入的参数。
2、指针有argv,是一个二维数组。argv的每个元素又是一个指针。

3.3.6 控制转移
1、if:在main部分的if(argc!=4)中,用cmpl和后面的je共同来实现。Cmp比较的结果,如果相等则跳转L2,反之则继续进行。

图 3.3.7 if
2、for:for(i=0;i<8;i++),如图所示,用cmpl和7作比较,如果小于等于则跳转L4,在L4的末尾有add $1的操作,进行更新,从而实现了for循环。

图 3.3.8 for
3.3.7函数操作
1、main()函数:
参数传递:edi保存argc,rsi保存argv
函数调用:用.main:
返回:movl $0, %eax,ret,也就是return 0。
2、printf()函数:
参数传递:第一次:leaq .LC0(%rip), %rdi,放入rdi中;
第二次:.LC1放入%rdi, 姓名放在 %rdx,学号放在 %rsi
函数调用:call puts@PLT
3、exit()函数:
参数传递:将%edi 设置为 1。
调用函数:call exit@PLT
4、atoi()函数:
参数传递:将%rdi 设置为 argv[3]。
调用函数:call atoi@PLT
返回:返回值存储在%eax
5、sleep()函数
参数传递:将%edi 设置为 经过atoi处理后的argv[3]。
调用函数:call sleep@PLT
6、getchar()函数
调用函数:call getchar@PLT
3.4 本章小结
本章学习了编译器是如何将C语言程序转化为汇编语言代码的,并且详细介绍了编译器对各种数据与操作的处理。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编器(Assembler)将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。.o文件是一个二进制文件,其包含程序的指令编码。

汇编的作用:
汇编器(as)首先检查汇编程序(.s文件)语法的正确性,若正确,则将其翻译成与之等价的机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件(.o二进制文件)中。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
截图如下所示:

图4.2.1命令
形成文件目录如下图:

图4.2.2文件目录
4.3 可重定位目标elf格式
首先,利用如下命令获取elf:
readelf -a hello.o
命令行中如下所示:

  1. elf头
    首先是用于生成该文件的系统的字的大小和字节顺序的一个 16 字节的序列Magic。剩下部分包含帮助链接器进行语法分析和解释目标文件的信息,其中包括类别,数据,版本,OS/ABI版本,入口点地址,程序头起点等。

图4.3.1elf头
2. 节头
Hello.o是可重定位目标文件,每个节都从0开始,用于重定位。节头包含了文件中出现的各个节的语义,包括节的名称,类型,地址,偏移量,大小,旗标,链接,信息,对齐等信息。
图4.3.2节头
旗标等其他信息:

图4.3.3旗标
3. 重定位节:
在rela.text里面有我们的重定位条目,这个条目能告诉链接器目标文件合并成可执行文件时如何修改引用。包含了需要被修改的引用节的偏移量、信息、重定位的类型、符号值和重定位需要对被引用值的偏移调整量等。

图4.3.4重定位
4. 符号表
符号表存放在程序中定义和引用的函数和全局变量的信息。其中,name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字;对于可定位目标文件来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行的地址。size是目标的大小(以字节为单位),bind字段表明符号是本地的还是全局的。Ndx段表示存储在什么段。这里给出的有伪节UNDEF未定义段,ABS段,还有用数字表示的一些比如text,data,rodata,bss等。未出现的还有例如COMMON段等。

图4.3.5符号表
4.4 Hello.o的结果解析
解析过程用的命令为:objdump -d -r hello.o
命令行运行结果如下所示:

图4.4.1hello.o
在hello.s和hello.o之间比较,汇编器(as)在汇编hello.s时:

  1. 为每条语句加上具体的地址,全局变量和常量都被安排到具体的地址里面。
  2. 操作数在hello.s都是十进制,在hello.o当中都是机器级程序所要的十六进制。
  3. 跳转语句对应的符号变成相对偏移地址。
  4. 函数调用的函数名字变成函数的相对偏移地址。
    Hello.s 的文件如下图所示:

图4.4.2hello.s

图4.4.3hello.s
4.5 本章小结
汇编器将汇编代码处理成机器可以看懂的机器码,也就是二进制代码。二进制代码较汇编代码来说,虽然可读性变得比较差,但是在执行效率方面有了非常大的提升,汇编代码虽然已经在原来的文本的基础上进行了优化,但是还是存在着一些字符等不能够直接处理的数据。但是二进制代码中,已经将所有的指令、函数名字等量变成了相应的存储地址,这样机器就可以直接读取这些代码并执行。所以总的来说hello.o已经非常接近一个机器可以执行的代码了。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是通过链接器(ld)将各种代码和数据片断收集并组合成一个单一文件的过程。这个文件可以被加载(复制)到内存并执行。

链接的作用:
因为有了链接这个概念的存在,所以我们的代码才回变得比较方便和简洁,同时可移植性强,模块化程度比较高。链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以 把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并并重新链接应用,而不必重新编译其他文件。
作为编译的多后一步链接,就是处理当前程序调用其他模块的操作,将该调用的模块中的代码组合到相应的可执行文件中去。
5.2 在Ubuntu下链接的命令
命令如下图所示,将需要的各种收.o文件链接上去即可,注意链接顺序。

图5.2.1 命令行
当前文件目录如下图所示:

图5.2.2文件目录
5.3 可执行目标文件hello的格式
利用命令readelf -a hello.o查看:
Elf头如下:

图5.3.1hello可执行
节头如下:

图5.3.2节头
重定位节:

图5.3.3重定位节
5.4 hello的虚拟地址空间
使用edb的symbolviewer查看符号表,如下所示:

图5.4.1edb查看

图5.4.2edb继续查看
然后与上一节中的节头相对比,发现: 对应位置的信息完全相同,并且edb还比普通的readelf得到的指令多了很多关于位置的信息。然后通过查看datadump,我们也可以看到,与elf头部的magic序列相同,都是记录关于文件的信息,是:

5.5 链接的重定位过程分析
hello的重定位记录有两种,分别是PC相对地址的引用和绝对地址的引用,如下图所示:不带PLT的是绝对寻址,带PLT的是相对寻址。

图5.5.1寻址模式
进行重定位时,hello根据.rela.text和.rela.data中的重定位记录,在.symtab中查找需要修改的记录的符号,并结合符号与重定位记录中的位置信息对目标位置进行 修改。如果需要修改的符号是本地符号,则计算偏移量并修改目标位置;如果是共享库中的符号,则创建.got表项(如果是函数还需创建.plt项),并创建新的重定位记录指向.got表项。
我们列举出以下的多出来的节:
.interp:保存ld.so的路径
.gnu.hash:GNU拓展的符号的哈希表
.dynsym:运行时/动态符号表
.rela.dyn:运行时/动态重定位表
.plt:动态链接-过程链接表
.rela.plt:.plt节的重定位条目
.init:程序初始化需要执行的代码
.fini:当程序正常终止时需要执行的代码
.dynamic:存放被ld.so使用的动态链接信息
.got:动态链接-全局偏移量表-存放变量
.got.plt:动态链接-全局偏移量表-存放函数
图片对比如下:
用objdump -d -r查看如下:

图5.5.1objdump用-d -r
用-d 命令重定向后的文件少了很多内容,大小大小所示:

文件内容如下所示:

图5.5.2 普通-d
5.6 hello的执行流程
利用edb,查看相关内容如下所示:

图5.6.1 edb查看节表
5.7 Hello的动态链接分析
目前来说,Linux 下ELF 主要支持两种方式:加载时符号重定位及地址无关代码。地址无关加载时重定位与链接时重定位是一致的,只是把重定位的时机放
到了动态库被加载到内存之后,由动态链接器来进行。动态链接与静态链接时的几个关键的不同之处:1.因为不允许对可执行文件的代码段进行加载时符号重定位,因此如果可执行文件引用了动态库中的数据符号,则在该可执行文件内对符号的重定位必须在链接阶段完成,为做到这一点,链接器在构建可执行文件的时候,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的内存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域。2.ELF 文件对调用动态库中的函数采用了所谓的"延迟绑定"(lazy binding)策略, 只有当该函数在其第一次被调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态库和可执行程序中的相应代码都不需要进行了。
延迟绑定的实现步骤如下:1.建立一个GOT.PLT 表,该表用来放全局函数的实际地址,但最开始时,该里面放的不是真实的地址而是一个跳转。2.对每一个全局函数,链接器生成一个与之相对应的影子函数。3.所有对fun 的调用,都换成对fun@plt 的调用。

图5.7.1got.plt
在节头表中找到GOT.PLT,起始位置是0x404000。

图5.7.2 edb查看
我们发现在ld-2.29.so!_dl_start 之前这里都是00,在ld-2.29.so!_dl_start 之后这里发生里变化。

图5.7.3edb进一步查看
5.8 本章小结
第五章是生成可执行文件的最后一步-链接,将多个可重定位文件链接生成一个可执行文件。涉及了重定位和符号解析,对重定位有了更深刻的理解。以及静态链接动态链接过程,深化了动态链接和共享库的应用方式。本章讨论了hello的链接过程。链接过程可以发生在编译时,也可以发生在加载时,甚至可以发生在程序执行时。静态链接直接将目标文件和库文件打包至一个可执行文件中,而动态链接则只在可执行目标文件中添加相应重定向记录,并通过GOT表项和延迟绑定的方法实现对目标模块中符号的引用。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
作用:进程提供给应用程序两个关键抽象:
逻辑控制流
a) 每个程序似乎独占地使用CPU
b) 通过OS内核的上下文切换机制提供
私有地址空间
a) 每个程序似乎独占地使用内存系统
b) OS内核的虚拟内存机制提供
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型应用程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)
其基本功能是解释并运行用户的指令,重复如下处理过程:
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。
3.检查第一个(首个,第0个)命令行参数是否是一个内置的shell命令。
4.如果不是内部命令,调用fork()创建新进程/子进程。
5.在子进程中,用步骤2获取的参数,调用execve()执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回
7.如果用户要求后台运行(命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
在bash中输入 ./hello 1190201313 并敲击回车后,bash解析此条命令,发现./hello不是bash内置命令,于是在当前目录尝试寻找并执行hello文件。此时bash使用fork函数创建一个子进程(这个子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本),并更改这个子进程的进程组编号。并准备在这个子进程执行execve。
运行结果如下图所示:

图6.3.1 命令行测试
6.4 Hello的execve过程
在fork子进程之后,execve函数调用内存中的启动加载器来执行hello程序,加载器删除子进程现有的虚拟内存段, 并创建一组新的代码、数据、堆和栈段。
通过书上的图例进行说明:

图6.4.1查看栈帧
最后,加载器设置PC指向_start地址,_start最终调用hello中的main函数。
6.5 Hello的进程执行
在输入合适参数执行hello程序之后,hello进程一开始运行在用户模式。内核为hello维持一个上下文,它由一系列的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(比如页表、进程表、文件表)。
在hello运行时,也有一些其它进程在并行地运行,这些进程的逻辑流的执行时间与hello的逻辑流重叠,称为并发流。而一个进程和其它进程轮流运行的概念叫作多任务,一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
hello初始运行在用户模式,不久后调用sleep函数,定时器开始计时。Sleep函数使进程后陷入内核模式,内核处理休眠请求主动释放当前进程,并调度其他进程。此时需要先进行上下文切换:
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文
3)将控制传递给这 个新恢复的进程
整个过程在内核模式下完成。完成上下文切换后,新的进程以用户模式进行。
当定时器到时时发送一个中断信号,再次进入内核模式执行中断处理。产生中断的例子如下所示:

图6.5.1中断查看
完成上下文切换后,hello进程进入用户模式继续向下执行,直到再一次调用其他函数,执行流程与上述类似。
具体过程如下图所示:

图6.5.2 上下文切换
接下来的十秒中,内核继续执行上下文切换,轮流运行hello与其它进程,十次循环结束后,hello返回,程序终止。
6.6 hello的异常与信号处理
正常执行,如下所示:

图6.6.1正常执行
用ps查看如下:

图6.6.2 ps查看
输入ctrl-Z结果如下:

图6.6.3ctrl-z输入查看
该操作向进程发送了一个SIGSTP信号,将进程挂起,但输入ps命令我们可以看到进程未被回收。我们可以用fg 命令将其调至前台继续执行直至结束并被回收。

图6.6.4ps查看
输入ctrl-C,结果如下:

图6.6.5 ctrl-c输入
这样会向进程发送一个SIGINT信号,进程处理后,结束hello并将其回收。
输入ctrl-Z时候输入pstree查看如下:

图6.6.6 查看树形图
乱按:
无影响,字符会显示:

图6.6.7乱按
6.7本章小结
本章主要介绍了进程的定义与作用、shell 的一般处理流程,调用fork创建新进程的过程、execve函数的过程、hello的异常与信号处理等内容。首先对进程有了概念–一种符合我们认知的假象。然后了解了进程的建立,运行,终止。通过hello进程的演示,有了更深的理解。了解了异常的类型和产生的原因,通过对异常了解,进程有了更深刻的概念。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:即段地址:偏移地址。实模式下: 逻辑地址CS:EA =物理地址CS*16+EA;保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。
线性地址:非负整数地址的有序集合
虚拟地址: N = 2n 个虚拟地址的集合 =线性地址空间
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。
段式管理: 逻辑地址->线性地址
虚拟地址
页式管理: 虚拟地址->物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址是由段地址和偏移地址两部分
段标识符是16位长,也叫段选择符,我们先看T1字符是0/1确定要转移的是GDT中的段还是LDT中的段,再根据指定的寄存器的地址和大小得到数组。再拿出段选择符的前13位,在数组中查找到对应的段描述符,可得基地址,再结合段内偏移量获得线性地址。如下图:

图7.2.1地址
1.TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
2. RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位
于最低级的用户态,第0级高于第3级。
3.高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。
接下来是段描述符:段描述符是一种数据结构,实际上就是段表项,分两类:
1.用户的代码段和数据段描述符
2.系统控制段描述符,又分两种:
(1)特殊系统控制段描述符,包括:局部描述符表(LDT)描述符和任务状态段(TSS)描述符
(2)控制转移类描述符,包括:调用门描述符、任务门描述符、中断门描述符和陷阱门描述符
描述符表实际上就是段表,由段描述符(段表项)组成。有三种类型
1.全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段
2.局部描述符表LDT:存放某任务(即用户进程)专用的描述符
3.中断描述符表IDT:包含256个中断门、陷阱门和任务门描述符

一个完整的逻辑地址是段选择符:偏移地址,先了解当前转换时GDT的段还是LDT的段,根据寄存器得到地址和大小,然后得到一个组。再拿出段选择符前13位,找到对应段描述符,即可知道基址地址。队友把基址地址和得到数组结合,就得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
形式上来说,地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素之间的映射。MMU实现了这种线性地址到物理地址的变换,它是利用页表来实现的。CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分:一个P位的虚拟页面偏移,和一个n-p位的虚拟页号。MMU利用VPN来选择适当的PTE。例如VPN0选择PTE0,VPN1选择PTE1,以此类推。将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移和VPO是相同的。如下图所示:

图7.3.1地址变换
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址VA被分成VPN和VPO两部分,VPN被分为TLBT和TLBI用于在TLB中查询。变换地址时,CPU产生一个虚拟地址,MMU从TLB中取出相应的PTE。如果命中,则得到对应的物理地址。如果不命中,VPN会被分成4个部分。MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成 PA,并且向TLB中添加条目

图7.4.1页表变换
7.5 三级Cache支持下的物理内存访问
MMU发送物理地址PA给L1缓存,L1缓存从物理地址中抽取出缓存偏移CO、缓存组索引CI以及缓存标记CT。高速缓存根据CI找到缓存中的一组,并通过CT判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则继续从L2、L3缓存中查询,若仍未命中,则从主存中读取数据。

图7.5.1物理内存访问
7.6 hello进程fork时的内存映射
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
当用fork创建一个进程时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、vm_area_struct和页表的原样副本。然后将这两个两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello的步骤如下:
1.删除当前的用户区域
2.映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。
3.映射共享区域:hello程序与标准C库链接,这些对象动态链接到这个程序,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。execve做得最后一件事情是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

图7.7.1栈帧

7.8 缺页故障与缺页中断处理
缺页故障:虚拟内存中的字不在物理内存中(DRAM缓存不命中)
如下图,VP3已经被映射到页表中,但却没有被缓存到物理内存中,此时堆VP3的引用会引发缺页故障。

图7.8.1缺页故障
缺页会导致页面出错引发一个缺页中断,而缺页异常处理程序会选择一个牺牲页(如下图选择了VP4,将VP4从内存交换到磁盘,并从磁盘读取VP3交换到物理内存)。

图7.8.2映射
此时令导致缺页的指令重新启动,就可以使得页面命中了。
7.9动态存储分配管理
一.动态内存分配器维护着一个进程的虚拟内存区域,称为堆。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应程序使用。空闲块可用来分配。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
1.显式分配器,要求应用显式地释放任何已分配的块。
2.隐式分配器,另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
二.malloc函数
malloc函数返回一个指针,指向大小至少为size字节的内存块,这个块可能包含在这个块内的任何数据对象类型做对齐。
1.隐式空闲链表
任何实际的分配器都需要一些数据结构,允许它来区别块边界。大多数分配器将这些信息嵌入块本身,如下图:

图 7.9.1隐式空间链表
假设块的格式如上图所示,我们可以将堆组织为一个连续的已分配块和空闲块的序列,如下图:

图7.9.2 内部结构
称这种结构为隐式空闲链表。
2.放置已分配的块
分配器执行这种搜索的方式是由放置策略确定的。一些常见的策略是首次适配,下一次适配和最佳适配。
(1)首次适配从头开始搜索空闲链表,选择第一个合适的空闲块
(2)下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一次查询结束的地方开始。
(3)最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
3.分割空闲块
一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,那就是分配这个空闲块中多少空间。一个选择是用整个空闲块。虽然这种方式简单而快捷,但是主要的缺点就是会产生内部碎片。
4.获取额外的堆内存
如果分配器不能为请求块找到合适的空闲块,一个选择是合并。另一个选择就是调用sbrk函数,向内核请求额外的堆内存。
5.合并空闲块
当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻。这些邻接的空闲块可能引起一种现象,叫做假碎片。为了解决这个问题,任何实际的分配器都必须合并空闲块这个过程称为合并。
下面介绍一下带边界标记的合并:

图 7.9.3显示空间链表
如上图,在每个块的结尾处添加一个脚部, 其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断当前一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
6.显示空闲链表
一种更好的方式是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred和succ指针
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于我们所选择的空闲链表中块的排序策略。
一种方法是后进先出的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。
另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于按照地址排序二点首次适配比LIFO排序的首次适有更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章主要介绍了hello的存储器地址空间、段式管理和页式管理,以intel Core7 为例介绍了VA到PA的变换、物理内存访问,以及进程fork、execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理等
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux 文件就是一个m 个字节的序列:B1,B2,…,Bk,…,Bm-1。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO 和STDERROR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。
读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k开始,然后将k 增加到k+n。给定一个大小为m 字节的文件,当k>=m 时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在
文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。
ssize_t read(int fd, void *buf, size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制之多n个字节到描述符fd的当前文件位置。
DIO *opendir(const char *name);
函数opendir以路径名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。
struct dirent readdir(DIR dirp);
每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更过目录项则返回NULL。
int closedir(DIR dirp);
函数closedir关闭流并释放其所有的资源。
关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.3 printf的实现分析
首先printf函数中va list arg = (va_ list)(char
)(&fmt)+ 4);我们知道typedefchar
va_list,所以说(char
)(&fmt)+4)表示的是…中的第一个参数的地址。vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt.用格式字符串对个数变化的参数进行格式化,产生格式化输出。就是打印出来的字符串的长度。write是给几个寄存器传递了几个参数,然后-一个int结束。sys_ _call 就实现一个功能:显示格式化了的字符串。字符显示驱动子程序:从ASCII到字模库到显示vram (存储每- - 个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成ASCII码,保存到系统的键盘缓冲区之中。getchar等调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。
getchr函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章阐述了Linux 的IO 设备管理方法、简述Unix IO 接口及其函数、printf 的
实现分析、getchar 的实现分析。
(第8章1分)
结论
hello的一生主要经历了如下阶段:

  1. 键盘输入程序,得到hello.c源文件,hello诞生了
  2. 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始C
    程序,生成hello.i。
  3. 编译阶段:编译器(ccl)将文本文件hello.i 翻译成文本文件hello.s,它包含一个汇编语言程序。
  4. 汇编阶段:接下来,汇编器(as)将hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文
    件hello.o 中。
  5. 链接阶段:连接器(ld)合并标准C 库和hello.o 生成可执行文件hello。
  6. 运行程序:在终端输入./hello 1190201313 tangyafeng 2, 2是时间。shell进程调用fork为hello创建一个子进程,随后调用execve启动加载器,加载映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
  7. 执行:CPU为其分配时间片,在一个时间片中hello享有CPU资源,顺序执行自己的控制逻辑流。
  8. 内存:MMU 将程序中使用的虚拟内存地址通过页表映射成物理地址。printf 会调用 malloc 向动态内存分配器申请堆中的内存。
  9. 进程结束:shell父进程回收子进程。
    体验:通过学习本课程,以及进行这个大作业的编写,让我对于csapp有了刻骨铭心的掌握以后日常生活中想问题也能通过这种系统层面进行思考了,感谢老师。
    (结论0分,缺失 -1分,根据内容酌情加分)

附件
Hello.o, hello.s, hello.i, hello.c ,hello
(附件0分,缺失 -1分)

参考文献
[1].《深入理解计算机系统》[J].科学中国人,2018(09):69.
[2]刘江.深入理解计算机之道[J].程序员,2006(09):130-131+10.
(参考文献0分,缺失 -1分)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值