计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 xxxxxxxxx
班 级 xxxxxxxx
学 生 xxx
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本论文通过hello.c程序,对在CSAPP课程中所学知识进行整理,在Ubuntu虚拟机Linux系统下进行所有操作,运用Linux系统的工具,分析hello程序的一生。
关键词: hello,计算机系统,linux,程序人生
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process,即 从程序到过程。在 linux 中,我们的 hello.c 文件经过编译器 cpp 的预处理成为 hello.i、再经过编译器 ccl 的编译成为 hello.s、接着被汇编器 as 汇编成 hello.o、最终经过链接器 ld 的链接最终成为 可执行目标程序 hello,执行此文件,操作系统会为其 fork 产生子进程,再调 用 execve 函数加载进程。至此,P2P 结束。
020:From Zero-0 to Zero-0,shell 通过 execve 加载并执行 hello,映射虚拟内存。先删除当前虚拟地址的数据结构并为 hello 文件创建新的区域结构。 进入程序入口后,程序开始载入物理内存,然后进入 main 函数执行目标代码, CPU 为运行的 hello 分配时间片执行逻辑控制流。当程序运行结束后,shell 父进程负责回收 hello 进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2.30GHz;8G RAM;64GB SSD ;2TB HDD ;2G RAM; 20GB SCSI。
软件环境:Windows10 64 位;Ubuntu 16.04 LTS 64 位;VMware Workstation Pro。
开发工具:gcc + gedit , Visual Studio 2019, Codeblocks , gdb edb。
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c :hello 源代码
hello.i:预处理生成的文本文件
hello.s:编译后得到的汇编语言文件
hello.o:汇编后得到的可重定位目标文件
hello:链接生成的可执行目标文件
objdump :hello 的反汇编代码
objdump_o :hello.o 的反汇编代码
1.4 本章小结
本章分析了hello的P2P和020过程,列出了撰写本论文的环境和工具,并写出生成的中间文件的名字和作用。
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理是预处理器(cpp)所进行的操作,具体为读取源程序中#开头的命令, 修改程序文本,如:hello.c 第八行的#include <stdlib.h>就是读取系统头文件stdlib.h的内容,将之直接插入程序文本。 本操作完成后,就得到了另一个程序文本,一般以.i 作为文件扩展名。
作用:
1. 宏替换,将宏名替换为文本;
2. 加载头文件;
3. 处理条件编译;
4. 处理特殊符号。
2.2在Ubuntu下预处理的命令
应截图,展示预处理过程!
命令行:gcc hello.c -E -o hello.i
(图2-1,预处理指令)
2.3 Hello的预处理结果解析
预处理后hello.c文件变成hello.i文件,字节数发生了变化。hello.i文件中main函数部分没有改变且在最后面,而头文件部分展开,宏定义也被处理。若存在嵌套关系,则cpp也会逐层展开,这样hello.i可以直接被译为hello.s文件。
2.4 本章小结
本章介绍了预处理的概念和作用,说明了如何在ubuntu下对hello.c进行预处理,并对hello的预处理结果进行解析。
第3章 编译
3.1 编译的概念与作用
概念:利用编译程序从源语言编写的源程序产生目标程序的过程。
作用:用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
命令行:gcc -S hello.i -o hello.s
(图3-1 编译指令)
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1汇编指令
(图3-2 编译结果中汇编指令)
.file:声明源文件;
.text:代码节;
.section:指示把代码划分成若干个段(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的页面设置不同的读、写、执行权限;
.rodata:只读代码段;
.align:数据或者指令的地址对齐方式;
.string:声明一个字符串(.LC0,.LC1);
.global:声明全局变量;
.type:声明一个符号是数据类型还是函数类型。
3.3.2数据
hello.s 中 C 语言的数据类型主要有:字符串,局部变量,main函数,各种立即数,数组。
3.3.2.1字符串
两个字符串都在只读数据段中,作为printf函数的参数。
(图3-3 编译结果中字符串)
3.3.2.2局部变量
int i;局部变量,通常保存在寄存器或是栈中。根据 movl $0, -4(%rbp)操作 可知 i 的数据类型占用了 4 字节的栈空间。
(图3-4 编译结果中局部变量)
3.3.3赋值
(图3-5 编译结果中赋值语句)
对局部变量 i 的赋值:使用 movl 语句,对应于 C 程序中 i=0。
3.3.4main函数
参数argc作为用户传给main的参数,也是被放到堆栈里的。
3.3.5算术操作
编译器将i++编译为:
(图3-6 编译结果中对地址算术操作语句)
每次执行到此处时,令对应地址处的内容+1。
3.3.6关系操作
- 编译器将i<8则跳转编译为
(图3-7 编译结果中对跳转的关系操作)
(当i<=7时则跳转.L4)
- 编译器将argc=4则跳转编译为
(图3-8 编译结果中对跳转的关系操作)
(当argc=4则跳转.L2)
3.3.7数组
main函数的第二个参数是数组,每一个元素都是指向字符类型的指针。.L4中能看到它的起始地址等
(图3-9 对数组结构的编译语句)
3.3.8全局变量
hello.c文件中声明了全局函数int main (int argc, char *argv[]),经过编译后,main函数中使用的字符串常量也被放在数据区。.global main说明main函数是全局变量。
3.3.9控制转移
如 3.3.6 中所描述,cmpl 语句比较后设置条件码,判断 ZF 标志,满足 jmp 要 求则进入相关部分。
3.3.10函数操作
调用函数时有以下操作:(假设函数A调用函数B)
传递控制:进行过程B的时候,程序计数器必须设置为B的代码的起始地址,然后在返回时,要把程序计数器设置为A中调用 B 后面那条指令的地址。
传递数据:A必须能够向B提供一个或多个参数B必须能够向A中返回一个值。
分配和释放内存:在开始时,B可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
- .main 函数: 参数传递:传入参数 argc 和 argv[],分别用寄存器%rdi 和%rsi 存储。 函数调用:被系统启动函数调用。 函数返回:设置%eax 为 0 并且返回,对应 return 0 。
- printf 函数: 参数传递:call puts 时只传入了字符串参数首地址;for 循环中 call printf 时 传入了 argv[1]和 argc[2]的地址。 函数调用:if 判断满足条件后调用,与 for 循环中被调用。
- exit 函数: 参数传递:传入的参数为 1,再执行退出命令 函数调用:if 判断条件满足后被调用.
- sleep 函数: 参数传递:传入参数 atoi(argv[3]), 函数调用:for 循环下被调用,call sleep
- getchar 函数: 函数调用:在 main 中被调用,call getchar
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章阐述了编译的概念及作用,介绍了在Ubuntu下编译的命令,并说明编译器是怎么处理C语言的各个数据类型以及各类操作。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程。
作用:汇编语言的诞生是由于机器代码难以记忆,所以用助记符代替操作码形成的方便记忆的语言,这种机器不能直接识别。用程序将其翻译为机器语言后,才可以被识别运行。
汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成可重定位目标程 序的格式,并将结果保存在目标文件 hello.o 中。此时的 hello.o文件不再是一个文本文件,而是一个二进制文件,包含的是程序的指令编码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令行:gcc -c hello.s -o hello.o
(图4-1 汇编指令)
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
(图4-2 用readelf等列出hello.o的ELF格式各节的基本信息)
1.ELF Header以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
(图4-3 ELF Header)
2.Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。描述了不同节的位置和大小,其中目标文件中每个节都有一个固定 大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
(图4-4 Section Headers)
- .symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
(图4-5 .symtab)
4.重定位节.rela.text中包含.text节的重定位信息,在链接时程序将通过这些信息和代码提供的偏移找到正确的需要调用的函数地址。当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存何处, 也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。 所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重 定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。 代码的重定位条目放在.rel.text 中,已初始化数据的重定位条目放在.rel.data 中。
本程序中,需要重定位的包括.rodata节中的两个数据,全局变量sleepsecs, 函数puts, exit, printf, sleep, getchar。
(图4-6 重定位节)
4.4 Hello.o的结果解析
objdump -d -r hello.o
(图4-7 hello.o的反汇编)
分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
- hello.s 中的操作数时十进制,hello.o 反汇编代码中的操作数是十六进制。
- 分支转移:跳转语句之后,hello.s 中是.L2 和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,也即间接地址。
- 函数调用:去掉了面向程序员的助记符,跳转/调用指令后的函数名/助记符替换为了相对偏移地址。由于尚未进行链接,无法确定函数地址,所以偏移暂时为零。同时,在重定位节中添加相应条目。hello.s 中,call 指令使用的是函数名称,而反汇编代码中 call指令使用的是 main 函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text 节中为其添加了重定位条目。
4.5 本章小结
阐述了汇编的概念和作用,介绍了Ubuntu下汇编的命令。说明了如何对hello.s进行汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处。
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并组合为一个单一文件的过程。
作用:不用将一个大型的应用程序组织为一个巨大的源文件,而是可以分解为更小的、更好管理的模块,从而可以独立地修改和编译单一模块。这令分离编译成为可能,节省了大量的工作空间。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
ld 链 接 命 令 :
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 链接命令)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(图5-2 readelf等列出可执行目标文件 hello各段的基本信息)
可执行目标文件 hello 的格式类似于可重定位目标文件的格式。而ELF 头描述文件的总体格式,它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。 .text、 .rodata 和.data 节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init 节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel 节。
5.4 hello的虚拟地址空间
在edb中打开hello,通过Data Dump查看hello程序的虚拟地址空间各段信息。 在 Memory Regions 选择 View in Dump 可以分别在 Data Dump 中看到只读内存段 和读写内存段的信息。 结合 5.3 中获得的指令与地址的关系表,就可以很轻易地查看。
(图5-3 hello的虚拟地址空间)
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
objdump -d -r hello
分析hello与hello.o的不同,说明链接的过程。
- 链接增加新的函数:增加了一些外部函数。
2.增加的节:增加了包括.init, .plt, .fini在内的一些节。
3.地址访问:相对偏移地址变为了虚拟内存的地址。hello.o 中的相对偏移地址变成了 hello 中的虚拟内存地址。而 hello.o 文件中对 于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重 定位,在汇编成机器语言时,将操作数全部置为 0,并且添加重定位条目。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
链接时,链接器通过符号表和节头了解到.data和.text在每个文件中的偏移和大小,进行合并,然后为新的合并出来的数据和代码节分配内存,并映射虚拟内存地址。最后修改对各种符号的引用,完成重定位。
(图5-4 链接的重定位过程)
5.6 hello的执行流程
地址 程序名
(图5-5 hello调用与跳转的各个子程序名或程序地址)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态链接库中的函数只在程序执行时才确定它们的地址,因此编译器不能确定它们的地址,也不能像静态库中的函数那样在汇编代码中实现它们。
hello程序对动态链接库的引用是基于这样一种机制,即数据段和代码段之间的相对距离是常数,因此代码段中的任何指令和数据段中的任何变量之间的距离都是运行时常数。
加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。
动态链接采用延迟加载的策略,即在调用函数时才进行符号的映射,使用偏移量表GOT+过程链接动态表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本,并将对函数的调用转换成对副本函数的调用。
(图5-6 在节头部表中找到 GOT 的首地址)
首先在节头部表中找到 GOT 的首地址——0x00403ff0,在 edb 中查看其变化。
调用dl_init之前的.got.plt内容:
(图5-7 调用dl_init之前的.got.plt内容)
调用后:
(图5-8 调用dl_init之后的.got.plt内容)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章阐述了链接的作用,对hello.o进行链接得到hello文件,并对hello进行分析,与hello.o的反汇编代码进行比较,更好的掌握重定位过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:
shell最重要的功能是命令解释。shell是一个命令解释器。用户提交了一个命令后,shell首先判断它是否为内置命令,如果是就通过shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或使用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。
流程:
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有从处理的结果中用到的注释删除,並且按照下面的顺序实行命令的检查:A. 内建的命令B. shell函数(由用户自己定义的)C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。fork函数只被调用一次,但会返回两次。一次是在调用进程中,一次是在新创建的子进程中。在父进程中,fork返回子进程的pid,在子进程中,fork返回0。
具体为根据 shell 的处理流程,可以推断,输入命令执行 hello 后,父进程如果判断不是内部指令,即会通过 fork 函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。 父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于 PID 的不同。 Fork 函数只会被调用一次,但会返回两次,在父进程中,fork 返回子进程的 PID,在子进程中,fork返回0。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新程序。当读取文件出现错误时,返回原程序,否则不返回。具体步骤如下:
根据第一个参数加载文件,通过启动代码对栈进行设置,并完成控制传递;
顺序执行,用第二,三个参数调用main函数。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
(图6-1 Hello的进程执行)
进程上下文信息: 进程的上下文由程序正确运行所需的状态组成,这个状态包括存放在内存中 的程序的代码和数据,还包括它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程时间片: 分时操作系统分配给正在运行的进程的一段 CPU时间。
调度的过程: 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
用户态与核心态转换: 为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。操作系统内核会为每一个进程维持一个上下文,而在进程执行的某些时刻,内核可以进行调度、抢占当前进程,并重新开始一个先前被抢占了的进程。其过程如下:保存当前进程的上下文;恢复先前被抢占进程保存的上下文;控制权传递给这个新恢复的进程。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
(1)乱按
(图6-2 hello程序运行过程中乱按)
运行时的乱按并不会影响程序的运行,但因为程序中存在 getchar 函数,按到 回车键时,getchar 会读入回车符及之后字符串,导致在程序运行完成后,将这些 字符串作为 shell 的命令行输入。
(2)ctrl+c
(图6-3 hello程序运行过程中ctrl+c)
退出程序,内核发送一个SIGINT信号到前台进程组中的每个进程,终止前台作业,此时hello进程已结束并被回收。
- ctrl+z
(图6-4 hello程序运行过程中ctrl+z)
程序停止内核发送一个SIGSTP信号到前台进程组中的每个进程,前台作业被挂起。
- ctrl+z 后 ps 挂起后,ps 命令列出当前系统中的进程(包括僵死进程)。
(图6-5 hello程序运行过程中ctrl+z后ps)
- ctrl+z 后 pstree 与 ps 相似,但 pstree 命令是以树状图显示进程间的关系,这里只展示图的部分。
(图6-6 hello程序运行过程中ctrl+z后pstree)
- jobs:显示前台作业状态
(图6-7 hello程序运行过程中jobs)
- fg :将挂起程序调回前台继续运行。
(图6-8 hello程序运行过程中fg)
(8)kill:对于kill命令发送的信号:信号编号是9,即SIGKILL,终止。kill -9 %1是杀死后台hello程序。
6.7本章小结
这一章介绍了异常控制流,进程,信号的处理。这一章的重点在于阐述了应用是如何与操作系统交互的。这些交互都是围绕着ECF(异常控制流)的。从异常开始,异常位于硬件和操作系统交界的部分。系统调用是为应用程序提供到操作系统的入口点的异常。还有进程和信号,它们位于应用和操作系统的交界之处。学习这一章对于理解用户程序和系统内核的交互有重要的作用。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:是在有地址变换功能的机器中访内指令给出的地址。也叫相对地址,也就是在机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算才能得到内存储器中的实际有效地址。在hello中,生成的hello.o文件中的地址即偏移量,都是逻辑地址。
线性地址:如果地址空间中的整数是连续的,那么就说它是一个线性地址空间。在这里讨论的线性地址就是虚拟地址。
虚拟地址:一个带虚拟内存的系统中,CPU 从一个有 N=2^n 个地址空间中生 成虚拟地址,与物理地址相对应。
物理地址:用于内存芯片级的单元寻址,与处理器和 CPU 连接的地址总线相 对应。地址翻译器会将虚拟地址转化为物理地址,放在 hello 中也是这样的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(1)实地址模式:在实地址模式下,处理器使用20位的地址总线,可以访问1MB(0~FFFFF)内存。而8086的模式,只有16位的地址线,不能直接表示20位的地址,采用内存分段的解决方法。段地址存放于16位的段寄存器中(CS、DS、ES或SS)
(2)保护模式:在保护模式下,段寄存器存放段描述符在段描述符表中的索引值,称为段选择器,此时CS存放代码段描述符的索引值,DS存放数据段描述符的索引值,SS存放堆栈段描述符的索引值
RPL表示程序的当前优先级,TI位表示段描述符的位置,TI=0段描述符在GDT中,TI=1段描述符在LDT中
- 观察段选择符,0则转换的是GDT(全局)中的段,否则就是LDT(局部)中的段。
2.根据相应寄存器,找到其起始地址。
3.通过段标识符找到对应的基地址。
4.用基地址加上偏移,得到线性地址。
保护模式下:线性地址 = 基址(由段描述符给出)+ 偏移量
7.3 Hello的线性地址到物理地址的变换-页式管理
- 根据线性地址前十位找到对应页表的地址。
2.根据线性地址中间十位找到对应页的起始地址。
3.页的起始地址加上线性地址最后十二位,得到物理地址。
其中页目录项和页表项的格式如下:
P:P=1表示页表或页在主存中;P=0表示页表或页不在主存中。
R/W:该位为0时表示页表或页只能读不能写;为1时表示可读可写。
U/S:该位为0时表示用户进程不能访问;为1时允许用户进程访问。
PWT:用来控制页表或页对应的cache写策略是直写还是写回。
PCD:用来控制页表或页能否被缓存到cache中。
A:A=1表示指定页表或页被访问过,初始化时操作系统将其清0。
D:脏位,只在页表项中有意义。D=1表示被修改过,否则表示为被修改,操作系统将页面替换出主存时,无须将页面写入硬盘。
页目录项和页表项中的高20位是页表或页在主存中的首地址对应的页框号,即首地址的高20位。每个页表的起始位置都按4kb对齐。
7.4 TLB与四级页表支持下的VA到PA的变换
将VA分为四段。依次通过每段地址找到对应的PML4,PGD,PMD,PTE表,找到对应地址,组合得到PA。
TLB(翻译后备缓冲器)是一个位于MMU中的小的虚拟地址的具有较高相联度的缓存,其每一行都是一组由数个PTE组成的块,TLB极大地减小了CPU访问PTE的开销,且能实现虚拟页面向物理页面的映射,同时对于页面数很少的页表可以完全包含在TLB中。
使用K级页表:虚拟地址被分成4个VPN和一个VPO,每个VPNi都是一个到第i级页表的索引,第j级页表中的每个PTE指向j+1级某个页表的基址。在翻译虚拟地址时通过四级页表查询到PPN,与VPO结合成PA。
1~3级页表条目:
P: 子页表在物理内存中 (1)不在 (0).
R/W: 对于所有可访问页,只读或者读写访问权限.
U/S:对于所有可访问页,用户或超级用户 (内核)模式访问权限.
WT: 子页表的直写或写回缓存策略.
A: 引用位 (由MMU在读或写时设置,由软件清除).
PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义).
页表物理基地址:子页表的物理基地址的最高40位 (强制页表 4KB 对齐)
XD: 能/不能从这个PTE可访问的所有页中取指令
4级页表条目:4级页表条目桶1-3级不同在于其没有了PS项,多了D:修改位(由MMU 在读或写时设置,由软件清除).
7.5 三级Cache支持下的物理内存访问
i7处理器每个CPU有4个核,每个核有自己私有的L1 i-cache、L1 d-cache和L2高速缓存,所有核共享一个L3缓存。CPU寄存器中保存着从L1缓存中取出的字,L1缓存保存着从L2缓存中取出的缓存行,L2缓存保存着从L3缓存中取出的缓存行,L3缓存保存着从主存中取出的缓存行。
Cache分为以下三类:
- 直接映射高速缓存
- 组相联高速缓存
- 全相联高速缓存
现分析三级 cache 支持下的物理内存访问: 如图 7-5,以 L1 d-cache 的介绍为例,L2 和 L3 同理。 L1 Cache 是 8 路 64 组相联。块大小为 64B。因此 CO 和 CI 都是 6 位,CT 是 40 位。根据物理地址(PA),首先使用组索引 CI,每组 8 路,分别匹配标记 CT。 如果匹配成功且块的有效位是 1,则命中,根据块偏移 CO 返回数据。 如果没有匹配成功,或者匹配成功但是标志位是 0,则不命中,向下一级缓存 申请请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。 一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个 现存的块,一般采用最近最少被使用策略 LRU 进行替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
调用Execve时,系统首先删除了当前进程中用户部分已有的结构,然后映射私有区域(建立新的文件结构,包括.data在内的各种节),共享区域(当前进程的动态链接),并设置PC。
7.8 缺页故障与缺页中断处理
在异常控制流中学过,缺页异常是一种经典的故障。发生故障时,处理器将控制转移给故障处理程序。如果处理程序额能够修正这个错误的情况,它就将控制返回到引起故障的指令,重新执行。否则处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
1.段错误:地址不合法,即无法匹配到已有的区域结构中;
2.非法访问:没有应有的读写权限;
3.正常缺页:选择一页进行替换。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存,动态内存分配器维护着进程的一个虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器有两种风格,显示分配器(要求应用显式地释放任何已分配的块),隐式分配器(要求分配器检测一个已分配块是否仍然需要,不需要则释放)。
而一个实际的分配器需要考虑以下几个问题:
1.空闲块组织:如何记录空闲块?
2.放置:如何选择一个合适的空闲块来放置一个新分配的块?
3.分割:在将一个新分配的块放置到某个空闲块之后,如何处理这个空闲块中的剩余部分?
4.合并:如何处理一个刚刚被释放的块?
隐式空闲链表的数据结构可以较好地解决这些问题。
分配策略:
1.空闲链表:
(1) 隐式:在每块的头,尾部增加32位存储块大小,以及是否空闲。
(2) 显式:在隐式的基础上在头部增加对前后空闲块的指针。
(3) 分离:同时维护多个空闲链表。
2.带边界标记的合并:
利用每块头尾的大小和空闲状态信息合并空闲块。
3.无合适空闲块时,申请额外的堆空间。
7.10本章小结
在本章中,对虚拟内存相关的知识进行了回顾,对 hello 程序在执行过程中对 存储空间的影响做了分析,并做节归纳了段式与页式管理的内容,对 TLB 与四级 页表支持下的 VA 到 PA 的变换,三级 Cache 支持下的物理内存访问等内容也做出了讨论。最后还列出了动态存储分配管理的内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。
8.2 简述Unix IO接口及其函数
8.2.1打开文件:open函数,打开一个已存在的文件或者创建一个新文件
int open(char *filename,int flags,mode_t mode);
8.2.2关闭文件:close函数,关闭一个已打开的文件
int close(int fd);
8.2.3读:read函数,执行输入
ssize_t read(inf fd,void *buf,size_t n );
8.2.4写:write函数,执行输出
ssize_t write(inf fd,const void *buf,size_t n );
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
Vsprintf:接受一个格式化的命令,并把制定的匹配的参数格式化输出。
Write:把字符串中n个元素的值写到终端(n为第二个参数)
系统调用:显示格式化的字符串。
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
本次大作业深刻体会了hello的一生:
1.预处理:经过预处理器cpp预处理,将#开头的行进行预处理得到hello.i
2.编译:编译器cc1将hello.i编译生成hello.s文件
3.汇编:汇编器as又将得到的hello.s翻译成机器语言指令得到可重定位目标文件。
4.链接:链接器ld将hello.o与动态链接库链接生成可执行文件hello
5.运行:在shell中输入./hello 120L022021 tongchuhan
6.创建子进程:为hello fork一个子进程
7.加载:调用execve函数来加载运行hello.
8.执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流
9.访问内存:将虚拟地址翻译成为物理地址,通过L1 L2 L3三级cache访问内存
10.动态内存分配:根据需要申请动态内存
11.信号:shell的信号处理函数可以接受程序的异常和用户的请求
12.终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构
至此,hello结束了自己灿烂光辉的一生。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c :hello 源代码
hello.i:预处理生成的文本文件
hello.s:编译后得到的汇编语言文件
hello.o:汇编后得到的可重定位目标文件
hello:链接生成的可执行目标文件
objdump :hello 的反汇编代码
objdump_o :hello.o 的反汇编代码
参考文献
[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.