摘 要
HelloWorld是每个程序员接触的第一个程序,表面上平平无奇的它背后却是由操作系统许多设计精巧的机制支撑的。本文通过分析hello程序,从预处理开始,到汇编、链接,载入内存成为进程到最后结束,从编译、存储管理、进程管理等多角度、全方面地阐述了其在Linux系统中的一生,加深对计算机系统运行机制的理解。
关键词:计算机系统;编译;存储管理;进程管理;P2P
目 录
第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在Ubuntu下预处理的命令 - 6 -
2.3 Hello的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在Ubuntu下编译的命令 - 8 -
3.3 Hello的编译结果解析 - 9 -
3.4 本章小结 - 12 -
第4章 汇编 - 13 -
4.1 汇编的概念与作用 - 13 -
4.2 在Ubuntu下汇编的命令 - 13 -
4.3 可重定位目标elf格式 - 13 -
4.4 Hello.o的结果解析 - 14 -
4.5 本章小结 - 17 -
第5章 链接 - 18 -
5.1 链接的概念与作用 - 18 -
5.2 在Ubuntu下链接的命令 - 18 -
5.3 可执行目标文件hello的格式 - 18 -
5.4 hello的虚拟地址空间 - 20 -
5.5 链接的重定位过程分析 - 22 -
5.6 hello的执行流程 - 22 -
5.7 Hello的动态链接分析 - 24 -
5.8 本章小结 - 25 -
第6章 hello进程管理 - 26 -
6.1 进程的概念与作用 - 26 -
6.2 简述壳Shell-bash的作用与处理流程 - 26 -
6.3 Hello的fork进程创建过程 - 26 -
6.4 Hello的execve过程 - 27 -
6.5 Hello的进程执行 - 27 -
6.6 hello的异常与信号处理 - 29 -
6.7本章小结 - 31 -
第7章 hello的存储管理 - 34 -
7.1 hello的存储器地址空间 - 34 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 34 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 35 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 36 -
7.5 三级Cache支持下的物理内存访问 - 37 -
7.6 hello进程fork时的内存映射 - 38 -
7.7 hello进程execve时的内存映射 - 39 -
7.8 缺页故障与缺页中断处理 - 39 -
结论 - 41 -
附件 - 42 -
参考文献 - 43 -
第1章 概述
1.1 Hello简介
1.1.1 Hello的P2P过程
P2P即From Program to Process。
Program指的是hello.c源文件,程序员输入的源文件;Process即最后Hello运行时的进程。
hello的P2P过程是从一个程序员输入的源文件hello.c开始的。hello.c经过预处理器cpp的预处理生成修改了的源程序hello.i,接着通过编译器ccl的编译生成汇编程序hello.s,然后通过汇编器as的汇编生成可重定位目标程序hello.o,最后通过链接器ld的链接生成可执行目标程序hello。
当在shell中输入./hello后,shell会调用fork函数创建一个子进程,然后调用execve函数将Hello的内容加载到子进程中,实现了由Process向Process的转变。
1.1.2 Hello的020过程
020即From Zero-0 to Zero-0。
程序运行前,execve将hello程序加载到相应的上下文中,从main函数开始执行代码;在程序结束后shell回收进程,释放hello的内存删除上下文,清除痕迹。整个hello程序就是从无到有再到无的过程,即From Zero-0 to Zero-0。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;3.2GHz;16G RAM;1024GHD Disk
1.2.2 软件环境
Windows11 64位;Vmware 15;Ubuntu 22.04 LTS 64位;
1.2.3 开发与调试工具
Visual Studio 2022 64位;CodeBlocks 64位;
vi/vim/gedit+gcc,edb,gcc,gdb,readelf,HexEdit,vim。
1.3 中间结果
文件名 | 文件作用 |
---|---|
hello.c | 源文件 |
hello.i | hello.c经过预处理(cpp)后的文本文件 |
hello.s | hello.i经过编译(ccl)得到的汇编文件 |
hello.o | hello.s经过汇编(as)得到的可重定位目标文件 |
hello | hello.o与其他目标文件链接后得到的可执行目标文件 |
elf.txt | hello.o的elf文件 |
elf_out.txt | hello的elf文件 |
hello_obj.s | hello的反汇编代码 |
1.4 本章小结
本章介绍了hello的P2P过程和020过程,初步介绍了hello的程序人生,介绍了环境和工具,说明了处理过程的各项中间结果,为本论文奠定基础。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
预处理是源文件编译前需要做的预备工作,预处理器(cpp)会修改原始的C程序,会将以#开始的代码解释为预处理指令,比如宏定义(#define)、文件包含(include)、条件编译(#if)等,将这些内容直接插入到程序文本,删除注释和多余空白字符。
2.1.2预处理的作用
(1)宏定义:用实际值替换用#define定义的字符串。
(2)文件包含:将#include所包含的头文件直接加入到文本文件中。比如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。
(3)条件编译:如#ifdef,#ifndef,#else,#elif,#endif等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
源文件经过预处理可以得到便于编译器工作的.i文件。
2.2在Ubuntu下预处理的命令
在程序所在终端输入如下命令,得到预处理文件hello.i。
gcc -m64 -no-pie -fno-PIC hello.c -E -o hello.i |
---|
2.3 Hello的预处理结果解析
hello.c源代码如下图所示:
打开预处理后的hello.i文件观察,可以发现有几点明显的变化:
①代码行数由原来的23行变为3091行。
②代码中的注释被删除。
③源文件中include的三个头文件已经被预处理器写入.i文件。
可以看到源文件经过了预处理得到了便于编译器工作的.i文件。
2.4 本章小结
本章介绍了预处理的概念和作用,指出预处理指令,对hello.c源文件进行了预处理,并对预处理结果进行了分析。
第3章 编译
3.1 编译的概念与作用
3.1.1概念
编译是指编译器将源程序转换为计算机可以识别的机器语言——汇编语言,编译可以分为分析和整合两部分,分析过程将源程序分成多个结构,校验其格式,收集源程序信息,并将其放在符号表中;整合过程根据分析过程传递的信息构造目标程序。最后生成hello.s文件
3.1.2 作用
编译共六个步骤:
(1)词法分析:词法分析器读取源程序的字符流并对其进行扫描,将其组成有意义的词素序列,传递给语法分析。
(2)语法分析:语法分析其用词法单元的第一个分量来创建语法树,树中的每个非叶结点都表示一个运算,左右结点表示运算分量。
(3)语义分析:语义分析器语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。语义分析器也收集类型信息,以便后续的中间代码生成器使用。
(4)中间代码生成:生成一个明确的低级类机器语言的中间表示。
(5)代码优化:生成效率更高,更好的目标代码
(6)将生成的中间代码映射为机器代码,为每个变量分配寄存器或内存位置,并且将中间指令翻译成机器指令序列。
3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s |
---|
3.3 Hello的编译结果解析
3.3.1 常量的存储
该程序中的常量包括两类:数值常量和字符串常量。
数值常量为判断argc数量的4和for循环中的循环界限8,数据保存在.text节中,以立即数的形式直接嵌入汇编代码。
字符串常量为printf函数中的字符串,保存在.rodata节中。
3.3.2变量存储
程序中无全局变量,仅有局部变量i、argc和指针数组argv[]和局部变量,均保存在栈中。
首先使用(%rsp-32)为栈开辟32字节的空间,然后将argc存储在(%rbp-20)的位置,将argv存储在(%rbp-32)的位置。
局部变量i保存在(%rbp-4)的位置,并赋初值0。
3.3.3算数操作
在for循环中每次循环变量i加1,addl指令将(%rbp-4)地址对应内存处的值加1,即进行i++的操作。
3.3.4 条件判断和exit函数调用
在源程序中,使用if判断argc的值,若不为4,则打印对应提示并调用exit退出。
在编译后的汇编代码中,使用cmpl指令将(%rbp-20)地址处保存的argc变量与立即数4比较。若相等,跳转到.L2;否则将打印.LC0保存的字符串,并使用call指令调用exit函数。
3.3.5 控制转移和getchar函数调用
在图3-9中第25行汇编代码可以看到,如果argc的值为4,则跳转到.L2。在进行循环变量i的赋初值。
而在图3-6中可以看到初始化i后,跳转到.L3进行循环条件判断。图3-11展现了循环部分的汇编代码。可以看到,在.L3中,如果i≤7,就跳转到.L4进行循环体内容的执行,在第51行循环结束后进行i++的操作,接着执行.L3,以此不断循环。
当.L3中判断循环条件不成立时,则调用getchar函数(第55行),接着使用ret指令(第59行)结束main函数的执行。
3.3.6 循环体内函数调用和数组操作
在for循环内部调用了printf函数,sleep函数,atoi函数。
调用printf函数时%edi保存第一个参数:字符串.LC1;%rsi保存第二个参数argv[1],%rdx保存第三个参数argv[2],他们由栈指针%rax从栈中赋值。
调用sleep函数时,(%rax+24)从栈中获取参数argv[3]并赋给%rdi,调用函数atoi将字符串转换为int类型,最后调用sleep函数。
3.4 本章小结
本章聚焦hello.s文件,对照源文件一步一步解析汇编指令,分析数据储存,变量保存,函数调用,控制转移,数组操作等如何用汇编语言一步步实现,并分析了汇编操作中寄存器和栈的变化。
第4章 汇编
4.1 汇编的概念与作用
4.1.1概念
汇编是指编译器(as)将.s汇编文件翻译成机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果输出为.o文件的过程。.o文件是一个二进制文件,它包含程序的指令编码。
4.1.2作用
将汇编语言翻译成机器语言,使其在链接后能被机器识别并执行。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o |
---|
4.3 可重定位目标elf格式
使用readelf命令查看hello.o的elf文件。
readelf -a hello.o > elf.txt |
---|
ELF头由以16字节序列Magic开始,描述了生成该文件的系统的字的大小和字节顺序,剩下部分包含 ELF 头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,及节头部表中条目的大小和数量等。
不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
节 | 描述 |
---|---|
.text | 已编译程序的机器代码 |
.rodata | 只读数据 |
.data | 数据节,可读可写,已初始化的全局和静态变量。 |
.bss | 未初始化或初始化为0的全局和静态变量 |
.symtab | 符号表,存放在程序中的函数和全局/静态变量的信息、节的名称和位置 |
.rel.text | 可重定位代码,存放.text 节的可重定位信息、在可执行文件中需要修改的指令和指令地址 |
.rel.data | 可重定位数据,存放.data 节的可重定位信息、在合并后的可执行文件中需要修改的指针数据的地址 |
.debug | 调试符号表,符号调试的信息 |
.line | 原始C源程序中的行号和.text节中机器指令之间的映射 |
.strtab | 字符串表,包括.symtab和.debug节中的符号表 |
重定位节是一个.text节中位置的列表,包含text节中需要进行重定位的信息,用于在链接时填入指令中的地址。
4.4 Hello.o的结果解析
使用objdump指令将hello.o反汇编,得到hello_obj.s文件。
objdump -d -r hello.o > hello_obj.s |
---|
仔细观察,可以发源码与反汇编的代码有些许不同,具体表现在以下几个方面:
(1)进制表示不同
在原汇编文件中使用十进制数,在反汇编文件中表示为十六进制。例如:
值得一提的是,在原汇编中使用了movq指令,而反汇编中仅仅使用mov指令,没有字宽标记q,原因在于使用寄存器%rax已经暗含了8字节,使得表述更加精简。
(2)分支转移表示不同
在原汇编文件中跳转使用的是标记,而在反汇编文件中表示为具体地址(相对寻址)。例如:
(3)函数调用不同
在原汇编文件中call指令使用的是函数名字,而在反汇编文件中表示为具体地址(可能为多种寻址方式)。例如:
4.5 本章小结
本章中首先介绍了汇编的概念和作用,接着对hello.s文件进行汇编,生成可重定位目标文件hello.o,之后使用readelf工具,查看了hello.o的ELF头、节头表,可重定位信息和符号表等,接着对hello.o文件和反汇编文件进行了对比,分析了机器语言与汇编语言的一一对应关系。
第5章 链接
5.1 链接的概念与作用
5.1.1 概念
链接是将各种代码和数据片段连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置,组合成为一个可执行文件的过程,这个文件可被加载(复制)到内存并执行。
5.1.2 作用
链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件,节省时间;可以将公共函数聚合为单个文件,而可执行文件和运行内存映像只包含他们实际使用的函数的代码,节省空间。
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查看hello可执行文件的ELF文件elf_out.txt。
readelf -a hello > elf_out.txt |
---|
可以看到,hello的ELF头中类型为执行文件,而程序的入口地址也从0x0修改为了0x4010f0,程序头起点从0变为64,节头表的位置也发生了改变,还有一些别的信息也有所不同,具体见图5-2。
elf可执行文件易加载到内存,可执行文件连续的片被映射到连续的内存段,程序头部表描述了这一映射关系。程序头部表包括各程序头的名称、类型、偏移量、内存地址、对其要求、目标文件与内存中的段大小及运行时访问权限等信息。
5.4 hello的虚拟地址空间
edb --run hello |
---|
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
在ELF节头中,.init节显示在40100地址位置。
在虚拟内存中找到相应位置,便可依次寻找到其他节头在虚拟内存中的地址。
5.5 链接的重定位过程分析
objdump -d -r hello |
---|
在hello的反汇编文件中多了很多节和函数,如.init节和puts函数,在链接过程中,加入了库函数,并进行了重定位。
在hello.o的反汇编中,main函数是从0地址开始的,在hello中保存的虚拟内存的地址为401125。
在hello.o的反汇编中存在重定位条目,在hello的反汇编中,链接的函数如puts、printf、getchar等都有了分配到虚拟内存的地址,函数调用确定了这些函数重定位后的地址,可以直接执行。
在重定位过程中:
分析hello.o中的puts可以发现与main函数偏移地址为1f,格式为R_X86_64_PLT32 调用后位置偏移量为-0x4
所以(unsigned)(0x401090 + (-0x4) - (0x401125 + 0x1f))=0xff ff ff 48,转换为小端法即为48 ff ff ff。
5.6 hello的执行流程
(1)加载hello
(2)开始执行hello:_start call __libc_start_main
(3)执行main:puts@plt、printf@plt、getchar@plt、atoi@plt、sleep@plt
(4)终止hello:exit@plt
地址 | 子程序名 |
---|---|
0x4010f0 | _start |
0x401000 | _init |
0x401125 | main |
0x401090 | puts@plt |
0x4010d0 | exit@plt |
0x4011b4 | _fini |
0x4010e0 | sleep@plt |
0x4010c0 | atoi@plt |
0x4010a0 | printf@plt |
0x4010b0 | getchar@plt |
0x401120 | _dl_relocate_static_pie |
0x401020 | .plt |
5.7 Hello的动态链接分析
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时。
如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。
PLT是一个数组,PLT[0]跳转到动态链接器中,PLT[1]调用系统启动函数,初始话执行环境,调用main函数并处理返回值。
GOT是一个数组,和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]时动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应一个被调用的函数,其地址在运行时被解析。
从节头表可知,hello的GOT表地址如图5-11。
在edb中找到该位置,调用dl_init之前的值如图5-12。
由观察得,GOT[0]和GOT[1]的值发生了改变。
5.8 本章小结
本章介绍了链接的概念和作用,详细说明了链接的过程,可执行文件hello的ELF头和可执行文件hello的反汇编过程,对链接的重定位过程进行了分析,对hello的动态链接过程进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1概念
进程是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。它是操作系统进行资源分配的基本单位,每次用户向shell输入一个可执行目标文件的名字来运行程序时,shell就会创建一个新的进程,并在这个新进程中的上下文中运行这个可执行目标的文件。应用程序也可以创建新进程,并且在新进程的上下文中运行它们的代码或其他应用程序。
6.1.2作用
进程可以使一个系统并发执行多个任务。进程提供给应用程序的关键抽象:
①逻辑控制流(Logical control flow):它提供一个假象,好像我们的程序独占地使用处理器。由OS内核通过上下文切换机制实现。
②私有的地址空间(Private address space):它提供一个假象,好像我们的程序独占地使用内存系统。由OS内核的虚拟内存机制实现。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1作用
shell是一个交互型应用级程序,代表用户运行其他程序,用户通过其提供的界面访问操作系统内核的服务,是用户使用 Linux 的桥梁。shell接收用户命令,然后调用相应的应用程序。
6.2.2处理流程
(1)终端进程读取用户由键盘输入的命令行;
(2)分析命令行字符串,获取命令行参数并构造传递给execve的argv向量;
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令;
(4)如果不是内部命令,调用fork( )创建新进程/子进程;
(5)在子进程中,用步骤2获取的参数,调用execve()执行指定程序;
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid();
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
父进程调用fork函数创建一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据短、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork()函数时,子进程可以读取父进程中打开的任何文件。子进程有不同于父进程的PID。
6.4 Hello的execve过程
Hello进程创建后调用execve函数,在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。
只有当出现错误时,例如找不到filename,execve才会返回到调用程序,调用成功不会返回。与fork不同,fork一次调用两次返回,execve一次调用从不返回。
6.5 Hello的进程执行
6.5.1上下文信息
上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
6.5.2 逻辑控制流
进程运行过程中PC值的序列叫做逻辑控制流。
6.5.3 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.4 调度
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。
当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
6.5.5 用户模式和内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权.
当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;
设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。上下文切换的时候,进程就处于内核模式。
6.5.6 上下文切换
当内核决定抢占当前进程时,需要进行上下文切换,其过程通常包括以下三个步骤:
①保存当前进程的上下文;
②恢复某个先前被抢占的进程被保存的上下文;
③将控制传递给这个新恢复的进程。
6.5.7 hello的程序执行
以hello作为一个独立的进程与其他进程并发执行,内核为hello维持一个上下文,在hello的某个时间片内,若内核判断它已经运行了足够长的时间,那么内核可以决定抢占hello进程,并重新开始一个之前被抢占了的进程,并使用上下文切换的机制将控制转移到新的进程。这样,内核就完成了对hello与其他进程的调度。
6.6 hello的异常与信号处理
6.6.1异常
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。其属性列表如下:
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
(1)中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。
(2)陷阱
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
(3)故障
故障由错误情况引起,可能能够被故障处理程序修正。
当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
(4)终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。
终止处理程序从不将控制返回给应用程序。将控制返回给一个abort例程,该例程会终止这个应用程序。
6.6.2在hello执行过程中进行测试
(1)不停乱按和回车
在hello执行过程中随意输入字符和回车并不会对程序本身造成任何影响。由于shell同时只能有一个前台任务,乱按敲出的乱码被认为是命令,所有的输入会被阻塞在缓冲区中,待hello结束后进行处理。
(2)Ctrl-Z
输入Ctrl-Z,系统将发送SIGTSTP信号给前台进程组的所有进程,使前台进程组暂停。
(3)Ctrl-C
输入Ctrl+C,系统将发送SIGINT信号给前台进程组的所有进程,使前台进程组终止。
(4)运行ps、jobs、pstree、fg、kill等命令
在输入Ctrl-Z之后,接着运行ps、jobs、pstree、fg、kill命令,效果如下。
ps命令列出当前进程及其PID。
jobs命令列出当前作业。
pstree命令将所有进程以树状图显示。
fg命令将重启hello进程。
kill命令杀死hello进程。
6.7本章小结
本章介绍了进程的概念和作用,对Shell-bash的作用和处理流程,hello的fork进程创建过程,execve过程和进程执行过程进行了阐述,并研究了异常和信号处理的过程,通过命令行查看进程的状态等。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址是程序经过编译后出现在汇编代码中的地址,用来指定一个操作数或者是一条指令的地址。
由一个段标识符加上一个指定段内相对地址的偏移量,表示为[段标识符: 段内偏移量]。
7.1.2 线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
7.1.3 虚拟地址
线性地址的别称。
7.1.4 物理地址
用于内存芯片级的单元寻址,与地址总线相对应。电路根据这个地址与物理内存中的数据进行读写。
7.2 Intel逻辑地址到线性地址的变换-段式管理
进程的地址空间按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名,每段从0开始编址。分段系统的逻辑地址由段描述符和段内偏移地址组成。
内存以段为单位进行分配,每个段在内存中占连续空间,但各段之间可以不相邻。为了保证程序能正常运行,必须能从物理地址中找到各个逻辑段的存放位置。因此,需要为每个进程建立一张段表。每个段对应一个段表项,记录该段在内存中的起始位置和段的长度。
段描述符存放在描述符表中,即GDT或LDT中。通过查看段选择符的TI来判断选择哪个描述符表,TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。从被选中的段描述符中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即虚拟地址(VA)到物理地址(PA)的变换通过分页方法来进行。VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块,类似地,物理内存被分割为物理页。CPU芯片上叫做内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。
当页命中时:处理器生成一个虚拟地址,并将其传送给MMU;MMU 使用内存中的页表生成PTE地址;MMU 将物理地址传送给高速缓存/主存;高速缓存/主存返回所请求的数据字给处理器。
当发生缺页时:处理器将虚拟地址发送给 MMU;MMU 使用内存中的页表生成PTE地址;有效位为零, 因此 MMU 触发缺页异常;缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘);缺页处理程序调入新的页面,并更新内存中的PTE;缺页处理程序返回到原来进程,再次执行缺页的指令。
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 后备缓冲器(TLB)
TLB是MMU中一个小的、具有高相联度的缓存,实现虚拟页号VPN向物理页号PPN的映射,页数很少的页表可以完全放在TLB中,以减少访存速度,提高运行效率。
7.4.2 多级页表
虚拟地址空间中每个虚拟页不一定全部都分配,也即都还未被使用,也就没必要保存一条PTE在页表中占用空间。虚拟地址被划分成为k个VPN和1个VPO,每个VPNi都是一个到第i级页表的索引。前k-1级页表中的每个PT都指向下一级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。
为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。对于只有一级的页表结构,PPO和VPO是相同的。
7.4.3 VA到PA的转换过程
开始时,MMU从虚拟地址中抽取出VPN,并检查TLB,看它是否因为前面的某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLBT和TLBI,进行匹配,如果命中,将缓存的PPN返回给MMU,得到PPN和VPO组成的PA;
如果TLB 中没有命中,MMU 向页表中查询确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN。
将PPN与与 VPO 组合成 PA,并且向 TLB 中添加条目。
如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。
7.5 三级Cache支持下的物理内存访问
现阶段CPU普遍采用三级Cache来提高访问速度,L1-Cache与L2-Cache、L3-Cache工作原理相同。
Cache使用物理地址PA进行访问。地址划分为标记CT、组索引CI、块内偏移CO三部分。其中,CT对应于PPN,而将PPO划分为CI与CO。CI的大小取决于Cache的组数,而CO的大小取决于Cache一块的大小。
使用页表获取物理地址后,系统根据物理地址的索引位(CI)进行查找,并匹配物理地址的标志位(CT)。如果匹配成功,且标志位为1,则根据物理地址的偏移量(CO)取出块内的数据。若匹配失败,或标志位不为1,则前往下一级缓存查找数据,直至查找到第三级。之后层层往上,放置到最高级缓存中。若上一级缓存无空闲块,则使用牺牲块算法进行块数据的替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。
为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
以调用execve(“a.out”, NULL, NULL)函数为例。
execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:
(1)删除当前进程虚拟地址的用户部分中的已存在的区域结构(页表、结构体、vm_area_strcut链表)。
(2)映射私有区域(创建自己的新的区域结构)。为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
①代码和数据区域被映射为a.out文件中的.text和.data区。
②bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。
③栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。将共享对象动态链接到这个程序,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)。设置当前进程上下文中的PC,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障
当指令引用了一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。
7.8.2 缺页中断处理
当出现缺页故障时,缺页异常调用内核中的缺页异常处理程序,将所缺的页面调入内存,并更新PTE。
若此时内存中没有空闲物理块安置请求调入的新页面,则系统按照替换策略选择一个牺牲页。如果牺牲页已经被修改了,那么内核就会将它复制回磁盘。接着内核将牺牲页的页表条目有效位置0。
7.9本章小结
本章主要介绍hello的存储器地址空间,阐述了Intel逻辑地址到线性地址的变换-段式管理、hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理。
结论
hello的一生:
①由程序员编写hello.c程序;
②hello.c由预处理器进行预处理,形成hello.i文件;
③hello.i经过编译,形成汇编代码hello.s;
④hello.s经过编译,生成可重定位目标文件hello.o;
⑤hello.o经过链接器链接,形成可执行文件hello;
⑥shell-bash程序调用fork函数为hello生成新进程,并调用execve函数;
⑦操作系统将hello载入内存并运行;
⑧hello程序运行的效果通过I/O设备呈现;
⑨hello运行终止后被shell回收,内核清空hello的数据结构等信息。
这门课程的内容非常丰富。它从低层次的硬件组成、汇编语言、操作系统等方面全面介绍了计算机系统的各个方面。同时,它还涉及到了一些高级的话题,如多线程和并发编程、虚拟内存、程序优化等。这些内容囊括了计算机系统的方方面面,让我对计算机系统有了更加全面的认识。此外,通过本门课程我还学习到了gdb调试工具的使用、x86汇编语言等,能够帮助我们更好地学习和理解计算机系统的工作原理。
现在的我看待代码有了一个全新的不同的视角,我相信通过本门课程的学习,我编写的代码可以更高效、对硬件更加友好;当程序出BUG时,可以更快、更精准地定位,可以更深层次地理解程序出错的原因。
总之,这门课程我学习到了许多关于计算机的底层知识,收获颇丰。
附件 中间文件及其作用
文件名 | 文件作用 |
---|---|
hello.c | 源文件 |
hello.i | hello.c经过预处理(cpp)后的文本文件 |
hello.s | hello.i经过编译(ccl)得到的汇编文件 |
hello.o | hello.s经过汇编(as)得到的可重定位目标文件 |
hello | hello.o与其他目标文件链接后得到的可执行目标文件 |
elf.txt | hello.o的elf文件 |
elf_out.txt | hello的elf文件 |
hello_obj.s | 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.