题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 1190201613
班 级 1903009
学 生 范佳乐
指 导 教 师 吴锐
摘 要
本文主要以hello程序在Linux系统的生命周期为例,通过探讨从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程以及计算机系统是如何对hello进行进程管理、存储管理和I/O管理,旨在将课本知识与实例结合学习,更加深入地理解计算机系统的课程内容。
关键词:计算机系统;Hello程序;预处理;编译;汇编;链接;进程;虚拟内存;shell;信号;cache;I/O ;
第1章
概述… - 4 -
1.1 Hello简介… - 4 -
1.2 环境与工具… - 4 -
1.3 中间结果… - 4 -
1.4 本章小结… - 5 -
第2章
预处理… - 6 -
2.1 预处理的概念与作用… - 6 -
2.2在Ubuntu下预处理的命令… - 6 -
2.3 Hello的预处理结果解析… - 7 -
2.4 本章小结… - 8 -
第3章
编译… - 10 -
3.1 编译的概念与作用… - 10 -
3.2 在Ubuntu下编译的命令… - 10 -
3.3 Hello的编译结果解析… - 10 -
3.4 本章小结… - 15 -
第4章
汇编… - 16 -
4.1 汇编的概念与作用… - 16 -
4.2 在Ubuntu下汇编的命令… - 16 -
4.3 可重定位目标elf格式… - 16 -
4.4 Hello.o的结果解析… - 18 -
4.5 本章小结… - 19 -
第5章
链接… - 20 -
5.1 链接的概念与作用… - 20 -
5.2 在Ubuntu下链接的命令… - 20 -
5.3 可执行目标文件hello的格式… - 20 -
5.4 hello的虚拟地址空间… - 21 -
5.5 链接的重定位过程分析… - 22 -
5.6 hello的执行流程… - 24 -
5.7 Hello的动态链接分析… - 25 -
5.8 本章小结… - 25 -
第6章
hello进程管理… - 26 -
6.1 进程的概念与作用… - 26 -
6.2 简述壳Shell-bash的作用与处理流程… - 26 -
6.3 Hello的fork进程创建过程… - 26 -
6.4 Hello的execve过程… - 27 -
6.5 Hello的进程执行… - 27 -
6.6 hello的异常与信号处理… - 29 -
6.7本章小结… - 31 -
第7章
hello的存储管理… - 32 -
7.1 hello的存储器地址空间… - 32 -
7.2 Intel逻辑地址到线性地址的变换-段式管理… - 32 -
7.3 Hello的线性地址到物理地址的变换-页式管理… - 33 -
7.4 TLB与四级页表支持下的VA到PA的变换… - 33 -
7.5 三级Cache支持下的物理内存访问… - 34 -
7.6 hello进程fork时的内存映射… - 34 -
7.7 hello进程execve时的内存映射… - 34 -
7.8 缺页故障与缺页中断处理… - 35 -
7.9动态存储分配管理… - 35 -
7.10本章小结… - 36 -
第8章
hello的IO管理… - 37 -
8.1 Linux的IO设备管理方法… - 37 -
8.2 简述Unix IO接口及其函数… - 37 -
8.3 printf的实现分析… - 38 -
8.4 getchar的实现分析… - 38 -
8.5本章小结… - 38 -
结论… - 39 -
附件… - 40 -
参考文献… - 41 -
第1章 概述
1.1 Hello简介
P2P:From Program to
用户使用高级语言编辑成文本文件生成Hello.c->计算机经过cpp预处理文件对宏进行预处理生成Hello.i ->编译器编译为汇编语言,生成Hello.s -> 通过编译器汇编生成二进制文件Hello.o ->编译器链接函数库生成Hello可执行文件-> Linux系统中通过内置命令行解释器shell加载运行hello程序,为hello程序fork进程, 再调用execve,执行hello.o程序。
O2O:From Zero-0 to Zero-0
最开始程序不被分配内存空间,直到在shell中使用execve加载并执行该程序时,操作系统为程序分配一部分虚拟空间,将程序加载到虚拟空间所映射的物理内存空间中,然后执行目标程序。在Hello子进程运行完,进程正常终止,操作系统回收Hello子进程,删除子进程分配的所有资源。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:VirtualBox/Vmware11以上;Ubuntu 16.04 LTS
64位/优麒麟 64位 以上
开发与调试工具:vim,gcc,edb,readelf
1.3 中间结果
hello.i 预处理之后文本文件
hello.o 编译之后的汇编文件
hello.s 汇编之后的可重定位目标执行
hello.elf hello ELF
hello 链接器处理后的可执行文件
hello.out hello的反汇编代码
helloo.out hello.o的反汇编代码
helloo.elf hello.o ELF
1.4 本章小结
本章整体性地概括了hello在计算机系统中的生命过程,主要介绍了大作业需要使用的软硬件环境和开发工具
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在编译执之前进行的处理。包括三个方面:宏定义、文件包含、条件编译。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如第六行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
预处理的作用:预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
命令行:
gcc -no-pie -fno-PIC -E hello.c -o hello.i
2.3 Hello的预处理结果解析
经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,文件的内容增加,且仍为可以阅读的C语言程序文本文件,并且可以看到预处理后的.i文件将包含的库的全部宏加进文本中,即预处理操作。
2.4 本章小结
掌握了Linux预处理的指令,掌握了预处理的概念与其在hello.c程序处理过程中的作用。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念以及作用:编译(compilation , compile) 1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
命令行:gcc -S
hello.i -o hello.s
3.3 Hello的编译结果解析
程序头:
.file:源文件名
.text:代码段
.section,
.rodata:只读数据段
.align:对齐方式
.string:字符串常量
.globl main:主函数,可以在其他的模块中被引用的全局函数
.type
main,@function:指明main是函数类型
数据类型:
1字符串
输出字符串保存在.rodata节中,一共有两个字符串
图2.3字符串
2局部变量
main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。局部变量i存放在栈中,位于-0x4(%rbp)的空间中,大小为四个字节,并且赋值为0.
图2.4局部变量i
argc存储在edi寄存器中,然后将其放在-20(%rbp)的空间中,大小为四个字节.
图2.5 argc
3各种立即数
立即数直接体现在汇编代码中
4数组
程序中只有一个数组char*
argv[],它是main()函数的第二个参数,每个元素大小为8个字节,数组占据了连续的栈空间,可以使用相对寻址利用数组的首地址与每个元素的大小来寻址。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf。
图2.6argv
3汇编语言操作
1赋值操作
本程序里将i赋值为0
图2.7赋值操作
根据数据的类型有不同的的后缀
movb;传送字节
movw:传送字
movl:传送双字
movq:传送四字
2算术操作
本程序中:for循环里i++使用了算术操作
图2.8算式操作i++
3关系操作
本程序中进行了比较判断
图2.9判断argc!=4
图2.10判断i<8
前者使用立即数4与argc比较。argc存放在寄存器%edi中,之后又将%edi存放的值传送给-20(%rbp)
后者进行for 循环中进行条件判断,i的位置在%rbp-4
4类型转换
本程序中argv[3]是字符型,使用atoi将其转换为整型变量
图2.11类型转换
5控制转移
cmpl语句判断大小关系,判断argc是否等于4,如果相等,则跳入.L2中;如果不相等,则继续往下执行。
图2.12 等于跳转
for循环中判断i是否小于等于7,若是则跳转到.L4,执行循环体。若不是则继续往下执行。
图2.13小于等于7跳转
4函数操作
1.Main函数:参数传递:从内核中获取命令行参数和环境变量地址,传入参数argc和argv;函数调用,主函数,第一个执行的函数;函数返回,返回0 。
图2.14main函数
2.Printf函数:函数调用使用call指令调用;函数返回,将返回值存储在%rax中,使用指令ret返回 。
图2.15 printf函数
3.Exit函数:参数传递,将1放在%edi中作为参数传递;函数调用,使用call指令进行调用;函数返回,无返回
图2.16exit函数
4.Sleep函数:参数传递,将%eax赋值给%edi,%edi中的值作为第一个参数;函数调用,使用call指令进行调用;函数返回将返回值存储在%eax,使用ret指令返回。
图2.17 sleep函数
5.atoi函数: 将第四个命令行参数使用相对寻址找到,然后赋值给%rdi,作为atoi()函数的参数,通过call sleep@PLT指令来调用sleep函数。函数返回值为由字符型参数转化来的整型数。
图2.18 atoi函数
3.4 本章小结
本章介绍了汇编器将一个预处理文件hello.i翻译成hello.s的具体操作,在汇编语言的程序头、数据类型、算术操作、控制转移、函数操作等方面对hello.s进行了解释说明。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将汇编文件(hello.s)和翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在二进制文件中(hello.o)
作用:根据汇编指令和特定的平台,把汇编指令翻译成机器代码;合并各个节,合并符号表,生成.o文件
4.2 在Ubuntu下汇编的命令
命令行:gcc
hello.s -c -o hello.o
图3.1 汇编
4.3 可重定位目标elf格式
ELF头:文件格式
.text:已经编译程序的机器代码
.rodata:只读数据
.data:初始化的全局和静态变量
.bss:未初始化的全局和静态变量
.symtab:符号表
.rel.text:一个.text节中位置的列表
.rel.data:别模块引用或定义的所有全局变量和重定位信息
.debug:调试符号表
.line:行号与.text的机器指令之间的映射
.strtab:一个字符串表
(1)
ELF Header:用命令:readelf -h hello.o
图3.2
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,及其类型,节头部表的文件偏移,节头部表中条目的大小和数量
2节头部表
命令行:readelf -S
hello.o
节头部表:不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
图3.3
3重定位节
重定位节,描述了.text节中需要重定位的信息,即当链接器链接该目标文件时需要修改的信息。
符号表:符号表中有hello.o中定义和引用的函数和全局变量,信息,其中包含大小、类型、名字等信息
4.4 Hello.o的结果解析
objdump -d
-r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
图3.4
1 hello.s中的操作数是十进制,hello.o的反汇编代码的操作数是十六进制;
2在跳转时,hello.s是直接利用段的名字进行跳转,而在反汇编中则使用相对寻址来进行跳转
3 (2)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并根据该文件分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,最后分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
作用:合并各个.obj文件的节合并符号表,进行符号解析;进行符号地址的重定位;生成可执行文件
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
图4.1
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,及其类型,节头部表的文件偏移,节头部表中条目的大小和数量。
图4.2hello的ELF格式
图4.3
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0
图4.4
图4.5
根据节头部表,可以通过edb找到各个节的信息
5.5 链接的重定位过程分析
objdump -d
-r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
图4.6
不同:hello反汇编的代码有确定的虚拟地址,已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。hello反汇编的代码中多了很多的节以及很多函数的汇编代码
重定位的过程:
在完成符号解析之后,链接器将代码中的每个符号引用和符号定义关联,于是链接器就得到了代码节和数据节的大小。然后先进行重新定位,所有同类型的数据都会被重定位到一个节上,然后再对每个节赋予不同的地址,以及节中的每个符号,这个操作保证了程序中的每个节和每个符号都有相应的地址。之后对代码节和数据节中有地址操作的命令或者是数据进行修改,使他们能调用正确的地址。链接器完成符号解析后,将代码中每个符号引用和一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来,此时,链接器就知道了它的输入目标模块中的代码节和数据节的确切大小。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名称
程序地址
ld-2.27.so!_dl_start
0x7fce 8cc38ea0
ld-2.27.so!_dl_init
0x7fce 8cc47630
hello!_start
0x400500
libc-2.27.so!__libc_start_main
0x7fce 8c867ab0
-libc-2.27.so!__cxa_atexit
0x7fce 8c889430
-libc-2.27.so!__libc_csu_init
0x4005c0
hello!_init
0x400488
libc-2.27.so!_setjmp
0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp
0x7fce 8c884b70
–libc-2.27.so!__sigjmp_save
0x7fce 8c884bd0
hello!main
0x400532
hello!puts@plt
0x4004b0
hello!exit@plt
0x4004e0
*hello!printf@plt
–
*hello!sleep@plt
–
*hello!getchar@plt
–
ld-2.27.so!_dl_runtime_resolve_xsave
0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup
0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x
0x7fce 8cc420b0
libc-2.27.so!exit
0x7fce 8c889128
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
执行前:
图4.7
执行后:
图4.8
在执行完函数后,global_offset表由全0状态被赋值为相应的值
5.8 本章小结
本章介绍了链接的概念和作用,分析了可执行文件hello的ELF格式及其虚拟地址空间,分析了重定位、加载以及运行时函数调用顺序及动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程提供给应用程序的关键抽象如下:
一个独立的逻辑控制流。它提供一个假象,好像我们的程序独占地使用处理器。
一个私有的地址空间。它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:一个交互型的应用级程序。可以接受用户输入的命令,然后再系统中运行相应的程序,是用户与系统之间的桥梁。
处理流程: 1.在虚拟的shell界面上出现命令提示符($或#) ; 2.获取用户指令:获取用户在命令提示符后面输入的命令及其参数,并注意命 令输入的最大长度; 3.解析指令:对用户输入的命令进行解析,解析出命令名和参数;如果是内置命令则立即执行,否则调用相应的程序为其分配子程序并进行 4.寻找命令文件:每个命令的执行都必须依靠对应的可执行文件,这些文件的存放路径存放在用户的PATH环境变量里; 5.执行命令:可通过fork系统调用创建一 个进程来完成执行命令的任务,具体的命令执行用execv函数。 6.等待并回收子程序。
6.3 Hello的fork进程创建过程
我们在终端输入./hello
1190201613 范佳乐 1 , 命令行会对该命令进行解析,发现不是内置命令,则判断为可执行文件,然后终端程序会调用fork()函数创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的
逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
图5.1
6.4 Hello的execve过程
Execve函数加载并运行可执行文件,将hello加载到当前的进程中,将原先程序覆盖,释放掉原先程序的用户控件,加载hello程序到用户空间中,hello程序和源程序的进程相同,具有相同的PID。Execve加载了可执行程序的名字hello后调用启动代码,启动代码设置栈,将控制传递给hello的主函数
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
(1) 逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程,进程轮流使用处理器,。
(2) 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(3) 用户模式和内核模式:处理器通常使用一个寄存器描述了进程当前享有的特权,对两种模式区分。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
(4) 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
hello
sleep进程调度的过程:
-
当调用 sleep 之前,如果 hello 程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换
-
上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行
a) 保存以前进程的上下文
b)恢复新恢复进程被保存的上下文,
c) 将控制传递给这个新恢复的进程 ,来完成上下文切换。
-
hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入等待队列
-
定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,
-
进入内核状态执行中断处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
图5.2
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs
pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello程序出现的异常可能有: 中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出
(2)键盘上各种操作导致的异常
1正常执行hello程序的结果:当程序执行完成之后(以键入回车结束),进程回收
图5.3
2在程序输出2条info之后按下ctrl-z :
当按下 ctrl-z之后,shell 父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起。
图5.4
3在程序输出2条info之后按下ctrl-c :当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。
图5.5
4在程序运行中途乱按的结果:乱按只是将屏幕的输入缓存到 stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做shell若干条命令行输入。
图5.6
6.7本章小结
本节中介绍了进程异常信号的概念,对一场控制流的概念作了解释,并介绍了核心态和用户态两种模式,分析了进程的创建,程序的运行,上下文的切换,并发程序的机理,简要描述了一些异常,信号和其处理函数,描述了一些命令行的作用和shell工作简单原理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
物理地址:CPU 通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着 CPU 和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
(1) 将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。分段功能在实模式和保护模式下有所不同:
a) 实模式:逻辑地址=线性地址=实际的物理地址,即不设防。段寄存器存放真实段基址,同时给出32位地址偏移量,可以访问真实物理内存。
b) 保护模式:线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。32位段基址被称作选择符,段寄存器无法放下,用于引用段描述符表中的表项来获得描述符。
描述符表中的一个条目描述一个段,构造如下:
Base:基地址,32 位线性地址指向段的开始。
Limit:段界限,段的大小。
DPL:描述符的特权级,包括:0(内核模式)、-3(用户模式)。
所有的段描述符被保存在两个表中:全局描述符表GDT和局部描述符表LDT。gdtr 寄存器指向 GDT 表基址。
段选择符构造如下:
TI:0 为 GDT,1 为 LDT。Index 指出选择描述符表中的哪个条目,RPL 请求特权级。
在保护模式下,分段机制:段选择符在段描述符表-> Index ->目标描述符条目 Segment Descriptor->目标段的基地址 Base address+偏移量 offset=线性地址 Linear Address。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分成两部分,一部分是VPO一部分是VPN VPN作为索引,用于在页表中查询物理地址的一部分。 VPO与页表中查到的物理地址组合成为完整的物理地址。 在用VPN做索引来查找物理地址时会发生两种情况。 页命中:1、处理器生成一个虚拟地址,并把它传送给MMU 2、MMU生成PTE地址,并从高速缓存或主存请求得到它 3、高速缓存或主存向MMU返回PTE 4、MMU构造物理地址,并把它出送给告诉缓存或主存 5、高速缓存或主存返回所请求的数据给处理器
每一个线性地址被分解为,10位的面目录索引、10位的页表、12位的索引偏移: 然后根据线性地址前十位,找到索引项,根据页索引项来确定页表的地址。根据线性地址的中间十位,在页表中找到页的起始地址。将页的起始地址与线性地址中最后12位相加,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的虚拟寻址的缓存,其中每一行都保存着一个有单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号提取的
TLB由TLB标记,PTE虚拟地址被划分位:a1位 a2位 a3位 a4位
MMU先在TLB中寻找PTE;根据虚拟地址的后p+t-1位值作为组号,找到其对应的TLB的组,VPN剩下的位作为标记,与该组中的标记进行对比,如果存在标记相同切有效为为1的pte那么获得pte中的ppn如果不存在,那么将vpn发送给内存和cache。通过一个固定寄存器中的值获得以及页表的基地址,由a1位值作为索引,知道对应表中的值,将其作为二级页表的机制,由a2位值作为索引,直到有a4作为索引获得四级页表的pte的值,这样就可以得到vpn所对应的ppn的值,再由ppn+vpo就可以得到va对应的pa
7.5 三级Cache支持下的物理内存访问
将物理地址分为标记位、组索引、块内地址,通过组索引去确定数据在cache的哪组,然后通过标记为来确定是否是我们想要的数据,如果命中那就通过块内地址来选取想要的数据,如果不命中那就向下级cache寻找相应的数据,在找到数据后如果cache当前行中有空闲那就将数据放入空闲行,如果没有就替换一个最晚用到的行。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序加载并运行 hello 需要以下几个步骤:
(1) 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2) 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
(3) 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4) 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到MMU,这次 MMU 就能正常翻译 VA 了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个 变量brk(读做“break"),它指向堆的顶部。 分配器将堆视为一-组不同大小的块(block)的集合来维护。每个块就是一一个连续的虛拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicit allocator), 要求应用显式地释放任何已分配的块。
隐式分配器(implicit allocator), 另一方面,要求分配器检测-一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbagecollec-tor),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbagecollection)。 在程序运行时程序员使用动态内存分配器(如malloc)获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。
分配器的类型包括显式分配器和隐式分配器。前者要求应用显式地释放任何已分配的块,后者在检测到已分配块不再被程序所使用时,就释放这个块。
动态内存管理的策略包括:首次适配、下一次适配和最佳适配。 首次适配会从头开始搜索空闲链表,选择第一个合适的空闲块。搜索时间与总块数(包括已分配和空闲块)成线性关系。会在靠近链表起始处留下小空闲块的“碎片”。
下一次适配和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快,避免重复扫描那些无用块。 最佳适配会查询链表,选择一个最好的空闲块,满足适配,且剩余最少空闲空间。它可以保证碎片最小,提高内存利用率。
分割空闲块:适配到合适的空闲块,分配器将空闲块分割成两个部分是分配块,一个是新的空闲块。 增加堆的空间:通过调用sbrk函数,申请额外的存储器空间,插入到空链表中。 合并空闲块:合并的策略包括立即合并和推迟合并——即合并即释放完就合并,但这样可能导致没必要的分割重复;推迟合并即需要的时候再合并,这样可以避免抖动的产生。
7.10本章小结
本章主要介绍了hello的存储器地址空间、段式管理、页式管理,以及VA到PA的变换、说明了如何使用物理内存访问cache。在将进程使用到的fork和execve的内存映射情况加以描述
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix
IO接口及其函数
Unix I/O 接口统一操作:
(1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个
I/O 设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
(3) 改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。应用程序能够通过执行seek,显式地将改变当前文件位置 k,例如各种fread或fwrite。
(4) 读写文件:
i.
读操作就是从文件复制n>0个字节到内存。从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件。当k>=m时,触发EOF。
ii.
写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k=k+n。
(5) 关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
(1) int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的(即fopen的内层函数)。open函数将filename(文件名,含后缀)转换为一个文件描述符(C中表现为指针),并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件(读或写或两者兼具),mode参数指定了新文件的访问权限位(只读等)。
(2) int close(fd),fd是需要关闭的文件的描述符(C中表现为指针),close 返回操作结果。
(3) ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4) ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用read读取存储在键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章讲述了IO设备的管理方法,IO接口及其函数,最后分析了printf和getchar函数的实现方法。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello程序虽小,但它运行的过程却凝聚着计算机行业发展百年的精华,令人感到震撼。
(1)
编写
(2)
预处理:将hello.c调用的所有外部的库展开,所有的宏定义替换,合并到一个hello.i文件中
(3)
编译:将 hello.i 编译成为汇编文件 hello.s
(4)
汇编:将 hello.s 会变成为可重定位目标文件 hello.o
(5)
链接:将 hello.o 与可重定位目标文件和动态链接库链接成为可执行目
标程序 hello
(6)
运行:在shell(terminal)中输入./hello 1190201613 范佳乐
(7)
创建子进程:shell父进程调用fork函数为hello创建子进程
(8)
运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序,开始载入物理内存,然后CPU进入main函数执行程序。
(9)
执行指令:CPU为hello分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
(10) 访问内存:MMU将程序中使用的虚拟内存地址,通过页表映射成物理地址。
(11) 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
(12) 信号:如果运行途中键入ctr-c、ctr-z,则调用shell的信号处理函数分别停止、挂起。
(13) 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 预处理之后文本文件
hello.o 编译之后的汇编文件
hello.s 汇编之后的可重定位目标执行
hello.elf hello ELF
hello 链接器处理后的可执行文件
hello.out hello的反汇编代码
helloo.out
hello.o的反汇编代码
helloo.elf hello.o ELF
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴.
空间控制技术[M].
北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C].
北京:中国科学出版社,1999.
[3] 赵耀东.
新时代的工业工程师[M/OL].
台北:天下文化出版社,1998
[1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖.
空间交会控制理论与方法研究[D].
哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H.
Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in
the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23].
http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] Bryant,R.E 深入理解计算机系统第三版
(参考文献0分,缺失 -1分)