计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 1170301028
班 级 1703010
学 生 梁雅琪
指 导 教 师 史先俊
计算机科学与技术学院
2018年12月
摘 要
hello.c只是一个短短十几行的程序文件,所谓麻雀虽小五脏俱全,hello.c文件包含了头文件,各个函数,各个参数,各个变量。我们可以从预处理到编译,到汇编到链接的hello.i,hello.s,hello.o,hello可执行目标文件以及一些重定位文件中找到这些内容出现的身影。尽管小并且简单,它在电脑的内存中也占有一席之地。还要有进程管理和设备管理为它安排……
关键词:hello;预处理;编译;链接;内存;进程;管理
(摘要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:首先编写生成一个hello.c的程序文本,然后经过cpp预处理—>ccl编译—>as汇编—>ld链接,生成一个可执行目标程序,然后shell为他fork建立一个子进程,成为一个Program。
020:shell帮他execute,将他映射进虚拟内存,然后在开始运行进程的时候分配并载入物理内存,然后进入main函数执行目标代码。CPU为他分配时间片执行逻辑控制流。运行结束后,shell回收进程,内核删去有关数据结构。
1.2 环境与工具
硬件环境:X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:Codeblocks,objdump,gdb,edb.vim.gcc,ld,ebd,readelf等
1.3 中间结果
hello.i 预处理后的文本文件
hello.s 编译后的文本文件
hello.o 汇编之后的可重定位目标执行文件
hello 链接之后的可执行的目标文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式
helloo.txt hello.o的反汇编代码
hello.txt hello的反汇编文件
1.4 本章小结
本章节简述了本次大作业的内容,环境工具,以及在实验过程中的产生的文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(cpp)根据以#开头的命令,修改显示的C程序。
作用:1)将源文件中用#include形式声明的文件复制到新的程序中。比如hello.c第6-8行中的#include
2)用实际值替换用#define定义的字符串
3)根据#if后面的条件决定需要编译的代码
4)还包括此次hello中没有的#line,#error,#pragma,以及单独的空指令的处理。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图2.2预处理产生hello.i文件
2.3 Hello的预处理结果解析
使用vim打开hello.i文件后,看到hello.i有3118行代码。
图2.3.1vim查看 hello.i文件底端
在代码中看到,头文件执行很靠前,其中包含了在电脑中头文件的位置和其他相关信息。头文件的执行运用了很多的结构体,代码数也较多,执行起来原来并不如hello.c文件中看起来的那么容易。
图2.3.2vim查看hello.i文件
接下来是一些宏定义和函数的声明。代码数也十分庞大。
图2.3.3vim查看hello.i文件
而真正的main函数却是在最后3009行的位置。
看的出来预处理所做的工作,就是对#的指令进行处理(调用等)。
2.4 本章小结
本章节简单介绍了编译前预处理的过程,包括预处理的概念和作用,对于处理进行演示,通过分析更好的说明了理论的内容。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念与作用:
编译(compilation , compile) 1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。
编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这个过程称为编译,也是作用。把高级语言变成计算机可以识别的2进制语言。计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
也可以说,编译就是“翻译”。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3.2编译产生hello.s文件
3.3 Hello的编译结果解析
打开hello.s文件,其中代码行数仅有65行,规模已经很小了。
它主要分了几个部分:
3.3.0汇编指令
代码开始就是一些汇编指令:
图3.3.1 hello.s文件开头
对指令解释如下:
.file 声明源文件
.text 以下是代码段
.data 以下是数据段
.globl 声明一个全局变量
.align 声明对指令或者数据存放地址进行对齐的方式
.type 指定函数类型或对象类型
.size 声明大小
.long 声明一个long类型
.string 声明一个string类型
.section .rodata 以下是rodata节
3.3.1数据:
hello.c中使用到的数据类型有字符串、整数和数组:
1) 字符串
hello.c中字符串有两个:
1>“Usage: Hello 学号 姓名!\n”, 第一个printf传入的输出格式化参数,可以发现字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
2>“Hello %s %s\n”,第二个printf传入的输出格式化参数
声明在.LC0和.LC1段中,并且声明在了只读数据节,声明如下:
图3.3.2编译产生hello.s文件
2) 整数
图3.3.3编译产生hello.s文件
1> 全部变量 int sleepsecs
一个整型的变量,初值为浮点数。所以值应当为2,而不是2.5。
声明在了.globl(全局变量)之下,并且已经赋值了,所以编译处理也应当在.data 节声明这个变量。在hello.s中,它先放在.text代码段中声明是全部变量,其次在.data中设置对齐方式为4,设置大小为4字节,设置为long类型,值为2。
由此看来,全部变量作为在代码中任意函数皆可调用的变量,他的实现也会比较麻烦。而这里为什么要设置为long类型呢,应该是编译器偏好。
2> int i:编译器将局部变量存储在寄存器或者栈空间中,在hello.s中编译器将i存储在栈上空间-4(%rbp)中,可以看出i占据了栈中的4B。
3> int argc:作为第一个参数传入
4> 立即数:其他整形数据的出现都是以立即数的形式出现的,直接硬编码在汇编代码中
3) 数组
char *argv[] main,函数执行时输入的命令行,argv作为存放char指针的数组同时是第二个参数传入。而只使用数组中的两个值
图3.2编译产生hello.s文件
Argv指针指向一片分配好的,连续的,存放着字符的空间,然后分别获取argv[1](黑色框显示)和argv[2](绿色框显示)的地址,其中只有栈的指针的移动。
3.3.2赋值
赋值操作只有两个:
下一条jmp指令应当是跳入循环的指令
3.3.3类型转换
这里应该只有一个隐式类型转换:int sleepsecs=2.5
更加容易忽略,但在初学C语言时,就接触过,不应当忘记。
整型变量赋值了浮点数类型,浮点数默认类型为double型,这里的原则是:向零舍入,所以值应当为2。
3.3.4算术操作
对算术操作的汇编指令做一个整理:
图3.3.3.1算术操作指令
这里的整理只写了部分具体的操作,可以发现,汇编语言中的算术操作与别的操作一样,主要操作数是后边的那个。
hello.c中涉及到算术操作的有:
1)i++,在for循环中做计数器,使用的指令如下:
2)汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。
这里我确实没注意到,是由同学指点发现的。
3.3.5关系操作
对关系操作的汇编指令做一个总结:
图3.3.4.1关系操作指令
hello.c程序中涉及到的有:
1)argc!=3:
比较之后,如果等于做跳转。
2)i<10
比较之后,如果小于或等于做跳转。
3.3.6控制转移
程序中涉及的控制转移有:
图3.3.6编译产生hello.s文件
图3.3.6.2编译产生hello.s文件
3.3.7函数操作
函数操作的指令总结和概念整理:
图3.3.7call指令
传递控制:在进入过程Q的时候,程序计数器必须被设置为Q代码的起始位置,然后返回时,要把程序程序计数器设置为调用的那一条语句
传递数据:P必须向Q传递n个参数,Q必须向P返回一个值。
分配和释放内存:在开始是,Q可能需要为局部空间分配内存,而在返回之前必须释放掉这些存储空间。
x86-64的过程实现包括特殊的指令和一些对机器资源使用的约束。
转移控制的实现需要上面两条汇编指令的支持。P调用个过程Q,执行call Q指令,该指令会把调用过程的下一条指令A保存在P的栈帧中,并把PC寄存器设置为Q的起始位置。对应的指令会将PC设置为A,并将A弹出P的栈帧。
程序中涉及的函数操作的有:
main函数
传递控制:main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数
传递数据:传入参数argv[],argc。分别用%rdi和%rsi存储
释放:ret相当于pop IP
printf函数
传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
exit函数
传递数据:将%edi设置为1
sleep函数
传递数据:将%edi设置为sleepsecs
getchar函数
3.4 本章小结
本章主要阐述了编译的概念、作用和各类操作的分析。展示了如何将C程序编译成汇编程序。C语言与汇编语言之间分别表现了两种语言的形式和着重点。其中C语言能更加方便快捷的被程序员使用,汇编语言则更贴近计算机执行的过程。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
as将.s汇编程序经过编译生成.o机器语言程序
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图4.2汇编产生hello.o文件
4.3 可重定位目标elf格式
ELF文件主要有三种类型: (1)可重定位文件包含了代码和数据.可与其它ELF文件建立一个可执行或共享的文件. (2)可执行文件时可直接执行的程序. (3)共享目标文件包括代码和数据。这里是第一种:
使用readelf -a hello.o > hello.elf 指令获得hello.o文件的ELF格式。
组成如下:
1)ELE头:
图4.2.1重定位后的ELF格式文件
ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行、共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量
2)节头:
图4.2.2重定位后的ELF格式文件
各个节:
[1].text:已编译程序的机器代码
[2].rodata:只读数据
[3].data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
[4].bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0.
[5].symtab:符号表,过程和静态变量名,节名和位置
[6].rel.text:文本部分的重新定位信息,在可执行文件中需要修改的指令的地址,修改指令
[7].rel.data:数据段的重新定位信息,在合并的可执行文件中需要修改的指针数据的地址
[8].debug:符号调试信息(gcc -g)
[9].line:初始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
[10].srttab:一个字符串表,以null结尾的字符串序列
3)程序头:记录如何整合到虚拟地址空间的
4)重定位节
图4.2.3重定位后的ELF格式文件
重定位节.rela.text ,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。图中8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、sleepsecs、sleep函数、getchar函数进行重定位声明。.rela.eh_frame:eh_frame节的重定位信息
过程:
(1)判断运行地址和链接地址是否相等
(2)如果运行地址和链接地址是相等的话,那么就没有必要进行重定位。直接跳过重定位的部分,直接跳转到clean_bss
(3)如果运行地址和链接地址不相等的话,那么就需要进行重定位了。重定位的实际上就是拷贝数据段和代码段的内容从运行地址到链接地址处。
4.4 Hello.o的结果解析
图4.4hello.o与hello.o的对比
打开之后发现汇编代码和机器码的代码规模差不多,看起来也有很多相似的内容,做了每一行的对比之后发现,.s文件中有操作码的每一行都有在.o中文件中对应的代码。
.s文件其中含有.cfi_startproc等类似的内容,出现在代码的开头和结尾,应当是进程从main函数开始,和回收的相应的指令,而.o文件中并没有类似内容。
.o文件中R_X86_64_PC32等内容,发生在lea指令(后面带有只读代码段的记号),和函数调用指令后。而.s文件中没有
1.全局变量访问:
对于hello.s,全局变量的访问方式为:段名称+%rip,而对于hello.o的反汇编为0+%rip,因为rodata的数据地址是在运行时确定的,故也需要重定位,所以现在是0,并添加了重定位条目。
2.分支转移
我们可以发现,在hello.s中跳转到的目标位置都是用.L3/.L4来表示的,在hello.o反汇编之后,这些目标被用具体的地址位置代替。
3. 函数调用
在原先的hello.s中,调用一个函数只需被表示成call+函数名,但是在hello.o反汇编的结果中我们可以看见,这里的call是call一个具体的地址位置。
4.5 本章小结
本章阐述了hello.s汇编语言转换成hello.o机器语言的过程,重定位查看hello.o的内容,分析了作用,然后又对比了hello.s与hello.o之间映射与差别。
机器语言与汇编语言的差距不是很大,但是计算机仍然要将汇编语言翻译为机器语言,才能达到人与机器的沟通,在整理期间,我也在思考既然差别不大,为什么人们不直接编译机器语言。其实忽略了,重要的并非是我们可以看明白的代码,而是我们看不懂的但机器能懂得的16进制的机器码。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接器主要是将有关的目标文件彼此相连接生成可加载、可执行的目标文件。即从 hello.o 到hello生成过程。链接器的核心工作就是符号表解析和重定位。
作用:
图5.2链接生成hello可执行目标文件
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
图5.3重定位后的ELF格式文件
ELF头:这里的节头数量从刚才的13变为现在的25,规模显然大了不少。
图5.3.1重定位后的ELF格式文件
节头:
图5.3.2重定位后的ELF格式文件
节头对hello中所有的节信息进行了声明,其中包括大小以及在程序中的偏移量,因此根据节头中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中地址是程序被载入到虚拟地址的起始地址
图5.3.3重定位后的ELF格式文件
程序头中:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
5.4 hello的虚拟地址空间
这里显然是ELF头
ELF是以16个字节的序列开始,从这里与5.3中对比完全一致。
这里与程序头中的各节地址相对应。
5.5 链接的重定位过程分析
图5.4hello.o与hello对比
5.5.1hello.o与hello不同
5.6 hello的执行流程
ld-2.27.so!_star 0x7fff f7dd6090
ld-2.27.so!_dl_start_user 0x7fff f7dd6098
ld-2.27.so!_dl_init 0x7fff f7dd5630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fff f7dd5ab0
libc-2.27.so!__cxa_atexit 0x7fff f7a27430
libc-2.27.so!__new_exitfn 0x7fff f7a27220
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fff f7a22c10
-libc-2.27.so!_sigsetjmp 0x7fff f7df2090
hello!main@plt 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt – 0x4004e0
*hello!printf@plt – 0x4004c0
*hello!sleep@plt – 0x4004e0
*hello!getchar@plt – 0x4004f0
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fff f7dec680
-ld-2.27.so!_dl_fixup 0x7fff f7de4f90
-ld-2.27.so!_dl_lookup_symbol_x 0x7fff f7de00b0
libc-2.27.so!exit 0x7fff f7df2030
执行hello过程:
首先由hello!_start—>libc-2.27.so!__libc_start_main—>-libc-2.27.so!__cxa_atexit—>libc-2.27.so!__new_exitfn—>hello!__libc_csu_init—>hello!_init—>
libc-2.27.so!_setjmp—>libc-2.27.so!_sigsetjmp—> libc-2.27.so!__sigjmp_save—>
hello!main
5.7 Hello的动态链接分析
执行前:
执行后:
在edb调试之后我们发现原先0x00600a10开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量,这是初始化hello程序的一步。
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。在一次熟悉用edb调试,其中要注意查找的功能。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量
6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。
处理流程:
1)读取用户的输入
2)分析输入内容,获得输入参数
3)如果是内核命令则直接执行,否则调用相应的程序执行命令
4)在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应
6.3 Hello的fork进程创建过程
fork通过复制父进程来创建一个新的进程。这个新创建 的进程称为调用fork()函数的子进程,这个调用fork()的进程称为子进程的父进程。子进程除了以下几点之外就是父进程的一个复制品。
1)子进程有其唯一的PID;
2)子进程的父进程PID(PPID)和父进程的PID是相同的;
3)子进程不继承父进程的内存块;
4)子进程的资源使用计数器和CPU时间计数器都将被置为空;
5)子进程挂起信号量的数目初始化为0;
6)子进程并不继承父进程的信号量调节器;
7)子进程并不继承父进程的记录锁;
8)子进程不继承父进程 的时间计数器;
9)子进程不继承父进程的异步I/O操作和异步I/O操作内容
图6.3进程的过程
6.4 Hello的execve过程
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
图6.4堆栈信息
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成
加载器设置PC指向_start地址,_start最终调用hello中的main函数。
通过逻辑控制流,在不同的进程间切换。分配给某个进程的时间就叫做进程的时间片。上下文信息即重新启动一个被抢夺的进程的条件。用户态中,程序不允许执行一些特权功能,而核心态中可以,它们之间需要某些条件才能切换
图5.3进程
6.6 hello的异常与信号处理
正常运行时:当程序执行完成之后,进程被回收
按ctrl+c后:当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程
按多个回车后:回车被当做一个输入,没有输入就没有指令。
按ctrl+z后:当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起
ps:这里说明进程没有被回收
jobs:值为1
pstree:
kill:杀死进程之后,ps中就没有hello了
6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。这里的内容更加抽象了。也有点难理解,但是结合书上内容和以往实验的内容还是能够解决。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是机器语言指令中,用来指定一个操作数或者是一条指令的地址。
线性地址与虚拟地址:csapp课本上,虚拟地址就是线性地址。逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
这其实说明了,逻辑地址和线性地址都是不真实的地址,逻辑地址对应的硬件平台段氏管理转换前地址的话,那么线性地址就对应了硬件页式内存的转换前地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。它是真实的。
结合hello来说,hello.o中应该为逻辑地址,hello.s应当为线性地址或者虚拟地址,hello中的地址应为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1、逻辑地址=段选择符+偏移量
2、每个段选择符大小为16位,段描述符为8字节(注意单位)。
3、GDT为全局描述符表,LDT为局部描述符表。
4、段描述符存放在描述符表中,也就是GDT或LDT中。
5、段首地址存放在段描述符中。
每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。下图是一个段选择符的格式
从上图可以看出段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引)。当TI=0时,表示段描述符在GDT中,当TI=1时表示段描述符在LDT中。index表示某个段描述符在数组中的索引。
现在我们假设有一个段的段选择符,它的TI=0,index=8。我们可以知道这个段的描述符是在GDT数组中,并且他的在数组中的索引是8。
假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index,从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
基本概念:分页的基本方法是将地址空间人为地等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。目前,以大小为4KB的分页是绝大多数PC操作系统的选择.
1)逻辑空间等分为页;并从0开始编号。
2)内存空间等分为块,与页面大小相同;从0开始编号。
3)分配内存时,以块为单位将进程中的若干个页分别装入。
转化方法:
1.CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。
2.从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。
3.将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
1.内存管理单元MMU既有段描述符也有页描述符。TLB位于MMU内部,不需要经过页表就可以将虚拟地址转换为物理地址,速度更快。
每一个TLB寄存器的每个条目包含一个页面的信息:有效位,虚页面号,修改位,保护码,和页面所在的物理页面号,它们和页面表中的表项一一对应
MMU在翻译的时候,先查看虚拟地址的虚拟页面号是否存在TLB(并行的查找)中,如果存在,且没有违背读写权限限制,则直接给出TLB中的物理页面号;若在TLB中不存在,则进行常规的页表的查找,然后从TLB中淘汰一个条目,并更新为刚刚查找的页面
2.VA:未使用MMU
3.PA:使用MMU,接受CPU发来的地址做某种转化。
转换主要需要建立映射关系:
VA到PA的转化过程:
首先需要CPU发出VA,经过一个索引,找到相应的下标,查表后将之翻译出来,得到物理地址的前12位,与CPU发出的VA后20位结合,就是实际的物理地址。
图7.4 VA到PA转化过程
7.5 三级Cache支持下的物理内存访问
图7.5.1储存器层次结构
cache:高速缓存,从上图就可以看出高速缓存的高效和高昂,三级cache是为了能在达到效率的前提下降低成本。
cache的访问:
获得物理地址VA后
,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换
图7.5.2内存访问过程
7.6 hello进程fork时的内存映射
fork的内存映射是使用特殊文件提供匿名内存映射,而这种内存映射,适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程 继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射(可以使用fd =-1 的方式,也可以用fd = open(“dev/zero”,)的方式)的方式。此时,不必指定具体的文件,只要设置相应的标志即可。
表面看起来fork()创建子进程子进程拷贝了父进程的地址空间其实不然
刚调用完fork()之后,子进程只是拥有一份和父进程相同的页表,其中页表中指向RAM代码段的部分是不会改变的,而指向数据段,堆段,栈段的会在我们将要改变父子进程各自的这部分内容时,才会将要操作的部分进行部分复制。
7.7 hello进程execve时的内存映射
使用execve就是一次系统调用,首先要做的将新的可执行文件的绝对路径从调用者(用户空间)拷贝到系统空间中。在得到可执行文件路径后,就找到可执行文件打开,由于操作系统已经为可执行文件设置了一个数据结构,就初始化这个数据结构,保存一个可执行文件必要的信息。其中步骤有一下几步:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
7.8.1缺页终端概念:
进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
7.8.2查看缺页终端次数:
ps -o majflt,minflt -C program查看
majflt和minflt表示一个进程自启动以来所发生的缺页中断的次数;
7.8.3产生缺页中断的几种情况:
1、当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页中断,并且可能是栈溢出导致的缺页中断;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,它的vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页中断的写时复制;
7.8.4缺页中断的处理:
1.当前执行流程在内核态时
(1)通过mm是否存在判断是否是内核线程,对于内核线程,进程描述符的mm总为NULL,一旦成立,说明是在内核态中发生的异常,跳到no_context
if (in_atomic() || !mm)
goto no_context;
如果当前执行流程在内核态,不论是在临界区还是内核进程本身(内核的mm为NULL),说明在内核态出了问题,跳到标号no_context进入内核态异常处理,由函数_do_kernel_fault完成。
(2)
这个函数首先尽可能的设法解决这个异常,通过查找异常表中和目前的异常对应的解决办法并调用执行;如果无法通过异常表解决,那么内核就要在打印其页表等内容后退出。
2.用户进程的缺页中断
对于用户空间的缺页中断,则会调用函数_do_page_fault.
首先从CPU的控制寄存器CR2中读出出错的地址address,然后调用find_vma(),在进程的虚拟地址空间中找出结束地址大于address的第一个区间,如果找不到的话,则说明中断是由地址越界引起的,转到bad_area执行相关错误处理;
确定并非地址越界后,控制转向标号good_area。在这里,代码首先对页面进行例行权限检查,比如当前的操作是否违反该页面的Read,Write,Exec权限等。如果通过检查,则进入虚拟管理例程handle_mm_fault().否则,将与地址越界一样,转到bad_area继续处理。
handle_mm_fault()用于实现页面分配与交换,它分为两个步骤:首先,如果页表不存在或被交换出,则要首先分配页面给页表;然后才真正实施页面的分配,并在页表上做记录。具体如何分配这个页框是通过调用handle_pte_fault()完成的。
handle_pte_fault()函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:
(1)请求调页:被访问的页框不在主存中,那么此时必须分配一个页框,分为线性映射、非线性映射、swap情况下映射
(2)写实复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中
handle_pte_fault()调用pte_non()检查表项是否为空,即全为0;如果为空就说明映射尚未建立,此时调用do_no_page()来建立内存页面与交换文件的映射;反之,如果表项非空,说明页面已经映射,只要调用do_swap_page()将其换入内存即可
7.9动态存储分配管理
图7.9动态内存分配管理过程
注意:代码段中存储的是可执行的代码和只读常量,很多人看到代码段就认为里面只有代码,数据段里面才是存储数据的,其实不是这样的。
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有三种分别是:首次适配,下一次适配和最佳适配。首次适配是从开始处往后搜索,下一次适配是从上一次适配发生处开始搜索,最佳适配依次检查所有块,性能要比首次适配和下一次适配都要高。
策略:分为隐式空闲链表和显示空闲链表。任何实际的分配器都需要一些数据结构,允许他来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。
显示空间链表的基本原理:
将空闲块组织成链表形式的数据结构。因为根据定义,程序需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面,例如,堆可以组织成一个双向空闲链表,在每 个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如下图:
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时 间减少到了空闲块数量的线性时间。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处, 使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在 这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块 的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索 来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配 有着更高的内存利用率,接近最佳适配的利用率。
隐式空闲链表:通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。详尽的介绍了与hello的存储管理有关的内容。
总结:
创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。进程对内存区域的分配最终都会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap()),
内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况, do_mmap()函数都会将一个地址区间加入到进程的地址空间中--无论是扩展已存在的内存区域还是创建一个新的区域。
同样,释放一个内存区域应使用函数do_ummap(),它会销毁对应的内存区域。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
用到的文件I/O函数有以下几个:打开文件、读文件、写文件、关闭文件等对应用到的函数有:open、read、write、close、lseek(文件指针偏移)
1.打开文件:打开一个已经存在的文件或者创建一个新的文件
2.关闭文件:关闭一个打开的文件
3.读文件:从当前文件位置复制字节到内存位置
4.写文件:从内存复制字节到当前文件位置
设备管理:unix io接口
I/O接口的基本功能:
1.地址译码,选取接口寄存器
2.接收控制命令,提供工作状态信息
3.数据缓冲(速度匹配),格式转换
4.控制逻辑,如中断、DMA控制逻辑、设备操作等
8.2 简述Unix IO接口及其函数
5个基本的I/O系统函数: open(), read(), write(), lseek(), close()
由此可知,printf的参数虽然五花八门,打印的内容各种形式,但都要做一定的处理,将它格式化。
write函数的实现难度在于权限,还有与硬件的联系。
而前期准备那么多,sys_call才是真正能将格式化的字符串显示出来的。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中).当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符.getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕.如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
主要的就是与缓冲区的联系。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。
(第8章1分)
结论
hello.c只是一个短短十几行的程序文件,所谓麻雀虽小五脏俱全,hello.c文件包含了头文件,各个函数,各个参数,各个变量。我们可以从预处理到编译,到汇编到链接的hello.i,hello.s,hello.o,hello可执行目标文件以及一些重定位文件中找到这些内容出现的身影。尽管小并且简单,它在电脑的内存中也占有一席之地。还要有进程管理和设备管理为它安排……
为了实现打印短短的hello,我们要结合前辈们的辛苦结果,硬件本身的强大,还有程序员的对代码的敏感和热情。短短十几行浓缩的是这个时代的快速、便捷和智能!
hello world更像是新生对世界的希望,计算机的影子已经遍布全世界。它在向我们打招呼的时候我们就已经预料到他对世界的影响,可没有想到自它出现就没有东西能取代它。hello world,也是程序员对这一方机器的问候。
这次的大作业让我对计算机系统的各项内容又有了更加深刻的认识,在各种实现中都是对以往实验的引用和理解。其中出现的更多的是书上的内容和实验的内容。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 作用
hello.i 预处理后的文本文件
hello.s 编译后的文本文件
hello.o 汇编之后的可重定位目标执行文件
hello 链接之后的可执行的目标文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式
helloo.txt hello.o的反汇编代码
hello.txt 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.
[7] https://blog.csdn.net/shreck66/article/details/47039937
[8] https://www.cnblogs.com/gtarcoder/p/5295281.html
[9] https://www.cnblogs.com/tcicy/p/10185353.html
[10] https://blog.csdn.net/YYYY_zzzz/article/details/53354091
[11] https://zhidao.baidu.com/question/2052432115864568507.html
[12] https://blog.csdn.net/guiliguiwang/article/details/80605456
[13] https://blog.csdn.net/hellojoy/article/details/77340238
[14]《深入理解计算机系统》教材
[15] https://www.cnblogs.com/lhyhahaha/p/8053768.html
[16]www.baidu.com
(参考文献0分,缺失 -1分)
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 1170301028
班 级 1703010
学 生 梁雅琪
指 导 教 师 史先俊
计算机科学与技术学院
2018年12月
摘 要
hello.c只是一个短短十几行的程序文件,所谓麻雀虽小五脏俱全,hello.c文件包含了头文件,各个函数,各个参数,各个变量。我们可以从预处理到编译,到汇编到链接的hello.i,hello.s,hello.o,hello可执行目标文件以及一些重定位文件中找到这些内容出现的身影。尽管小并且简单,它在电脑的内存中也占有一席之地。还要有进程管理和设备管理为它安排……
关键词:hello;预处理;编译;链接;内存;进程;管理
(摘要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:首先编写生成一个hello.c的程序文本,然后经过cpp预处理—>ccl编译—>as汇编—>ld链接,生成一个可执行目标程序,然后shell为他fork建立一个子进程,成为一个Program。
020:shell帮他execute,将他映射进虚拟内存,然后在开始运行进程的时候分配并载入物理内存,然后进入main函数执行目标代码。CPU为他分配时间片执行逻辑控制流。运行结束后,shell回收进程,内核删去有关数据结构。
1.2 环境与工具
硬件环境:X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:Codeblocks,objdump,gdb,edb.vim.gcc,ld,ebd,readelf等
1.3 中间结果
hello.i 预处理后的文本文件
hello.s 编译后的文本文件
hello.o 汇编之后的可重定位目标执行文件
hello 链接之后的可执行的目标文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式
helloo.txt hello.o的反汇编代码
hello.txt hello的反汇编文件
1.4 本章小结
本章节简述了本次大作业的内容,环境工具,以及在实验过程中的产生的文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(cpp)根据以#开头的命令,修改显示的C程序。
作用:1)将源文件中用#include形式声明的文件复制到新的程序中。比如hello.c第6-8行中的#include
2)用实际值替换用#define定义的字符串
3)根据#if后面的条件决定需要编译的代码
4)还包括此次hello中没有的#line,#error,#pragma,以及单独的空指令的处理。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图2.2预处理产生hello.i文件
2.3 Hello的预处理结果解析
使用vim打开hello.i文件后,看到hello.i有3118行代码。
图2.3.1vim查看 hello.i文件底端
在代码中看到,头文件执行很靠前,其中包含了在电脑中头文件的位置和其他相关信息。头文件的执行运用了很多的结构体,代码数也较多,执行起来原来并不如hello.c文件中看起来的那么容易。
图2.3.2vim查看hello.i文件
接下来是一些宏定义和函数的声明。代码数也十分庞大。
图2.3.3vim查看hello.i文件
而真正的main函数却是在最后3009行的位置。
看的出来预处理所做的工作,就是对#的指令进行处理(调用等)。
2.4 本章小结
本章节简单介绍了编译前预处理的过程,包括预处理的概念和作用,对于处理进行演示,通过分析更好的说明了理论的内容。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念与作用:
编译(compilation , compile) 1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。
编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这个过程称为编译,也是作用。把高级语言变成计算机可以识别的2进制语言。计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
也可以说,编译就是“翻译”。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3.2编译产生hello.s文件
3.3 Hello的编译结果解析
打开hello.s文件,其中代码行数仅有65行,规模已经很小了。
它主要分了几个部分:
3.3.0汇编指令
代码开始就是一些汇编指令:
图3.3.1 hello.s文件开头
对指令解释如下:
.file 声明源文件
.text 以下是代码段
.data 以下是数据段
.globl 声明一个全局变量
.align 声明对指令或者数据存放地址进行对齐的方式
.type 指定函数类型或对象类型
.size 声明大小
.long 声明一个long类型
.string 声明一个string类型
.section .rodata 以下是rodata节
3.3.1数据:
hello.c中使用到的数据类型有字符串、整数和数组:
1) 字符串
hello.c中字符串有两个:
1>“Usage: Hello 学号 姓名!\n”, 第一个printf传入的输出格式化参数,可以发现字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
2>“Hello %s %s\n”,第二个printf传入的输出格式化参数
声明在.LC0和.LC1段中,并且声明在了只读数据节,声明如下:
图3.3.2编译产生hello.s文件
2) 整数
图3.3.3编译产生hello.s文件
1> 全部变量 int sleepsecs
一个整型的变量,初值为浮点数。所以值应当为2,而不是2.5。
声明在了.globl(全局变量)之下,并且已经赋值了,所以编译处理也应当在.data 节声明这个变量。在hello.s中,它先放在.text代码段中声明是全部变量,其次在.data中设置对齐方式为4,设置大小为4字节,设置为long类型,值为2。
由此看来,全部变量作为在代码中任意函数皆可调用的变量,他的实现也会比较麻烦。而这里为什么要设置为long类型呢,应该是编译器偏好。
2> int i:编译器将局部变量存储在寄存器或者栈空间中,在hello.s中编译器将i存储在栈上空间-4(%rbp)中,可以看出i占据了栈中的4B。
3> int argc:作为第一个参数传入
4> 立即数:其他整形数据的出现都是以立即数的形式出现的,直接硬编码在汇编代码中
3) 数组
char *argv[] main,函数执行时输入的命令行,argv作为存放char指针的数组同时是第二个参数传入。而只使用数组中的两个值
图3.2编译产生hello.s文件
Argv指针指向一片分配好的,连续的,存放着字符的空间,然后分别获取argv[1](黑色框显示)和argv[2](绿色框显示)的地址,其中只有栈的指针的移动。
3.3.2赋值
赋值操作只有两个:
下一条jmp指令应当是跳入循环的指令
3.3.3类型转换
这里应该只有一个隐式类型转换:int sleepsecs=2.5
更加容易忽略,但在初学C语言时,就接触过,不应当忘记。
整型变量赋值了浮点数类型,浮点数默认类型为double型,这里的原则是:向零舍入,所以值应当为2。
3.3.4算术操作
对算术操作的汇编指令做一个整理:
图3.3.3.1算术操作指令
这里的整理只写了部分具体的操作,可以发现,汇编语言中的算术操作与别的操作一样,主要操作数是后边的那个。
hello.c中涉及到算术操作的有:
1)i++,在for循环中做计数器,使用的指令如下:
2)汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。
这里我确实没注意到,是由同学指点发现的。
3.3.5关系操作
对关系操作的汇编指令做一个总结:
图3.3.4.1关系操作指令
hello.c程序中涉及到的有:
1)argc!=3:
比较之后,如果等于做跳转。
2)i<10
比较之后,如果小于或等于做跳转。
3.3.6控制转移
程序中涉及的控制转移有:
图3.3.6编译产生hello.s文件
图3.3.6.2编译产生hello.s文件
3.3.7函数操作
函数操作的指令总结和概念整理:
图3.3.7call指令
传递控制:在进入过程Q的时候,程序计数器必须被设置为Q代码的起始位置,然后返回时,要把程序程序计数器设置为调用的那一条语句
传递数据:P必须向Q传递n个参数,Q必须向P返回一个值。
分配和释放内存:在开始是,Q可能需要为局部空间分配内存,而在返回之前必须释放掉这些存储空间。
x86-64的过程实现包括特殊的指令和一些对机器资源使用的约束。
转移控制的实现需要上面两条汇编指令的支持。P调用个过程Q,执行call Q指令,该指令会把调用过程的下一条指令A保存在P的栈帧中,并把PC寄存器设置为Q的起始位置。对应的指令会将PC设置为A,并将A弹出P的栈帧。
程序中涉及的函数操作的有:
main函数
传递控制:main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数
传递数据:传入参数argv[],argc。分别用%rdi和%rsi存储
释放:ret相当于pop IP
printf函数
传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
exit函数
传递数据:将%edi设置为1
sleep函数
传递数据:将%edi设置为sleepsecs
getchar函数
3.4 本章小结
本章主要阐述了编译的概念、作用和各类操作的分析。展示了如何将C程序编译成汇编程序。C语言与汇编语言之间分别表现了两种语言的形式和着重点。其中C语言能更加方便快捷的被程序员使用,汇编语言则更贴近计算机执行的过程。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
as将.s汇编程序经过编译生成.o机器语言程序
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图4.2汇编产生hello.o文件
4.3 可重定位目标elf格式
ELF文件主要有三种类型: (1)可重定位文件包含了代码和数据.可与其它ELF文件建立一个可执行或共享的文件. (2)可执行文件时可直接执行的程序. (3)共享目标文件包括代码和数据。这里是第一种:
使用readelf -a hello.o > hello.elf 指令获得hello.o文件的ELF格式。
组成如下:
1)ELE头:
图4.2.1重定位后的ELF格式文件
ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行、共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量
2)节头:
图4.2.2重定位后的ELF格式文件
各个节:
[1].text:已编译程序的机器代码
[2].rodata:只读数据
[3].data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
[4].bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0.
[5].symtab:符号表,过程和静态变量名,节名和位置
[6].rel.text:文本部分的重新定位信息,在可执行文件中需要修改的指令的地址,修改指令
[7].rel.data:数据段的重新定位信息,在合并的可执行文件中需要修改的指针数据的地址
[8].debug:符号调试信息(gcc -g)
[9].line:初始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
[10].srttab:一个字符串表,以null结尾的字符串序列
3)程序头:记录如何整合到虚拟地址空间的
4)重定位节
图4.2.3重定位后的ELF格式文件
重定位节.rela.text ,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。图中8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、sleepsecs、sleep函数、getchar函数进行重定位声明。.rela.eh_frame:eh_frame节的重定位信息
过程:
(1)判断运行地址和链接地址是否相等
(2)如果运行地址和链接地址是相等的话,那么就没有必要进行重定位。直接跳过重定位的部分,直接跳转到clean_bss
(3)如果运行地址和链接地址不相等的话,那么就需要进行重定位了。重定位的实际上就是拷贝数据段和代码段的内容从运行地址到链接地址处。
4.4 Hello.o的结果解析
图4.4hello.o与hello.o的对比
打开之后发现汇编代码和机器码的代码规模差不多,看起来也有很多相似的内容,做了每一行的对比之后发现,.s文件中有操作码的每一行都有在.o中文件中对应的代码。
.s文件其中含有.cfi_startproc等类似的内容,出现在代码的开头和结尾,应当是进程从main函数开始,和回收的相应的指令,而.o文件中并没有类似内容。
.o文件中R_X86_64_PC32等内容,发生在lea指令(后面带有只读代码段的记号),和函数调用指令后。而.s文件中没有
1.全局变量访问:
对于hello.s,全局变量的访问方式为:段名称+%rip,而对于hello.o的反汇编为0+%rip,因为rodata的数据地址是在运行时确定的,故也需要重定位,所以现在是0,并添加了重定位条目。
2.分支转移
我们可以发现,在hello.s中跳转到的目标位置都是用.L3/.L4来表示的,在hello.o反汇编之后,这些目标被用具体的地址位置代替。
3. 函数调用
在原先的hello.s中,调用一个函数只需被表示成call+函数名,但是在hello.o反汇编的结果中我们可以看见,这里的call是call一个具体的地址位置。
4.5 本章小结
本章阐述了hello.s汇编语言转换成hello.o机器语言的过程,重定位查看hello.o的内容,分析了作用,然后又对比了hello.s与hello.o之间映射与差别。
机器语言与汇编语言的差距不是很大,但是计算机仍然要将汇编语言翻译为机器语言,才能达到人与机器的沟通,在整理期间,我也在思考既然差别不大,为什么人们不直接编译机器语言。其实忽略了,重要的并非是我们可以看明白的代码,而是我们看不懂的但机器能懂得的16进制的机器码。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接器主要是将有关的目标文件彼此相连接生成可加载、可执行的目标文件。即从 hello.o 到hello生成过程。链接器的核心工作就是符号表解析和重定位。
作用:
图5.2链接生成hello可执行目标文件
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
图5.3重定位后的ELF格式文件
ELF头:这里的节头数量从刚才的13变为现在的25,规模显然大了不少。
图5.3.1重定位后的ELF格式文件
节头:
图5.3.2重定位后的ELF格式文件
节头对hello中所有的节信息进行了声明,其中包括大小以及在程序中的偏移量,因此根据节头中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中地址是程序被载入到虚拟地址的起始地址
图5.3.3重定位后的ELF格式文件
程序头中:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
5.4 hello的虚拟地址空间
这里显然是ELF头
ELF是以16个字节的序列开始,从这里与5.3中对比完全一致。
这里与程序头中的各节地址相对应。
5.5 链接的重定位过程分析
图5.4hello.o与hello对比
5.5.1hello.o与hello不同
5.6 hello的执行流程
ld-2.27.so!_star 0x7fff f7dd6090
ld-2.27.so!_dl_start_user 0x7fff f7dd6098
ld-2.27.so!_dl_init 0x7fff f7dd5630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fff f7dd5ab0
libc-2.27.so!__cxa_atexit 0x7fff f7a27430
libc-2.27.so!__new_exitfn 0x7fff f7a27220
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fff f7a22c10
-libc-2.27.so!_sigsetjmp 0x7fff f7df2090
hello!main@plt 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt – 0x4004e0
*hello!printf@plt – 0x4004c0
*hello!sleep@plt – 0x4004e0
*hello!getchar@plt – 0x4004f0
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fff f7dec680
-ld-2.27.so!_dl_fixup 0x7fff f7de4f90
-ld-2.27.so!_dl_lookup_symbol_x 0x7fff f7de00b0
libc-2.27.so!exit 0x7fff f7df2030
执行hello过程:
首先由hello!_start—>libc-2.27.so!__libc_start_main—>-libc-2.27.so!__cxa_atexit—>libc-2.27.so!__new_exitfn—>hello!__libc_csu_init—>hello!_init—>
libc-2.27.so!_setjmp—>libc-2.27.so!_sigsetjmp—> libc-2.27.so!__sigjmp_save—>
hello!main
5.7 Hello的动态链接分析
执行前:
执行后:
在edb调试之后我们发现原先0x00600a10开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量,这是初始化hello程序的一步。
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。在一次熟悉用edb调试,其中要注意查找的功能。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量
6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。
处理流程:
1)读取用户的输入
2)分析输入内容,获得输入参数
3)如果是内核命令则直接执行,否则调用相应的程序执行命令
4)在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应
6.3 Hello的fork进程创建过程
fork通过复制父进程来创建一个新的进程。这个新创建 的进程称为调用fork()函数的子进程,这个调用fork()的进程称为子进程的父进程。子进程除了以下几点之外就是父进程的一个复制品。
1)子进程有其唯一的PID;
2)子进程的父进程PID(PPID)和父进程的PID是相同的;
3)子进程不继承父进程的内存块;
4)子进程的资源使用计数器和CPU时间计数器都将被置为空;
5)子进程挂起信号量的数目初始化为0;
6)子进程并不继承父进程的信号量调节器;
7)子进程并不继承父进程的记录锁;
8)子进程不继承父进程 的时间计数器;
9)子进程不继承父进程的异步I/O操作和异步I/O操作内容
图6.3进程的过程
6.4 Hello的execve过程
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
图6.4堆栈信息
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成
加载器设置PC指向_start地址,_start最终调用hello中的main函数。
通过逻辑控制流,在不同的进程间切换。分配给某个进程的时间就叫做进程的时间片。上下文信息即重新启动一个被抢夺的进程的条件。用户态中,程序不允许执行一些特权功能,而核心态中可以,它们之间需要某些条件才能切换
图5.3进程
6.6 hello的异常与信号处理
正常运行时:当程序执行完成之后,进程被回收
按ctrl+c后:当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程
按多个回车后:回车被当做一个输入,没有输入就没有指令。
按ctrl+z后:当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起
ps:这里说明进程没有被回收
jobs:值为1
pstree:
kill:杀死进程之后,ps中就没有hello了
6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。这里的内容更加抽象了。也有点难理解,但是结合书上内容和以往实验的内容还是能够解决。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是机器语言指令中,用来指定一个操作数或者是一条指令的地址。
线性地址与虚拟地址:csapp课本上,虚拟地址就是线性地址。逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
这其实说明了,逻辑地址和线性地址都是不真实的地址,逻辑地址对应的硬件平台段氏管理转换前地址的话,那么线性地址就对应了硬件页式内存的转换前地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。它是真实的。
结合hello来说,hello.o中应该为逻辑地址,hello.s应当为线性地址或者虚拟地址,hello中的地址应为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1、逻辑地址=段选择符+偏移量
2、每个段选择符大小为16位,段描述符为8字节(注意单位)。
3、GDT为全局描述符表,LDT为局部描述符表。
4、段描述符存放在描述符表中,也就是GDT或LDT中。
5、段首地址存放在段描述符中。
每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。下图是一个段选择符的格式
从上图可以看出段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引)。当TI=0时,表示段描述符在GDT中,当TI=1时表示段描述符在LDT中。index表示某个段描述符在数组中的索引。
现在我们假设有一个段的段选择符,它的TI=0,index=8。我们可以知道这个段的描述符是在GDT数组中,并且他的在数组中的索引是8。
假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index,从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
基本概念:分页的基本方法是将地址空间人为地等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。目前,以大小为4KB的分页是绝大多数PC操作系统的选择.
1)逻辑空间等分为页;并从0开始编号。
2)内存空间等分为块,与页面大小相同;从0开始编号。
3)分配内存时,以块为单位将进程中的若干个页分别装入。
转化方法:
1.CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。
2.从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。
3.将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
1.内存管理单元MMU既有段描述符也有页描述符。TLB位于MMU内部,不需要经过页表就可以将虚拟地址转换为物理地址,速度更快。
每一个TLB寄存器的每个条目包含一个页面的信息:有效位,虚页面号,修改位,保护码,和页面所在的物理页面号,它们和页面表中的表项一一对应
MMU在翻译的时候,先查看虚拟地址的虚拟页面号是否存在TLB(并行的查找)中,如果存在,且没有违背读写权限限制,则直接给出TLB中的物理页面号;若在TLB中不存在,则进行常规的页表的查找,然后从TLB中淘汰一个条目,并更新为刚刚查找的页面
2.VA:未使用MMU
3.PA:使用MMU,接受CPU发来的地址做某种转化。
转换主要需要建立映射关系:
VA到PA的转化过程:
首先需要CPU发出VA,经过一个索引,找到相应的下标,查表后将之翻译出来,得到物理地址的前12位,与CPU发出的VA后20位结合,就是实际的物理地址。
图7.4 VA到PA转化过程
7.5 三级Cache支持下的物理内存访问
图7.5.1储存器层次结构
cache:高速缓存,从上图就可以看出高速缓存的高效和高昂,三级cache是为了能在达到效率的前提下降低成本。
cache的访问:
获得物理地址VA后
,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换
图7.5.2内存访问过程
7.6 hello进程fork时的内存映射
fork的内存映射是使用特殊文件提供匿名内存映射,而这种内存映射,适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程 继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射(可以使用fd =-1 的方式,也可以用fd = open(“dev/zero”,)的方式)的方式。此时,不必指定具体的文件,只要设置相应的标志即可。
表面看起来fork()创建子进程子进程拷贝了父进程的地址空间其实不然
刚调用完fork()之后,子进程只是拥有一份和父进程相同的页表,其中页表中指向RAM代码段的部分是不会改变的,而指向数据段,堆段,栈段的会在我们将要改变父子进程各自的这部分内容时,才会将要操作的部分进行部分复制。
7.7 hello进程execve时的内存映射
使用execve就是一次系统调用,首先要做的将新的可执行文件的绝对路径从调用者(用户空间)拷贝到系统空间中。在得到可执行文件路径后,就找到可执行文件打开,由于操作系统已经为可执行文件设置了一个数据结构,就初始化这个数据结构,保存一个可执行文件必要的信息。其中步骤有一下几步:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
7.8.1缺页终端概念:
进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
7.8.2查看缺页终端次数:
ps -o majflt,minflt -C program查看
majflt和minflt表示一个进程自启动以来所发生的缺页中断的次数;
7.8.3产生缺页中断的几种情况:
1、当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页中断,并且可能是栈溢出导致的缺页中断;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,它的vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页中断的写时复制;
7.8.4缺页中断的处理:
1.当前执行流程在内核态时
(1)通过mm是否存在判断是否是内核线程,对于内核线程,进程描述符的mm总为NULL,一旦成立,说明是在内核态中发生的异常,跳到no_context
if (in_atomic() || !mm)
goto no_context;
如果当前执行流程在内核态,不论是在临界区还是内核进程本身(内核的mm为NULL),说明在内核态出了问题,跳到标号no_context进入内核态异常处理,由函数_do_kernel_fault完成。
(2)
这个函数首先尽可能的设法解决这个异常,通过查找异常表中和目前的异常对应的解决办法并调用执行;如果无法通过异常表解决,那么内核就要在打印其页表等内容后退出。
2.用户进程的缺页中断
对于用户空间的缺页中断,则会调用函数_do_page_fault.
首先从CPU的控制寄存器CR2中读出出错的地址address,然后调用find_vma(),在进程的虚拟地址空间中找出结束地址大于address的第一个区间,如果找不到的话,则说明中断是由地址越界引起的,转到bad_area执行相关错误处理;
确定并非地址越界后,控制转向标号good_area。在这里,代码首先对页面进行例行权限检查,比如当前的操作是否违反该页面的Read,Write,Exec权限等。如果通过检查,则进入虚拟管理例程handle_mm_fault().否则,将与地址越界一样,转到bad_area继续处理。
handle_mm_fault()用于实现页面分配与交换,它分为两个步骤:首先,如果页表不存在或被交换出,则要首先分配页面给页表;然后才真正实施页面的分配,并在页表上做记录。具体如何分配这个页框是通过调用handle_pte_fault()完成的。
handle_pte_fault()函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:
(1)请求调页:被访问的页框不在主存中,那么此时必须分配一个页框,分为线性映射、非线性映射、swap情况下映射
(2)写实复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中
handle_pte_fault()调用pte_non()检查表项是否为空,即全为0;如果为空就说明映射尚未建立,此时调用do_no_page()来建立内存页面与交换文件的映射;反之,如果表项非空,说明页面已经映射,只要调用do_swap_page()将其换入内存即可
7.9动态存储分配管理
图7.9动态内存分配管理过程
注意:代码段中存储的是可执行的代码和只读常量,很多人看到代码段就认为里面只有代码,数据段里面才是存储数据的,其实不是这样的。
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有三种分别是:首次适配,下一次适配和最佳适配。首次适配是从开始处往后搜索,下一次适配是从上一次适配发生处开始搜索,最佳适配依次检查所有块,性能要比首次适配和下一次适配都要高。
策略:分为隐式空闲链表和显示空闲链表。任何实际的分配器都需要一些数据结构,允许他来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。
显示空间链表的基本原理:
将空闲块组织成链表形式的数据结构。因为根据定义,程序需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面,例如,堆可以组织成一个双向空闲链表,在每 个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如下图:
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时 间减少到了空闲块数量的线性时间。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处, 使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在 这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块 的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索 来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配 有着更高的内存利用率,接近最佳适配的利用率。
隐式空闲链表:通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。详尽的介绍了与hello的存储管理有关的内容。
总结:
创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。进程对内存区域的分配最终都会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap()),
内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况, do_mmap()函数都会将一个地址区间加入到进程的地址空间中--无论是扩展已存在的内存区域还是创建一个新的区域。
同样,释放一个内存区域应使用函数do_ummap(),它会销毁对应的内存区域。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
用到的文件I/O函数有以下几个:打开文件、读文件、写文件、关闭文件等对应用到的函数有:open、read、write、close、lseek(文件指针偏移)
1.打开文件:打开一个已经存在的文件或者创建一个新的文件
2.关闭文件:关闭一个打开的文件
3.读文件:从当前文件位置复制字节到内存位置
4.写文件:从内存复制字节到当前文件位置
设备管理:unix io接口
I/O接口的基本功能:
1.地址译码,选取接口寄存器
2.接收控制命令,提供工作状态信息
3.数据缓冲(速度匹配),格式转换
4.控制逻辑,如中断、DMA控制逻辑、设备操作等
8.2 简述Unix IO接口及其函数
5个基本的I/O系统函数: open(), read(), write(), lseek(), close()
由此可知,printf的参数虽然五花八门,打印的内容各种形式,但都要做一定的处理,将它格式化。
write函数的实现难度在于权限,还有与硬件的联系。
而前期准备那么多,sys_call才是真正能将格式化的字符串显示出来的。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中).当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符.getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕.如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
主要的就是与缓冲区的联系。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。
(第8章1分)
结论
hello.c只是一个短短十几行的程序文件,所谓麻雀虽小五脏俱全,hello.c文件包含了头文件,各个函数,各个参数,各个变量。我们可以从预处理到编译,到汇编到链接的hello.i,hello.s,hello.o,hello可执行目标文件以及一些重定位文件中找到这些内容出现的身影。尽管小并且简单,它在电脑的内存中也占有一席之地。还要有进程管理和设备管理为它安排……
为了实现打印短短的hello,我们要结合前辈们的辛苦结果,硬件本身的强大,还有程序员的对代码的敏感和热情。短短十几行浓缩的是这个时代的快速、便捷和智能!
hello world更像是新生对世界的希望,计算机的影子已经遍布全世界。它在向我们打招呼的时候我们就已经预料到他对世界的影响,可没有想到自它出现就没有东西能取代它。hello world,也是程序员对这一方机器的问候。
这次的大作业让我对计算机系统的各项内容又有了更加深刻的认识,在各种实现中都是对以往实验的引用和理解。其中出现的更多的是书上的内容和实验的内容。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 作用
hello.i 预处理后的文本文件
hello.s 编译后的文本文件
hello.o 汇编之后的可重定位目标执行文件
hello 链接之后的可执行的目标文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式
helloo.txt hello.o的反汇编代码
hello.txt 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.
[7] https://blog.csdn.net/shreck66/article/details/47039937
[8] https://www.cnblogs.com/gtarcoder/p/5295281.html
[9] https://www.cnblogs.com/tcicy/p/10185353.html
[10] https://blog.csdn.net/YYYY_zzzz/article/details/53354091
[11] https://zhidao.baidu.com/question/2052432115864568507.html
[12] https://blog.csdn.net/guiliguiwang/article/details/80605456
[13] https://blog.csdn.net/hellojoy/article/details/77340238
[14]《深入理解计算机系统》教材
[15] https://www.cnblogs.com/lhyhahaha/p/8053768.html
[16]www.baidu.com
(参考文献0分,缺失 -1分)