计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L022124
班 级 2003007
学 生 田茂尧
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本文介绍了hello的整个生命历程。主要是通过纵观hello生命中每一环节的变化来感受到程序的魅力。其中我们使用了gcc,edb,gdb等工具辅助观察hello变化的过程,一步步从源程序开始过渡到可执行程序(p2p-program to process),再从一个子进程开始开始hello自己的发挥,直到生命的终结(020-zero to zero)。通过对hello这个简单程序的详细分析,我们能够更加深入地理解计算机系统。
关键词:hello;程序;p2p;020;
目 录
第1章 概述
1.1 Hello简介
Hello的P2P(From Program to Process)过程:首先在IDE(codeblock)中编写C语言代码,得到最初的hello.c,即最初的Program。编译器驱动程序(也可cmd)按顺序调用语言预处理器、编译器、汇编器和链接器。首先运行C预处理器(cpp),将C的源程序hello.c翻译成一个ASCII码的中间文件hello.i;然后运行C编译器(cc1)将中间文件翻译成一个ASCII汇编语言文件hello.s;之后运行汇编器(as)将汇编语言文件翻译成可重定位目标文件hello.o;最后运行链接器(ld)创建一个可执行目标文件hello.out。在shell中执行hello,通过fork创建一个子进程来执行hello,此时hello已经成为Process了。
Hello的020(From Zero-0 to Zero-0)过程:从zero开始,子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,然后使用mmap函数创建新的内存区域,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片, 新的代码和数据段被初始化为可执行文件的内容。CPU以流水线形式读取并执行指令,执行逻辑控制流。操作系统负责进程调度,为进程分时间片。执行过程中通过L1、L2、L3高速缓存、TLB、多级页表等进行存储管理,通过I/O系统进行输入输出。当程序运行结束,通过信号处理机制回收子进程,结束hello整个生命周期,以zero结束。
1.2 环境与工具
X64 CPU;2.10GHz;16G RAM;512GHD Disk
软件环境
Windows11 64位;Vmware 15;Ubuntu 20.04 LTS 64位;
开发工具
Visual Studio 2022 64位;CodeBlocks 64位;gedit+gcc
1.3 中间结果
hello.c:源代码
hello.i:hello.c预处理生成的文本文件。
hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序。
hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello.out:链接器产生的可执行目标文件,用于分析链接的过程。
hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
hellold.txt:hello的反汇编文件,用于分析可执行目标文件hello。
helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
helloldelf.txt:hello的ELF格式,用于分析可执行目标文件hello。
4 本章小结
本章简述了Hello的P2P、020的整个过程并介绍了进行此次实验的,环境、工具以及总结了实验中间生成的文件。
第2章 预处理
2.1 预处理的概念与作用
常用的预处理解析:
- #define:define常用来定义常量和字符串常量。
- #include:是将多个源文件连接成一个源文件进行编译,结果就生成一个目标文件(obj)。用尖括号括起来的头文件一般都是系统自带的,表示系统将在指定的路径进行寻找。双引号一般则用于我们自己编写的头文件,系统也会优先在当前目录中查找。
- 条件编译:对源程序中一部分内容只在满足一定条件时才进行编译,即指定编译的条件。可以按不同的条件去编译不同的程序部分,从而产生不同的目标代码文件。
- #Error:它的作用人如其名,是用来提示错误的,编译程序时如果遇到#error就会生成一个编译错误提示信息并停止编译。关于提示的错误信息都是系统定义好的。
2.2在Ubuntu下预处理的命令
预处理命令:cpp hello.c > hello.i或gcc –E hello.c –o hello.i
预处理过程:
2.3 Hello的预处理结果解析
预处理前hello.c
预处理得到的hello.i
解析:将源文件中以”include”格式包含的文件复制到编译的源文件中,例如<stdio.h>即打开了指定路径中的stdio.h文件,将其内容加入在了hello.i的main之前。并且文本中还存在很多条件编译,并且删掉了文件中的所有注释,但main中内容并没有发生改变,并且仍旧是c语言的表达,可以看成是对头文件源代码的引入。
2.4 本章小结
本章介绍了预处理的概念和作用,结合操作hello.c预处理生成hello.i,分析了预处理的过程,接触到头文件引入、条件编译等预处理手段。
第3章 编译
3.1 编译的概念与作用
编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。
编译器的第一个步骤称为词法分析或扫描。词法分析器读入组成源程序的字符流,并将其组成有意义的词素的序列。 编译的第二个步骤称为语法分析或解析。语法分析器使用由词法分析器生成的各词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元的语法结构。编译的第三个步骤为语义分析,语义分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致 。编译的第四个步骤是中间代码生成,在源程序的语法分析和语义分析完成之后(也会生成中间表示,区别语法树),很多编译器生成一个明确的低级的或类机器语言的中间表示。该中间表示有两个重要的性质:1.易于生成;2.能够轻松地翻译为目标机器上的语言。编译的第五个步骤为代码优化,代码优化试图改进中间代码,以便生成更好的目标代码。即更快(省时),更短(省空间)或能耗更低。编译的第六个步骤为代码生成,代码生成以中间表示形式作为输入,并把它映射为汇编语言。
3.2 在Ubuntu下编译的命令
编译命令:cc1 hello.i –o hello.s或gcc –S hello.i –o hello.s
编译过程:
3.3 Hello的编译结果解析
3.3.1 数据
(1)常量
hello.c源程序中的两个printf的参数是字符串常量,分别为"用法: Hello 学号 姓名!\n"和"Hello %s %s\n"。
在编译生成的hello.s中可以看到,这两个字符串常量分别由.LC0和.LC1指示,均存放在只读数据段.rodata中。
(2)局部变量
源代码中:
汇编代码:
1为move语句初始化i为0,i被存在栈顶指针-4的内存位置;
2为跳转至L3后i与7进行比较,小于等于即跳转至L4,进入循环;
3为循环结束,add语句为i加一;
易被遗忘的局部变量:argc与*argv。
由比较语句可知agrc存储在栈顶指针-20内存位置,则*agrv在-32内存位置;
3.3.2 赋值操作
在不考虑优化的前提下,所有的赋值操作都转化成mov类的数据传送指令。指令的后缀取决于操作数据的字节大小。对i赋值,由于i为四字节,因此使用指令movl.
3.3.3 算数操作
hello.c源程序中只包含一次算术操作,出现在循环变量i每次增加1的时候。算术操作为++。
add类的加法指令,使用立即数1来实现每次增加1。
3.3.4 关系操作
判断argc是否等于4;
Cmpl进行比较立即数4与栈中所存数关系,并设置条件码。跳转指令je根据条件码决定是否跳转。若相等跳转L2即跳过if语句;
判断i是否小于8;
汇编代码:
Cmpl进行比较立即数7与栈中所存数关系,并设置条件码。跳转指令jle根据条件码决定是否跳转。若大于则跳转L4即跳出for语句;这里进行比较的值是7而不是8,与编译的过程中进行了优化有关。
3.3.5 数组操作
有关数组的操作出现在访问argv元素的时候,通过argv[1],argv[2],argv[3]访问了字符指针数组中的元素。
汇编代码中使用首地址+偏移量的方式来访问数组元素,数组首地址存储在%rbp-32的位置,通过将首地址加8获得argv[1]的地址,将首地址加16获得argv[2]的地址,同理获得argv[3]的地址。
3.3.6 控制转移
if判断argc的取值后的控制转移:
每次for循环结束时的控制转移:
3.3.7 函数操作
参数传递:大部分的参数传递通过寄存器实现,通过寄存器最多传递6个参数,按照顺序依次为%rdi、%rsi、%rdx、%rcx、%r8、%r9。多余的参数通过栈来传递。
汇编代码:
第一个printf与exit分别依靠一个寄存器传递参数;
第二个printf依靠寄存器%rdi传递参数;
Atio与sleep函数分别依靠一个寄存器传递参数;
Getchar不需要传递参数;
函数调用:及以上提及的相关函数;
函数返回:
返回值存在%eax中;
3.4 本章小结
本章介绍了编译的概念和作用,并针对具体的例子hello.s,详细地分析了编译器如何处理C语言环境下的包括:常量,数据结构,函数等。
第4章 汇编
4.1 汇编的概念与作用
汇编的过程将编译生成的ASCII汇编语言文件hello.s翻译成一个可重定位目标文件hello.o。可重定位目标文件包含指令对应的二进制机器语言,这种二进制代码能够被计算机理解并执行。因此汇编是将汇编语言转换成最底层的、机器可理解的机器语言的过程。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
4.3.1 ELF头
使用命令readelf -a hello.o > helloelf.txt查看hello.o的ELF格式,并将结果重定向到helloelf.txt便于查看分析。
如图,ELF头以一个16字节的目标序列开始,Magic这个序列描述了生成该文件的系统的字的大小和字节顺序。这个16字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节(两个16进制数),字节顺序为小端序。
4.3.2 节头
[1].text节:已编译程序的机器代码,大小为0x92字节,类型为PROGBITS,偏移量为0x40,标志为AX(表明该节的数据只读并且可执行),对齐量1字节。(后面节同理)
[2] .rela.text节:.text节的重定位信息,大小为0xc0字节,类型为RELA,偏移量为0x388,标志为I。
[3].data节:已初始化的全局和静态C变量,大小为0x0字节(hello.c源代码中无该类型变量),类型为PROGBITS,偏移量为0xd2,标志为WA(表明该节的数据可读可写)。
[4].bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。大小为0x0字节(同理),类型为NOBITS,偏移量为0xd2,标志为WA(表明该节的数据可读可写)。
[5].rodata节:只读数据,大小为0x33字节,类型为PROGBITS,偏移量为0xd8,标志为A(表明该节的数据只读)。
[6].comment节:包含版本控制信息,大小为0x2c字节,类型为PROGBITS,偏移量为0x10b,标志为MS。
[11].symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。大小为0x1b0字节,类型为SYMTAB,偏移量为0x160。
- strtab节:一个字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字。大小为0x48字节,类型为STRTAB,偏移量为0x340。
- shstrtab节:包含节区名称,大小为0x74字节,类型为STRTAB,偏移量为0x460。
4.3.3符号表
符号表存放程序中定义和引用的函数和全局变量的信息,每个符号表是一个条目的数组,每个条目包括value:距定义目标的节的起始位置的偏移;size:目标的大小;type:指明数据还是函数;bind:表示符号是本地的还是全局的等等。
如图,符号表一共描述了17个符号。例如num=3的表,Ndx=3表明它在.data节,value=0表明它在.data节中偏移量为0的地方,size=0表明大小为0字节,bind=local表明它是本地符号,type=section。而对于函数main,Ndx=1表明它在.text节,value=0表明它在.text节中偏移量为0的地方,size=146表明大小为146字节,bind=GLOBAL表明它是全局符号,type=FUNC:表明它是函数。其他的符号如puts、exit、printf、sleep和getchar都是外部的库函数,需要在链接后才能确定,所以type为notype。
4.3.4重定位节
汇编器遇到对最终位置未知的目标引用,会产生一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位信息就放在重定位节.rel.text中。
每个重定位条目包括offset:需要被修改的引用的节偏移;sym.Name:标识被修改引用应该指向的符号;type:重定位类型(两种最基本的重定位类型包括R_X86_64_PC32(重定位使用32位PC相对地址的引用)和R_X86_64_32(重定位使用32位绝对地址的引用)),告知链接器如何修改新的引用;attend:一些重定位要使用它对被修改引用的值做偏移调整,例如attend=-4,则可理解为引用时占据4个系统的字。
4.4 Hello.o的结果解析
反汇编文件:
编译文件:
差异:
- 操作数不同:编译文件中立即数表示为10进制,而反汇编得到汇编代码立即数用16进制表示,这是因为在机器语言中,都转化为了2进制表示。
- 分支跳转不同:编译文件中给出了明确的跳转符号指明跳转位置(例如L1),而反汇编文件中给出的是相对程序初识地址的偏移量(例如<main+2f>即代表跳转至main开始位置偏移0x2f个字节位置)。
- 函数调用表示不同:编译文件中call直接声明调用函数名即可,而在反汇编文件中给出的是调用函数所在pc的相对位置,例如函数getchar,这里给出了attend=-4,e8 00 00 00 00 ,因为其为外部库函数,还未链接重定位其相对位置,即表示为0,所以此时引用位置为<main+0x8b>。
4.5 本章小结
本章介绍了汇编的概念和作用,通过对比hello.s和hello.o分析了汇编的过程,同时分析了可重定位目标文件的ELF格式,重点理解了文件中的节头表,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命令readelf -a hello > helloldelf.txt查看可执行目标文件hello的ELF格式,并将结果重定向到helloldelf.txt便于查看分析。
5.3.1 ELF头
ELF头以一个16字节的目标序列开始,如图所示,这个序列描述了生成该文件的系统的字的大小和字节顺序。这个16字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节,字节顺序为小端序。
ELF头剩下的部分包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。ELF头的大小:64字节;目标文件的类型:EXEC(可执行文件);节头部表的文件偏移:14208bytes;节头部表中条目的数量:27。同时,ELF头中还包括程序的入口点(偏移量64字节),即程序运行时要执行的第一条指令的地址。
5.3.2 节头表
相较于可重定位文件的节头表,可执行文件的节头表中主要多出来.init节,其用于定义_init函数,该函数的功能是:进行可执行目标文件开始执行时的初始化工作。
5.3.3 程序头表
程序头部表描述了可执行文件的连续的节映射到连续的虚拟内存段的映射关系。包括目标文件的偏移、段的读写/执行权限、内存的开始地址、对齐要求、段的大小、内存中的段大小等。其中两个可装入段(type=Load)。
如图所示,第二个LOAD,Offset说明段的偏移量为1000;VirtAddr说明映射到的虚拟内存段的开始地址是0x401000,physaddr说明物理地址开始地址是0x401000;FileSiz说明段的大小为0x245字节;Memsiz说明内存中的段大小也是0x245字节;Flags为R E,标志段的权限为只读且可执行;Align说明段的对齐要求为0x1000字节(4kb)。该段一般包括(ELF头,程序头表,.init,.text,.rodata节)
第四个load同理,标志段权限为读写,一般包括.data,.bss节。
5.3.4 符号表
符号表存放程序中定义和引用的函数和全局变量的信息,每个符号表是一个条目的数组,每个条目包括value:距定义目标的节的起始位置的偏移;size:目标的大小;type:指明数据还是函数;bind:表示符号是本地的还是全局的,ndx表明该符号处于哪个节中(1,2,3......)。
如图,hello的符号表一共描述了51符号,比hello.o多出24个符号。多出的符号都是链接后产生的库中的函数以及一些必要的启动函数。
还多出了一个动态符号表(9个),表中的符号都是共享库中的函数(例如printf),需要动态链接(一般为执行时实现)。
5.3.5 重定位表
这些重定位条目都和共享库中的函数有关,因为此时还没有进行动态链接,共享库中函数的确切地址仍是未知的,因此仍然需要重定位节,在动态链接后才能确定地址。而原来对.text的重定位已经完成,所没有这个部分。
5.4 hello的虚拟地址空间
由5.3中的节头部表可以获得各个节的偏移量信息,从而得知各节在虚拟地址空间中的地址。
例如其中的.rodata节,节头信息如下所示:偏移量0x2000,位于0x402000,其大小为0x3b,在虚拟地址中找到该位置,里面所存即两个字符串常量。
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > hellold.txt对hello进行反汇编,并将结果重定向到hellold.txt中便于查看分析。差异主要为以下几点:
(1) hello中的汇编代码已经使用虚拟内存地址来标记了,从0x400000开始;而hello.o中的汇编代码永远从0开始的。
(2)在hello.o中,只存在main函数的汇编指令;而在hello中,由于链接过程中发生重定位,引入了其他库的各种数据和函数,以及一些必需的启动/终止函数(例如.init),因此hello中除了main函数的汇编指令外,还包括大量其他的指令。
(3)main函数中涉及重定位的指令的二进制代码被修改。在之前汇编的过程中,汇编器遇到对最终位置未知的目标引用,会产生一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。因此在链接的过程中,链接器会根据重定位条目以及已知的最终位置对修改指令的二进制码,这个过程就是重定位的过程。
Hello.o中重定位信息:
这里涉及到两种不同的重定位类型,分别是R_X86_64_PC32(重定位使用32位PC相对地址的引用--引用数据)和R_X86_64_PLT32(重定位使用32位相对地址的引用--调用函数)。对于第一种重定位类型,以第一个条目为例,第一个条目的信息说明需要重定位的位置在.text中偏移量为0x1c的地方。
观察hello.o的反汇编文件,找到该偏移位置;
这条指令的目的是将某一个数传送到%rdi中,使其作为printf的参数。由源程序可知,这个指令对应与语句为 printf("用法: Hello 学号 姓名 秒数!\n"); 因此参数应该是字符串常量"用法: Hello 学号 姓名 秒数!\n"的地址。由于字符串常量的最终位置未知,因此产生了一个重定位条目。而重定位的目的就是修改这个数据,使得传入%rdi的是"用法: Hello 学号 姓名 秒数!\n"的最终地址。同时,重定位类型为R_X86_64_PC32,因此地址为PC相对地址,此时(%rip)中存的应该是当前指令语句的地址。查看hello的反汇编结果,如下:
由5.4中查看虚拟地址可知该字符串常量地址在0x402008,与图中相符,因为该处为相对地址,而对应的下一条语句地址为0x401145,因此相对地址为0x402008 – 0x401145 = 0xec3。因此重定位会将数据修改为0x0e3c(在机器代码中为小端表示)。
对于第二种重定位类型,以第二个条目为例,第二个条目的信息说明需要重定位的位置在.text中偏移量为0x21的地方。
观察hello.o的反汇编文件,找到该偏移位置;
这条指令的目的是调用函数puts。由于函数puts的最终位置未知,因此产生了一个重定位条目。而重定位的目的就是修改这个数据,使得call指令的地址为puts函数的起始地址。同时,重定位类型为R_X86_64_PLT32,因此地址为相对地址。从hello的反汇编结果可以获得puts函数的地址为0x401090,下一条语句地址为0x40114a,所以相对地址为0x401090-0x40114a=0xffffff46,指令后四个字节即存为相对位置(小端表示)。
5.6 hello的执行流程
如下图所示(按顺序)
5.7 Hello的动态链接分析
在程序中动态链接是通过延迟绑定来实现的,延迟绑定的实现依赖全局偏移量表GOT和过程连接表PLT实现。GOT是数据段的一部分,PLT是代码段的一部分。
PLT数组中每个条目时16字节,PTL[0]是一个特殊的条目,他跳转到动态链接器中。每个可被执行程序调用的库函数都有自己的PLT条目。PLT[1]调用__libc_start_main函数负责初始化。
GOT数组中每个条目八个字节。GOT[0]和GOT[1]中包含动态链接器解析地址时会用的信息,GOT[2]时动态练级去在ld-linux.so模块的入口点。其余的每一个条目对应一个被调用的函数。
通过5.3.2的节头表我们可以找到.GOT.PLT的数据从0x404000开始。
初始化前:
初始化后:
经过初始化后,PLT和GOT表就可以协调工作,一同延迟解析库函数的地址了。
5.8 本章小结
本章介绍了链接的概念与作用,简要分析了可执行文件的ELF格式(比较与可重定位文件的差异),hello的虚拟地址空间和执行流程(利用edb),同时详细地分析了静态链接的重定位过程以及动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例,即程序的一次运行过程。
作用:进程提供给应用程序两个关键抽象
逻辑控制流 (Logical control flow):
每个进程似乎独占地使用CPU ,通过OS内核的上下文切换机制提供
私有地址空间 (Private address space) :
每个进程似乎独占地使用内存系统 ,OS内核的虚拟内存机制提供
6.2 简述壳Shell-bash的作用与处理流程
作用:shell 是一个交互型应用级程序,代表用户运行其他程序
shell执行一系列 的读/求值步骤,读步骤读取用户的命令行,求值步骤解析命令,代表用户运行。
处理流程:从终端读入输入的命令行->解析输入的命令行,获得命令行指定的参数->检查命令是否是内置命令,如果是内置命令则立即执行,否则在搜索路径里寻找相应的程序,找到该程序就执行它。
6.3 Hello的fork进程创建过程
当在shell中输入命令,shell解析输入的命令行,在这里按空格分割参数,获得命令行指定的参数。由于./hello不是shell内置的命令,因此shell将hello看作一个可执行目标文件,在相应路径里寻找hello程序,找到该程序就执行它。shell会通过调用fork()函数创建一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同但独立的一个副本,包括代码段、数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,子进程可以读写父进程中打开的任何文件。父进程和子进程之间最大的区别在于它们的PID不同。
6.4 Hello的execve过程
Shell创建一个子进程之后,需要在子进程中调用exceve()函数在当前进程的上下文中加载并运行我们需要的hello程序,覆盖当前进程的代码、数据、栈。execve函数加载并运行可执行文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
具体步骤如下:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序(即hello)的代码、数据、bss和栈区域等创建新的区域结构。所有这些区域都是私有的、写时复制的。
(3)映射共享区域。
(4)设置程序计数器。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。当内核调度这个进程时,它就将从这个入口点开始执行。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文切换: 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
由于负责进程调度的是内核,因此内核调度需要运行在内核模式下。当内核代表用户执行系统调用时,可能会发生上下文切换,中断也可能引发上下文切换。同时,系统通过某种产生周期性定时器中断的机制判断当前进程已经运行了足够长的时间,并切换到一个新的进程。
以hello的进程执行为例。当子进程调用exceve()函数在上下文中加载并运行hello程序后,hello进程等待内核调度它。当内核决定调度hello进程时,它就抢占当前进程,进行上下文切换,将控制转移到hello进程,并从内核模式变为用户模式,这时hello进程开始运行应用程序代码。当hello进程调用sleep时,由于sleep是系统调用,进程陷入内核模式。内核会选择调度其他进程,通过上下文切换保存hello进程的上下文,将控制传递给新调度的进程。当内核再次调度hello进程时,恢复保存的hello进程的上下文,就可以从刚才停止的地方继续执行了。调用getchar的时候同样会陷入内核模式,由于getchar需要等待来自键盘的传输,内核会去调度其他进程。当键盘输入完成后,会向处理器发送中断信号,进入内核模式,就可以再次调度hello进程了。同时,系统还会为hello进程分配进程时间片,简单地说即时间片被消耗完,尽管没有出现系统调用或中断引发调度其他进程,内核也会判断时间过长,进而选择上下文切换将控制交给其他进程。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 异常
异常分类如下:
在运行hello中出现的异常情况:
中断:可能会出现定时器触发中断使得内核从用户端取回控制权使得hello进程中断,或者外部设备I/O引发的(例如键盘上敲击crtl-c);
陷阱:系统调用sleep()和getchar()都会造成陷阱;
故障:第一次调用hello时映射物理地址会发生缺页故障,但可以修复;
终止:输入了非法的指令会导致终止的发生;
6.6.2 信号
(1)运行过程中不断按键盘(回车及字母键等),不会影响进程的进行,敲进去的第一个额外字符会被进程中getchar读取掉,剩下的会留在缓存区等待hello进程结束,作为对shell的命令行输入。
- 键盘输入ctrl-z,使得进程挂起(stopped),ps查看进程pid,jobs查看作业状态,处于挂起,pstree显示hello为shell的子进程,fg使其回复前台工作;
- 键盘上输入ctrl-c,hello进程被终止;
- 发送kill信号,杀死pid为5241的hello进程;
6.7本章小结
本章介绍了进程的概念和作用,简述shell的工作原理,并分析了使用fork+execve的组合运行hello,执行hello进程以及hello进程运行时的异常情况及信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:包含在机器语言指令中用来指定一个操作数或一条指令的地址,每个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。例如我们常说的结构体中某个参数的地址其实就相当于:结构体首地址 + 偏移量。逻辑地址是相对于应用程序而言的,例如在hello程序中,其中的访问argv数组即是依靠寻找逻辑地址。
线性地址/虚拟地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和段内偏移量两部分组成。段标识符由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,是对段描述符表的索引,每个段描述符由8个字节组成,具体描述了一个段。后3位包含一些硬件细节,表示具体是代码段寄存器还是栈段寄存器还是数据段寄存器等。通过段标识符的前13位,可以直接在段描述符表中索引到具体的段描述符。每个段描述符中包含一个Base字段,它描述了一个段的开始位置的线性地址。将Base字段和逻辑地址中的段内偏移量连接起来就得到转换后的线性地址。
对于全局的段描述符,放在全局段描述符表中,局部的(每个进程自己的)段描述符,放在局部段描述符表中。全局段描述符表的地址和大小存放在gdtr控制寄存器中,而局部段描述符表存放在ldtr寄存器中。
给定逻辑地址,看段选择符的最后一位是0还是1,用于判断选择全局段描述符表还是局部段描述符表。再根据相应寄存器,得到其地址和大小。通过段标识符的前13位,可以在相应段描述符表中索引到具体的段描述符,得到Base字段,和段内偏移量连接起来最终得到转换后的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
依靠页表的地址变换(虚拟内存→物理内存)
Mmu收到cpu传来的虚拟地址vp,请求得到该地址所在页表,根据VPN在页表中找到相应页面,如若该页面的有效位为1,则pte中存有与虚拟内存相对应的物理页号(ppn),并通过vpo与ppo总是保持一致从而得到物理地址。如果有效位为0,则代表页面不在存储器中,则进行缺页处理。
页表条目格式如下:
7.4 TLB与四级页表支持下的VA到PA的变换
在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须依次访问四个PTE。
7.5 三级Cache支持下的物理内存访问
Intel Core i7使用了三级cache来加速物理内存访问,L1级cache作为L2级cache的缓存,L2级cache作为L3级cache的缓存,而L3级cache作为内存(DRAM)的缓存。由va获得pa后,首先我们利用其中的CI进行组索引,得到匹配的组后,按照标志位CT的内容进行匹配,如果匹配成功并且有效位为1,则命中,即可按照偏移量取出数据。如果不命中,就要向下一级的cache寻找数据,三级都没有,则要向主存中寻找。找到之后更换cache中的空闲块,若没有空闲块,则需要根据自己的策略来驱逐一个块来更新。
7.6 hello进程fork时的内存映射
Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容,这个过程叫作内存映射。
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。
为新进程创建虚拟内存:
创建当前进程的的mm_struct, vm_area_struct和页表的原样副
本.;两个进程中的每个页面都标记为只读;两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW);在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存;随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行新程序Hello.out的步骤:
(1删除已存在的用户区域
(2创建新的区域结构代码和初始化数据映射到.text和.data区(目标文件提供).bss和栈映射到匿名文件
(3设置PC,指向代码区域的入口点 (Linux根据需要换入代码和数据页面)
7.8 缺页故障与缺页中断处理
1) 处理器将虚拟地址发送给 MMU
2-3) MMU 使用内存中的页表生成PTE地址
4) 有效位为零, 因此 MMU 触发缺页异常
5) 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
6) 缺页处理程序调入新的页面,并更新内存中的PTE
7) 缺页处理程序返回到原来进程,再次执行导致缺页的指令
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存. 数据结构的大小只有运行时才知道.
¢ 动态内存分配器维护着一个进程的虚拟内存区域,称为堆.
基本方法:
方法 1: 隐式空闲链表 (Implicit list) 通过头部中的大小字段—隐含地连接所有块
方法 2: 显式空闲链表 (Explicit list) 在空闲块中使用指针
方法 3: 分离的空闲列表 (Segregated free list) 按照大小分类,构成不同大小的空闲链表
方法 4: 块按大小排序 在每个空闲块中使用一个带指针的平衡树,并使用长度作为权值
放置策略:
§ 首次适配, 下一次适配, 最佳适配等等。
§ 减少碎片以提高吞吐量
§ 有趣的观察 : 近似于最佳适配算法,独立的空闲链表不需要搜索整个空闲链表
¢ 分割策略:
§ 什么时候开始分割空闲块,能够容忍多少内部碎片
¢ 合并策略:
§ 立即合并 (Immediate coalescing): 每次释放都合并;
§ 延迟合并 (Deferred coalescing): 尝试通过延迟合并,即直到需要才合并来提高释放的性能.例如: 为 malloc扫描空闲链表时可以合并;外部碎片达到阈值时可以合并
7.10本章小结
本章总结了hello运行过程中有关内存管理的内容。重点在于由虚拟地址映射到物理地址的过程,其中囊括了TLB、多级页表支持下的地址翻译、cache支持下的内存访问,还研究缺页的处理、fork+execve过程的内存映射以及动态存储分配的过程。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:B0,B1 ,B2……Bm-1
所有的 IO 设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux内核引出一个简单、低级的应用接口,称为 Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
8.2.1. 接口的操作
(1)打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
(2)shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
(3)改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。
(5)关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2 函数
Open:
Close:
Read:
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;
}
printf函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。printf中调用了两个函数,分别为vsprintf和write。
vsprintf函数根据格式串fmt,并结合args参数产生格式化之后的字符串结果保存在buf中,并返回结果字符串的长度。
write函数将buf中的i个字符写到终端,由于i保存的是结果字符串的长度,因此write将格式化后的字符串结果写到终端。
字符显示驱动子程序:从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;
}
getchar函数会从stdin输入流中读入一个字符。调用getchar时,会等待用户输入,输入回车后,输入的字符会存放在缓冲区中。第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar的返回值是读取字符的ASCII码,若出错则返回-1。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法和Unix IO接口及其函数,并分析了printf和getchar函数的实现。
结论
编写得到hello.c源程序;
hello.c经过预处理得到hello.i;
hello.i经过编译得到汇编文件hello.s;
hello.s经过汇编得到可重定位目标文件hello.o;
hello.o经过链接得到可执行文件hello.out;
输入命令 ./hello 120L022124 田茂尧 1 运行程序;
shell调用fork函数创建子进程,并调用execve函数加载运行hello程序
cpu将为其分配时间片,在自己的时间片里,hello顺序执行自己的逻辑控制流;
hello执行过程中,会访问内存,请求一个虚拟地址,再由MMU将其转化为物理地址(其中涉及tlb表,cache,主存,磁盘的访问);
运行过程中,同时也会调用一些函数,例如printf函数,这些函数与linux I/O的设备模拟化密切相关
运行过程中,还会遇到各种各样的信号,shell为其准备了各种的信号处理程序(比如系统调用会触发异常处理程序);
最后hello程序结束,发送信号让父进程回收。内核会收回它的所有信息。由此,hello结束了它的一生。
附件
hello.i:C预处理器产生的一个ASCII码的中间文件,用于分析预处理过程。
hello.s:C编译器产生的一个ASCII汇编语言文件,用于分析编译的过程。
hello.o:汇编器产生的可重定位目标程序,用于分析汇编的过程。
hello:链接器产生的可执行目标文件,用于分析链接的过程。
hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
hellold.txt:hello的反汇编文件,用于分析可执行目标文件hello。
helloelf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
helloldelf.txt: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.