我的Hello啊

计算机系统

大作业

题 目 程序人生-Hello’s P2P

计算机科学与技术学院
2018年12月
摘 要
我是Hello,我是每一个程序猿¤的初恋(羞羞……)
却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜……),他们很快喜欢上sum、sort、matrix、PR、AI、IOT、BD、MIS……,从不回头。
只有我自己知道,我的出身有多么高贵,我的一生多么坎坷!
多年后,那些真懂我的大佬(也是曾经的菜鸟一枚),才恍然感悟我的伟大!
本篇论文主要讲述了hello.c执行成功全过程,以及各类计算机基础知识。
关键词:可执行文件;预处理;虚拟内存;高速缓存;汇编;反汇编;链接

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

目 录

第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):首先,键盘输入利用高级语言C语言编辑生成hello.c源文件,在Linux中,经过终端输入命令行执行处理(此过程包括预处理gcc、as,与函数库链接等)最终形成可执行文件ELF文件。
下一步,执行该文件,操作系统会使用fork函数形成子进程,然后使用execve函数加载进程。
以上,实现完成P2P。
O2O(From Zero-0 to Zero-0):进入程序入口后程序开始载入物理内存(程序运行过程会使用到各种数据),CPU工作,在这个过程中还涉及操作系统的信号处理,控制进程等等,IO管理与信号处理结合,完成到屏幕输出工作。当程序执行结束后,操作系统进行进程回收。
以上,实现完成O2O。
1.2 环境与工具
//列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。//
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位; Oracle VM VirtualBox;Ubuntu16.04 LTS
开发工具:gcc; edb; objdump
1.3 中间结果
//列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。//
hello.i: 研究预编译的作用以及进行编译器的下一步编译操作。
hello.s: 研究汇编语言以及编译器的汇编操作
hello.o: 可重定位目标程序,用于链接器或编译器链接生成最终可执行程序。
hello.out:可以用来反汇编,包括进入main函数前后发生的过程。
hello.elf:可重定位目标文件,用于读取类别,各节信息
hello1.elf:作用同上
1.4 本章小结
本章主要讲解了对于.c文件的执行过程,更进一步认识到操作系统的原理。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
作用:预处理的结果就得到了另外一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
//应截图,展示预处理过程!//
预处理命令:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

预处理结果代码(部分示例)
经过预处理的hello代码被展开,在main之前插入了大量代码。这些代码是根据#include从stdio.h、unistd.h、stdlib.h中提取的,其中包含了printf的声明等。

2.4 本章小结
Hello.c在编译之前需要经过预处理。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
//注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序//
概念:编译程序(Compiler,compiling program)也称为编译器,是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的翻译程序。编译程序属于采用生成性实现途径实现的翻译程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。编译出的目标程序通常还要经历运行阶段,以便在运行程序的支持下运行,加工初始数据,算出所需的计算结果。
作用:将高级语言源程序翻译成目标程序。

3.2 在Ubuntu下编译的命令
应截图,展示编译过程!

3.3 Hello的编译结果解析
//此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。//
3.3.1数据
用到的数据类型:整数 int ;字符串;数组
①整数
Int sleepsecs 全局变量 已被赋值2

Int i:编译器将局部变量存储在寄存器或者空间中,在hello.s中编译器将i存储在栈上空间
Int argc:作为第一个参数传入
②字符串
(1)“Usage: Hello 学号 姓名!\n”,第一个printf传入的输出格式化参数,在hello.s中声明如图3.2,可以发现字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
(2)“Hello %s %s\n”,第二个printf传入的输出格式化参数,在hello.s中声明如图

③数组
程序中 char *argv[ ] main ,argv作为存放char指针的数组同时是第二个参数传入。

3.3.2 运算与操作
赋值操作
把i赋值为0. movl赋值语句

比较操作
两个比较,利用cmpl实现

算术运算
for循环 addl实现

3.3.3. 控制转移
if语句
利用cmpl+je/cmpl+jmp语句实现

3.3.4 函数调用
printf调用:以printf(“Hello %s %s\n”,argv[1],argv[2]);为例,格式化字符串被存放在edi传递,argv[1]被放在rsi,argv[2]被放在rdx。使用call来调用printf,而printf的返回值则会被存入eax返回。
exit函数的调用:参数被存放在edi传递,然后使用call调用exit。
sleep的调用:参数被存放在edi传递,然后使用call调用sleep。
getchar的调用:使用call。
main函数的返回值放在eax传递。
3.3.5 个别标识
.file 源文件
.data 数据段
.globl 全局标识符
.string 字符串类型
.long long类型
.text 代码段
3.4 本章小结
本章主要讲述汇编处理指令,高级语言与汇编语言之间的转换(以及机器码)。
深入了解了汇编怎样处理各种数据,各种操作(运算、赋值等),函数调用等。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
//注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。//
概念:汇编指的是汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,包含hello程序执行的机器指令。
作用:实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!

4.3 可重定位目标elf格式
//分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。//
生成可执行文件:

如图,
ELF头:该处清晰显示各类别,大小等

节头:节头布表(文件出现各个节的语义、类型、位置和大小等信息)

重定位节:,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。如图4.4,图中8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、sleepsecs、sleep函数、getchar函数进行重定位声明。

4.4 Hello.o的结果解析
//objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。//
总体示意图——反汇编

部分截图比较(以下图中已经用相同颜色表明对应操作,必要的已经进行标注)
反汇编示意图4.4.1

与之对应 hello.s示意图4.4.1

可见此处包含数据访问(全局变量访问),函数调用。
数据访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
反汇编示意图4.4.2

与之对应hello.s示意图

4.5 本章小结
本章学习了将hello.c转换成hello.elf,hello.o,hello.s以及对其反汇编。汇编过程将汇编语言转换为机器语言,生成可重定位目标文件,使机器可以直接处理执行。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
//注意:这儿的链接是指从 hello.o 到hello生成过程。//
概念:链接是指将文件中调用的各种函数跟静态库及动态库链接,并将它们打包合并形成目标文件,即可执行文件。
作用:通过链接可以实现将头文件中引用的函数并入到程序中。
5.2 在Ubuntu下链接的命令
//使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件//
命令: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的格式
//分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。//
使用readelf -a hello > hello.elf 命令生成hello程序的ELF格式文件。

elf文件:

5.4 hello的虚拟地址空间
//使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。//
在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,自0x400fff结束,这之间每个节(开始 ~ .eh_frame节)的排列即开始结束同图5.2中Address中声明。

查看ELF格式文件中的Program Headers,程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。

5.5 链接的重定位过程分析
//objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。//
hello.o反汇编结果:

hello反汇编结果:

经过对比可以发现,hello.o没有经过链接,所以main的地址从0开始,并且不存在调用的如printf这样函数的代码。另外,很多地方都有重定位标记,用于后续的链接过程。hello.o反汇编代码的相对寻址部分的地址也不具有参考性,没有经过链接并不准确。
函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
5.6 hello的执行流程
//使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。//
程序名称 地址
hello!_start 0x400500
hello!_init 0x400488
hello!main 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0

5.7 Hello的动态链接分析
//分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。//
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

没有调用dl_init之前的全局偏移量表.got.plt

调用dl_init之后的全局偏移量表.got.plt

目标程序-动态链接器

5.8 本章小结
本章学习了链接过程对程序的处理。经过链接,ELF可重定位的目标文件变成可执行的目标文件,链接器会将静态库代码写入程序中,以及动态库调用的相关信息,并且将地址进行重定位,从而保证寻址的正确进行。静态库直接写入代码即可,而动态链接过程相对复杂一些,涉及共享库的寻址。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一种交互型的应用级程序。它能够接收用户命令,然后调用相应的应用程序,即代表用户运行其他程序。
处理流程:从终端读入输入的命令→将输入字符串切分获得所有参数→如果是内置命令则立即执行→否则调用相应的程序为其分配子进程并运行→shell接受键盘输入信号,对信号进行相应处理
6.3 Hello的fork进程创建过程
当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
过程:
程序入口→fork(hello子进程:代码逻辑j→exit)→pid→……→exit
6.4 Hello的execve过程
创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。
在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。
6.5 Hello的进程执行
//结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。//
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
hello程序执行过程中同样存储时间分片,与操作系统的其他进行并发运行。并发执行涉及到操作系统内核采取的上下文交换策略。内核为每个进程维持一个上下文,上下文就是内核重新启动一个先前被抢占的进程所需的状态。
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。
在此基础上,hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。
程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再及时改用户态,从而保证系统的安全与稳定。
6.6 hello的异常与信号处理
// hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。//
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
Ctrl-Z

Ctrl-C

Ps:

Jobs

Pstree

Fg

运行中乱按:

6.7本章小结
Shell(Gnome-Terminal)下达命令,进程管理为hello提供了活动空间,Shell为其fork,为其execve,为其分配时间片。
程序运行中难免遇到异常,异常分为中断、陷阱、故障和终止四类,均有对应的处理方法。操作系统提供了信号这一机制,实现了异常的反馈。这样,程序能够对不同的信号调用信号处理子程序进行处理。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
//结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。//
物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址:实际上就是这里的线性地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。
Intel处理器从逻辑地址到线性地址的变换通过段式管理,介绍段式管理就必须了解段寄存器的相关知识。段寄存器对应着内存不同的段,有栈段寄存器(SS)、数据段寄存器(DS)、代码段寄存器(CS)和辅助段寄存器(ES/GS/FS)。

7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元负责把一个线性地址转换为物理地址。从管理和效率的角度出发,线性地址被划分成固定长度单位的数组,称为页(page)。例如,一个32位的机器,线性地址可以达到4G,用4KB为一个页来划分,这样,整个线性地址就被划分为一个2^20次方的的大数组,共有2的20次方个页,也就是1M个页,我们称之为页表,改页表中每一项存储的都是物理页的基地址。
这里不得不说的是另一个“页”,我们称之为物理页,或者页框、页桢。是分页单元将所有的物理内存都划分成了固定大小的单元为管理单位,其大小一般与内存页大小一致。
如果内存页按照这种方式进行管理,管理内存页需要220次方的数组,其中每个数组都是32bit,也就是4B(其中前20位存储物理内存页的基地址,后面的12位留空,用于与给定的线性地址的后12位拼接起来一起组成一个真实的物理地址,寻找数据的所在。这样就需要为每个进程维护4B*220=4MB的内存空间,极大地消耗了内存。
为了能够尽可能的节约内存,CPU在页式内存管理方式中引入了两级的页表结构,如图所示。

这种页式管理方式中,第一级的页表称之为“页目录”,用于存放页表的基地址;第二级才是真正的“页表”用于存放物理内存中页框的基地址。
1、二级页目录的页式内存管理方式中,第一级的页目录的基址存放在CPU寄存器CR3中,这也是转换的开始点;
2、每一个活动的进程,都有其对应的独立虚拟内存(页目录也是唯一的),那么它对应一个独立的页目录地址。–运行一个进程,需要将它的页目录地址放到CR3寄存器中,将别的页目录的基址暂时换到内存中;
3、每个32位的线性地址被划分成三部分,页目录索引(10位),页表索引(10位),偏移量(12位)。
线性地址转换成物理地址的过程如下:
1、从CR3中取出进程的页目录的地址(操作系统在负责进程的调度的时候,将这个地址装入对应的CR3地址寄存器),取出其前20位,这是页目录的基地址;
2、根据取出来的页目录的基地址以及线性地址的前十位,进行组合得到线性地址的前十位的索引对应的项在页目录中地址,根据该地址可以取到该地址上的值,该值就是二级页表项的基址;当然你说地址是32位,这里只有30位,其实当取出线性地址的前十位之后还会该该前十位左移2位,也就是乘以4,一共32位;之所以这么做是因为每个地址都是4B的大小,因此其地址肯定是4字节对齐的,因此左移两位之后的32位的值恰好就是该前十位的索引项的所对应值的起始地址,只要从该地址开始向后读四个字节就得到了该十位数字对应的页目录中的项的地址,取该地址的值就是对应的页表项的基址;
3、根据第二步取到的页表项的基址,取其前20位,将线性地址的10-19位左移2位(原因和第2步相同),按照和第2步相同的方式进行组合就可以得到线性地址对应的物理页框在内存中的地址在二级页表中的地址的起始地址,根据该地址向后读四个字节就得到了线性地址对应的物理页框在内存中的地址在二级页表中的地址,然后取该地址上的值就得到线性地址对应的物理页框在内存中的基地址;(这一步的地址比较绕,还请仔细琢磨,反复推敲)
4、根据第3步取到的基地址,取其前20位得到物理页框在内存中的基址,再根据线性地址最后的12位的偏移量得到具体的物理地址,取该地址上的值就是最后要得到值;
其实,对比一级页表机制和二级页表机制可以发现,二级页表机制中同样需要1024个二级页表,每个页表都有1024项,每项的大小都是4B,因此一个二级页表需要4KB,1024个二级页表需要4MB的空间,再加上页目录,好像比只有一级页表机制占用了更多的内存,而且寻址方式变得更复杂了,似乎是在自己给自己找麻烦。从CPU的开销中可以看到,如果是一级页表机制,那么CPU取到一个数需要访问内存两次,而使用二级页表机制之后,CPU想要取得一个操作数,需要访问内存三次。其实,使用二级页表机制还是有很多优点的.
7.4 TLB与四级页表支持下的VA到PA的变换

如图显示此过程。
7.5 三级Cache支持下的物理内存访问
Cache的概念:
cache是为了解决处理器与慢速DRAM设备之间巨大的速度差异而出现的.
cache属于硬件系统,linux不能管理cache.但会提供flush整个cache的接口.
cache分为一级cache,二级cache,三级cache等等.一级cache与cpu处于同一个指令周期.
Cache的工作模式
数据回写(write-back):这是最高性能的模式,也是最典型的,在回写模式下,cache内容更改不需要每次都写回内存,直到一个新的cache要刷新或软件要求刷新时,才写回内存.
写通过(write-through):这种模式比回写模式效率低,因为它每次强制将内容写回内存,以额外地保存cache的结果,在这种模式写耗时,而读和回写模一样快,这都为了内存与cache相一致而付出的代价.
预取(prefectching):一些cache允许处理器对cache line进行预取,以响应读请求,这样被读取的相邻内容也同时被读出来,如果读是随机的,将会使CPU变慢,预取一般与软件进行配合以达到最高性能.
三类分析:

7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
删除已存在的用户区域。删除当前进程虚拟地址用户部分中的已存在的区域结构。映射私有区域。为新程序的文本、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时拷贝的。文本和数据区域被映射为a.out文件中的文本和数据区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的。
映射共享区域。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。

7.8 缺页故障与缺页中断处理
进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。

7.9动态存储分配管理
//Printf会调用malloc,请简述动态内存管理的基本方法与策略。//
所谓动态内存分配(Dynamic Memory Allocation)就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
所谓动态内存分配就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
动态内存管理的策略有很多,1.首次适应算法(First Fit) 算法思想:将空闲分区链以地址递增的顺序连接;在进行内存分配时,从链首开始顺序查找,直到找到一块分区的大小可以满足需求时,按照该作业的大小,从该分区中分配出内存,将剩下的空闲分区仍然链在空闲分区链中。2. 循环首次适应算法(Next Fit) 算法:分配内存时不是从链首进行查找可以分配 内存的空闲分区,而是从上一次分配内存的空闲分区的下一个分区开始查找,直到找到可以为该进程分配内存的空闲分区。3. 最佳适应算法(Best Fit) 算法:将空闲分区链中的空闲分区按照空闲分区由小到大的顺序排序,从而形成空闲分区链。每次从链首进行查找合适的空闲分区为作业分配内存,这样每次找到的空闲分区是和作业大小最接近的,所谓“最佳”。4. 最坏适应算法(Worst Fit) 算法:与最佳适应算法刚好相反,将空闲分区链的分区按照从大到小的顺序排序形成空闲分区链,每次查找时只要看第一个空闲分区是否满足即可。

7.10本章小结
存储器系统(memory system)是一个具有不同容量、成本和访问时间的存储设备的层次结构。 CPU寄存器保存着最常用的数据。靠近CPU的小的、快速的高速缓存存储器作为一部分存储在相对慢速的主存储器中数据和指令的缓冲区域。主存缓存存储在容量较大的、慢速磁盘上的数据,而这些磁盘常常又作为存储在通过网络连接的其他机器的磁盘或磁带上的数据的缓冲区域。
存储器层次结构是可行的,这是因为与下一个更低层次的存储设备相比来说,一个编程良好的程序倾向于更频繁地访问上一个层次上的存储设备。所以,下一层的存储设备可以更慢速一点,也因此可以更大,每个比特位更便宜。
计算机程序有一个被称为是局部性的基本属性。具有良好局部性的程序倾向于一次又一次地访问相同的数据项集合,或是倾向于访问临近的数据项集合。具有良好局部性的程序比局部性差的程序更多地倾向于从存储器层次结构中的较高层次处访问数据项,因此运行得更快。
Hello在运行时,通过合理的储存器分配,如高速缓存,页表提升了程序的运行速度。分配器为其分配了内存空间,保证它可以顺利执行,分配器通过分配算法给hello找到了合适的空间让其运行。(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix-IO这种将设备优雅地映射为文件的方式,允许Linux内 核引出一个简单、低级的应用接口。
1.打开文件。open()
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在续对此文件的所有操作中标示这个文件。

2.改变当前文件的文件位置。lseek()
对于每个打开的文件,内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。

3.读写文件:read() and write()
一个读操作就是从文件拷贝n>0个字节到存储器。写操作就是从存储器拷贝n>0个字节到一个文件。
4.关闭文件:close()
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的存储器资源。
8.3 printf的实现分析
//https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。//
Printf函数定义如下

先来看printf函数的内容:
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义:typedef char va_list
这说明它是一个字符指针。其中的: (char
)(&fmt) + 4) 表示的是…中的第一个参数。表示的是…中的第一个参数的地址。
i = vsprintf(buf, fmt, arg);
然后观察vsprintf(buf, fmt, arg)是什么函数

vsprintf返回的是一个长度,返回的是要打印出来的字符串的长度 ,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数的第一个参数为fd,也就是描述符,而1代表的就是标准输出。查看write函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后调用syscall结束。write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。
内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。

8.4 getchar的实现分析
//异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。//
getchar的实现:

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
输入输出设备(IO设备),是数据处理系统的关键外部设备之一,可以和计算机本体进行交互使用。如:键盘、写字板、麦克风、音响、显示器等。因此输入输出设备起了人与机器之间进行联系的作用。

(第8章1分)
结论
//用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。//
通过不断查询,不断试验,一步一步写完hello.c,对于这个小文件说不痛恨是不可能的,不过依然记得第一次成功打出“Hello World!”的喜悦。本篇论文,经过不断上网查资料,理解,实践,最终完成了一份算不上最好的报告吧。有许多疑惑依旧没有解开,但是会继续探索下去。感谢深入理解计算机系统,让我备受折磨也一点一点接受,提升。
hello的成功运行经历了预处理,编译,汇编,链接,execve加载,访问内存,动态申请内存,处理信号,最终输出结果显示在屏幕上,亦如人生,不断试验,不断进步,继续加油,计算机的路还很长,望能坚持。
(结论0分,缺少 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i: 研究预编译的作用以及进行编译器的下一步编译操作。
hello.s: 研究汇编语言以及编译器的汇编操作
hello.o: 可重定位目标程序,用于链接器或编译器链接生成最终可执行程序。
hello.out:可以用来反汇编,包括进入main函数前后发生的过程。
hello.elf:可重定位目标文件,用于读取类别,各节信息
hello1.elf:作用同上
hello.odjdump:可执行文件hello生成的反汇编文件,用于分析各种函数进程以及调用

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1]百度百科 https://baike.baidu.com/
[2] 《深入理解计算机系统》 兰德尔 E. 布莱恩特 & 大卫 R.奥哈拉伦 机械工业出版社
(参考文献0分,确实 -1分)

发布了8 篇原创文章 · 获赞 3 · 访问量 329

猜你喜欢

转载自blog.csdn.net/qq_41507243/article/details/85464436