计算机系统
大作业
题 目 程序人生-Hello’s
P2P
专 业 环境科学与工程
学 号 1182700305
班 级 1827003
学 生 蒋泓轩
指 导 教 师 史先俊
计算机科学与技术学院
2020年3月
摘 要
本文以hello.c文件为初始文件,通过学习计算机系统相关知识,在Linux系统下对其进行预处理、编译、汇编、链接等操作,实现将c文件一步步转变为可执行文件过程。同时并对链接后的hello文件进程管理过程、存储管理过程、IO管理过程进行了分析探讨。
关键词:计算机系统 预处理 编译 汇编
目 录
第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的P2P,020的整个过程。
1.首先在编辑器中编辑hello代码,得到hello程序。
将hello程序编写后,通过预处理器处理以 #开始的预编译指令;
使用编译器将C语言,翻译汇编代码;
使用汇编器将汇编代码翻译成二进制机器语言;
使用链接器将汇编器生成的目标文件外加库链接为一个可执行文件。
以0为开始,通过操作系统的shell去fork一个子进程,调用execve加载执行文件hello,映射虚拟内存,运行程序后将程序载入物理内存,进入 main函数执行代码。当程序运行结束后,shell回收hello进程,内核删除相关数据结构,再次回到0状态。
1.2 环境与工具
1.2.1 硬件环境
CPU:
内存:
硬盘:
1.2.2 软件环境
Windows:
IDE:
Visual Studio 2019、Codeblocks 17.12(自带编译器)
Linux,:
OS版本号:Ubuntu 15.5
IDE:
Codeblocks
1.2.3 开发工具
Windows:
IDE:Codeblocks 17.12;Visual Studio 2019
Linux:
IDE:Codeblocks
1.3 中间结果
文件名称
文件说明
hello.c
hello源文件
hello.i
预处理后文本文件
hello.s
编译得到的汇编文件
hello.o
汇编后的可重定位目标文件
hello
链接后可执行文件
hello.objdump
hello可执行文件反汇编代码
hello.elf
hello的elf文件
helloo.objdump
hello.o(链接前)的反汇编文件
helloo.elf
hello.o的ELF格式
1.4 本章小结
本章对hello进行了简单的介绍,分析了P2P和020的过程,列举了实验用到的环境与工具,介绍了过程中出现的中间产物及其作用。
第2章 预处理
2.1 预处理的概念与作用
预处理是计算机对程序处理的第一步,通过预处理器对.c文件进行初步处理,形成.i文件,之后计算机会对.i文件进行下一步的处理。
计算机用预处理器(Preprocessor)来执行预处理操作,操作的对象为原始代码中以字符#开头的命令。
预处理阶段的作用是让编译器在随后对文本进行编译的过程中,更加方便,减少了编译器的工作,提高效率。
2.2在Ubuntu下预处理的命令
命令 gcc -E hello.c -o hello.i , 输入后如图2.1所示:
图2.1 预处理命令结果
2.3 Hello的预处理结果解析
打开hello.c,我们发现其中包含了三条会被预处理的语句,即:
在hello.i中可以看到三处库文件调用已被解析,如图2.2, 2.3, 2.4。
图2.2 hello.i中对于#include
图2.3 hello.i中对于#include
图2.4 hello.i中对于#include
2.4 本章小结
预处理是计算机对程序操作的第一个步骤,在这个过程中预处理器会对hello.c文件进行初步的解释,对头文件、宏定义和注释进行操作,将程序中涉及到的库中的代码补充到程序中,将初步处理完成的文本保存在hello.i中,让编译器在随后对文本进行编译的过程中,更加方便。
第3章 编译
3.1 编译的概念与作用
编译阶段是编译器对hello.i文件进行处理的过程。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。
将由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,把作为字符串的源程序改造成为单词符号串的中间程序;将词法分析得到的标记流生成一棵语法生成树,判断是否合法;最后将语法生成树转换为依赖于目标机器的汇编代码。
3.2 在Ubuntu下编译的命令
命令 gcc -S hello.i -o hello.s ,输入后如图3.1所示:
图3.1 编译命令结果
3.3 Hello的编译结果解析
图3.2 hello.s头部段
3.3.1
汇编指令
指令
含义
.file
c文件
.text
代码段
.section .rodata
只读数据段rodata
.align 8
声明对指令或者数据的存放地址的对齐方式
.long、.string
声明long、string的变量类型
.globl
全局变量
.type
指定函数类型或对象类型
表3.1 汇编指令及含义
3.3.2
数据
argc作为第一个参数被引入,通过将栈地址保存在%rbp中,%edi保存argc,并在%rbp -20处保存argc。
图3.3 argc 保存位置
2.char argv[ ]
程序中有一个数组argv[
],在hello.s中,main 函数访问argv[1]和argv[2]时,分别通过argv[
]的地址取出数据。将栈地址保存在%rbp中,%rsi保存传入函数的第二个参数,如图可知argv[ ]的首地址在%rbp-32处。
图3.4 数组argv[ ] 保存位置
3.int i
编译器将局部变量i存储在寄存器或者堆栈中,在此hello.s中,编译器将i存储在%rsp-4处。
图3.5 局部变量i保存位置
4.字符串
程序中有两个字符串,即“Usage: Hello 学号 姓名!\n"和"Hello %s %s\n”。两个printf传入的输出格式化参数,都存放在只读数据段.rodata中。
图3.6 字符串保存位置
3.3.3
赋值 i
i是保存在栈中的局部变量,位置为%rbp-4,这里直接用mov语句对i进行赋值为0。
图3.7 局部变量i赋值
3.3.4
算数操作
1.addl 指令的i++
采用add指令对i进行增加,在汇编语言中被解释为被操作数加一,即可理解为i++。
图3.8 addl指令操作
2.leaq指令
addl循环需要对LC1处字符串进行打印,使用leaq指令,计算LC1的段地址%rip+.LC1并传递给%rdi。
图3.9 leaq指令操作
3.3.5
关系操作
进行关系操作的指令如下:
指令
基于
解释
CMP
S1, S2
S2-S1
比较设置条件码
TEST
S1, S2
S1&S2
测试设置条件码
SET…D
D=…
按照…将条件码设置D
J…
——
根据…与条件码进行跳转
hello.s中涉及的关系运算如下:
1.判断argc不等于4,即argc!=4
hello.s中使用cmpl $4,
-20(%rbp)计算argc-4的值,为之后je的跳转做准备。
图3.10 argc!=4的判断
hello.s中使用cmpl $7, -4(%rbp),计算i-7的值,为之后jle跳转做准备。
图3.11 i<8汇编解释
3.3.6
控制转移
程序中控制转移共有两处:
1.对argv是否等于4的处理
当argv不等于4的时候执行程序段中的代码。首先使用cmpl $4, -20(%rbp),设置条件码,如果为0,说明argv-4=0 argv==4,则直接跳转到.L2;否则按顺序执行下一条语句。其过程为if语句。
图3.12 对argv是否等于4的处理
2.对i<8的处理
.L3为比较代码,使用cmpl $7, -4(%rbp)进行比较,如果i<=7,则跳入.L4中进行循环,直到变量i==8为止,循环结束。
图3.13对i<8的处理
3.4 本章小结
本章介绍了编译的相关知识,介绍编译器,通过编译命令将
.i文件转换为汇编语言的 .s文件,并对编译的结果进行了相对具体解析。
第4章 汇编
4.1 汇编的概念与作用
汇编指的是汇编器将hello.s翻译成机器语言指令,打包成可重定位目标文件保存在目标文件hello.o中。hello.o文件是一个二进制文件,包含hello程序执行的机器指令。
汇编的作用是将在hello.s中保存的汇编代码翻译成可以供机器执行的二进制代码。使之在链接后能够被计算机直接执行。
4.2 在Ubuntu下汇编的命令
命令 gcc -c
hello.s -o hello.o输入后如图4.1所示:
图4.1 编译命令结果
4.3 可重定位目标elf格式
使用命令 readelf -a hello.o > hello_o_elf.txt,读取hello.o文件的ELF格式至hello_o_elf_txt中。
4.3.1 ELF头
ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4.2 hello.o ELF头
4.3.2 节头部表
节头部表包括节的全部信息,各个节的名称及内容如下:
图4.3 hello.o 节头部表
节名称
包含内容
.text
已编译程序的机器代码
.rela.text
一个.text节中位置的列表,链接器链接其他文件时,需修改这些内容
.data
已初始化的全局和静态C变量
.bss
未初始化的全局和静态C变量和所有被初始化为0的全局或静态变量
.rodata
只读数据段
.comment
包含版本控制信息
.note.GNU-stack
包含注释信息,有独立的格式
.symtab
符号表,存放程序中定义和引用的函数和全局变量信息
.strtab
字符串表,包括.symtab和.debug节中的符号表以及节头部中的节名字
.shstrtab
包含节区名称
4.3.3 重定位信息
重定位是将EFL文件中的未定义符号关联到有效值的处理过程。在hello.o中,对printf,exit等函数的未定义的引用和全局变量替换为该进程的虚拟地址空间中机器代码所在的地址。
图4.4 hello.o 重定位节
4.3.4 符号表
符号表是用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。
图4.5 hello.o 符号表(.symtab)
4.4 Hello.o的结果解析
使用 objdump -d -r hello.o > helloo.objdump获得反汇编代码。
图4.6 反汇编命令结果
图4.7 反汇编main函数
通过比较我们发现反汇编代码helloo.objdump和hello.o之间主要差别如下:
4.4.1 分支转移
反汇编代码跳转指令的操作数使用的不是段名称如.L2,段名称只是在汇编语言中便于编写的助记符,在汇编成机器语言之后使用地是确定的地址。
图4.8 分支转移结果
4.4.2 函数调用
在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。
因为hello.c中调用的函数都是共享库中的函数,最终需要通过link后才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0,然后在.rela.text节中为其添加重定位条目,在链接后再进一步确定。
4.4.3 全局变量
在.s文件中,访问.rodata,使用段名称+%rip,在反汇编代码中0+%rip,因为.rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
图4.9 全局变量
4.5 本章小结
本章介绍了hello从hello.s到hello.o的汇编过程,通过使用objdump得到反汇编代码,查看hello.o的ELF格式并将helloo.objdump与hello.s进行比较分析,观察这其中需要进行的转换。
第5章 链接
5.1 链接的概念与作用
链接是通过链接器(Linker)将文件中调用的各种函数跟静态库及动态库链接,并将它们打包合并形成可执行文件的过程。可执行文件可以被加载器加载到内存并执行。
通过链接可以实现将头文件中引用的函数并入到程序中,解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。完成程序中各目标文件的地址空间的组织,这可能涉及重定位工作。
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.3 可执行目标文件hello的格式
使用readelf -a hello > hello.elf读取hello的ELF格式至hello.elf中。分析hello的ELF格式,使用readelf命令查看各段的基本信息。
5.3.1. ELF 头
hello.o的ELF以一个16进制序列:7f 45 4c
46 02 01 01 00 00 00 00 00 00 00 00 00作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为10字节和字节顺序为小端序。ELF头的大小为64字节;目标文件的类型为EXEC(可执行文件);机器类型为Advanced Micro Devices
X86-64;节头部表的文件偏移为0;以及节头部表中条目的大小,其数量为25。
图5.2 ELF头
5.3.2. 节头表
图5.3 连接后hello的节头表
可以看到hello文件中的节的数目比hello.o中多了很多,说明在链接过后有新文件添加进来。
5.3.3. 程序头表
图5.4 连接后hello的程序头表
5.3.4. 版本信息
图5.5 连接后hello的版本信息
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
图5.6 hello的虚拟地址空间
5.5 链接的重定位过程分析
使用命令objdump -d -r hello >
hello.objdump 得到hello的反汇编文件。
图5.7 hello反汇编命令结果
图5.8 hello.o的反汇编内容
图5.9 hello反汇编内容
结合hello.o的重定位项目,经比较,hello.o反汇编结果与hello反汇编结果在以下几个方面存在不同:
1.函数个数
使用ld命令链接时,hello
增加了许多节和被调用的函数。
2.函数调用
链接器解析重定条目时已经进行了重定位。此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。
3…rodata段引用
链接器解析重定条目时需要对.rodata的重定位,即printf中的两个字符串,.rodata与.text节之间的相对距离确定,链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。这里以计算第一条字符串相对地址为例说明计算相对地址的算法。
5.6 hello的执行流程
使用edb执行hello,查看从加载hello到_start,到call main,以及程序终止的所有过程。下表列出其调用的程序名称与各个程序地址。
程序名称
程序地址
ld-2.27.so!_dl_start
0x7fe4494cd093
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
hello!main
0x400532
hello!puts@plt
0x4004b0
hello!exit@plt
0x4004e0
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
5.7 Hello的动态链接分析
动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址,在汇编代码中也无法像静态库的函数那样体现。
hello程序对动态链接库的引用,基于数据段与代码段相对距离不变这一个机理,因此代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。
5.8 本章小结
本章分析了链接过程中对程序的处理。分析了hello 的ELF格式,经过链接,ELF可重定位的目标文件变成可执行的目标文件,链接器会调用动态库将地址进行重定位,保证寻址的正确进行。而静态库将直接写入代码,动态链接过程相对复杂一些,涉及共享库的寻址。
第6章 hello进程管理
6.1 进程的概念与作用
进程是对执行中的程序进行操作运行的一次活动,每一个进程都有它自己的地址空间,包括代码段、数据段、和堆栈区。代码段存储CPU执行的代码,数据段存储变量和进程执行期间使用的动态分配的内存,堆栈区存储活动过程调用的指令和本地变量。
进程的作用是每次用户向shell输入一个可执行目标文件的名字并运行时,shell就会创建一个新的进程,然后在这个进程下运行这个可执行目标文件。形成一个独立的逻辑控制流和一个私有的地址空间,好像程序可以独使用处理器,好像程序独占整个内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个命令解释器,它解释由用户输入的命令并把它们送到内核。shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。其处理流程为:
shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序。
shell在可执行程序的目录列表里寻找这些应用程序。
如果键入的命令不是一个内部命令并且在列表里没有找到这个可执行文件,将会显示一条错误信息。
4.如果成功找到命令,该内部命令或应用程序将被分解为系统调用并传给内核。
6.3 Hello的fork进程创建过程
shell通过fork创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。
子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
子进程与父进程最大的区别就是有不同的pid。fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0。
图6.1终端中fork hello过程
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数的功能为:加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时,execve会返回到调用程序,即execve调用一次且从不返回。
图6.2 进程的地址空间
6.5 Hello的进程执行
6.5.1. 上下文信息
上下文是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
6.5.2. 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.3. 用户模式与内核模式
一个寄存器通常提供两种模式:
1.当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;
2.当有设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
从图6.3中可以比较清晰的看出CPU是如何在程序间进行切换的。假设hello进程在sleep之前一直在顺序执行。在执行到sleep函数的时候,切换到内核模式,将hello进程挂起,然后切换到用户模式执行其它进程。当发生一个中断,切换到内核模式,继续运行之前被挂起的进程。最后切换回用户模式,继续运行hello进程。
图6.3 hello与其他进程间的切换
6.6 hello的异常与信号处理
hello执行过程中会出现的异常:
1.中断:信号SIGTSTP,默认行为是停止直到下一个SIGCONT
2.终止:信号SIGINT,默认行为是 终止
图6.4展示了程序正常运行的结果:程序执行完后,进程被回收,再按回车键退出程序。
图6.4 hello正常运行的结果
图6.5 展示了运行时乱按时的结果,乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入。
图6.5 hello运行时乱按的结果
图6.6展示了运行时按Ctrl+C。父进程收到SIGINT信号,终止hello进程,并且回收hello进程。
图6.6 hello运行时按Ctrl+C的结果
图6.7展示了运行时按Ctrl+Z后运行ps命令。按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程。运行jobs命令列出当前shell环境中已启动的任务状态。
图6.7 hello运行时按Ctrl+Z的结果
图6.8展示了运行时按下Ctrl+Z后运行pstree命令。pstree命令是以树状图显示进程间的关系。
图6.8 hello运行时按Ctrl+Z后执行pstree的结果
图6.9展示了运行时按下Ctrl+Z后运行fg命令将进程调到前台的结果,程序继续执行至回车后正常结束。
图6.9挂起
hello后输入fg的结果
6.7本章小结
本章中主要介绍了进程的概念和作用,介绍了shell的作用和处理流程,介绍了执行hello时的fork和execve过程。分析了hello的进程执行并展示了hello的异常与信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1.
逻辑地址
逻辑地址是指由CPU产生的与段相关的偏移地址部分。逻辑地址由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。即hello.o里相对偏移地址。
7.1.2.
线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
7.1.3.
虚拟地址
虚拟地址是程序保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
7.1.4.
物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段选择符和段内偏移量。段选择符是由一个16位长的字段组成,称为段选择符。最初8086处理器的寄存器是16位的,为了能够访问更多的地址空间但不改变寄存器和指令的位宽,所以引入段寄存器,8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
分段功能分为实模式和保护模式:
在实模式中,逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存;
在保护模式中,分段机制就可以描述为:通过解析段寄存器中的段选择符在段描述符表中根据Index选择目标描述符条目,从目标描述符中提取出目标段的基地址,最后加上偏移量共同构成线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址到物理地址之间的转换通过分页机制完成。分页机制是对虚拟地址内存空间进行分页。
Linux系统有自己的虚拟内存系统,
Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。
系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传输的单元,在linux下每个虚拟页大小为4KB,类似地,物理内存也被分割为物理页,虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
图7.1 使用页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。
如果TLB未命中,则一步步向页表中查询分级页表的起始地址,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。
7.5 三级Cache支持下的物理内存访问
在已经获得了物理地址VA情况下,使用CI进行索引,分别匹配CT。如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出数据返回。如果没有匹配成功或者匹配成功但是标志位是0,则不命中(miss),向下一级缓存中查询数据。
7.6 hello进程fork时的内存映射
fork函数被调用时,内核分配给hello一个唯一的PID。为了创建虚拟内存,fork创建了当前进程的mm_struct(内存描述符)、vm_area_struct(区域结构描述符)和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有,这样就只能在写入时复制。
7.7 hello进程execve时的内存映射
execve函数调用保存在内核区域的启动加载器代码,在当前进程中加载并运行hello,用hello程序有效地替代了当前程序。其步骤如下:
删除当前进程虚拟地址的用户部分中的已存在的区域结构。
为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
创建新的共享区域。将hello程序与共享对象libc.so链接,然后再映射到用户虚拟地址空间中的共享区域内。
设置当前进程上下文的程序计数器PC,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
1.段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)。
2.非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
3.如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
1.隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
2.显式分配器:要求应用显式地释放任何已分配的块。
7.10本章小结
本章讨论了存储器地址空间,虚拟地址、物理地址、线性地址、逻辑地址的概念,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:Unix I/O接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
8.2.1. Unix
I/O接口统一操作
打开文件
应用程序通过要求内核打开相应的文件,想要访问一个I/O设备时,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出,标准错误。
改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个k是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置。
读写文件
读操作是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,将会触发EOF条件。
写操作是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件
当完成对文件的访问后,通知内核关闭此文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
8.2.2. Unix
I/O函数
1.int open(char* filename,int flags,mode_t
mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。
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函数功能:接受字符串指针数组fmt,然后将匹配到的参数按照fmt格式输出。图8-1是printf的代码,printf内部调用了两个外部函数,一个是vsprintf,还有一个是write。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数通过调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。除非n<0,否则返回时将直接返回buf的第一个元素。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简述了Linux的I/O设备管理机制,Unix I/O接口及函数,简单叙述了printf函数和getchar函数的实现。
结论
对于一个hello程序,其“简单”流程如下:
1.编写:
通过编辑器编写hello.c。
2.预处理:
预处理器将hello.c调用的库函数展开,合并到hello.i文件。
3.编译:
编译器将hello.i编译成汇编文件hello.s。
4.汇编:
汇编器将hello.s汇编为可重定位目标文件hello.o。
5.链接:
链接器将hello.o与动态链接库进行链接,成为可执行目标程序hello。
6.运行:
在shell中输入:./hello 1182700305 蒋泓轩 1s!
shell进程调用fork函数为hello创建子进程。shell进程调用execve启动加载器,映射虚拟内存,进入hello程序入口后,将程序载入物理内存,然后进入 main函数执行hello。
8.执行指令:
CPU为其分配时间片。在时间片中,hello享有CPU资源,顺序执行自己相应的控制逻辑流。如果运行中键入Ctrl + C或Ctrl + Z,则调用shell的信号处理函数分别停止、挂起。
9.访问内存:
MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:
printf会调用malloc向动态内存分配器申请堆中的内存;
12.结束:
shell进程回收hello进程,内核删除为hello进程创建的所有数据结构。
附件
下表列出所有hello分析过程中的中间产物的文件名,和相应的内容。
文件名称
文件说明
hello.c
hello源文件
hello.i
预处理后文本文件
hello.s
编译得到的汇编文件
hello.o
汇编后的可重定位目标文件
hello
链接后可执行文件
hello.objdump
hello可执行文件反汇编代码
hello.elf
hello的elf文件
helloo.objdump
hello.o(链接前)的反汇编文件
helloo.elf
hello.o的ELF格式
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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] ELF文件格式解析https://blog.csdn.net/mergerly/article/details/94585901.
[8] 内存地址转换与分段https://blog.csdn.net/drshenlei/article/details/4261909
[9] Linux下逻辑地址、线性地址、物理地址详细总结https://blog.csdn.net/freeelinux/article/details/54136688
[10] printf
函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
[11] Linux进程的睡眠和唤醒 https://blog.csdn.net/shengin/article/details/21530337