计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1190201215
班   级 1903007
学 生 冯开来    
指 导 教 师 吴锐

计算机科学与技术学院
2021年5月
摘 要
本文通过对一个简单程序 hello 的一生的分析,经过预处理、编译、汇编、链接等一系列操作形成hello进程,再通过程序对控制流的管理、内存空间的分配、信号的处理、对 I/O 设备的调用彻底解释hello从创建到结束的过程,进而加深对计算机系统的理解。

关键词:程序的生命周期 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简介
P2P: From Program to Process
用户利用高级语言输入程序形成hello.c,经过cpp预处理形成hello.i,再经过ccl编译形成汇编语言程序hello.s,然后经过as转换为机器语言指令,形成可重定位目标程序hello.o,最后通过ld与库函数链接并重定位,形成可执行目标文件hello。Hello通过shell运行加载,fork产生子进程,于是hello从program变为了process。
020: From Zero-0 to Zero -0
Hello产生子进程后,通过execve进行加载,先删除当前虚拟地址已存在的数据结构,为hello的代码、数据、bss等创建区域,然后映射共享区域,设置程序计数器,进入main函数,CPU分配时间片执行逻辑控制流。执行过程中,虚拟内存为进程提供独立的空间;存储结构层层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O设备通过描述符与接口实现了hello的输入输出。多方面合作配合之下,hello完成执行。然后,shell回收hello进程,删除hello的所有痕迹,释放运行中占用的内存空间。这就完成了020过程。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;8G RAM;256GHD Disk
1.2.2 软件环境
Windows10 64位;Vmware 14;Ubuntu 16.04 LTS 64位;
1.2.3 开发与调试工具
gcc, readelf, objdump, edb, ld, gedit
1.3 中间结果
hello.i:hello.c的预处理文件
hello.s:hello.i的编译文件
hello.o:hello.s的汇编文件
helloo.elf:hello.o文件的ELF格式
hello.objdump:hello.o文件的反汇编文件
hello.elf:hello文件的ELF格式
hello.objdumpp:hello文件的反汇编文件
hello:可执行文件
1.4 本章小结
本章简单介绍了hello从程序到执行,再到结束的整个过程,并说明了本次应用的工具和中间生成的文件信息。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp) 根据以字符#开头的命令,修改原始的C 程序,形成完整的文件。C语言的预处理主要有三个方面的内容: 1.宏定义;(#define 标识符 文本) 2.文件包含;(#include “文件名”) 3.条件编译。(#ifdef,#else,#endif)
作用:

  1. 宏替换。例如:#define a 10,则在预处理时将所有的a变为10。
  2. 文件包含。预处理器读取头文件中的内容,并插入到程序文本中。例如:#include <stdio.h>则将stdio.h的代码扎入到hello中。
  3. 删除注释。
  4. 条件编译。根据某个条件判断进行静态编译。主要有#ifdef, #else, #elif, #endif, #ifndef等条件语句。
    2.2在Ubuntu下预处理的命令
    gcc -E hello.c -o hello.i

图2-1.预处理命令
2.3 Hello的预处理结果解析
Hello.i共3060行,main函数在第3047行,hello.i将头文件中的内容引入,例如声明函数、定义结构体、定义变量、定义宏等内容。预处理将头文件中的内容全部放到hello.i文件中,方便编译等操作。

图2-2.hello.i部分代码
2.4 本章小结
本章介绍了程序hello的预处理部分,包括预处理的概念、功能,并对预处理的结果进行解析对比。

第3章 编译
3.1 编译的概念与作用
概念:将hello.i转变为hello.s,即将预处理后的文件生成汇编语言程序。
作用:编译器经过词法分析、语法分析、语义分析等过程,在检查无错误后将代码翻译成汇编语言,得到的汇编语言代码可供编译器进行生成机器代码、链接等操作。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

图3-1.编译命令
3.3 Hello的编译结果解析

图3-2.汇编语言程序前半部分

图3-3.汇编语言程序后半部分
3.3.1.常量
在hello.c中,“Usage: Hello 学号 姓名 秒数!”为常量。在图3-2中,我们可以看到被编译为
.LC0:
.string “\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201”
该结果表示编译对中文字符进行了utf-8编码,并存储在只读代码区的.rodata节,在程序运行时会直接通过寻址找到常量。
3.3.2.变量
不同类型的变量在不同位置定义,局部变量在堆上进行定义和释放,初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节。
在hello.c中,存在局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。根据图3-2可知,局部变量i放在栈上-4(%rbp)的位置。
.L2:
movl $0, -4(%rbp)
jmp .L3
3.3.3.类型
编译器对类型的体现通过申请不同大小的空间,进行不同的操作等方式。例如char大小为1个字节,那么只会申请1个字节的空间,而int大小为4字节,则会申请4个字节的空间。例如int i只有4个字节的空间。
3.3.4.赋值
在hello.c中,我们对i赋初值,
根据图3-3,
对i:movl $0, -4(%rbp)
局部变量赋值采取MOV指令,根据不同大小的数据类型有movb、movw、movl、movq等。
3.3.5.类型转换
隐式转换:隐式转换就是系统默认的、不需要加以声明就可以进行的转换数据类型自动提升。显示转换:程序通过强制类型转换运算符将某类型数据转换为另一种类型。
3.3.6.算术操作
+转换成add,-转换成sub,*转换成imul,/转换成div。
在hello.c中,存在i++,根据图3-3可知,汇编为addl $1, -4(%rbp)。
同时,根据argv首地址获得argv[1]和argv[2]也通过加减操作:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
3.3.7.关系操作
hello.c中有argc!=4,根据图3-3可知,汇编语言为cmpl $4, -20(%rbp)。
hello.c中有i<10,根据图3-3可知,汇编语言为cmpl $8, -4(%rbp)。
所以,编译器通常通过将比较编译为cmp指令实现。根据不同的数据大小,有cmpb、cmpw、cmpl和cmpq。比较之后,通过jmp系列指令跳转。

                   图3-4.jump系列指令

3.3.8.数组/指针操作
对数组的索引相当于在第一个元素地址的基础上通过加索引值乘以数据大小来实现。在hello.c中,存在char *argv[],根据图3-3可知,根据argv首地址获得argv[1]和argv[2]需要通过加减操作:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
3.3.9.控制转移
hello.c中存在if,for语句,在汇编代码中,采取cmp比较执行条件跳转指令进行执行。
例1:cmpl $4, -20(%rbp)
je .L2
例2:cmpl $8, -4(%rbp)
jle .L4
3.3.10.函数操作
hello.c中包括main函数,printf函数,sleep函数,getchar函数。
首先,内核shell获取命令行参数和环境变量地址,执行main函数,在main中需要调用其它函数,在main中为被调用函数分配栈空间。调用函数需要借助栈,先将返回地址压入栈中,并将PC设为被调用函数的起始地址,然后调用。返回时,先从栈中弹出返回地址,再PC设置为该返回地址。return正常返回后,leave恢复栈空间。
由图3-2,3-3可知,分别调用函数为:
call puts@PLT
call exit@PLT
call printf@PLT
call sleep@PLT
call getchar@PLT
3.4 本章小结
本章介绍了编译的概念和功能,重点介绍编译器将hello.i转变为hello.s的过程,并对每种数据和操作都进行了概述。

第4章 汇编
4.1 汇编的概念与作用
概念:从hello.s转换为hello.o,即将汇编语言程序变为机器语言二进制程序。
作用:将汇编指令转换成机器可以直接读取分析的机器指令,用于后续的链接。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

图4-1.汇编命令
4.3 可重定位目标elf格式
命令为readelf -a hello.o > helloo.elf
4.3.1 ELF头
ELF头包括生成该文件的系统的字的大小和字节顺序, ELF头的大小,目标文件的类型(如可重定位、可执行或共享的),机器类型(如x86-64),以及节头部表中条目的大小和数量等信息。

图4-2.ELF头
4.3.2节头
节头记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。

图4-3.节头
4.3.3 重定位节
当链接器把这个目标文件和其他文件组合时,需要修改表中的这些位置。一般,调用外部函数或者引用全局变量的指令都需要修改。
由图4-4可知,在hello.o的重定位节中包含了main函数调用的puts、exit、printf、sleep、getchar函数以及全局变量sleepsecs,还有只读区域.rodata节。表格记录了它们的偏移量、信息、类型、符号值、符号名称及加数。另用rela.eh_frame记录了.text的信息。
若重定义类型为R_X86_64_PC32,重定位一个使用32位PC相对地址的引用。若若重定义类型为R_X86_64_32,重定位一个使用32位绝对地址的引用。根据重定位条目和重定位算法即可得到相应的重定位位置。

图4-4.重定位节
重定位算法:
foreach section s
{
foreach relocation entry r
{
refptr = s + r.offset;
if (r.type == R_386_PC32)
{
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr);
}
if (r.type = R_386_32)
*refptr = (unsigned)(ADDR(r.symbol) + *refptr);
{
}

4.3.4 符号表
用于存放程序中定义和引用的函数和全局变量的信息。

4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello.objdump

图4-6.反汇编代码
将图4-6与图3-2,3-3对比发现,大多数代码是一样的,有以下几点不同:
(1) hello.s中包含.type .size .align以及.rodata只读数据段等信息,而hello.objdump中只有函数的相关内容。
(2) 分支转移。hello.s主要借助.L0,.L1等转移,而hello.objdump直接借助地址进行跳转(未链接的地址)
(3) 函数调用。hello.s中直接调用函数的名称,而hello.objdump中利用下一条地址相对函数起始地址的偏移量,链接重定位后才能确定地址。
4.5 本章小结
本章介绍了汇编的概念及其作用,并通过readelf查看各个节的信息,最后生成反汇编文件与编译文件进行对比,充分理解汇编过程。
(第4章1分)

第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

图5-1.链接命令
5.3 可执行目标文件hello的格式
readelf -a hello > hello.elf

图5-2.各节起始地址和大小前半部分

图5-3.各节起始地址和大小后半部分
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

图5-4.ELF内容
通过ELF可知,程序从0x00400000到0x00400fff。

图5-5.program headers
根据图5-5,可知包含:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
其余的节的内容是存放在0x00400fff后面。
5.5 链接的重定位过程分析
objdump -d -r hello

图5-6.链接的重定位
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在 hello 到 hello.o 中,首先是重定位节和符号定义,链接器将所有输入到 hello 中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data 节被全部合并成一个节,这个节成为 hello 的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改 hello 中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
对比图5-6 hello与图4-6 hello.o,发现有以下几个不同:
(1) hello中包含init,puts,pringtf,getchar等函数,hello.o中只有.text。
(2) hello.o中是相对偏移地址,hello为虚拟内存地址。
(3) hello中增加了外部函数。
(4) hello中调用为call+函数名,hello.o中为call+相对偏移地址。
5.6 hello的执行流程
程序名称 程序地址
ld-2.27.so!_dl_start 0x7fce 8cc3 8ea0
ld-2.27.so!_dl_init 0x7fce 8cc4 7630
libc-2.27so!_libc_start_main 0x7fce 8c86 7ab0
-libc-2.27.so!_cxa_atexit 0x7fce 8c88 9430
-libc-2.27.so!_libc_csu_init 0x4005c0
hello!_start 0x400500
ld-2.27.so!_setjmp 0x7fce 8c88 4c10
ld-2.27.so!exit 0x7fce 8c88 9128

5.7 Hello的动态链接分析
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
调用 dl_init 之前的全局偏移表:

调用 dl_init 之后的全局偏移表:

由图可知,已经发生动态链接,GOT条目已改变。
5.8 本章小结
本章介绍了链接的概念和功能,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位、动态链接进行深入的分析。

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。
作用:每次用户通过向 shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell 是一个交互型的应用级程序,它代表用户运行其他程序。
处理流程:

  1. 从终端读入命令
  2. 切分字符串得到各种参数
    3.判断是否为内置命令
    3.1 若是,立即执行
    3.2 若不是,调用相应的程序为其分配子进程并运行
  3. 接受键盘输入信号,并对这些信号进行相应处理
    6.3 Hello的fork进程创建过程
    进程的创建:父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的id。fork后调用一次返回两次,在父进程中fork会返回子进程的PID,在子进程中fork会返回0;父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
    对于hello:

图6-1.进程创建

6.4 Hello的execve过程
创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序,即 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置 PC 指向_start 地址,_start 最终调用 hello 中的 main 函数。execve 函数加载并运行可执行目标文件 filename, 且带参数列表 argv 和环境变量列表 envp 。只有当出现错误时,例如找不到 filename, execve 才会返回到调用程序。所以,与 fork 一次调用返回两次不同, execve 调用一次并从不返回。
6.5 Hello的进程执行
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

图6-2.进程调度
由图6-2可知,hello在前台printf中,由于调用sleep,进入后台内核模式,开始sleep2.5s,然后中断信号,再回到前台继续执行下一个操作。
6.6 hello的异常与信号处理
Hello 在执行的过程中,可能会出现处理器外部 I/O 设备引起的异常,执行指令导致的陷阱、故障和终止。第一种被称为外部异常,常见的有时钟中断、外部设备的 I/O 中断等。第二种被称为同步异常。陷阱指的是有意的执行指令的结果,故障是非有意的可能被修复的结果,而终止是非故意的不可修复的致命错误。
在发生异常时会产生信号。例如缺页故障会导致 OS 发生 SIGSEGV 信号给用户进程,而用户进程以段错误退出。常见信号种类如下所示。
ID 名称 默认行为 相应事件
2 SIGINT 终止 来自键盘的中断
9 SIGKILL 终止 杀死程序
11 SIGSEGV 终止 无效内存引用
14 SIGALRM 终止 定时器信号
17 SIGCHILD 忽略 子进程停止或终止

1.正常运行

  1. 执行中乱按

3.CTRL+C

  1. CTRL+Z
  2. CTRL+Z 后 ps

6.CTRL+Z 后 jobs

7.CTRL+Z 后 pstree

8.CTRL+Z 后 fg 再停止

9.CTRL+Z 后 kill ps

6.7本章小结
本章介绍了进程的概念和作用,shell的基本操作以及各种内核信号和命令,并总结了shell 新建子进程的过程、execve的执行进程以及hello进程如何在内核和前端中反复跳跃运行的。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分,即hello.o 里面的相对偏移地址。
线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间,即hello里面的虚拟内存地址。
虚拟地址:CPU 通过生成一个虚拟地址,即hello里面的虚拟内存地址。
物理地址:计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由 16 位字段组成,前 13 位为索引号。索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前 13 位,在这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。base 字段,描述了段开始的线性地址,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由 T1 字段决定使用哪个。

图7-1.段选择符
以下是具体的转化步骤:

  1. 给定一个完整的逻辑地址,[段选择符:段内偏移地址]
  2. 看段选择符 T1,知道要转换的是 GDT 中的段还是 LDT 中的段,通过寄存器得到地址和大小。
  3. 取段选择符中的 13 位,再数组中查找对应的段描述符,得到 BASE,就是基地址。
  4. 线性地址等于基地址加地址偏移量。
    对于偏移量,基址寄存器还是变址寄存器有不同的计算方法,后者需要经过乘比例因子等处理。
    7.3 Hello的线性地址到物理地址的变换-页式管理
    Intel处理器从线性地址到物理地址的变换通过页式管理实现。

图7-2.二级页表结构
具体步骤为:
1、从CR3中取出进程的页目录的地址,取出其前20位,这是页目录的基地址;
2、根据取出来的页目录的基地址以及线性地址的前十位,进行组合得到线性地址的前十位的索引对应的项在页目录中地址,根据该地址可以取到该地址上的值,该值就是二级页表项的基址;
3、根据第二步取到的页表项的基址,取其前20位,将线性地址的10-19位左移2位,按照和第2步相同的方式进行组合就可以得到线性地址对应的物理页框在内存中的地址在二级页表中的地址的起始地址,根据该地址向后读四个字节就得到了线性地址对应的物理页框在内存中的地址在二级页表中的地址,然后取该地址上的值就得到线性地址对应的物理页框在内存中的基地址;
4、根据第3步取到的基地址,取其前20位得到物理页框在内存中的基址,再根据线性地址最后的12位的偏移量得到具体的物理地址,取该地址上的值就是最后要得到值;
7.4 TLB与四级页表支持下的VA到PA的变换

TLB通过虚拟地址VPN部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU在读取PTE时会直接通过TLB,如果不命中再从内存中将PTE复制到TLB。

图7-4.页表翻译
根据图7-4,我们可知Core i7 MMU 如何使用四级页表来将虚拟地址翻译成物理地址。36 位的 VPN 划分为 4 个 9 位的片,每个片对应一个页表的偏移量。CR3寄存器存有 L1 页表的物理地址。VPN1 提供一个到 L1 PET 的偏移量,这个 PTE包含 L2 页表的基地址。最终经过L4后,将页的物理地址给PPN。VPN2 提供一个到 L2PET 的偏移量。

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

图7-5. 三级Cache支持下的物理内存访问
CPU 发出一个虚拟地址在 TLB 里搜索,如果命中,直接发送到 L1cache 里;如果没有命中,就现在加载到表里之后再发送过去,到了 L1 中,寻找物理地址又要检测是否命中,如果没有命中,就向 L2/L3 中查找。这就用到了 CPU高速缓存。
在层与层之间读取数据时,首先在高速缓存中查找所需字w的副本。如果命中,立即返回字w给CPU。如果不命中,从存储器层次结构中较低层次中取出包含字w的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字w。
读取数据分为三种:
1)直接映射高速缓存
直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存命中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否命中然后抽取的过程分为三步:(1)组选择;(2)行匹配;(3)字选择。
组选择即从w的地址中间抽取出s个索引位,将其解释为一个对应组号的无符号整数,从而找到对应的组;行匹配即对组内的唯一一行进行判断,当有效位为1且标记位与从地址中抽取出的标记位相同则成功匹配,否则就得到不命中;而字选择即在行匹配的基础上通过地址的后几位得到块偏移,从而在高速缓存块中索引到数据。

图7-6.组选择

图7-7.行匹配和字选择
2)组相联高速缓存
组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。
3)全相联高速缓存
全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E =C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。地址中不存在索引位,地址只被划分为一个标记位和一个块偏移。行匹配和字选择同组相联高速缓存。
写入数据时,有两种方法来更新w在层次结构中紧接着低一层中的副本,分别是直写和写回:
1)直写
立即将w的高速缓存块写回到紧挨着的低一层中。虽然简单,但是每次写都会引起总线流量。其处理不命中的方法是非写分配,即避开高速缓存,直接将这个字写到低一层去。
(2)写回
尽可能地推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层中。虽然能显著地减少总线流量,但是增加了复杂性,必须为每个高速缓存行增加一个额外的修改位,表明是否被修改过。写回处理不命中的方法是写分配,加载相应低一层中的块到高速缓存中,然后更新这个高速缓存块,试图利用写的空间局部性,但会导致每次不命中都会有一个块从低一层传到高速缓存。
通过这样的Cache读写机制,实现了从CPU寄存器到L1高速缓存,再到L2高速缓存,再到L3高速缓存,再到物理内存的访问,极大地提高了速度和效率。
7.6 hello进程fork时的内存映射

图7-8.fork进程
当 fork 函数被 shell 调用时,内核为 hello 进程创建各种数据结构,并分配给它一个唯一的 PID 。为了给 hello 进程创建虚拟内存,它创建了 hello 进程的 mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当 fork 在 hello 进程中返回时,hello 进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含可执行目标文件hello中的程序,加载、运行 hello 需要以下步骤:

  1. 删除已存在的用户区域。删除 shell 虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域。为 hello 的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text 和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域。如果 hello 程序与共享对象(或目标)链接,比如标准 C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
    经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。
    7.8 缺页故障与缺页中断处理

图7-9.缺页
DRAM 缓存不命中称为缺页(page fault). 图7-9为缺页之前我们的示例页表的状态。CPU 引用了 VP 3 中的一个字,VP 3 并未缓存在 DRAM 中。地址翻译硬件从内存中读取 PTE 3, 从有效位推断出 VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP3 中的 VP4 。如果 VP4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改 VP4 的页表条目,反映出 VP 4 不再缓存在主存中这一事实。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜索完以后最佳的方案,当然这种的会在搜索速度上大有降低。
策略:这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。

图7-10.使用边界标记的堆块
7.10本章小结
本章结合书上第六章和第九章的知识(主要是第九章),介绍了存储器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射,并详细描述了系统如何应对缺页异常,最后描述了malloc的内存分配管理机制。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的 I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有
的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射
为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix
I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是
Unix I/O 接口
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。返回:若成功则为写的字节数,若出错则为-1。
8.3 printf的实现分析

图8-1.printf函数
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
我们发现,printf调用了两个外部函数,一个是vsprintf,还有一个是write。
通过查看这两个函数,可知vsprintf函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数是将buf中的i个元素写到终端的函数。
所以,vsprintf 的作用就是格式化。它接受确定输出格式的格式字符串 fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从 vsprintf生成显示信息,到 write 系统函数,直到陷阱系统调用 int 0x80 或 syscall。显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。
8.4 getchar的实现分析
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(–n >= 0)?(unsigned char) *bb++ : EOF;
}
根据上面的getchar函数可知:
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar的大概思想是读取字符串的第一个字符之后再进行返回操作。

8.5本章小结
本章介绍了Unix I/O,通过LinuxI/O设备管理方法以及Unix I/O接口及函数了解系统级I/O的底层实现机制,并通过对printf和getchar函数的底层解析加深对Unix I/O以及异常中断等的了解。

(第8章1分)
结论
hello的一生主要经过一下过程:

  1. 程序员通过 I/O 设备在编译器中通过高级语言写下 hello.c,并存储在内存中;
  2. 预处理器通过对头文件、注释的处理得到修改了的文本文件 hello.i;
  3. 编译器翻译成汇编语言得到 hello.s;
  4. 汇编器处理得到可重定位目标文件 hello.o;
  5. 链接器将 hello.o 和如 printf.o 的其他可重定位目标文件链接得到可执行目标文件 hello;
    6.在 shell 里运行hello程序;
  6. fork 创建子进程,shell 调用;
  7. 运行程序,调用 execve;
  8. 执行指令,为 hello 分配时间片,hello 执行自己的逻辑控制流;
    10.三级cache访问内存,将虚拟地址映射成物理地址;
  9. 信号、异常控制流,hello 对不同的信号会执行不同操作;
  10. kill hello,回收子进程;
    这一生,和人类类似,从出生到一点点成长,一步步长大,并且是一个老实听话的人,如果中途遇到其他事情,便会立刻的执行。等它忙碌了一辈子后,最终还是要入土为安,被回收走,不留一点痕迹。

感悟:
总体回顾hello的一生,觉得计算机系统也是有生命的,这一生也充满了很多故事。这一生我们理解起来很艰难,但是我们可以通过对 hello一生的追溯来学习计算机对一个程序的处理过程,有助于我们以后写出更好的代码,并且在对 hello 一步步操作的学习中我们加深了对计算机系统的理解。
当做这个大作业时,彻底将看似孤立的每一章连了起来,知道了它们的前因后果,并且觉得这门课还是挺有意思的。其实,如果这个大作业能在开学初的时候布置下来,随着章节的学习一步步地做,可能会消化理解得更好一些。

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i:hello.c的预处理文件
hello.s:hello.i的编译文件
hello.o:hello.s的汇编文件
helloo.elf:hello.o文件的ELF格式
hello.elf:hello文件的ELF格式
hello:可执行文件

(附件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.
(参考文献0分,缺失 -1分)

PA-1190201215-冯开来(计算机系统大作业)相关推荐

  1. 计算机系统大作业:Hello's P2P

    计算机系统大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术 指 导 教 师 史先俊 计算机科学与技术学院 2019年12月 摘 要 本文介绍了一个.c源程序在计算机系统中从编 ...

  2. 哈工大2022计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 指 导 教 师 吴锐 计算机科学与技术学院 2021年5月 摘  要 本文介绍了hello程序的一生. ...

  3. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

  4. 哈尔滨工业大学计算机系统大作业2022春

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学   号 120L02**** 班   级 2003005 学       生 无敌飞龙 指 导 教 ...

  5. 哈工大csapp计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022009 班   级 2003007 学       生 李沛聪 指 导 教 ...

  6. 哈工大计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学   号 120L022123 班   级 2003007 学       生 鹿鸣 指 导 教 师 ...

  7. 哈工大2022计算机系统大作业---程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L021923 班    级 2003006 学       生 甄自镜 指 导 ...

  8. HIT计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学 号 120L022305 班       级 2003008 学       生 XXX 指 导 ...

  9. 哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题          目程序人生-Hello's P2P 专          业 计算机科学与技术 学       号120L022401 班          级 200300 ...

最新文章

  1. oracle实例没有连到监听上6,oracle LISTENER未监听到oracle实例问题解决
  2. 计算机科学导论链式存储,计算机科学导论3.pdf
  3. 项目配置laungchImage
  4. webRTC开启摄像头
  5. KubeMeet 直播 | 现场直击大规模集群、混合环境下的云原生应用交付难题
  6. 转发: Visual Studio 2005常用插件
  7. 【Ubuntu】ubuntu物理机安装方法:U盘安装
  8. toad查看oracle的plsql包,Oracle logminer 分析redo log(TOAD与PLSQL)
  9. 修改linux默认启动级别(包括Ubuntu)
  10. Netty工作笔记0063---WebSocket长连接开发2
  11. Datawhale 零基础入门数据挖掘-Task2 数据分析
  12. 百面机器学习—模型复杂度与模型的方差、偏差的关系
  13. linux ruby安装目录,SuseLInux下安装Ruby 及 Rails遇到的问题及解决方法
  14. VC6++配置汇编环境和第一个简单程序
  15. 树莓派安装qq linux,在(Raspberry Pi)树莓派上安装NodeJS
  16. OFDM载波间隔_NBloT上行子载波
  17. 北京地区常用dns地址解析速度快
  18. STEP2——《数据分析:企业的贤内助》重点摘要笔记(四)——数据清洗
  19. MLX90640开发笔记(十)成果展示-红眼睛相机
  20. 中国男人配不上中国女人?

热门文章

  1. MySQL之建表时[Err] 1050 - Table ‘users‘ already exists异常解决方法
  2. 线性回归算法在 java 中是如何实现的?
  3. JavaScript (递归函数)
  4. 74LS85 比較器 【数字电路】
  5. 用Java数组实现选队长的游戏
  6. 出租车GPS数据分析的常规方法
  7. STM32外部中断的关闭和打开
  8. 2022年最新云开发去水印小程序源码
  9. Github项目分享——tabby
  10. python speech语言模块安装