计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021110770
班 级 21W0311
学 生 覃萧何
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文介绍了Hello程序的一生:预处理,编译,汇编,链接,虚拟地址映射,运行,访存,结束,被父进程回收。在linux系统中演绎了Hello这个扮演着人与机器沟通的桥梁,这个短小但又不简单的程序的一生,说明程序的运行不仅需要机器软件与硬件的协调,也需要程序员与机器的合作。
关键词:程序;linux;预处理;编译;汇编;链接;
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
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 -
第1章 概述
1.1 Hello简介
--P2P:
作为Hello程序生命周期的开始,作为人与机器沟通桥梁的开端,hello.c程序被程序员创造出来。
图1.1 hello.c
使用C预处理器(cpp)对hello.c进行预编译,将程序员的话语进行翻译与扩充,即将各种宏进行展开的同时引入头文件,得到了hello.i;
使用编译器(cc1)对hello.i程序进行编译得到hello.s并使用汇编器进行汇编得到可重定位文件hello.o,像魔法一般,真正将程序员的话语翻译为了机器世界的语言(二进制文件);
最后还需要结合时代背景补充语义,使用链接器ld与其他库文件进行链接生成了可执行文件hello,让机器真正意义上地理解了程序员的语言。以上过程被称为P2P,是计算机开始理解程序员语言的过程。
--020:
Linux Shell命令行接收“./hello”的命令后,使用fork函数创建了一个子进程,之后通过execve函数在在进程上下文中加载并运行hello,将进程映射到虚拟内存空间,并加载需要的物理内存,在这个整个世界只有对方的世界,计算机将仔细聆听对方的话语(各种硬件协同配合使得一个进程独占整个虚拟内存世界)。为了理解程序员的命令,计算机大脑一刻都不休息(执行时,将各种指令加入CPU流水线中,使得指令高效执行)。为了快速回忆起程序员的命令,计算机利用局部性原理,通过多级Cache加速了访存。最后反馈执行结果,安静地消失,仿佛从来没来过一般(执行结束后父进程将回收这一进程,内核清除这一进程的相关信息,进程结束)。以上过程被称为020,是计算机执行程序员命令并让程序员理解计算机的过程。
1.2 环境与工具
硬件环境:处理器:X86 AMD Ryzen 7 5800H with Radeon Graphics CPU
RAM:32GB
系统:64位操作系统
图1.2 计算机硬件环境
软件环境:Windows 10;
Vmware Workstation Pro 15;
Windows Subsystem for Linux(WSL);
Visual Stdio Code
开发工具:gcc,as,ld,gdb,edb,readelf
1.3 中间结果
hello.c 源文件
hello.i 源文件预处理所得文件
hello.s 汇编程序
hello.o 可重定位文件
hello 可执行文件
hello.elf 可重定位文件hello.o的解析结果
hello.txt 可重定位文件hello.o的反汇编结果
Hello.elf 可执行文件hello的解析结果
Hello.txt 可执行文件hello的反汇编结果
1.4 本章小结
本章主要介绍了hello程序的生命周期,对hello程序进行分析的硬件软件环境,
以及实验和分析过程产生的文件。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改源程序。
作用:
1. 处理头文件:#include命令告诉预处理器读取头文件的内容,并将其直接插入程序文本中。
2. 处理宏定义:对于#define指令,进行宏替换,对于代码中所有使用宏定义的地方使用符号表示的实际值替换定义的符号
3. 处理条件编译:根据可能存在的#ifdef来确定程序需要执行的代码段。
4. 处理特殊符号:例如#error等,预编译程序可识别一些特殊的符号,并在后续过程中进行合适的替换。
5. 删除源程序中的注释部分。
2.2在Ubuntu下预处理的命令
如图执行gcc -E hello.c -o hello.i,预处理hello.c程序并将预处理结果命名为hello.i。
图3 预处理指令
图2.1 预处理结果文件hello.i
2.3 Hello的预处理结果解析
注意到,经过预处理后,文件的长度大大增加,这是预处理器将头文件的内容复制进了源程序文本文件的结果。如图头文件的长度高达3000行,源文件的文本出现在3000行之后。
图2.2:hello.i文件
并且可以发现,原本存在于源文件的注释也已经消失不见,说明预处理器会在将库文件复制入源文件的同时将注释删除。
同时可以发现,在预处理时,预处理器不仅将在源文件中引用的头文件复制入文本中,还将未出现的库文件加入了其中(alloca.h),这是或许是因为在源程序被引用的库文件中也引用了不同的文件。
图2.3 alloca.h
2.4 本章小结
本章介绍了预处理的概念与作用,结合hello.i分析了预处理器预处理源程序的过程,展示了在Ubuntu中预处理hello.c的结果。
第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将高级程序设计语言翻译为汇编语言的过程。
作用:
1. 分析程序编写是否符合C语言程序编写规范要求,如若错误输出语法错误提示;
2. 根据程序员编译优化指令,在保证安全,正确执行程序的同时进行相应级别的优化。
3.2 在Ubuntu下编译的命令
图3.1 编译指令
图3.2 编译结果
3.3 Hello的编译结果解析
3.3.1 数据
1. 常量:
(1):数字常量:在汇编代码中,数字常量一般直接使用,如在该段代码中:
如果argc与4不相等,则会在提示信息后直接退出。可以发现,该段代码经过汇编后,数字常量4原封不动地在汇编代码中使用。
(2):字符串常量:可以注意到,在汇编代码中,字符串常量一般存储在汇编代码的某个段内,如在该段代码中:
两个printf使用的字符串分别被保存在了.rodata的.LC0与.LC1段内。
2.变量:
(1):全局变量:已初始化的全局变量一般存储在.data段内,但在源程序hello.c中没有声明全局变量,因此汇编代码中并不存在.data段。
(2):局部变量:局部变量可以保存在栈中或保存在寄存器中。
可以注意到循环变量i在每次进入循环时都会对循环条件i<9进行判断,经编译器处理成与将栈中地址%rbp-4处的值与8进行比较,因此可以得知循环变量被保存在栈中%rbp-4的位置:
因为在调用printf函数前需要进行参数传递,经过汇编代码的分析得知,传入的两个参数分别保存在寄存器%rdx与寄存器%rax中,其值分别为地址%rbp-16与地址%rbp-24处的值,因为传入参数分别为数组argv的第二与第三个元素,所以得知argv数组被保存在栈中%rbp-28的位置:
3.3.2 赋值
在汇编代码中赋值是最常见的操作之一,在hello.s中赋值操作主要用于局部变量的赋值与调用函数前对被用作传入参数的寄存器进行赋值。
在调用exit函数前,将1赋值给被用作传入参数的寄存器%edi。
在循环开始时将循环变量i赋值为零,在汇编代码中直接使用mov指令将保存了i的栈中内存地址为%rbp-4处赋值为0。
3.3.3 类型转换
可以注意到,atoi函数返回值为有符号整数int类型,而sleep函数接收参数类型为无符号整数unsigned int类型,因此在进行sleep函数调用传入参数时,需要进行隐式类型转换。
由计算机系统知识得知,在有符号整数与无符号整数之间做类型转换时,在没有发生溢出的情况下,转换时二进制数据本身并未发生改变,同时观察汇编代码可以发现,在进行参数传递时也仅仅只是使用了movl指令,即将寄存器%eax的低32位复制入寄存器%edi中,同时将寄存器%edi的高32位清零,并未发生数据的变动,印证了课上学习到的知识与我们的想法。
3.3.4 算术操作
1. 对于循环变量i,在每次循环时都需要将其自增加一:
2. 使用printf函数打印argv数组中字符串时,需要进行参数传递,因此需将栈中argv元素通过栈基址寄存器%rbp与偏移量进行算术运算取出:
3.3.5 关系运算
源代码中一共出现了两处关系操作。
第一处是对于argc的判断,不等于4时输出错位提示信息并退出程序,否则将进行条件跳转:
另一处是在for循环中对于循环变量i的判断,当循环变量小于9时继续进行循环,否则退出循环:
3.3.6 数组,指针,结构操作
源代码中数组操作仅出现了一次,即对于argv数组的操作,观察汇编代码可以发现argv储存的两个值都存放在栈中,argv[1]的储存地址是%rbp-16,argv[2]存储地址为%rbp-24:
3.3.7 函数调用
在这一段代码中出现了多个函数调用的情况,在x86系统中函数参数储存第一至第六参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9 寄存器中,其余的参数保存在栈中的某些位置。
- main函数:
---参数:传入参数为argc与argv,其中argv储存在栈中,argc 储存在%rdi 中。
---返回:源代码中返回语句为return 0,因此在汇编代码中最后为将%eax 设置为0并返回这一寄存器:
- printf函数:
---参数:第一次调用的时候传入了字符串参数首地址并且编译器将其转换为调用puts函数;第二次在for循环中调用时传入了argv[1]和argv[2]的地址。
---调用:第一次满足if条件的时候调用,第二次则为for循环条件满足的时调用。
- sleep函数:
---调用:将atoi函数返回值存入寄存器%edi中作为参数传递。
---返回:同样将返回值保存在%eax中。
- atoi函数:
---调用:将argv[3]保存在寄存器%rax中作为参数进行传递
---返回:将返回值保存在%eax中,并将返回值作为参数传递给sleep函数.
- getchar函数
---调用:在for循环结束后调用。getchar函数不需要参数,因此不需要寄存器进行参数传递。
---返回:将返回值保存在%eax中。
- exit函数:
---调用:在argc不等于4时进入if语句块与main函数结束时调用。传入参数保存在%edi中。
---返回:将返回值保存在%eax中。
3.4 本章小结
本章主要介绍了在将修改了的源程序文件转换为汇编程序的时候主要发生的 变化,以及汇编代码文件中使用到的操作,即源代码各语句对应的汇编代码中的指令对应的展示。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器as将hello.s翻译成机器语言指令,并将这些指令打包为可重定位目标程序的格式,将结果保存在二进制目标文件hello.o中。
作用:将汇编代码根据特定的转换规则转换为二进制机器代码,即为计算机真正能够理解的代码格式。
4.2 在Ubuntu下汇编的命令
图 4.1 汇编命令
4.3 可重定位目标elf格式
4.3.1 生成elf文件命令
readelf -a hello.o > hello.elf 使用重定位手段得到所需elf解析文件。
4.3.2 ELF头(ELF Header)
ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。具体ELF头的代码如下:
图4.2 ELF头
4.3.2 节头表(Section Headers)
描述了.o文件中每一个节的信息,例如每个节的名字(存储在字符串表中对应的位置),该节在可重定位文件中的位置,该节的类型(包括符号表,字符串表),以及该节所占字节大小。目标文件中的每一个节都有一个固定大小的条目。具体内容如图所示:
图4.3 节头表
4.3.3 各个节的具体内容
在表述各个节信息的节头表后,.elf文件列出了各个节的具体内容,如符号表:
图4.4 符号表
符号表描述了各符号(全局变量,函数…)的信息,包括:符号名称(存储在字符串表中对应的位置),符号类型(为函数还是节),是全局的还是局部的…
同理也elf文件也包含了重定位节,其中描述了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计算出正确的地址。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,sleep,getchar,atoi这些符号同样需要与相应的地址进行重定位。具体信息如下图所示:
图4.5 重定位节
4.4 Hello.o的结果解析
使用命令objdump -d -r hello.o > hello.txt 得到hello.o的反汇编结果hello.txt
图4.6 hello.o反汇编指令
图4.7 反汇编结果
分析hello.o的反汇编,并与hello.s进行对照,可以发现:
1. 两者进制表示不同:hello.s反汇编之后对于数字的表示为十进制,而hello.o反汇编之后数字的表示为十六进制。
2. 分支转移结果形式不同:对于条件跳转,hello.s反汇编中给出的是段的名字,例如.L2等来表示跳转的地址,而hello.o由于已为可重定位文件,对于每一行都已经分配了相应的地址,因此跳转命令后跟着的是需要跳转部分的目标地址。
3. 函数调用操作数不同:hello.s中,call指令后跟的是需要调用的函数的名称,而hello.txt反汇编代码中call指令使用的是main函数的相对偏移地址。同时可以发现在hello.o反汇编代码中调用函数的操作数都为0,即函数的相对地址为0,因为在链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。
4.5 本章小结
本章对汇编过程进行了一个简单但较为完整的叙述。经过汇编器之后,生成了一个可重定位的文件,为下一步链接做好了准备。并通过与hello.s的反汇编代码的比较,更加深入的理解了在汇编过程中发生的变化。
第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.1 链接命令
5.3 可执行目标文件hello的格式
命令:readelf -a hello > Hello.elf
图5.2 解析elf文件命令
5.3.1 ELF头
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量
图5.3 ELF头
5.3.2 节头表
描述了文件中每一个节的信息,例如每个节的名字(存储在字符串表中对应的位置),该节在可重定位文件中的位置,该节的类型(包括符号表,字符串表),以及该节所占字节大小。目标文件中的每一个节都有一个固定大小的条目:
图 5.4 节头表
分析hello的ELF格式,可知各段的起始地址,大小等信息都可以在节头表中获得,如存放代码的.text段的大小为0x145字节,在文件中的位置为0x10f0。
5.4 hello的虚拟地址空间
使用edb打开hello可执行文件,可以在edb的Data Dump窗口看到hello的虚拟空间分配情况:
图5.5 hello虚拟地址分配
可以发现这一段程序的地址是从0x401000开始的,并且该处有ELF的标识, 可以判断从可执行文件时加载的信息。接下来可以分析其中的一些具体的内容:其中PHDR保存的是程序头表;INTERP保存了程序执行前需要调用的解释器;LOAD记录程序目标代码和常量信息;DYNAMIC储存了动态链接器所使用的信息;NOTE记录的是一些辅助信息;GNU_EH_FRAME保存异常信息;GNU_STACK使用系统栈所需要的权限信息;GNU_RELRO保存在重定位之后只读信息的位置。
5.5 链接的重定位过程分析
命令:objdump -d -r hello > Hello.txt
图5.6 反汇编hello命令
图5.7 反汇编结果
分析Hello.txt与hello.txt这两个反汇编结果,可以发现:
1.在链接过程中,hello中加入了代码中调用的一些库函数,例如系统函数_init,库函数getchar,puts,printf,atoi等,同时每个函数都有对应的虚拟地址,如_init:
图5.8 _init代码及其虚拟地址
2.对于全局变量的引用,由于hello.o中还未对全局变量进行定位,因此 hello.o 中用0加上%rip的值来表示全局变量的位置,而在hello中,由于已经进行了定位, 因此全局变量的的值使用一个确切的值加上%rip表示全局变量的位置:
图5.9 hello.o反汇编结果
图5.10 hello反汇编结果
3.hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。这是由于hello.o中对于函数还未进行定位,只是在.rel.text 中添加了重定位条目,而hello进行定位之后自然不需要重定位条目。
4.地址访问:在链接完成之后,hello中的所有对于地址的访问或是引用都调用的是虚拟地址地址。
结合Hello.txt与计算机系统知识,可知链接主要分为两个过程:符号解析和重定位。
符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。
重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
5.6 hello的执行流程
根据反汇编代码可以看出执行函数及虚拟内存地址如下:
(1) _start
(2) __libc_start_main
(3) __GI___cxa_atexit
(4) __internal_atexit
(5) __lll_cas_lock
(6) __new_exitfn
(7) __libc_csu_init
(8) _init
(9) _setjmp
(10) __sigsetjmp
(11) __sigjmp_save
(12) main
(13) puts@plt
(14)exit
(15) _dl_runtime_resolve_xsavec
(16) _dl_fixup
(17) _dl_lookup_symbol_x
(18) do_lookup_x
(19) _fini
(20)__libc_csu_fini
图5.11 edb分析hello执行过程
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地 址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init 后.got.plt节发生的变化。首先可以观察elf中.got.plt节的内容:
图5.12 .got.plt节内容
edb查看发现:
图5.13 执行_init前地址
图5.14 执行_init后地址
5.8 本章小结
在链接过程中,各种代码和数据片段收集并组合为一个单一文件。利用链接器,分离编译称为可能,我们不用将应用程序组织为巨大的源文件,只是把它们分解为更小的管理模块,并在应用时将它们链接就可以完成一个完整的任务。经过链接,已经得到了一个可执行文件,接下来只需要在shell中调用命令就可以为这一文件创建进程并执行该文件。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是执行中程序的抽象。
作用:在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:一个独立的逻辑控制流,提供一个程序独占处理器的假象;一个私有的地址空间,提供一个程序独占地使用内存系统的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并根据解析结果运行程序。
处理流程:
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有从处理的结果中用到的注释删除,并且按照一定顺序实行命令的检查
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新 创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同 的(但是独立的)一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获 得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载 并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序。所以,与fork一次调用返回两次不同,在exceve调用一次 并从不返回。当加载可执行目标文件后,exceve调用启动代码,启动代码设置栈, 将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的 第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
6.5 Hello的进程执行
进程调度:即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在运行时动态链接到程序的共享对象中的指令。这个PC的序列叫做逻辑控制流,或者简称逻辑流。进程是轮流适用处理器的,每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被 抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
内核模式转变到用户模式:操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,它是内核重启被抢占的进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。
进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了 的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:(1)保存当前进程上下文;(2)恢复某个先前被抢占的进程被保存的上下文;(3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。如下图所示:
图6.1 上下文切换
6.6 hello的异常与信号处理
正常执行状态:
图6.2 程序正常执行
异常类别:
图6.3 异常类型
处理方式:
- 中断:
图6.4 中断异常处理
- 陷阱和系统调用:
图6.4 陷阱和系统调用异常处理
- 故障:
图6.5 故障异常处理
- 终止:
图6.6 终止异常处理
不停乱按结果:将屏幕的输入缓存到缓冲区。乱码被认为是命令,不影响当前进程的执行。
图6.7 乱按输入结果
按下Ctrl-Z:程序运行时按Ctrl-Z,这时,产生中断异常,它的父进程会接收到信号SIGSTP并运行信号处理程序,然后便发现程序在这时被挂起了,并打印了相关挂起信息:
图6.7 按下Ctrl-Z输入结果
按下Ctrl-Z后运行ps指令:打印出了各进程的pid,可以看到之前挂起的进程hello。
图6.8 按下Ctrl-Z后运行ps结果
按下Ctrl-Z后运行jobs指令:打印出了被挂起进程组的jid,可以看到之前被挂起的hello,以被挂起的标识Stopped。
图6.9 按下Ctrl-Z后运行jobs结果
按下Ctrl-Z后运行pstree指令:
图6.10 按下Ctrl-Z后运行pstree结果
按下Ctrl-Z后运行fg:因为之前运行jobs是得知hello的jid为1,那么运行fg 1可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分,然后输入hello回车,程序运行结束,进程被回收。
图6.11 按下Ctrl-Z后运行fg结果
按下Ctrl-Z后运行kill:重新执行进程,可以发现hello的进程号为1736,那么便可通过kill -9 1736发送信号SIGKILL给进程1736,它会导致该进程被杀死:
图6.12 按下Ctrl-Z后运行kill结果
按下Ctrl-C:进程收到SIGINT信号,结束hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。
图6.13 按下Ctrl-C后结果
6.7本章小结
本章主要介绍了hello可执行文件的执行过程,包括进程创建、加载和终止,以及通过键盘输入等过程。从创建进程到进程并回收进程,这一整个过程中需要各种各样的异常和中断等信息。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持hello能够顺利地在计算机上运行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。从hello的反汇编代码中看到的地址,它们需要通过计算,通过加上对应段的基地址才能得到真正的地址,这些便是hello中的逻辑地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏 移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。
---段式管理特点:
1.段式管理以段为单位分配内存,每段分配一个连续的内存区。
2.由于各段长度不等,所以这些存储区的大小不一。
3.同一进程包含的各段之间不要求连续。
4.段式管理的内存分配与释放在作业或进程的执行过程中动态进行。
(以下格式自行编排,编辑时删除)
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动 态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把 内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU 从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存 了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中 是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核 需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
图7.1 Core i7 地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU 中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
虚拟地址VA虚拟页号VPN和虚拟页偏移VPO组成。若TLB命中时,所做操作与7.3中相同;若TLB不命中时,VPN被划分为四个片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量, 依次类推。最后在L4页表中对应的PTE中取出PPN,与VPO连接,形成物理地址PA。
图7.3 多级页表管理
7.5 三级Cache支持下的物理内存访问
MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中条目,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则在下一级cache或是主存中寻找需要的内容,储存到上一级cache后再一次请求读取。
图7.4 存储器层次结构
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。
为了给这个新进程创建虚拟内存,系统创建了当前进程的mm_struct、区域结 构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。
当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟 内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:
(1)删除已存在的用户区域
(2)映射私有区域:为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
(3)映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),指向代码的入口点
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完 成的:
1.处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
图7.5 缺页操作
7.9动态存储分配管理
定义:一种内存管理方法。对内存空间的分配、回收等操作在进程执行过程中进行,以便更好地适应系统的动态需求,提高内存利用率。
分配器的基本风格:
1.显示分配器:要求应用显示地释放任何已分配的块。2.隐式分配器:要求分配器检测一个已分配的块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器。
基本方法与策略:
1.带边界标签的隐式空闲链表分配器管理带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个符合大小的空闲块来放置这个请求块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。在释放一个已分配块的时候需要考虑是否能与前后空闲块合并,减少系统 中碎片的出现。
2.显示空间链表管理显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。放置策略与上述放置策略一致。
7.10本章小结
本章主要介绍了hello进程在执行的过程中的虚拟内存与物理内存之间的转 换关系,以及一些支持这些转换的硬件或软件机制。同时介绍了在发生缺页异常的时候系统将会如何处理这一异常。最后介绍了动态内存分配的作用以及部分方法与策略。
结论
hello的一生是简单的但是又蕴含着每一个c语言程序执行前的必经之路:
1.预处理,hello.c文件通过cpp的预处理,得到了扩展后的源程序文件hello.i
2.编译,hello.i通过编译器的处理,被翻译成了汇编语言程序hello.s
3.汇编,在汇编器as的处理下,hello.s生成了可重定位文件hello.o
4.链接,链接器将重定位目标文件链接为可执行目标文件hello
5.生成子进程,在shell中输入指定命令shell调用fork函数为hello生成进程。
6.execve加载并运行hello程序,将它映射到对应虚拟内存区域,并依需求载入物理内存。
7.hello将在cpu流水线中执行每一条指令
8.程序运行结束后,父进程会对其进行回收,内核把它从系统中清除。这样,hello就结束了它的一生。在计算机系统的设计与实现过程中所必须要满足的就是准确程序的执行必须能输出准确的结果,在这一基础上进行一定的优化能够让程序执行的更快,包括cache,流水线,超标量等设计都是基于这些的。在完成大作业的过程中相当于回顾了一遍这学期的学习内容,对于计算机系统设计与实现也有了更深切的感悟。
附件
hello.c 源文件
hello.i 源文件预处理所得文件
hello.s 汇编程序
hello.o 可重定位文件
hello 可执行文件
hello.elf 可重定位文件hello.o的解析结果
hello.txt 可重定位文件hello.o的反汇编结果
Hello.elf 可执行文件hello的解析结果
Hello.txt 可执行文件hello的反汇编结果
参考文献
[1]《深入理解计算机系统》Randal E.Bryant David R.O’Hallaron 机械工业出版社