计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
学 号 8200880130
班 级 2036011
学 生 朱容志
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
HELLOWORLD是每个程序员第一次接触到计算机时所学习的程序,他打开了未知的计算机世界的大门,也是我们学习的起点,现在让我们回到起点,去探究蕴含在HELLOWORLD程序中无数计算机系统设计者的思想精华。
本文主要通过分析hello.c的程序,回顾本学期计算机系统这门课所学习的大部分知识,主要以ubuntu为操作系统,分析hello.c从c语言程序经过预处理、编译、汇编、链接生成可执行文件的过程,及该过程中存在的进程管理,存储管理,I/O管理,也即通过对hello一生周期的探索,学习计算机系统基础知识。
关键词:计算机系统;P2P;O2O;预处理;编译;汇编;链接;进程;存储;I/O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目录
6.2 简述壳Shell-bash的作用与处理流程 - 28 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 33 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 34 -
7.7 hello进程execve时的内存映射 - 36 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:首先,Hello开始是存储在磁盘上的程序文本(program),在需要运行这个代码文件时,预处理器处理hello.c文件,生成一个hello.i,之后hello.i通过编译器生成了hello.s文件,接着输入到汇编器,产生一个hello.o,经过链接器的链接将生成可执行目标程序hello,此时在shell中调用相关命令创建进程(process),执行进程。
O2O:在shell中输入相关命令后,调用fork函数创建进程,之后将通过exceve在进程的上下文中加载并运行hello,将进程映射到虚拟内存空间,并加载需要的物理内存。Cpu分配其进入cpu流水线执行,结束后父进程回收该进程,内核清除相关信息,该进程结束。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:处理器:AMD Ryzen 7 4800H,2.90GHz
内存:16GB
系统:64位操作系统,基于x64的处理器
软件环境:windows10,Ubuntu20.04
开发与调试工具:gcc edb gedit gdb readelf ld
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件名称 | 用途 |
Hello.i | 预处理后的文件 |
Hello.s | 编译后的汇编程序 |
Hello.o | 可重定位目标文件 |
Hello | 可执行的目标程序 |
hello.o_elf.txt | Hello.o的elf格式 |
hello_elf.txt | Hello的elf格式 |
dump1.txt | Hello.o的反汇编程序 |
dump2.txt | Hello的反汇编程序 |
1.4 本章小结
本章对hello执行流程进行了一个总体的描述,并且阐明了实验中所使用的软硬件条件和中途会产生的中间结果文件的名字及作用。
第2章 预处理
2.1 预处理的概念与作用
概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——预处理记号(preprocessing token)用来支持语言特性(如宏调用)。
作用:
1.处理头文件:比如hello.c的第一行的#include<stdio.h>命令告诉预处理器读取系统有文件stdio.h的内容,并把它直接插入程序文本中。
2.处理宏定义:对于#define指令,进行宏替换
3.处理特殊符号:预编译程序可以识别一些特殊符号,并在后续中进行替换
4.处理条件编译:如#if等
5.预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
6.处理注释部分
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
图 2.2-1 预处理过程
2.3 Hello的预处理结果解析
观察生成的hello.i文件,我们可以发现23行的源代码被扩展到了3060行,最开始的一段代码是hello.c拼接的各种库文件。
图 2.3-1预处理结果
中间的代码,对很多内部的函数进行声明,而这些代码与头文件的库进行对比,可以知道cpp对头文件进行了预处理。
图 2.3-2预处理结果
在程序最后是源程序,且源程序前的注释部分不在了,说明预处理可以去除掉注释部分。
图 2.3-3预处理结果
2.4 本章小结
本章研究了C预处理的概念与作用。使用cpp对hello.c进行了预处理,并与源文件,头文件进行比对,更直观地观察了cpp的作用。我们浏览了hello.i的代码,对hello.i的内容有了感性认识。
第3章 编译
3.1 编译的概念与作用
概念:在编译阶段,编译器(cc1)能够将文本文件hello.i翻译成文本文件hello.s,hello.s由汇编代码构成。
作用:
- 扫描(词义分析):将源代码程序输入扫描器,将源代码中的字符序列分割为一系列c语言中的符合语法要求的字符单元,这一部分可以分为自上而下的分析和自下而上的分析两种方式。
- 语法分析:编译程序的语法分析器以单词符号作为输入生成语法分析树,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
- 语义分析:在语法分析完成之后由语义分析妻进行语义分析,主要就是为了判断指令是否是合法的c语言指令,这一部分也可以叫做静态语义分析,并不判断一些在执行时可能出现的错误,例如如果不存在IDE优化,这一步对于1/0这种只有在动态类型检查的时候才会发现的错误,代码将不会报错。
- 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码
- 代码优化:根据用户指定的不同优化等级对代码进行安全的、等价的优化,这一行为的目的主要是为了提升代码在执行时的性能
- 目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图 3.2-1 编译过程
3.3 Hello的编译结果解析
- 数字常量
通过观察我们可以发现在源代码中使用的数字常量都储存在.text段里,包括比较时使用的数字4和循环时的数字等都储存在.text中,
图 3.3-1 hello.o中的数字常量
- 字符串常量
可以发现在printf等函数中使用的字符串常量是储存在.rotate段的
图 3.3-2 hello.o中的字符串常量
- 全局变量
已经初始化并且初始值非零的全局变量储存在.data节,它的初始化不需要汇编语句,而是通过虚拟内存请求二进制零的页直接完成的。
- 局部变量
Main函数声明了一个局部变量i,循环中有i=0和i++的操作,从该操作我们可以找到在汇编程序中i的位置
图 3.3-3 hello.o中的局部变量i
可见i是被存储在-4(%rbp)中的
对于argc,我们可以看到程序中有它与4比较的语句,从该语句我们可以找到,argc被存储在-20(%rbp)中
图 3.3-4 hello.c中的局部变量argc
对于数组argv[],观察发现他存储在栈中,开始时数组指针只想-32(%rbp)
图 3.3-5 hello.o中的局部变量argv[]数组
使用数据传送命令,我们可以进行赋值操作。最简单形式的数据传输类型是MOV类,MOV有movb,movw,movl,movq。分别操作1、2、4、8字节的数据。mov操作的源操作数可以是:立即数、寄存器、内存。目的操作数可以是:寄存器、内存。
在hello程序中有一个赋值,即i=0,观察汇编代码:
图 3.3-6 hello.o中的赋值语句
也就是说,通过movl,我们将立即数0,放入i中(i是int型,占4个字节,与movl对应)。
Hello程序中atoi是一个显式类型转换,在汇编语言中,直接利用call指令调用atoi函数,来进行显示类型转换
图 3.3-7 hello.o中的类型转换
Hello程序中算术操作有i++,由于是i是int类型的,因此汇编代码只用addl就能实现其他的操作有
图 3.3-8 hello.o中的算术操作
该程序中不存在上述运算
对于关系操作,编译器一般会用cmp语句来进行,在源文件中有两个关系操作:
比较argc与4是否相等
图 3.3-9 hello.o中的关系操作1
上面我们已经知道-20(%rbp)中存放的是argc的值,所以该步就是在比较argc和4的大小,如果相等就发生跳转。
比较i与8的大小
图 3.3-10 hello.o中关系操作2
i<8转化为i<=7,这是等价的,所以就是i和7进行比较
源代码中对数组的操作,在编译器中会转换为对地址的加减操作,在源代码中存在着对数组argv[]的访问
图 3.3-11 hello.o中的数组访问
我们已知-32(%rbp)中存放着数组的头指针,将该地址传给rax,我们在程序中要获取argv[1],argv[2],argv[3]的值,argv[1]地址为-32(%rbp)+$8,argv[2]地址为-32(%rbp)+$16,argv[3]地址为-32(%rbp)+$24即进行了地址的加减操作以访问数组。
控制转移是指C语言源文件中的选择分支、循环结构等经过编译器的翻译,产生一些跳转的语句,在该汇编程序中控制转移有三处:
图 3.3-12 hello.o中的控制转移1
比较argc和4是否相等,如果相等就跳转到.L2中去
图 3.3-13 hello.o中的控制转移2
将i初始化为0,然后无条件跳转到.L3中去
图 3.3-14 hello.o中的控制转移3
比较i和7的大小,如果小于等于7就跳转到.L4中,即继续进行for循环。
函数调用一般会进行参数传递和返回值。在hello.s中,一共有六次函数调用。
调用exit函数,参数为1,在汇编语言中,首先将立即数1放到寄存器%edi中,然后执行call exit@PLT调用指令exit函数。
图 3.3-15 hello.o中的exit函数
调用puts函数,将所需打印的字符串存放在寄存器%rdi中,然后执行call puts@PLT指令打印字符串
图 3.3-16 hello.o中的puts函数
调用printf函数,首先进行参数的准备,将argv[2]存放在寄存器%rdx中,将argv[1]存放在寄存器%rsi中,将字符串“Hello %s %s\n”,存放在寄存器%rdi中,然后执行call printf@PLT 指令打印。
图 3.3-17 hello.o中的printf函数
调用atoi函数,参数为argv[3],首先进行参数的准备,将argv[3]放入寄存器%rdi中,然后执行call atoi@PLT指令调用atoi函数
图 3.3-18 hello.o中的atoi函数
调用sleep函数,将atoi的返回值放入寄存器%edi中,然后执行call sleep@PLT
图 3.3-19 hello.o中的sleep函数
调用getchar()函数,没有参数,直接call调用
图 3.3-20 hello.o中的getchar函数
3.4 本章小结
本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言。
第4章 汇编
4.1 汇编的概念与作用
概念:驱动程序运行汇编器as,将汇编语言的ascii码文件(这里是hello.s)翻译成机器语言的可重定位目标文件(hello.o)的过程称为汇编。
作用:汇编的作用是把汇编语言翻译成机器语言,用二进制码0、1代替汇编语言中的符号,即让它成为机器可以直接识别的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
图 4.2-1 汇编过程
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
首先查看hello.o的ELF格式,在终端中输入如下指令即可查看:readelf -a hello.o
图 4.3-1 查看elf格式
ELF格式的开头是ELF头,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包含了帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小,目标文件的类型等。
图 4.3-2 ELF头
节头部表包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
图 4.3-3 节头部表
ELF文件格式中的重定位节包含两个部分:.rela.text节与.rela.eh_frame节
hello.o需重定位:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar等符号。
其中不同名词表示的意思如下:
偏移量:需要重定位的信息的字节偏移位置
信息:重定位目标在.symtab中的偏移量和重定位类型
类型:表示不同的重定位类型
符号名称:被重定位时指向的串
加数:重定位过程中要使用它对被修改引用的值做偏移调整
图 4.3-3 重定位节
ELF文件格式中的符号表中存放了程序中所定义和引用的的全局变量以及函数的信息。(不包含局部变量)
图 4.3-4 符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o > dump1.txt
图 4.4-1 反汇编过程
可以看出每一句都有一串相应的地址和一串十六进制的指令
图 4.4-2 hello.o反汇编代码
与hello.s的反汇编代码比较有如下不同
在hello.s的反汇编文件中,我们可以看到立即数如4等都是十进制的,而在hello.o的反汇编文件中数都是16进制的
图 4.4-3 反汇编中的常数
hello.s中列出了每个段的段名,分支转移时,跳转指令后用对应的段的名称表示跳转位置;而在hello.o的反汇编代码中每个段都有明确的地址,跳转指令后用相应的地址表示跳转位置。
图 4.4-4 反汇编中的分支转移
在hello.s中都是通过call指令之后直接调用函数名来实现的,而在hello.o的反汇编代码中,call目标地址是下一条语句的地址,第二行为所调用的函数名,且可以看到操作数都为0.这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
图 4.4-5 反汇编中的函数调用
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
第5章 链接
5.1 链接的概念与作用
概念:链接(linking)是指将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行
作用:链接令分离编译成为可能,方便了程序的修改和编译:无需重新编译整个工程,而是仅编译修改的文件。链接还有利于构建共享库。源程序节省空间而未编入的常用函数文件(如printf.o)进行合并,生成可以正常工作的可执行文件。
5.2 在Ubuntu下链接的命令
ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
图 5.2-1 链接生成过程
生成hello文件如上图所示
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
利用如下命令可生成hello的elf格式。
readelf -a hello > hello_elf.txt
图 5.3-1 elf格式生成
同上我们可以列出hello的elf格式
ELF头:
图 5.3-2 elf头
从中我们可以看出此文件类型是一个可执行文件而不是重定位文件。
节头:
图 5.3-3 节头部表
节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。从中我们就可以获取各段的基本信息。其中第一列是按地址顺序列出的各段的名称大小,第三列为起始地址,最后一列为偏移量。
程序头:
图 5.3-4程序表
5.4 hello的虚拟地址空间
利用edb打开hello文件,在data dump中我们可以看到整个程序所占用的虚拟地址空间,可以看到起始于0x400000,且在该地址可以看到ELF标识。
图 5.4-1 起始地址
按程序头表所给出的地址我们就可以查看每个程序的信息,比如PDHR程序起始于0x400040,大小为0x2a0
图 5.4-2 PDHR地址
INTERP起始于0x4002e0,大小为0x1c
图 5.4-3 INTERP地址
DYNAMIC起始于0x403e10,大小为0x1e0
图 5.4-4 DYNAMIC地址
GNU_RELRO起始于0x403e00,大小为0x200
图 5.4-5 GNU_RELRO地址
5.5 链接的重定位过程分析
利用如下指令查看hello的反汇编代码
objdump -d -r hello > dump2.txt
与hello.o的反汇编代码进行对比,发现有如下几方面的不同:
hello的反汇编代码代码量远大于hello.o的反汇编代码,达到了248行
图 5.5-1 hello反汇编代码
链接加入了c语言库中的函数,如exit、printf、sleep、atoi、getchar等。
图 5.5-2 hello反汇编代码中新增函数
hello已经完成了重定位,所以函数的调用地址变更为准确的虚拟地址
图 5.5-3 hello反汇编代码中的重定位
由于每一句都有了自己确切的虚拟地址,所以跳转时调用的地址已经是语句确切的虚拟地址。
图 5.5-4 hello反汇编代码中的跳转指令
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址。
其主要过程包含两步,第一步是符号解析,解析目标文件定义和引用的符号,第二步为重定位,重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
0x7f42ff32ddf0 _dl_start
0x7f42ff33dc10 _dl_init
0x401090 puts@plt
0x4010a0 printf@plt
0x4010b0getchar@plt
0x4010c0atoi@plt
0x4010d0exit@plt
0x4010f0 _start
0x401120 _dl_relocate_static_pie
0x401130 _deregister_tm_clones
0x4011d6 main
0x401270 __libc_csu_init
0x4012e0 __libc_csu_fini
0x4012e8 _fini
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
由于编译器无法编译库函数的地址,因此提供了延迟绑定的方法,在第一次调用该过程时再来绑定该过程的地址,通过GOT和表PLT协作解析函数地址,加载时,动态链接器会重定位GOT中的每个条目,来得到正确的地址。
首观察elf中.got.plf的内容
图 5.7-1 .got.plt的地址
可以看到它的起始地址为0x404000且大小为0x48,然后在edb中查看该地址
图 5.7-2 edb中的起始内容
这是执行前的改地址的内容,然后我们执行后再查看该地址
图 5.7-3 edb中修改后的内容
可以看到0x404008开始的后面多个字节发生了变化,这里变化的字节就分别对应了GOT[1]和GOT[2]的位置,在和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点.
5.8 本章小结
本章主要介绍了链接的概念和作用,并详细阐述了hello.o是怎么链接成为一个可执行的程序的,分析了可执行程序hello的虚拟空间地址和个部分内容。使用edb执行hello,得到了在执行流程中所调用的函数,同时分析了在dl_init前后.got.plt节内容的变化,以此来分析动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是程序的一次执行,进程是程序及其数据在CPU下顺序执行时所发生的活动,进程是具有独立功能的程序在数据集上运行的过程,它是系统进行资源分配和调度的一个独立单位
作用:进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。可以说,如果没有进程,体系如此庞大的计算机不可能设计出来。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell能为用户提供一个交互界面,通常指的是命令行界面的解析器
用户通过这个界面能够访问操作系统内核。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)如果不是则调用fork()函数创建一个子进程
5)调用execve()函数在子进程的上下文中加载并运行hello程序,hello程序的内容会加载到当前进程的虚拟地址空间中。
6)调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行。
6.3 Hello的fork进程创建过程
Fork进程即为父进程通过调用fork()函数来创建一个新的子进程。子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
在fork调用时会返回两次,子进程返回0,父进程则返回子进程的PID
在shell中输入如下命令
./hello 8200880130 zrz 1
6.4 Hello的execve过程
在子进程创建完成后,exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表,当出现错误时,返回到调用程序,所以exceve函数调用后不返回。exceve加载完毕可执行目标文件hello后,会调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制转移给新程序的主函数。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程提供给应用程序的抽象,hello进程的执行依赖于进程提供的抽象的基础上:
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
再来看hello进程执行,在调用execve函数之后,进程已经为了hello程序分配了新的虚拟地址空间。最初hello运行在用户模式下,输出hello 8200880130 zrz 1,然后运行到sleep函数,进程陷入内核模式,hello会进入休眠直到sleep函数调用结束,此过程中会主动释放当前进程,并将hello进程从运行队列移入等待队列。内核进行上下文切换将进程控制权交给其他进程,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程。一段时间后,定时器发出中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了
6.6 hello的异常与信号处理
每隔一秒正常输出一行Hello 8200880130 zrz,进入循环,共打印8次,打印完毕后,调用getchar(),等待用户输入回车终止,Shell回收hello子进程,继续等待用户输入指令。
图 6.6-1 正常运行的情况
在输出中间不停乱按,并不会影响输出,输入的字符会直接显示在终端界面,最后依然需要一个回车退出程序。
图 6.6-2 不停乱按的情况
输入回车,会在打印过程中,先换行,然后继续输出,并不会影响输出,在最后由于之前的回车都被存在stdin中,所以并不需要单独输入回车来getchar(),getchar()函数会自动读取stdin中的回车。
图 6.6-3 按回车的情况
按ctrl-z后,产生中断异常,发送SIGSTP,这是hello会被挂起,并打印相关信息。
图 6.6-4 ctrl-z后的情况
输入ps:
输入ps,打印出各进程的pid。
图 6.6-5 输入ps打印pid
输入jobs:
打印出被挂起的hello的jid即标识。
图 6.6-6 输入jobs打印任务
输入pstree:
打印进程树之间的关系,我们可以看到父进程以及对应的子进程。
图 6.6-7 输入pstree打印进程树
输入fg:
已被挂起的程序会回到前台,继续打印剩余的字符串,然后等待输入回车结束程序。
图 6.6-8 输入fg继续进程
输入kill:
由输入ps可以知道hello任务的pid为2347,然后输入kill -9 2347可以中止掉该程序,将该程序杀死
图 6.6-9 输入kill取消进程
进程收到内核产生的SIGINT信号,结束 掉hello任务。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。
图 6.6-10 输入ctrl-c取消进程
6.7本章小结
在本章中,阐述了进程的定义及作用,介绍了shell的作用和基本处理流程,然后分析了几个重点函数的调用原理,包括fork,execve函数。执行hello程序,并分析在执行过程中遇到不同情况的输出结果与信号处理,更深入的理解了进程与信号的原理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分,一般由一个段标识符加上一个指定段内相对地址的偏移量表示为,[段标识符:段内偏移量]。
线性地址是逻辑地址到物理地址变换之间的中间层,是地址空间中一段连续的整数。程序hello的代码会产生逻辑地址,hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址,也常说线性地址就是虚拟地址。
虚拟地址指程序访问存储器所用的逻辑地址,CPU启动保护模式后,程序hello运行在虚拟地址空间中。linux中虚拟地址即为线性地址。
物理地址是放在寻址总线上的地址,用于内存芯片级的单元寻址,计算机系统的内存可以看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,物理地址可以和物理内存一一对应。
7.2 Intel逻辑地址到线性地址的变换-段式管理
由上面的定义我们可以知道,逻辑地址由两部分组成:段标识符和段内偏移量,段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,其中TI表示选择全局或是局部描述表即GDT或LDT,RPL表示用户态或是内核态。如下图所示
根据段选择符我们可以直到去GDT(TI=1)或是LDT中(TI=0)选择查看基地址,通过段标识符的前十三位到对应的描述表中查看即可得到基地址的值(Base),再将Base与段内偏移量的值相加即可得到线性地址,这个过程称为段式内存管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
在分页系统下,一个程序发出的虚拟地址由两部分组成:虚拟页号(VPN)和虚拟页内偏移值(VPO)。
实现原理:将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
首先我们得到hello的页目录地址,前十位表示一个页表的地址,中间十位表示页表索引,可以在页表中找到相应的起始地址,然后最后十二位即为页内偏移值,与前者相加即可得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:即旁路转换缓冲,或称为页表缓冲;里面存放的是一些页表文件(虚拟地址到物理地址的转换表)。又称为快表技术。由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。其结构如下图:
VA到PA的变换:
若TLB命中,所作操作与7.3相同,即MMU会从TLB中提取相应的PTE,翻译为物理地址进行后续操作。
若TLB不命中,VPN被划分为四个片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址。VPN1提供一个L1 PTE的偏移量,这包含了L2页表的基地址,同理VPN2提供了一个到L2 PTE的偏移量,以此类推,从L4中即可取出相应的PPN与VPO连接,这就是对应的物理地址。
在多级页表的情况下,无非就是不断通过索引 – 地址 – 索引 - 地址重复四次进行寻找。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照7.4所述的操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CI(组索引),CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,数据返回给MMU,进而传给CPU;如果未命中,就依次去L2,L3主存中判断是否命中,同时将数据传给CPU更新各cache的储存。
7.6 hello进程fork时的内存映射
fork函数被调用时,内核创建一个新进程,并为它创建各个数据结构,分配给该子进程唯一的pid。此时要给该进程创建一个虚拟内存,位创建该内存,fork函数创建了当前进程的 mm_struct、区域结构和页表的原样副本,并将两个进程的每个页面都标记为只读,将两个进程中的区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
Shell中执行了execve的调用,execve函数运行可执行目标文件hello来代替当前程序,为此需要做到以下几点:
- 删除已存在的用户区域
- 映射到私有区域:为新程序的代码数据栈区等创建新的区域结构,且这些区域都是私有的、写时复制的。
- 映射共享区域: hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC):设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
当指令引用一个虚拟地址,而与该地址相应的物理地址不在内存中时,就会发生缺页故障,通过查询页表PTE我们就可以知道虚拟页在磁盘中的位置,缺页处理程序会确认出物理内存中的牺牲页,如果该页面已被更改则加入到物理内存中,否则加载新的页面,更新PTE。之后缺页处理程序会返回到原来的进程,再次执行导致缺页的命令,CPU将虚拟地址发到MMU,因为该页面已在物理内存中,所以就会命中。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器(如malloc)来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合。每个块就是一个连续的虚拟内存页,要么是已分配的,要么是空闲的。
分配器有两种风格:
- 显示分配器:要求应用显式地释放任何已分配的块,如C语言中调用malloc函数来分配一个块,然后调用free函数来释放一个块。
- 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,就释放这个块。因此,隐式分配器也叫垃圾分配器。
Hello中printf会调用malloc函数,malloc返回的是void*类型,可以先动态分配一个空间给printf函数,然后函数结束后再free掉不需要的区域。
首先我们对选择基本方法,可以是显示的、隐式的、分离空闲链表等,然后由于分配器查找空闲块时,有三种不同的放置策略,首次适配、下一次适配和最佳适配,可以选择不同的放置策略。在释放一个已分配块的时候需要考虑是否能与前后空闲块合并,减少系统中碎片的出现。在合并时,可以采用带边界标记的合并,就像在上一段基本方法中所描述,通过边界标记来判断当前块周围是否也同样是空闲块,以此来判断是否需要合并。
7.10本章小结
本章介绍了hello在执行过程中不同地址之间的关系,以及相关地址之间转换的原理和涉及到的软硬件知识,同时介绍了发生缺页异常时的系统处理方法,最后介绍了动态内存分配的方法及策略。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
结论
- hello.c预处理到hello.i文本文件
- hello.i编译到hello.s汇编文件
- hello.s汇编到二进制可重定位目标文件hello.o
- hello.o通过链接生成可执行文件hello
- bash-shell进程调用fork函数,生成子进程
- 子进程运行过程中信号会对它的运行产生不同的影响,也有不同的处理方法。
- execve函数加载运行当前进程的上下文并运行新程序hello
- 程序的运行就需要地址的概念,而不同地址之间又有不同的关系,虚拟地址为物理地址在虚拟空间上的抽象,为程序的运行提供了空间。
- hello最终被父进程回收,内核回收其创建的所有信息。
附件
中间结果文件名称 | 用途 |
Hello.i | 预处理后的文件 |
Hello.s | 编译后的汇编程序 |
Hello.o | 可重定位目标文件 |
Hello | 可执行的目标程序 |
hello.o_elf.txt | Hello.o的elf格式 |
hello_elf.txt | Hello的elf格式 |
dump1.txt | Hello.o的反汇编程序 |
dump2.txt | 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.