哈尔滨工业大学
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 7203610217
班 级 2036011
学 生 张文婷
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
摘 要
本文主要通过hello.c程序的一生,分析了在linux系统下,一个程序完整的运行周期。研究了文件通过预处理、编译、汇编、链接生成可执行文件的全过程,分析计算机系统是如何对hello进行进程管理,存储管理,I/O管理,了解了虚拟内存、异常信号等相关内容。
关键词:预处理,编译,汇编,链接,进程管理,存储管理,I/O管理,虚拟内存,异常信号
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:hello.c经过预处理,汇编,编译,链接,形成可执行的目标文件。运行文件时,shell通过系统调用fork创建一个子进程,execve函数加载进程,hello由program转变为了process。
O2O:hello.c运行时,fork函数创建一个带有自己独立虚拟地址空间的新进程,execve在当前进程中加载新的程序,从mian开始执行目标代码。代码执行结束后,父进程便会回收hello,删除相关数据。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
开发工具:Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i: hello.c预处理得到的文件
hello.s: hello.i编译得到的机器语言汇编文件
hello.o: hello.s汇编后将汇编代码翻译成机器码的可重定位文件文件
hello.out: 经过链接后的可执行文件
1.4 本章小结
本章初步介绍了hello程序运行的整个过程,详细描述了P2P以及O2O的内涵,提供了实验环境和实验中间文件。
第2章 预处理
2.1 预处理的概念与作用
概念:
编译预处理主要是读取c源程序,对其中的伪指令和特殊符号进行处理。在将一个C源程序转换为可执行程序的过程中, 编译预处理是最初的步骤. 这一步骤是由预处理器来完成的。
作用:
1.处理宏定义指令
2.处理条件编译指令,这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3处理头文件包含指令,在头文件中一般用伪指令#define定义了大量的宏,同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。
4.处理特殊符号,预编译程序可以识别一些特殊的符号。
2.2在Ubuntu下预处理的命令
指令:gcc -E hello.c -o hello.i
解析:“gcc -E hello.c -o hello.i ”中,参数E是告诉gcc命令值经行编译,不做其他的处理,用参数o指明输出的文件名为hello.i。命令运行完毕后就会产生一个名为hello.i的文件。
2.3 Hello的预处理结果解析
解析:在预处理之后,伪指令和特殊符号都被替换掉了,cpp在源代码中填充了大量其他代码,使得篇幅大量增加。
2.4 本章小结
本章描述了预处理的概念和作用,用介绍了预处理指令的使用,解析了预处理之后生成的hello.i文件。
第3章 编译
3.1 编译的概念与作用
概念:对预处理文件进行词法分析,语法分析和语义分析,分析结束后进行代码优化生成相应的.s文件
作用:将高级语言指令转换为功能等效的汇编代码,使得目标代码能在目标机器上运行
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
3.3.1 文件解析
.file:声明源文件
.text:程序代码段
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明字符串
.global:声明全局变量
.type:声明符号是数据类型还是函数类型
3.3.2 数据
变量:
变量分为全局变量、静态变量、局部变量。已初始化的全局变量和静态变量存放在.data节,未初始化的全局变量和静态变量存放在.bss节,而局部变量存放在栈中管理。在此程序中,没有全局变量和静态变量,所以文件最开始没有.data和.bss,如下图。
同时上图可以看到,文件最开始有.rodata,这是因为定义了只读数据节printf格式的字符串,这里定义了两个,分别是.LC0和.LC1,.LC1中的%s是占位符
在hello.s中,定义了局部变量i,int类型的i存储在栈中,地址是%rbp-4,这正好符合了局部变量存放在栈中管理。
Main函数:参数argc作为用户传给main的参数,被放到了堆栈中。
常量:常量一般是以立即数的形式出现在汇编代码中
3.3.3 赋值操作
赋值操作可以通过数据传送命令来完成,最简单的数据传输类型是MOV类,常见的有movb,movw,movl,movq,还有零拓展符号传送指令movz和和符号拓展传送指令movs。在此程序中,有一个赋值语句i=0。
3.3.4算术操作
在hello.c程序中,有i++操作,汇编语言中用addl指令来实现
3.3.5关系操作
常用的关系操作指令有cmp和test,CMP指令和TEST指令能够设置条件码并不改变其他寄存器,常见的条件码有:
CF:进位标志 描述了最近操作是否发生了进位
ZF:零标志 最近操作结果为0
SF:符号标志最近操作结果为负数
OF:溢出标志最近操作导致一个补码溢出 补码溢出通常有两种结果
CMP指令与SUB指令行为一致;TEST指令与AND指令行为一致。
在hello.s中,两处用到了cmpl指令
检查是否小于8
检查是否与4相等
3.3.6 控制转移
控制转移常用jump指令来实现,在hell.s中,通过跳转来实现argc!=4,如下图
之后,同样通过跳转来控制i<8,下如图,如果i<=7,则跳转到L4,而L4中则正对应循环中的语句
3.3.7函数操作
C程序的控制结构会确保一个入口、一个出口,支持函数的嵌套调用。编译器通过维护一个栈的内存结构来确保代码的正确流动。关于堆栈平衡,可由调用函数负责,也可由被调函数负责。当有多个参数时,由调用约定来进行规定参数计算的顺序。
在hello.s中,如下图所示,前两行将参数.LC0传递到%rdi中,然后调用puts@PLT函数,后两行将参数1传递到%edi中,然后调用exi@PLTt函数。
而在printf("Hello %s %s\n",argv[1],argv[2])中,则传递了4个参数
同时,hello.s也调用了atoi,sleep等函数
3.3.8数组操作
此程序中包含指针数组argv,每个数组元素是一个指向参数字符串的指针,数组各个元素通过数组头指针加上偏移量来存储。
3.4 本章小结
文章介绍了编译的概念及其作用,编译将hello.o文件转化为了hello.s文件,同时分析了编译器对C语言的各个数据类型以及各类操作的处理。
第4章 汇编
4.1 汇编的概念与作用
汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
通用的可重定位目标文件的ELF格式:
通过命令readelf -a hello.o >hello.elf,生成hello.o文件的elf格式
1.elf文件以16字节的ELF头开始,这个16字节的序列描述了生成该文件的系统的字的大小和字节顺序
2.下图包含了帮助链接器语法分析和解释目标文件的信息,即ELF头的大小、目标文件的类型、机器类型是、节头部表的文件偏移,以及节头部表中条目的大小和数量。
类型表明这里是 64位的 ELF 格式
数据表明文件中的数据按照小端存储
版本号为 1
操作系统是UNIX - System
类型表明是重定位文件
节头包含文件中各个节的语义,包括节的类型、位置、大小等,每个节都从零开始因为是可重定位目标文件,根据节头表中的字节偏移信息可知各节的起始位置以及所占空间的大小。
.text:已编译程序的机器代码。
.rodata:只读数据,如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局和静态C变量。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态C变量
.symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息
3.重定位节 '.rela.text'存放了8个重定位条目,包含了需要被修改的引用的偏移量,被修改引用应该指向的符号,重定位的类型,告知链接器如何修改新的引用,重定位的目标的名称等。当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。
4.重定位节 '.rela.eh_frame',这部分包含了一个条目,存储了eh_frame 节的重定位信息
5.符号表:符号表保存了程序实现或使用的所有全局变量和函数,如果程序引用一个自身代码未定义的符号,则称之为未定义符号,这类引用必须在静态链接期间用其他目标模块或库解决,或在加载时通过动态链接解决。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
hello.o的反汇编:
机器语言的构成:机器语言指令是一种二进制代码,由操作码和操作数两部分组成。操作码规定了指令的操作,是指令中的关键字,不能缺省。操作数表示该指令的操作对象。
与汇编语言的映射关系:将反汇编代码与机器语言相比较,可以看出反汇编代码不仅显示汇编代码,还显示机器代码,机器代码能够直接被电脑识别。反汇编器使用的指令命名规则与汇编代码的部分有区别。如:
分支跳转时,hello.s中是jump到.L2和.L4,用一个符号表示,而在反汇编代码中,表示为主函数加段内偏移量。这是因为汇编语言更易于人类识别,而机器语言是给电脑识别的,汇编语言变为机器语言后,这些符号都会转为地址。
函数调用时,hello.s中是将参数传递到寄存器之后,call后跟着的是函数名称,但是在机器语言中,call后是地址和重定位条目,用于链接时重定位。
4.5 本章小结
本章分析了hello.o文件,对可重定位目标ELF文件格式进行了介绍,对比了反汇编代码和机器代码,展现了二者的异同之处,解析了机器语言与汇编语言的映射关系。
第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的格式
先使用命令readelf -a hello >hello1.elf命令生成hello文件的elf格式
ELF头:可以看到,可执行文件的类型变为了EXEC,表明它是一个可执行文件。头部同可重定位文件一样,同样包含了ELF头的大小、目标文件的类型、机器类型是、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头:节头对Hello中所有节的信息进行了声明,如每个节的名称、偏移量、大小、位置等,根据里面的信息可以定位各个节所占的区间,地址为被加载到虚拟地址的初始地址。
程序头:程序头定义了可执行文件在装入内存后的段内存布局,每个程序头对应进程的一个内存段。每一个条目包含了各段在虚拟地址空间大小和物理地址,标志,访问权限和对齐方式。
符号表:符号表保存了程序实现或使用的所有全局变量和函数,如果程序引用一个自身代码未定义的符号,则称之为未定义符号,这类引用必须在静态链接期间用其他目标模块或库解决,或在加载时通过动态链接解决。
5.4 hello的虚拟地址空间
使用edb加载hello,Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序,得到下图,根据节头部表,可以通过edb查看各个节的信息,可以得到edb与之前的elf文件是相对应的。
5.5 链接的重定位过程分析
在hello.o的反汇编代码中,有许多地方并没有填入正确的地址,正等待进行链接。通过objdump -d -r hello命令,得到下图,可以看出hello反汇编的代码有明确的虚拟地址,完成了重定位,并且hello反汇编代码中多出了其他节如.init和.plt节,在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep。
根据hello和hello.o的不同,可以得出链接过程为:链接就是链接器将各个目标文件组装在一起,文件中的各个函数段按照一定规则累积在一起。
5.6 hello的执行流程
401000 <_init>
401020 <.plt>
401030 <puts@plt>
401040 <printf@plt>
401050 <getchar@plt>
401060 <atoi@plt>
401070 <exit@plt>
401080 <sleep@plt>
401090 <_start>
4010c0 <_dl_relocate_static_pie>
4010c1 <main>
401150 <__libc_csu_init>
4011b0 <__libc_csu_fini>
4011b4 <_fini>
5.7 Hello的动态链接分析
在elf中查看与动态链接相关的段:
通过调试可以看出共享链接库代码是动态的目标模块,对动态链接的重定位过程就是在程序开始运行或者调用程序加载时,自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接起来。在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现函数的动态过程链接,这样它就包含了正确的绝对运行时地址。
5.8 本章小结
本章介绍了链接的概念与作用,简述了可执行文件hello的格式与虚拟空间地址,使用edb分析了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域和堆栈。
进程的作用:进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础,他可以申请和拥有系统资源,他不仅包含程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash作用:shell是运行在终端中的文本互动程序,bash是最常用的一种shell。他用于解释命令,连接用户和操作系统以及内核。
处理流程:1.输入:在交互模式下,输入来自终端。bash使用GNU Readline库处理用户命令输入,Readline提供类似于vi或emacs的行编辑功能。在非交互模式下,输入一般来自文件。此时,bash使用C语言标准库的stdio来获得输入。
2.解析:解析阶段的主要工作为:词法分析和语法解析 词法分析指分析器从Readline或其他输入获取字符行,根据元字符将它们分割成word,并根据上下文环境标记这些word。
3.扩展:扩展阶段对应于单词的各种变换,最终得到可用于执行的命令。
4.执行:不同类型的命令,bash的执行方式有所差异。bash中每种复合命令都使用一个C函数来实现,功能包括执行恰当的展开(如for循环中关键词in后面的单词),执行特定的命令,根据命令的返回值来变更执行流程等等。对于管道命令,管道两侧的命令会在不同的两个子进程中执行。 此时命令要经历 fork()系统调用创建子进程,连接管道然后命令的执行步骤如下述简单命令的执行。
6.3 Hello的fork进程创建过程
先判断./Hello是否是内置命令,他不是内置命令,则通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是相互独立的副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程的PID是不同的,是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
execve函数是在当前进程的上下文中加载并运行一个新程序。在执行fork得到子进程后随即使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序,只有当出现错误找不到Hello时,execve才会返回到调用程序。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户态和核心态:处理器通常使用控制寄存器中的一个模式位来记录当前进程运行的模式,如果模式位已经设置,则程序运行在内核模式中,一个运行在内核模式中的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置,没有设置模式位时,进程就运行在用户模式中,用户模式中的进程步韵熙执行特权指令。
调度过程:hello的一个上下文切换是调用sleep函数时,hello 请求休眠,控制转移给另一个进程,此时计时器开始计时,过了1s时,它会产生一个中断信号,中断当前正在进行的进程,进行上下文切换,恢复 hello 在休眠前的上下文信息,控制权回到 hello 继续执行。当循环结束后,hello 调用 getchar 函数,之前 hello 运行在用户模式下,在调用 getchar 时进入内核模式,内核中的陷阱处理程序请求来自键盘缓冲区的 DMA传输,并执行上下文切换且把控制转移给其他进程。当完成键盘缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程切换回 hello 进程,之后进程终止。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
1.正常情况下的输出:
2.乱按:当乱按的时候,程序还是会继续进行,但是乱按的内容会被输出,可以看到,第一个回车之前的输入内容被缓冲掉了,下一个回车的内容被当作为了命令。
3.按下ctrl-Z:内核向前台进程发送一个SIGSTP信号,前台进程被挂起。用ps查看其进程PID,可以看到,hello的进程ID是2322,输入fg 1可以再次调回前台继续运行。
4.kill:可以看到,kill命令将被挂起的程序彻底杀死了
5.按下ctrl-C:内核向前台进程发送一个SIGINT信号,前台进程终止,内核再向父进程发送一个SIGCHLD信号,通知父进程回收子进程,使用ps命令,可以看到,已经没有hello的进程ID了,此时子进程已经被彻底销毁。
6.pstree:可以看到,pstree命令可以打印出进程树
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章介绍了进程的概念与作用,shell的处理流程,分析了hello 进程的创建过程,execve过程,执行过程,并分析了hello执行过程中的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:hello程序产生的和段相关的偏移地址部分。表示为 [段标识符:段内偏移量]。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。hello会产生逻辑地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么hello的线性地址能再经变换以产生一个hello的物理地址。若没有启用分页机制,那么hello的线性地址直接就是hello的物理地址。
虚拟地址:虚拟内存为每个程序提供了一个大的、一致的和私有的地址空间。其每个字节对应的地址称为虚拟地址。
物理地址:物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址能再经变换以产生一个hello的物理地址。若没有启用分页机制,那么hello的线性地址直接就是hello的物理地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,一个段标识符和一个段内相对地址的偏移量。段标识符是一个16位长的字段,称为段选择符,而偏移量是一个32位长的字段。段选择符存储在短寄存器(6个)中:CS,SS,DS,ES,FS,GS。
CS:代码段寄存器,指包含程序指令的段。
SS:栈段寄存器,指向包含当前程序栈的段。
DS:数据段寄存器,指向包含静态数据或者是全局数据段。
其他的三个寄存器可以指向任意的数据段。
段描述符:每个段由一个8字节的段描述符表示,它描述了段的特征。段描述符放在全局描述符表或局部描述符表中。GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正在被使用的LDT地址和大小放在ldtr控制寄存器中。在单处理系统中只有一个。在多处理系统中每个CPU对应一个GDT;LDT的地址始终可以有多个。
为了快速完成线性地址到物理地址的转换,X86处理器提供了一种6个段寄存器对应的不可编程的寄存器;每个不可编程的寄存器包含8个自己的段描述符,由相应的段寄存器中的段选择符来指定。每当一个段选择符装入段寄存器,由该选择符指定的段描述符就由内存被装入相应的不可编程寄存器。此后,针对该段的逻辑地址转换就可不必经过内存中的GDT或LDT,处理器只需引用存放在不可编程寄存器中的段描述符既可。仅当段寄存器中的内容改变时,才需访问GDT或LDT。
获取段描述符的步骤:段选择符载入段寄存器,根据TI的值,CPU从gdtr或者idtr中获取段描述符表的首地址,从而段描述符的地址为:gdtr或idtr的地址+index*8,之后将段描述符的内容载入到段选择符对应的非编程寄存器中。
获取线性地址的步骤:经过上述段描述符的获取,线性地址为:段描述符中的base+逻辑地址中的32位偏移量。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址,而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址。
虚拟内存地址和物理内存地址的分离,给进程带来便利性和安全性。虚拟地址必须和物理地址建立一一对应的关系,才可以正确的进行地址转换。记录对应关系最简单的办法,就是把对应关系记录在一张表中。为了让翻译速度足够地快,这个表必须加载在内存中。不过,这种记录方式惊人地浪费。因此,Linux采用了分页的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页来管理内存。在Linux中,通常每页大小为4KB。
依据以下步骤进行转换:
1.从cr3中取出进程的页目录地址;
2.根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址,页的地址被放到页表中去了。
3.根据线性地址的中间十位,在页表中找到页的起始地址;
4.将页的起始地址与线性地址中最后12位相加,得到最终所要。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块,从TLB中获取物理地址,需经过:CPU产生一个虚拟地址;MMU从TLB中取出相应的PTE;MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存;高速缓存/主存将所请求的数据字返回给CPU。四级页表中包含了一个地址字段,它里面保存了40位的物理页号,这就要求物理页的大小要向 4kb对齐。四级页表每个表中均含有512个条目,第四级页表每个条目对应4kb区域;第三级页表每个条目对应2MB区域;第四级页表每个条目对应1GB区域;第四级页表每个条目对应512GB区域。将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。
7.5 三级Cache支持下的物理内存访问
根据CS寻找到正确的组 L1 cache中的某个组,在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为 1,若有,则命中,从这一行对应物理地址 b 位块偏移的位置取出一个字节,若有效位不为1,则说明不命中,需要继续访问下一级 cache,若三级 cache 都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为hello进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
execve函数在hello进程中加载并运行包含可执行文件hello.out的程序,用hello.out有效地代替了当前的程序。加载并运行hello.out需要一下几个步骤:
1. 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的 区域结构。
2. 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构, 所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3. 映射共享区域,hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到 这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器,execve 设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页处理程序搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。
因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux在链表中构建了一棵树,比你高在这棵树上进行查找。
如果试图进行的访问内存是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。此时,内存选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并进行更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送虚拟内存到MMU。这次, MMU就能正常的翻译虚拟内存,而不会再发生缺页中断了。
7.9动态存储分配管理
在进程运行的时候,动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。分配器将对视为一组大小不同的块的集合来分配。每个块就是一个连续的虚拟存储器片,要么是已经分配的,要么是还没有分配的。已经分配的,显示的保留给应用程序使用。空闲的,则是可以继续分配。一个已经分配的块保持已经分配的转态,直到被free掉,得以被堆重用。当已经分配的块一直没有被free,那么一直保持分配转态,这个就是内存泄漏了。
分配器有两种基本风格:
显示分配器:要求应用显示的释放已经分配的块,例如C标准库有malloc程序包的显示分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。
隐式分配器:分配器检测一个已经分配的块何时不再被程序所使用时,自动free掉。这个就是垃圾收集。
7.10本章小结
本章以hello程序为例,介绍了程序的存储管理机制。讨论了存储器地址空间,逻辑地址到线性地址的变换,VA到PA的变换,三级cache下的物理内存访问,fork和execve函数的内存映射,缺页处理和动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件:
所有的I/O设备都被模型化为文件,甚至内核也被映射为文件
设备管理:unix io接口
Linux/unix I/O:将设备映射为文件的方式,允许Unix内核引出一个简单、低级的应用接口。 Linux/unix IO的系统调用函数很简单,它只有5个函数:open(打开)、close(关闭)、read(读)、write(写)、lseek(定位)。但是系统IO调用开销比较大,一般不会直接调用,而是通过调用Rio包进行健壮地读和写,或者调用C语言的标准I/O进行读写。尽管如此,Rio包和标准IO也都是封装了unix I/O的,所以学习系统IO的调用才能更好地理解高级IO的原理。
8.2 简述Unix IO接口及其函数
unix IO接口:
1. 打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
2. shell 在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
3. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
4. 读写文件:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终 止时,内核都会关闭所有打开的文件并释放它们的内存资源。
unix IO函数:
1.函数open()和openat():
函数原型:
int open(const char* path, int oflag, .../*mode_t mode*/);
返回值:
若文件打开失败返回-1,打开失败原因可以通过errno或者strerror看;
若成功将返回最小的未用的文件描述符的值。
open与openat的区别:
- open和openat的区别主要在fd上:path参数指定的是绝对路径名,在这种 情况下,open与openat相同,fd忽略;path参数是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数通过打开相对路径名所在的文件目录获取。即此时fd为打开相对路径所获取的文件描述符;path参数为相对路径,fd参数为AT_FDCWD,此时相对路径为当前目录,作用于open相同。
2.函数create():
函数原型:
int create(const char *path, mode_t mode);
返回值:
若文件创建失败返回-1;
若创建成功返回当前创建文件的文件描述符。
函数功能:
create(path, mode)函数功能为创建新文件,与open(path, O_CREATE|O_TRUNC|O_WRONLY)功能相同。
3.函数close():
函数原型:
int close(int fd);
函数功能:
该函数的作用是关闭指定文件描述符的文件,关闭文件时还会释放该进程加在该文件上的所有的记录锁。当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都是利用这一功能而不是close函数关闭打开的文件。但是对于长期运行的函数,最好还是使用close关闭打开的文件。
4.lseek()函数:
函数原型:
int lseek(int fd, off_t offset, int whence);
函数功能:
使用lseek()函数显式的为一个打开的文件设置偏移量。lseek仅将文件的偏移量记录在内核中,并不引起IO开销。
参数说明:若whence为SEEK_SET,则将该文件的偏移量设置为距离当前文件开始处offset字节;若whence为SEEK_CUR,则将该文件的偏移量设置为距离当前偏移量加offset个字节,此时offset可正可负;若whence为SEEK_END,则将该文件的偏移量设置为当前文件长度加offser个字节,此时offset可正可负。
- read()函数:
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
返回值:
若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数。
若读取失败,返回-1。
参数说明:fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。
8.3 printf的实现分析
printf函数的函数体如下:
它调用的vsprintf函数如下:
vsprintf将printf的参数按照各种各种格式进行分析,将要输出的字符串存在 buf中,最终返回要输出的字符串的长度。write 函数的第一个参数为 fd,也就是描述符,而1代表的就是标准输出。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,终端请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后转化为ASCII码,保存在系统的键盘缓冲区之中。getchar函数落实到底层调用了系统的read函数,通过系统调用read读取在键盘缓冲区ASCII码,直到读到回车符,然后返回整个字串,getchar进行封装,大体逻辑时读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,简述了UNIX IO接口及其函数,并分析了printf 函数和 getchar 函数的实现过程。
结论
Hello的一生:
hello.c经过预编译,得到了hello.i文本文件,hello.i经过编译,得到了汇编代码hello.s汇编文件,hello.s经过汇编,得到了二进制可重定位目标文件hello.o,hello.o经过链接,得到了可执行文件hello。bash执行hello,首先bash会fork一个进程,然后在这个新的进程中execve hello,execve会清空当前进程的数据并加载hello,然后把rip指向hello的程序入口,把控制权交给hello;执行指令时,会为 hello 分配时间片,hello 执行自己的逻辑控制流;计算机用三级cache访问内存,将虚拟地址映射成物理地址;hello执行的过程中可能收到来自键盘或者其它进程的信号,当收到信号时hello会调用信号处理程序来进行处理,可能出现的行为有停止终止忽略等。在最后的最后,父进程或者init进程或kill掉hello,hello的一生就谢幕了。
对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
hello的一生,虽然短暂,却绚丽而深刻,他的一生,凝聚了无数人的奋斗于智慧。hello的一生,体现了计算机无数个组成部分的原理,计算机各个部分的相互配合,并且将各个章节的精髓都连接了起来。通过这个大作业,我对计算机的基本结构及其基本运行过程有了更深刻的了解,对计算机系统这门课程有了进一步的掌握。
附件
文件名 | 文件的作用 |
hello.i | 预处理后的文件 |
hello.s | 汇编文件 |
hello.o | 可重定位的目标文件 |
hello.elf | hello.o文件 的 ELF 格式 |
hello1.elf | hello文件的ELF格式 |
hello | 可执行文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.