程序人生-Hello’s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 微电子科学与工程

学   号 1182100506

班   级 1821203

学 生 崔旭洋

指 导 教 师 史先俊

计算机科学与技术学院

2020年3月

摘 要

本文将从hello.c的编写,预处理,编译,汇编,链接,进程,存储和I/O八个角度并利用objdump和edb等工具对hello.c的生成到消失过程中和计算机以及外界人类交互产生的影响变化进行分析,剖析hello.c究竟每一步都做了什么,影响了什么东西,产生了什么效果。这八个部分是CSAPP课程的浓缩,贯穿了全书,本文不仅是对hello.c进行了分析,实际上也是对深入了解计算机系统这门课的一个总体性复习。

关键词:计算机系统、CSAPP、预处理、编译、汇编、链接、进程、存储管理、虚拟内存/系统I/O;

(摘要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简介

有一天,程序员操控键盘鼠标通过系统I/O将hello.c的DNA传进了计算机,然后后者在体内创造了一个由以c语言形式编码的源文件hello.c,随着预处理的命令,gcc和外部文件一起对hello.c进行处理生成了hello.i。在编译命令下达后gcc根据说明书对hello.i进行了改造,将它变成了汇编语言的样子。后来gcc -o hello.o
hello.s告诉gcc要对hello.s进行再一次的改变,从另一个角度思考.s体内的结构,并将其转化为机器语言的样子。最后经过链接命令,无数hello.o的伙伴和它链接在一起并生成了能执行的可执行文件hello.out。生成的hello.out成功的登上了shell的舞台,fork函数将hello.out克隆了一份,创建了子进程,在execve函数的帮助下linux加载运行了hello.out,可是外部的大魔王程序员偏偏在这时候按下了键盘,hello进程在运行过程中受到了信号必须进行反应——被挂起等待进一步的指示。终于,大魔王放过了hello进程,外部输入命令让其继续运行。经过无数磨难后hello进程终于执行完了他的任务,随着kill -9命令的下达,收到了SIGINT终止信号的它迎来了程生的终点,消失在无数的进程中,但是他知道,总有一天会有新的“hello.out”来顶替他的位置……

1.2 环境与工具

1.2.1 硬件环境

Intel Core i5 7300HQ、4GBytes
DDR4内存X2、NVIDIA GeForce GTX 1060独立显卡、Intel® HD Graphics 630核显、128G SSD、1TB机械硬盘

1.2.2 软件环境

64位Windows10家庭中文版、64位Linux Ubuntu19.10

1.2.3 开发工具

WIN10: Visual Studio Community2019、记事本、Dev C++、WinHex、VMware、CPU-Z

LINUX: Code::Blocks IDE , Terminal, hex edit

1.3 中间结果

hello.i:对hello.c进行预处理后产生的文件,将源文件中#语句进行替换(宏替换,文件调用、条件编译)

hello.s:对hello.i进行编译后产生的文件,将高级语言翻译成汇编语言。

hello.o:对hello.s进行汇编时后的文件,通过汇编器将汇编语言翻译为机器指令,并这些机器指令输出为可重定位目标程序的格式。

hello:hello.o链接生成的可执行目标文件,对hello.o进行重定位,链接库,完成最后加工。

1.4 本章小结

以上简单的介绍了hello的一生和此次试验所用的环境和工具,后面将详细分节的介绍hello.c的一生

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理指的是在编译之前进行的处理,预处理是通过预处理器CPP实现的,其根据以字符#开头的命令如#include,#define,#ifndef等修改原始的C程序,实现如上述例子一样文件包含(比如插入头文件),宏定义(对某个定义进行宏替换),条件编译(根据条件判断是否编译后面的代码)的功能,将传进来的.c文件变成.i文件输出。

2.2在Ubuntu下预处理的命令

Ubuntu下的预处理明令为gcc -o x.i x.c ,此处使用带参数的命令进行预处理:gcc -m64
-no-pie -fno-PIC -E -o hello.i hello.c,使用预处理命令后生成hello.c对应的预处理文件hello.i,生成效果如下图所示

图2-1

2.3 Hello的预处理结果解析

图2-2

可以看到hello.c中在一开头有三个include的头文件,原理上讲在预处理后应该插入到程序中,这点可以在下图中验证,在以下部分截图中的第14,33,44,50行等出现了包括上述头文件的语句,从中也可以看到这些头文件所在的位置。

值得注意的是,源程序hello.c只有短短23行,而预编译后的hello.i却有上千行,许多重复的内容不用程序员每次都打出来,编译器会自己导入,可见加入导入头文件这种语句可以大大的提高程序效率。

图2-3

2.4 本章小结

预处理是在编译阶段前的一个阶段,也是连接源程序与机器程序的第一个桥梁,通过对#语句的处理使得整个程序的编写有了很高的效率,这种提高效率的创新精神值得我们深入思考和学习。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是指编译器cc1将预处理得到的.i文件对词义、语义、语法进行分析并进行优化(如-Og,-O1,-O2等)且按一定语言逻辑翻译成汇编语言写成的.s文件的过程,

注意:这儿的编译是指从
.i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

所使用编译的命令为gcc -o x.s x.i ,对于此程序使用带参数的命令gcc
-m64 -no-pie -fno-PIC -S -o hello.s hello.i,执行完后出现hello.s文件如下图

图3-1

3.3 Hello的编译结果解析

此处为了更详细的解析编译结果,会按照.s文件的顺序结构对汇编意义进行分析,数据类型的处理都融合在里面,没有单列出来。

3.3.1…LFB6的解析

图3-2

main函数中在一开始将rbp压栈进行保存,随后把栈顶指针rsp保存到rbp,然后在栈顶开辟32字节的空间,在旧栈顶下20字节储存edi,即argc的值,占4个字节,在32字节处储存rsi,即argv[0]的指针,占8个字节。

随后进行if语句的判断,比较4和-20%(rbp)即4和argc的大小,如果相等就转到.L2,即main函数后面的部分,如果不等则继续,把.LC0即代表源程序中字符串“用法:
Hello 学号 姓名 秒数! \n”的数传给edi,接着调用puts函数执行printf语句并退出1。

3.3.2 .L2 .L3 .L4的解析

图3-3

(1).L2的解析

可以看到当之前if语句判断的argc等于4之后会跳转到L2,L2相当于源程序中int变量i的初始化,i=0的赋值将在这部分进行:进入L2后将-4(%rbp)即原栈顶下4字节(int的大小)到原栈顶处都改为0,然后跳转到.L3

(2).L3的解析

.L3实际上是源程序中for循环判断的汇编部分,比较-4(%rbp)即i和7的大小,如果i<=7就表示在循环内,跳转到.L4,i>7的话跳过跳转语句,执行main函数后面的内容即getchar函数和return 0

(3).L4

.L4实际上是源程序中for循环的循环体汇编部分,首先将-32(%rbp)即argv[0]的指针传到rax中,由于一开始定义的是char *argv[],故argv[0]和argv[1]的指针是连续储存的,由于指针占8个字节,从argv[0]的指针到argv[2]的指针差了8*2字节,即16,就是汇编中的在后面把rax加16,然后将rax地址存的数,即argv[2]的指针传给rdx作为第三个参数,后面类似,将argv[1]的指针传给rsi作为第二个参数,再加上.LC1即字符串“Hello %s %s\n”的数作为第一个参数然后调用printf函数实现输出。之后再把24-32(%rbp)即argv[3]的指针传给rdi作为第一个参数传进atoi,返回eax,储存到edi中传给sleep函数,即源程序中的sleep(atoi(argv[3])),后面再将-4(%rbp)即i加一。

图3-4 栈示意

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。

3.4 本章小结

编译部分是整个程序编译过程的精髓,是真正将类自然语言翻译成类机器语言的部分:把人类用语言描述的for循环,if判断以及int和char*定义等逻辑变成了一个“机器”能一步步执行的文本逻辑,即汇编语言。看汇编语言能明白机器真正是在做什么,能思考你的程序真正的执行和你所想是不是相同,还能从这其中寻找到高级语言逻辑看不到的优化点,是连接源程序与机器程序的第二个也是最重要的桥梁。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编是汇编器as将编译完成的汇编语言写成的.s文件按照一一对应关系翻译成机器指令语言写成的具有可重定位目标程序格式的文本,并将其存放在.o文件中的过程。

注意:这儿的汇编是指从
.s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

汇编所使用的命令为gcc
-o x.o x.s,本实验采用的是带参数的命令: gcc -m64 -no-pie -fno-PIC -c -o
hello.o hello.s,可以看到在执行完命令后文件夹里生成了一个hello.o文件如下图所示

图4-1

4.3 可重定位目标elf格式

在ubuntu下可以用readelf指令查看ELF格式文件的相关信息,这里使用readelf -a hello.o查看hello.o的所有信息:

图4-2 ELF文件头部分

图4-3 节头部表部分

图4-4 重定位信息部分

图4-5 .symtab符号表部分

可以看到在重定位信息中,除了两个R_X86_64_32类型的条目位于.rodata段外其他的都是R_X86_64_PLT32格式,代表着函数入口表,后面也可以看到对应着各种puts,exit,printf等系统函数,这些都会在接下来链接的过程中重定位符号地址,进行引用。

使用readelf
-x.rodata hello.o命令可以查看R_X86_64_32类型的条目究竟储存了什么,从之前重定位信息可以得到第一个从.rodata+0处开始,储存了puts要打印的字符串“用法: Hello 学号 姓名 秒数! \n”,第二个从.rodata+26开始,存放了第二个printf要打印的字符串“Hello %s %s\n”。

图4-6 .rodata段数据

4.4 Hello.o的结果解析

在ubuntu下使用objdump -d -r hello.o 命令对hello.o进行反汇编,可以得到下图:

图4-7 hello.o的反汇编代码

对比第三章编译得到的hello.s,我们可以发现:在.o重定位文件中变成了16进制构成的操作码和操作数组成的机器指令,不再是汇编语言,之后的每条的操作都有了相对地址,比如使用je 2d跳转到main函数+2d的地址,不再使用.L1 .L2等标签进行定位。在调用函数的时候不再是直接call printf,而是对这些可重定位的条目进行分析,变成e8 00 00 00 00(是call 00 00 00 00,因为还没有进行重定位,故后面的符号函数引用地址是0)和随后对应函数的重定位地址的信息。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

汇编是将类机器语言(汇编语言)转化为机器语言的第一步,将之前的汇编语言构成的.s文件进行和机器语言的映射,翻译成操作码和操作数组成的机器指令,并对之前的分支跳转,循环和函数调用进行重定义:使用相对地址而不是标签,并加上重定位信息等待后面的链接,是连接源程序与机器程序的第三个桥梁

(第4章1分)

第5章 链接

5.1 链接的概念与作用

链接是指连接器linker将各种代码和数据片段如.o文件收集并组合成为一个单一的可执行文件(linux下是.out,windows下是.exe)的过程,这个过程可以执行于编译时也可以执行与加载时,也可以作为dll在运行时执行,链接在软件开发中扮演者一个关键的角色:使分离编译成为可能,不用将一个大型的应用软件组织为一个大的源文件,而是把它分解成为更小,更好管理的模块并可以独立的修改和编译这些模块,极大地提升了效率。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

在ubuntu下尝试使用ld -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
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o hello.o -lc
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z
relro -o a.out命令进行连接,但链接报错,改用gcc链接:gcc -m64 -no-pie -fno-PIC -o hello hello.o,在文件夹中生成了.out文件如下图

图5-1 链接的生成

5.3 可执行目标文件hello的格式

通过使用readelf -a
hello 命令可以看到hello.out文件的头部如下图

图5-2 hello.out的头部表

5.4 hello的虚拟地址空间

在ubuntu下使用edb加载hello,由5.3知道.rodata在402000的位置,.text在4010f0,.data在404048的位置,在data dump中go to expression可以查看本进程的虚拟地址空间各段信息:

图5-3 .rodata段信息

图5-4 .text段机器代码

或者也可以直接在edb菜单中查看加载的符号表信息:

图5-5 加载的各段信息

5.5 链接的重定位过程分析

使用objdump -d
-r hello命令可以看到hello.out的反汇编如下图: 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

5-6 反汇编的main函数段

在重定位时链接器将所有同类型的节比如.text段合到一起变成一个新的.text段,所以在.o文件中.text节只有main函数的机器指令,而在链接后的.out文件中不仅有main函数,还有其他的比如puts,printf等函数的机器指令。

同时可以看到,重定位后的程序使用的是虚拟内存空间的绝对地址,而不是相对地址,链接器根据符号表将之前不确定的引用改成确定的地址,如在之前连接时的e8 00 00 00 00后面的0由于其确定函数被加载到内存中故替换成了对应函数在虚拟内存空间中的地址。

5.6 hello的执行流程

使用edb执行hello,加载hello到_start,到call main,以及程序终止的所有过程,其调用与跳转的各个子程序名或程序地址如下

0x7f5b0f49cde0 0x7f5b0f4ac0b0 4010f0 4011d6 401090 4010d0
4010a0 4010c0 4010e0 4010b0 401000 401030 401040 401050 401060
401070 401080
401090 4010d0 401100 401140 401170 401172 401200 401260 401264 0x7f5bd9be5f20

5.7 Hello的动态链接分析

由于ELF
格式的共享库使用 PIC 技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC 在代码中的跳转和分支指令不使用绝对地址。PIC 在 ELF 可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表 GOT,所以可以通过查看GOT位置内容分析动态链接,由加载符号表可知其在0x404000的位置如下图,系统的dl_init地址为0x7f5b0f4ac0b0。

图5-7 dl_init函数的位置

图5-8 init加载之前的GOT

图5-9 init加载之后的GOT

可以明显的看到执行dl_init之后将90 41 92 13 5e 7f加载到了GOT。

5.8 本章小结

链接过程是链接源程序和执行程序的最后一个桥梁,它补齐了之前汇编所缺的函数调用地址,将ELF文件中该合并的部分合并或者在后面执行的时候进行动态加载,将相对地址改为机器能真正执行的虚拟内存中的绝对地址,是之前的半成品.o文件真正的变成一个cpu能执行的可执行程序

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义是一个执行中程序的实例,这个定义很像c++中类和具体实例的关系。在现代系统上运行一个程序是,我们会得到一个假象,好像我们的程序是系统当前运行的唯一的程序一样:我们的程序好像是独占的使用内存和处理器,处理器好像是无间断地一条接一条地执行我们程序中的指令,最后我们的程序中的代码和数据好像是系统内存中唯一的对象,这些假象都是通过进程带给我们的。

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

Shell是一种壳层与命令行界面,是Unix操作系统下传统的用户和计算机的交互界面。普通意义上的shell就是可以接受用户输入命令的程序。它之所以被称作shell是因为它隐藏了操作系统低层的细节。Unix操作系统下的shell既是用户交互的界面,也是控制系统的脚本语言,而bash是shell的一种。

Shell可以执行一系列的来自用户传入的命令行或者对读入的命令行进行解析的步骤然后根据需要终止。

6.3 Hello的fork进程创建过程

一个进程包括代码、数据和分配给进程的资源,fork函数通过系统调用创建一个与原来进程几乎完全相同的进程,当一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同,相当于克隆了一个自己。其中子进程和父进程有不同的PID,fork函数只会被调用一次但会返回两次:子进程一次,父进程一次。

6.4 Hello的execve过程

execve是在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数,execve()用来执行参数filename字符串所代表的文件路径argv,第二个参数envp是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组,execve函数只调用一次而且不返回。

6.5 Hello的进程执行

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

内核为每一个进程维持一个上下文(内核重新启动一个被抢占的进程所需的状态),他由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。

而当在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,其过程如下图所示

图6-1 进程上下文的切换

当没有设置模式位时进程就运行在用户模式,用户模式中的进程不允许执行特权指令比如停止处理器或改变模式位。而当设置了模式位的时候,进程就运行在内核模式中,可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。

6.6 hello的异常与信号处理

在hello的执行过程中会出现终止和中断异常:产生一个来自键盘的中断即SIGINT,会终止当前程序,或者产生一个来自终端的停止信号SIGTSTP,停止直到下一个SIGCOONT。

如果在执行hello程序的时候在键盘上按ctrl+z会将进程中断如下图所示

图6-2 中断

图6-3 运行ps和jobs指令

可以看到hello进程被stopped了

图6-4 pstree指令的部分

执行kill -l命令查看kill的类型,可以看到kill -9即SIGKILL是需要的

图6-5 kill指令帮助

前面可以看到hello的PID是4111,使用kill -9 4111指令终止hello进程,可以发现在结束进程后该进程消失。

图6-6 kill指令执行及效果

6.7本章小结

进程是可以让你用一个系统一个处理器完成多个任务的基础,操作系统通过上下文和用户模式内核模式的切换有效的保障了对异常控制流的响应,让程序可以正确有效的“同时”进行

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory
cell)、存储单元(storage element)、网络主机(network host)的地址,比如hello.o中的相对地址偏移

线性地址是指地址空间中的整数是连续的的个地址空间

虚拟地址是在带虚拟内存的系统中cpu生成的有很多个地址的地址空间,可以写成段:偏移量的形式,比如之前重定位后hello.out中.rodata段的0x402000,.text段的0x4010f0,.data段的0x404048等

物理地址是指放在寻址总线上的地址,是真正的内存的地址,与虚拟地址空间存在映射关系

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

在段式存储管理中以段为单位分配内存,每段分配一个连续的内存区,但各段之间不要求连续,内存的分配和回收类似于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包括两个部分:段号和段内位移

在段式管理地址变换过程中,和页式变换基本相同,先要为运行的进程建立一个段表包括段号、段长、存储权限、状态、起始地址、修改位和增补位,具体过程如下图所示

图7-1 段式管理

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

概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址作为数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中的其他缓存一样,磁盘上的这些数据在缓存时被分割成块,块作为磁盘和主存(较高层)之间的传输单元。虚拟内存VM系统通过将虚拟内存分割为大小固定的“虚拟页”来处理这个问题,每个虚拟页的大小为2^p节。类似的,物理内存也被分割为大小等同与虚拟页的“物理页”,其大小成为页帧,过程类似下图所示:虚拟页0和3未被分配,因此在磁盘上还不存在,没有映射关系,虚拟页1,4,6已被缓存在物理内存中,也2,5,7已被分配但还未被缓存。

图7-2 页式管理

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

每次CPU产生一个虚拟地址MMU都必须查阅一个PTE来将虚拟地址翻译成物理地址,而最差的情况这会要求从内存多度一次数据(几十到几百个周期)。为了解决这个问题许多系统都在MMU中包括了一个关于PTE的小缓存称为TLB,速度非常快。TLB每一行都保存着一个由单个PTE组成的块,有高度的相连度。如下图所示虚拟地址中把虚拟页号拆分为TLB标记和TLB索引来访问TLB

图7-3 虚拟地址中用以访问TLB的组成部分

CSAPP课本中Intel CORE i7™如下图所示是采用四级页表来转换虚拟地址到物理地址的。其中36位的VPN被划分成4个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含一级页表的物理地址。VPN1提供一个一级页表条目(PTE)的偏移量,这个PTE包含二级页表的基地址。VPN2提供到一个二级页表条目的偏移量,以此类推。

图7-4 Intel CORE i7™的页表翻译

7.5 三级Cache支持下的物理内存访问

在CSAPP课本中Intel CORE i7™中支持三级cache访问,其内存系统如下图所示,处理器封装了四个核,一个大的所有核共享的L3高速缓存,以及一个DDR3内存控制器。每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路,这种链路基于QuickPath技术,是为了让一个核和外部I/O桥直接通信。TLB是虚拟寻址的,是四路组相联的。L1、L2和L3高速缓存是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。页大小可以在启动时被配置为4KB或4MB。Linux使用的是4KB的页。

图7-5 Intel CORE i7™的内存系统

如下图所示,i7采用四级页表层次结构,每个进程都有它自己私有的页表层次结构。CR3控制寄存器只想第一级页表L1的其实位置,CR3的值是每个进程的上下文的一部分,每次上下文切换的时候CR3的值都会被恢复。

图7-6 Intel CORE i7™的地址翻译

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

假设在运行当前进程中的程序执行了如下的execve调用:

execve(“a.out”,
NULL, NULL);

函数execve在当前进程中加载并运行包含在可知行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行需要以下几个步骤:

1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构

2.映射私有区域,为hello.out的代码、数据、bss和栈区域创建新的区域结构

3.映射共享区域,将动态链接到hello.out的比如标准C库libc.so等映射到虚拟地址空间中的共享区域内

4.设置程序计数器,设置当前进程上下文中的程序计数器,使之指向代码区域的入口点

加载器映射用户地址空间区域的过程如下图所示

图7-7 用户地址空间区域的加载

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。如下图所示,CPU引用了VP3中的一个字,VP3并未缓存在DRAM中,地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存并且出发一个缺页异常,调用缺页异常处理程序,选择一个牺牲页即放在PP3中的VP4。若VP4已经被修改,内核会将它复制回硬盘。

图7-8 VM的缺页

接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP3已经缓存在主存中,页命中并正常处理,缺页异常处理后的状态如下图所示

图7-9 缺页的处理

7.9动态存储分配管理

方式一

heap_1 动态内存管理方式是五种动态内存管理方式中最简单的,这种方式的动态内存管理一旦申请了相应内存后,是不允许被释放的。

方式二

与heap_1动态内存管理方式不同,heap_2动态内存管理利用了最适应算法,并且支持内存释放。

方式三

这种方式实现的动态内存管理是对编译器提供的 malloc
和 free 函数进行了封装,保证是线程安全的。

方式四

与heap_2动态内存管理方式不同,heap_4 动态内存管理利用了最适应算法,且支持内存碎片的回收并将其整理为一个大的内存块。

方式五

有时候我们希望 FreeRTOSConfig.h 文件中定义的 heap 空间可以采用不连续的内存区,比如我们希 望可以将其定义在内部 SRAM 一部分,外部 SRAM 一部分,此时我们就可以采用 heap_5 动态内存管理方式。

7.10本章小结

本章节主要参考了CSAPP课本上的一些内容和实例,在以存储器为核心的现代计算机结构中怎么由虚拟地址得到物理地址并提高效率显得尤为重要,前人给出的答案是段式管理,页式管理以及TLB和多级页表。再思考linux下的fork和execve函数对内存的映射和缺页故障的处理,存储器结构显得尤为重要值得我们深思。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对应文件的读和写来执行,允许Linux内核引出一个简单、低级的应用接口即Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件,改变当前的文件位置,读写文件和关闭文件

8.2 简述Unix
IO接口及其函数

1.进程通过open()函数打开一个已经存在的文件或者创建一个新文件

2.进程通过close()函数关闭一个打开的文件

3.应用程序通过调用read()和write()函数来执行输入和输出

4.应用程序可以通过调用lseek()函数显示地修改当前文件的位置

8.3 printf的实现分析

printf函数的函数原型如下图所示,其中调用了vsprint和write函数,按照输入的指定格式输出字符串且用va_list读入不定数量的参数传给arg:

图8-1 printf函数原型

vsprintf的作用就是格式化,它接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出,然后再调用write系统函数准备输出,然后陷阱-系统调用 int 0x80或syscall,字符显示驱动子程序,从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息),最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次调用getchar时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。

如下图所示,getchar函数会调用read系统函数,通过系统将整个缓冲区都读进buf里面,当buf长度为0时调用read函数否则将保存在buf中的最前面元素返回。

图8-2 getchar函数原型

8.5本章小结

系统I/O是沟通计算机和人之间的桥梁,真正的使人能和计算机交互,发挥功能。平时想打印输出printf时只需要简简单单的敲一个函数出来就能打印,但实际上对计算机来说并不简单:不仅需要计算打印内容,还要规划屏幕上哪个点显示什么颜色,简单的函数背后隐藏着系统I/O,人机交互的秘密。

(第8章1分)

结论

程序员操控键盘鼠标通过系统I/O将hello.c的DNA传进了计算机,然后后者在体内创造了一个由以c语言形式编码的源文件hello.c,随着gcc -o hello.i hello.c的命令,gcc和外部文件一起对hello.c进行预处理生成了hello.i。在gcc -o hello.s hello.i命令下达后gcc根据说明书对hello.i进行了改造,将它编译成了汇编语言的样子。后来gcc -o hello.o
hello.s告诉gcc对hello.s进行再一次的改变,从另一个角度思考.s体内的结构,并将其转化为机器语言组成的hello.o。最后经过gcc -o hello hello.o,gcc将无数hello.o的伙伴和它链接在一起并生成了能执行的可执行文件hello.out。生成的hello.out成功的登上了shell的舞台,fork函数将hello.out克隆了一份,创建了子进程,再调用execve函数加载运行了hello.out,可是外部的大魔王程序员偏偏在这时候按下了键盘,hello进程在运行过程中受到了信号必须进行反应——被挂起等待进一步的指示。终于,大魔王放过了hello进程,在输入命令让其继续运行。终于hello进程执行完了他的任务,随着kill -9命令的下达,收到了SIGINT终止信号的他迎来了程生的终点,消失在无数的进程中,但是他知道,总有一天会有新的“hello.out”来顶替他的位置……

回想几年前打开Dev C++,简简单单的printf一句话,普普通通的一个编译运行按键,平平淡淡一行Hello World背后却是一个融合了无数程序员智慧的结晶和心血的庞大计算机系统的工作。每时每刻想到第一个C语言,写出操作系统的人,心中总是涌现无尽的敬佩:他们对计算机的难造就了我们对计算机的易。这么有工程美感的东西怎么不值得我们学习呢?

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

附件

hello.i:对hello.c进行预处理后产生的文件,将源文件中#语句进行替换(宏替换,文件调用、条件编译)

hello.s:对hello.i进行编译后产生的文件,将高级语言翻译成汇编语言。

hello.o:对hello.s进行汇编时后的文件,通过汇编器将汇编语言翻译为机器指令,并这些机器指令输出为可重定位目标程序的格式。

hello:hello.o链接生成的可执行目标文件,对hello.o进行重定位,链接库,完成最后加工。

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

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] https://blog.csdn.net/chdhust/article/details/8460829 全局偏移表(GOT)和过程链接表(PLT)

[2] https://baike.baidu.com/item/机器指令/8553126?fr=aladdin 机器指令

[3] https://www.jianshu.com/p/a702a01db5c7 什么是shell
和 bash

[4] https://www.cnblogs.com/Wangjiaq/p/9815822.html 操作系统中的fork()函数对应的进程创建过程

[5] https://baike.baidu.com/item/execve/4475693?fr=aladdin
execve函数

[6] https://baike.baidu.com/item/逻辑地址/3283849?fr=aladdin 逻辑地址

[7] https://www.cnblogs.com/pianist/p/3315801.html printf函数的实现

[8] https://baike.baidu.com/item/getchar/919709?fr=aladdin getchar函数

[9] https://www.cnblogs.com/yangguang-it/p/7223601.html FreeRTOS
动态内存管理

[10] https://blog.csdn.net/weixin_30421525/article/details/97476291 段式管理

[11] https://blog.csdn.net/sinat_31135199/article/details/73605628 段式页存储

[12] https://blog.csdn.net/fuzhongmin05/article/details/58061584 操作系统的分区分页

[13] https://www.baidu.com/s?tn=80035161_2_dg&wd=%E4%BB%A5%E5%AD%98%E5%82%A8%E5%99%A8%E4%B8%BA%E6%A0%B8%E5%BF%83%E7%9A%84%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%93%E6%9E%84

以存储器为核心的计算机结构

[14] CS:APP Randal E.Bryant David R.O’Hallaron

(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值