HIT-2019ICS大作业 程序人生-Hello’s P2P

HIT-2019ICS大作业 程序人生-Hello’s P2P

计算机系统

大作业

word可查看图片

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号 1180400420
班   级 1803003
学 生 刘文洋   
指 导 教 师 史先俊

计算机科学与技术学院
2019年12月
摘 要
本文通过对hello程序的P2P和020过程的整体介绍,详尽阐述了hello从我们编写好的C文件经过一步步的处理成为Process的过程,并且接着介绍了hello进程从创建到回收的020过程。通过对hello程序的分析,进一步加深对课本知识的理解。

关键词:P2P:020:进程;程序的生命周期

(摘要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简介
Hello是每个学习计算机学科学生第一个接触到的程序,初识hello时,我们都会认为它是那么的简单且基础,只需要编写几行简单的函数,在IDE中按下调式便可以运行。
P2P(From Program to Process):但实际上,hello从我们编写的C文件到最终在屏幕上打印出hello world的过程并不简单。linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生子进程的过程。
020(From Zero-0 to Zero-0):shell通过execve加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.2 环境与工具
硬件环境:X64 CPU;
软件环境:Windows 10 64位 ,Vmware 14 ,Ubuntu 18.04 LTS 64 位
开发工具:gcc,readelf,gedit,edb,gdb,ld,visual studio 2019

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.3 中间结果
(1) hello.c:hello源程序
(2) hello.i:预处理后的文本文件
(3) hello.s:汇编后的文本文件
(4) hello.o:可重定位目标文件
(5) hello:可执行文件
(6) hello.elf:hello的elf格式文件
(7) helloo.elf:hello.o的elf格式文件
(8) hello.objdump:hello的反汇编代码
(9) helloo.objdump:hello.o的反汇编代码

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章主要介绍了hello程序的P2P,020过程。并介绍了本次实验使用的软硬件环境和中间的结果。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
一.概念
预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个“.i”文件中等待进一步处理。
例如hello.c文件中就有三个这样的头文件,如图2-1所示:

图2-1 hello.c中的头文件

二.作用
预编译过程主要处理那些源代码中以#开始的预编译指令,主要处理规则如下:
1)将所有的#define删除,并且展开所有的宏定义;
2)处理所有条件编译指令,如#if,#ifdef等;
3)处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。
4)删除所有的注释//和 /**/;
5)添加行号和文件标识,如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息;
6)保留所有的#pragma编译器指令,因为编译器须要使用它们;
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
应截图,展示预处理过程!

图2-2 hello.c预处理命令行

图2-3 预处理生成的hello.i文件
2.3 Hello的预处理结果解析
首先观察到hello.c经过预处理得到hello.i后,文件行数从23行增加到了3113行。文件大小从527bytes增加到了66.1kb,如图2-4,图2-5所示:

图2-4 预处理前后文件大小

图2-5 预处理前后行数大小
接着使用gedit打开hello.c和hello.i文件,首先可以看到开头的注释等无用信息被删除。如图2-5所示:

图2-5 开头的注释信息被删除

虽然hello.i行数很多,但不难看出是stdio.h unistd.h stdlib.h的依次展开,以stdio.h为例,预处理器到默认的环境变量中寻找sidio.h,发现其中仍然使用了大量的#开头的语句,预处理器对其递归展开,直到最终的hello.i中不再有#开头的命令,如图2-6所示。在文件的最下方是main函数,如图2-7所示。

图2-6 用来描述使用的运行库在计算机中的位置

图2-7 main函数

2.4 本章小结
本章主要介绍了预处理阶段的相关过程。简要介绍了预处理过程的概念和作用,并对预处理的过程进行演示,并对预处理阶段的结果进行了解析。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译阶段是在预处理之后的下一个阶段,在预处理阶段过后,我们获得了一个hello.i文件,编译阶段就是编译器(ccl)对hello.i文件进行处理的过程。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。
3.1.2 编译的作用
编译器会在编译阶段对代码的语法进行检查,如果出现了语法上的错误,会在这一阶段直接反馈回来,造成编译的失败。如果在语法语义等分析过后,不存在问题,编译器会生成一个过渡的代码,也就是汇编代码,在随后的步骤中,汇编器可以继续对生成的汇编代码进行操作。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
命令行:gcc -S hello.i -o hello.s
如图3-1所示,输入上述命令行后,得到了一个hello.s文件。

图3-1 编译生辰hello.s文件
应截图,展示编译过程!
3.3 Hello的编译结果解析
首先先介绍hello.s文件开头部分的汇编指令的意义,如图3-2所示。

图3-2 hello.s各种汇编指令
.file:源文件名
.text:代码段
.data:数据段
.section .rodata:下面是.rodata节
.align:对齐方式
.string:字符串
.globl:全局变量
.type:指定是对象类型或是函数类型
.size:大小
.long:长整型

接着根据PPT给出的参考C数据与操作,分别进行如下解析:
3.3.1 数据
Hello.s中用到的C数据类型有:整数,字符串,数组。
分别进行分析。
(一) 字符串
程序中出现的字符串分别是:
(1)“用法: Hello 学号 姓名 秒数!\n”,第一个printf传入的输出格式化参数,在hello.s中的声明如图3-3所示。注意到这里字符串被编码为UTF-8格式,一个汉字占三个字节。一个/代表一个字节。

图3-3 hello.s声明的字符串1
(2)“Hello %s %s\n”,第二个printf传入的输出格式化参数,在hello.s中的声明如图3-4所示。

图3-4 hello.s声明的字符串2
(二) 整数
程序中出现的整型数据主要有三种。
(1) 局部变量int i。
通常编译器将局部变量存储在寄存器或者栈空间中,在hello.s中观察可知,int i被存储在-4(%rbp)中,占据4字节。
(2) int argc
argc是函数传入的第一个参数,存储在%edi中。
(3) 立即数
C程序中有许多直接给出的如4,8的立即数,在hello.s中直接以立即数的形式给出。
(三) 数组
hello.c中的数组只有一个char* argv[],函数执行的命令行参数。Argv是一个字符串数组,作为第二个参数传入,大小为8字节。栈中为char* argv[]分配了一段连续的空间来存放相关的内容,并且数组一般是从栈底指针开始分配。函数传参时按逆序的方式将参数压入栈中,这样取参数就是正序。在hello.s中,两次使用(%rax)(两次rax分别为argv)如图3-5所示。

图3-5 计算地址取出argv[]的值

3.3.2 赋值
程序中涉及的赋值操作为:
(1) 对局部变量i赋值,如图3-6所示

图3-6 对i赋值
(2) 对返回值%eax赋值,如图3-7所示

图3-7 对%eax赋值
添加链接描述
另外需要说明,根据数据的大小mov指令使用不同的后缀,如图3-8所示:

图3-8 mov指令
3.3.3 类型转换
程序中出现的隐式类型转换是调用atoi函数后返回值应该是整型。所以当输入的数据为浮点数时,会出现强制类型转换。当double或float向int进行类型转换时,程序改变数值的原则是:值会向零舍入。比如输入的秒数为 2.3,将会被舍入为2。
3.3.4 算数操作
进行数据算数操作的汇编指令,如图3-9所示:

图3-9 常用的算数操作指令

程序中涉及的数据运算为:

(1) 加法操作
有对计数器i进行的+1操作,还有对%eax进行加法以便后面(%eax)的操作。如图3-10所示。

图3-10 加法操作
(2) 减法操作
初始时对栈指针进行减法操作。如图3-11所示。

	图3-11 减法操作
(3)加载有效地址

使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。结果是祛除了LC1处的字符串。如图3-12所示。

图3-12 加载有效地址

3.3.5 关系操作
常用的进行关系操作的汇编指令,如图3-13所示:

图3-13 常用关系操作指令
程序中涉及的关系操作有:
(1) if(argc!=4)
判断输入的参数的个数是否正确,不正确则打印提示字符串并退出。正确则继续运行。在hello.s的语句如图3-14所示。Hello.s如图所示的cmpl语句,计算argc-4,然后设置条件码,为下一步je做准备。

	 图3-14  if语句

(2)for(i=0;i<8;i++)
判断i是否小于8。Hello.s使用如图3-15所示的语句,计算i-7,然后设置条件码,为下一步jle做准备。

图3-15 i<8语句

3.3.6 数组/指针/结构操作
hello.c中的数组只有一个char* argv[],函数执行的命令行参数。Argv是一个字符串数组,作为第二个参数传入,大小为8字节。栈中为char* argv[]分配了一段连续的空间来存放相关的内容,并且数组一般是从栈底指针开始分配。函数传参时按逆序的方式将参数压入栈中,这样取参数就是正序。在hello.s中,两次使用(%rax)(两次rax分别为argv)如图3-16所示。

图3-16 计算地址取出argv[]的值

3.3.7 控制转移
常用的控制转移的指令,如图3-17所示。常常与3.3.5节提到的关系操作的指令配合使用。先用关系操作设置条件码,再根据条件码进行跳转操作。

图3-17 常用的控制转移指令

程序中出现的控制转移有:

(1) if(argc!=4)
当argv不等于4时,打印提示行信息,并退出。对于if判断,编译器使用跳转指令实现,首先cmpl比较argv和4,设置条件码,使用je判断ZF标志位,如果为0,说明argv-4=0,则不执行if中的代码直接跳转到.L2,否则顺序执行下一条语句,即执行if中的代码。如图3-18所示。

	图3-18 if语句的编译

(2)for(i=0;i<8;i++)

当argv不等于4时,使用计数器i循环8次。编译器首先无条件跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果i<=7,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。如图3-19所示。

	图3-19 for循环语句的编译

3.3.8 函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。
函数调用涉及到参数传递,控制转移,函数返回(交回控制权)几部分。汇编通过call,ret指令实现(稍微复杂点的函数需要设置栈底指针rbp和leave操作)。
P调用Q涉及以下操作:
1)参数压栈:过程Q需要用到的参数存放在P的栈帧中。64位程序中有6个参数寄存器,寄存器不够的参数存放在栈中。
2)控制转移:利用call指令调用过程Q,call指令有两个动作,一是将P的下一条指令的地址压栈(确保能返回),跳转到Q的代码地址处。Q执行完之后用ret指令返回给P。
3)分配和释放内存:利用栈帧结构来实现内存的动态分配释放。

64位下函数传递参数存储顺序为(第六个之后的通过栈传递):
(1)%rdi
(2)%rsi
(3)%rdx
(4)%rcx
(5)%r8
(6)%r9
程序中涉及函数操作的有:
(一)main函数:
(1)传递控制,main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数。
(2)传递数据,外部调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。
(3)分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。
(二)printf函数:
(1)传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
(2)控制传递:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。
(三)exit函数:
(1)传递数据:将%edi设置为1。
(2)控制传递:call exit@PLT。
(四)sleep函数:
(1)传递数据:将%edi设置为sleepsecs。
(2)控制传递:call sleep@PLT。
(五)getchar函数:
(1)控制传递:call gethcar@PLT

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章主要介绍了编译器是如何处理编译C语言的各个数据类型和各类操作的。通过结合具体hello.s的代码进行了详尽的阐述。编译器将hello.i编译生成hello.s。经过编译后,得到了相应的汇编语言代码。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
(1)汇编的概念
汇编器(as)将hello.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在hello.o中。这里的hello.o是一个二进制文件。
(2)汇编的作用
我们知道,汇编代码也只是我们人可以看懂的代码,而机器并不能读懂,真正机器可以读懂并执行的是机器代码,也就是二进制代码。汇编的作用就是将我们之前再hello.s中保存的汇编代码翻译成可以供机器执行的二进制代码,这样机器就可以根据这些01代码,真正的开始执行我们写的程序了。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
使用上述指令进行汇编,得到一个hello.o文件。如图4-1所示:

图4-1 汇编得到hello.o
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

首先先来了解一下ELF格式中都存储了哪些文件,如图4-2所示,ELF中存储了很多不同的节的信息,每一个节中保存了程序中对应的一些变量或者重定位等这些信息。

图4-2 elf可重定位目标文件格式

使用readelf -a hello.o > helloo.elf 指令获得hello.o文件的ELF格式。其组成如下:
(1) ELF Header
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。如图4-3所示:

图4-3 ELF Header
(2)Section Header节头部表
节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。如图4-4所示。

图4-4 节头部表

(3).rel.text节 重定位节
重定位节.rela.text ,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。如图4-5所示,图中8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、atoi函数、sleep函数、getchar函数进行重定位声明。

图4-5 .rel.txet节

该重定位包含的信息和含义如图4-6所示:

图4-6 重定位条目格式及其意义

以.L1为例,链接器根据Info信息向.symtab节中查询链接目标的符号,由info.symbol=0x05,可以发现重定位的内容在.rodata节中的.L0,设重定位条目为r,则由重定位条目可知:
r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32,r.addend=4
重定位使用PC相对寻址方式,计算方法如图4-7所示:

图4-7 重定位地址的计算
其他重定位计算方法类似。

(4).rela.eh_frame : eh_frame节的重定位信息。如图4-8所示。

图4-8 .rela.eh_frame节
(5).symtab:符号表,用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。如图4-9所示:

图4-9 符号表

4.4 Hello.o的结果解析
使用 objdump -d -r hello.o > helloo.objdump获得反汇编代码。
与hello.s中的汇编代码比较,总体的差别不大,大致具有一一对应的关系。但有些不同之处,需要特别注意,如图4-10所示。
(1) 操作数
hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
(2) 分支转移
反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(3)函数调用
在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
(4)全局变量访问
在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。

图4-10 反汇编语言和hello.s中汇编语言对比

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章介绍了从hello.s到hello.o的汇编过程,通过readelf查看hello.o的elf格式,详细介绍了可重定位目标文件elf格式。通过objdump得到反汇编代码并与hello.s进行比较,详细阐述了从汇编语言映射到机器语言汇编器需要实现的转换。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
(1) 概念
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
(2) 作用
当程序调用函数库(如标准C库)中的一个函数printf,printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个函数必须通过链接器(ld)将这个文件合并到hello.o程序中,结果得到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
使用上述命令行,如图5-1所示:

图5-1 链接命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
使用readelf -a hello > hello.elf 命令生成hello程序的ELF格式文件。

可执行目标文件hello的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
如图5-2所示,Section Headers对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中Address是程序被载入到虚拟地址的起始地址。

图5-2 ELF格式中的Section Headers Table

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,自0x400fff结束,这之间每个节(开始 ~ .eh_frame节)的排列即开始结束同5.3节中Address中声明一致,如图5-3所示。

图5-3 与5.3节对比
如图5-4,查看ELF格式文件中的Program Headers,程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。在下面可以看出,程序包含8个段:

(1)PHDR保存程序头表。
(2)INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。
(3)LOAD表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。
(4)DYNAMIC保存了由动态链接器使用的信息。
(5)NOTE保存辅助信息。
(6)GNU_STACK:权限标志,标志栈是否是可执行的。
(8)GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。

图5-4 ELF格式文件中的Program Headers
通过Data Dump查看虚拟地址段0x6000000x602000,在0fff空间中,与0x4000000x401000段的存放的程序相同,在fff之后存放的是.dynamic.shstrtab节。如图5-5所示。

图5-5 Data Dump中的信息

5.5 链接的重定位过程分析
使用objdump -d -r hello > hello.objdump 获得hello的反汇编代码。
我们对比之前获得hello.o的反汇编代码,得到hello与hello.o有以下不同(对比图为5-6和图5-7所示):

图 5-6 对比图1

图5-7 对比图2

(1)节数
hello相对hello.o多了很多的节类似于.init,.plt等
(2)函数个数
hello中相对hello.o增加了许多的外部链接来的函数。如exit,printf,sleep等函数。
(3)虚拟内存地址
如图中所示,hello.o中的相对偏移地址到了hello中变成了虚拟内存地址。hello.o文件中对于.rodata内字符串全局变量的访问,是0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在hello中可以看到,他们经过重定位后都有了具体的数值。
(4)函数调用
链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
(5)全局变量访问
链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。这里以计算第一条字符串相对地址为例说明计算相对地址的算法(算法说明同4.3节)重定位条目如图5-6所示。

图5-6 hello.o重定位条目
refptr = s + r.offset = Pointer to 0x40059A
refaddr = ADDR(s) + r.offset= ADDR(main)+r.offset=0x400582 + 0x18 =0x40059A
*refptr=(unsigned)(ADDR(r.symbol)+(r.addend-refaddr) = ADDR(str1)+r.addend-refaddr=0x400698+(-0x4)-0x40059A=(unsigned) 0xFA

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
程序名称
_dl_start
_dl_init
_start
__libc_start_main
__cxa_atexit
__libc_csu_init
_init
_setjmp
_sigsetjmp
__sigjmp_save
main
puts@plt
exit@plt
printf@plt
atoi@plt
sleep@plt
getchar@plt
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
exit

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
hello程序的动态链接项目:global_offset表 。在elf文件中查询到global_offset表地址为0x601000。如图5-7所示。

图5-7 GOT表

接着在edb中进行调试,在Data Dump中查看GOT表的变化,得到如下变化。如图5-8所示。

图5-8 _GLOBAL_OFFSET_TABLE_的变化对比图
在edb调试之后我们发现原先0x006008c0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量 。

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章介绍了hello的链接过程。链接过程可以发生在编译时,也可以发生在加载时,甚至可以发生在程序执行时。静态链接直接将目标文件和库文件打包至一个可执行文件中,而动态链接则只在可执行目标文件中添加相应重定向记录,并通过GOT表项和延迟绑定的方法实现对目标模块中符号的引用。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
(1)概念
狭义上:进程是一个执行中程序的示例。广义上:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
(2)作用
1.每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2.进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
(1)shell的作用
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
(2)shell的处理流程
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程
Shell通过调用fork 函数创建一个新的运行的子进程。也就是Hello程序,Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Sehll和Hello进程之间最大的区别在于它们有不同的PID。
下面给出fork创建子进程的进程图,如图6-1所示。

图6-1 fork创建一个子进程
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序,否则将永不返回。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型(如图6-2所示):

图6-2 execve函数
当main开始执行时,用户栈的组织结构如图6-3所示。让我们从栈底(高地址)往栈顶(低地址)依次看一看。首先是参数和环境字符串。栈往上紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量environ指向这些指针中的第一个envp[0]。紧随环境变量数组之后的是以null 结尾的argv[ ]数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数libe_ start_ _main的栈帧。

图6-3 一个新程序开始后,典型栈结构
6.5 Hello的进程执行
先阐述下面几个必要的概念:
(1)进程上下文信息
就是内核重新启动一个被抢占的程序所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。
(2)进程时间片
是指一个进程和执行它的控制流的一部分的每一时间段。
(3)用户模式和内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
了解了上面的基本概念后,我们来分析hello程序中的具体执行情况。图6-4给出了hello代码中会主动引起中断的代码。

图6-4 hello文件的部分代码
这段代码中调用了sleep函数,这个sleep函数的作用就是当运行到这一句的时候,程序会产生一个中断,内核会将这个进程挂起,然后运行其它程序,当内核中的计时器到了输入的秒数的时候,会传一个时间中断给CPU,这时候CPU会将之前挂起的进程放到运行队列中继续执行。
如图6-5所示。

图6-5 hello进程的上下文切换

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
(1)程序正常执行结果:连续输出8行,再等待屏幕输入,按下回车,结束程序。如图6-6所示。

图6-6 正常执行结果
(2)程序执行中按下ctrl+c:通过ps指令看到后台没有hello程序,说明ctrl+c指令终止了hello程序,并回收了hello进程。如图6-7所示。

图6-7 运行时按Ctrl+C
(3)程序执行中按下Ctrl+Z:通过ps和jobs指令都可以看到后台被挂起的hello程序,可以通过fg命令将其调成前台继续执行,说明Ctrl+Z是停止(挂起)前台作业。

图6-9 程序执行中按下Ctrl+Z
(4)程序运行中乱按:可以看出在程序执行过程中乱按实际上是将屏幕输入缓存到stdin,当getchar读到\n字符时,其他字符当做命令输入。如图6-10所示。

图6-10 程序运行中乱按
(5)程序执行过程中按Ctrl+Z后运行pstree。如图6-11所示。

图6-11程序执行过程中按Ctrl+Z后运行pstree
(6)kill命令杀死hello,如图6-12所示。

图6-12 kell命令杀死hello

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
异常控制流发生在计算机系统的各个层次。比如, 在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序。在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程。在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应。
本章主要介绍了hello进程的执行过程,主要是hello的创建、加载和终止,通过键盘输入,对hello执行过程中产生信号和信号的处理过程有了更多的认识,从而对异常控制流的相关知识有了更深的理解。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。
线性地址:地址空间(address space) 是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space) 。就是hello里面的虚拟内存地址。
虚拟地址:CPU 通过生成一个虚拟地址(Virtual Address, VA) 。就是hello里面的虚拟内存地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件。 索引号是“段描述符(segment descriptor)”的索引,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成。
将逻辑地址分成段选择符+段描述符的判别符(TI)+地址偏移量的形式,然后先判断TI字段,看看这个段描述符究竟是局部段描述符(ldt)还是全局段描述符(gdt),然后再将其组合成段描述符+地址偏移量的形式,这样就转换成线性地址了。如图7-1所示。

图7-1 逻辑地址向线性地址转换。

7.3 Hello的线性地址到物理地址的变换-页式管理
CPU芯片上有一个专门的硬件叫做内存管理单元(MMU),这个硬件的功能就是动态的将虚拟地址翻译成物理地址的。N为的虚拟地址包含两个部分,一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE(页表条目)。接下来在对应的PTE中获得PPN(物理页号),将PPN与VPO串联起来,就得到了相应的物理地址。如图7-2所示。

图7-2 使用页表的地址翻译
其中,页面翻译命中的情况步骤为,如图7-3所示:

  1. 处理器生成一个虚拟地址,并将其传送给MMU
    2-3) MMU 使用内存中的页表生成PTE地址
  2. MMU 将物理地址传送给高速缓存/主存
  3. 高速缓存/主存返回所请求的数据字给处理器

图7-3 页面命中
页面发生缺页异常情况的步骤为,如图7-4所示:

  1. 处理器将虚拟地址发送给 MMU
    2-3) MMU 使用内存中的页表生成PTE地址
  2. 有效位为零, 因此 MMU 触发缺页异常
  3. 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
  4. 缺页处理程序调入新的页面,并更新内存中的PTE
  5. 缺页处理程序返回到原来进程,再次执行缺页的指令

图7-4 缺页异常
7.4 TLB与四级页表支持下的VA到PA的变换
首先来介绍一下TLB具体是什么东西。我们注意到,每次在进行虚拟地址翻译的过程中都会有访问PTE的操作,如果在比较极端的情况下,就会存在访存的操作,这样的效率是很低的。TLB的运用,就可以将PTE上的数据缓存在L1中,也就是TLB这样一个专用的部件,他会将不同组中的PTE缓存在不同的位置,提高地址翻译的效率。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。如图9-15所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2’个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。如图7-5所示。

图7-5 访问TLB
其次我们来介绍一下多级页表的概念。在前面我们了解了一级页表是如何进行工作的。可以发现一级页表有一个弊端,就是对于每一个程序,内核都会给他分配一个固定大小的页表,这样有一些比较小的程序会用不到开出的页表的一些部分,就造成了空间的浪费,多级页表就很好的解决了这个问题。以二级页表为例,首先我们先开一个比较小的一级页表,我们将完整的页表分组,分别对应到开出来的一节页表的一个PTE中,在执行程序的过程中,如果我们用到了一个特定的页表,那么我们就在一级页表后面动态的开出来,如果没用到就不开,这样就大大的节省了空间。
图7-6给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN(物理页号)。

图7-6 Core i7页表翻译
7.5 三级Cache支持下的物理内存访问
通过7.4 Core i7 MMU使用四级页表来将虚拟地址翻译成物理地址,我们得到了物理地址PA。现在分析三级cache支持下的物理内存访问。如图7-6,以L1 d-cache的介绍为例,L2和L3同理。
L1 Cache是8路64组相联。块大小为64B。因此CO和CI都是6位,CT是40位。根据物理地址(PA),首先使用CI组索引,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中取出被请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
如图7-7所示。

图7-7 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
(3)映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
图7-
图7-8 加载器是如何映射用户地址空间的区域的

7.8 缺页故障与缺页中断处理
缺页现象的发生是由于页表只相当于磁盘的一个缓存,所以不可能保存磁盘中全部的信息,对于有些信息的查询就会出现查询失败的情况,也就是缺页。
对于一个访问虚拟内存的指令来说,如果发生了缺页现象,CPU就会触发一个缺页异常。缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,例如图7-9中存放在PP3中的VP4,如果VP4已经被更改,那就先将他存回到磁盘中。

图7-9 VM缺页(之前)
找到了要存储的页后,内核会从磁盘中将需要访问的内存,例如图7-10所示的VP3放入到之前已经操作过的PP3中,并且将PTE中的信息更新,这样就成功的将一个物理地址缓存在了页表中。当异常处理返回的时候,CPU会重新执行访问虚拟内存的操作,这个时候就可以正常的访问,不会发生缺页现象了。

图7-10 页面调度之后

7.9动态存储分配管理
7.9.1 动态内存分配器的基本原理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
2. 隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collec-tor), 而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。例如,诸如Lisp、 ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.9.2 隐式空闲链表分配器的基本原理
(1)基本结构
对于带边界标签的隐式空闲链表分配器,如图7-11所示,一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。

图7-11 隐式空闲链表结构

(2)简单的放置策略
首次适配:从头搜索,遇到第一个合适的块就停止;
下次适配:从头搜索,遇到下一个合适的块停止;
最佳适配:全部搜索,选择合适的块停止。
(3)合并空闲块
在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。如图7-12所示。

图7-12 使用边界标记的块
设置了头部和脚部后,我们可以方便的对前面的空闲块进行合并。合并的 情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四 种情况分别进行空闲块合并,我们只需要通过改变 Header 和 Footer 中的值就可以 完成这一操作。

7.9.3 显式空闲链表基本原理
一种更好的方法是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如图7-13所示。

图7-13 使用双向空闲链表的堆块格式
关于空闲块的维护方式一共有两种,一种是后进先出的方式,另一种是按照地址的方式。按照地址维护很好理解,与隐式的结构大致相同。后进先出的方式的思想是,当一个分配块被free之后,将这个块放到链表的最开头,这样在malloc的时候会首先看一下最后被free的块是否符合要求。这样的好处是释放一个块的时候比较高效,直接放在头部就可以。

Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
虚拟内存是对主存的一个重要抽象。本章主要介绍了TLB和四级页表支持下VA到PA的转换,以及得到PA后,三级cache下的物理内存的访问过程。并介绍了fork函数和execve函数和虚拟内存的关系,最后详细介绍了动态内存分配的基本原理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。

8.2 简述Unix IO接口及其函数
8.2.1 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)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

8.2.1 UNIX I/O函数
(1)open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1
(2)close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include <unistd.h>
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错
(3)read()函数
功能描述: 从文件读取数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
(4)write()函数
功能描述: 向文件写入数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)
(5)lseek()函数
功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include <unistd.h>,#include <sys/types.h>
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析
查看printf代码,如图8-1所示:

图8-1 printf代码
在形参列表里有这么一个token:… ,这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。
先看va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义:typedef char va_list
这说明它是一个字符指针。 其中的: (char)(&fmt) + 4) 表示的是…中的第一个参数。下一句printf调用了一个函数vsprintf,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。Vsprintf代码如图8-2所示:

图8-2 vsprintf代码
再下一句write(buf , i),还是先看一下write函数的内容:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
其中int表示要调用中断门了。通过中断门,来实现特定的系统服务。 再跟踪syscall的实现
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

Syscall将字符串中的字节从寄存器复制到显卡的显存中,以ASCII字符形式。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成ASCII码,保存到系统的键盘缓冲区之中。

int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return (–n>=0)?(unsigned char)*bb++:EOF;
}
getchar调用了一个read函数,这个read数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法,Unix IO接口及其相关函数,分析了printf函数和getchar函数。
了解系统级IO是重要的,有时你除了使用Unix I/O 以外别无选择。在某些重要的情况中,使用高级I/O 函数不太可能,或者不太合适。例如,标准I/O 库没有提供读取文件元数据的方式,例如文件大小或文件创建时间。同时这对于我们理解网络编程和并行流是很有帮助的。

(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

hello程序终于走完了他一生的过程,然我们来回顾一下他从一个.c文件是怎样一步一步的变成可以输出我们想看到的结果的程序:
(1) hello被IO设备编写,以文件的方式储存在主存中。
(2) hello.c被预处理hello.i文件
(3) hello.i被编译为hello.s汇编文件
(4) hello.s被汇编成可重定位目标文件hello.o
(5) 链接器将hello.o和外部文件链接成可执行文件hello
(6) 在shell输入命令
(7) shell调用fork函数,创建了新的子进程
(8) execve函数调用加载器将hello程序加载到了该子进程。这里还有虚拟内存机制的帮助,我们能够轻松地完成内存映射。
(9) 在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流
(10) hello的VA通过TLB和页表翻译为PA
(11) 三级cache 支持下的hello物理地址访问
(12) hello在运行过程中会有异常和信号等
(13) printf会调用malloc通过动态内存分配器申请堆中的内存
(14) shell父进程回收hello子进程,内核删除为hello创建的所有数据结构

感悟:通过对计算机系统这门课的学习,让我认识到,我们第一个接触到的hello程序,看上去很简单,但如果做深入的研究,便会发现计算机系统为了实现这个程序做出的种种努力。虽然这次大作业的完成之路上遇到了很多的困难,但通过一步步的学习,感受到自己一步步的进步,也是一种十分欣喜的过程。进一步有进一步的惊喜。

创新理念:在完成这份大作业过程中,体会到不同的工具在进行不同任务时的优势,在进行调用函数的列举中,用gdb明显比edb方便很多。

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

附件
列出所有的中间产物的文件名,并予以说明起作用。

(10) hello.c:hello源程序
(11) hello.i:预处理后的文本文件
(12) hello.s:汇编后的文本文件
(13) hello.o:可重定位目标文件
(14) hello:可执行文件
(15) hello.elf:hello的elf格式文件
(16) helloo.elf:hello.o的elf格式文件
(17) hello.objdump:hello的反汇编代码
(18) helloo.objdump:hello.o的反汇编代码

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] 博客园 printf函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[3] 浅谈Window内存管理方式——页式管理、段式管理、段页式管理
https://blog.csdn.net/zhouziyu2011/article/details/69815638
[4] c语言编译过程详解,预处理,编译,汇编,链接(干货满满)
https://blog.csdn.net/weixin_41143631/article/details/81221777.
[5] getchar(计算机语言函数)
https://baike.baidu.com/item/getchar/919709?fr=aladdin.
[6] Linux文件IO函数与标准IO库函数
https://blog.csdn.net/Gouhailiang/article/details/73123974.
(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值