摘 要
一个程序的运行,看似简单,却包含许多复杂的过程:从预处理到编译再到汇编和链接;从内存的加载,到进程的执行。本文通过研究hello程序的一生,在linux系统下梳理hello程序的产生到运行结束,从编译处理、进程管理、内存管理、异常控制流等几个阶段进行探讨,更深入的让人了解hello程序执行的完整过程
**关键词:**hello程序;计算机系统;linux;汇编
**
**
目 录
6.2 简述壳Shell-bash的作用与处理流程… - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理… - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理… - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换… - 11 -
7.7 hello进程execve时的内存映射… - 11 -
**
**
第1章 概述
1.1 Hello简介
Hello程序的P2P过程(即from process to process),是指编译器对源程序hello.c文件进行处理,经过预处理、编译、汇编、链接四个阶段,最终形成可执行文件的过程。hello.c文件在预处理阶段通过预处理器生成hello.i文件;hello.i文件在编译器作用下编译产生文本文件hello.s;hello.s文件在汇编器作用下产生二进制的可重定位目标文件hello.o;最终在链接阶段,hello程序调用printf函数与hello.o文件结合生成可执行文件hello。
图1 编译系统
Hello程序的020过程(即from zero to zero),是指程序从零开始,shell程序fork子进程,函数由program转换为process,调用execve函数运行hello.c程序映射到虚拟内存(删除当前虚拟存储空间已有的数据结构),创建新的区域结构,将hello的信息在进程开始运行时分配载入物理内存,进入main函数执行代码。CPU为运行的hello程序分配时间片执行逻辑控制流。在程序完成后,父进程对hello进程(僵死进程)进行回收,内核删除相关的数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk
软件环境:Macos Catalina10.15.7; VMware 11.5.5;Ubuntu18.04 LTS 64位
开发工具:GDB;EDB;GCC
1.3 中间结果
hello.i(预处理得到的文本文件)
hello.s(编译得到的文本文件)
hello.o(汇编得到的可重定位目标文件)
hello(链接生成的可执行文件)
hello.elf(可重定位目标文件ELF格式)
hello2.elf(可执行文件ELF格式)
hello_o.txt(可重定位目标文件的反汇编代码)
hello.txt(可执行目标文件的反汇编代码)
1.4 本章小结
本章为论文的概述部分,主要对hello程序的P2P及020过程进行了分析阐述,注明了本次实验的实验环境及工具,并且列举了实验的中间产物。
**
**
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的hello.c程序。如hello.c程序第一行的#include <stdio.h>命令告诉与处理器读取系统头文件stdio.h的内容,并把它插入到程序文本中,得到另一个c程序,通常为.i文件。[1]
作用:C语言中提供多种预处理功能,如宏定义、文件包含、条件编译等。
\1. 宏定义:将宏替换为文本。
\2. 文件包含:文件包含语句的功能是把制定的文件插入该语句行位置,从而把制定的文件和当前源程序连成一个源文件。
\3. 条件编译:使编译器按照不同的条件去编译不同的代码。
2.2在Ubuntu下预处理的命令
预处理指令:linux> gcc –E hello.c–o hello.i
图2-1 预处理命令
2.3 Hello的预处理结果解析
生成的预处理文件部分内容如下图所示:
图2-2 预处理文件部分内容(1)
图2-3 预处理文件部分内容(2)
生成的预处理文件hello.i共3105行,开头与结果如上图2-2及2-3所示,可以观察到与源程序相比,预处理文件的内容大大增加(多为头文件<stdio.h> <unistd.h> <stdlib.h>内容);同时未观察到宏定义内容。这与预处理的作用(预处理、宏定义)是对应的。
2.4 本章小结
本章主要描写c语言程序的预处理阶段。阐述了预处理阶段的定义及意义,在观察预处理文件内容的同时对预处理的作用有进一步的体会。
**
**
第3章 编译
3.1 编译的概念与作用
概念及作用:编译器(ccl)将文本文件hello.i汇编文本文件hello.s的过程。编译程序读取源程序,进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。[1]
3.2 在Ubuntu下编译的命令
图3-1 编译命令
3.3 Hello的编译结果解析
图3-2 编译文件部分内容
3.3.1 c语言数据类型
1.变量(全局/局部/静态):整型变量argc、字符串数组argv、局部整型变量i
由图3-3参数argc和argv存放在寄存器%edi和%rsi中,然后压入栈中;由后缀可知其类型分别为int类型(4字节)及字符指针类型(8字节)。
图3-3 汇编代码部分内容1
由图3-4局部整形变量i作为循环控制变量,被存放在栈中;数据类型为int类型。
图3-4 汇编代码部分内容2
由图3-5源代码中printf函数的内容以两个字符串的形式存放在rodata段中。其中中文字符在汇编代码中以utf-8编码表示。其余数据在汇编代码中以立即数的形式表示。String1为默认打印的“用法: Hello 学号 姓名 秒数!”,string2则根据输入参数决定。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jG1iQjDW-1624878684681)(/Users/zhouziyi/Desktop/二学位/21春季学期/计算机系统/PA-120E036101-周子仪/图片 9.png)\
图3-5 汇编代码部分内容3
3.3.2 赋值操作
Move指令表示将后者值传递给前者,由图3-6指令movl $0, -4(%rbp)得到,i初始化为0(立即数形式),存储在栈上%rbp-4指向位置;move后缀l,表示执行4字节操作,对应变量为int类型。
图3-6 汇编代码部分内容4
3.3.3 算术操作
Add既可以表示加法算术操作(图3-7),也可以表示地址的偏移(图3-8)。Sub表示减法操作。
图3-7 汇编代码部分内容5
图3-8 汇编代码部分内容6
3.3.4 关系操作
cmp指令表比较,test指令表测试,源代码中涉及到的argc!=3判断和循环判断i<10均通过比较指令实现。
图3-9 汇编代码部分内容7
3.3.5 数组/指针
argv即为字符指针,存储一个字符数组的开头地址。hello.s中argv的首地址存放在寄存器%rsi中,后来被放在栈中,便于使用。
图3-10 汇编代码部分内容8
3.3.6 函数操作
函数是一种过程,通过跳转到特定代码段执行特定函数后再返回来实现功能。函数调用包含着以下操作:
1.传递控制:将控制传递给被调用的函数,才能执行函数代码。在函数调用结束后,被调用函数要将控制返回给调用函数以继续执行接下来的代码。
2.传递数据:函数调用需要传递参数给被调用函数。传递的参数存放在寄存器中。同时,被调用函数也要能够返回一个值给调用函数,这个值也在寄存器中。
3.分配和释放内存:函数的执行需要空间。函数开始执行时,它会分配一定空间,在结束时,也要将这些空间释放。
Hello程序中调用了main函数、printf函数、exit函数、sleep函数、getchar函数。
以Main函数为例:
1.传递控制:main函数被系统启动函数调用。
2.传递数据:系统向main函数传递argc(存放在%rdi),argv(存放在%rsi),函数正常结束会返回0.
3.分配和释放内存:main函数使用栈指针,同时使用栈帧%rbp来记录使用情况。
3.4 本章小结
本章分析了hello程序汇编文本文件hello.s的内容。详细分析了编译器处理hello.c源程序的具体操作,进一步地了解编译的原理及操作。
**
**
第4章 汇编
4.1 汇编的概念与作用
概念与作用:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable Object program)的格式,并将结果保存在目标文件hello.o中;hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码,如果在文本编辑器中打开hello.o文件,会看到一堆乱码。[1]
4.2 在Ubuntu下汇编的命令
图4-1 汇编命令
4.3 可重定位目标elf格式
图4-2 ELF格式获取
图4-3 ELF开头部分
如图4-3所示,Elf文件开头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括elf头的大小、目标文件的类型、机器类型、节头部表的文件偏移以及节头部表中的条目的大小与数量。Elf头剩下的部分包含elf头的大小、目标文件的类型(如可重定位、可执行或可共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。[1]
图4-4 ELF节头部分
一个典型的elf可重定位目标文件如图4-4所示,包含下面几个节:
.text:已编译程序的机器代码。
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局和静态c变量。局部c变量在运行时被保存在栈中。
.bss:未初始化的全局和静态c变量,以及所有的被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.rela.text:一个.text中位置的列表
Hello的重定位节包括rela.text节、rela.eh_frame节。
各列包含以下信息:
1.Offset:需要进行重定向文件在.text或者.data中的偏移量。
2.Info:包括symbol和type两部分, symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型。
3.Type:重定向的目标类型。
4.Sym.Name:重定向到目标名称。
5.Addend:重定向位置的辅助信息。
图4-5 ELF重定位节部分
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
图4-6 反汇编命令
反汇编代码的跳转指令的操作数从助记符变成了具体地址,操作数均为16进制;反汇编代码中涉及到的控制转移,如跳转操作和函数调用,其操作数均为目标指令的地址,而汇编代码中操作数是段名称或函数名称。汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式。
图4-7 反汇编部分代码内容
4.5 本章小结
本章讨论了hello从hello.s到hello.o的汇编过程,通过分析可重定位文件的ELF文件和利用objdump得到的反汇编代码,对二进制可重定位文件有了进一步了解,进一步了解从汇编语言映射到机器语言这一过程。
**
**
第5章 链接
5.1 链接的概念与作用
(以下格式自行编排,编辑时删除)
注意:这儿的链接是指从 hello.o 到hello生成过程。
概念:hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(1d)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件 (或者简称为可执行文件),可以被加载到内存中,由系统执行。[1]
作用:由链接程序将编译后形成的一组目标模块以及它们所需要的库函数链接在一起,形成一个完整的载入模型。链接主要解决模块间的相互引用问题。分为地址和空间分配,符号解析和重定位几个步骤。
5.2 在Ubuntu下链接的命令
图5-1 链接命令
5.3 可执行目标文件hello的格式
图5-2 ELF格式
图5-3 ELF文件头
图5-4 ELF文件头部表开头
图5-5 ELF重定位节1
图5-5 ELF重定位节2
在hello的elf格式文件中有段名、类型、起始地址、偏移量、大小、对齐等信息。其中节头部表条目增多,对节信息进行了声明。
5.4 hello的虚拟地址空间
图5-6 edb打开hello
data Dump可以查看内存区域的值,这一块内存区域应该是伴随程序分配的内存区域。
图5-6 data dump窗口
图5-7 symbol窗口
5.5 链接的重定位过程分析
图5-8 hello反汇编代码部分内容
分析hello与hello.o的区别如下:
1.在hello.o中保存的都是相对偏移地址;而在hello中保存的是虚拟内存地址,进行了重定位。
2.hello可执行目标文件中多出了.init节和.plt段。.init节用于初始化程序执行环境;.plt段则是程序执行时的动态链接
3.在hello中链接加入了在hello.c中用到的函数
5.6 hello的执行流程
hello执行流程如下:
(1)ld-linux-x86-64.so!_dl_start
(2)ld-linux-x86-64.so!_dl_init
(3)hello!_start
(4)hello!__libc_csu_init
(5)hello!_init
(6)libc.so!_setjmp
(7)hello!main
(8)hello!puts@plt
(9)ld-linux-x86-64.so!_dl_runtime_resolve_xsave
(10)ld-linux-x86-64.so!_dl_fixup
(11)ld-linux-x86-64.so!_dl_lookup_symbol_x
(12)hello!exit@plt
(13)libc.so!exit
(14)hello!_fini
5.7 Hello的动态链接分析
图5-9 dl_init之前.got.plt节的内容
图5-10 dl_init之后.got.plt节的内容
地址0x404000处的值改变,此处为.got.plt节。
动态库是在进程启动的时候加载进来的,加载后,动态链接器需要对其作一系列的初始化,如符号重定位(动态库内以及可执行文件内)。因此为了节约时间对函数的重定位延迟进行,使得可以对一些动态库里包含的很多全局函数之中的很小一部分使用到的、执行到的进行重定位。等到第一次发生对该函数的调用时才进行符号绑定,即延迟绑定。
延迟绑定的实现步骤有:
1.建立一个.got.plt表,该表用来放全局函数的实际地址,但最开始时,里面放的不是真实的地址而是一个跳转。
2.对每一个全局函数,链接器生成一个与之相对应的影子函数。
而所有对 (被调用函数名) 的调用,都换成对{(被调用函数名)@plt} 的调用。而这个{(被调用函数名)@plt}的的第一条指令会实现先直接从 got.plt 中去拿真实的函数地址,如果已经之前已经发生过调用,got.plt 就已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 {(被调用函数名)@plt} 中的第二条指令,这就使得当执行第一次调用时, {(被调用函数名)@plt}中的第一条指令其实什么事也没做。
直接继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号作为参数传给 _init(),而 _init() 这个函数则用于把 被调用函数 进行重定位,然后把结果写入到 got.plt 相应的地方,最后直接跳过去该函数。
5.8 本章小结
本章分析了可重定位目标文件到可执行目标文件的过程。包括两个重要过程:链接、重定位。通过分析ELF文件,同时利用edb查找对应关系,更深入的了解程序在链接和重定位中发生的变化。
**
**
第6章 hello进程管理
6.1 进程的概念与作用
概念及作用:进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互型应用级程序,它代表用户运行其它程序。Shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,代表用户运行程序。
流程:
从终端读入输入的命令。
将输入字符串切分,分析输入内容,解析命令和参数。
如果命令为内置命令则立即执行,如果不是内置命令则创建新的进程调用相应的程序执行。
在程序执行期间始终接受键盘输入信号,并对输入信号做相应处理。
6.3 Hello的fork进程创建过程
Shell通过fork()函数创建一个子进程,该子进程具有相同但独立的地址空间,父进程与子进程具有相同的用户栈,相同的本地变量值,相同的堆,相同的全局变量值。fork()函数会返回两次,一次是在父进程中,他返回子进程的PID,在子进程中他返回0.
6.4 Hello的execve过程
Execve函数家在并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp,execve函数调用一次永不返回。
在execve加载了hello之后,调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型:
int main(int argc,char **argv,char **envp);
当main开始执行时,用户栈的组织结构如图所示:
图6-1 新程序开始时用户栈典型组织结构
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象构成。
时间片:一个进程执行它的控制流的一部分的每一个时间段。
调度:在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。
用户态:进程运行在用户模式中时,不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个I/O操作,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
核心态:进程运行在内核模式中时,可以执行指令集中的任何指令,并且可以访问内存中的任意位置。
用户态与核心态转换:程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再改回用户态。
图6-2 上下文切换
6.6 hello的异常与信号处理
图6-3 正常运行
图6-4 输入任意字符结果
程序正常运行结果如图6-3,图6-4显示了程序运行过程中任意输入不会影响程序的运行,仅会在终端中显示输入的字符。
图6-5 输入ctrl z结果
输入ctrl z之后, hello进程暂时挂起。(如图6-5)
图6-6 使用ps指令观察进程
由图6-6,hello进程并未被回收。
图6-7 使用fg恢复进程
图6-8 当前作业
再运行一个新的hello进程2并挂起,输入fg指令,可以看到进程2继续执行。利用ctrl z挂起进程,使用jobs查看当前作业,可以看到之前对两个hello进程。
图6-9 输入ctrl c结果
恢复第二个进程,输入ctrl c,进程终止,查看当前作业发现只有一个进程。
图6-9杀死进程后查看当前进程
再使用kill命令杀死进程,最后查看当前进程发现无进程执行。
6.7本章小结
本章介绍了进程的概念和作用。与预处理、编译、汇编等以单个文件为中心进行的处理相比,进程管理要更复杂一些。本章演示了各类操作,分析了进程运行中的异常控制流、信号处理情况,同时对于进程及上下文切换进行了阐述。
**
**
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在汇编程序中地址。Hello.o文件中的地址即为逻辑地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。即为hello中的虚拟地址。
虚拟地址:带虚拟内存的系统中,CPU从一个地址空间中生成虚拟地址,即为虚拟地址空间。Hello的反汇编代码中使用的就是虚拟地址。
物理地址:物理内存的实际地址。Hello实际执行时存储在内存中的地址就是物理地址。[2]
7.2 Intel逻辑地址到线性地址的变换-段式管理
在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在段式存储管理系统中,为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。 [2]
图7-1 段式管理的地址变换
段式管理是实现逻辑地址到线性地址转换机制的基础,段的特征有段基址、段限长、段属性。这三个特征存储在段描述符中,用以实现从逻辑地址到线性地址的转换。
7.3 Hello的线性地址到物理地址的变换-页式管理
在页式系统中,指令所给出的地址分为两部分:逻辑页号和页内地址。
原理:CPU中的内存管理单元(MMU)按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址(见图4-4)。
逻辑页号,页内偏移地址->查进程页表,得物理页号->物理地址:
图7-2 页式管理的地址变换
上述过程通常由处理器的硬件直接完成,不需要软件参与。通常,操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存:
第一次用来查找页表将操作数的 逻辑地址变换为物理地址;
第二次完成真正的读写操作。
7.4 TLB与四级页表支持下的VA到PA的变换
图7-3使用页表的地址翻译
图7-4 虚拟地址中访问TLB的组成部分
TLB(翻译后备缓冲器)是一个位于MMU中的小的虚拟地址的具有较高相联度的缓存,其每一行都是一组由数个PTE组成的块,TLB极大地减小了CPU访问PTE的开销,且能实现虚拟页面向物理页面的映射,同时对于页面数很少的页表可以完全包含在TLB中。
图7-5 一个两级页表层次结构
系统实际上会采用多级页表的方式来进行地址翻译,这样能有效压缩页表大小。CR3确定一级页表的基址。一级页表中的每个PTE负责映射虚拟地址空间中的一个4MB的片,每一片都是由1024个连续的页面组成的。若该片中的每个页面都未被分配,则该一级PTE为空,否则一级PTE指向一个二级页表的基址。二级页表中的每个PTE都负责映射一个4KB的虚拟内存页面。三级和四级页面按同样的方式映射。
此时虚拟地址被分成4个VPN和一个VPO,每个VPNi都是一个到第i级页表的索引,第j级页表中的每个PTE指向j+1级某个页表的基址。在翻译虚拟地址时通过四级页表查询到PPN,与VPO结合成PA。具体可见图7-4-2 。
7.5 三级Cache支持下的物理内存访问
当CPU请求访问的虚拟地址VA被翻译为物理地址PA后,高速缓存根据组索引CI找到缓存组,在缓存组中根据标记CT与缓存行中的标记位匹配。如果匹配成功且有效位为1,则命中,按照块偏移CO访问指定数据。否则不命中,向下一级缓存中请求数据。如果下一级缓存中已缓存所需数据,那么按替换策略决定本级缓存的牺牲快进行替换,否则继续向下一级存储中寻找数据。
图7-6 Core i7地址翻译的概况
7.6 hello进程fork时的内存映射
fork函数为新进程创建虚拟内存时需:
1.创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.
2.两个进程中的每个页面都标记为只读
3.两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)
在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存
随后的写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
execve函数在当前进程中建在并运行包含在可执行目标文件ahello中的程序,用hello程序有效代替了当前长须,如图7.12所示,记载并运行hello程序需要以下几个步骤:
1.删除已存在的用户区域:
2.删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域:
为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域:
hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC):
execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页:DRAM缓存不命中。
一旦发生了缺页,就会出发一个缺页异常,缺页异常会调用内核中的缺页异常处理程序,从磁盘取来我们需要的页。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,此时就可以正常翻译VA了。
7.9动态存储分配管理
图7-7 堆块的格式
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。[1]
分配器有两种基本风格,两种风格都要求应用显式的分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器,要求应用显式地释放任何已分配的块。
隐式分配器,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
7.10本章小结
本章主要研究了虚拟地址空间与物理地址空间相互映射变换的关系,以及虚拟地址到物理地址的寻址方式,分析了进程的内存映射、缺页故障和缺页故障处理,还对动态内存分配器有了一定的了解。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix i/o接口
在Linux系统中,一个 Linux 文件就是一个 m 字节的序列:B0,B1…Bk… Bm-1,除此之外,所有的I/O设备都被模型化为文件,,甚至内核也被映射为文件。/boot/vmlinuz-3.13.0-55-generic(内核映像),/proc(内核数据结构),这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口操作:
1.打开文件。一个应用程序要求通过内核打开相应的文件,来宣告它想要访问一个I/O设备。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开始的字节偏移量。应用程序能够通过执行seek操作,现显式地设置文件的位置为k。
4.读写文件。一个读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file的条件,应用程序能够检测到这个条件。在文件结尾处并没有明确的"EOF符号"。
5.关闭文件。当应用程序完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。[4]
Unix I/O函数接口提供了以下函数:
1.open:进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的。
2.close:进程通过调用close函数关闭一个打开的文件。
3.read:应用程序通过read函数来执行输入。
4.write:应用程序通过write函数来执行输出。
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;
}
va_list的定义:
typedef char *va_list
这说明它是一个字符指针。
其中的: (char*)(&fmt) + 4) 表示的是…中的第一个参数。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 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;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章研究了hello系统I/O管理,介绍了I/O接口和函数,分析了printf函数和getchar函数的实现过程,对程序与I/O设备之间的交互过程有了更清晰的理解。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello的一生经历了这些过程:
(1)编写程序:源程序hello.c在编译器中完成。
(2)预处理:预处理器(cpp)将修改源程序,生成hello.i文件。
(3)编译:编译器(ccl)将hello.i文件翻译为汇编文件hello.s。
(4)汇编:汇编器(as)将hello.s文件翻译为二进制机器语言,生成可重定位目标文件hello.o。
(5)链接:链接器(ld)将可重定位目标文件hello.o和其他目标文件链接成为可执行文件hello。
(6)创建进程:shell进程调用fork函数为hello创建新进程,并调用execve函数运行hello。
(7)访问内存:通过MMU将需要访问的虚拟地址转化为物理地址,并通过缓存系统访问内存。
(8)动态申请内存:hello运行过程中可能会通过malloc函数动态申请堆中的内存。
(9)异常:hello运行过程中可能会产生各种异常和信号,系统会针对出现的异常和收到的信号做出反应。
(10)终止:hello运行结束后被父进程回收,内核删除相关数据。
通过本次大作业,我通过一步步的操作更直观的感受了计算机系统的运作,系统地回顾了这个学期所学的几乎所有知识。
**
**
附件
hello.i(预处理得到的文本文件)
hello.s(编译得到的文本文件)
hello.o(汇编得到的可重定位目标文件)
hello(链接生成的可执行文件)
hello.elf(可重定位目标文件ELF格式)
hello2.elf(可执行文件ELF格式)
hello_o.txt(可重定位目标文件的反汇编代码)
hello.txt(可执行目标文件的反汇编代码)
参考文献
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] 逻辑地址、线性地址和物理地址之间的转换 - 孤独剑 - CSDN博客
https://blog.csdn.net/gdj0001/article/details/80135196
[3] UNIX 常用IO函数 - 技术&邂逅 - CSDN博客
https://blog.csdn.net/lingjun_love_tech/article/details/40706599
[4] printf 函数实现的深入剖析 - Pianistx - 博客园https://blog.csdn.net/hit_shaoqi/article/details/78516508