赞
踩
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1190200628
班 级 1903004
学 生
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
本文通过分析hello程序从C文件转变为可执行文件的全过程,包括预处理、编译、汇编、链接阶段,每一步如何对上一步形成的文件进行操作,形成新文件的过程。详细了讲述了hello程序在Linux系统的生命周期,hello进程在shell执行的过程,存储管理的过程,I/O处理的过程。本文全面回顾了计算机系统课程的主体框架,让我们对计算机系统有更深的了解。
**关键词:**hello程序;计算机系统;P2P;020;内存;进程;信号;链接
目 录
\1. P2P过程:Linux下,hello.c经过c预处理器进行预处理、编译器编译为汇编代码、汇编器汇编为二进制目标代码文件、链接器进行链接产生最终的可执行文件hello。在shell输入命令后,shell调用fork函数为可执行代码创建子进程,这就是hello的P2P(From Program to Process)过程;
\2. 020过程:然后shell通过调用execve函数加载并执行hello,映射虚拟内存。进入程序入口后,程序开始载入物理内存,然后进入main函数执行目标代码。 CPU为正在运行的hello分配时间片以执行逻辑控制流。程序结束后,shell父进程负责回收hello进程,内核删除相关的数据结构这就是hello的020过程。
AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx; 2.10 GHz; 8.00 GB RAM; 512GHD Disk
Windows10 64位; Vmware 15.5.0; Ubuntu 20.04 LTS 64位
vim/gedit; gdb; edb; objdump; readelf; hexedit
文件名称 | 文件说明 |
---|---|
hello.c | hello源文件 |
hello.i | 预处理后文本文件 |
hello.s | 编译生成的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello | 链接后可执行文件 |
hello_exe.s | hello可执行文件反汇编代码 |
hello.elf | hello的ELF文件 |
hello.objdump | hello.o的反汇编文件 |
hello_elf.txt | hello.o的ELF文件 |
本章对hello进行了简单的介绍,分析了P2P和020的过程,列出了本次实验的软硬件环境以及调试工具。同时列出了完成本次实验的过程中生成的中间结果文件的名字及其作用。
预处理器根据以字符#开头的命令,修改原始的C程序,得到了另一个C程序,通常是以.i作为文件扩展名。
计算机用预处理器(CPP)来执行预处理操作,操作的对象为原始代码中以字符#开头的命令,包括#include的头文件、#define的宏定义,#if、#ifdef、#endif等条件编译;同时删除注释。
预处理的过程中,对于引用一些封装的库或者代码的这些命令来说,预处理器会读取头文件中库的代码,并将这段代码直接插入到程序文件中;对于宏定义来说,会完成对宏定义的替换;注释会直接删除掉。最后得到另一个C程序,通常以.i为文件扩展名。
预处理阶段的作用是让编译器在随后对文件进行编译的过程中更加方便,因为访问库函数等操作在预处理阶段已经完成,减少了编译器的工作,提高效率。
Ubuntu下预处理的命令为 gcc -E hello.o -o hello.i
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TTQ5AJhl-1624816449110)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image004.png)]
我们查看原hello.c文件发现三条以字符#开头的命令,处理后查看hello.i文件,发现这三条命令已经不见了,同时文件扩展为三千多行,说明命令已经被解析,并把相应库文件内容复制到文件中;预处理阶段也将程序中的注释删除掉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gskNi3aE-1624816449112)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image006.png)]
hello.c部分截图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWTxh4Rd-1624816449115)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image008.png)]
hello.i部分截图
预处理是计算机对程序进行操作的第一个步骤,本章节介绍了预处理的概念和作用,并在Ubuntu下对hello.c进行了预处理,对结果进行了分析,了解到预处理实际上是在编译之前将头文件中的宏定义、变量定义等插入到文本中。
编译阶段是编译器对hello.i文件进行翻译得到文本文件hello.s的过程。此阶段编译器会完成对hello.i代码的语法和语义进行分析,生成汇编代码,并将这个代码保存在hello.s文件中。
编译的功能是把源程序(高级语言)翻译成目标程序。部分源程序的分析是经过词法分析、语法分析和语义分析三个步骤实现的。
\1. 词法分析由词法分析程序完成,其任务是识别单词(即标识符、常数、保留字,以及各种运算符、标点符号等)、造符号表和常数表,以及将源程序换码为编译程序易于分析和加工的内部形式。
\2. 语法分析程序是编译程序的核心部分,其主要任务是根据语言的语法规则,检查源程序是否合乎语法。如不合乎语法,则输出语法出错信息;如合乎语法,则分解源程序的语法结构,构造中间语言形式的内部程序。语法分析的目的是掌握单词是怎样组成语句的,以及语句又是如何组成程序的。
\3. 语义分析程序是进一步检查合法程序结构的语义正确性,其目的是保证标识符和常数的正确使用,把必要的信息收集和保存到符号表或中间语言程序中,并进行相应的语义处理。
Ubuntu下编译的命令为 gcc -S hello.o -o hello.s
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZDu22CeT-1624816449117)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image010.png)]
指令 | 含义 |
---|---|
.file | 源文件声明 |
.text | 代码段 |
.section .rodata | 只读数据段 |
.align 8 | 数据和指令存放地址的对齐方式 |
.string | 声明string型的数据,.LC0, .LC1 |
.globl | 声明全局变量 |
.data | 已初始化的全局和静态C变量 |
.type | 指明函数类型或数据类型 |
.size | 声明变量大小 |
.long | 声明long型的数据 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97S4M6un-1624816449119)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image012.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DEUx12gY-1624816449120)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image014.jpg)]
hello.c中使用的数据类型包括整型、数组和字符串三种类型。
\1. 字符串
程序中有两个字符串,作为printf函数的格式化参数,存放在只读数据段.rodata中。
l “用法: Hello 学号 姓名 秒数!\n”
可以发现字符串被编码为utf-8格式,一个汉字在utf-8编码中占三个字节。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uwiqBTYZ-1624816449121)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image015.jpg)]
l “Hello %s %s\n”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eTk7uZ0E-1624816449122)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image016.png)]
\2. 整形数据
a. int argc
argc 是main函数的第一个参数,初始为寄存器%rdi中,随后被保存至运行时栈中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uc1O6FIf-1624816449124)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image017.png)]
b. int i
i是整型局部变量,位于运行时栈中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ArVzzshr-1624816449125)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image018.jpg)]
\3. 数组
char *argv[]
argv是hello.c中唯一的数组,作为main的第二个参数,初始位于寄存器%rsi中,随后被保存至运行时栈中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tT5o7kg5-1624816449127)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image019.png)]
程序中出现的赋值语句是i = 0;程序中的赋值操作在汇编代码主要使用 mov 指令来实现,而根据数据的类型又有好几种不一样的后缀:
movb:传送字节
movw:传送字
movl:传送双字
movq:传送四字
movabsq:传送绝对的四字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F3WKc6B4-1624816449128)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image021.jpg)]
l addl $1, -4(%rbp):hello.c中的算数操作有:i++,由于是i是int类型的,因此汇编代码使用addl指令就能实现。
l leaq .LC(%rip), %rdi
for循环内部需要对LC1处字符串进行打印,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-71grDkPQ-1624816449129)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image023.jpg)]
l argc != 4 :判断argc不为4,这条指令被编译为 cmpl $4,-20(%rbp),这条cmpl指令会计算argc-4的结果并根据结果设置条件码,根据条件码来判断是否需要跳转到分支中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5uns8nLE-1624816449130)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image024.png)]
l i<8:在hello.c作为循环判断条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算i-7的结果并根据结果设置条件码,根据条件码判断是否需要跳转。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hcPgWDUN-1624816449131)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image025.png)]
程序中的控制转移有两处:
l if(argc != 4):当argc不等于4时执行26行到29行程序段中的代码。如果argc等于4,则跳转到.L2处执行以下指令。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZl6CrKm-1624816449132)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image027.jpg)]
l for(i=0;i<8;i++):使用计数变量i循环8次。首先无条件跳转到位于循环体.L3处的比较代码,使用cmpl指令进行比较,如果i<=7,则跳转到.L4处循环体执行,否则说明循环终止,顺序执行for之后的指令。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uQWO6ebP-1624816449133)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image029.jpg)]
函数是一种过程,过程提供了一种封装代码的方式,用一组参数和返回值来实现某种功能。比如P中调用函数Q包含以下步骤:
\1. 传递控制:
进行函数Q的时候,PC必须设置为Q的起始地址,然后在返回时,要把PC设置为函数P中调用Q后面那一条指令的地址。
\2. 传递数据:
函数P必须能够向函数Q提供一个或多个参数,同时Q能够向P中返回一个值。
\3. 分配和释放内存:
在开始时,Q可能需要为局部变量分配空间,而在返回前,必须释放这些空间。
在hello.c程序中涉及的函数操作及分析如下:
l main函数:
\1. 传递控制:
使用系统相应的一个call内部指令调用语句对一个main调用函数内部指令进行函数调用(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址压栈,然后跳转到main函数。
\2. 传递数据:
外部调用过程向main函数传递参数argc和argv,分别使用%edi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。
\3. 分配和释放内存:
使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于
mov %rbp,%rsp
pop %rbp
恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP。
l printf函数:
\1. 传递数据:
第一次printf将%rdi设置为“用法: Hello 学号 姓名 秒数!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rdx为argv[1],%rsi为argv[2]。
\2. 控制传递:
第一次printf因为只有一个字符串参数,所以call puts@PLT,第二次printf使用call printf@PLT。
l exit函数:
\1. 传递数据
将%edi设置为1。
\2. 控制传递
call exit@PLT。
l sleep函数:
\1. 传递数据
将%edi设置为sleepsecs。
\2. 控制传递
call sleep@PLT。
l getchar函数:
\1. 传递数据
空白字符
\2. 控制传递
call gethcar@PLT
本章详细地解读了hello.i经编译器处理得到hello.s的过程,介绍了编译的概念、作用并详细分析了hello.i程序的编译结果。分析了C语言的数据与各种操作在机器之中如何被处理翻译的。
汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o 中。hello.o文件是一个二进制文件,它包含的字节是程序的指令编码。
将汇编代码转换为机器指令,使其在链接后能被机器识别并执行。
汇编命令为gcc -c hello.s -o hello.o
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pQHHPUQT-1624816449134)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image031.jpg)]
利用readelf工具分析可重定位目标文件hello.o,查看其各节的基本信息,特别是重定位项目分析。可以分别查看各节内容,也可以通过指令 readelf -a hello.o > hello_elf.txt 将所有信息重定向到hello_elf.txt 文件中。
\1. ELF头 ELF header
ELF头是一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF 头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。通过命令readelf -h hello.o查看ELF头。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0uF1eo8-1624816449135)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image033.jpg)]
\2. 节头表 section headers talbe
节头表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从地址0开始,未来将会重定位。在ELF头中可以得到节头表的信息,通过节头表中的字节偏移可以得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码段是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXYyF7lu-1624816449136)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image035.jpg)]
节名称 | 包含内容 |
---|---|
.text | 已编译程序的机器代码 |
.rela.text | 一个.text节中位置的列表,链接器链接其他文件时,需修改这些位置。 |
.data | 已初始化的全局和静态C变量 |
.bss | 未初始化的全局和静态C变量和所有被初始化为0的全局或静态变量 |
.rodata | 只读数据段 |
.comment | 包含版本控制信息 |
.note.GNU-stack | 包含注释信息,有独立的格式 |
.symtab | 符号表,存放程序中定义和引用的函数和全局变量信息 |
.strtab | 字符串表,包括.symtab和.debug节中的符号表以及节头部中的节名字 |
.shstrtab | 包含节区名称 |
\3. 重定位信息
.rela.text∶ 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NwxBRJtQ-1624816449137)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image037.jpg)]
\4. 符号表:
.symtab∶ 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab 中都有一张符号表(除非程序员特意用 STRIP 命令去掉它)。然而,和编译器中的符号表不同,.symtab 符号表不包含局部变量的条目。name是符号名称,对于可重定位目标文件,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZahAjSbU-1624816449138)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image039.jpg)]
使用指令objdump -d -r hello.o得到反汇编代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ieI6xtE-1624816449139)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image041.jpg)]
对比hello.s中的main函数与反汇编后main函数对比如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUWnRSxX-1624816449141)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image043.jpg)]
差别如下:
\1. 分支转移
hello.s中的跳转指令使用的是段名称如.L2, .L3等,段名称只是汇编语言中便于编写的助记符,反汇编代码跳转指令的操作数使用的是确定地址。
\2. 函数调用
在hello.s中函数调用call之后直接跟着函数名称,而在反汇编程序中,call指令之后的目标地址就是下一条指令开始地址。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的地址,汇编成机器语言时,对于不确定的函数调用,call指令后的相对地址全部设置为0,然后在.rela.text节中添加重定位条目,在链接时进一步确定相对地址。
\3. 访问全局变量
在hello.s文件中,访问.rodata中的数据,使用的是段名称+%rip,在反汇编代码中是0+%rip,因为.rodata中数据地址也是在运行时确定,故访问也需要重定位,故将偏移设置为0,在.rela.data节中添加重定位条目,在链接时进一步确定相对地址。
本章详细地介绍了hello.s到hello.o的汇编过程,通过readelf工具查看了hello.o的ELF格式,并分析了其中的各个条目的内容。最后比较了hello.o反汇编的结果与hello.s的内容,更加深入地了解了从汇编语言映射到机器语言汇编器的转换过程。
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时; 也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时; 甚至执行于运行时(run time),也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。
本处链接是指将可重定位目标文件hello.o与其他一些文件组合为可执行目标文件hello。
链接器使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,面是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时, 只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
链接指令为:
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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90gKQfd1-1624816449142)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image045.jpg)]
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
使用readelf -a hello > hello.elf命令将hello的ELF信息重定向到文件hello.elf中,查看文件内容,分析如下:
\1. ELF头
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vhriRrA3-1624816449143)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image047.jpg)]
和hello.o的ELF头基本相同,但是类型由REL(可重定位文件)变成了EXEC(可执行文件),增加了程序头,节头数量增加到了27个。
\2. 节头表
节头表对hello中所有的节信息进行了声明,其中包括各节的虚拟地址起始地址、大小、类型、偏移量。
几个重要的节:
.init:从地址0x401000开始,其中定义了一个小函数,叫做_init,程序的初始化代码会调用它。
.text:从地址0x4010f0开始,即程序的入口点。
代码段从0x400000开始;数据段从0x404048开始。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0yhU3JLr-1624816449144)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image049.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBw9vQpD-1624816449146)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image051.jpg)]
\3. 程序头表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZRb4imy-1624816449148)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image053.jpg)]
\4. 重定位节
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ApzaraDz-1624816449149)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image055.jpg)]
\5. 符号表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y38mHJuY-1624816449150)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image057.jpg)]
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
.init段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5o3pZser-1624816449151)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image059.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4e1gUTVX-1624816449152)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image061.jpg)]
.text段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4iUSDQ93-1624816449153)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image062.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0iygdQo-1624816449154)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image064.jpg)]
.rodata段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wgkiXFjR-1624816449156)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image066.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TV9u3HbR-1624816449157)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image068.jpg)]
.data
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zTPVILRF-1624816449160)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image070.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RY7nfhR4-1624816449161)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image072.jpg)]
执行命令objdump -d -r hello > hello_exe.s,将hello的反汇编文件重定向的hello_exe.s中。
总体来看,hello的反汇编代码比hello.o的反汇编代码多了一些节(如.int, .plt, .plt.sec)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E91lVGsK-1624816449162)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image074.jpg)]
hello中加入了一些函数,比如_init(); _start()以及一些在主函数中调用的库函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Twoi3qVl-1624816449163)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image076.jpg)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-midFcb68-1624816449164)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image078.jpg)]
hello中不再存在hello.o中的可重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gzhmaAWG-1624816449166)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image080.jpg)]
由以上分析,我们可以看出,链接过程会扫描分析所有相关的可重定位目标文件,并完成两个主要任务:首先进行符号解析,将每个符号引用与一个符号定义关联起来;随后进行重定位,连接器使用汇编器产生的重定位条目的详细指令,把每个符号定义与一个内存位置关联起来。最终的结果是将程序运行所需的各部分组装在一起,形成一个可执行文件。
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
(1) 载入:_dl_start、_dl_init
(2)开始执行:_start、_libc_start_main
(3)执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
(4)退出:exit
程序名称 | 程序地址 |
---|---|
ld-2.27.so!_dl_start | 0x7fce:8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce:8cc47630 |
hello!_start | 0x4010f0 |
libc-2.27.so!__libc_start_main | 0x7fce:8c867ab0 |
hello!puts@plt | 0x401080 |
hello!sleep@plt | 0x4010c0 |
hello!getchar@plt | 0x4010a0 |
libc-2.31.so!exit | 0x400530 |
共享库在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,是由一个叫作动态链接器的程序来执行的。
首先在hello.elf文件中找到.got的地址0x403ff0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdJin23x-1624816449167)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image082.jpg)]
在edb找到对应的地址。在dl_init处设置断点,查看dl_init前后该地址内容的变化。
dl_init之前:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YgBmGhAB-1624816449169)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image084.jpg)]
dl_init之后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXlv3SXD-1624816449170)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image086.jpg)]
过程链接表(PLT)。PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
全局偏移表(GOT)。GOT是一个数组,其中每个条目是8字节地址。与PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。初始时,每个GOT条目都指向对应PLT条目的第二条指令。
本章分析了链接过程中对程序的处理。Linux系统使用可执行可链接格式,即ELF,具有.text,.rodata等节,并且通过特定的结构组织。
经过链接,ELF可重定位的目标文件变成可执行的目标文件,链接器会将静态库代码写入程序中,以及调用动态库等相关信息,将地址进行重定位,从而保证寻址的正确进行。静态库直接写入代码即可,而动态链接过程相对复杂一些,涉及共享库的寻址。
链接后,程序便能够在作为进程通过虚拟内存机制直接运行。
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,包括代码段、数据段、和堆栈区。代码段存储CPU执行的代码,数据段存储变量和进程执行期间使用的动态分配的内存,堆栈区存储活动过程调用的指令和本地变量。进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,有任务的启动而创建,随着任务的完成而消亡,所占用的内存也随之释放。
进程提供给应用程序两个关键抽象:逻辑控制流和私有地址空间。简化了程序员的编程以及语言处理系统的处理,即简化了编译、链接、共享和加载等过程。
\1. 每次用户向shell输入一个可执行目标文件的名字运行时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
\2. 提供给应用进程两个关键抽象:
a) 一个独立的逻辑控制流,好像程序可以独使用处理器。
b) 一个私有的地址空间,好像程序独占整个内存系统。
shell是一个交互型应用级程序,代表用户运行其他程序。shell执行一系列的读/求值步骤。读步骤读取用户的命令行,求值步骤解析命令。代表用户运行。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。
shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,或者是自由软件)。然后shell在可执行程序的目录列表里寻找这些应用程序。如果键入的命令不是一个内部命令并且在列表里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
在终端中输入./hello 1190200628 董恒基,shell判断它不是内置命令,于是会加载并运行当前目录下的可执行文件hello。
此时shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程最大的区别就是有不同的pid。
fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0。父进程与子进程是并发运行的独立进程。
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数的功能是加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。进程的地址空间如图6-2所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FtXHXrXD-1624816449171)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image088.png)]
加载并运行hello需要以下几个步骤:
\1. 删除已存在的用户区域
删除当前进程虚拟地址的用户部分中已存在的区域结构。
\2. 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
\3. 映射共享区域
如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址中的共享区域。
\4. 设置程序计数器
设置当前进程上下文中的程序计数器,使之指向代码段的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
在6.1进程的作用中已经提到过,当前的CPU中并不是只有hello一个程序在运行,这只是一个假象,实际上有很多进程需要执行。首先先了解几个概念:
上下文是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-buaQFa1c-1624816449172)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image090.jpg)]
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
在hello中,程序执行sleep函数时,sleep显式请求让调用进程休眠,调度器抢占当前的进程,并且利用上下文切换转移到新进程。Sleep函数结束后,再通过上下文切换返回到hello函数中。
hello执行过程中会出现的异常:
中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT
终止:信号SIGINT,默认行为是 终止
\1. 程序正常运行的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jM3zjNwT-1624816449174)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image092.jpg)]
\2. 不断乱按的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oh63FanS-1624816449175)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image094.jpg)]
\3. 运行时按Ctrl+C的结果
运行时按Ctrl+C。父进程收到SIGINT信号,终止hello进程,并且回收hello进程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZS5yi3re-1624816449176)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image096.jpg)]
\4. 运行时按Ctrl+Z后运行ps命令的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4WJydvfP-1624816449177)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image098.jpg)]
\5. 运行时按Ctrl+Z后运行jobs命令的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6sITuPgd-1624816449178)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image100.jpg)]
\6. 运行时按Ctrl+Z后运行pstree命令的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2L6bpae-1624816449180)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image102.jpg)]
\7. 运行时按Ctrl+Z后运行fg命令的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HeFf5fLV-1624816449181)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image104.jpg)]
\8. 运行时按Ctrl+Z后运行kill命令的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-co6gX1q9-1624816449182)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image106.jpg)]
本章介绍了进程的概念与作用;shell的作用与处理流程;fork, execve的过程;hello的进程执行以及hello中的异常与信号处理机制。
逻辑地址是指由程序产生的与段相关的偏移地址部分。逻辑地址由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。即hello.o里相对偏移地址。
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址是程序保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。就是hello里面的虚拟内存地址。
CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
一个逻辑地址由两部分组成,段选择符和段内偏移量。段选择符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。TI:0为GDT,1为LDT。Index指出选择描述符表中的哪个条目,RPL请求特权级。如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gCqekXRa-1624816449183)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image108.png)]
最初8086处理器的寄存器是16位的,为了能够访问更多的地址空间但不改变寄存器和指令的位宽,所以引入段寄存器,8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
分段功能在实模式和保护模式下有所不同。
\1. 实模式
即不设防,说逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
\2. 保护模式
线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。段寄存器无法放下32位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。描述符表中的一个条目描述一个段,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PVlfrjA3-1624816449184)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image110.jpg)]
对各个部分的功能做简要介绍:
Base:基地址,32位线性地址,指向段的开始。
Limit:段界限,指出这个段的大小。
DPL:描述符的特权级0(最高特权,内核模式)-3(最低特权,用户模式)。
所有段描述符被保存在两个表中:全局描述符表(GDT)和局部描述符表(LDT)。电脑中的每一个CPU(或一个处理核心)都含有一个叫做gdtr的寄存器,用于保存GDT的首个字节所在的线性内存地址。为了选出一个段,必须向段寄存器加载以上格式的段选择符。
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符在段描述符表中根据Index选择目标描述符条目Segment Descriptor,从目标描述符中提取出目标段的基地址Base address,最后加上偏移量offset共同构成线性地址Linear Address。
线性地址(即虚拟地址VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
Linux系统有自己的虚拟内存系统,其虚拟内存组织形式如图所示,Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYnrSAJC-1624816449185)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image112.png)]
系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传输的单元,在linux下每个虚拟页大小为4KB,类似地,物理内存也被分割为物理页(PP/页帧),虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
如图,不考虑TLB与多级页表(在7.4节中包含这两者的综合考虑),虚拟地址分为虚拟页号VPN和虚拟页偏移量VPO,根据位数限制分析可以确定VPN和VPO分别占多少位是多少。通过页表基址寄存器PTBR+VPN在页表中获得条目PTE,一条PTE中包含有效位、权限信息、物理页号,如果有效位是0+NULL则代表没有在虚拟内存空间中分配该内存,如果是有效位0+非NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中,如果有效位是1则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PYALRgzf-1624816449186)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image114.jpg)]
在Intel Core i7环境下研究VA到PA的地址翻译问题。前提如下:
虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。
解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。
如图所示 ,CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT+TLBI向TLB中匹配,如果命中,则得到PPN与VPO组合成PA。
如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。
如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PRQnoyuf-1624816449188)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image116.jpg)]
由于L1、L2、L3各级Cache的原理相同,只做L1 Cache的分析。L1 Cache是8路64组相连高速缓存。块大小64B。因为有64组,所以需要6 bit CI进行组寻址,共有8路,块大小为64B,所以需要6 bit CO表示数据偏移位置,因为VA共52 bit,所以CT共40 bit。
在上一步中已经获得了物理地址VA,使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)。如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是0,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ai4qPjJ2-1624816449191)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image118.jpg)]
当fork函数被shell调用时,内核为hello创建各种数据结构,并分配给它一个唯一的PID。为了给hello创建虚拟内存,fork创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello中返回时,hello现在的虚拟内存刚好和调用shell的虚拟内存相同。当这两个进程中的任何一个进行写操作时,写时复制机制会创建新页面。因此也就为每个进程保持了私有地址空间的概念。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u37a7isC-1624816449192)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image120.jpg)]
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。图7-7描述了加载器映射用户地址空间区域的模型。加载并运行hello需要以下几个步骤:
删除当前进程虚拟地址的用户部分中的已存在的区域结构。
代码和数据区域被映射为文件中的.text和.data区。.bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。
hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QwPouSNV-1624816449193)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image122.jpg)]
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出时就会发生故障。故障处理流程如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hiolhtSx-1624816449195)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image123.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L6AKY3ka-1624816449196)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image124.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4e18qkw3-1624816449197)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image126.jpg)]
缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
printf函数会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
\1. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cp9uAgwr-1624816449198)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image128.jpg)]
\2. 显式分配器:要求应用显式地释放任何已分配的块。如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-evv9KWlO-1624816449199)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image130.jpg)]
\1. 隐式空闲链表
头部一共四个字节,前三个字节存储的是块的大小,最后一个字节存储的是当前这个块是空闲块还是已分配的块,0代表空闲块,1代表已分配的块。中间的有效载荷就是用于存放已分配的块中的信息用的。最后的填充部分是为了地址对齐等一些要求用的。
隐式链表的结构就是根据地址从小到大进行连接的,如图所示。其中的每一个元素表示的是一个空闲块或者一个分配块,由于空闲块会合并的特性,链表中的元素的连接一定是空闲块的分配块交替连接的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DcsUbcIp-1624816449200)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image132.jpg)]
空闲块合并方法:因为有了 Footer,所以可以方便地对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,只需要通过改变 Header 和 Footer 中的值就可以完成这一操作。
\2. 显式空闲链表
显示结构在空闲块中增加了8个字节,分别保存当前空闲块的前驱空闲块的地址和后继空闲块的地址。显式的结构比隐式结构多维护了一个链表,就是空闲块的链表。这样做的好处就是在malloc的时候,隐式的方法是要遍历所有的块,包括空闲块了分配块。但是显式的结构只需要在空闲块中维护的链表检索就可以了,这样降低了在malloc时候的复杂度。
关于空闲块的维护方式一共有两种,一种是后进先出的方式,另一种是按照地址的方式。按照地址维护很好理解,与隐式的结构大致相同。后进先出的方式的思想是,当一个分配块被free之后,将这个块放到链表的最开头,这样在malloc的时候会首先看一下最后被free的块是否符合要求。这样的好处是释放一个块的时候比较高效,直接放在头部就可以。
本章讨论了hello的存储器地址空间,虚拟地址、物理地址、线性地址、逻辑地址的概念,Intel逻辑地址到线性地址的变换-段式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理。
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O设备管理方法。
\1. 打开文件。应用程序通过要求内核打开相应的文件,想要访问一个I/O设备时,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
\2. shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误,分别对应描述符的0,1,2。
\3. 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个k是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变文件的当前位置。
\4. 读写文件。读操作是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF;写操作是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
\5. 关闭文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
\1. int open(char* filename,int flags,mode_t mode)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8CwgLpUI-1624816449201)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image134.jpg)]
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。
\2. int close(fd)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9guvr9nT-1624816449202)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image136.jpg)]
进程通过调用close函数关闭一个打开的文件。
\3. ssize_t read(int fd,void *buf,size_t n)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UU9qaisU-1624816449203)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image138.jpg)]
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
\4. ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
printf函数体:接受字符串指针数组fmt,然后将匹配到的参数按照fmt格式输出。printf内部调用了两个函数,一个是vsprintf,还有一个是write。printf函数代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ANcSRfjj-1624816449205)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image140.jpg)]
va_list的定义:
typedef char *va_list
这说明它是一个字符指针。
其中的: (char*)(&fmt) + 4) 表示的是…中的第一个参数。
vsprintf函数代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ohw7jG6w-1624816449206)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image142.png)]
vsprintf的作用是格式化。接受确定输出格式的格式字符串fnt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,并返回要打印的字符串的长度。
write的实现如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tcKieg8u-1624816449207)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image144.jpg)]
write函数中,先给寄存器传了几个参数,然后通过系统调用sys_call
sys_call的实现如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MeuphpsB-1624816449208)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image146.jpg)]
syscall函数将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar函数实现如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ve23307H-1624816449209)(file:///C:/Users/86133/AppData/Local/Temp/msohtmlclip1/01/clip_image148.jpg)]
本章简述了Linux的I/O设备管理机制,Unix I/O接口及函数,并简要分析了printf函数和getchar函数的实现。
一个hello程序的“简单”流程如下:
\1. 编写,通过编辑器将代码键入hello.c;
\2. 预处理,预处理器将hello.c调用的所有外部的库展开合并到一个C文件hello.i中,删除所有的注释;
\3. 编译,编译器将hello.i编译成为汇编文件hello.s;
\4. 汇编,汇编器将hello.s汇编成为可重定位目标文件hello.o;
\5. 链接,链接器将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello;
\6. 运行,在shell中输入./hello 1190200628 董恒基 1
\7. 创建子进程,shell进程调用fork函数为hello创建子进程
\8. 运行程序,shell调用execve函数,execve函数调用启动加载器,加载映射虚拟内存,进入hello程序入口后将程序载入物理内存,进入 main函数执行hello;
\9. 执行指令,CPU为其分配进程时间片,在一个时间片中,hello享有CPU资源,顺序执行相应的控制逻辑流;
\10. 访问内存,MMU将程序中使用的虚拟内存地址通过页表映射成物理地址;
\11. 动态申请内存,printf会调用malloc向动态内存分配器申请堆中的空间;
\12. 发送/接受信号,如果运行中键入Ctrl + C或Ctrl + Z,则调用shell的信号处理函数分别停止、挂起;
\13. 结束,shell进程回收hello进程,内核删除为hello进程创建的所有数据结构。
经过这次实验,我理解了hello.c文本文件转变为可执行程序的全过程,感悟到计算机系统设计的精密严谨和可靠,每一步都分割得很清楚,恰到好处得完成应该实现的功能,而且尽可能地优化。对可能出现的问题也考虑到了,并对每种问题进行了处理,非常地amazing。
所有hello分析过程中的中间产物如下表。
文件名称 | 文件说明 |
---|---|
hello.c | hello源文件 |
hello.i | 预处理后文本文件 |
hello.s | 编译生成的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello | 链接后可执行文件 |
hello_exe.s | hello可执行文件反汇编代码 |
hello.elf | hello的ELF文件 |
hello.objdump | hello.o的反汇编文件 |
hello_elf.txt | hello.o的ELF文件 |
[1] Randal E.Bryant David R.O’Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.
[2] Linux下逻辑地址、线性地址、物理地址详细总结https://blog.csdn.net/freeelinux/article/details/54136688.
[3] Linux C 常用库函数手册https://www.linuxfoundation.org/resources/open-source-guides/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。