哈工大计算机系统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 1190201021
班   级 1903011
学 生 马文韬  
指 导 教 师 史先俊

计算机科学与技术学院
2021年5月
摘 要
本文主要利用计算机系统课程中所学的知识,结合深入理解计算机系统这本书,在linux下逐步模拟了hello.c程序的P2P过程并进行分析,展示了hello在linux系统下的生命周期。
关键词:csapp;hello的生命周期;linux;P2P;

(摘要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
在linux中,hello.c经过cpp的预处理,ccl的编译,as的汇编,ld的链接,最终成为了可执行文件hello,在shell中启动,fork产生一个子进程,hello就从一个程序(program)变成了进程(process),这就是p2p(program to process)。
020:From Zero to Zero
在这之后,shell为其execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell 父进程负责回收hello进程,内核删除相关数据结构,以上全部便是020的过程。
1.2 环境与工具
硬件环境:Intel Core i5-8250U x64CPU 8G RAM 1T HDD
软件环境:Ubuntu18.04.1 LTS
开发与调试工具:codeblocks,gcc,gdb,edb,readelf,hexedit,ld
1.3 中间结果
hello.i 预处理后的文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标文件
hello 链接后的可执行目标文件
hello.elf hello的elf文件
helloo.elf hello.o的elf文件
helloobjdump.txt hello的反汇编代码文件
hellooobjdump.txt hello.o的反汇编代码文件

1.4 本章小结
本章介绍了hello的P2P和020过程,作业的环境与工具,以及整个过程的中间结果。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理中会展开以#起始的行,试图解释为预处理指令,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。在所给hello文件中就是:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
预处理器(cpp)把这些头文件的内容插入到程序文本中,方便编译器进行下一步的编译。结果就是得到了另一个c程序,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
命令:gcc -E -o hello.i hello.c

生成一个hello.i文件。

2.3 Hello的预处理结果解析

可以看到只有#开头的部分发生了变化,而且增加了3000多行。增加的文本就是三个头文件的源码。
2.4 本章小结
本章介绍了预处理的概念和作用,linux下执行预处理的命令,并结合hello.i文件解析了hello的预处理结果。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的一种解释是利用编译程序从源语言编写的源程序产生目标程序的过程;另一种解释是用编译程序产生目标程序的动作。在这里指的是把预处理后的文件翻译成等价的汇编语言文本程序。
编译的作用是把高级语言翻译成计算机可以识别的2进制语言,工作过程分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。
3.2 在Ubuntu下编译的命令
命令:gcc -S -o hello.s hello.i

生成一个hello.s文件。

3.3 Hello的编译结果解析
指令含义
.file声明源文件
.text标志代码段
.section .rodata标志rodata节
.globl声明全局变量
.type指定是函数类型或是对象类型
.size声明大小
.long、.string声明一个string类型
.align声明对指令或者数据的存放地址进行对齐的方式
3.3.1处理变量

  1. 整数
    int i:编译器将局部变量存储在寄存器或者栈空间中。

可以看到局部变量i存放在了内存寄存器-4(%rbp)中。
int argc:作为第一个参数传入。
立即数:其他整形数据的出现都是以立即数的形式出现的,即直接编码在汇编代码中。
2. 字符串
可知程序中的字符串有:“用法: Hello 学号 姓名 秒数!\n”,这是第一个printf传入的输出格式化参数, 字符串中数字没有变化,而汉字被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节(一个\为一个字节)。“Hello %s %s\n”,这是第二个printf传入的输出格式化参数。

  1. 数组
    可知程序中涉及的数组是:
    char argv[] main,记录函数执行时输入的命令行,argv作为存放char指针的数组同时是也第二是个参数传入。
    argv中没个元素char大小为8B,argv指针指向已经分配好的的连续空间(若为链表则不一定连续)。argv作为数组名,也是数组的首地址。在main函数内访问数组元素argv[1], argv[2]时,按照起始地址argv大小8B计算数据地址取数据,在hello.s中,使用两次(%rax)取出其值。

3.3.2 赋值

i=0:整型数据的赋值使用mov指令完成,分为movb、movw、movl、movq四种,分别对应数据大小为1、2、3、4个字节
因为于i是int类型,大小为4个字节,所以使用movl进行赋值。

3.3.3 算数操作

对算数操作i++的处理为:

3.3.4 关系操作

对关系操作argc!=4的处理为:

对关系操作i<8的处理为:

编译器会优化为i<=7。
3.3.5 控制转移

对if(argc!=4)的处理为:

将argc与4比较,相等则跳到L2。

对for(i=0;i<8;i++)的处理为:

将i与7比较,i<=7时跳到L4中执行循环体的操作。
3.3.6 函数操作
P 中调用函数 Q 包含以下动作:
传递控制:在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。
传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,必须释放这些存储空间。

  1. main 函数:
    传递控制:main 函数因为被调用 call 才能执行,call 指令将下一条指令的地址 dest 压栈, 然后跳转到 main 函数。
    传递数据:外部调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储,函数正常出口为return 0,将%eax设置为0返回。
    分配和释放内存:使用%rbp记录栈帧的底,函数分配栈帧空间 在%rbp 之上,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp, pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。

  2. printf 函数:
    传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx 为argv[2]。
    控制传递:第一次printf只有一个字符串参数,编译器会优化成call puts@PLT;第二次 printf使用call printf@PLT。

  3. exit函数:
    传递数据:将%edi设置为1。
    控制传递:

  4. sleep函数:
    传递数据:传入参数atoi(argv[3])。
    控制传递:

  5. atoi函数:
    传递数据:传入参数argv[3]。
    控制传递:

  6. getchar函数:
    控制传递:

3.4 本章小结
本章介绍了编译的概念和作用,linux下执行编译的命令,并结合hello.s文件对编译器处理C语言的各个数据类型以及各类操作的方式进行了解析。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,这个过程称为汇编。.o文件是一个二进制文件,它包含目标程序的指令编码。
汇编的作用就是把汇编语言翻译成机器语言。
注意:这儿的汇编是指从.s到.o即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o

生成一个hello.o文件。

4.3 可重定位目标elf格式
命令:readelf -a hello.o > elf.txt

查看elf.txt:

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

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

  3. 重定位节.rela.text,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

偏移量(offset):需要被修改的引用的节的偏移。
信息(info):包括symbol和type两部分, 其中symbol占前 4 个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型。
类型(type):告知链接器如何修改新的引用。
符号名称(name):重定位目标的名称。
加数(addend):一个有符号常数,一些类型的重定位要使用它对被修改引用的值做便宜调整。
4. .rela.eh_frame :eh_frame节的重定位信息。

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

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数最终需要通过动态链接器才能确定函数的运行时地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0,然后在.rela.text节中为其添加重定位条目,等待修改。
全局变量访问:在.s文件中,访问rodata,使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
4.5 本章小结
本章介绍了汇编的概念和作用,linux执行汇编的命令,查看了hello.o的elf,使用了objdump工具得到反汇编代码,并解析了反汇编代码与hello.s文件的不同之处。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可
被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代
码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至
于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接
器使得分离编译成为可能。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86.so.2
/usr/lib/x86-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o
/usr/lib/x86-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
生成一个hello文件:

5.3 可执行目标文件hello的格式
命令:readelf -a hello > helloelf.txt

查看helloelf.txt,节头对hello中的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中Address是程序被载入到虚拟地址的起始地址。

5.4 hello的虚拟地址空间

查看edb的data dump块发现虚拟地址与节头部表中的是一致的。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
对比可知在hello中多了一些节头:
.interp 保存ld.so的路径
.note.ABI-tag linux下特有的节
.gnu.hash gnu的扩展符号hash表
.dynsym 动态符号表
.dynstr 动态符号表中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化需要执行的指令
.plt 动态链接-过程链接表
.fini 程序正常终止时需要执行的指令
.eh_frame 程序执行错误时的指令
.dynamic 存放被ld.so使用的动态链接信息
.got 存放程序中变量的全局偏移量
.got.plt 存放程序中函数的全局偏移量
.data 已初始化的全局变量或已声明的函数
链接过程:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
5.6 hello的执行流程
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave
-ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表.got.plt实现函数的动态链接,.got.plt中存放函数目标地址,PLT 使用GOT中地址跳转到目标函数。在_dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
5.8 本章小结
本章介绍了链接的概念和作用,linux下执行链接的命令,查看了hello的elf,使用了objdump工具得到反汇编代码,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据、栈、通用目的寄存器的内容、程序计数器、环境变量和打开文件描述符的集合。
进程给我们一个假象,就好像我们的程序是当前系统运行的唯一程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们的程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数。
3)如果是内置命令则立即执行。
4)否则调用相应的程序为其分配子进程并运行。
5)shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
用户在终端输入对应的指令,这时shell就会读取输入的命令,并开始进行以下操作:第一步:判断hello不是一个内置的shell指令,所以调用应用程序,找到当前所在目录下的可执行文件hello,准备执行。Shell会自动的调用fork()函数为父进程创建一个新的子进程,子进程就会因此得到与父进程虚拟地址空间相同的一段各种的数据结构的副本,包括代码和数据段,堆,共享库和用户栈。父进程与子进程最大的不同在于他们分别拥有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程运行完毕。
6.4 Hello的execve过程
fork之后,子进程调用execve函数,在当前进程的上下文中加载并运行hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。
新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容,然后跳转到_start,_start函数调用系统启动函数__libc_start_main来初始化环境,调用用户层中hello的main函数,并在需要的时候将控制返回给内核。
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
hello初始在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核休眠,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,继续执行。
6.6 hello的异常与信号处理
正常运行:

  1. 不停乱按:

可以看到,hello运行过程中,无论输入什么指令(除非是停止之类的)都不会引起变化,当运行结束后,发现之前回车间隔之间的输入都会作为shell的命令读入,应该是被放到了缓冲区中。
2. Ctrl-C:

输入Ctrl-C指令会发送信号SIGINT给hello的进程,通过信号处理程序直接将hello的进程终止。
3. Ctrl-Z:

Ctrl-Z后ps:

Ctrl-Z后jobs:

Ctrl-Z后pstree:

Ctrl-Z后fg:

输入Ctrl-Z后,hello的进程停止,但是没有消失,这时如果输入fg指令,hello会继续运行。
Ctrl-Z后如果输入kill指令,shell会调用kill函数发送SIGKILL信号,终止hello的进程。
6.7本章小结
本章介绍了进程的概念和作用,shell的处理流程,shell调用fork创建新进程,调用execve执行hello,hello的进程执行以及hello的异常和信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在汇编程序中地址。逻辑地址由选择符和偏移量组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址:课本中提到的虚拟地址,实际上就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理系统将程序的地址空间划分为若干个段,这样每个进程有一个二维的地址空间。每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般称作段表地址寄存器。
3 Hello的线性地址到物理地址的变换-页式管理
控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN向TLB中匹配,如果命中,则得到PPN与VPO组合成物理地址PA。如果没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成物理地址PA,并向TLB中添加条目。如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
物理地址被分为标志位CT、索引位CI和偏移量CO。
使用CI进行组索引,对每一路的块分别进行匹配,如果匹配成功且块的有效位为1,则命中,根据CO取出数据并返回。如果没有匹配成功或者匹配成功但是标志位是 0,则不命中,向下一级缓存中查询数据,查询顺序为L2L3主存。查询到数据之后,如果有空闲块则直接放在空闲块里,否则根据LRU等算法寻找牺牲块替换。
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 缺页故障与缺页中断处理
    Linux将虚拟内存组织成段的集合。内核为每个进程维护一个单独的任务结构,这个任务结构的第一个条目指向mm_struct,它描述了虚拟内存的当前状态,其中的pgd字段又会指向一个区域结构的链表,每个区域结构都描述了当前虚拟地址的一个区域,或者称为一个段。一个具体的区域结构包括vm_start和vm_end等字段,记录区域的相关信息。假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
    首先判断虚拟地址A是否合法,缺页处理程序会搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果指令不合法则触发段错误,从而终止该进程。
    然后判断试图进行的内存访问是否合法,也就是进程是否有读写这个区域内页面的权限。如果访问不合法,那么处理程序会触发一个保护异常,终止这个进程。
    确保了以上两点的合法性后,根据页式管理的规则,牺牲一个页面,并赋值为需要的数据,然后更新页表并再次触发MMU的翻译过程。
    7.9动态存储分配管理
    动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
    分配器有两种基本风格,两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
    显式分配器:
    要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
    隐式分配器:
    要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
    一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小,以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
    7.10本章小结
    本章介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,VA到PA的变换、物理内存访问、hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
    (第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
  2. Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
  3. 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
  4. 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  5. 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
    Unix I/O函数:
  6. int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
  7. int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
  8. ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
  9. ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
    8.3 printf的实现分析
    printf函数:

其中*fmt是格式化用到的字符串,而后面省略的则是可变的形参,对应于字符串里面的缺省内容。
va_start的作用是取到fmt中的第一个参数的地址,下面的write来自Unix I/O,而其中的vsprintf则是用来格式化的函数。vsprintf函数:

这个函数的返回值是要打印出的字符串的长度,也就是write函数中的i。该函数会将printbuf根据fmt格式化字符和相应的参数进行格式化,产生格式化的输出,从而write能够打印。write函数:

在Linux下,write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。syscall:

内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现字符串显示在屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本介绍了Linux的IO设备管理方法、Unix IO接口及其函数,并对printf函数和getchar函数进行了分析。
(第8章1分)
结论
hello所经历的过程:
1.编写:使用文本编辑器编写hello.c。
2.预处理:将hello.c调用的所有外部的库取出合并到一个hello.i文件中。
3.编译:将hello.i编译为汇编文件hello.s。
4.汇编:将hello.s汇编为可重定位目标文件hello.o。
5.链接:将hello.o与可重定位目标文件和动态链接库链接为可执行目标程序hello。
6.运行:在shell中输入./hello后面加上三个参数1190201021 马文韬 1。
7.创建子进程:shell进程调用fork为其创建子进程。
8.运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
9.执行指令:CPU为其分配时间片,在一个时间片中,hello有对CPU的控制权,顺序执行自己的代码。
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号:运行途中键入Ctr-C则停止,键入Ctrl-Z则挂起。
13.结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
通过这次大作业,我对程序运行中计算机内部的运作过程感到震撼,一个简单的hello程序就需要这么复杂的计算机内部处理,看来要读懂计算机还有相当长的路要走,深入理解计算机系统这本书还需要反复观看。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
hello.i 预处理后的文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标文件
hello 链接后的可执行目标文件
hello.elf hello的elf文件
helloo.elf hello.o的elf文件
helloobjdump.txt hello的反汇编代码文件
hellooobjdump.txt hello.o的反汇编代码文件

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Bryant,R.E.《深入理解计算机系统》.北京:机械工业出版社,2016
[2] printf函数实现的深入剖析
https://blog.csdn.net/zhengqijun_/article/details/72454714
[3] LINUX 逻辑地址、线性地址、物理地址和虚拟地址 转
https://www.cnblogs.com/zengkefu/p/5452792.html
[4] ELF 构造:https://www.cs.stevens.edu/~jschaum
(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值