CS大作业 程序人生 Hello's p2p

CS大作业 程序人生 Hello’s p2p

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1180300801
班   级 1803008
学 生 崔同发    
指 导 教 师 吴锐

计算机科学与技术学院
2019年12月
摘 要
hello.c程序是以字节序列的方式储存在文件中的。每个字节都有一个整数值,对应于某些字符。为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令接照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。从源文件到目标文件的转化是由编译器驱动程序完成的:GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,这个翻译过程可分为四个阶段完成(预处理、编译、汇编、连接)。此刻, hello.c源程序已经被编译系统翻译成了可执行目标文件hello,并被存放在磁盘上。要想在Unix系统上运行该可执行文件,我们将它的文件名输人到称为shell的应用程序中,shell将加载并运行 hello程序(为hello fork新进程,并领用execve来将可重定位目标文件载入,分配虚拟内存,映射私有和共享区域,修改PC使之指向hello的首地址),然后等待程序终止。hello程序在屏幕上输出它的消息,然后终止。
关键词:预处理;编译;汇编;链接;进程;虚拟内存;

目 录

第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简介
第一步,生成可执行目标文件
在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:
1inux> gcc –o hello hello.c
GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行 目标文件hello。这个翻译过程可分为四个阶段完成,如图。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)。

图 1-1
第二步,实例化(program to process)
此刻, hello.c源程序已经被编译系统翻译成了可执行目标文件hello,并被存放在磁盘上。要想在Unix系统上运行该可执行文件,我们将它的文件名输人到称为shell的应用程序中:
linux> ./hello
shell是一个命令行解释器,它输出一个提示符,等待输人一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令,那么shell就会假设这是 一个可执行文件的名字,它将加载并运行这个文件。在此处,shell将会使用fork函数为hello程序创建一个新进程,然后调用execve函数来加载hello程序到这个新进程中,分配虚拟内存,映射私有和共享区域,并设置PC(程序计数器,存储下一条执行指令的地址),使之指向hello程序入口。之后hello就开始运行在进程的上下文中,并输出相应的内容。shell会使用waitpid函数来显示的等待hello进程终止,并回收这个进程。

1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk
1.2.2 软件环境
Windows10 64位;Vmware;Ubuntu 16.04 LTS 64位
1.2.3 开发工具
GCC,edb, CodeBlocks,cpp,ccl,as,ld等
1.3 中间结果
hello.i 预处理得到的文件
hello.s 汇编文件
hello.o 可重定位目标文件
hello.elf hello.o的ELF格式文件
hello.elf1 hello的ELF格式文件
hello 可执行目标文件
1.4 本章小结
本章对hello进行了简单的介绍,分析了其P2P和020的过程,列出了本次任务的环境和工具,并且阐明了任务过程中出现的中间产物及其作用。

第2章 预处理
2.1 预处理的概念与作用
预处理器(cpp)根据以字符#开头的命令(主要包括: 1.宏定义; 2.文件包含; 3.条件编译),修改原始的C程序。比如 hello.c中第1行的#include 命令告诉预处理器读取系统头文件 stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
cpp hello.c > hello.i

图 2-1

图 2-2
2.3 Hello的预处理结果解析
进过预处理,预处理器(cpp)根据以字符#开头的命令:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
将引用的库插入到hello的程序文本中,以下是部分示例:

图 2-3

图 2-4
2.4 本章小结
本章简述了hello的预处理阶段,此阶段中,hello的程序文本中插入了它所引用的库。

第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:1.扫描(词法分析),2.语法分析,3.语义分析,4.源代码优化(中间语言生成),5.代码生成,目标代码优化
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

图 3-1

图 3-2
3.3 Hello的编译结果解析
3.3.1声明
.file声明源文件
.text后是代码段
.rodata以后是rodata节
.globl声明一个全局变量
.type用来指定是函数类型火是对象类型
.size声明大小
.long、.string声明long和string类型
.align声明对齐方式
3.3.2数据
hello.s中用到的数据类型有:
a) int sleepsecs:在C程序中被声明为全局变量,且已经被赋值,编译器处理时在.data节声明该变量,.data节存放已经初始化的全局和静态C变量。在下图中,可以看到,编译器首先将sleepsecs在.text代码段中声明为全局变量,其次在.data段中,设置对齐方式为4、设置类型为对象、设置大小为4字节、设置为long类型其值为2(long类型在linux下与int相同为4B,将int声明为long应该是编译器偏好)

图 3-3

b) int i:编译器将局部变量存储在寄存器或者栈空间中,在hello.s中编译器将i存储在栈上空间-4(%rbp)中,可以看出i占据了栈中的4B。

图 3-4

c) int argc:作为第一个参数传入,存储在%edi中

d) 常量:其他整形数据的出现都是以立即数的形式出现的,位于代码段中

e) 指针数组变量char * argv[]其地址内含的值为字符型变量:printf调用的第二个参数(保存在%rsi中),第三个参数(保存在%rdx中)分别为argv1,argv2。因此可以看出系统通过(%rbp-32)得到argv[0]的指针的地址、然后+8得到argv1的地址。同理,+16得到了argv2的地址。

图 3-5

f) 字符串常量:“Usage: Hello 学号 姓名!\n"和"Hello %s %s\n”:
如图可知在.LC0、.LC1段声明的字符串常量即为"Usage: Hello 学号 姓名!\n"和"Hello
%s %s\n",它们均在.rodata只读数据节中,其中\345…等类似这样的是汉字在UTF-8中的编码。其中汉字要占3个字节一个\代表一个字节。

图 3-6

3.3.3赋值

  1. int sleepsec = 2.5,这里涉及到类型转换,如3.3.4所述。
  2. 对循环变量i的赋值为直接立即数赋值,如图所示。

图 3-7

3.3.4类型转换
因为2.5是浮点数类型,而sleepsecs是int型,所以赋值为2.5时,值会向零舍入,正如图3-2中sleepsecs隐式地被赋值为2
3.3.5算术操作
for循环中的i++:

图 3-8
3.3.6关系操作
if条件判断:

图 3-9
for循环中i与10比较大小:

图 3-10
3.3.7数组/指针/结构操作
指针数组argv[]中,argv[0]指向输入程序的路径和名称,argv1和argv2分别表示两个字符串。其中char 数据类型占8个字节,因而系统通过(%rbp-32)得到argv[0]的指针的地址、然后+8得到argv1的地址。同理,+16得到了argv2的地址

图 3-11
3.3.8控制转移
if判断语句:

图 3-12
for循环:

图 3-13
3.3.9函数操作
1.main函数:
参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回。

2.printf函数:
参数传递:call、puts时只传入了字符串参数首地址;for循环中call、printf时传入了
argv1和argc2的地址。
函数调用:在for循环中被调用。

3.exit函数:
参数传递:传入的参数为1,再执行退出命令。
函数调用:if判断条件满足后被调用。

4.sleep函数:
参数传递:传入参数sleepsecs,传递控制call
sleep。
函数调用:在for循环中被调用。

5.getchar函数
传递控制:call getchar。
函数调用:在main中被调用。
3.4 本章小结
本章将hello.i文件转换为hello.s文件,并对hello.s文件进行了详细的解读。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将.s文件(这里指hello.s)翻译成机器语言指令,把这些指令打包成 一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件(这里是hello.o)中。hello.o文件是一个二进制文件。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o

图 4-1
4.3 可重定位目标elf格式
在终端输入readelf -a hello.o > hello.elf,获得hello.o的ELF格式文件:

图 4-2
1)ELF Header:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息

图 4-3
2)Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

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

图 4-5

图 4-6
结合图4-5中各符号的重定位条目,利用图4-6的重定位函数就可以对hello程序进行重定位符号引用
4).rela.eh_frame : eh_frame节的重定位信息

图 4-7
5).symtab:符号表,用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。

图 4-8
4.4 Hello.o的结果解析
输入objdump -d -r hello.o获得hello.o的反汇编代码:

图 4-9
1)分支转移:hello.o中反汇编代码跳转指令的操作数使用的不是hello.s中的段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

图 4-10

图 4-11
2)函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

图 4-12

图 4-13
3)全局变量的访问:在hello.s文件中,对于全局变量sleepsecs的访问,是sleepsecs(%rip),而在反汇编代码中是0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

图 4-14

图 4-15
4.5 本章小结
本章介绍了从汇编代码到可重定位目标文件的转换,hello.o的ELF格式及各节的作用,还简单分析了hello.s与hello.o异同。

第5章 链接
5.1 链接的概念与作用
概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:将可重定位目标文件链接成为可执行目标文件
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-1
5.3 可执行目标文件hello的格式
输入命令行readelf -a hello > hello.elf1得到hello的ELF格式文件:

图 5-2
1) ELF Header: 同图4-3相比,一是程序头大小改变,节头数量也增加了,二是没有了.rel节,因为可执行目标文件已经完成了重定位,三是可执行文件hello的ELF头中标明了入口点地址——程序运行执行的第一条指令的地址0x401080。

图 5-3
2)Section header:对hello中对所有的节信息进行了声明,其中包括size大小和在程序中的偏移量offset,因为是已链接的程序,所以根据标出的信息就可以确定程序实际被加载到虚拟地址的地址。

图 5-4
3)程序头:这是hello.o没有的:

图 5-5
4)Section .symtab:相比hello.o,多了很多符号,hello.o只有17个而hello.s有49个:

图 5-6
5.4 hello的虚拟地址空间
使用edb加载hello:

图 5-7
如图5-7所示,左下角的Data Dump显示的是数据段的详细信息。而左上窗口显示的是实际的程序里对应地址里的信息,相当于是图5-4、图5-5里段的对应地址里的详细内容。
5.5 链接的重定位过程分析
输入命令行objdump -d -r hello查看hello的ELF格式文件:

图 5-8
1.hello与hello.o的异同:
1).链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。

图 5-9
2).增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。

图 5-10
3).函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4).地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata和sleepsecs等全局变量的访问,是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

图 5-11
2.链接的过程为:链接器ld将各个目标文件组装在一起,把.o文件中的各个函数段按照一定规则累积在一起,比如解决符号依赖,库依赖关系,并生成可执行文件。
3.hello重定位过程:
重定位过程合并输入模块,并为每个符号分配运行时地址,主要有以下两步:
1).重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。包括hello.o在内的所有可重定位目标文件中的.data节被全部合并成一个节,这个节成为输出的可执行目标文件hello中的.data节。然后,连接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中每条指令和包括.rodata、sleepsecs等全局变量都有唯一的运行时内存地址了。
2).重定位节中的符号引用。链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。
5.6 hello的执行流程
(参数:1180300801 崔同发)
0x7f9b86e36030
0x7f9b86e449e0
0x401080 <_start>
0x4010b1 _libc_start_main
0x4010cd puts@plt
0x4010d7 exit@plt
0x40110a printf@plt
0x401117 sleep@plt
0x4010de getchar@plt
(随便输入一个数字后)
0x7f9b86c773c0 lib_2.29.so!exit
5.7 Hello的动态链接分析
跳转到hello程序的动态链接项目:global_offset表,在一开始lobal_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量:

图 5-12 do_init前

图 5-13 do_init后
5.8 本章小结
本章主要介绍了linux下链接的过程,链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。通过查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,更好地掌握了链接尤其是重定位的过程,但是我们知道链接并不止于此,hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。

第6章 hello进程管理
6.1 进程的概念与作用
进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
shell:一个交互型的应用级程序,它代表用户运行其他程序,是用户和系统内核沟通的桥梁。用户可以通过shell向操作系统发出请求,操作系统选择执行命令。
处理流程:1.用户输入一个命令
2. shell获取命令,判断是否是一个内置命令。若是,则立即执行,否则shell将其视作一个可执行文件。
3.shell执行一个可执行文件:调用fork函数创建一个新进程,并调用execve函数使其加载到新进程的上下文中。
4.若命令的末尾有”&”,则在后台执行,shell不会等待其完成,通过信号机制来回收它。若无,则在前台执行,shell通过waitpid函数来显示的等待其完成,并回收之。
6.3 Hello的fork进程创建过程
根据shell的处理流程,输入命令执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程Hello。Hello进程几乎但不完全与父进程相同,Hello进程得到与父进程用户级虚拟空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库、以及用户栈。Hello进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,Hello进程可以读写父进程中打开的任何文件。父进程和Hello进程最大的区别在于它们有不同的PID。
fork函数只被调用一次,却会返回两次。在父进程中,fork返回Hello进程的PID,在Hello进程中,fork返回0 。
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

图 6-1(来自教材《深入理解计算机系统》)
6.5 Hello的进程执行
hello的进程执行:在Hello执行的某些时刻,比如sleep函数,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。当内核调度了一个新的进程运行后,它就抢占Hello进程,并且使用上下文切换机制来将控制转移到新的进程。
Hello进程初始运行在用户模式中,直到Hello进程中的sleep系统调用,它显式地请求让Hello进程休眠,内核可以决定执行上下文切换,进入到内核模式。当定时器中断时,内核就能判断当前Hello休眠运行了足够长的时间,切换回用户模式
相关知识:
1.进程上下文信息,就是内核重新启动一个被抢占的程序所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。
2.进程时间片,是指一个进程和执行它的控制流的一部分的每一时间段。
3.用户模式和内核模式:处理器为了安全起见,不至于损坏操作系统,必须限制一个应用程序可执行指令能访问的地址空间范围。就发明了两种模式用户模式和内核模式,其中内核模式(上帝模式)有最高的访问权限,甚至可以停止处理器、改变模式位,或者发起一个I/O操作,处理器使用一个寄存器当作模式位,描述当前进程的特权。进程只有当中断、故障或者陷入系统调用时,才会将模式位设置成内核模式,得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全。
6.6 hello的异常与信号处理
hello执行过程中会出现的异常:
中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT
终止:信号SIGINT,默认行为是 终止

图 6-2 乱按

图 6-3 回车

图 6-4 Ctrl-z

图 6-5 Ctrl-C

图 6-6 ps

图 6-7 jobs

图 6-8 pstree

图 6-9 fg
6.7本章小结
本章简述了进程管理的一些简要信息,包括进程的概念作用、shell的基本原理、shell如何fork和execvehello进程、hello进程在执行时会遇到的情况以及它是怎么被执行的,从而使我们对hello执行过程中产生信号和信号的处理过程有了更多的认识

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
对于一个逻辑地址如何转化为线性地址分以下几步:
1.看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。
2.出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3.把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式存储管理将程序逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。每一个作业有一个页表,用来记录各个页在内存中所对应的块。
在linux下线性地址(linear address)(也称虚拟地址 virtual address),所以从从虚拟地址如何变换到物理地址主要是通过MMU进行地址翻译。
首先读入一个虚拟地址,将其划分为VPN和VPO,使用VPN在页表里查找对应的页表项。若其有效位为0,那么页面不存在,执行缺页处理操作。若其有效位为1,则将其的PPN加上PPO就得到了物理地址(PPO = VPO)。
为了加速地址变换的过程,,在地址变换机构中增设了一个具有并行查找能力的高速缓冲存储器——快表,又称联想寄存器(TLB),用来存放当前访问的若干页表项。

图 7-1(来自教材)
7.4 TLB与四级页表支持下的VA到PA的变换
VA到PA的变换主要以下几步组成:
将需要转化的虚拟地址(VA)将其划分为VPN和VPO,再将VPN分为前32位TLBT,后四位TLBI,依据TLBI找到TLB的组,TLBT进行索引位匹配,匹配成功找到PPN,加上PPO就得到了物理地址(PPO=VPO)。如果匹配不成功,则向页表查询。

虚拟地址前36位被分为4个VPN每个9位对应一到四级页表的偏移量。先从CR3中找到一级页表的位置,根据VPN1找到L2页表的位置,重复以上操作最后在四级页表中找到页的PPN,加上PPO就得到了物理地址(PPO = VPO)。

图 7-2 四级页表下的地址翻译
7.5 三级Cache支持下的物理内存访问
现在计算机通常使用高速缓存来实现物理内存访问,多级缓存能大幅提高访存的速度,具体步骤如下:
将PA分解成CT,CI,CO三部分。
根据CI找到合适的组,在组内寻找和CT相同的标志位,找到后查看其有效位,若为1,则hit。
hit之后根据CO将数据返回给CPU
如果没找到与CT相同的标志位或者找到了但是有效位为0,则向下一级缓存中查找(L2),若L2中同样miss,继续向下寻找L3,最后寻找主存(概率极小)。
在下一级中hit之后,在上一级缓存中找到一个空闲块或驱逐一个其他块来放置hit的新块,重置块内信息。

图 7-3
7.6 hello进程fork时的内存映射
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。Fork函数为新进程创建虚拟内存。创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行新程序hello.out的步骤:
删除已存在的用户区域:删除当前进程虚拟地址用户部分的区域结构
映射私有区域:为新程序的代码、数据、.bss和栈区域创建新的区域结构,同时标记为私有的写时复制的。代码和数据段映射到hello的.text及.data段,.bss请求二进制零,映射到匿名文件,其大小在程序头部表中,堆栈也是请求二进制零,初始长度为零。
映射共享区:hello与系统执行文件链接,如lib.so,这部分映射到共享区域。
设置程序计数器PC:设置当前进程上下文中的PC,指向entry point。

图 7-4
7.8 缺页故障与缺页中断处理
缺页故障:当mmu翻译虚拟地址时发现在页表项中,该页表项的有效位为0(这个数据还在磁盘中,未被加载到内存中),此时会触发缺页中断,进程陷入内核模式,进入异常处理程序,异常处理程序将一个页块大小的数据加载到内存中,之后将控制返还给源程序。如果选择的内存页被修改过,则要先将这个块交换会磁盘之后再将目标数据加载到内存页
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(如图7-5)。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

图 7-5
1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
隐式空闲链表:

图 7-6
这样的一种结构,主要是由三部分组成:头部、有效载荷、填充(可选);
头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据
简单的放置策略:
1)首次适配:从头搜索,遇到第一个合适的块就停止;
2)下次适配:从头搜索,遇到下一个合适的块停止;
3)最佳适配:全部搜索,选择合适的块停止。
分割空闲块:
适配到合适的空闲块,分配器将空闲块分割成两个部分,一个是分配块,一个是新的空闲块,如图

图 7-7
增加堆的空间:
通过调用sbrk函数,申请额外的存储器空间,插入到空闲链表中
合并空闲块:

  1. 合并空闲块的目的
    如图,虽然释放了两个3字节大小的数据空间,而且空闲的空间相邻,但是就是无法再分配4字节的空间了,这时候就需要进行一般合并:合并的策略是立即合并和推迟合并,立即合并,可能有不好的地方。如果我们马上合并上图的空间后又申请3字节的块,那么就会开始分割,释放以后立即合并的话,又将是一个合并分割的过程,这样的话推迟合并就有好处了。需要的时候再合并,就不会产生抖动了。

图 7-8
2. 带边界标记的合并

图 7-9
Knuth提出了一种边界标记技术,允许在常数时间内进行对前面快的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
显式空闲链表的基本原理:
根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
7.10本章小结
虚拟内存是对主存的一个抽象。本章通过对虚拟内存的了解,学会了TLB和四级页表支持下VA到PA的转换,以及得到了PA后,三级cache下的物理内存的访问过程。通过本章内容,更深入掌握了fork函数和exceve函数和虚拟内存的种种联系,最后还学会了动态内存分配的管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix
I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需要记住这个描述符。
2)Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek操作,显式地将改变当前文件位置为k。
4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到 k+n。给定一个大小为m字节的文件,当k>=m时执行读写操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。类似地,写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5)关闭文件:当应用完成了对文件的访问之后,它就会通知内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。无论一个进程因何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename,
int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回0,成功;返回-1,失败。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void
*buf, size_t n);
ssize_t write(int fd, const
void buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。
8.3 printf的实现分析
printf函数代码如下所示:
int printf(const char fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
(char
)(&fmt)
表示的是…可变参数中的第一个参数的地址。而vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息,到write系统函数,直到陷阱系统调用
int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
当hello调用getchar的时候,实际就是再从I/O设备加载数据,此时进程会向处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断,这个异常号标识了触发中断的设备。再从I/O设备中读取完数据之后,处理器注意到终端引脚的电压变高了,就从系统总线读取异常号,之后键盘中断处理子程序中,接受扫描码,将这个扫描码保存到系统的键盘缓冲区。当处理完成之后,就将控制返回给原程序的下一条指令。
8.5本章小结
本章学会了linux下IO设备的管理方法,了解了Unix IO和Unix IO函数,深入分析了printf函数和getchar函数的实现。

结论
hello的一生:最开始时,hello还是hello.c,它以字节序列的方式储存在文件中的。每个字节都有一个整数值,对应于某字符。为了是hello.c转换为hello并运行,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令接照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。从源文件到目标文件的转化是由编译器驱动程序完成的:GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,这个翻译过程可分为四个阶段完成(预处理生成hello.i、编译生成hello.s、汇编生成hello.o、连接生成hello)。此刻, hello.c源程序已经被编译系统翻译成了可执行目标文件hello,并被存放在磁盘上。要想在Unix系统上运行hello,我们将它的文件名输人到称为shell的应用程序中,shell将加载并运行 hello程序(为hello fork新进程,并领用execve来将可重定位目标文件载入,分配虚拟内存,映射私有和共享区域,修改PC使之指向hello的首地址),然后等待程序终止。hello程序在屏幕上输出它的消息,然后终止。hello的一生到此结束。
附件
hello.i 预处理得到的文件
hello.s 汇编文件
hello.o 可重定位目标文件
hello.elf hello.o的ELF格式文件
hello.elf1 hello的ELF格式文件
hello 可执行目标文件
参考文献
1 《深入理解计算机系统》
2 https://baike.baidu.com/item/预处理命令/10204389
3 https://blog.csdn.net/qq_43319321/article/details/85522719

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06 Mon 13 Mon 20 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.2.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值