计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
学 号 2021112934
班 级 21WL022
学 生 袁麟
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
本文先简述Hello的P2P,020的整个过程,然后针对P2P与O2O的过程内涉及到的全部计算机内执行的操作进行分析,总结并回顾了半学期所学习的预处理、编译、汇编、进程、存储、系统级I/O等相关知识,达到了很好的复习效果。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;I/O管理
目 录
第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 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
HELLO的生命始于程序员一字一键的敲进电脑并存成的hello.c文件,然后由编译系统自带的编译器驱动程序运行c预处理器(cpp),将hello.c翻译为带ASCII码的中间文件hello.i,接下来,驱动程序运行c编译器(ccl),将hello.i翻译为ASCII汇编语言文件hello.s,然后其运行汇编器(as),将hello.s翻译为可重定位目标文件hello.o,最后运行链接器程序ld,将hello.o与必要的系统目标文件组合起来创建一个可执行目标程序prog。最后在shell中输入./prog,令shell为hello.c进行子进程从而实现了P2P(from program to process)
之后shell进行execve,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码、数据、bss和栈区域创建新的区域结构,然后映射共享区域,设置程序计数器,映射虚拟内存,然后加载物理内存,,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,以上全部便是o2o的过程。
1.2 环境与工具
1.2.1 硬件环境
12th Gen Intel(R) Core(TM) i9-12900H;2.50 GHz;16.0 GB RAM;512GHD Disk
1.2.2软件环境
Windows11 64位;VirtualBox 7.0;Ubuntu 20.04.4 LTS;
1.2.3 开发工具
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c文件:编写程序的代码;
hello.i文件:预处理后的文本文件;
hello.s文件:编译后的汇编文件;
hello.o文件:汇编之后的可重定位文件;
hello文件:链接之后的可执行目标文件;
hello.elf文件:hello.o的ELF文件;
hello.objdump文件:hello.o的反汇编代码;
hello_out.elf文件:hello的ELF文件;
hello_out.objdump文件:hello的反汇编代码
1.4 本章小结
本章简述Hello的P2P,020的整个过程,基本上概述了hello一生的经过几部分,这些部分解析所用到的工具和文件,给整个大作业列出一个框架结构。
第2章 预处理
2.1 预处理的概念与作用
概念:在程序代码实际编译之前,对代码进行一系列的处理操作
作用:
1.将源文件中以”include”格式包含的文件复制到编译的源文件中。
2.用实际值替换用“#define”定义的字符串。
3.根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
对比可见hello.i相对hello.c展开至3060行,通过在文本编辑器中观察可见main函数之前为stdio.h,stdlib.h,unistd.h等头文件依次读取到.i中的代码。同时编译预处理器会删除注释等机器不需要的语句。
2.4 本章小结
本章主要介绍了预处理的概念以及作用,并通过hello.c以及预处理后hello.i的文件对比;对hello预处理结果进行解析,深入的了解了预处理过程的相关知识点,为接下来的处理步骤做准备。
第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,该程序包含main函数的定义,每一条定义都以文本格式描述的一条低级机器语言指令。
作用:1.帮助程序员了解代码在底层上是如何实现的。汇编语言程序通常比机器语言程序更容易阅读和理解,这是因为它通常不会像机器语言程序那样是一串二进制数,而是由标识符和操作码等组成的可读性更强的代码。
2.帮助程序员更好地进行调试和优化。使用汇编语言程序,程序员可以深入了解程序的内部细节,找出潜在的性能问题并进行优化,且例如编译器可以优化部分代码。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1指令解析
.file | 声明源文件 |
.text | 代码段 |
.section\.rodata | Rodata节 |
.global | 声明一个全局变量 |
.type | 声明函数类型、对象类型 |
.size | 声明大小 |
.long | 声明long类型 |
.string | 声明string类型 |
.align | 声明数据和指令地址对齐方式 |
3.3.2数据
1.常量
1.1数字常量:立即数直接在前面加上$后直接放入汇编代码中
显然,4作为立即数存在了指令之中,通过比较argc是否为四判断是否跳转
1.2字符串常量:汇编代码中,字符串常量一般存在汇编代码的某个段中如下
程序中使用的两个字符串分别存在了.rodata的.LC0与.LC1中
2.变量
2.1全局变量
该程序中没有全局变量,故无法查看汇编代码中全局变量的表示形式
2.2局部变量
局部变量一般保存于寄存器中,或者存于栈中,
C程序中:
汇编程序中对应部分:
可见汇编程序将局部变量i的值先放到栈rbp-4处,每次调用for内容前都会比较i与4的大小,小于等于就执行跳转执行for内的代码,汇编语言中体现为L4层内容代码。
3.3.3赋值
C程序中
汇编程序中
可见对于exit函数在调用前,需要先将返回值赋给传入参数的寄存器edi中,而前文局部变量i也是通过这种赋值方式传入栈rbp-4位置处的;
3.3.4程序跳转
第一处:
C程序中
汇编程序中
参数:.LC0的字符串常量首地址放入传递参数的寄存器rdi
调用:不涉及其余参数,则编译器选择puts函数,满足if条件时调用;
第二处:
C程序:
汇编程序:
参数:返回值1传入寄存器edi中;
返回值:返回值保存与eax寄存器中
第三处:
C程序:
汇编程序:
参数:argv[1]、argv[2]分别传入rsi与rdx,并且将LC1中字符串常量首地址传入rdi中
调用:由于涉及多个参数,编译器选择调用printf函数,只需要满足for循环执行条件即可执行
第四处:
C程序:
汇编程序:
参数:将argv[3]传入rdi寄存器中,供atoi函数调用;
返回值:返回值存于eax中,后续传入edi作为sleep函数的参数;
第五处:
C程序:
汇编程序:
参数:atoi返回值存于eax传入edi供sleep函数调用;
返回值:存于eax中
第六处:
C程序:
汇编程序:
参数:getchar()函数无需参数;
调用:执行完for循环即可执行
返回值:存于eax寄存器中
3.3.5类型转换
C程序
汇编程序
通过查阅资料得知atoi()函数返回一个int类型数据,而sleep()函数需要一个unsigned int类型数据,故其中涉及隐式类型转换,即将eax寄存器的低32位传给edi并且清空其中高32位的数据从而使数据不发生变动。
3.3.6算数操作
C程序:
汇编程序:
可见c程序中的for循环在汇编程序中体现为L3层代码以及L4代码中对栈中rbp-4位置处存的c程序中的i的维护,体现为循环结束后的i++;
以及汇编程序中
对应c程序中
可见其希望打印"Hello %s %s\n"语句时,先将栈中对应argv[1]与argv[2]的数即-24(%rbp)与-16(%rbp)传入rdx与rsi寄存器中,再将存储在LC1段的字符串常量传入rdi寄存器中,从而可以执行print函数,其中也用到了算数操作计算地址偏移。
3.3.6关系运算
该程序共有两处关系运算
第一处:
C程序:
汇编程序:
先比较argc是否为4,若为4则跳转而不执行接下来的代码;
第二处:
C程序:
汇编程序:
判断i是否小于5,若小于等于4则跳转执行循环内容,否则不执行跳转语句
3.3.7 数组,指针,结构操作
C程序:
汇编程序:
显然,程序中只涉及一个数组,即argv[],而从汇编程序中不难看出,argv[1]、argv[2]、argv[3]分别存在-24(%rbp)、-16(%rbp)、-8(%rbp)中,而汇编程序也通过对栈底指针加上偏移量的方式获取数组中的数据。
3.4 本章小结
编译器(ccl)将文本hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,我们通过hello.s文件中汇编指令,我们深入了解了汇编器是如何高效的将C语言中各个数据类型以及各类操作转换为汇编语言指令,汇编器将经过编译预处理的.i文件编译成.s文件,C语言程序经过编译处理成为低级机器语言(汇编语言)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,汇编器(as)将.s汇编程序翻译成机器语言并将这些指令打包成可重定目标程序的格式存放在.o目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
作用:将汇编语言翻译成机器语言,使其在链接后能够被机器识别并执行。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1 将.o文件使用readelf命令获得hello.o的ELF格式的可读文件
用文本编辑器打开如下
ELF头:
ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头:
节头包含.o文件中的每个节的信息:包括名称,大小,类型,全体大小,地址,旗标,链接,信息,偏移量与对齐
.symtab表
包含程序中所有使用的函数以及全局变量的基本信息,包括大小、类型、类别、与名称;
重定位节:
其中分为'.rela.text'与'.rela.eh_frame'两个部分,分别代表text节里面内容的位置的列表与对en_frame节的重定位信息。当链接器把这个目标文件和其他文件组合时,需要修改列表数据;可见两个列表均包含相对各自固定位置的偏移量、对相对地址引用的类型(如R_X86_64_PC32就表示重定位一个使用32位PC相对地址的引用)、符号名称
4.4 Hello.o的结果解析
反汇编结果:
Hello.s:
- 二者进制不同:反汇编操作数为十六进制,而汇编程序为十进制;
- 分支转移方式不同,汇编程序中条件跳转给出的是段的名字,如L3,L4;而反汇编的结果是从可重定位文件获得的,所有代码均有对应地址,故反汇编中直接跟着需要跳转位置的目标地址。
- 函数调用方式不同:汇编程序中直接使用函数名称加上@PLT获取代码修正,而反汇编程序中函数调用直接用main+偏移量的方式按地址调用,理由同分支转移中原因。
4.5 本章小结
本章介绍了hello.s汇编到hello.o的过程。我们查看了hello.o的可重定位目标文件的格式,使用反汇编查看hello.o经过反汇编过程生成的代码并且把它与hello.s进行比较,观察分析他们之间的差异,分析和阐述了从汇编语言进一步翻译成为机器语言的汇编过程。
第5章 链接
5.1 链接的概念与作用
概念:链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。
作用:把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
5.3.1用readelf生成hello的ELF格式
5.3.2分析hello_out.elf
5.3.2.1 ELF头
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量
5.3.2.2节头
对hello中所有的节信息进行了声明,包括大小Size以及在程序中的偏移量Offset,大小、全体大小、旗标、链接、信息、对齐等信息,根据Section Headers中的信息可以定位各个节所占的区间。其中地址一般是程序被载入到虚拟地址的起始地址。
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
0x00400000处有ELF标志,可以判断从可执行文件时加载的信息。
对应于5.3表中1-10号
对应表中11号
对应于表中12到16号
其中PHDR保存的是程序头表;INTERP保存了程序执行前需要调用的解释器;LOAD记录程序目标代码和常量信息;DYNAMIC储存了动态链接器所使用的信息;NOTE记录的是一些辅助信息;GNU_EH_FRAME保存异常信息;GNU_STACK使用系统栈所需要的权限信息;GNU_RELRO保存在重定位之后只读信息的位置。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
重定位命令objdump -d -r hello
与hello.o的重定位项目hello.outdump相对比:
hello_out.objdump地址为虚拟内存地址,并且多了许多节和子程序。
分析:在使用ld链接命令时,动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。重定位由两部组成。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它;为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用got中地址跳转到目标函数。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
由该程序名与地址对应关系可知
观察0x00404000处数据在_init执行前如下
_init执行后
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章通过解释链接的概念及作用,和分析hello的ELF格式,以及hello的虚拟导致空间,重定位过程,执行流程,和动态连接过程,深入学习了hello.o 可重定位文件到hello可执行文件的流程,和链接的各个过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例。系统中每一个程序都运行在某个进程的上下文中。上下文是程序正确运行所需的状态的组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:
1. 提供一个独立的逻辑控制流,它提供一种假象,好像我们运行的每个程序独占的使用处理器。
2. 提供一个私有的地址空间,它提供一种假象,含香我们的程序独立的使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
处理流程:
1.shell打印一个命令行提示符,等待用户在输入命令行,然后对这个命令行求值。
2.解析命令行之后,shell调用函数检查第一个命令行参数是否是一个内置的shell命令。如果是,它就立即解释这个命令
3.如果不是内置命令,那么shell建立一个子程序,并在子程序中执行所请求的程序。如果用户要求在后台运行该程序,那么shell返回到循环的顶部,等待下一个命令行,否则,shell使用waitpid函数等待作业终止。作业终止之后开始下一轮迭代。
6.3 Hello的fork进程创建过程
父进程调用fork函数创建一个新的运行的子进程,fork进程:父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的PID
特点
1.调用一次,返回两次;在父进程中fork会返回子进程的PID(总为非零值),在子进程中fork会返回0。
2.并行执行:父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
3.相同但是独立地址空间。父进程与子进程的地址空间是相同的,但他们同时都有自己的私有地址空间。
4.共享文件。
6.4 Hello的execve过程
子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
2. 用户模式和内核模式:处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。该寄存器描述了当前进程享有的特权。当设置了模式位时,进程就运行在内核模式中。没有设置模式位时,进程就运行在用户模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存;用户模式的进程不允许和执行特权指令、也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
3. 上下文切换:较高形式的异常控制流来实现都任务;内核为每个进程维持一个上下文,上下文就是内核重新启动的一个被强占的进程所需的状态。由包括通用目的寄存器、浮点寄存器、程序计数器、用户站、状态寄存器、内核栈和各种内核数据结构。
4.进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.6 hello的异常与信号处理
Hello程序执行时有正常运行、中断、陷阱、故障、终止几种状态;
其中
中断来自处理器外部I/O设备的信号,通过中断处理程序处理硬件的中断异常;
陷阱一般为指令控制,陷阱处理程序将控制返回下一条指令;
故障由错误情况引起,遇到故障时,处理器先尝试将控制转移故障处理程序,成功则将控制返回引起故障的指令,进而重新执行;否则处理程序返回到abort例程以终止故障的程序;
终止为不可恢复的致命错误引起,直接会进入abort例程终止该程序;
程序正常执行时:
按下Ctrl+z时
可见该命令产生了一个中断异常,其父进程收到信号SIGSTP并且运行信号处理程序,将hello程序挂起,并打印相关信息;
使用ps命令时(后续操作均是在用ctrl+z中断程序后)
执行完ctrl+z后,输入ps命令,打印了当前各个进程的pid
使用jobs命令
使用jobs命令时,可以看到被挂起进程的jid,以及表征其被挂起状态的已停止标识;
使用pstree命令
使用fg命令
通过直接使用fg命令,可以将被挂在后台的最后一个程序(在该例子中是jid为2的hello进程)调到前台执行,并且打印剩余部分后结束进程
使用kill命令
使用kill -9 pid 命令可以结束指定pid的进程,该指令通过发送SIGKILL给进程6818,使之被杀死;
使用ctrl+c(用fg将挂起的任务恢复后)
在程序执行时,使用ctrl+c命令会使其发送SIGINT信号结束当前运行程序;
程序执行时不停乱按
不干扰程序的运行,会被终端以每行为整体(以\n结尾)当成命令存储于缓冲区,在程序执行后当成命令执行。
6.7本章小结
本章复习了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果。
第7章 hello的存储管理
7.1 hello的存储器地址空间
虚拟地址:保护模式下程序访问存储器所用的逻辑地址,为hello程序的虚拟内存地址;
逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一,在hello程序中体现为相对偏移地址;
线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为hello程序的虚拟内存地址
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址,为hello运行时虚拟内存地址对应的物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。
8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,即逻辑地址。将内存分为不同的段,每个段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
32位的页目录项与页表项
P:1表示页表或页在主存中;P=0表示页表或页不在主存,即缺页,此时需将页故障线性地址保存到CR2。
R/W:0表示页表或页只能读不能写;1表示可读可写。
U/S:0表示用户进程不能访问;1表示允许访问。
PWT:控制页表或页的cache写策略是全写还是回写(Write Back)。
PCD:控制页表或页能否被缓存到cache中。
A:1表示指定页表或页被访问过,初始化时OS将其清0。利用该标志,OS可清楚了解哪些页表或页正在使用,一般选择长期未用的页或近来最少使用的页调出主存。由MMU在进行地址转换时将该位置1。
D:修改位(脏位dirty bit)。页目录项中无意义,只在页表项中有意义。初始化时OS将其清0,由MMU在进行写操作的地址转换时将该位置1。
高20位是页表或页在主存中的首地址对应的页框号,即首地址的高20位。每个页表的起始位置都按4KB对齐。
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
Cpu给出虚拟地址后经由MMU翻译传给L1cache中,L1cache获取物理地址缓存偏移,索引与标记,若缓存终端组索引与标记匹配且其中数据有效位为1,则输出,否则再从下一级cache中获取需要内容
7.6 hello进程fork时的内存映射
Hello进程fork时,内核为其创建一系列数据结构,并分配一个pid。对每个映射私有对象的进程,其私有区域的页表条目为只读,且区域结构为私有的写时复制,在没有进程试图写入其私有区域,每个映射私有对象的进程均共享物理内存中对象的单独副本。当有程序试图写页面时,故障处理程序创造一个该页面的新副本,恢复其可写权限,更新页表条目指向这个副本即可。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:
(1)删除已存在的用户区域
(2)映射私有区域:为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
(3)映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),指向代码的入口点
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
1.处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
7.9.1.采用显示空闲链表
策略:LIFO后进先出法:将新释放的块放置在链表的开始处(常数时间;碎片太多)
或地址顺序法:按地址顺序维护链表(需要搜索;碎片少于LIFO)
7.9.2采用隐式空闲链表
策略:使用malloc分配直至空间分配达到一定限度,然后标记所有从根节点可以访问到的块,扫描后解放所有没有标记的内存。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章主要介绍了hello进程在执行的过程中的虚拟内存与物理内存之间的转 换关系,以及一些支持这些转换的硬件或软件机制。同时介绍了在发生缺页异常的时候系统将会如何处理这一异常。最后介绍了动态内存分配的作用以及部分方法与策略。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备都被模型化为文件,而所有的输入与输出都被当成对应文件的读和写来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口:
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.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1. int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
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 write(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息
先用传入的字符串地址为跳转条件,依次复制其中不为%的字符串内容至buf中
当出现有%的字符时,进入switch语句,将传入的第二个参数从数字化为字符串加入buf中,即将数字填入以buf为首地址的字符串中。程序执行完返回需要打印的字符串的长度。
通过查询资料可以知道int INT_VECTOR_SYS_CALL的作用是调用sys_call这个函数,而这个函数内含一个字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
输入/输出(I/O) 是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O 设备复制数据到主存,而输出操作是从主存复制数据到I/O 设备。所有语言的运行时系统都提供执行I/O 的较高级别的工具。例如, ANSI C 提供标准I/O 库,包含像printf 和scanf 这样执行带缓冲区的I/O函数。C++ 语言用它的重载操作符<<(输入)和>>(输出)提供了类似的功能。在Linux 系统中,是通过使用由内核提供的系统级Unix I/O 函数来实现这些较高级别的I/O 函数的。
结论
编译预处理:将所有hello.c调用的函数附到其之前,再去掉注释,进行条件编译,宏展开;
编译:将hello.i编译为汇编文件hello.s
汇编:将hello.s会变成为可重定位目标文件hello.o
链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
运行:使用edb –run hello命令用edb运行hello;
创建子进程:shell进程调用fork;
运行程序:shell调用execve,再让其启动加载器,映射虚拟内存,进入程序入口,程序载入物理内存,进入main函数;
执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
访问物理内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址
动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存
信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起
回收子进程:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
通过本次大作业,我跟着hello的生命周期系统的复习了一边计算机系统的程序机器级表示、存储器层次结构、链接、异常、虚拟内存以及部分I/O的内容,从一个更加贴近程序生命周期的角度运用所学的知识让人对计算机系统的整体有更深刻的认识。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c文件:编写程序的代码;
hello.i文件:预处理后的文本文件;
hello.s文件:编译后的汇编文件;
hello.o文件:汇编之后的可重定位文件;
hello文件:链接之后的可执行目标文件;
hello.elf文件:hello.o的ELF文件;
hello.objdump文件:hello.o的反汇编代码;
hello_out.elf文件:hello的ELF文件;
hello_out.objdump文件:hello的反汇编代码
参考文献
[1]《深入理解计算机系统》Randal E.Bryant David R.O’Hallaron 机械工业出版社