计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190201801
班 级 1903012
学 生 耿健
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
本文将从计算机系统的视角,从预处理阶段开始逐步分析hello.c的生命进程,从预处理,到编译,再到执行,理清其背后的整套流程。从信息的表示,到进程的管理,再到内存管理以及生命结束后的一系列处理。彻底弄清一个程序的生命过程。
关键词:计算机系统,编译,进程,内存,IO,异常;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
1) From Program to
Process。首先我们需要编程,即program编写程序hello.c,然后这个文件会进行一系列的预处理,编译,汇编,链接生成一个.out可执行文件,我们打开shell之后,如果去运行它,那么hello就会为hello创建一个新的进程,即progress,然后调用execve函数,将hello的可执行文件进行加载,运行。
2) FROM ZERO TO
ZERO。首先在真正加载前shell这个壳需要为hello申请虚拟内存空间,然后将物理内存与虚拟内存进行映射,同时处理器内核还需要为hello分配时间片,使得其看似独享整个资源,当需要物理地址时,cpu会向mmu发出一个虚拟地址,mmu将虚拟地址转化为物理地址之后,在提供相应的数据。同时当我们在程序运行期间,输入信号时,会使内核产生一个中断,当按下ctrl
z 或 ctrl
c时,会向hello进程发送一个信号,如果子进程终止之后,会有父进程将僵尸进程进行回收,避免占用资源。
1.2 环境与工具
硬件: Core I7-10875H X64 ; 32G RAM; 1TB SSD
软件: Ubuntu 20.10
工具: VIM , GCC, EDB, OBJDUMP, READELF, LD
1.3 中间结果
hello.c:源 程序
hello.i: 经过预处理的源程序
hello.s:hello.i 经 过编译的汇编程序
hello.o:hello.s 经过汇编的可重定位目标程序
hello:hello.o 经过链接后的可 执行目标程序
hello.elf:hello.o 的 elf 格 式
hello.elf:hello 的 elf 格式
helloo.objdump:hello.o的反汇编程序
hello.objdump:hello的反汇编程序
1.4 本章小结
本章节简述了 P2P 和 020 的含义,列出了测试环境和工具和中间结果的文件名
和文件作用。从整体上大致介 绍了 hello
的一生,并且列出了做本次作业的软硬件环境以及工具。也相当于漫游了一下hello的一生。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理器根 据 以 字 符# 开头的 命令修改 原始 的 C 程序。 比如 hello.c 中的 命
令告诉预处理 器 读 取 对 应 的 三 个 系 统 头 文件的内 容,并把它 直接插入到程
序文 本中,结果就得 到了另一个 C 程序。 其中,ISO
C/C++要求支持的包括#define(宏定义)、
#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实
现 相 关的 杂注 )以 及 单 独 的 #(空指令)。
作用:
-
#include 指令告诉预处理器(cpp)读取 源程序所 引用 的系统 源文件,并把 源文
件直接 插入程序文本中。 -
执行宏替换。将目 标的字 符替换 为我们所定 义的字符。
-
条件编译。根 据 定 于 的 条 件,来确 定 编译的 条件,即目 标是否 是
真正需要的,类似于ifelse。 -
特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
用vim打开hello.i,开始寻找main函数:
这里找到mian函数的位置,在这之前出现的是头文件 stdio.h unistd.h stdlib.h
的依次展开。
以 stdio.h 的展开为例:stdio.h 是 标准库文 件,cpp 到Ubuntu中 默 认 的 环
境变量下寻找 stdio.h,打开文件/usr/include/stdio.h ,发 现 其 中依 然 使 用
了#define语句,cpp 对stdio中的define宏定义 递 归 展 开。
最终.i文件将预处理指令进行替换,而原来的预处理指令就不需要了;发
现其中使用了大量的#ifdef #ifndef条件编译的语句,cpp 会对 条件 值 进 行 判 断
来 决 定 是 否执 行包含其中的逻辑。 特殊符号,预 编译程序可以 识别一 些 特 殊 的
符 号,预编译程序 对于 在 源 程 序 中出现 的 这 些 串 将 用 合适的值进行替换。在
下面我们可以看到其寻找的头文件。
2.4 本章小结
本章主要 介绍了预处理的
定义与作用,并且分析了.i文件的整个分析过程、并结合.i程序对预处理结果进
行了分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是利用编译程序 从预处理文本 文件产生汇编程序的过程。主要包含五个阶段:词法分析;语 法分析;语义检查、中间代码生成、 目标代码 生成。
编译的作用:
-
词法分析。将源代码程序输入扫描器,将源代码的字符序列分割成一 系列记号。
-
语法分析。基于词法分析得到的一系列记号,生成语法树。
-
语义分析。由语义分析器完成,指示判断 是否合法,并不判断对错。
-
目标代码的生成与优化。目标代码生 成 阶 段 编 译器 会选 择合 适的寻址
方式,左移 右 移代替乘除,删除多余指令。
3.2 在Ubuntu下编译的命令
命令行: gcc -S hello.i -o hello.s
图 5
图 6 .s文件
3.3 Hello的编译结果解析
hello.c 中的数据类型有整型、字符串和数组。
整数
可以看出.data 节是 8 字节对齐的,
图 7
hello.c 中还有 argc 和 i 两 个 整 型 变 量。其中 int i 是 循 环中 用来
计数的局部变量,argc 是从终端 输 入 的 参 数 的个数 ,也是 main 函 数 的
第一个参数。 hello.s 将 i 存 储 在 -4(%rbp)中,初始值为 0,每 次 循 环 加 一,
退出循环条件是 i 大于 7。
图 8
图 9
字符串 编 译 器 一 般 会 将 字符串存 放在.rodata 节。hello.c
中共有两个字符串,分 别是 两 个 printf 格 式 化 输 出 的 字 符串。 对应找 到
he ll o.s 中的.rodata 节:
图 10
可以看出 hello.s 中字符串由.string 声明,第一个字符串.LC0 包 含 汉字,每
个汉字在 utf-8 编码中被编码为三个字节,第二个字符串的两个%s 为用户
在终端输入的两个参数。
数组 hello.c 中的 数组是 main()函 数的第二个参 数 char *argv[],argv
是字符串指 针的数组,每个元素是一个指向一个字符串首 地 址 的 指
针,作为函数的第 二 个 参数 ,argv[]开始被保存在寄存器%rsi 中,然 后 又 被
保 存 到 栈 中32 (%rbp)的 位 置。
图 11
可以看到 main 函数并没有访问 argv[0],而是访问了 argv[1]和 argv[2],这 是因为
argv[0]指 向 程 序 运 行 的 全 路 径 名。
函数操作 程序运行时先进入程序入口处,然后自动调用 main
函数。若程序员需要调用 函数,在汇编代码中需要使用 call
指令,在使用之前需要先设置好参数。进行传参。
图 12
结束后的ret:
%rbp为栈帧的底部,函数在%rbp上分配空间
leave指令:相当于mov %rbp,%rsp ,pop %rbp,恢 复 栈 空 间 为 调 用 main函 数 之
前 的 状 态。
图 13
3.4 本章小结
本节对应于书上与汇编语言相关的章节,总 结 并 分 析 了 编 译 器 是
如何处理c语言的各个数据类型和各类操作,如算术操作,关系操作 和
函数调用的。经过该步骤 hello.s已经是更 加 接 近 机器层面的汇编代码。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
-
汇编的概念:汇编器(as)将hello.s翻译 成机器 能读懂 的 机器 语
言指令,并将这些指令打包成可重定位目标程序hello.o,hello.o是一个二 进 制
文件 -
作用:产生 机器能 读 懂 的代 码,使得 程序能 被 机 器 执 行。由 于 几
乎每一条汇 编指 令都对应 于一 条机 器 代码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器 语 言 二 进
制程序的过程。
4.2 在Ubuntu下汇编的命令
命令为: gcc -c hello.s -o hello.o
图 14
应截图,展示汇编过程!
4.3 可重定位目标elf格式
输入: readelf -a hello.o > hello.elf 指令.获得ELF格式文件。
图 15
-
ELF Header:以 16B 的序列 Magic 开始,Magic 描 述了生成该
文件的系统的字的大小和字节顺序,ELF 头剩下的 部 分 包 含帮助 链 接 器 语 法
分 析和 解 释 目标文件的信息,其中包括 ELF 头 的 大 小、
目标文件的类型、机器类型、字节头部表(section header table)的 文 件 偏 移,
以 及 节 头 部 表 中 条 目 的 大 小 和 数 量等 信 息。图 16
图 17
-
Section Headers:节 头 部表,包含了文件中出 现 的 各 个 节的 语 义,
包括节的类型、位置和大小等信息。图 18
-
重 定 位节.rela.text ,一个.text 节 中 位 置的列表,包含.text 节 中 需要
进行重定位的信息,当链接器 把这 个目 标文件和 其他文 件 组
合时,需要修改这些位置。8 条重定位信息分别是 对.L0(第一 个 printf
中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf
函数、sleepsecs、sleep 函数、getchar 函数进行重定位声明。图 19
以.L1 的 重定 位 为 例阐 述 之后的 重定 位过程: 链接器根据 info
信息向.symtab 节中查 询 链 接目标 的符号,由 info.symbol=0x05,可 以 发 现
重 定位 目标链 接到.rodata 的.L1,设重定位条目为 r,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,完成该处重定位。 -
符号表(Symbol
Table)目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引
0 表示表中的第一表项,同时也作为定义符号的索引。图 20
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
使用 objdump -d -r hello.o > helloo.objdump
获得反汇编代码。Hello.s和helloo.objdunp除去显示格式之外两者差别不大,主要差别如下:
-
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
-
函数调用:在.s
文件中,函数调用之后直接跟着函数名称,而在反汇编序中,call
的目标地址是当前下一条指令。这是因为 hello.c
中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其
call 指令后的相对地址设置为全
0(目标地址正是下一条指令),然后在.rela.text
节中为其添加重定位条目,等待静态链接的进一步确定。 -
全局变量访问:在.s 文件中,访问 rodata(printf
中的字符串),使用段名称+%rip,在反汇编代码中 0+%rip,因为 rodata
中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全
0 并添加重定位条目。
-
图 21
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章介绍了 hello 从 hello.s 到 hello.o 的汇编过程,通过查看 hello.o 的 elf
格式和使用 objdump 得到反汇编代码与 hello.s
进行比较的方式,间接了解到从汇编语言映射到机器语言汇编器需要实现的转换
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:
是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载(复制)到内存并执行。
作用:
1)链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
2)链接使得分离编译(seperate compila)成为可能。更便于我们维护管理,我们可以独立的修改和编译我们需要修改的小的模块。
注意:这儿的链接是指从 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
图 22
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
图 23
- 各节的基本信息均在节头表(描述目标文件的节)中进行了声明。节头表(包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息),下面是它的截图。
图 24
)]
图 25
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
图 26
图 27 edb加载hello后
图 28
- 在这里可以看到0x400000开始的位置内存信息,从0x400000-0x400fff之间的节对应上文中的节表头声明。
图 29
- 关于5.3节节头表中的.dynamic到.shstrtab的处理。首先查看hello的elf格式文件重的程序头,它包含的信息:类型,偏移,虚拟地址,物理地址,对齐,标志等,如截图5。通过Data
Dump窗口查看虚拟地址段
0x600000到0x602000的部分,在0到fff的空间中,与0x400000到0x401000段的存放的程序相同;而在
fff之后存放的是.dynamic到.shstrtab节。
5.5 链接的重定位过程分析
- 使用 objdump -d -r hello > hello.objdump 获得 hello 的反汇编代码。
图 30
其中右侧是对hello进行反汇编,而左侧是对hello.o的可重定位文件进行的反汇编。
- 第2步,分析列举hello反汇编文件与hello.o反汇编文件的区别(即helloo.objdump与hello.objdump的对比)。
-
我们发现hello.objdump比helloo. objdump多了 许 多 文 件 节 。比 如.
interp节和.hash节等而hello.o反 汇 编 得 到 的 程 序 直 接 从.text 的 代 码
段开始 。图 31
-
hello.objdump中增加了许多外部链接的共享库函数。如puts@plt共享库函数,printf@plt共享库函数以及getchar@plt函数。
图 32
-
跳转和函数调用的地址在hello.objdump中是虚拟内存地址:
图 33
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
使用EDB,只需要逐步执行,将程序名列出即可:
函数 | 地址 |
---|---|
ld-2.27.so!_dl_start | 0x7fce 8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce 8cc47630 |
hello!_start | 0x400500 |
libc-2.27.so!__libc_start_main | 0x7fce 8c867ab0 |
-libc-2.27.so!__cxa_atexit | 0x7fce 8c889430 |
-libc-2.27.so!__libc_csu_init | 0x4005c0 |
hello!_init | 0x400488 |
libc-2.27.so!_setjmp | 0x7fce 8c884c10 |
-libc-2.27.so!_sigsetjmp | 0x7fce 8c884b70 |
–libc-2.27.so!__sigjmp_save | 0x7fce 8c884bd0 |
hello!main | 0x400532 |
hello!puts@plt | 0x4004b0 |
hello!exit@plt | 0x4004e0 |
*hello!printf@plt | – |
*hello!sleep@plt | – |
*hello!getchar@plt | – |
ld-2.27.so!_dl_runtime_resolve_xsave | 0x7fce 8cc4e680 |
-ld-2.27.so!_dl_fixup | 0x7fce 8cc46df0 |
–ld-2.27.so!_dl_lookup_symbol_x | 0x7fce 8cc420b0 |
libc-2.27.so!exit | 0x7fce 8c889128 |
使用edb执行hello,说明从加载hello到_start,到call
main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
-
dl_init 调用后,0x601008 和 0x601010 两个地址 的数 据 都 产 生 变 化。
由于编译器无法预测函数的运行时地址,所以需要添加重定位记录。链接器
采用延迟绑定的策略,使用 PLT+GOT 实 现 函 数的 动 态 链 接。 -
PLT 使用 GOT 中的 地 址 跳到目标函数。
-
在 dl_init 调用之前,函数调用都指向 PLT 中的代码逻辑。第一次执行时,为 GOT
赋上相应的偏移量,初始化了函数调用,dl_init
就是做了这件事。此后每次执行时不需要经过如此操作,每次都直接跳转到目标函数的地址。
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的
虚拟地址空间、重定位过程、执行流程、动态链接过程。
链接器的两个主要任务就是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个信号的最终内存地址,并且修改对那些目标符号的引用。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是程序的一个实例,每一个进程都有它自己的地址空间。在地址 空间内,每个进程的地址空间结构的一样的。
进程为用户提供了如下假象:程序好像在独占处理器、内存,处理器无间断 地运行进程,该进程好像是系统中唯一运行的程序。
6.2 简述壳Shell-bash的作用与处理流程
-
Shell 是一个程序,它可以读取用户输入的命令,执行相应的操作。
-
Shell 应用 程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程: 当从 shell
里输入命令(字符串)时,第一个单词是可执行程序的名称,后面 则是参数列表。 -
shell 会传进参数列表来执行对应程序,创建进程,并在进程终止 后回收进程。
读入后,shell 先解析字符串,得到命令行参数(char **argv)。 -
若命令行参数 的最后一个单词是&,表示要在后台执行,shell
可以继续输入命令来做其他工作, 否则则为前台执行,必须等待该进程结束并回收。
6.3 Hello的fork进程创建过程
在终端Terminal中键入./hello 1190201801
耿健,运行的终端程序会对输入的命令行进行解析。
-
hello 不是一个内置的shell命令所以解析之后终端程序判断./hello
的语义为执行当前目录下的可执行目标文件 hello。 -
之后终端程序首先会调用 fork
函数创建一个新的运行的子进程,新创建的子进程几乎父进程相同,但不完全与相同。 -
父进程与子进程之间最大的区别在于它们拥有不同的
PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用 fork
时,子进程可以读写父进程中打开的任何文件。 -
内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
-
父进程和子进程独立 运行,二者结束顺序不可知。父进程负责回收子进程。
所以可以根据 fork 的返回值不同来区分子进程和父进程。
6.4 Hello的execve过程
-
进程调用 execve
函数,该函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化。 -
加载目标程序。换句话说,execve 函数将用目 标 程 序 的 进 程 替 换
当前进程,并传入相应的参数和环境变量,控制转移到新程序的 main 函数。
图 34
6.5 Hello的进程执行
- 由于运行 hello 进程的同时可能在同时运行其他进程,内核并发运行多个进程
需要用到上下文切换来实现多任务。内存为每个进程维持一个上下文,即内核重
新启动一个被抢占的进程所需的状态。假如 hello 没有被抢占,则一条条运行汇编
指令;若被抢占,内核则进行上下文切换:假如从 hello 进程切换到 A 进程,则
-
保存 hello 进程的上下文
-
恢复 A 进程的上下文
-
控制转移给 A 进程
-
hello 进程最初运行在用户模式,但是程序调用了 sleep 函数,调用时产生了用
户态和核心态的转变。进程主动请求休眠,于是产生上述的上下文切换,内核将
控制转移到其他进程,将 hello 进程从运行队列加入等待队列,从用户模式变成内
核模式,并开始计时。当计时结束时,发送中断信号,将 hello 进程从等待队列中
移出,从内核模式转为用户模式。此时 hello 进程就可以继续执行逻辑控制流了。 -
在 getchar 时,实际上也是执行了 read 的调用,此时也产生了如上所述的上下
文切换,进程等待键盘缓冲区的输入。当完成键盘缓冲区到内存的数据传输后,内核从其他进程上下文切换到
hello 进程。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
- ctrl + z 向进程发送信号,使进程挂起。
图 35 输入ctrl + z
- ctrl + c 向进程发送信号, 令进程终止:
图 36
- ps fg 将进程放置后台(前台)运行:
图 37
图 38 执行jobs
图 39 pstree
图 40
图 41 当前进程:
图 42 kill之后的进程:
图 43
- 在进行乱按的时候,第一个字符串会被读入,而其他的字符串会在缓冲区中储存,然后再程序结束的时候,当成命令行命令读入.
图 44
6.7本章小结
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。有四种不同类型的异常,中断,故障,终止,和陷阱。
在操作系统层,内核用ECF提供进程的基本概念,给应用程序两个重要的抽象: 1)
逻辑控制流2)私有地址空间.
应用程序可以创建子进程,等待他们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号.
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
-
逻辑地址:程序代码经过编译后出现在汇编程序中地址。
-
线性地址:逻辑地址经过段机制转化后为线性地址,用于描述程序分页信息
的地址。以 hello 为例,线性地址就是 hello 应该在内存的哪些块上运行。
虚拟地址:同线性地址。 -
物理地址:处理器通过地址总线的寻址,找到真实的物理内存对应地址。是
内存单元的真实地址。以 hello 为例,物理地址就是 hello 真正应该在内存的哪些
地址上运行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理,是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实 体,程序员需要知道并使用它。
图 45
它的产生是与程序的模块化直接有关的。段式管 理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外 还需要主存占用区域表、主存可用区域表。
7.3 Hello的线性地址到物理地址的变换-页式管理
首先查看页表
图 46 页表
然后使用页表的地址翻译:
图 47 翻译地址
线性地址转换成物理地址的过程如下:
首先我们先将线性地址分为 VPN(虚拟页号)+VPO(虚拟页偏移)的形式。
然后再将 VPN 拆分成 TLBT(TLB 标记)+TLBI(TLB 索引)然后去 TLB 缓存里 找所对应的 PPN(物理页号)如果发生缺页情况则直接查找对应的 PPN,找到 PPN 之后,将其与 VPO 组合变为 PPN+VPO 就是生成的物理地址了。
7.4 TLB与四级页表支持下的VA到PA的变换
- 书中的二级页表示例:
图 48
- 在实际的运行的过程中,二级页表是远远不够的,因此我们需要多级页表:
图 49
- 在多级页表中,每一级页表中的一个项都对应下一页表的起始位置,这样不需要的页表就不需要载入内存中,节省空间。
7.5 三级Cache支持下的物理内存访问
此时我们已经得到物理地址,只需要在 cache 寻找即可。与课本高速缓存章节 类似,将物理地址分为 CT(标记)+CI(索引)+CO(偏移量),然后在一级 cache 内部找,如果没有一直向下递归寻找。找到之后将其写入cache, 返回结果。
图 50 整体寻址过程
- 我们先在L1缓存中寻找结果,如果命中,就将缓存中的数据项保存,返回递归向下一级存储结构中寻找。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell
进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的
PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve
函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件
hello 中的程序
-
删除已存在的用户区域
-
映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构。
-
映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so
是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。 -
设置程序计数器(PC),使之指向代码区域的入口点。
图 51
7.8 缺页故障与缺页中断处理
假设当前虚拟地址是 A,现在翻译地址 A 触发了一个缺页异常,导致控制转
移到内核的缺页处理程序,处理程序将执行以下步骤:
-
A
在某个区域结构定义的区域是否合法,若不合法,则产生一个段错误,然后终止这个进程。 -
该内存访问呢是否合法,若访问是不合法的,那么缺页处理程序会触发一个保护异常,终止这个进程。
-
此时内核知道该操作是合法的,那么将把对应页面加载并更新页表,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换
出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动
引起缺页的指令。即可。
7.9动态存储分配管理
printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:
-
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。
-
每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。
-
空闲块可用来分配。空闲块保 持 空 闲,直到它显式地被应用所分配。
-
一个已分配的块保持已分配状态,直到它被释放。
带边界标签的隐式空闲链表
- 堆及堆中内存块的组织结构:
在内存块中增加4B的Header(用于寻找下一个blcok)和4B的Footer(用于寻找上一个block)。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知。
- 隐式链表
对比于显式空闲链表,隐式空闲链表代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表。
图 52
- 空闲块合并
可以利用Footer方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。通过改变Header和Footer四种情况分别进行空闲块合并。
图 53
显示空间链表
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个
pred(前驱)和 succ(后继)指针。
图 54
7.10本章小结
虚拟内存是对主存的一个抽象,它提供三个重要的功能,第一,自动缓存最近使用的存放磁盘上的虚拟地址空间。第二,简化内存管理,进而简化链接,在进程间共享数据,以及程序的加载。
内存的使用和释放时一个容易出错的地方,需要我们进一步理解对它的认识,也要在编程工作中,优化内存管理的策略。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:所有的 IO 设备都 被模 型化
为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux
内核引出一个简单低级的应用接口,称为 Unix I/O。
这就使得所有的输入和输出都能一一个统一且一致的方式来执行:
-
打开文件。
-
Linux
Shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出,标准错误。 -
改变当前文件的位置。
-
读写文件。
-
关闭文件。
8.2 简述Unix IO接口及其函数
-
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O
设备。内核返回一个小 的非 负 整数 ,叫做描述符。描述符在 后 续 对此文件的 所
有 操作 中标识这个文件 ,内核记 录 有 关 这 个 打 开 文件的所有信息。 -
Shell 创建的每个进程都有三个打开的文 件:标准输入,标准输出,标准错误。
-
改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初
始为0。这个文件位置k表示 的 是 从 文 件 开 头 起 始的 字节 偏 移 量。 -
读写文件:
读操作就是从文件复制n>0个字节到内存。
写操作就是从内存中复制n>0个字节到一个文件。
-
关闭文件:内 核 释 放 文件打开时创建的数据结构,无论 一 个 进 程
以何种原因终止时,内核都会关闭所有打开的文件,并且 释 放 他 们 的 内
存资源。
函数:
int open (cahr *filename, int flags, mode_t mode);
int close (int fd);
ssize_t read (int fd, void *buf, size_t n);
ssize_t write (int fd, const *buf, size_t n);
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int
0x80或syscall等.字符显示驱动子程序:从ASCII 到 字 模 库 到 显 示vram。显 示
芯片按照刷新频率逐 行 读 取vram,并通过信号 线 向 液 晶 显 示 器 传 输
每一个点。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘 中断 处 理 子 程 序。接 受按 键 扫 描 码
转成ascii码,保存到系统的键盘缓冲区。
getchar 函数在实现时到底层调用了系统函数 read,通过系统调用read读取存储在 键 盘
缓冲 区中的 ASCII 码直到读到回车符然后 返 回 整 个 字 串,getchar
进行封装,大体逻 辑是读 取字符 串的第一 个字符然后返回。
8.5本章小结
Linux 提供了少量的基于 Unix
i/o模型的系统函数,他们允许应用程序打开,关闭,读写文件,执行io重定向。
而标准io库是基于unix io 实现的,并且提供了一组强大的高 级io 例
程,对于大多数应用程序而言,标准io更加简单,是优于 unix io 的选择,但 是 对
标准io和网络文件的一些互相不兼容的显式,unix io 比标准io 更适合 用 于 网 络
应用程序的编程。
(第8章1分)
结论
伟大而又可怜的HelloWorld,全世界都知道它的名字,但全世界都抛弃了它,它的一生是伟大的,是短小的,是精彩的。
它一生经历的阶段,正如我们人生的一个个阶段,它的一生也许比我们想象的要单调,也许比某些碌碌无为的人的一生更加丰富。让我这个无名的小程序员,斗胆总结hello的一生:
-
hello 的 C 语言程序代码被程序员写成,保存为 hello.c。
-
预处理:将 hello.c 经过预处理器(cpp),将所有以#开头的预处理命令解
析,得到 hello.i -
编译:将 hello.i 经过编译器翻译成汇编程序 hello.s
-
汇编:将汇编程序 hello.s 经过汇编器翻译成二进制文件 hello.o,一种可重
定位目标程序的格式。 -
链接:将 hello.o 与各种库链接称为可执行目标程序 hello
-
用户用 shell 输入参数列表,运行 hello
-
shell 用 fork 和 execve 创建 hello 的子进程。为 hello 创建 地 址区域,进入
程 序 入口进行 初始化,然 后 转 入 main 函数 -
CPU 将一步步执行 hello 的指令,hello 进程与其他进程并发运行。
-
访存:hello 有自己的地址空间,当有访存操作时,MMU 把虚拟地址翻译
成物理地址,通过三级 cache 访问内存。 -
异常:程 序 运行 时可能 会有 许多异常,比如 ctrl+z/c,hello 中的 sleep
函数, 会触发上下文切换,hello 会在用户态和内核态切换。 -
getchar 函 数 会 调用 read 触 发 中 断 异 常,从缓冲区读取字符进行处理。
-
hello 进程结束,shell 回收子进程,hello 进程消失。
学习CSAPP是一个痛苦并快乐的过程,写这篇大作业也是…
虽然这门课很折磨,但我也从中学到了计算机系统深刻的知识,让我换了一个视角来看待我面前这个精美而复杂的计算机,Hello轻轻地来了,正如它轻轻地走,它挥一挥衣袖,不带走一片云彩!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello | hello 的执行程序 |
---|---|
hello.c | hello 的源程序 |
hello.elf | hello 的elf头部 |
hello.i | hello 的预处理程序 |
hello.o | hello 的可重定位的目标程序 |
hello.objdump | hello 的反汇编文件 |
hello.s | hello 的汇编程序 |
helloElf | hello.o 的Elf头部 |
helloo.objdump | hello.o 的反汇编程序 |
列出所有的中间产物的文件名,并予以说明起作用。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello | hello 的执行程序 |
---|---|
hello.c | hello 的源程序 |
hello.elf | hello 的elf头部 |
hello.i | hello 的预处理程序 |
hello.o | hello 的可重定位的目标程序 |
hello.objdump | hello 的反汇编文件 |
hello.s | hello 的汇编程序 |
helloElf | hello.o 的Elf头部 |
helloo.objdump | hello.o 的反汇编程序 |
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统第三版
[2] 百度,知乎,掘金,CSDN。
[3] bilibili cmu网课
[4] gcc–编译的四大过程及作用:https://blog.csdn.net/shiyongraow/article/details/8
1454995
[5] 网络用户. 阿里云. ELF格式文件符号表全解析及readelf命令使用方法. 2018:07-19.
https://www.aliyun.com/zixun/wenji/1246586.html
[6] C语言预处理命令之条件编译. 2009:08-16.
http://www.kuqin.com/language/20090806/66164.html
[7] CSDN. 编译器工作流程详解. 2014:04-27.
https://blog.csdn.net/u012491514/article/details/24590467
(参考文献0分,缺失 -1分)