计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L020212
班 级 2003003
学 生 曾正维
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
-本文对hello的整个生命周期进行了整体严密的分析,按步骤从预处理,编译,汇编,链接每一步的过程与作用都做出详细的说明。最终得到了可执行目标文件hello。接着在终端输入”./hello 120L020212 曾正维 1”,针对程序的执行过程,现象,以及如何执行,其底层实现都进行了分析。最后结束进程,由父进程进行回收,hello的生命终止。
关键词:预处理;编译;汇编;链接;进程;存储;I/O管理。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process,在Linux系统上,GCC编译器驱动程序读取源程序hello.c,经过预处理器进行预处理,生成hello.i文件,编译器将hello.i翻译成hello.s,接下来汇编器将hello.s翻译成机器语言指令,结果保存在hello.o文件中,然后经链接器链接得到可执行目标文件,最后OS为其fork一个子进程,调用execve将程序加载到内存中,由系统执行。
O2P:From Zero-0 to Zero-0,可执行文件hello执行后,shell通过execve函数进行虚拟内存映射,分配空间,调用_start函数,初始化执行环境,调用用户层的main函数,CPU流水线式读取并执行指令,通过TLB、4级页表、3级Cache,Pagefile等方式加速程序运行,程序进程终止后,通过信号处理机制,hello的父进程对hello进行回收,释放内存删除有关进程上下文。进程由0开始,由0结束。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
1.2.2 软件环境
Windows10 64位;Vmware 15.5.6;Ubuntu 16.04 LTS 64位
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 | 作用 |
hello.i | 预处理后的文件,用于分析预处理过程 |
hello.s | 经过编译生成的汇编文件,用于分析编译的过程 |
hello.o | 汇编生成的可重定位目标文件,用于分析汇编的过程。 |
hello | 链接生成的可执行文件,用于分析链接的过程。 |
hello.elf | hello.o的elf文件,分析可重定位目标文件hello.o |
hello_asm.txt | 将hello.o反汇编生成的文件,分析可重定位目标文件hello.o |
hello1.elf | hello的elf文件,分析可执行目标文件hello |
hello_asm1.txt | 将hello反汇编生成的文件,分析可执行目标文件hello |
1.4 本章小结
简述了Hello的P2P,020的整个过程。介绍了编写论文过程中使用的软硬件环境,以及开发与调试工具。列出为编写论文生成的中间结果文件的名字和文件的作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
-概念:在编译之前进行的处理。C语言的预处理主要有以下三个方面的内容:1. 宏定义;2. 文件包含;3. 条件编译,预处理命令以符号#开头,如#if, #endif, #define, #include等。
作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
命令: gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
打开.i文件可以发现总共有3060行,且从3047行开始是hello.c函数的main函数
预处理器对源文件进行了宏展开,读取系统头文件stdio.h的内容,并把它直接插入程序文本中,而对main函数进行了保留,没有改变。
2.4 本章小结
本章对预处理的概念和作用进行了介绍,对Linux下终端内预处理的命令进行了说明和师范,并对预处理hello.c后生成的hello.i文件进行了结果解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译器将预处理完的.i文件通过一系列词法分析、语法分析和优化之后生成汇编文件,将c语言这种高级语言转换为更低级、更底层、机器更好理解的汇编语言程序。
作用:生成的汇编文件每条语句都以一种文本格式描述了一条低级机器指令,汇编语言为不同的高级语言的不同编译器提供了通用的输出语言,汇编语言相对于预处理文件更利于机器理解。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
3.3.1 汇编指令
.file:声明源文件。
.text:代码节。
.sectio:把代码划分成若干个段(Section)。
.rodata:只读代码段。
.align:数据或者指令的地址对齐方式。
.string:声明一个字符串。
.global:声明全局变量。
.type:声明一个符号是数据类型还是函数类型。
.align:声明对指令或数据的存放地址进行对齐的方式。
3.3.2数据类型
1)argc:
我们在下面-20(%rbp)与4比较,相等就跳到L2,我们就能判断出存在-20(%rbp)里的是argc,而-20(%rbp)的值是由%edi传送得到,故argc最开始是存储在%edi中。
2)argv数组:
通过L4中反复调用-32(%rbp),可以判断出这是在循环语句中调用argv数组的参数。
其他数字以立即数形式出现。
3)字符串:
两个需要打印的字符串存在LC0和LC1。
4)局部变量i:
在L2内,将0赋值给-4(%rbp),而且-4(%rbp)之后在L3里与7作比较,因此可以判断-4(%rbp)是i。
3.3.3 操作类型
1)赋值
赋值操作利用mov语句。如果是局部变量不赋初值,在汇编代码里没有体现,只在用到并赋初值时才用寄存器等来存储;全局或静态变量变量不赋初值,则会存放在.bss段;已初始化的全局变量,会存放在.data段。
比如i=0。由于i是int类型,因此采用的movl形式。
2)算数操作
在hello.c中的算数操作只有i++,在hello.s中表现出来的就是addl。
3)关系操作
进行条件比较判断,比如等于,大于,小于,以及他们之间的组合,在hello.c中有argc!=4和i<8这两个条件判断。都用到了cmpl,即只设置条件码,不改变值。
将4与argc进行比较,如果相等,就跳到L2去运行,若不相等,就不执行je,继续向下运行。
将i与7进行比较,如果i<=7就进入循环体运行,如果i>7就退出循环
3.3.4控制跳转指令
如上图
第一个如果argc=4,就跳转到L2。
第二个是在for循环中判断,i如果小于8,就继续循环,对应的就是,i<=7时,执行jle .L4,跳到循环体内执行循环体。
3.3.5函数操作
call指令,比如函数P调用函数Q,需要将程序计数器(PC)设置为Q的代码的起始位置,并把地址A压入栈中,压入的地址A叫做返回地址,是紧跟在call指令后的那条指令的地址。
hello.s中的函数操作有:
其中还有main函数,其参数为argc和argv
3.3.6类型转换
hello.c中atoi(argv[3])将字符串类型转换为整型。int、float、double、short、char可以进行相互转化。在hello.s中表现为
3.4 本章小结
本章对编译的概念和作用进行了初步讲解,介绍了在Ubuntu下编译的指令,针对hello.c编译生成的hello.s,从汇编指令,数据类型,操作类型,控制跳转指令,函数操作和类型转换等方面对编译器编译的机制进行了细致分析,对编译生成的汇编代码有了更深入的理解。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言书写的程序.s文件翻译成与之等价的机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o文件,.o文件是二进制文件。
作用:将汇编代码翻译为机器语言指令,即机器能识别的二进制代码,使其在链接过后能被机器识别并运行。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
应截图,展示汇编过程!
4.3 可重定位目标elf格式
典型的ELF可重定位目标文件
在Ubuntu终端内可用readelf -a hello.o > hello_elf生成hello.o文件的elf格式,对其进行查看。
1)ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述,其中目标文件中每个节都有一个固定大小的条目(entry)。
2)一个典型的ELF可重定位目标文件包含下面几个节:
3)节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
4)重定位节:.rela.text,一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令不需要修改。调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
5)符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
使用命令:objdump -d -r hello.o > hello_asm.txt
与hello.s的对比:
1)hello.s中操作数采用十进制,反汇编得到 hello_asm.txt里指令中操作数是由十六进制表示。
2)hello_asm.txt中指令前有指令的地址,hello.s中没有。
3)反汇编的分支转移跳转指令采用的是对main函数的相对偏移量,hello.s中则是一个明确的段名称如.L2。
4)反汇编代码中调用函数是采用重定向的方式进行跳转,链接时根据重定位条目来获得地址信息,其也是一个对main函数的相对漂移量的表示,而汇编代码hello.s是直接使用函数名。
5)在汇编代码中使用.LC0(%rip)访问全局变量,反汇编代码中为0x0 (%rip),由于访问全局变量时需要重定位,所以反汇编中需要初始化为0并且添加重定位条目。
4.5 本章小结
本章介绍了汇编的概念和作用,分析hello.o的ELF格式,用readelf等列出其各节的基本信息,详细介绍了节头部表、符号表等信息,特别是重定位项目分析。通过生成的反汇编文件与汇编文件hello.s进行了对照分析,详细说明了机器语言中的操作数与汇编语言之间不一致的地方,还特别对分支转移函数调用等进行了重点说明。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接可以使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
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
1.ELF头:hello的文件头和hello.o文件头不同之处的在于hello是一个可执行目标文件,有27个节。
2.节头:对 hello中所有的节信息进行了声明,其 中包括大小以及在程序中的偏移量,因此根据节头中的信息我们就可以定位各个节所占的区间(起始位置,大小)。其中地址是程序被载入到虚拟地址的起始地址。
3.程序头
4.重定位节.rela.text:
5.符号表.symtab:
5.4 hello的虚拟地址空间
由图可知虚拟地址空间从0x400000开始,并且ELF头标识位于0x40000,可以得知可执行文件的代码段和数据段从地址0x400000开始。
根据5.3中的节头部表,可以通过edb找到各个节的信息,比如
.inerp节
.text节
.fini节
5.5 链接的重定位过程分析
命令: objdump -d -r hello > hello_asm1.txt
hello_asm1.txt和hello_asm.txt相比,首先多了很多经过重定位之后的函数,如_init、puts@plt等,hello_asm.txt在.text段之后只有一个main函数;hello_asm.txt的地址是从0开始的,是相对地址,而hello_asm1.txt的地址是从0x401000(_init的地址)开始的,是已经进行重定位之后的虚拟地址;在hello_asm1.txt的main函数中,条件跳转指令和call指令后均为绝对地址,而hello_asm.txt中是相对于main函数的相对地址。
hello比hello.o多出的节头表:
.interp 保存ld.so的路径
.note.ABI.tag Linux下特有的section
.note.gnu.build-i 编译信息表
.gnu.hash gnu的扩展符号hash表
.dynsym 动态符号表
.dynstr 动态符号表中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化
.plt 动态链接表
.fini 程序终止时需要的执行的指令
.eh_frame 程序执行错误时的指令
.dynamic 存放被ld.so使用的动态链接信息
.got 存放程序中变量全局偏移量
.got.plt 存放程序中函数的全局偏移量
.data 初始化过的全局变量或者声明过的函数
链接器完成的两个主要任务:符号解析和重定位。
重定位由两步组成:
1. 重定位节和符号定义。
2. 重定位节中的符号引用。
重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,已经赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
重定位节的符号引用这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中成为的重定位条目的数据结构。
重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中。
重定位地址算法如图
如对puts函数的重定位:
在hello.o反汇编代码中,该行二进制编码为e8 00 00 00 00
addr(text)= 0x401105
refaddr = addr(text)+offset = 0x401126,即引用运行时的地址
addr(r.symbol) = addr(puts) = 0x401080
然后更新该引用,*refptr = (unsigned) (addr(r.symbol) + r.addend - refaddr)
= (unsigned) (0x401080 +(-4) – 0x401126) = (unsigned) (-aa) = ff ff ff 56
将其以小段序填入可得 56 ff ff ff ,与反汇编代码一致。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名称 | 程序地址 |
_init | 0x401000 |
_start | 0x4010f0 |
_libc_start_main | 0x2f12271d |
main | 0x401125 |
puts@plt | 0x401030 |
printf@plt | 0x401040 |
atoi@plt | 0x401060 |
exit@plt | 0x401070 |
sleep@plt | 0x401080 |
getchar@plt | 0x401050 |
5.7 Hello的动态链接分析
动态链接的基本思路是当创建可执行文件时,静态执行一些链接,然后再程序加载时,动态完成链接过程。基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
在elf文件中找到.got的地址
根据上图,我们可以知道动态链接调用的函数的位置。
在edb找到相应地址处,并且在dl_init处设置断点,分析在dl_init前后该地址附近变化:
dl_init前:如图都为0。
dl_init后:如图所示都有了值,各个函数也都从0变成有地址了。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
5.8 本章小结
在本章中介绍了链接的概念和作用,演示了链接的过程。并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。他给了我们一种假象,好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器好像无间断地一条接一条地执行我们程序中的指令,程序中的代码和数据好像是系统内存中唯一的对象。
作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。可以显示当前内存中运行的程序,服务等。通过进程你可以判断某个程序占用多少CPU和内存使用量,还可以通结束进程来结束无法关闭的程序。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型应用级程序,代表用户运行其他程序。是用户使用Linux的桥梁。它提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。其基本功能就是解释并运行用户的指令。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数,并改造传递给execve的argv变量。
3)检查argv[0]是否是内置的shell命令,如果是内置命令则立即执行。
4)不是内置命令,则调用fork()函数创建子进程。将程序在该子进程的上下文中运行。
5)根据&等符号,判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回,在后台执行。
6)shell 接受键盘输入信号,以及各种内核发送给程序的信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
父进程沟通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
输入执行hello的命令后,由于输入的命令不是一个内置命令,shello就会调用fork函数创建一个子进程,将程序在该子进程的上下文中运行。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello,且带参数列表argv(也就是学号 姓名 秒数)和环境变量envp。只有出现错误,execve才会返回到调用程序。execve调用一次并从不返回。execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但并没有创建一个新进程。进的进程仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
execve函数加载并运行hello需要一下几个步骤:
- 删除已存在的用户区域。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构,这些区域都是私有,写时复制的。
- 映射共享区域。共享对象都是动态连接到hello的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC)。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
让我们来深入的看看这些抽象:
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
逻辑控制流:一系列程序计数器PC的值,这些值唯一的对应于包含在程序的可执行目标文件中的指令,这个PC值的序列叫做逻辑控制流。进程是轮流使用处理器的,每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。
用户模式和内核模式:处理器通常用某个控制寄存器的一个模式位,来提供用户模式/内核模式这一机制的功能。没有设模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文切换:操作系统内核使用一种称为上下文切换的异常控制流来实现多任务。当内核选择一个新的进程运行时,称为内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。同时进行这些操作
- 保存当前进程上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程。
在hello进程执行时,在进程调用execve函数之后,进程会为hello分配虚拟地址空间,为.text节和.data节分配代码区和数据区。一开始hello运行在用户模式下,输出hello 120L020212 曾正维,然后hello就调用了sleep函数,进程陷入内核模式,内核会请求释放当前进程,将hello进程移出运行队列加入等待队列,这时计时器开始计时,内核也进行上下文切换将当前进程的控制权交给其他进程。根据我们选择的时间,时间到后,会发送一个中断信号,此时又进入内核状态执行中断处理,将hello进程重新加入运行队列,hello就继续执行自己的控制逻辑流。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常可以分为四类:中断(Interrupt)、陷阱(trap)、故障(fault)和终止(abort)。
中断:异步发生,是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序称为中断处理程序。
陷阱:是有意的异常,最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障:由错误情况引起,可能能被故障处理程序修正。如果能修正,就将控制返回到引起故障的指令,从而重新执行它。否则返回到内核中的abort例程,终止引起故障的应用程序。
终止:是不可恢复的致命错误造成的结果,终止处理程序从不将控制返回给应用程序,将控制返回给一个abort例程,终止这个应用程序。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令
1.不停乱按
无关输入被缓存到stdin,直到按回车getchar。
2.按回车
回车被缓存到stdin,当getchar读出一个回车’\n’作为结束符的字串,其他字串会被当成shell的命令输入。
3.按ctrl z
停止前台作业(进程组的每个进程),随后可用fg 或bg恢复运行。
4.按ctrl c
终止前台作业(进程组的每个进程)。
5.ps
在Linux中是查看进程的命令。ps查看正处于Running的进程,ps aux查看所有的进程。
6.jobs
显示当前暂停的进程
7.pstree
将所有进程以树状图显示,树状图将会以 pid (如果有指定) 或是以 init 这个基本进程为根 (root),如果有指定使用者 id,则树状图会只显示该使用者所拥有的进程。
使用权限:所有使用者。
8.fg
fg %n 使第n个任务在前台运行。不带%n 表示对最后一个进程操作。
9.kill
kill –l:列出信号
kill –SIGKILL 17130: 杀死pid为17130的进程
kill -9 17130 :杀死pid为17130的进程,或者:
kill -9 -17130:杀死进程组17130中的每个进程
killall -9 pname: 杀死名字为pname的进程
这里选择用 kill -9 3573。
6.7本章小结
本章介绍了进程的概念和作用,简述了壳Shell-bash的作用与处理流程。对hello的fork和execve过程进行了详细说明,结合进程上下文信息、进程时间片,进程调度的过程,用户态与核心态转换等解说了hello的进程执行。对hello程序运行过程中如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后运行ps jobs pstree fg kill 等命令,分别进行了梳理和说明,对各种异常和信号等进行了介绍,对hello 程序的理解有了进一步的提高。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址空间:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
段地址:偏移地址
23:8048000 段寄存器(CS等16位):偏移地址(16/32/64)
实模式下: 逻辑地址CS:EA =è物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
线性地址空间:也就是虚拟地址。是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。 非负整数地址的有序集合:{0, 1, 2, 3 … }
虚拟地址空间: N = 2n 个虚拟地址的集合 ===线性地址空间
{0, 1, 2, 3, …, N-1}
在hello_run_asm.txt文件中,显示出的地址为虚拟地址:
物理地址空间: M = 2m 个物理地址的集合
{0, 1, 2, 3, …, M-1}
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。结合hello来说的话,比如i的逻辑地址就是&i得到的,这时只是他在进程中的当前数据段的一个地址,然后和段的基地址结合就有了虚拟地址,这个虚拟地址再映射到一个物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段描述符是一种数据结构,实际上就是段表项,分两类:
用户的代码段和数据段描述符
系统控制段描述符,又分两种:
特殊系统控制段描述符,包括:局部描述符表(LDT)描述符和任务状态段(TSS)描述符
控制转移类描述符,包括:调用门描述符、任务门描述符、中断门描述符和陷阱门描述符
描述符表实际上就是段表,由段描述符(段表项)组成。有三种类型
全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段
局部描述符表LDT:存放某任务(即用户进程)专用的描述符
中断描述符表IDT:包含256个中断门、陷阱门和任务门描述符
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页表是一个页表条目 (Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址。 虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。
CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE。将页表条目中物理页号(PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。
页命中:
缺页异常:
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。
TLB命中:
TLB不命中:
下图为Core i7 MMU使用四级页表将VA转PA的过程:
36位VPN被划分为四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
完成从虚拟地址到物理地址的转换后,就可以使用物理地址进行内存访问。
Intel Core i7使用了三级cache来加速物理内存访问,L1级cache作为L2级cache的缓存,L2级cache作为L3级cache的缓存,而L3级cache作为内存(DRAM)的缓存。已知Core i7的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。Corei7实现支持48位虚拟地址空间和52位物理地址空间。因为L1块大小为64字节,所以B=64,b=6.因为L1是8路组相联所以S=8,s=3.所以标记位为43位。根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为1,若有,则说明命中,从这一行对应物理地址b位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级cache,访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。三级cache不仅仅支持数据指令的访问,也支持页表条目的访问,在MMU进行虚拟地址到物理地址的翻译过程中,三级cache也会起作用。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行hello,需要以下几个步骤:
1.删除已存在的用户区域
2.映射私有区域。创建新的区域结构,这些新的区域都是私有的、写时复制的。代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈堆映射到匿名文件,栈堆的初始长度0.
3.映射共享区域。共享对象由动态链接映射到本进程共享区域。
4.设置PC,指向代码区域的入口点。Linux根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为为缺页。缺页故障是指当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。
缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成。
缺页处理程序执行的步骤:
- 虚拟地址A是合法的吗?即A是否在某个区域结构定义的区域内。缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果指令不合法,就触发段错误,从而终止。
- 进行的内存访问是否合法?即进程是否有读、写或者执行这个区域内页面的权限。例如对代码段的只读页面进行写操作,一个运行在用户模式中的进程试图从内核虚拟内存中读取字,像这些不合法的访问,缺页处理程序会触发一个保护异常,从而终止。
- 此时经过上两个步骤,内核知道了这个缺页是对合法的虚拟地址进行合法的操作造成的。然后它选择一个牺牲页面,如果这个页面被修改过,就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常翻译A,而不会再产生缺页中断。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存管理通过动态内存分配器实现。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但不失通用性,假设堆是一个请求二进制零的区域,紧接在未初始化数据区域后开始,向上生长。对每个进程,内核维护一个全局变量brk指向堆顶。分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留,供应用程序使用;空闲块可用来分配。空闲块保持空闲,直到空闲块显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的(即显式分配器),要么是内存分配器自身隐式执行的(即隐式分配器)。显式分配器和隐式分配器是动态内存分配器的两种基本风格。两种风格都要求应用显式地分配块,不同之处在于由哪个实体来负责释放已分配的块。显式分配器要求应用显式地释放任何已分配的块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。
显式分配器必须在一些约束条件下工作:处理任意请求序列;立即响应请求;只使用堆;对齐要求;不修改已分配的块。在这些限制条件下,分配器试图实现吞吐率最大化和内存使用率最大化,但这两个性能目标通常是相互冲突的。
分配器的具体操作过程以及相应策略:
(1)放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。执行这种搜索的常见策略包括首次适配、下一次适配和最佳适配等。
(2)分割空闲块:一旦分配器找到了匹配的空闲块,需要决定分配这个空闲块中多少空间。可以选择用整个块,但会造成额外的内部碎片;也可以选择将空闲块分割为两部分,第一部分变成已分配块,剩下的变成新的空闲块。
(3)获取额外的堆内存:如果分配器不能为请求块找到空闲块,分配器通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插到空闲链表中,然后被请求的块放在这个新的空闲块中。
(4)合并空闲块:分配器释放一个已分配块时,要合并相邻的空闲块。分配器决定何时执行合并,可以选择立即合并或者推迟合并。合并时需要合并当前块和前面以及后面的空闲块。
显式空间链表:已分配块的块结构和隐式链表的相同,由一个字的头部、有效载荷、可能的一些额外的填充以及一个脚部组成。而在每个空闲块中,增加了一个前驱指针和后继指针。通过这些指针,可以将空闲块组织成一个双向链表。空闲链表中块的排序策略包括后进先出顺序、按照地址顺序维护、按照块的大小顺序维护等。显式空闲链表降低了放置已分配块的时间,但空闲块必须足够大,以包含所需要的指针、头部和脚部,这导致了更大的最小块大小,潜在提高内部碎片程度。
隐式空间链表:带边界标签的隐式空闲链表与普通的空闲链表不同,一个块除了是由一个字的头部、有效载荷、可能的一些额外的填充组成外,还有一个与头部相同的脚部组成。头部和脚部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。
我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。在带边界标签的隐式空闲链表中,我们的脚部就标记了一个块的结束。
合并的时候分配器就可以通过检查脚部来检查前一块的状态和大小了。
malloc采用的是分离的空闲链表。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小升序排列,当分配器需要一个大小为n的块时,就搜索相应大小类对应的空闲链表。如果不能找到合适的块,就搜索下一个链表,以此例推。
7.10本章小结
本章总结了hello运行过程中有关内存管理的内容。简述了TLB、多级页表支持下的地址翻译、cache支持下的内存访问、缺页的处理、fork+execve过程的内存映射以及动态存储分配的过程。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
每个Linux文件都有一个类型(type)来表明它在系统中的角色。
- 普通文件(regular file)包含任意数据。要区分文本文件和二进制文件,文本文件是只含有ASCII或Unicode字符的普通文件;二进制文件时是所有其他的文件。对内核而言,这两者没有区别。
- 目录(directory)是包含一组链接(link)的文件,其中每个链接都将一个文件名(filename)映射到一个文件,这个文件可能是另一个目录。
- 套接字(socket)是用来与另一个进程进行跨网络通信的文件。
设备管理:unix io接口
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,成为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
1)打开和关闭文件
open()和close()
2)读写文件
read()和write()
3)改变当前的文件位置
指示文件要读写位置的偏移量。
lseek()
8.2 简述Unix IO接口及其函数
Unix IO 接口,使得所有的输入和输出都能以一种统一且一致的方式来执行:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,即描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件中的常量可以代替显式的描述符值。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发EOF条件,应用程序能检测到这个条件。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
(1)进程通过调用open函数打开一个存在的文件或者创建一个新文件。
int open(char* filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;mode参数指定了新文件的访问权限位。
(2)进程通过调用close函数关闭一个打开的文件。
int close(int fd);
fd是需要关闭的文件描述符,成功返回0,错误返回-1。关闭一个已关闭的描述符会出错。
(3)应用程序通过分别调用read和write函数来执行输入和输出。
ssize_t read(int fd,void *buf,size_t n);
ssize_t wirte(int fd,const void *buf,size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
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;
}
printf函数中调用了vsprint和write两个函数。
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall等,将buf中的i个字符写到终端,由于i保存的是结果字符串的长度,因此write将格式化后的字符串结果写到终端。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
最后程序返回我们实际输出的字符数量i。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
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;
}
getchar函数会从stdin输入流中读入一个字符。调用getchar时,会等待用户输入,输入回车后,输入的字符会存放在缓冲区中。第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar的返回值是读取字符的ASCII码,若出错则返回-1。
getchar的底层实现是通过系统函数read实现的。getchar通过read函数从缓冲区中读入一行,并返回读入的第一个字符,若读入失败则返回EOF。read的具体实现如下:
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法和Unix I/O接口及其函数。对printf和getchar两个函数的实现进行了分析。通过本章对Unix I/O有了更深的理解,对hello的实现有了更深的领悟。
(第8章1分)
结论
过程:
- 编程:得到hello.c的c代码。
- 预处理:预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。得到hello.i文件。
- 编译:编译器将文本文件hello.i翻译成汇编语言文件hello.s。
- 汇编:汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
- 链接:链接器将hello.o与其他可重定位目标文件和动态链接库链接成为可执行文件。
- 执行:在终端中输入./hello 120L020212 曾正维 1。
- 创建子进程:由于输入的命令不是内置的shell命令,因此调用fork函数创建一个子进程。
- 加载程序:shell调用execve函数,加载并运行hello,映射虚拟内存。
- 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。 10.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
- 信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
- 终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
感悟:
hello从诞生到结束,经历了千辛万苦,在硬件、操作系统、软件的相互协作配合下,终于完美地完成了它的使命。这让我认识到,一个复杂的系统需要多方面的协作配合才能更好地实现功能。计算机系统中无不充斥着抽象这一概念,进程是抽象,虚拟内存是抽象、I/O也是抽象。同时,计算机系统提供的一系列抽象使得实际应用与具体实现相互分离,可以很好地隐藏实现的复杂性,降低了程序员的负担,使得程序更加容易地编写、分析、运行。这让我认识到抽象是十分重要的,是计算机科学中最为重要的概念之一。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名 | 作用 |
hello.i | 预处理后的文件,用于分析预处理过程 |
hello.s | 经过编译生成的汇编文件,用于分析编译的过程 |
hello.o | 汇编生成的可重定位目标文件,用于分析汇编的过程。 |
hello | 链接生成的可执行文件,用于分析链接的过程。 |
hello.elf | hello.o的elf文件,分析可重定位目标文件hello.o |
hello_asm.txt | 将hello.o反汇编生成的文件,分析可重定位目标文件hello.o |
hello1.elf | hello的elf文件,分析可执行目标文件hello |
hello_asm1.txt | 将hello反汇编生成的文件,分析可执行目标文件hello |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
(参考文献0分,缺失 -1分)