CS大作业

CS大作业@TOC
目 录

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

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P: Hello.c经过cpp的预处理,ccl的编译、as的汇编、ld的链接最终成为可执行目标程序Hello,在shell中键入启动命令后,shell通过fork产生子进程, hello便从program变成了process。
020: shell通过execve映射虚拟内存,进入程序入口后载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流,当结束后,shell父进程负责回收hello进程。
1.2 环境与工具
硬件环境: Intel® Core™ i5-8250U CPU @ 1.60GHz 1.80GHz
物理内存:8.00GB
512GB HDD
软件环境: Windows7 64位以上;
Ubuntu 18.04 64位;
Vmware 14.11;
开发工具: Visual Studio 2017 64位;
Code Blocks;
gedit,gcc,notepad++;
1.3 中间结果
Hello.c 源程序
Hello.i 预处理之后的文本文件
Hello.s 编译之后的汇编文件
Hello.o 汇编之后的可重定位文件
Hello 连接之后的可执行目标文件
Asm.txt hello.o的反汇编代码
Asm1.txt hello的反汇编代码

1.4 本章小结
本章主要介绍P2P和020的过程,以及描述了实验的软硬件环境和实验中间结果。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器对程序源代码文本进行处理,把源代码分割或处理成为特定的单位,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析。
作用:
1: 宏定义。
宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。
预处理(预编译)工作也叫做宏展开:将宏名替换为文本(这个文本可以是字符串、可以是代码等)。
掌握"宏"概念的关键是“换”。一切以换为前提、做任何事情之前先要换,准确理解之前就要“换”。
例:#define PI 3.1415926,把程序中全部的标识符PI换成3.1415926。

2:文件包含。
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
编译时以包含处理以后的文件为编译单位,被包含的文件是源文件的一部分。
编译以后只得到一个目标文件.obj
被包含的文件又被称为“标题文件”或“头部文件”、“头文件”,并且常用.h作扩展名。
修改头文件后所有包含该文件的文件都要重新编译。

3:条件编译。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
有些语句希望在条件满足时才编译。
使用条件编译可以使目标程序变小,运行时间变短。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i

2.3 Hello的预处理结果解析

使用vim打开hello.i之后发现,整个hello.i程序已经拓展为3118行,main函数出现在hello.c中的代码自3102行开始。
在这之前出现的是stdio.h unistd.h stdlib.h的依次展开,以stdio.h的展开为例,cpp到默认的环境变量下寻找stdio.h,打开/usr/include/stdio.h 发现其中依然使用了#define语句,cpp对此递归展开,所以最终.i程序中是没有#define的。而且发现其中使用了大量的#ifdef #ifndef的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。其他类似。
2.4 本章小结
本章介绍了预处理的定义与作用。并且实践了hello.i的生成,亲身体会了#include语句是如何将系统头文件插入程序文本中的。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这个过程称为编译。
编译的作用:   
1.扫描(词法分析)
2.语法分析
3.语义分析
4.源代码优化(中间语言生成)
5.代码生成,目标代码优化。
3.2 在Ubuntu下编译的命令
命令:gcc –s hello.c –o hello.s

3.3 Hello的编译结果解析
3.3.1 对于数据的处理
全局变量:hello.c中的全局变量有一个 sleepsecs它是一个int类型的数但是却赋给了它一个浮点的值。此时编译器会自动进行隐式类型转化将sleepsecs的初值改为2并把它存入data节中。如图所示:

通过图中的信息我们还可以看到这个程序是4字节对齐的并且sleepsecs的类型是object(对象)
局部变量:hello.c中有argc 和 i 两个局部变量。这两个参数在该程序中分别被保存在运行时堆栈中。
其中argc被保存在 -20(%rbp)中,i被保存在 -4(%rbp)中。

立即数:直接写入到汇编程序中去并没有保存到堆栈或者寄存器。
字符串:hello.c中出先得字符串数据一共有两个如图所示:

分别是两个printf语句中得格式串。
编译后他们被存储在rodate节中只读不能更改!

3.3.2对于赋值的处理
Hello.c程序中共有两个赋值语句。对于sleepsecs的赋值和对于i的赋值。
其中对于sleepsecs的赋值由于sleepsecs是全局变量将这个信息存储在了.data节中这一点我们在3.3.1节中已经讲过了,那么对于i的赋值汇编语言运用了mov语句如图:

3.3.3 对于类型转换的处理
Hello.c程序中的类型转换只有对于sleepsecs的类型转换,它将一个浮点数赋值给了一个整型数,然后在编译过程中对该变量做了隐式类型转换。
3.3.4对于算术操作的处理
Hello.c程序中的算术操作看上去只有i++一个,对于这个操作在汇编代码中

通过每次将1加到存储变量i位置的堆栈中去进行更新。
但是我们通过查看汇编代码发现
这句话也是一个算术操作,他将存储在rodate节中的printf语句的格式串传递给了rdi作为调用printf函数的参数。同理这句话也是。

3.3.5 对于关系运算的处理
Hello.c中一共有两个关系运算。
argc!=3:判断argc不等于3。hello.s中使用

计算argc-3然后设置条件码,为下一步je利用条件码进行跳转作准备。
i<10:判断i小于10。hello.s中使用

计算i-9然后设置条件码,为下一步jle利用条件码进行跳转做准备。
3.3.6 对于数组操作的处理
数组: hello.c程序中有一个char*数组argv数组得值也被存入到内存得相应得值中去,汇编程序通过地址查询相应数组得值。

具体来说:将argv数组存储的起始位置压入栈中,因为每个char类型的长度为8byte所以该地址加八即使下一个char的位置(注意到rsi是第二个参数,rdx是第三个参数可以推得)
3.3.7 对于控制转移的处理

条件控制以上图为跳转依据。

Hello.c中的控制转移一共有两个,一个是if条件判断句的控制转移,另外一个是控制循环是否终止的控制转移。
对应if的控制转移中

如果不等于3就继续执行否则跳转到.L2处。
对于循环中的条件控制

首先有一个判断循环是否终止的条件如果循环终止的话就继续执行后续代码否则挑战到.L4执行循环部分的操作。
3.3.8 对于函数的处理
a)Main函数
通过系统启用函数__libc_start_main使用call语句调用main函数,先将dst压入栈中,再通过rdi和rsi传递参数argc 和 argv, 最后将返回值放到rax中返回零即return 0

b)Printf函数
将格式串信息放在rdi中,将剩下的参数信息依次放在rsi,rdx,rcx中进行参数传递。具体调用过程使用call语句调用。
其中第一个printf语句因为只需要输出相应字符串信息,所以编译器将其优化为puts

c)Exit函数
传递参数edi值为1 通过call 调用exit函数
d)Sleep函数
传递参数使得edi的值为sleepsecs通过call调用sleep函数。
e)Getchar函数
直接通过call调用getchar函数并没有传递参数。
3.4 本章小结
本章主要介绍了编译器是如何处理 C 语言的各个数据类型以及各类操作。
编译器将.i编译为.s 的汇编代码。经过编译之后,我们的 hello 自C 语言变为汇编语言。
(第3章2分)

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

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

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

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

.rela节的包含的信息有(readelf显示与hello.o中的编码不同,以hello.o为准):

offset
需要进行重定向的代码在.text或.data节中的偏移位置,8个字节。
Info
包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型
Addend
计算重定位位置的辅助信息,共占8个字节
Type
重定位到的目标的类型
Name
重定向到的目标的名称

下面以.L1的重定位为例阐述之后的重定位过程:链接器根据info信息向.symtab节中查询链接目标的符号,由info.symbol=0x05,可以发现重定位目标链接到.rodata的.L1,设重定位条目为r,根据图4.5知r的构造为:

r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32, r.addend=-4,

重定位一个使用32位PC相对地址的引用。计算重定位目标地址的算法如下(设需要重定位的.text节中的位置为src,设重定位的目的位置dst):

refptr = s +r.offset (1)

refaddr = ADDR(s) + r.offset (2)

*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)

其中(1)指向src的指针(2)计算src的运行时地址,(3)中,ADDR(r.symbol)计算dst的运行时地址,在本例中,ADDR(r.symbol)获得的是dst的运行时地址,因为需要设置的是绝对地址,即dst与下一条指令之间的地址之差,所以需要加上r.addend=-4。

之后将src处设置为运行时值*refptr,完成该处重定位。

对于其他符号的重定位过程,情况类似。

3).rela.eh_frame : eh_frame节的重定位信息。

4).symtab:符号表,用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。
4.4 Hello.o的结果解析
反汇编和.s文件在内容上差别不大,有一些细节差别:
分支跳转:(左.s,右反汇编)

反汇编不使用段名称这样的助记符,直接使用地址。
函数调用:(左.s,右反汇编)

在.s 文件中,函数调用之后直接跟着函数名称,在反汇编程中,call 的目标地址是当前下一条指令。
因为 hello.c 中调用的都是共享库中的函数,需要通过动态链接器确定地址,在汇编成为机器语言的时候,将其call指令后的相对地址设置为0。
4.5 本章小结
本章介绍了 hello 从 hello.s 到 hello.o 的过程,查看 hello.o 的 elf 格式 和使用 objdump 得到反汇编代码与 hello.s 进行比较的方式。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可 被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至 于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接 器使得分离编译成为可能。
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.3 可执行目标文件hello的格式

5.4 hello的虚拟地址空间
在data dumo块中可以看到,以.dynstr和.init为例,起始地址的确对应着被载入后的虚拟地址

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。   

5.5 链接的重定位过程分析

可执行文件中,与.o文件的差别在于:
①多出了很多函数,这些函数是链接器从共享库中提取出来的,main函数所调用的函数,把它加入可执行目标文件使其完整
②调用函数的方式:call后面跟上的是所调用的函数的实际地址,而不是下一条指令的地址,因为链接器已经帮我们计算出了位置。
5.6 hello的执行流程
程序名:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave
-ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表 PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向 PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
5.8 本章小结
   链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需要的状态组成的。这个状态包括存放在内存中的程序的代码和数据,他的栈,通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell 是一个用 C 语言编写的程序,他是用户使用 Linux 的桥梁。 Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问 操作系统内核的服务。
处理流程:shell 执行一系列的读/求值(read /evaluate ) 步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
6.3 Hello的fork进程创建过程
输入./hello之后,调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全 与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的) 一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的 任何文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。
如图:

6.4 Hello的execve过程
调用fork函数之后,在子进程中加载execve函数,载入并运行hello函数。
覆盖当前进程的代码,数据,栈。保留相同的PID,继承已打开的文件描述符和信号上下文。
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成
以调用sleep函数为例,如图:

调用sleep之后,
①内核处理sleep发出的休眠请求,将hello挂起。(内核)
②hello被挂起,其他进程获得当前进程控制权(用户)
③休眠结束,收到中断信号(内核)
④hello不再挂起重新恢复,进行自己的逻辑控制流。
6.6 hello的异常与信号处理
乱按:

可以看到,程序只是把输入缓存在stdin中,由于程序最后有一个getchar,读取这之前的一个字符串作为输入,剩下的则在执行完毕之后全部打印在屏幕上。

输入ctrl+C:

进程被中断收回。

输入ctrl+Z及命令:

键入Ctrl+z进程被挂起,有ps可以看出这一点,调用jobs也能说明其当前的状态。再调用pstree查看当前的进程树。
调用fg命令把刚才挂起的hello进程转到前台,可以看到其在继续运行。
最后调用一个kill指令发送终止信号杀死了hello进程。

6.7本章小结
本章中,介绍了进程的定义与作用, Shell 的一般处理流程,调用 fork 创建新进程,调用 execve 执行 hello,hello 的进程执行,hello 的异常与信号处理等内容
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
虚拟内存(virtual memory)
这是对整个内存的抽象描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存
逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。
线性地址(linear address)或也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
基本原理
        在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
      在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。
      在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。
      段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。
      程序通过分段划分为多个模块,如代码段、数据段、共享段:
      –可以分别编写和编译
      –可以针对不同类型的段采取不同的保护
      –可以按段为单位来进行共享,包括通过动态链接进行代码共享
      这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。
       总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。

地址变换
                              
        在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址(见图4—5)。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
基本原理
        将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是页号,后一部分为页内地址w(位移量),如图所示:
       
      页式管理方式的优点是:
       1)没有外碎片,每个内碎片不超过页大比前面所讨论的几种管理方式的最大进步是,
       2)一个程序不必连续存放。
       3)便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。
      缺点是:要求程序全部装入内存,没有足够的内存,程序就不能执行。

地址变换
 在页式系统中,指令所给出的地址分为两部分:逻辑页号和页内地址。
       原理:CPU中的内存管理单元(MMU)按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址(见图4-4)。
        逻辑页号,页内偏移地址->查进程页表,得物理页号->物理地址:
        
       上述过程通常由处理器的硬件直接完成,不需要软件参与。通常,操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存:
       第一次用来查找页表将操作数的 逻辑地址变换为物理地址;
       第二次完成真正的读写操作。

7.4 TLB与四级页表支持下的VA到PA的变换

流程如图所示:
①CPU产生虚拟地址,前36位VPN与TLB匹配,如果匹配到,得到PPN与VPO(即PPO)组成物理地址。
②若TLB不命中,进入页表。VPN在四级链表中,每四分之一,即9位是一级链表的偏移量,而每一级链表中(除第四级)存储着的是下一级链表的起始地址,由此一层层进入下一层链表,直到在第四级链表中取出PPN构成物理地址。
7.5 三级Cache支持下的物理内存访问

如图所示,物理地址被分为CT(标志位)CI(索引位)CO(偏移量)
根据CI在cache中找到对应组,根据CT找到匹配的标志位,若改行的有效位为1,则命中,根据CO找到数据块传输,流程完毕。
若找不到对应tag或者标志位0,则发生了不命中,则向L2,L3,和主存中依次查询数据,将其放到L1当中。放置时,如果有空闲块直接放在空闲块里,如果没有,根据LRU等算法寻找牺牲块替换。

7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制
7.7 hello进程execve时的内存映射
execve函数在当前进程中 加载并运行新程序hello的 步骤
1) 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2) 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text和.data区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3) 映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4) 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理
页面不命中导致缺页(缺页异常) 
缺页异常处理程序选择一个牺牲页 (VP 4) 
导致缺页的指令重新启动: 页面命中

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果 这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺 页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送虚拟地址到 内存管理单元,这次内存管理单元就能正常翻译虚拟地址了
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用 来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
带边界标签的隐式空闲链表

一个块是有一个字的头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。
再为每个块添加一个脚部,脚部就是头部的一个副本。如果每个块包括一个脚部,那么分配器可以通过检查它的脚部,判断前面一个块的起始位置和状态。这个脚部总是在距当前块开始位置一个字的距离。
显示空间链表
将空闲块组织委员某种形式的显式数据结构,堆组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,如图

使用两种排序策略:
一种方法使用后进先出的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间完成。
另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用率。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问, ello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态 存储分配管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
每个Linux文件都有一个类型来表明它在系统中的角色:
普通文件:包含人一数据。应用程序常常需要区分文本文件和二进制文件,文本文件是只含有ASCLL或Unicode字符的普通文件;二进制文件是所有其他文件,对于内核来说这二者没有却别。
目录:是包含一组链接的文件,其中每一个链接都将一个文件名映射到另一个文件。
套接字:是用来与另一个进程进行跨网络通信的文件。
其他文件类型包括命名通道、符号链接以及字符和块设备。
unix io接口包括打开和关闭文件、读和写文件以及改变当前文件的位置。
8.2 简述Unix IO接口及其函数
Unix I/O 接口统一操作:
1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4) 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5) 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
Unix I/O 函数:
1) int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
2) int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
研究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的格式,然后将匹配到的参数按照fmt格式输出。我们看到printf函数中调用了两个系统调用分别是vsprintf和write,先看看vsprintf函数:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
char tmp[256];
va_list p_next_arg = args;

for (p = buf; *fmt; fmt++)
{
    if (*fmt != '%')
    {
        *p++ = *fmt;
        continue;
    }

    fmt++;

    switch (*fmt)
    {
    case 'x':
        itoa(tmp, *((int *)p_next_arg));
        strcpy(p, tmp);
        p_next_arg += 4;
        p += strlen(tmp);
        break;
    case 's':
        break;
    default:
        break;
    }
}
return (p - buf);

}
可以看到这个函数的作用是将所有参数内容格式化后存入buf,然后返回格式化数组的长度。而另一个函数write是一个输出到终端的系统调用在此不做赘述。
所以printf函数的执行过程是:从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
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;
}
可以看到getchar函数调用了系统函数read,读入BUFSIZE字节到buf,然后返回buf的首地址,注意到只有当n = 0时才会调用read函数,如果n = 0还会返回EOF文件终止符。异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
结论
过程:

①预处理,将hello.c调用的外部库归入到hello.i中
②编译,将hello.i编译为汇编文件hello.s
③汇编,将汇编文件hello.s变为可重定位目标文件hello.o
④链接,将hello.o和其他的可重定位目标文件和动态链接库链接成可执行目标文件hello
⑤运行,输入./hello 1170300720 taofeiyu
⑥fork,调用fork,创建子进程
⑦execve,调用execve函数,加载映射虚拟内存,进入程序入口后再加载物理内存,最后进入main函数。
⑧执行,CPU为hello分配资源,享有控制逻辑流
⑨访存,内存管理单元将VA翻译为PA
⑩申请动态内存,printf调用malloc,申请堆中内存
⑪信号,如果有Ctrl+c,Ctrl+z键入,停止,挂起hello
⑫结束,子进程被父进程或者init回收,其享有的资源被回收

感悟:
计算机系统是一个复杂又简单的整体,深入理解它为我们的计算机学习打下了坚而厚实的基础。

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

附件
Hello.c 源程序
Hello.i 预处理之后的文本文件
Hello.s 编译之后的汇编文件
Hello.o 汇编之后的可重定位文件
Hello 连接之后的可执行目标文件
Asm.txt hello.o的反汇编代码
Asm1.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.
(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值