CSAPP大作业

在这里插入图片描述

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 xxxx
班   级 xxxx
学 生 xxxx    
指 导 教 师 xxxx

计算机科学与技术学院

2022年5月

摘 要

本文将分析hello程序从产生到运行时终止的全部过程,给出在执行每一步操作时计算机系统的操作方式,来展现hello程序从开始到结束的生命历程。旨在通过hello程序的分析,更加深入地理解计算机系统各个部分地运作方式,使读者能够从一个整体的角度来把握计算机的工作机制与功能实现手段。
关键词:hello;计算机系统;进程管理;处理器体系结构;存储器体系结构;信息的存储表示

第1章 概述

1.1 Hello简介

Hello程序麻雀虽小,五脏俱全,其从产生,执行到回收具备一般计算机程序的一般流程,体现了计算机系统设计的伟大构思方法。
P2P:from program to process
 Program:开发者在IDE或普通editer中按高级语言语法键入的程序
 Process:以C语言为例,源代码文件hello.c经过cpp预处理器预处理,ccl编译器编译,as汇编器汇编,再在ld中进行连接,最终成为相应平台下的可执行文件,在linux下的shell/bash下键入之,shell将fork ,exerve加载程序成为process进程。
020:from Zero-0 to Zero-0
 Zero-0:意思是,Hello成为一个实例(进程)时,由OS为其映射虚拟内存空间,程序载入物理内存。进入主函数后执行代码,CPU为进程分配时间片,执行逻辑控制流。运行结束后,父进程shell负责回收hello占用的资源,实现由0最终回到0

1.2 环境与工具

硬件环境:处理器:AMD Ryzen 5 3500U with Radeon Vega Mobile x86_64架构;RAM :8.00GB
软件环境:windows10 64位,ubuntu20.04 64位
工具:Visual Studio 2019 ,CodeBlocks,Mingw64,gcc,objdump,gdb,Oracle VM VirualBox,edb

1.3 中间结果

文件名 文件作用
hello.i hello.c预处理后生成文件
hello.s hello.i编译后生成的汇编文件
hello.o hello.s经过汇编过后生成的可重定向文件
hello hello.o经过链接之后生成的可执行文件
hello.elf hello的elf格式文件
hello_o.elf hello.o的edf格式文件
dhello_.s. hello.o经过反汇编后生成的反汇编文件
dhello.s. hello经过反汇编后生成的反汇编文件

1.4 本章小结

本章初步介绍了hello程序的产生,执行与回收,介绍了p2p与020过程,展示了本次实践所使用的软硬件工作环境与各种工具,列举了下面所要使用的文件,为下面的进一步细说创造了条件。

第2章 预处理

2.1 预处理的概念与作用

预处理器(C Preprocessor)对源程序进行直接修改,本质上是一个文本替换工具,识别以字符#开头的命令对代码进行处理,常见的命令有
命令 功能 预处理器行为
#define 定义宏 对其下面的代码段进行替换
#include 声明头文件 根据头文件的读取文件内容直接插入代码段
#undef 取消宏 不再对下面代码段实行替换
#if,#else,#elif,#endif 指示预处理器条件编译 选择性保留符合条件的语句块以编译
#ifdef #ifudef #else,#elif #endif#…defined 判断宏是否未定义的条件编译 根据是否定义,选择是否保留下面语句块以编译
#error 指示直接输出标准化错误(相当于输出函数) 当遇到标准错误时,输出错误消息
#pragma 对特定代码块指示特殊编译命令 较为复杂,部分命令还会保留到编译阶段处理,向编译器发布特殊命令
#line 指定行号,文件名 执行所给的指定
同时预处理器支持预定义宏,如__DATE__(当前时间),FILE(当前文件名),以及一些预处理器运算符,incline内联函数同样会在此时被处理

2.2在Ubuntu下预处理的命令

cpp hello.c > hello.i
gcc -E hello.c -o hello.i
gcc -m64 -no-pie -fno-PIC hello.c -E -o hello.i
在这里插入图片描述

2.3 Hello的预处理结果解析

以stdio.h为例,可以发现预处理之后,不再包括它以及任何宏常量,而是进行了递归性的展开与宏常量的代入,直到不再含有任何对头文件的include关键字与宏常量的声明等等。
在这里插入图片描述在这里插入图片描述

打开头文件stdio.h可以发现大量#define,以及#if defined语句,而这些语句在被引入时继续替换或递归展开,可以发现最后得到代码中是不再有这些语句的,并且按照语句要求对代码进行了相应处理。在这里插入图片描述

2.4 本章小结

本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念和应用功能,以及Ubuntu下预处理的两个指令,同时具体到我们的hello.c文件的预处理结果hello.i文本文件解析,详细了解了预处理的内涵

第3章 编译

3.1 编译的概念与作用

编译器(ccl)通过对后缀为.i的文本文件进行词法分析与语法分析,确认所有指令都符合语法规则之后,将翻译成为后缀为.s的文本文件,得到一个汇编语言程序。
编译的基本流程:

  1. 语法分析:编译程序含有语法分析器,把单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,这是通过自上而下分析法或自下而上分析法实现的;
  2. 中间代码:源程序的一种内部表示,或者称为中间语言。中间代码的作用是可以是编译后程序的结构在逻辑杀昂更为简单明确,特别是可以是优化更易实现
  3. 代码优化:指对程序进行多种等价变换使从变换后的程序出发,能生成更有效的目标代码
  4. 目标代码:编译的最后阶段,目标代码生成器把上述步骤结束后生成的中间代码变换成目标代码(汇编语言代码)
    这些步骤为下一步汇编器将程序转换为二进制机器码奠定了基础。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s
gcc -m64 -no-pie -fno-PIC hello.i -S -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析

在这里插入图片描述
在这里插入图片描述

3.3.1数据
3.3.1.1. 常量

hello.s中对常量的处理分为两类:整型常量与字符串常量。
整型常量被汇编程序指示存储在.text节中,属于是可读不可写的,但在汇编程序中,4,1,0,8等常量是直接出现在指令中的在这里插入图片描述
常量4的直接使用
而字符串常量则被存储在.rodata节中
在这里插入图片描述

在下文中的汇编指令中以标签代指。

3.3.1.2. 变量

全局变量
全局变量存储在.data节中,不需要汇编语言完成初始化。hello程序中貌似没有全局变量
局部变量
局部变量存储在寄存器中或栈中,举例来说
在这里插入图片描述

可以在以下汇编代码中找到i的特征(循环变量),发现变量存储在-4(%rbp)位置(也在栈中)
在这里插入图片描述

3.3.2 算术操作
helloworld程序中使用了addl操作,用来对循环变量执行反复加一的操作
每次操作后-4(%rbp)地址处(是栈内)数值加一

在这里插入图片描述

3.3.3 关系操作与控制转移
hello程序使用了!=与<=这两种关系,均可以转换为条件码指令操作cmpl

在这里插入图片描述
在这里插入图片描述

其意义是按照更具后操作数与前操作数作差的结果更改条件码
控制转移操作有je,jle,jmp两种,前两者都依赖于之前cmpl产生的条件码。
第一个执行if(argc!=4)跳转,后两个共同完成for(i=0;i<8;i++)跳转
在这里插入图片描述
在这里插入图片描述

3.3.4 数组/指针/结构操作

hello程序涉及到指针数组char argv[]
main函数的开头部分有
注意到左边划线段的mov操作,结合main函数传参顺序可判断
argv[]数组先保存在%rsi处后放在了%rsp(%rbp-32)处.
结合printf前对argv的访问与原函数,也可以判断出argv[1]存储在%rbp-16(也即%rsp+16)处,argv[2]存储在%rbp-24(也即%rsp+8)处,

在这里插入图片描述
在这里插入图片描述

3.3.5 函数操作

X86-64寄存器与函数相关的寄存器功能表如下。
在这里插入图片描述

由此展开对函数的分析:
main:
由main函数声明(源码):
int main(int argc, char *argv[])
可知argc在%rdi中,*argv[]首地址在%rsi中
printf&puts:
由printf的使用方法(源码)确定参数寄存器在这里插入图片描述


在这里插入图片描述

到相应寄存器寻找函数参数,结果如下
在这里插入图片描述
在这里插入图片描述

printf被换为puts以简化,%rdi传入的应该是右图变量的地址(以后分配)。
在这里插入图片描述在这里插入图片描述

由上面对此处访问argv的分析可知%rdi为输入格式串,%rsi为argv[1],%rdx为argv[2]
exit:
由源代码
在这里插入图片描述

	传入参数为1,放置于%rdi,可锁定代码位置
	 
**sleep:**
	源代码:
	 
	发现传入的atoi(argv[3])应该在%rdi中
需要事先将argv[3]的值放入%rdi中,调用atoi函数,再将返回值%rax传递给%rdi,再调用sleep

在这里插入图片描述

getchar:
源代码:
在这里插入图片描述

无输入,也不保存输出

在这里插入图片描述

3.4 本章小结

本章主要介绍了编译的概念以及过程。同时表现了c语言所转换成为的汇编代码。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。编译程序所做的工作,就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码表示。包括之前对编译的结果进行解析,都令我更深刻地理解了C语言的数据与操作,对C语言翻译成汇编语言有了更好的掌握。因为汇编语言的通用性,这也相当于掌握了语言间的一些共性。

第4章 汇编

4.1 汇编的概念与作用

汇编,即是通过汇编器(as)将汇编语言程序翻译成机器语言程序的过程,同时将.s文本文件转化为.o二进制文件,即是可重定位目标文件。
汇编真正将之前所产生的非机器语言程序转换为等效的由可直接识别的命令所组成的二进制文件,并且将这些指令集中打包形成了可重定位目标程序的格式。

4.2 在Ubuntu下汇编的命令

as hello.s -o hello.o
gcc -c hello.s -o hello.o
gcc -m64 -no-pie -fno-PIC hello.s -c -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

4.3.1使用命令:
readelf -a hello.o > ./hello_o.elf

在这里插入图片描述

4.3.2 ELF头:
ELF头包含的信息包括系统信息,编码方式,ELF头大小,节的大小于数量等等一系列信息。内容如下:

在这里插入图片描述

4.3.3节头目表:
节头目表描述了.o文件中出现的各个节的类型,位置,所占空间等信息。

在这里插入图片描述

4.3.4重定位节:
表述了各个段引用的外部符号等,这些节或符号的位置是在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。  

在这里插入图片描述

4.3.5符号表:
Symbol table 符号表存放了程序中定义与引用的函数与全局变量的相关信息

在这里插入图片描述

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o >dhello_o.s
在这里插入图片描述

分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

  1. 机器语言中数的表示使用16进制而不是汇编中的10进制
  2. 机器语言中采用相对偏移地址(间接地址)进行跳转而不仅仅像汇编程序只是标签符号
  3. 函数调用时也采用相对偏移地址。
    说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章详尽介绍了汇编结果。汇编语言经过汇编器转化为机器语言,为后面的链接做了准备。对比了hello.s和hello.o反汇编代码的区别,,对可重定位目标elf格式进行了详细的分析。同时对hello.o文件进行反汇编,将dhello.s与之前生成的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.3 可执行目标文件hello的格式

命令:readelf -a hello > hello.elf
在这里插入图片描述

ELF文件头:

各段的基本信息如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4 hello的虚拟地址空间

在这里插入图片描述在这里插入图片描述

在这里插入图片描述

这一段内存包括Section Headers 表中一直到rel.plt的部分。

在这里插入图片描述
变为在这里插入图片描述

可知.init,.plt,.plt.sec,.text,.fini,.rodata,.eh_frame,.dynamic,,.got.plt,.data在其中。

5.5 链接的重定位过程分析

(以下格式自行编排,编辑时删除)
objdump -d -r hello > dhello.s
不同:

  1. dhello_o.s中只有对.text节的分析,而dhello.s中还含有对.init,.plt,.plt.sec,.fini段的分析。
  2. dhello_o.s中代码行首是针对main的相对偏移量,而dhello.s中代码段行首已经被映射到了虚拟内存空间。函数调用也完全依赖于向地址的跳转而不是
  3. 在新出现的.plt.sec节中发现了printf,sleep等C库函数,.text节出现了start一类的新函数。
    链接过程综述:
  4. 链接引用各种.o文件,静态库中的.o文件以及动态库,保证所有的符号都能在这些文件中找到定义。(有动态库,创建.dynamic节和动态连接结构表)
  5. 链接器根据原先各个文件中编译器生成的.rel.text,.rel.data等信息,先填充各个跳转调用的待补全位置,然后将原先的外部引用符号的地址引入各个代码段,从而将各个文件中的相应节合并(典型的三段:数据段,代码段,符号段(动态库保存只保存引用所需))。

5.6 hello的执行流程

(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
跳转主要流程如下:
在这里插入图片描述

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
查询符号表发现其位置
在这里插入图片描述

然后调试程序

在这里插入图片描述
在这里插入图片描述

可以看到在进出后,.got(global offset table)发生了变化,进行了初始化写入。

5.8 本章小结

本章主要了解温习了在linux中链接的过程。通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,更好地掌握了链接与之中重定位的过程。不过,链接远不止本章所涉及的这么简单,就像是hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。

第6章 hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。

6.2 简述壳Shell-bash的作用与处理流程

作用:解释命令,连接用户和操作系统以及内核
流程:
shell先分词,判断命令是否为内部命令,如果不是,则寻找可执行文件进行执行,重复这个流程:

  1. Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
  2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
  3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
  4. Shell对~符号进行替换。
  5. Shell对所有前面带有$符号的变量进行替换。
  6. Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
  7. Shell计算采用$(expression)标记的算术表达式。
  8. Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
  9. Shell执行通配符* ? [ ]的替换。
  10. shell把所有结果中用到的注释删除,并且按照下面的顺序实行命令的检查:
    a) 内建的命令
    b) shell函数(用户自定义)
    c) 可执行脚本文件(要求文件路径)
  11. 在执行前的最后一步是初始化所有的输入输出重定向。
  12. 最后,执行命令。

6.3 Hello的fork进程创建过程

函数原型pid_t fork(void);
进程包括代码、数据和分配给进程的资源。fork通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程,从而为他们安排不同的操作。

6.4 Hello的execve过程

函数原型int execve(const char *filename, char *const argv[], char *const envp[]);
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。
只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。
execve执行的操作依次是:

  1. 删除已存在的用户区域(自父进程独立)。
  2. 映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
  3. 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
    4设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
逻辑控制流:
多个进程的逻辑控制流在时间上可以交错,表现为交替运行。进程控制权的交换需要上下文切换。操作系统内核使用一种成为上下文切换的较高层形式的异常控制流来实现多任务。
时间片:一个进程执行它的控制流的一部分的每一个时间段。
上下文:
内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。
在这里插入图片描述

用户态与核心态转换:
进程运行在用户模式中时,不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个I/O操作,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据
而当进程运行在内核模式中时,可以执行指令集中的任何指令,并且可以访问内存中的任意位置。
这两种划分是为了使处理器安全运行,不至于损坏操作系统。划分了以后,进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
进程调度的过程:
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。内核中有调度器(代码段)来完成这个事情。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

6.6 hello的异常与信号处理

(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
正常运行:
在这里插入图片描述

  1. 回车+ctrl z+jobs+fg %1+回车(无+SIGTSTP+SIGCONT)
    在这里插入图片描述

回车不影响进程运行,ctrlz向进程发送一个SIGTSTP信号,进程收到后挂起。使用jobs可查看到其状态。接下来使用fg %1想起发送SIGCONT信号使其继续运行且位于前台。
2. 后台+ctrl c+回车+ps+kill+ps(失败+SIGKILL)
在这里插入图片描述

按下CtrlC,向终端发送SIGINT信号,但是shell只是转发给前台,故进程继续输出。输出完毕时输入回车,发现进程未自行结束并回收而是挂起,ps确认后使用kill向其发送SIGJKILL信号,继续用ps发现进程已回收

hello异常:
类别 原因 异步/同步 返回行为
中断 来自I/O的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令

故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
在这里插入图片描述

				中断处理

在这里插入图片描述

				陷阱处理

在这里插入图片描述

				故障处理

在这里插入图片描述

				终止处理

6.7本章小结

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

第7章 hello的存储管理

7.1 hello的存储器地址空间

(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:
包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:
逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:
一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

Logical Address= Segment Descriptor +Offset
SDT includes GDT(System) and LDT(Process)
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
给定一个完整的逻辑地址段选择符+段内偏移地址,
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
把Base + offset,就是要转换的线性地址了

7.3 Hello的线性地址到物理地址的变换-页式管理

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

优点:
1、 由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。
2、 动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。
缺点:
1、 要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本。
2、 增加了系统开销,例如缺页中断处理机,
3、 请求调页的算法如选择不当,有可能产生抖动现象。
4、 虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面较大,则这一部分的损失仍然较大。

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

TLB:

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。其工作流程如下:
(1)CPU产生一个虚拟地址。
(2)MMU从TLB中取出相应的PTE。(若可以找到)
(3)MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
(4)高速缓存/主存将所请求的数据字返回给CPU。

在这里插入图片描述
在这里插入图片描述

				TLB命中						

在这里插入图片描述

TLB不命中

多级页表:
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
在这里插入图片描述

`
Intel 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.6 hello进程fork时的内存映射

fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制(私有的写时复制)就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序的步骤:

  1. 删除已存在的用户区域
  2. 创建新的区域结构
    a) 代码和初始化数据映射到.text和.data区(目标文件提供)
    b) .bss和栈映射到匿名文件
  3. 设置PC,指向代码区域的入口点
    a) Linux根据需要换入代码和数据页面
    exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
    在这里插入图片描述

7.8 缺页故障与缺页中断处理

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
整体的处理流程:

  1. 处理器生成一个虚拟地址,并将它传送给MMU
  2. MMU生成PTE地址,并从高速缓存/主存请求得到它
  3. 高速缓存/主存向MMU返回PTE
  4. PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
  5. 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
  6. 缺页处理程序页面调入新的页面,并更新内存中的PTE
  7. 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

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

7.10本章小结

本章详细介绍了加载hello的过程中所涉及到的有关存储管理的相关知识,描述了系统通过各种地址空间的转换在存储器山结构下以局部性原理来减小访问时空成本的具体实现方法,并分析了进程的内存映射、缺页故障和缺页故障处理,还对动态内存分配器做了相关的拓展。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:unix io接口

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:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
    返回值:成功:返回文件描述符;失败:返回-1
  2. close()函数
    功能描述:用于关闭一个被打开的的文件
    所需头文件: #include <unistd.h>
    函数原型:int close(int fd)
    参数:fd文件描述符
    函数返回值:0成功,-1出错
  3. read()函数
    功能描述: 从文件读取数据。
    所需头文件: #include <unistd.h>
    函数原型:ssize_t read(int fd, void *buf, size_t count);
    参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
    返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
  4. write()函数
    功能描述: 向文件写入数据。
    所需头文件: #include <unistd.h>
    函数原型:ssize_t write(int fd, void *buf, size_t count);
    返回值:写入文件的字节数(成功);-1(出错)
  5. lseek()函数
    功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
    所需头文件:#include <unistd.h>,#include <sys/types.h>
    函数原型:off_t lseek(int fd, off_t offset,int whence);
    参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
    返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析

(以下格式自行编排,编辑时删除)
原链接https://www.cnblogs.com/pianist/p/3315801.html
printf函数的源代码为

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

从上到下依次呈调用关系。即
printfvsprintfwritesys_call
功能简图如下
在这里插入图片描述

vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

代码实现如下
在这里插入图片描述

由代码可以知晓,n等于0时bb处将读取缓冲区的BUFSIZ个字符。若返回时n大于0,就返回bb处第一个字符,否则就是读入失败,返回EOF
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法、Unix IO接口及其函数,并分析了printf函数和getchar函数。

结论

hello的一生:
1、 编程阶段:hello程序的逻辑在编辑器中被高级语言所实现;
2、 预处理阶段:hello.c源文件将被预处理器cpp修改,替换,插入,展开生成hello.i文件
3、 编译阶段:hello.i经过编译器ccl的优化和翻译,成为汇编语言程序文件hello.s
4、 汇编阶段:hello.s经过汇编器as翻译,编程二进制机器语言的可重定位目标文件hello.o
5、 链接阶段:链接器(ld)将hello.o与其他可重定位目标文件链接(还有静态库,动态库)成为可执行文件hello
6、 创建进程:shell进程调用fork函数为hello创建新进程,并调用execve函数运行hello。
7、 访问内存:MMU将hello进程使用的虚拟地址转换为物理地址,通过多级缓存结构访问内存
8、 (动态申请内存):某些人编写的hello可能使用malloc动态申请内存。
9、 运行时异常/信号:hello运行时会产生异常或接收到信号,系统会做出反应。
10、 终止与回收:当hello进程终止后,其资源由父进程/init回收,内核删除相关数据
这就是hello平凡而伟大的一生,简单朴素的功能背后是庞大而又精巧的构架,足以用来支撑更有潜力的复杂程序,实现更重要的功能。
CSAPP是合格的计算机程序员的必修课之一,作为一名只学过程序语言,数据结构与算法等编程课的我们学生,深刻理解一个hello带给我们的不仅仅只是那些庞大体系下的知识,更是实践与研究中具体可行的思维与方法。
同时我认为这门课学时的削减,实验的减少,即是在客观上为连贯学习这本书的全部知识带来了困难,但也必须是必要的。紧张的课程安排后我将继续学习这本书,并且以此为基础来扩展计算机专业的更多知识,提高个人素养。

附件

列出所有的中间产物的文件名,并予以说明起作用。
文件名 文件作用
hello.i hello.c预处理后生成文件
hello.s hello.i编译后生成的汇编文件
hello.o hello.s经过汇编过后生成的可重定向文件
hello hello.o经过链接之后生成的可执行文件
hello.elf hello的elf格式文件
hello_o.elf hello.o的edf格式文件
dhello_.s. hello.o经过反汇编后生成的反汇编文件
dhello.s. hello经过反汇编后生成的反汇编文件

参考文献

[1] 兰德尔·E·布莱恩特,大卫·R·奥哈拉伦著;深入理解计算机系统[M].北京:机械工业出版社,2016.7
[2] printf函数实现的深入剖析 [http://www.cnblogs.com/pianist/p/3315801.html].
[3]码农的荒岛求生 [https://github.com/xfenglu/everycodershouldknow].

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哈尔滨工业大学(Harbin Institute of Technology,简称“哈工大”)是中国著名的重点大学,成立于1920年,是中国最早创办的六所工科高等学府之一。其中,哈尔滨工业大学的计算机科学与技术学院一直以来都是国内知名的学院。在其中,CSAPP是哈工大计算机科学与技术学院开设的一门经典课程,全称为《深入理解计算机系统》(Computer Systems: A Programmer's Perspective)。 这门课程涵盖了计算机系统的各个方面,从高级语言编程到机器级别的细都有涉及,深入剖析了计算机系统的内部机制,讲解了各种计算机组件的原理,如内存、处理器、I/O设备、网络等等。此外,课程内容还包括缓存、异常、程序优化、并发编程、虚拟内存等重要主题,并且还会涉及安全问题,例如注入攻击、缓冲区溢出等等。 相较于其他计算机相关的课程而言,CSAPP的特殊之处在于,它以程序员的视角,深入而生动地解释了计算机系统的工作方式和内部机制。课程强调了实践性,通过大量的例子及编程作业,学生可以实际操作并理解到具体的计算机系统的运行方式。 此外,CSAPP的教学团队非常强大,由哈工大的多位顶尖教授组成,能够保证教学质量和深度。学生通过学习这门课程,不仅可以深入了解计算机系统的各个方面,还可以提高编程能力和工程实践水平,有助于更好地应对工作中遇到的各种问题。 总之,CSAPP是哈尔滨工业大学计算机科学与技术学院开设的一门经典课程,其全面而深入的课程内容、强调实践性、优秀的教学团队等特色让其在国内享有较高声誉,对学生深入理解计算机系统、提高编程实践能力等方面,都有非常积极的作用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值