HIT CSAPP 大作业

HIT CSAPP 大作业

摘 要
本论文从hello.c到hello可执行程序再到程序的具体执行和终止逐步分析了hello程序。
hello的P2P,也就是program to process,从输入hello.c文件的源代码,到cpp预处理成hello.i,再到ccl编译生成汇编语言文件hello.s,并组装为可重定位文件hello.o,ld将其与各个库链接,shell收到./hello的指令后,调用fork创建进程,execve加载到内存来完成这个程序。之后程序由cpu控制其逻辑流程的操作、中断、上下文切换来处理异常,最后结束,父进程回收资源。
结合课本中的知识和一些资料,利用Ubuntu虚拟机对gdb、edb、gcc等工具进行调试和测试。 结合本学期计算机系统各章节的知识,展示自己的能力,形成自己的收获,体现自己对此课程的理解。

关键词:hello.c;大作业;深入理解计算机系统;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 5 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 7 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.4 本章小结 - 11 -
第4章 汇编 - 12 -
4.1 汇编的概念与作用 - 12 -
4.2 在UBUNTU下汇编的命令 - 12 -
4.3 可重定位目标ELF格式 - 12 -
4.4 HELLO.O的结果解析 - 14 -
4.5 本章小结 - 15 -
第5章 链接 - 16 -
5.1 链接的概念与作用 - 16 -
5.2 在UBUNTU下链接的命令 - 16 -
5.3 可执行目标文件HELLO的格式 - 17 -
5.4 HELLO的虚拟地址空间 - 17 -
5.5 链接的重定位过程分析 - 18 -
5.6 HELLO的执行流程 - 19 -
5.7 HELLO的动态链接分析 - 19 -
5.8 本章小结 - 20 -
第6章 HELLO进程管理 - 21 -
6.1 进程的概念与作用 - 21 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 21 -
6.3 HELLO的FORK进程创建过程 - 21 -
6.4 HELLO的EXECVE过程 - 22 -
6.5 HELLO的进程执行 - 22 -
6.6 HELLO的异常与信号处理 - 23 -
6.7本章小结 - 25 -
第7章 HELLO的存储管理 - 26 -
7.1 HELLO的存储器地址空间 - 26 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 26 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 27 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 28 -
7.5 三级CACHE支持下的物理内存访问 - 28 -
7.6 HELLO进程FORK时的内存映射 - 29 -
7.7 HELLO进程EXECVE时的内存映射 - 29 -
7.8 缺页故障与缺页中断处理 - 29 -
7.9动态存储分配管理 - 30 -
7.10本章小结 - 32 -
第8章 HELLO的IO管理 - 33 -
8.1 LINUX的IO设备管理方法 - 33 -
8.2 简述UNIX IO接口及其函数 - 34 -
8.3 PRINTF的实现分析 - 34 -
8.4 GETCHAR的实现分析 - 36 -
8.5本章小结 - 36 -
结论 - 37 -
附件 - 38 -
参考文献 - 39 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:首先hello.c 利用I/O设备通过总线存储进内存中。GCC编译器驱动然后读取源程序文件hello.c,通过预处理器cpp转化为hello.i(修改后的源程序),再利用编译器ccl读入hello.s(汇编语言),然后用as将其汇编变成hello.o(可重定位的目标程序),hello.o 是机器友好的二进制代码。最后,它通过链接器ld与标准C库动态链接,并最终成为hello(一个可执行的目标程序)。
此时,hello已经是一个可执行的程序。
然后在shell中输入字符串“./hello”。 shell 程序将字符串读入寄存器并解析。然后shell调用fork函数来创建一个新的子进程。子进程是父进程shell的副本,再通过execve函数调用启动加载器。加载程序删除子进程现有的虚拟内存段,然后使用mmap函数创建新的内存区域,创建一组新的代码、数据、堆栈段。新的堆栈段被初始化为零。通过将虚拟地址空间中的页面映射到可执行文件的页面大小块,新的代码和数据段被赋值为可执行文件的对应内容。最后,加载器跳转到_start的地址运行应用main函数。
020:程序在内存中From Zero to Zero,hello先是执行了P2P所述的过程,然后在程序执行结束之后,hello对应的进程会保持在终止状态,直到被其父进程回收然后退出,shell会再次变成执行hello之前的状态,也就是说又变成了Zero。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境
X64 CPU;2GHz;8G RAM;256GHD disk
软件环境
Windows10 64位;Vmware 11;Ubuntu 18.04;LTS 64位
开发工具
Visual Studio 2017;CodeBlocks 64位;vi/vim/gedit+gcc
Readelf;gdb/edb;objdump
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:c源代码文件
hello.i:预处理后的文件
hello.s:编译后的汇编文件
hello.o:汇编后的汇编可重定位文件
hello:链接后生成的可执行文件
hello.elf: hello.o的ELF文件
hellold.elf: hello.out的ELF文件
1.4 本章小结
第一章简单解释了P2P和020的概念,说明了本次大作业的环境和工具,列出了为编写本论文,生成的中间结果文件并解释了其作用。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
预处理:
预编译器根据以字符#开头的预处理指令,对代码进行初始的处理,最终的得到一个以.i为扩展名的C文件。
指令 作用

#define 定义宏
#undef 取消已定义的宏
#include 包含一个源文件
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif else if的简写
#endif 结束一个#if……#else条件编译块
#error 停止编译并显示错误信息
预处理的作用:
宏定义:用宏名与替换实际字串,增强代码可读性。
文件包含:将include的头文件复制到#后,提高编程的模块性。
条件编译:区分被编译和不被编译的代码,增加的选择。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
在这里插入图片描述
经过预处理之后,hello.c转化为hello.i文件
2.3 Hello的预处理结果解析
打开hello.i文件可以发现,文件main函数之前的内容变多,main函数C语言程序文本文件,只是对原文件中的宏进行了宏展开,头文件中的内容被加入此文件中。如果代码中有#define命令还会替换程序中对相应的符号。
(以下格式自行编排,编辑时删除)
2.4 本章小结
第二章介绍了预处理的相关概念及处理的类型,实现替换定义的宏符号、加入头文件的内容、根据指令进行选择性编译等。
(以下格式自行编排,编辑时删除)

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译是利用编译器从.i产生.s(汇编文本)的过程。主要包含五个阶段:词法分析;语法分析;语义检查、中间代码生成、目标代码生成。
作用:将文本文件hello.i翻译成文本文件hello.s,并提示出现的语法错误,执行过程主要从其中三个阶段进行分析:
词法分析:词法分析是扫描由字符组成的单词,把源程序中的字符串改造成为由单词符号串组成的的中间程序;
语法分析:将单词符号串输入语法分析器,其分析单词符号串是否符合语法规则得生成了语法单位;
目标代码生成:语法分析后的中间代码传入目标代码生成器,经汇编生成汇编语言代码,然后转化为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
命令:gcc -S -o hello.s hello.i
在这里插入图片描述
3.3 Hello的编译结果解析

打开hello.s文件

3.3.1 数据
局部变量:作为函数中的局部变量i被储存在栈中,栈地址:%rbp-4
argc:传入的参数,存在栈里,位于%rbp-20。
argv[]:传入的数组,存在栈里,argv的地址位于%rbp-32,利用argv的地址加i8,就能得到argv[i]。
立即数:储存在.data段中,在运行需要时加入寄存器中,如果无空闲寄存器则放入栈中。
表达式:存在代码段的.rodata中
3.3.2 赋值
为栈上的局部变量i赋初值=0,用movl赋值。
movl $0, -4(%rbp)
3.3.3 类型转换
atoi函数将字符型argv[3]转换为整型数。
3.3.4 算术操作
编译器将i++编译成
addl $1, -4(%rbp)
3.3.5 关系操作
编译器将i<8与跳转编译成
cmpl $7, -4(%rbp)
jle .L4
将argc!=4编译成
cmpl $4, -20(%rbp)
je .L2
3.3.6 数组/指针/结构操作
argv数组是传入的参数,储存在栈上。
初始地址位于%rbp-32,利用argv的地址加i
8,就能得到argv[i]
3.3.7 控制转移
编译器将if,for等控制转移语句都使用了cmp来比较然后使用了条件跳转指令来跳转。编译器将if(argc!=3)编译成:
cmpl $3, -20(%rbp)
je .L2
将for循环里面的比较和转移编译成:
cmpl $9, -4(%rbp)
jle .L4
3.3.8 函数操作
将printf(“用法: Hello 学号 姓名 秒数!\n”);编译为:

将printf(“Hello %s %s\n”,argv[1],argv[2]);编译为:

将sleep(atoi(argv[3]));编译为:

在这里插入图片描述
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
第三章讲述了编译阶段编译器如何把.i代码转换成.s汇编代码的,同时也分析c代码中的各数据与操作在汇编代码中是如何储存并实现的。汇编语言是每个程序员会经常接触到的一种代码语言,掌握汇编语言十分重要。(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:将.s汇编语言翻译成.o机器语言的过程
作用:汇编器将hello.s翻译成机器语言指令,并把这些指令打包转化成可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
在这里插入图片描述
应截图,展示汇编过程!
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello.elf
在这里插入图片描述

ELF头:文件格式
.text:机器代码
.rodata:只读数据(跳转表等)
.data:已初始化的全局变量
.bss: 未初始化的全局变量
.symtab:符号表
.rel.text:可重定位代码
.rel.data:可重定位数据
.debug:调试符号表
ELF头开始是一个16字节的序列,描述了生成该文件的系统的字的大小和字节顺序,剩下的部分是帮助链接器实现语法分析和解释目标文件的信息。其中包括ELF头大小,目标文件类型,节头部表的文件偏移,节头部表中条目的大小和数量。

重定位节.rela.text,如上图
重定位节:包含.text 节中需要进行重定位的代码,当链接器将其与其他文件链接时,需要修改这些位置。
重定位节.rela.text中各项符号的信息:
Offset:需要被修改的节的偏移量Info:包括symbol和type,symbol是前四个字节,type是后四个字节。
symbol:标识被修改后的内容应该指向的符号。
type:重定位的类型。
value:标识链接器如何修改新的应用。
Attend:有符号整型数,重定位的偏移调整。
Name:重定位到的目标的名称。
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
反汇编命令:objdump -d -r hello.o

将重定位信息(类型和相对位置等)加入,在连接时需修改其位置。
① 机器语言(由操作码和操作数组成)能够被机器识别
② 机器语言和汇编语言的关系是以一一对应的。
③ 机器语言的跳转(jxx、call等)使用的是目的地址与下一条指令的头部的偏移量来表示的,在汇编代码中使用的是标号表示的。
④ 机器语言的反汇编代码为每条指令都对应了具体的指令地址
⑤ 机器语言中,访问全局变量采用了短名称+%rip的方式
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
第四章介绍汇编和汇编后生成的可重定位文件,分析可重定位文件的结构和各个组成部分,以及各个组成部分的内容和功能,比较了机器语言和汇编代码的区别和关系,生成hello.o可重定位文件
(以下格式自行编排,编辑时删除)
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:链接器将文件hello.o中的一些未确定的变量,函数与其所要连接的.o文件进行合并,生成可执行文件hello
作用:可重定位目标文件.o不能直接执行,需要链接过后形可执行目标文件.out计算机才能执行hello程序。链接器实现了分离编译(模块化)。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
在这里插入图片描述
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

5.3 可执行目标文件hello的格式
命令:readelf -a hello > hellold.elf
在这里插入图片描述
在这里插入图片描述
在ELF格式文件中,Section Headers声明了hello中的所有section信息,包括程序中的size和offset,所以可以根据Section Headers中的信息使用HexEdit定位每个被占用的section在区间(起始位置,大小),其中Address是程序在虚拟地址处加载的起始地址。
只读内存段:起始:0x402000 大小:0x3b
读写内存段:起始:0x404048 大小:0x4
符号表和调试信息:起始:0x000000 大小:0x4c8
代码段:起始:0x4010f0 大小:0x145
……
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
edb中0x400000所储存的信息与表头所储存的信息一致
在这里插入图片描述

Init段存在于0x401000
rodata段在0x404048

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
在这里插入图片描述

不同:
跳转:hello.o为相对偏移地址,hello为虚拟内存地址。
Hello含有外部链接得到的函数。
hello相对hello.o增加了部分节(.init,.plt等)。
重定位:
一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即其一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道其输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:
·重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
·重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构,我们接下来将会描述这种数据结构。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
hello!_start 0x401090
__libc_start_main 0x403ff0
main 0x4010c1
hello!puts@plt 0x401030
hello!exit@plt 0x401070
hello!printf@plt 0x401040
hello!atoi@plt 0x401060
hello!sleep@plt 0x401080
hello!getchar@plt 0x401050
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态链接:
共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链楼(dynamic linking),是由一个叫做动态链接器(dytamic linkeg的程序来执行的。共享库也称为共享目标(shared object),在Linux系统中通常用,s0后缀来表示,微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)。
共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。
例:
GOT:GOT是一个数组,其中元素是8字节的地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时需要用到的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。根据hello的ELF文件可知GOT起始表位置为0x404000。
Dl_init执行前0x404000的16个字节均为0:
Dl_init执行后0x404000的16个字节有所改变:
在这里插入图片描述
在这里插入图片描述
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
第五章中主要介绍了链接的概念与作用,并且详细说明了hello.o是怎么与其他.o(.so)文件链接成为一个可执行目标文件的过程,展示了hello.o的ELF文件形式和各个节的含义,分析了hello的虚拟地址空间、重定位过程与动态链接过程。
(以下格式自行编排,编辑时删除)
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个正在运行的程序的实例。系统中的每个程序都在某个进程的上下文中运行。上下文由程序正确运行所必需的状态组成。这种状态包括存储在内存中的代码和程序数据、堆栈、通用寄存器的内容、程序计数器、环境变量和文件描述符的集合。
功能:进程为应用程序提供了两种抽象,一种是独立的逻辑控制流,一种是私有地址空间。提高CPU执行效率,减少因程序等待造成的CPU空闲和其他计算机软硬件资源的浪费。
6.2 简述壳Shell-bash的作用与处理流程
(以下格式自行编排,编辑时删除)
Shell是一个交互型应用及程序,代表用户运行其他命令,基本作用是解释并运行用户的指令。
流程:
① 读取用户命令行输入。
② 解析命令行字符串,获取命令行参数并构建传递给execve函数argv向量。
③ 检查第一个命令行参数是否是shell内置命令。
④ 否则fork创建子程序。
⑤ 在子进程中,继续步骤(2)获取参数并调用excve()运行程序。
⑥ 命令行末尾没有&,代表前台的工作,shell使用waitpid等待工作完成返回。
⑦ 命令行末尾有&,代表后台工作,shell返回。
6.3 Hello的fork进程创建过程
(以下格式自行编排,编辑时删除)
在终端中输入 ./hello ,shell 将解释命令行。 由于这不是一个内置的 shell 命令,所以调用fork来创建一个新的运行子进程并运行可执行程序 hello。 新创建的子进程几乎(但不完全)与父进程相同。 子进程从父进程获取用户级虚拟地址空间的相同但独立的副本。 所以当父进程调用fork时,子进程可以读写在父进程中打开的任何文件。
6.4 Hello的execve过程
调用execve函数:使用驻留在内存中的称为loader的操作系统代码加载并运行可执行目标文件hello,并映射私有区域创建新的程序代码、数据、bss和堆栈区。 code区和data区映射到hello中的.text和.data区,bss请求二进制0,映射到匿名文件。 栈和堆请求二进制零,初始长度为0。
映射共享区域:最后更新PC,将下一条指令指向代码区域的入口点即-start函数的地址。start函数调用系统启动函数 _libc_start_main来初始化执行环境,并调用main函数,此时argv向量被作为参数传递给主函数。
只有当出现错误时, execve 才会返回到调用程序。所以execve 调用一次并从不返回。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
在这里插入图片描述
进程的抽象:
独立的逻辑控制流,好像进程独占的使用CPU。
私有的地址空间,好像程序独占的使用内存。
hello进程的执行依赖于进程的抽象:
① 逻辑控制流::程序计数器 PC 的一系列值序列,进程轮流使用处理器,在同一个处理器核心中,每个进程执行其一部分流后被暂时挂起(上下文转换机制),然后处理器运行其他进程。
② 并发流:一个逻辑控制流的执行时间与另一个控制流重叠,成为并发流,这两个流(宏观上)并发的运行。
③ 时间片:一个进程执行其一部分控制流的一段时间叫做时间片。
④ 用户模式和内核模式:处理器通常使用一个寄存器区分两种模式的,该寄 存器记录着当前进程享有的权限,没有设置模式位时进程处于用户模式, 用户模式的进程不允许执行特权指令,也不允许直接引用内存空间中内核区内的代码和数据;设置模式位时进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问内存中的任何地址。
⑤ 上下文信息:上下文就是内核重新启动一个挂起的进程所需要的资源,包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等。
⑥ 上下文切换:在内核调用了一个新的进程并使其运行后,当前进程就会挂起,并使用上下文切换机制来将控制权转移到新的进程。
1)保存被挂起进程的上下文到内存
2)启用恢复进程保存的上下文,
3)将控制传递给新恢复的进程,来完成上下文切换。
在hello进程运行开始,shell已经fork后为hello程序分配了新的虚拟的地址空间,调用execve函数之后将hello的.txt和.data节分配到虚拟地址空间的代码区和数据区。当hello运行在用户模式下,输出hello 1190202001 杨永正,然后hello调用sleep函数后进入内核模式,它会处理sleep请求主动挂起当前进程,并将hello进程加入等待队列,sleep定时器开始计时,内核进行上下文切换并传递 控制当前进程到其他进程。当定时器倒计时结束,会发送一个中断信号。此时进入内核态进行中断处理,将hello进程从等待队列中移除并重新加入运行队列,hello进程继续其控制逻辑流。
在这里插入图片描述
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
异常:
在这里插入图片描述
信号(部分):
在这里插入图片描述

按下Ctrl+z会令内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。输入ps可以看到后台hello进程还在执行。

按下Ctrl+c会令内核发送一个SIGTSTP信号到前台进程组的每个进程,默认情况是终止前台作业。
在这里插入图片描述
程序运行过程中不停乱按可以发现输入的内容只是将屏幕的输入缓存到 stdin,当getchar()的时候读取了回车前的一个字符,其他字符串串只会作为 shell 命令行输入。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
第八章解释了hello进程管理的定义和功能。同时介绍了Shell的一般处理流程,分析了调用fork创建新进程、调用execve函数执行hello、hello进程执行、hello异常和信号处理的相关知识。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:用于确定机器语言中的指令或操作数的地址。一个逻辑地址包含一个段和一个偏移量,偏移量是一个相对偏移量,段决定了偏移量从哪里开始,这样就可以通过段和偏移量来确定地址。即hello.o中的相对偏移地址
线性地址:逻辑地址和物理地址之间的桥梁。即虚拟内存地址,可以通过将偏移量与段地址相加得到。也就是hello中的虚拟内存地址
虚拟地址:虚拟地址与线性地址相同。也就是hello中的虚拟内存地址
物理地址:装入内存地址寄存器的地址,内存单元的真实地址。通过地址转换器,可以将hello的虚拟地址转换为物理地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
在这里插入图片描述
7.2 Intel逻辑地址到线性地址的变换-段式管理
(1) 实地址模式:在实地址模式下,处理器使2位地址总线访问1MB(0~FFFFF)内存。 8086模式只有16位地址线,不能直接表示20位地址,采用内存分段方案。段地址存储在 16 位段寄存器中(CS、DS、ES、SS)
(2)保护模式:在保护模式下,段寄存器存储段描述符表中段描述符的索引值,称为段选择器。此时,CS存储代码段描述符的索引值,DS存储数据段描述。符号的索引值,SS存放堆栈段描述符的索引值
RPL代表程序当前的优先级,TI位代表段描述符的位置,TI=0段描述符在GDT中,TI=1段描述符在LDT中
48位全局描述符表寄存器GDTR指向GDT,即GDT在内存中的具体位置,16位局部描述符表寄存器LDTR指向LDT段在GDT中的位置。唯一的全局描述符表GDT包含了操作系统使用的代码段、数据段、堆栈段的描述符,每个程序的LDT段,每个程序都有一个独立的局部描述符表LDT,其中包含了操作系统的私有代码段对应的程序、数据段、堆栈段描述符、对应程序使用的门描述符:任务门、调用门等。
在保护模式下:线性地址=基地址(由段描述符给出)+偏移量
7.3 Hello的线性地址到物理地址的变换-页式管理
每个线性地址被分解为一个 10 位面目录索引、一个 10 位页表和一个 12 位索引偏移量:
然后根据线性地址的前十位找到索引项,根据页索引项确定页表的地址。根据线性地址的中间十位,在页表中找到该页的起始地址。将页面的起始地址与线性地址的最后 12 位相加,得到最终的物理地址。
页面管理就是把物理空间分成很多块。逻辑地址空间被相应地划分为许多页号和页地址,形成一个线性地址。我们通过页码找到页的起始地址,加上页地址得到物理地址。
线性地址分为两部分,VPO与VPN。
VPN作为索引查询页表中的部分物理地址。
在 VPO 中找到的物理地址和页表结合起来形成一个完整的物理地址。
使用 VPN 作为索引查找物理地址时会发生两件事。
页面命中:
1、处理器产生一个虚拟地址并传送给MMU。
2、MMU生成PTE地址并向缓存或主存请求。
3、缓存或主存将PTE返回给MMU。
4、 MMU构造一个物理地址并发送给缓存或主存。
5、缓存或主存将请求的数据返回给处理器。
缺页:
1、处理器产生一个虚拟地址并传送给MMU。
2、MMU 生成 PTE 地址并向缓存或主存请求。
3、缓存或主存将PTE返回给MMU。
4、PTE中的有效位为0,MMU解析一次异常,将CPU的控制权交给操作系统内核中的缺页异常处理程序。
5、页面错误处理程序确定物理内存的牺牲,如果它已被修改,则将其换出到磁盘。
6、页面错误处理程序被调用到一个新的页面中,并更新内存中的PTE。
7、如果处理程序返回到原来的进程,再次执行导致页错误的指令。 CPU 将导致页面错误的指令地址重新发送到 MMU。对于这样的命中,主存储器将请求的字返回给处理器。
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
36位VPN 被划分成四个9 位的片,每个片被用作到一级页表的偏移量。CR3 寄存器包含页目录表的物理地址。VPN1提供在Ll中的偏移量,这PTE包含L2页表的基地址。VPN2提供在Ll中的偏移量,以此类推即可得到物理页号,与虚拟地址VPO相连即可得到物理地址。
在这里插入图片描述
7.5 三级Cache支持下的物理内存访问
(以下格式自行编排,编辑时删除)
每一级Cache都分为S个组,每组都有E行,每一行有B个字节的块,以及一位有效位,64位的tag标记。
对物理地址值而言分为组索引,tag标记,以及块偏移。
首先对地址值的组索引找到相对应的Cache中的对应组。
然后根据tag标记找到符合tag标记的那一行。
如果有效值为1,则命中,在这一行中读或写(写回)取相对应块索引的数据
否则不命中,同理从下一级cache执行相同操作,未命中则再到下一级去找,如果三级cache都找不到则在内存中找到相对应的这一行,根据地址值对应的组加入缓存中,选择有效位为0的行优先放入,否则按照LRU算法替换缓存行。
7.6 hello进程fork时的内存映射
(以下格式自行编排,编辑时删除)
内存映射为每个进程创建一个独立的虚拟地址空间。当一个进程调用 fork 函数时,内核会为新进程创建各种数据结构,并为其分配一个唯一的 PID。通过创建进程得到mm_struct,区域结构与页表的原始副本,他将两个进程中的每个页面标记为只读,并将两个进程中的每个区域结构标记为copy-on-write。
当fork在新进程中返回时,新进程现在的虚拟内存与调用fork时存在的虚拟空间内存是一样的。当两个进程之一想要写入其私有部分时,就会触发只读保护并触发故障处理程序。把实现私有的写时复制,回到原程序的当前指令,执行写操作。对于共享部分,不同进程的不同虚拟页面会映射到内存的同一个物理页面。
7.7 hello进程execve时的内存映射
(以下格式自行编排,编辑时删除)
execve 函数调用驻留在内核区的引导加载程序代码,在当前进程中加载并运行可执行目标文件 hello 中包含的程序,有效地将当前程序替换为 hello 程序。加载和运行 hello 需要以下步骤:
1)删除已有的用户区,删除当前进程虚拟地址的用户部分中已有的区结构。
2)映射私有区域,为新程序的代码、数据、bss和堆栈区域创建新的区域结构。所有这些新区域都是私有的,并在书面上复制。代码和数据区域映射到 hello 文件中的 .text 和 .data 区域。bss 区域用于二进制零并映射到匿名文件。它的大小包含在你好。堆栈和堆地址也用于二进制零,初始长度为零。
3)映射共享区,hello程序链接共享对象libc.so,libc.so动态链接到这个程序,然后映射到用户虚拟地址空间中的共享区。
4)设置程序计数器(PC),execve做的最后一件事是设置当前进程上下文的程序计数器,使其指向代码区的入口点。
7.8 缺页故障与缺页中断处理
(以下格式自行编排,编辑时删除)
1、缺页:使DRAM缓存漏掉一个page fault
2、缺页故障:当cpu引用的虚拟地址所在的虚拟页的PTE有效位为0时,即对应的虚拟页不再在内存中时,会触发缺页异常。
3、缺页中断处理:缺页异常调用内核的缺页异常处理程序。 程序选择内存中的一页作为牺牲页,如果该页被修改(修改位被设置),则将该页写回磁盘; 然后根据目标虚拟页的PTE的磁盘地址,取出磁盘的页放入内存,同时修改PTE。然后在程序中断时返回当前指令, 并继续请求访问虚拟地址。

7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区城,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage colleetor),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML.以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
带边界标签的隐式空闲链表表示块的边界、已分配块和空闲块的方法如图:
在这里插入图片描述
一个块是由头部、脚部、有效载荷,以及可能的填充组成。头部编码表示了这个块的大小(包括头部和所有的填充)和这个块的分配状态(最后一位)。
头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。结尾的脚部是头部的一个副本。块的格式如图所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。
(1)放置已分配的块
当一个进程申请一个n字节的内存空间时,分配器搜索空闲链表。查找一个大小可以放置所申请空间的空闲块。搜索方式的常见策略是首次适配、下一次适配和最佳适配。
(2)分割空闲块
一旦分配器找到一个匹配的空闲块,就必须将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
(3)获取额外堆内存
如果分配器不能找到找到合适的空闲块,可以合并那些相邻的空闲块,如果还不能生成一个足够大的块,分配器就会调用sbrk函数,向内核申请额外的内存。
(4)合并空闲块
合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别对空闲块进行合并,只需要通过改变头部的信息就能完成合并空闲块。
在这里插入图片描述
7.10本章小结
(以下格式自行编排,编辑时删除)
第七章主要讲述了虚拟内存的知识,虚拟内存地址和物理内存地址的转换,TLB和页表的结构和查找原理,以及如何使用物理地址通过三级缓存访问内存,以及 还学习了fork和execve内存映射,以及动态存储分配管理的原理和实现。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个 Linux 文件是一个 m 字节的序列,所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件。并且所有的输入和输出都被认为是对相应文件的读取和写入并执行。这种将设备映射到文件的方法允许 Linux 内核在称为 Unix I/O 上运行一个简单的低级应用程序接口,它允许输入和输出以一致和一致的方式运行。
应用程序通过要求内核打开相应的文件来声明它要访问 I/O 设备。内核返回一个小的非负整数称为描述符,文件的关联数据由内核保存,应用程序只需要保存这个描述符。
Linux shell 创建的每个进程都包含三个文件:
标准输入、标准输出和标准错误以供操作时使用
对于每个打开的文件内核维护文件位置 k,它从 0 开始,它是从文件开头的字节偏移量。应用程序可以通过执行搜索显式更改该值。
对于读操作,从文件复制n个字节到内存,文件位置k增加到k+n,当k大于等于文件大小时,触发EOF条件,即结束文件被读取。
最后,在文件访问结束后。该文件将被内核关闭。内核释放文件打开时创建的数据结构,并将描述符恢复到现有的描述符池中。

8.2 简述Unix IO接口及其函数
Unix I/O 接口:
(1)打开文件:一个应用程序同步异常(陷阱)请求内核打开某文件,表示其要访问一个 I/O 设备,内核返回一个小的非负整数(描述符),然后对此文件的所有操作中标识这个文件,内核把有关这个文件的所有信息进行记录。
(2)Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
(3)更改当前的文件位置:对于已打开的某个文件,内核保存着一个文件位置,初始化为0,是从文件开头起始的字节偏移量是这个文件的位置,应用程序能够通过执行seek函数更改当前文件位置。
(4)读写文件:对于读操作,从文件复制n个字节到内存,文件位置k增加到k+n,当k大于等于文件大小时,触发EOF条件,即结束文件被读取。同理写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置k开始,写完成后更新 k。
(5)关闭文件:内核释放文件打开时创建的数据结构,并将描述符恢复到现有的描述符池中。
Unix I/O 函数:
(1)int open(char* filename,int flags,mode_t mode) ,通过调用open函数打开现有文件或创建新文件的过程。open函数将文件名转换为文件描述符并返回描述符编号。返回的描述符始终是进程标志中未打开的最小描述符参数。指定进程打算如何访问文件。 mode 参数指定新的文件访问权限位。
(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果(一个整型数)。
(3) ssize_t read(int fd,void *buf,size_t n),read函数从当前带有fd描述符的文件位置到buf内存位置最多分配n个字节,返回值-1表示错误,0表示EOF;否则返回值表示实际传输的字节数。
4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到当前描述符为 fd 的文件位置。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
printf函数的函数体如下:
int printf(const char fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char
)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
printf程序按照格式fmt结合参数args生成字符串,并返回串的长度。
然后是write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在printf中调用系统函数write将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。
int INT_VECTOR_SYS_CALLA表示通过调用系统syscall。
然后是sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
sys_call函数通过总线将字符串中的字节从寄存器复制到显卡的显存。显存存储ASCII字符码,字符显示驱动子程序通过ASCII码在字体库中查找点阵信息,将点阵信息存储在vram中。
显示芯片根据刷新频率逐行读取vram。并通过信号线将每个点(RGB分量)发送到液晶显示器。所以我们的输入字符串出现在屏幕上。从vsprintf生成显示数据,写系统函数,int 0x80拦截系统调用,或者sys_call字符显示驱动子程序:从ASCII到字体库显示vram。 (采集每个点的RGB颜色数据)
显示芯片根据刷新频率逐行读取vram。并通过vsprintf的信号线将每个点(RGB分量)发送到液晶显示器,生成显示数据。编写write函数,然后到陷阱-系统调用int 0x80 或 sys_call 等。
字符显示驱动子程序:从ASCII到字体库显示vram(存储每个点的RGB颜色数据),显示芯片相应地逐行读取vram 刷新频率并通过信号线将每个点(RGB分量)发送到液晶显示器。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
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;
}
异步异常-键盘中断的处理:当用户按下一个键时,键盘接口会得到一个代表该键的键盘扫描码,同时产生一个中断请求。 中断请求抢占当前进程运行键盘中断子程序。 键盘中断子程序首先从键盘接口获取按键的扫描码。 然后将按键扫描码转换成ASCII码保存在系统的键盘缓冲区中。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
第八章讲述了IO设备的管理方法,IO接口及其函数,最后分析了printf和getchar函数的实现方法。(第8章1分)

结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
编写:通过编辑器将代码输入hello.c。
预处理:将hello.c调用的所有外部库和宏替换预处理、扩展和合并到一个hello.i文件中。
编译:将hello.i编译成汇编文件hello.s。
汇编:把hello.s汇编成可重定位的目标文件hello.o。
链接:将hello.o与可重定位的目标文件和动态链接库链接成可执行的目标程序hello。
运行:在shell命令行中输入./hello 1190202001 杨永正 1。
创建子进程:shell进程调用fork为其创建子进程。
运行程序:shell调用execve,execve调用loader,添加映射的虚拟内存,程序进入程序入口后开始加载物理内存,然后进入main函数。
执行指令:CPU为其分配时间片,在一个时间片内,hello可以使用CPU,依次执行自己的控制逻辑流。
上下文切换:hello调用sleep函数之后进程进入内核模式,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制返还给hello进程。
访问内存:MMU将程序中使用的虚拟内存地址通过页表映射到物理地址。
动态内存申请:printf调用malloc动态内存分配器在堆中申请内存。
信号:如果在运行时输入ctr-c ctr-z,会分别调用shell的信号处理函数终止和停止。
结束:shell父进程或ini养父进程回收子进程,内核删除为这个进程创建的所有数据结构。
计算机系统的设计思想和实现是基于抽象实现的:抽象体现在:用二进制01表示的最低层信息,操作系统管理硬件,进程是处理器、主存和I/O设备的抽象。虚拟内存是主内存与磁盘设备联系的抽象等。
计算机系统的设计是统筹兼顾的:计算机系统的设计考虑了所有可能的实际情况,并相应地设计了一系列的处理方法来适应不同的情况。
计算机系统设计精巧:为了解决快设备小存储与大存储设备慢存储的不平衡,设计了Cache和TLB等缓存设备作为下层存储设备的缓存,很大程度上提高了CPU运行的速度。

(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c:c源代码文件
hello.i:预处理后的文件
hello.s:编译后的汇编文件
hello.o:汇编后的汇编可重定位文件
hello:链接后生成的可执行文件
hello.elf: hello.o的ELF文件
hellold.elf: hello.out的ELF文件
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 兰德尔E.布兰恩特 大卫R.奥哈拉伦 深入理解计算机系统(第三版)
[2] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值