计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1190201206
班 级 1903007
学 生 杨富生
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本论文通过研究hello在linux系统下的一生来对CSAPP课程所学知识进行梳理,通过这个程序深挖知识点,对ubuntu下的指令操作和理解和学习计算机系统有很大帮助。
关键词:CSAPP;大作业;Hello ;生命周期;历程;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:通过Editor编写代码得到hello.c程序,在linux下通过cpp预处理得到.i文件,这个是ASCII码的文件;之后使用ccl编译得到汇编代码.s文件,汇编器as运行得到.o文件,.o文件是可重定位目标文件,最后用链接器ld链接成可执行目标程序hello。随后用户输入启动命令,shell为其调用fork函数产生子进程。
020:子进程产生后,调用execve函数,然后进行映射虚拟内存,为hello分配时间片段来进行取指译码流水线等操作;程序结束之后,shell回收hello进程,内核删除其相关痕迹。
1.2 环境与工具
硬件环境:Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 2.59 GHz
软件环境:Windows10 64位,Vmware ,Ubuntu20.04.1 LTS
开发者与调试工具:gcc,gdb,objdump,ld,edb,Winhex,readelf,ldd等
1.3 中间结果
文件名 | 文件功能 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编文件 |
hello.o | 可重定位目标文件 |
hello | 链接后的可执行目标文件 |
hello.elf | hello.o的elf文件 |
hello.txt | hello.o的反汇编文件 |
hello_elf | hello的elf文件 |
hello的反汇编文件 |
1.4 本章小结
本章先是介绍了hello的P2P和020,对需要的环境和工具进行列举,并且对实验过程中用到的所有中间文件和其作用进行说明。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理概念:预处理是指在源程序被翻译成二进制代码之前的过程,预处理器cpp会依据以#开头的指令,试图解释为预处理指令。读入源代码,检查后对源代码进行响应的转换。这个过程中还删除程序中的注释和多余的空白字符。预处理指令用来使源代码在不同的执行环境中方便编译或修改。
预处理作用:
- 用实际值替换宏定义的字符串。
- 处理条件编译指令比如#ifdef,#else等,这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理包含指令如#include "FileName"或者#include 的头文件, 将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
如图:
2.3 Hello的预处理结果解析
查看hello.i,如图:
整个hello.i程序为3060行,main函数出现在hello.i中从3047行开始。
在main函数之前是头文件 stdio.h unistd.h stdlib.h 的展开。可以来看stdio的展开:cpp在寻找到stdio.h之后,发现其中还使用了#define的语句,又发现了大量的#ifdef #ifndef条件编译的语句,头文件里有大量的宏定义和条件编译语句存在,预处理阶段需要对这些语句进行相应的宏替换和条件编译处理,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.4 本章小结
本章介绍了预处理的概念和应用,演示了预处理阶段的编译以及对预处理结果进行了解析。(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:将源代码通过词法分析和语法分析以及语义分析且经过一系列优化后生成汇编代码文件的过程。在实验中,即将文本文件hello.i翻译成文本文件hello.s。
汇编的作用:将高级语言程序转化为机器可直接识别处理执行的的机器码,包括以下流程:
- 语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
- 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
- 代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
如图:
3.3 Hello的编译结果解析
3.3.1汇编指令
指令内容 | 含义 |
.file | 声明源文件 |
.text | 以下是代码段 |
.section .rodata | 以下是 rodata 节 |
.align | 声明对指令或者数据的存放地址进行对齐的方式 |
.string | 声明一个string 类型 |
.globl. | 声明一个全局变量 |
.type | 用来指定是函数类型或是对象类型 |
.size | 声明大小 |
3.3.2数据类型
hello.s 中用到的数据类型有:整数、字符串、数组。
3.3.2.1整数
程序中涉及的整数类型有:
- int i:编译器将局部变量存储在寄存器或者栈空间中,如图可以看出i存储在栈上空间-4(%rbp)中,可看出i占据了栈中的4个字节的空间。
这行代码表示i = 0,从这里看出可以看出i的位置。
- int argc:由于argc是main函数的第一个参数,又因为%edi是存储第一个参数的寄存器,所以从汇编代码我们可以分析得出argc的位置为-20(%rbp)
- 立即数:其他整形数据的出现都是以立即数的形式出现的,可以直接硬编码在汇编代码中。在汇编代码中是以“$+数字”出现的。
3.3.2.2字符串
程序中的字符串有:
- “用法: Hello 学号 姓名 秒数!\n”,这是第一个printf传入的输出格式化参数,可以发现字符串被编码成 UTF-8 格式,一个数字占一个字节,一个汉字在 utf-8 编码中占三个字节,一个\代表一个字节,一个!代表一个字节。
- “Hello %s %s\n”,第二个 printf 传入的输出格式化参数,后两个字符串都声明在了.rodata 只读数据节。
3.3.2.3数组
程序中的数组有:
char *argv[],该数组是main函数的第二个形参,来自于我们在终端输入的数据,(比如我输入1190201206 yfs 3)。char*的大小为8个字节,字符指针空间是连续分配的,我们访问到argv[1]、argv[2],已知argc的首地址,在argv之后:
我们可以看到在hello.s中,运用了2次movq %rax, %寄存器,目的是连续取出argv[1]和argv[2]内容,即我们终端输入的命令参数。
3.3.3赋值操作
程序中涉及的赋值操作有:
i=0:整型数据的赋值使用mov指令完成,move根据数据的大小不同使用不同后缀,分别为:
指令 | b | w | l | q |
大小 | 8b (1B) | 16b (2B) | 32b (4B) | 64b (8B) |
3.3.4算数操作
进行数据算数操作的汇编指令有:
指令 | 效果 |
1eaq S,D | D←&S |
INC D DEC D NEG D NOT D | D←D十1 D←D-1 D←一D D←一D |
ADD s,D | D←D+s |
SUB S,D | D←D-S |
IMUL S,D | D←D*S |
XOR S,D | D←D^S |
OR S,D | D←D|S |
AND S,D | D←D&S |
SAL k,D | D←D< |
SHL k,D | D←D< |
SAR k,D | D←D>>Ak |
SHR k,D | D←D>>Lk |
程序中涉及的算数操作有:
- i++,使用指令addl,传输4字节大小的数。
- 汇编语言中也有算术操作,比如寻址,加载有效地址指令leaq计算LC0的段地址%rip+.LC0并传递给%rdi。
- 还比如在访问数组元素时调整%rax的值,这里+8代表char*的大小,64位下为8个字节。
3.3.5关系操作
进行关系操作的汇编指令有:
指令 | 效果 | 描述 |
CMP S1,S2 | S2-S1 | 比较-设置条件码 |
TEST S1,S2 | S1&S2 | 测试-设置条件码 |
SETX D | —— | —— |
JX | —— | —— |
程序中涉及的关系运算为:
- argc!=4:判断argc不等于4。hello.s用cmpl $4,-20(%rbp),计算argc-4然后设置条件码,为下一步利用条件码跳转作准备。
- i<8:判断i小于8。hello.s中使用cmpl $7,-4(%rbp),计算i-7然后设置条件码,为下一步利用条件码跳转做准备。
3.3.5控制转移
程序中涉及的控制转移有:
- if (argc!=4) jmp .L3; :当argv不等于4的时候执行程序段中的代码。
程序反复跳转,用一次跳转举例:首先cmpl比较argv和4,设置条件码, 使用je判断ZF标志位,为0代表相等则不执行if中的代码直接跳转到.L2, 否则顺序 执行下一条语句,即执行if中的代码。
- for(i=0;i<8;i++) :使用计数变量 i 循环 8 次。
每次执行完都要对i进行加一,并且和7比较,不等于7还要返回循环头部继续执行。
3.3.6函数操作
函数是过程的一种形式。而过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。
当调用一个过程时,除了要把控制传递给它并在过程返回时再传递回来之 外, 过程调用还可能包括把数据作为参数传递,而从过程返回还有可能包括返 回一个值。
64 位程序参数存储顺序:
1 | 2 | 3 | 4 | 5 | 6 | 7 |
%rdi | %rsi | %rdx | %rcx | %r8 | %r9 | 栈空间 |
浮点数使用 xmm
程序中涉及函数操作的有:
1.main 函数:
- 传递控制,main函数被系统启动函数__libc_start_main 调用,call 指令将下一条指令的地址 dest 压栈,然后跳转到 main 函数。
- 传递数据,main函数被传递两个参数名分别为argc和argv,分别存储在%edi和%rsi中,之后被压入栈中,返回时正常返回0
- 分配和释放内存,使用%rbp 记录栈底,当结束时,调用 leave 指令,leave指令相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后 ret返回,ret 相当 pop IP,将下一条要执行指令的地址设置为 dest。
2.printf 函数:
- 传递数据:第一个printf将%rdi赋值为字符串“用法: Hello 学号 姓名 秒数!\n”字符串的首地址(leaq .LC0(%rip), %rdi),然后调用了puts函数,将字符串传入puts打印。第二个 printf 设置%rdi 为“Hello %s %s\n”的首地址,%rsi 放入argv[1],%rdx 放入argv[2]。
- 控制传递:第一次 printf 因为只有一个字符串参数,所以调用puts;第二次 printf 调用printf。
3.exit 函数:
- 传递数据:%edi存入1
- 控制传递:call exit@PLT。
4.sleep 函数:
- 传递数据:将%edi 设置为%eax值。
- 控制传递:call sleep@PLT
5.getchar 函数:
控制传递:call gethcar@PLT
- atoi函数
a)传递数据:将%rdi 设置为%rax值,这里就是argv[3]。
b)控制传递:call atoi@PLT
3.4 本章小结
本章介绍了编译的概念与作用,在Ubuntu下编译的命令,以及具体到对hello.c源文件的编译文件hello.s进行数据类型(包括整数,字符串,数组)和相关操作的细致分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编概念:汇编器as将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程。
汇编作用:汇编器将.s 汇编程序文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
4.3 可重定位目标elf格式
键入命令readelf -a hello.o >hello.elf将elf可重定位目标文件输出定向到文本文件hello.elf:
组成如图:
- ELF Header:第一行是16个字节,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
- Section Headers:节头部表,包括节的类型、位置和大小等信息。
- 重定位节.rela.text ,一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,8条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、atoi函数、sleep 函数、getchar 函数进行重定位声明。
.rela 节中包含以下信息:
offset | 需要进行重定向的代码在.text或.data节中的偏移位置,8个字节。 |
Info | 包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型 |
Addend | 计算重定位位置的辅助信息,共占8个字节 |
Type | 重定位到的目标的类型 |
Name | 重定向到的目标的名称 |
接下来进行重定位计算(我们的机器是x86-64位)。
一个基于32位x86的重定位类型的计算。
a)对于R_386_PC32,计算方式为S + A - P;
b)对于R_386_PLT32,计算方式为L + A - P;
重定位一个使用 32 位 PC 相对地址:
refptr = s +r.offset (1)
refaddr = ADDR(s) + r.offset (2)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)
下面以.L0的重定位为例阐述之后的重定位过程:
可以发现重定位目标链接到.rodata 的.L1,设重定位条目为 r,r 的构造为:r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32, r.addend=-4
其中(1)指向 src 的指针(2)计算 src 的运行时地址,(3)中,
ADDR(r.symbol)计算 dst 的运行时地址,在本例中,ADDR(r.symbol)获得的是 dst 的运行时地址,因为需要设置的是绝对地址,即 dst 与下一条指令之间的地址之差,所以需要加上 r.addend=-4。之后将 src 处设置为运行时值*refptr,完成该处重定位。
- 符号表(Symbol Table),包含用来定位、重定位程序中符号定义和引用的信息。符号表索引就是对这个数组的索引。
4.4 Hello.o的结果解析
使用 objdump -d -r hello.o > hello.txt 获得反汇编代码。Hello.s和hello.txt除去显示格式之外两者差别不大,主要差别如下:
-
- 分支转移:objdump产生的反汇编代码不使用段名称,即便于编写的助记符,地址都是确定的。
- 函数调用:反汇编程序中,call目标地址是当前下一条指令。因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全 0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
- 全局变量访问:在.s 文件中,访问 rodata(printf 中的字符串),使用段名称+%rip,因为 rodata 中数据地址也是在运行时确定,故访问也需要重定位。
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的格式
获取hello的elf格式文件:readelf -a hello >hello_elf(输出文件区别hello.elf)
截图1:hello的ELF格式
各节的基本信息均在节头表(描述目标文件的节)中进行了声明。节头表(包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息),截图如下:
5.4 hello的虚拟地址空间
第1步,找到edb位置,打开edb:
第2步,在edb中加载hello可执行文件
第3步,观察Data Dump窗口。窗口显示虚拟地址由0x401000开始,到0x401fff结束,之间的每一个节也对应5.3中节头表的声明。
5.5 链接的重定位过程分析
反汇编得到hello.asm,这个是对hello的反汇编:
横向对比hello.txt和hello.asm:
1.hello.txt只有text节,而hello.asm比hello.txt多了很多节,比如.init节和.plt节
2.hello.asm地址是虚拟地址,hello.txt地址是相对偏移地址.
3.hello.asm增加了很多外部库链接的共享库函数。
4.在hello.asm中跳转和函数调用也是虚拟内存地址
5.6 hello的执行流程
重新打开edb,观察函数执行流程,将过程中执行的主要函数列在下面:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
hello!main
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
libc-2.27.so!exit
5.7 Hello的动态链接分析
编译器没有办法预测函数运行时的地址,对于PIC函数,需要为其添加重定位记录。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。
接着我们查看dl_init函数调用前后.got.plt节的变化:
可以看出调用前后的条目变化。
5.8 本章小结
在本章中介绍了链接的概念与作用、hello在ubuntu下的链接,hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
作用:进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是用户与操作系统之间完成交互式操作的一个接口程序,它接受用户命令,调用相应的应用程序。
处理流程:
- 从终端读入用户输入的命令
- 将输入字符串切分来获取参数
- 对参数进行判断,如果是内置命令,立即执行;否则为其调用相应的程序为其分配子进程并运行
- shell接受键盘输入信号,对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
进程调用fork()函数后,就创建了一个子进程。函数为pid_t fork(void);若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;出错则返回-1.
1.对终端输入进行判断,若为非内置命令,则shell在硬盘上查找该命令,将其调入内存,并将其解释为系统功能调用转交给内核执行。
2.shell调用fork函数,创建子进程。hello子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。但是父子进程间不共享这些存储空间。
3.同时Linux将复制父进程的地址空间给子进程,因此,hello进程就有了独立的地址空间。
画出进程图
---------------hello程序----
|
---------+------------------------------
fork
6.4 Hello的execve过程
- 在子进程调用函数fork之后,子进程又调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序hello。
- execve调用留在内存中启动加载器的操作系统代码,并创建一组新的代码、数据、堆和栈段,新的栈和堆段被初始化为0,这是为了执行hello程序加载器、删除子进程现有的虚拟内存段。
- 新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start 地址,来调用 main 函数。除了一些头部信息,加载过程没有任何从磁盘到内存的数据复制。
6.5 Hello的进程执行
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 用户模式和内核模式:处理器通常使用一个寄存器描述了进程当前享有的特权,对两种模式区分。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
- 上下文信息:在内核中调度了一个新的进程后,该进程会抢占当前进程。上下文就是内核重新启动一个被抢占的进程所需要的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
hello sleep进程调度的过程:
- 先看hello程序是否被抢占,被抢占则进行上下文切换,不被抢占则顺序执行。
- 上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行
- 保存以前进程的上下文
- 恢复新恢复进程被保存的上下文
- 将控制传递给这个新恢复的进程
- hello初始为用户模式,调用sleep之后陷入内核模式,内核将 hello 进程从运行队列中移出加入等待队列。
- 定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时间(我们的程序是自己输入)发送一个中断信号,
- 进入内核状态执行中断处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
6.6 hello的异常与信号处理
1.正常运行:完成之后正常回收,最后输入回车结束
2.暂时挂起:
-
- 在程序输出2条info后按ctrl-z,shell 父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显,将hello进程挂起。
- 通过 ps 命令看hello 进程没有被回收,此时他的后台job 号是1。
- 调用fg 将其调到前台,此时 shell 程序首先打印hello的命令行命令,hello 继续运行打印剩下的6条info,程序结束,同时进程被回收。
3.中途中断:在程序输出2条info后按ctrl-c ,shell父进程收到SIGINT信号,结束hello,并回收hello进程。
4.乱按:系统会将乱按的字符串输入到缓冲区stdin,输出结束,乱按的字符会被当作命令执行
5.杀死hello程序。用kill -9 3545:
6.Pstree截图:
6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用 fork 创建新进程,调用 execve 执行 hello,hello的进程执行,hello 的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指由程序hello产生的与段相关的偏移地址部分(hello.o),逻辑地址由选择符和偏移量组成
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
虚拟地址:有时我们也把逻辑地址称为虚拟地址。
物理地址:物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由2部分组成,段标识符:段内偏移量.段标识符由一个16位长的字段组成,成为段选择符.其中前13位是一个索引号.后面3位包括一些硬件细节。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成。
段描述符表,实际上是虚拟内存被分成多段组成的段组,一个地址,通过段标识符的前13位,直接再段描述符中找到一个具体的段描述符,这个迷哦书符描述了一个段,包括了数据描述符,代码描述符和系统描述符,每一个段描述符由8个字节组成,其中包括Base字段,他描述了一个段的开始位置的线性地址. intel在设计时,一些全局的段描述符,放在全局段描述符表(GDT)中,局部的就放在局部段描述符表(LDT)中,用段选择符中T1字段表示,T1=0,表示用GDT;T1=1表示用LDT. LDT和GDT的地址和大小分别放在ldtr寄存器和gdtr寄存器中.
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。MMU利用VPN选择PTE,将列表条目中的PPN和虚拟地址的VPO链接起来,这样便能得到对用的物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构,变换如下:
为减少时间开销,MMU中存在一个关于PTE的缓存,成为翻译后备缓冲器TLB。其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。
四级页表翻译:
7.5 三级Cache支持下的物理内存访问
通过7.4中获得的PA取出索引值对应位,对应成功则向L1 d-cache对应组中查找,将L1对应组中的每一行的标记位进行对比,当且仅当标记位相同且有效位为1则命中,获得偏移量,取出相应字节,否则不命中,向下一级cache寻找,最后可能寻找到主存。
7.6 hello进程fork时的内存映射
内核调用fork创建子进程,为子进程创建各种数据结构,并且给它分配一个唯一的PID,过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。且将这两个进程的每个页面都标记是只读,将两个进程中的每个区域结构都标记是私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域。
- 映射私有区域。
(3)映射共享区域。
(4)设置程序计数器(PC),设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在程序执行过程中出现缺页故障时,内核会调用缺页处理程序:首先检查虚拟地址的合法性,不合法会触发段错误,程序终止。其次会检查进程是否有读、写或执行该区域页面的权限,不具有会触发保护异常,程序终止。在两步检查都无误后,程序内核选择一个牺牲页面,如果页面修改过还要换入新的页面来更新页表。
然后按照目标虚拟页的PTE的磁盘地址,将磁盘的页取出放内存中,同时修改PTE. 然后返回程序中断处的当前指令,继续请求访问该虚拟地址.
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
- 显式分配器(explicit allocator):要求应用显式地释放任何已分配的块。例如,C程序提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++的new和delete操作符与C中的malloc和free相当。
- 隐式分配器(implicit allocator):另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
基本方法:
隐式空闲链表: 一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。
显示空闲链表:再空闲块中,除了头部和尾部,还存在指向前一个空闲块和后一个空闲块的2个指针,通过指针可以找到所有的空闲块
策略:
首次分配:每一次分配,都从堆的开始寻找空闲块,直到找到一个可以存储的下的空闲块,就分配该块.
下次分配:每次分配都从上次的地方开始寻找空闲块,一旦找到可以存储的下的空间块,就分配该块.
最佳分配:遍历所有的空闲块,找到可以存储的下,且最小的块进行分配
所有的分配中,如果找不到合适的块,就会调用extend函数,扩大堆,增大brk
7.10本章小结
本章主要介绍了hello的存储地址空间,段式管理,页式管理,TLB与四级页表支持下的VA到PA的变换,三级cache支持下的物理内存访问,hello进程fork和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理等内容。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化;
文件:所有的I/O设备都被模型化为文件
设备管理;
unix io接口:Linux内核引出一个简单低级的应用接口,反应这种将设备优雅地映射为文件的方式。
8.2 简述Unix IO接口及其函数
接口
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
2.linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
函数
1.打开和关闭文件。
打开文件函数原型:int open(char* filename,int flags,mode_t mode)
返回值:若成功则为新文件描述符,否则返回-1;
flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode:指定新文件的访问权限位。
关闭文件函数原型:int close(fd)
返回值:成功返回0,否则为-1
2,读和写文件
读文件函数原型:ssize_t read(int fd,void *buf,size_t n)
返回值:成功则返回读的字节数,若EOF则为0,出错为-1
描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf
写文件函数原型:ssize_t wirte(int fd,const void *buf,size_t n)
返回值:成功则返回写的字节数,出错则为-1
描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置
8.3 printf的实现分析
1)首先看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;
}
函数体内部调用了函数vsprintf,看一下vsprintf函数。
其中va_list的定义被定义为字符指针。
2)再看vsprintf函数(在printf函数内部调用)。
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': //只处理%x一种情况
itoa(tmp, *((int*)p_next_arg));//将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //tmp字符串复制p
p_next_arg += 4; //下一个参数地址
p += strlen(tmp);//存放下一个参数的地址
break;
case 's':
break;
default:
break;
}
}
return (p - buf); //返回生成的字符串长度
}
函数描述:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
3)对于系统函数write
反汇编追踪write函数
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
发现反汇编语句中的int INT_VECTOR_SYS_CALL,它表示要通过系统来调用sys_call这个函数。
4)来看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
函数功能:显示格式化的字符串。将要输出的字符串从总线复制到显卡的显存中。并通过信号线向液晶显示器传输每一个点(RGB分量)。而我们要传输的“1190201206 yfs 1”就会被打印输出在显示器上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口得到一个代表该按键的键盘扫描码,同时产生一个中断请求,键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析printf函数和getchar函数。
(第8章1分)
结论
一个小程序hello走完了它的一生,但是却带领我们遍历了宏伟的计算机世界:
- hello.c开始,经过预编译得到hello.i文本文件
- hello.i经过编译,得到hello.s汇编语言文件
- hello,s经过汇编,得到可重定位目标二进制文件hello.o
- hello.o经过链接,生成可执行程序文件hello
- shell父进程调用fork为hello创建子进程,由execve函数调用启动加载器,CPU进入main函数执行程序。
- MMU将程序中使用的虚拟内存地址,通过页表映射成物理地址。
- hello运行时会调用printf函数这类函数,这类函数与linux I/O的设备模拟化密切相关
- hello最终被shell父进程回收。
感悟:对计算机系统的学习是一个不能停滞的过程,计算机在创新,计算机系统也在时时更新,对于本论文的编写,由于本人水平有限,所以做不到很深刻,只能笼统地去写一些东西来表达我对计算机系统这门课程的理解。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名 | 文件功能 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编文件 |
hello.o | 可重定位目标文件 |
hello | 链接后的可执行目标文件 |
hello.elf | hello.o的elf文件 |
hello.txt | hello.o的反汇编文件 |
hello_elf | hello的elf文件 |
hello.esm | 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/ai977313677/article/details/72799664
[8] printf 函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)