计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 2022113220
班 级 22WL021
学 生 眭菁莹
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本论文通过简单的hello.c程序为例,围绕一个程序从编写到运行的生命过程进行展开讲解。从C语言文件到可执行文件,论文详细介绍了预处理、编译、汇编、链接的过程,对在Shell中创建进程并加载hello可执行程序进行了探究。以《深入理解计算机系统》的授课内容为主线,进行了知识的回顾与应用。
关键词:计算机系统;hello的一生;预处理;编译;汇编;链接;进程管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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简介
1.1.1 P2P
是程序由一个项目变成一个进程的过程。Hello程序的诞生是程序员通过键盘输入得到hello.c源程序,而C语言源程序hello.c在预处理器(cpp)处理下,得到hello.i,通过编译器(ccl),得到汇编程序hello.s,再通过汇编器(as),得到可重定位的目标程序hello.o,最后通过链接器(ld)得到可执行的目标程序hello。在shell中键入运行命令后,shell调用fork函数为其创建子进程。
1.1.2 020
020是程序“从无到有再到无”的过程。程序经过系统OS,shell为hello进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。进入 main 函数执行目标代码,CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回、更新PC。当程序运行结束后, shell 父进程负责回收 hello 进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
开发与调试工具:Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc
软件环境:Ubuntu20.04、Vmware、Windows 11 家庭中文版
1.3 中间结果
hello.c : hello程序的C语言代码
hello.i: hello.c预处理后的文件
hello.s: hello程序的汇编语言文件
hello.o: hello程序经过as汇编后的可重定位文件
hello: hello程序经过链接得到的可执行文件
hello-elf.txt: hello.o文件的ELF格式文本文件
dishello.s: hello.o文件的反汇编文件
hello-elf2.txt: hello可执行文件的ELF格式文本文件
dishello2.s: hello可执行文件的反汇编文件
1.4 本章小结
本章主要介绍了hello 的P2P、020过程,并列出了本次实验的基本实验信息、环境、中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1 概念
预处理是依据预处理指令,对源程序进行修改,包含其引用的部分使其成为后缀.i的文本文件。其中预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
2.1.2 作用
1.宏定义
宏定义又称为宏代换、宏替换。预处理(预编译)工作也叫做宏展开,将宏名替换为字符串,且在对相关命令或语句的含义和功能作具体分析之前将所有的宏等价代换。是一种转义方式。
2.文件包含
文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。这种文件包含处理在程序开发中会给我们的模块化程序设计带来很大的好处,通过文件包含的方法把程序中的各个功能模块联系起来是模块化程序设计中的一种非常有利的手段。在源文件进行预处理时,连同被引用进来的文件一同添加到文本中。
3.条件编译
程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定哪些代码被编译,而哪些不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。
2.2在Ubuntu下预处理的命令
运行如下命令:gcc -E hello.c -o hello.i,生成hello.i文件。
图2.2 1 预处理后生成的文件
2.3 Hello的预处理结果解析
我们观察hello.i文件的末尾如下图
图2.3 1 分析hello.i文件
可以看到,原来仅三十行左右的代码扩展到了3092行。其中hello.i中的main函数(除注释与头文件)部分位于代码的最后,没有发生变化,而头文件部分展开,宏定义也被处理。若文件包含中存在嵌套关系,cpp会逐层展开,这样hello.i就可以直接被译为.s文件。
2.4 本章小结
本章主要介绍了预处理的概念以及预处理的作用,包括进行宏定义、处理文件包含,进行条件编译等特殊控制指令。我们在Ubuntu下将hello.c文件预处理生成了hello.i文件。发现预处理的过程会保留注释与#开头语句之外的部分,删除注释部分,同时将头文件与头文件所包含的文件直接插入到代码中,使仅30行的.c文件预处理后的文件竟有3000多行。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1 概念
对于预处理后的hello.i文件在确认所有指令都符合语法规则之后,将其翻译成等价的中间代码或者是汇编代码。其中.s后缀的为汇编语言程序。
3.1.2 作用
通过编译,使预处理后的hello.i文件变成了可以汇编为机器指令的汇编语言,编译阶段所有做的工作就是通过词法分析和语法分析,生成一棵语法树再转换为目标代码。编译阶段会对代码进行优化处理,不仅涉及到编译技术本身,还涉及到机器的硬件环境。
3.2 在Ubuntu下编译的命令
运行如下命令:gcc -S hello.i -o hello.s,生成hello.s文件。
图3.2 1 编译指令
图3.2 2 编译后生成的文件
3.3 Hello的编译结果解析
3.3.1 数据
3.3.1.1 数字常量
编译器常常将数字常量以立即数的形式进行处理。比如下面这条汇编代码中,将-20(%rbp)中的数值(即变量argc的值)和5比较,5就是立即数的形式。
图3.3 1 hello.c中出现的数字常量
图3.3 2 编译器将数字常量处理为立即数
3.3.1.2字符串常量
在hello.c文件中,有两个字符串。
图3.3 3 hello.c的两个字符串常量
编译器对这两句字符串进行处理时,将这两个字符串放入内存中的.rodata节常量区中。
图3.3 4 字符串常量存放在.rodata节中
打印时,编译器将语句翻译为先将字符串存放的地址存入寄存器%rdi,再进行打印。
图3.3 5 打印字符串常量
3.3.1.3 局部常量
编译器一般将局部变量存放在寄存器中或者栈中。
1.传入的参数char *argv[]存放在栈中,其中首地址存放在栈中-32(%rbp)的位置,argv[1]地址在-32(%rbp)+$8,argv[2]地址在-32(%rbp)+$16,argv[3]地址在-32(%rbp)+$24。具体如下图所示。
图3.3 6 传入参数char *argv[]
2.传入参数int argc存放在寄存器%edi中,然后将局部变量int argc存放在栈中-20(%rbp)的位置。
图3.3 7 局部变量int argc
3.将局部变量int i存放在栈中-4(%rbp)的位置,在循环中通过加或者比较-4(%rbp)处存放的值来实现i值的改变和控制循环,如下图所示。
图3.3 8 局部变量int i
3.3.2 赋值
编译器将赋值的操作变为对相应的寄存器或栈进行赋值。
例如对i的赋值,在hello.s文件中我们已经知道栈中-4(%rbp)的位置存放的是局部变量i,因此是将i赋值为0。
图3.3 9 hello.s中对局部变量i赋值
3.3.3 类型转换
在Hello.c文件中, argv[3]的类型为字符型,后经过函数atoi()转换为整型。经过编译器编译后,首先将argv[3]从栈中取出,赋值给%rdi,通过调用atoi@PLT指令调用atoi函数,最终转换为整型数。如图
图3.3 10 hello.s中调用函数实现类型转换
3.3.4 算术操作
在hello.c中,算术运算有for循环中的i++,我们已经知道栈中-4(%rbp)的位置存放的是局部变量i,每次执行如图这条指令,实现的是i自增1。
图3.3 11 hello.s中的i++
3.3.5 关系操作
对于关系操作,编译器一般会将关系操作翻译为cmp语句。在源文件hello.c中,有两处关系操作。
1.比较argc与5是否相等
图3.3 12 hello.s中对argc与5的关系操作
我们已经知道栈中-20(%rbp)的位置存放的是argc,因此这条指令就是在判断5与argc是否相等。
2. 比较i与10的大小,i >= 10时跳出循环
图3.3 13 hello.s中对i与10的关系操作
我们已经知道栈中-4(%rbp)的位置存放的是i,并且这里将i<10替换为判断i<=9。
3.3.6 数组/指针/结构操作
编译器对源代码中数组的操作往往翻译为对地址的加减操作。例如3.3.1.3中谈论的对数组argv[]的访问。
图3.3 14 hello.c中的数组操作
图3.3 15 hello.s中的数组操作
首地址存放在栈中-32(%rbp)的位置,argv[1]地址为-32(%rbp)+$8,argv[2]地址为-32(%rbp)+$16,argv[3]地址为-32(%rbp)+$24,即进行了地址的加减操作以访问数组。
3.3.7 控制转移
编译器编译后的文件hello.s中,控制转移有三处:
1.比较argc是否等于5,如果相等,跳转.L2,否则顺序执行。
图3.3 16 hello.s中的第一处控制转移
2. 将i初始化为0后,无条件跳转至循环中。
图3.3 17 hello.s中的第二处控制转移
3. 比较i是否小于等于9,如果小于等于9,则跳转至.L4,即满足循环条件,继续循环;如果大于9,则顺序执行,跳出循环。
图3.3 18 hello.s中的第三处控制转移
3.3.8 函数调用
在hello.s中,一共有六次函数调用。
1.调用puts()函数。首先将调用函数所需要的参数,即需要打印的字符串常量的地址存放在寄存器%rdi中,然后执行call puts@PLT指令打印字符串。相应的hello.s文件中的指令如下。
图3.3 19 hello.s中第一处函数调用
2. 调用exit()函数,参数为1, 在hello.s中将立即数1放入寄存器%edi中,然后执行call exit@PLT指令调用exit函数。
图3.3 20 hello.s中第二处函数调用
3. 调用含其他参数的printf()函数。在hello.s中,首先进行参数的准备。将argv[2]放入寄存器%rdx中,将argv[1]放入寄存器%rsi中,将字符串"Hello %s %s\n"放入寄存器%rdi中,然后执行call printf@PLT指令进行打印。
图3.3 21 hello.s中第三处函数调用
4. 调用atoi()函数,参数为argv[3]。在hello.s中,首先进行参数的准备。将argv[3]放入寄存器%rdi中,然后执行call atoi@PLT指令调用atoi函数。
图3.3 22 hello.s中第四处函数调用
5. 调用sleep()函数,参数为atoi(argv[3])的返回值。在hello.s中,首先进行参数的准备。将atoi(argv[3])的返回值放入寄存器%edi中,然后执行call sleep@PLT指令调用sleep函数。
图3.3 23 hello.s中第五处函数调用
6. 调用getchar()函数,没有参数。在hello.s中,直接执行call getchar@PLT指令进行函数的调用。
图3.3 24 hello.s中第六处函数调用
3.4 本章小结
主要介绍了编译器ccl通过编译由.i文件生成汇编语言的.s文件的过程,并分析了常量,变量,赋值,函数等各类C语言的数据与操作的汇编表示。汇编语言和高级语言很不同,即使是高级语言中一个简单的条件语句或者循环语句在汇编语言中都需要涉及到更多步骤来实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指将汇编语言程序经过编译器(as)转化为二进制的机器语言指令,并把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。在这里指汇编器器(as)将文本文件hello.s转换为可重定位目标程序hello.o的过程。
4.1.2 汇编的作用
汇编的作用是把汇编语言翻译成机器语言,用二进制码0、1代替汇编语言中的符号,即让它成为机器可以直接识别的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。
4.2 在Ubuntu下汇编的命令
使用命令gcc -c hello.s -o hello.o得到.o文件如下
图4.2 1 汇编后生成的hello.o文件
4.3 可重定位目标elf格式
4.3.1 ELF头
输入命令readelf -h hello.o。
图4.3 1 hello.o的ELF格式的ELF头
从一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2 节头部表
使用readelf -a hello.o查看,节头部表记录了每个节的名称、类型、属性(读写权限)、在ELF文件中占的度、对齐方式和偏移量。
.text节:以编译的机器代码。
.rela.text节:一个.text节中位置的列表。
.data节:已初始化的静态和全局C变量。
.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。
.comment节:包含版本控制信息。
.note节:注释节详细描述。
.eh_frame节:处理异常。
.rela.eh_frame节:一个.eh_frame节中位置的列表。
.shstrtab节:该区域包含节区名称。 .symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
.symtab节:本节用于存放符号表。
图4.3 2 hello.o的ELF格式的节头部表
4.3.3 重定位节
表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。
本程序需要重定位的信息有.rodata中的模式串、puts,exit,printf,slepsecs,sleep,getchar等符号。
图4.3 3 hello.o的ELF格式的重定位节
4.3.4 符号表
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
图4.3 4 hello.o的ELF格式的符号表
4.4 Hello.o的结果解析
运行 objdump -d -r hello.o 获取 hello.o 的反汇编代码。由于反汇编代码是由机器语言翻译形成的,因此在跳转时,地址通常表示为相对地址。同时,操作数通常以十六进制表示。
图4.4 1 hello.o对应的反汇编代码
分析hello.o的反汇编,并与 hello.s进行对照分析:
1.数的表示:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
2.分支转移:跳转语句之后,hello.s中是.L2和.LC0等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,也即间接地址。
3.函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.字符串常量的引用:hello.s中是用的全局变量所在的那一段的名称加上%rip的值,而hello.o中用的是0加%rip的值,因为当前为可重定位目标文件,之后还需经过重定位方可确定其具体位置,所以这里都用0来代替。
4.5 本章小结
汇编器将汇编语言转化成机器语言,机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。它是计算机的设计者通过计算机的硬件结构赋予计算机的操作功能。机器语言具有灵活、直接执行和速度快等特点。 不同型号的计算机其机器语言是不相通的,按着一种计算机的机器指令编制的程序,不能在另一种计算机上执行。
一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如,操作码字段和地址码字段,其中操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1 概念
链接是将各种代码和数据收集并组合到单个文件中的过程,链接执行符号解析和重新定位的过程。
5.1.2 作用
将可重定位的目标文件和命令行参数作为输入,以生成正常工作的可执行文件。这样就可以分离编译和保存工作区。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
使用命令: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/11/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello如下。
图5.2 1 链接过程
生成的可执行目标文件hello如下:
图5.2 2 生成的hello文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
使用readelf -a hello > hello1.elf查看hello的ELF格式,hello的ELF头如下:
图5.3 1 hello的ELF头
与hello.o的ELF头相比,有以下几处不同:
1. 文件类型从可重定位更改为可执行。
2. 程序的入口点、程序开始点、节头表偏移发生改变。
3. 共有29个节头表,增加了17个。
图5.3 2 hello的节头部表
图5.3 3 hello的程序头
图5.3 4 hello的段节
图5.3 5 hello的重定位节
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。 在edb的symbol窗口,可以查看各段对应的名称以及各段的起始位置与结束的位置,与5.3中所展示出来的elf格式展示出来的相对应。
图5.4 1 各段名称、起始位置和终止位置对应
Data Dump是从地址0x401000到402000的。例如./init段,从反汇编代码上可以看到起始地址位于0x401000, 并且机器码为f3 0f 1e fa……对应在Data Dump上也能找到此位置。
图5.4 2 Data Dump信息
5.5 链接的重定位过程分析
5.5.1分析hello与hello.o的不同
运行objdump -d -r hello,得到如图所示反汇编代码
图5.5 1反汇编生成代码
对比分析hello与hello.o的不同,有如下几点:
1.链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2.增加的节:
hello中增加了.init和.plt节,和一些节中定义的函数。
3.函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:
hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
5.5.2链接过程
链接的过程主要分为符号解析和重定位这两个过程。
1)符号解析:符号解析解析目标文件定义和引用符号,并将每个符号引用和一个符号定义相关联。
2)重定位:编译器和汇编器生成从0开始的代码和数据节。而链接器通过把每个符号定义与一个虚拟内存地址相关联,从而将这些代码和数据节重定位,然后链接器会修改对所有这些符号的引用,使得它们指向这个虚拟内存地址。
对于hello来说,链接器把hello中的符号定义都与一个虚拟内存位置关相关联,重定位了这些节,并在之后对符号的引用中把它们指向重定位后的地址。hello中每条指令都对应了一个虚拟地址,而且对每个函数,全局变量也都它关联到了一个虚拟地址,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地址来进行,从而执行这些指令。
5.6 hello的执行流程
使用edb执行hello,从加载hello到_start,到call main,以及程序终止的所有过程如下图所示。
图5.6 1各函数地址
5.7 Hello的动态链接分析
图 5.7 1 调用dl_init之前.got.plt段的内容
图 5.7 2 调用dl_init之后.got.plt段的内容
可以很明显地看出第2、3行的变化。 实际上,这是动态链接器的延迟绑定的初始化部分。延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)的协同工作实现函数的动态链接,其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。 在此之后,程序调用共享库函数时,会首先跳转到PLT执行指令,第一次跳转时,GOT条目为PLT下一条指令,将函数ID压栈,然后跳转到PLT[0],在PLT[0]再将重定位表地址压栈,然后转进动态链接器,在动态链接器中使用两个栈条目确定函数运行时地址,重写GOT,再将控制传递给目标函数。以后如果再次调用同一函数,则通过间接跳转将控制直接转移至目标函数。
5.8 本章小结
本章检查并介绍了 Linux 系统下链接的进程。链接是将程序转换为可执行文件的最后一步。通过链接,代码片段和数据片段被整合。本章通过查看 hello 在 edb 或终端上的虚拟地址空间,比较 hello.o 和 hello 的反汇编代码等,来分析和总结重定位、执行和动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 概念
进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。进程是计算机科学中最深刻,最成功的概念。
6.1.2 作用
我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。而这些假象就是通过进程来实现的。
6.2 简述壳Shell-bash的作用与处理流程
Shell指操作界面,可以接收用户命令并调用相关程序。处理流程如下:
1.读取输入命令并处理得到参数。
2.判断输入命令是内置还是外部命令,内置命令立刻执行,外部命令则调用相关程序。
3.根据后续输入向相应进程发送信号。
4.处理接收到的信号,更新进程状态。
6.3 Hello的fork进程创建过程
一个进程,包括代码、数据和分配给进程的资源。fork函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
6.4 Hello的execve过程
创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。
在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
当开始运行hello时,内存为hello分配时间片,如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。然后在用户态下执行并保存上下文。
如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,控制将交付给其他进程。
当hello 执行到 sleep时,hello 会休眠,再次上下文切换,控制交付给其他进程,一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权回到 hello 继续执行。
hello在循环后,程序调用 getchar() , hello 从用户态进入核心态,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1运行正常
正常执行时,hello每隔零秒打印一行“Hello 2022113220 眭菁莹 17789750495”,共打印十次。打印完毕后,调用getchar()函数,等待用户输入回车后程序终止,Shell回收hello子进程。
图6.6 1 hello正常运行
6.6.2 乱按
当在程序执行过程中乱按时,按下的字符串会直接显示,但不会干扰程序的运行,如果在乱按过程中没有输入回车,那么在最后一行hello的字符串打印完毕后,需要敲一个回车才能退出程序。(由于我手机号是五的倍数,输入的秒数是零,运行时间过短,不便于测试,因此在此之后都将秒数改为2)
图6.6 2 在hello执行过程中不停乱按
6.6.3 回车
在hello执行过程中敲回车时,会首先在打印的过程中显示换行,一个回车显示一排换行。在打印完毕最后一行字符串后,由于输入的回车依然存在于stdin中,所以在调用getchar()函数时,会读取stdin中的回车,因此无需再敲回车键就能终止程序。程序终止后,我们发现多出了六个空行,这是因为在程序的执行过程中敲的回车键都留在stdin中,getchar()只接收了其中的第一个回车,由于在程序终止后没有清空stdin,剩余的回车保留在其中。当继续运行时,遇到回车便开始处理,但单独的回车相当于一个空行,读入但不执行任何操作,因此留下了六个空行。
图6.6 3 在hello执行过程中按回车
6.6.4 Ctrl-Z
在程序执行过程中按Ctrl-z,产生中断异常,发送信号SIGSTP,这时hello的父进程shell会接收到信号SIGSTP并运行信号处理程序,hello被挂起,并打印相关信息。
图6.6 4 在hello执行过程中按Ctrl-z
6.6.5 输入ps
Ctrl-z之后,在shell命令行中输入ps,打印出各进程的pid,其中包括被挂起的hello。
图6.6 5 Ctrl-z后执行ps
6.6.6 输入jobs
Ctrl-z之后,在shell命令行中输入jobs,打印出被挂起的hello的jid及标识。
图6.6 6 Ctrl-z后执行jobs
6.6.7 输入pstree -p
Ctrl-z之后,在shell命令行中输入pstree -p,查看进程树之间的关系。在进程树中找到hello(6847),发现hello的父进程是bash(6484)。
图6.6 7 Ctrl-z后执行pstree以及hello的位置
6.6.8 输入fg
Ctrl-z之后,在shell命令行中输入fg,被挂起在后台的hello进程被重新调到前台执行,打印出剩余部分,按回车后终止程序。
图6.6 8 Ctrl-z后执行fg
6.6.9 输入kill
Ctrl-z之后,输入ps,得到hello的pid为6847,因此,在shell中输入kill -9 6847,可以发送信号SIGKILL给进程6847,该进程被杀死。
图6.6 9 Ctrl-z后执行kill
6.6.10 Ctrl-C
运行hello时按Ctrl-C,会导致中断异常,从而内核产生信号SIGINT,发送给hello的父进程,父进程收到它后,向子进程发生SIGKILL来强制终止子进程hello并回收它。这时再运行ps,可以发现他已经被终止并回收了。
图6.6 10 在hello执行过程中按Ctrl-c
6.7本章小结
本章简要介绍了进程的概念和作用,以及shell的处理流程。同时以hello文件为例分析了fork,execve,进程执行,以及异常和信号处理的过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址为由程序产生的与段有关的偏移地址。逻辑地址分为两个部分,一个部分为段基址,另一个部分为段偏移量。在CPU保护模式下,需要经过寻址方式的计算和变换才可以得到内存中的有效地址。
在hello的反汇编代码中的地址即为逻辑地址,需要加上相应的段基址才能得到真正的地址。
7.1.2 线性地址
线性地址是地址空间中连续的整数,是逻辑地址到物理地址变换之间的中间层。在各段中,逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。
可执行目标文件hello反汇编代码中的偏移地址(逻辑地址)与基地址相加后,即得到了对应内容的线性地址。
7.1.3 虚拟地址
虚拟地址是指程序访问存储器所使用的逻辑地址。使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送至内存前先转换成适当的物理地址。在linux中,虚拟地址数值等于线性地址。
在查看可执行目标文件hello的elf格式时,程序头中的VirtAddr即为各节的虚拟地址。由于在linux中,虚拟地址数值等于线性地址,所以在hello反汇编代码中的地址加上对应段基地址的值即为虚拟地址。
7.1.4 物理地址
计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组成的数组,其中每一个字节都被给予一个唯一的物理地址。
在hello的运行过程中,hello内的虚拟地址经过地址翻译后得到的即为物理地址,并在机器中通过物理地址来访问数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器从逻辑地址到线性地址的变换通过段式管理,介绍段式管理就必须了解段寄存器的相关知识。段寄存器对应着内存不同的段,有栈段寄存器(SS)、数据段寄存器(DS)、代码段寄存器(CS)和辅助段寄存器(ES/GS/FS)。
段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。段选择符分为三个部分,分别是索引、TI(决定使用全局描述符表还是局部描述符表)和RPL(CPU的当前特权级)。
图7.2 1 段选择符
这样,Intel处理器在通过段式管理寻址时,首先通过段描述符得到段基址,然后与偏移量结合得到线性地址,从而得到了虚拟地址。至于偏移量,基址寄存器还是变址寄存器有不同的计算方法,后者需要经过乘比例因子等处理。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
7.4 TLB与四级页表支持下的VA到PA的变换
现代 CPU 都包含一张名为 TLB(Transfer Look-aside Table),叫做快表,或者高速地址变址缓存,以加速对于页表的访问。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2^t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。
若TLB命中,会经历如下步骤:CPU产生一个虚拟地址;MMU从TLB中取出相应的PTE;MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存;高速缓存/主存将所请求的数据字返回给CPU。
若TLB不命中,对于四级页表来说虚拟地址被划分成4个VPN和1个VPO,VPN的每个片表示一个到第i级页表的索引,即偏移量,CR3寄存器包含L1页表的物理地址余下的页表中,第j级页表中的每个PTE,1≤j≤3,都指向j+1级的某个页表的基址。最后在L4页表中对应的PTE中取出PPN,与VPO连接,从而形成物理地址PA。
图7.4 1 加入TLB的命中与不命中
7.5 三级Cache支持下的物理内存访问
物理地址分为标记,组索引和块偏移。首先,在L1中匹配组索引位,若匹配成功,则根据标记和偏移的匹配结果决定缺失或是命中。若组索引匹配不成功,则进入下一级cache,重复直至进入内存。
具体过程如下:
1. 先在第一级缓存中寻找要找的数据,利用TLBI找到TLB中对应的组,再比较TLBT,若相同且有效为为1,则要找的数据就是该组的数据。
2. 否则,在第二级缓存中寻找,找到后需要再将其缓存在第一级,若有空闲块,则放置在空闲块中,否则根据替换策略选择牺牲块。
3. 否则,在第三级缓存中寻找,找到后需要缓存在第一,二级,若有空闲块,则放置在空闲块中,否则根据替换策略选择牺牲块。
4. 否则,在第四级缓存中寻找,找到后需要缓存在第一,二,三级,若有空闲块,则放置在空闲块中,否则根据替换策略选择牺牲块。
7.6 hello进程fork时的内存映射
当fork()函数被父进程调用时,内核创建一个子进程,为新的子进程创建各种数据结构,并分配给子进程一个唯一的pid(与父进程不同)。
为了给hello进程创建虚拟内存,fork()函数创建了当前进程的mm_struct、区域结构和页表的原样副本,并将两个进程的每个页面都标记为只读,将两个进程中的区域结构都标记为私有的写时复制。
当fork()在从新的进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同但相互独立,映射的也是同一个物理内存。当这两个进程中的任一个进行写操作时,写时复制机制就会创建新页面,在新的页面中进行写操作,并且原来的虚拟内存映射到创建的新页面上,因此每个进程都具有私有的地址空间。
7.7 hello进程execve时的内存映射
execve() 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的 .text 和 .data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execv() 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。如下例所示:CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位为0推断出VP3未被缓存,从而触发一个缺页异常。缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,即存放在PP3中的VP4。若VP4已经被修改了,那么内核就会将它复制回磁盘,否则直接修改。无论哪种情况,内核都会修改VP4的页表条目,替换为VP3的页表条目。
接下来,内存从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。此时,VP3已经缓存在主存中,可以正常处理。经过缺页处理前后的页表状态如下图所示:
图7.8 1 VM缺页前后
7.9动态存储分配管理
printf函数会调用malloc,下面简述动态内存管理的基本方法与策略。
7.9.1 方法
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。显式分配器要求应用显式地释放任何已分配的块;隐式分配器要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
7.9.2 策略
程序使用动态内存分配最重要的原因是:有时,当我们运行程序时,才会直到某些数据结构的大小,这样便需要使用到动态内存分配。
例如,要求一个C语言程序读一个ASCII码整数的链表,每一行一个整数,从stdin到一个C数组。输入是由整数n和接下来要读和存储到数组中的n个整数组成的。最简单的方法就是静态地定义这个数组,最大数组大小MAXN固定。
然而,如果这个程序使用者想要读取一个比MAXN大的文件,唯一的办法就是修改程序中MAXN的值,对大型软件产品而言不是一个好方法。
一种更好的方法是在已知了n的值后,动态地分配这个数组。使用这种方法,数组大小的最大值就只由虚拟内存数量来限制了。
hello中的printf作为一个已经编译、汇编好了,等待链接的函数,修改固定参数也是不现实的。
首先,对与堆中的块的组织,可以选择隐式、显示、分离空闲链表等。当分配器查找空闲块时,又可以采用不同的放置策略,如首次适配(从头开始搜索链表)、下一次适配(从上一次找到的空闲块的剩余块除法)以及最佳适配等。在分割空闲块时,又可以采用将剩余块分割为新的空闲块的策略。在合并时,可以采用带边界标记的合并,就像在上一段基本方法中所描述,通过边界标记来判断当前块周围是否也同样是空闲块,以此来判断是否需要合并。
7.10本章小结
本章简要介绍了文件的存储管理,包括各种地址的转换,错误的处理,以及动态存储的分配模式。
(第7章 2分)
结论
hello程序所经历的过程可以概括为以下阶段:
生成阶段:预处理→编译→汇编→链接;
加载阶段:shell fork子进程→execve;
执行阶段:磁盘读取、虚拟内存映射、CPU执行指令、内核调度、缓存加载数据、信号处理、Unix I/O输入与输出;
终止阶段:进程终止、shell与内核对其进行回收。
我们经过探究发现了一个简单的hello程序涉及一系列复杂的编译器、操作系统、硬件实现机制。程序的运行与内核、硬件的多方面协调工作密不可分。简单的一条指令需要成千上万条底层步骤,无论是内部处理还是输入输出。计算机的硬件系统经过数十年的迭代更新变得精巧与复杂。操作系统的设计体现了多方面程序设计思想,从底层出发让软件与应用层面能够调度硬件设备。抽象与系统的思想在计算机系统的实现过程中得到了深入的体现,一个简单的hello world程序背后也充满着程序人的智慧与情怀。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c : hello程序的C语言代码
hello.i: hello.c预处理后的文件
hello.s: hello程序的汇编语言文件
hello.o: hello程序经过as汇编后的可重定位文件
hello: hello程序经过链接得到的可执行文件
hello-elf.txt: hello.o文件的ELF格式文本文件
dishello.s: hello.o文件的反汇编文件
hello-elf2.txt: hello可执行文件的ELF格式文本文件
dishello2.s: 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分)