摘 要
本论文研究了hello.c这一简单c语言文件在Linux系统下的整个生命周期,以其原始程序开始,依次深入研究了编译、链接、加载、运行、终止、接纳的过程,从而相识hello.c文件的“一生”。在《深入明确计算机系统》这本书中,作者用一个程序员的角度对待了一个程序的生命周期,本文我们将根据本书的知识,先容hello.c程序的运行周期,通过对hello.c程序的深入研究,加深对程序的认识和计算机底层工作原理的相识。
关键词:预处置惩罚;编译;汇编;计算机系统;进程;链接
目 录
第1章 概述
1.1 Hello简介
1.2 情况与工具
1.3 中间结果
1.4 本章小结
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
2.2在Ubuntu下预处置惩罚的下令
2.3 Hello的预处置惩罚结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的下令
3.3 Hello的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的下令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的下令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地点空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处置惩罚流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处置惩罚
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地点空间
7.2 Intel逻辑地点到线性地点的变更-段式管理
7.3 Hello的线性地点到物理地点的变更-页式管理
7.4 TLB与四级页表支持下的VA到PA的变更
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页停止处置惩罚
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
第1章 概述
1.1 Hello简介
Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统下,hello.c 文件依次颠末cpp预处置惩罚、ccl编译、as 汇编、ld 链接终极成为可执行目标程序hello。打开shell,输入下令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。
图 1 生成可执行文件的过程
Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被接纳,并由内核删除hello相关数据,这即为“to 0”。
1.2 情况与工具
硬件:
11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz 2.30 GHz
16GB RAM 1TB HD Disk
软件:
Windows10 64位
Ubuntu 22.04 LTS 64位
调试工具:
Visual Studio 2022 64-bit;
gedit,gcc,notepad++,readelf, objdump, hexedit, edb
1.3 中间结果
文件名
| 功能
| hello.c
| 源代码
| hello.i
| 预处置惩罚后得到的文本文件
| hello.s
| 编译后得到的汇编语言文件
| hello.o
| 汇编后得到的可重定位目标文件
| hello.elf
| 用readelf读取hello.o得到的ELF格式信息
| hello.asm
| 反汇编hello.o得到的反汇编文件
| hello
| 链接之后形成的可执行目标文件
| hello2.elf
| 由hello可执行文件生成的.elf文件
| hello2.asm
| 反汇编hello可执行文件得到的反汇编文件
| 表1 中间结果
1.4 本章小结
本章扼要先容了hello的P2P,020的具体过程,同时列出了研究时接纳的具体软硬件情况和中间结果。
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
2.1.1预处置惩罚的概念
预处置惩罚步骤是指程序开始运行时,预处置惩罚器(cpp,C Pre-Processor,C预处置惩罚器)根据以字符#开头的下令,修改原始的C程序的过程。为编译做预备工作,主要举行代码文本的更换工作。
2.1.2预处置惩罚的作用
宏界说:在编译预处置惩罚时,对程序中全部出现的宏名,都用宏界说中的字符串去代换,这称为“宏代换”或“宏展开”。
文件包含:文件包含下令的功能是把指定的文件插入到该下令行位置取代该下令行,从而把指定的文件和当前的源程序文件连成一个源文件。利用文件包含指令可以节省时间并淘汰出错,方便后续处置惩罚。
条件编译:预处置惩罚程序提供了条件编译的功能。可以按差别的条件去编译差别的程序部分,因而产生差别的目标代码文件。
2.2在Ubuntu下预处置惩罚的下令
在Ubuntu系统下,举行预处置惩罚的下令为:
gcc -E hello.c -o hello.i
运行截图如下:
图 2 Ubuntu下预处置惩罚
2.3 Hello的预处置惩罚结果解析
图 3 预处置惩罚结果
在ubuntu下用vim打开生成的hello.i文件,可以看到文件行数剧增,但对于main函数的部分,代码并没有发生变化,因此可以推断,在预处置惩罚阶段,预处置惩罚器仅仅是对main函数之前的引用和声明做了改动,文件行数的剧增就是因为#预处置惩罚下令将头文件的程序、宏变量、特殊符号等插入到hello.c中,而对于main函数本身以及之后的代码,预处置惩罚器对此并不感爱好。
2.4 本章小结
本章主要先容了预处置惩罚的概念及作用、并结合Ubuntu系统下hello.c文件实际预处置惩罚之后得到的hello.i程序对预处置惩罚结果举行相识析。从预处置惩罚步骤就可以初步发现,一个小小的hello world程序,其背后隐藏的东西要多得多。
第3章 编译
3.1 编译的概念与作用
编译是指编译器(ccl)通过词法分析、语法分析、语义检查和中间代码生成、代码优化以及目标代码生成这五个阶段来讲一个源程序翻译成目标程序的工作过程,总的来说,编译就是把代码转化为汇编指令的过程。
将高级程序语言源程序翻译为汇编语言程序,为后续将其转化为二进制机器码做准备。此外,编译还可以举行错误分析,并给出提示信息;对程序中的代码举行优化。
留意:这儿的编译是指从 .i 到 .s 即预处置惩罚后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的下令
在Ubuntu系统下,举行预处置惩罚的下令为:
gcc -m64 -no-pie -fno-PIC -S -o hello.s hello.i
运行截图如下:
3.3 Hello的编译结果解析
对hello.s文件团体结构分析如下:
内容
| 含义
| .file
| 源文件
| .text
| 代码段
| .global
| 全局变量
| .data
| 存放已经初始化的全局和静态C 变量
| .section .rodata
| 存放只读变量
| .align
| 对齐方式
| .type
| 表示是函数类型/对象类型
| .size
| 表示大小
| .long .string
| 表示是long类型/string类型
| 表2 hello.s文件结构
3.3.2数据类型
在源代码之中,我们构造了数组,局部变量,常量和字符串四种数据类型,接下来我们观察在hello.s中这些变量的表示方式。
程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地点保存在栈中。
图 5 数组的情况
编译器将局部变量存储在寄存器或者栈空间中。i作为函数内部的局部变量,并不占用文件实际节的空间,只存在于运行时栈中。对于i的操作就是直接对寄存器或栈举行操作。
图 6 局部变量i的情况
图 7 常量的情况
可以看出编译过程中,常量直接用立即数来代替,并没有对其分配空间。
程序中保存了两个字符串,分别为:
图 8 字符串情况
两者均为字符串常量,储存在只读数据段中。\XXX为UTF-8编码。
3.3.3赋值操作
对局部变量i的赋值在汇编代码中通过mov指令完成。具体利用哪条mov指令由数据的大小决定。通过源代码可以得知赋值操作在for循环中。在汇编指令中的操作如下:
图 9 对i的赋值操作
3.3.4关系操作
由程序源代码可知,关系判断出现了2次,分别在if(argc!=4)和for(i=0;i<8;i++),通过下面的汇编代码可以看出,编译器利用cmp指令对两个操作数举行大小比力,并利用结果对操作码举行设置。
图 10 关系操作情况
3.3.5 算数操作
在hello.s中,具体涉及的算数操作包括:
- subq $32, %rsp:开辟栈帧
- addq $16, %rax:修改地点偏移量
- addl $1, -4(%rbp):实现i++的操作
可以参考图10。
3.3.6 数组操作
在3.3.2中提到,数组储存在栈中,分析汇编代码可以看出,数组接纳寄存器寻址的方式举行访问。
图 11 数组操作
3.3.7 控制转移
程序中控制转移的具体体现有两处:
1)if(argc!=4):
当argc不即是4时,执行函数体内部的代码。在hello.s中,利用cmpl $4,-20(%rbp),比力 argc与4是否相等,若相等,则跳转至.L2,不执行后续部分内容;若不等则继续执行函数体内部对应的汇编代码。
2)for(i=0;i<8;i++):
当i < 时举行循环,每次循环i++。在hello.s中,利用cmpl $7,-4 (%rbp),比力 i与7是否相等,在i<=7时继续循环,进入.L4,i>7时跳出循环。
跳转过程参考图10。
3.3.8函数操作
1)main函数
函数调用:由系统来调用,或者更准确地说,由execve函数来调用。
函数返回:利用movl指令将%rax寄存器中的值置为0。
2)printf函数
图 12 printf函数调用
传入.LC1作为参数,作为printf的输出
3)Aoti函数
图 13 Atoi函数调用
4)sleep函数
图 14 sleep函数调用
5) getchar函数调用
图 15 getchar函数调用
对于函数调用,需要call指令和ret指令配合完成,call指令负责将返回地点压栈,并将PC的值跳转为调用函数的第一条指令的地点,ret指令则将返回地点弹出到PC寄存器中,从而完成函数调用的返回。在函数调用过程中,有六个寄存器负责传递参数,参数数量超过六个需要用栈来传递。
3.4 本章小结
本章重点先容了编译的概念和作用,它是将文本文件转化为汇编语言程序的过程,为后续生成二进制机器码做准备。以hello.s文件为例,我们详细讨论了编译器如那边置惩罚差别的数据类型和各种操作,并验证了这些数据和操作在汇编代码中的实现方式。我们发现,编译器并不是简单地按照原始文本的次序逐条翻译代码。在编译过程中,编译器会举行一些隐式的优化,并利用控制转移等技能来处置惩罚代码中的跳转和循环等操作。终极,编译器生成了我们所需的hello.s文件。通过这一过程,我们可以更好地明确编译器在将高级语言转化为底层机器码的工作原理。
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,终极结果保存在.o 二进制目标文件中的过程
将我们之前在hello.s中保存的汇编代码翻译成可以供机器执行的二进制代码,如许机器就可以根据这些01代码,真正的开始执行我们写的程序。
留意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的下令
在Ubuntu下汇编的下令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
汇编过程如下:
图 16 Ubuntu下汇编下令
4.3 可重定位目标elf格式
1.ELF Header
以16字节序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节次序,其中7f 45 4c 46为固定的邪术字节,02表示64位,第一个01表示小端序,第二个01表示ELF头版本。ELF头剩下的部分包括帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位的)、机器类型、节头部表的文件偏移。以及节头目表中条目标大小和数量。
图 17 ELF header中的信息
2.重定位节.rela.text
一个.text节中位置的列表,存放着代码的重定位条目,其类型为 RELA,也就是说它是一个重定位表,当链接器把这个目标文件和其他文件组适时,需要修改这些位置。在此重定位表中,每个要被重定位的地方叫重定位入口,我们可以看到每个重定位入口在段中的偏移位置,重定位入口的类型,重定位入口的名称以及重定位修正的辅助信息等。
图 18 重定位表和重定位节
3.重定位节.rela.eh_frame
.rela.eh_frame节同.rela.eh_frame一样属于重定位信息的节,包含的是eh_frame的重定位信息
4.符号表Symbol table .symtab
符号表中存放着在程序中界说和引用的函数和全局变量的信息,与编译器的符号表差别,.symtab符号表不包含局部变量的条目。
图 19 符号表
4.4 Hello.o的结果解析
利用objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s举行对照分析。
图 20 hello.o的反汇编
反汇编得到的文件左边比hello.s多了机器语言,而机器语言实际上就是对汇编语言的编码,每条汇编代码都具有唯一的机器编码。就二者之间的差别更具体地分析如下:
1.分支转移:在hello.s中,跳转指令的目标地点为段名称,如.L2、.L3;在反汇编代码中,跳转指令的目标地点为具体的地点;在机器代码中为目标指令地点与下一条指令地点的差值。
2.操作数:机器语言和反汇编语言中的操作数都是十六进制的,而汇编语言是十进制的。
3.函数调用:在hello.s中,函数的调用是通过在call指令后面直接跟随函数名来实现的。然而,在反汇编代码中,对函数的调用的目标地点却是当前指令的下一条指令的地点。这种差别的缘故原由在于hello.c中调用的函数属于共享库函数,需要在运行时通过动态链接器确定函数的地点。因此,在汇编过程中,我们将call指令后的目标地点设置为下一条指令的地点,并在函数调用后生成了一个重定位条目。这个重定位条目标作用是告诉链接器利用函数名之前的重定位类型来举行函数引用的重定位操作。通过这种方式,我们可以在编译过程中处置惩罚共享库函数的调用,确保函数的正确引用和地点解析。
图 21 汇编与反汇编对比
4.5 本章小结
本章主要探究了汇编语言的概念和作用。首先,我们演示了如安在Linux系统下将hello.s文件举行汇编,生成了可重定位目标文件hello.o。通过利用readelf指令,我们还生成了hello.o的elf格式,并对其具体结构举行了研究。为了进一步明确hello.o的机器代码,我们将其与hello.s的汇编代码举行对比,举行了反汇编并解析了其中的指令。这个比力帮助我们认识到汇编语言与机器语言之间的相似之处和差别之处。需要指出的是,在二进制代码中,全部的指令和函数名等都被转换为相应的存储地点,使得机器可以直接读取和执行这些代码。因此,综上所述,hello.o已经非常靠近于可执行的机器代码情势。
第5章 链接
5.1 链接的概念与作用
链接的概念
链接是指通过链接器(Linker),将程序编码与数据块网络并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程。
链接的作用
链接可以将程序封装成多个模块。在编写程序时,我们只需要关注主程序部分,而可以直接调用其他模块,就像在C语言中利用printf函数一样。链接的使命是处置惩罚程序对其他模块的调用,并将这些模块中的代码组合到相应的可执行文件中。在链接的过程中,我们还可以利用静态库中提供的各种函数和声明,进一步扩展程序的功能。通过链接,我们可以将程序的各个部分连接在一起,形成一个完备的可执行文件,使得程序能够正常运行并调用所需的模块和库函数。
留意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的下令
在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
链接过程如下:
图 22 Ubuntu下链接
5.3 可执行目标文件hello的格式
readelf -a hello > hello1.elf 生成 hello 程序的 ELF 格式文件,保存为hello1.elf。
1.ELF Header
以16字节序列Magic开始,这个序列描述了生成该文件的系统 的字的大小和字节次序,其中7f 45 4c 46为固定的邪术字节,02表示64位 ,第一个01表示小端序,第二个01表示ELF头版本。ELF头剩下的部分包 括帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目 标文件的类型(可重定位的)、机器类型、节头部表的文件偏移。以及节头目表中条目标大小和数量。与hello.elf相比力,hello2.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地点。
图 23 hello可执行文件的ELF Header
2.节头
可以看到hello文件中的节的数量比hello.o中多了很多,说明在链接过后有很多文件有添加了进来。下面列出每一节中各个信息条目标含义:名称和大小这个条目中存储了每一个节的名称和这个节在重定位文件种所占的大小。地点这个条目中,保存了各个节在重定位文件中的具体位置也就是地点。偏移量这一栏中保存的是 这个节在程序内里的地点的偏移量,也就是相对地点。
图 24 hello的节头
3.程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
图 25 程序头
4.Dynamic section
图 26 Dynamic section
5.符号表
符号表中保存着定位、重定位程序中符号界说和引用的信息,全部重定位需要引用的符号都在其中声明。
图 27 hello的符号表
5.4 hello的虚拟地点空间
利用edb加载hello,查看本进程的虚拟地点空间各段信息,并与5.3对照分析说明。
根据计算机系统的特性,程序被载入至地点0x400000~0x401000中。在该地点范围内,每个节的地点都与前一节中节对应的Address相同。根据edb查看的结果,在地点空间0x400000~0x400fff中存放着与地点空间0x400000~0x401000相同的程序,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。
图 28 虚拟地点空间
5.5 链接的重定位过程分析
5.5.1.hello与hello.o的差别
1)链接后函数数量增加。动态链接器将共享库中hello.c用到的函数加入可执行文件中。所以链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。
2)函数调用指令call的参数发生变化。在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地点与下一条指令的地点之差,指向相应的代码段,从而得到完备的反汇编代码。
3)跳转指令参数发生变化。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT中相应函数与下条指令的相对地点,从而得到完备的反汇编代码。
5.5.2链接过程分析
1)链接器归并相同类型的部分:链接器首先将同一类型的部分归并到新的相同类型部分中。比方,全部文件的.data部分归并为一个新的.data部分,形成可执行文件hello的.data部分。
2)确定地点:链接器为新的聚合部分以及输入模块界说的部分和符号分配内存地点。一旦地点确定,全局变量、指令等都将具有唯一的运行时地点。同时,链接器判断输入文件是否为库文件,如果不是目标文件,则将其放入集合E 中。
3)符号解析:链接器解析目标文件中的符号,并将它们放入集合U中,如果符号看起来已界说但未利用,则将其放入集合D中。链接器还读取 crt* 库的目标文件。
4)动态链接库接入:链接器将动态链接库libc.so举行接入,使得可执行文件能够利用该动态链接库提供的函数和资源。
图 29 链接后的反汇编
5.6 hello的执行流程
利用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的全部过程。其调用与跳转的各个子程序名或程序地点。
0000000000401000 <_init>
0000000000401020 <.plt>
0000000000401090 <puts@plt>
00000000004010a0 <printf@plt>
00000000004010b0 <getchar@plt>
00000000004010c0 <atoi@plt>
00000000004010d0 <exit@plt>
00000000004010e0 <sleep@plt>
00000000004010f0 <_start>
0000000000401120 <_dl_relocate_static_pie>
0000000000401130 <deregister_tm_clones>
0000000000401160 <register_tm_clones>
00000000004011a0 <__do_global_dtors_aux>
00000000004011d0 <frame_dummy>
00000000004011d6 <main>
0000000000401270 <__libc_csu_init>
00000000004012e0 <__libc_csu_fini>
00000000004012e8 <_fini>
5.7 Hello的动态链接分析
动态链接的基本思想是将程序拆分为相对独立的模块,在程序运行时形成完备的程序,而不像静态链接那样将全部模块链接成单个可执行文件。然而,在形成可执行文件时,仍然需要利用动态链接库。当程序引用一个外部函数时,会检查动态链接库并找到相应的动态链接符号,但并不举行符号的重定位。直到程序执行过程中加载时,才会由动态链接器利用过程链接表(PLT)和全局偏移量表(GOT)来实现函数的动态链接。
每个被可执行程序调用的库函数都有自己的PLT条目,每个条目负责调用一个具体的函数。GOT则包含动态链接器在解析函数地点时利用的信息。在加载时,动态链接器会对GOT中的每个条目举行重定位,确保它们包含目标函数的正确绝对地点。
通过以上过程,动态链接器能够在程序执行时根据需要加载并链接库函数,实现函数的动态链接。这种动态链接的机制使得程序模块化、灵活性更高,能够在运行时根据需要动态装载和链接所需的库函数,提供了更加高效和灵活的程序执行方式。
图 30 动态链接前
图 31动态链接后
5.8 本章小结
本章详细先容了链接的概念和作用,并通过对链接后的可执行文件的ELF格式举行结构分析来深入明确链接的过程。我们对比了可重定位目标文件的ELF格式,分析了差别类型的信息。接下来,通过利用edb举行hello程序的调试,我们进一步分析了hello程序的虚拟地点空间,并验证了链接的过程。通过这个过程,我们清晰了hello程序的执行流程,并对其动态链接举行了深入分析,加深了对可执行文件执行过程和动态链接的明确。通过学习本章内容,我们对链接在程序开辟和执行中的重要性有了更深入的认识,也增强了对可执行文件的结构和执行过程的明确。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念
进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
进程的作用
给应用程序提供两个关键抽象:
一个独立的逻辑控制流,提供一个假象,好像程序独占地利用处置惩罚器
一个私有地点空间,提供一个假象,好像程序独占地利用内存系统
6.2 简述壳Shell-bash的作用与处置惩罚流程
以下是对提供的信息举行提炼和整理的一段话:
Shell是一个用C语言编写的交互式应用程序,其作用是代表用户运行其他程序,并提供了一个用户界面来执行系统的基本操作和访问操作系统内核的服务。Shell的处置惩罚流程包括以下步骤:
1.从Shell终端读取输入的下令。
2.切分输入字符串,提取和识别全部的参数。
3.如果输入的参数是内置下令,立即执行相应的操作。
4.如果输入的参数不是内置下令,则调用相应的程序为其分配子进程并运行。
5.如果输入的参数非法,则返回错误信息。
6.处置惩罚完当前参数后,继续处置惩罚下一个参数,直到全部参数都被处置惩罚完毕。
通过如许的流程,Shell能够接受用户输入的下令,并根据下令的类型执行相应的操作,包括执行内置下令或调用外部程序运行。Shell作为用户与操作系统之间的接口,提供了一种方便和灵活的方式来操作和管理计算机系统。
6.3 Hello的fork进程创建过程
打开Shell,输入下令./hello 2021112018 lr 1,带参数执行生成的可执行文件。
fork进程的创建过程如下:首先,父进程通过fork函数创建一个新的子进程hello,同时执行当前目录下的可执行文件hello。子进程会获取与父进程相同的上下文,包括栈、通用寄存器、程序计数器、情况变量和打开的文件的副本。子进程与父进程最大的区别是它有一个差别的PID,并且可以读取父进程打开的任何文件。
当子进程运行结束时,如果父进程仍然存在,则父进程会执行对子进程的接纳。如果父进程已经终止,那么子进程会由init进程往返收。如许可以确保子进程的资源被正确开释,制止产生僵尸进程。
图 32 程序执行情况
6.4 Hello的execve过程
execve函数的原型为:
int execve(const char *filename,const charargv[],const char envp[])
execve函数在当进步程的上下文中加载并运行一个新的程序。与fork函数差别的是,execve函数不创建新的进程,而是直接在当进步程中更换现有的虚拟内存段,并创建一组新的代码、数据、堆和用户栈段。
在执行execve函数时,当进步程的虚拟内存段被删除,并被新程序的代码、数据和堆所替代。栈和堆会被初始化为0,代码段和数据段则会被初始化为新程序可执行文件中的内容。最后,程序计数器(PC)被设置为新程序的入口点(通常为_start的地点)。
在CPU开始访问被映射的虚拟页时,内核会从磁盘中将需要的数据加载到内存中。如许,通过execve函数加载新程序后,当进步程就会执行新程序的代码,从而实现了程序的更换和执行。
6.5 Hello的进程执行
6.5.1进程相关概念先容:
1)用户模式和内核模式:
处置惩罚器利用控制寄存器的模式位来实现限制应用程序指令数和地点空间范围的功能。该寄存器描述了当进步程所拥有的权限。当模式位被设置时,进程以内核模式运行,可以执行指令集中的任何指令并访问系统内存的任意位置。相反,当模式位未被设置时,进程以用户模式运行,禁止执行特权指令,如停止处置惩罚器或更改模式位,并且不允许直接引用内核区域的代码片断和数据。此时,用户程序必须通过系统调用接口来间接访问内核代码和数据。
2)控制流:
从上电时间到断点位置计算的PC值序列,程序计数器称为控制流。
3)逻辑控制流:
利用调试器单步执行程序时,您会看到一系列程序计数器 (PC) 值,这些值与程序的可执行对象文件中包含的指令唯一对应,或者包含在运行时动态链接到程序的共享对象中。此 PC 值序列称为逻辑控制流,或简称为逻辑流。也就是说,逻辑控制流是进程中的一系列 PC 值。
4)进程上下文:
上下文是内核重新启动抢占进程所需的状态,它由对象的值构成,比方通用寄存器,浮点寄存器,程序计数器,用户堆栈,状态寄存器,内核堆栈和各种内核数据结构。
6.5.2 程序进程执行:
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,举行上下文切换,转入用户模式,调理其他进程。直到当hello调用sleep函数时,为了最大化利用处置惩罚器资源,sleep函数会向内核发送哀求将hello挂起,并举行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个停止,使得hello进程重新被调理,将其从等待队列中移出,并内核模式转为用户模式。此时 hello 进程就可以继续执行其逻辑控制流。
6.6 hello的异常与信号处置惩罚
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处置惩罚的。
程序运行过程中可以按键盘,如不绝乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等下令,请分别给出各下令及运行结截屏,说明异常与信号的处置惩罚。
1.在程序正常运行时,打印八次提示信息,以输入回车为标记结束程序,并接纳进程。
图 33 程序正常运行
2.乱按和一直按回车,程序可以正常结束
图 34 乱按和回车
3.按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并接纳hello进程。
图 35 Ctrl +C结束进程
4.按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell表现屏幕提示信息并挂起hello进程。
图 36 Ctrl+Z挂起
5.ps查看进程,发现hello进程还在继续
图 37 ps查看进程
6.fg继续hello进程
发现Shell首先打印hello的下令行下令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程接纳。
图 38 继续hello进程
7.kill杀死进程
图 39 杀死进程
8.jobs查看作业
图 40 jobs结果
9.pstree查看进程树
图 41 进程树
6.7本章小结
本章先容进程的概念和作用,详细说明了Shell-bash的作用和执行流程。通过对Shell-bash下hello程序的执行举行研究,我们深入探究了fork函数创建子进程的过程、execve函数的执行过程,以及各种异常和信号处置惩罚的结果。
第7章 hello的存储管理
7.1 hello的存储器地点空间
逻辑地点:逻辑地点指由程序产生的段内偏移地点。逻辑地点与虚拟地点二者之间没有明确的界限。在hello中,逻辑地点为hello.asm中的相对偏移地点。
线性地点:指虚拟地点到物理地点变更的中间层,是处置惩罚器可寻址的内存空间(称为线性地点空间)中的地点。逻辑地点,或者说段中的偏移地点,加上相应段基址就成了一个线性地点。在hello中,线性地点标记着hello应在内存上哪些具体数据块上运行。
虚拟地点:是由程序产生的由段选择符和段内偏移地点构成的地点。这两部分构成的地点并不能直接访问物理内存,而是要通太过段地点的变化处置惩罚后才会对应到相应的物理内存地点。
物理地点:指内存中物理单位的集合,地点转换的终极地点,进程在运行时执行指令和访问数据最后都要通过物理地点来存取主存。
7.2 Intel逻辑地点到线性地点的变更-段式管理
通过段式管理方式,Intel处置惩罚器实现了从逻辑地点到线性地点的变更。每个程序在系统中都有一个段表,用于保存各个段在主存中的状态信息。这些信息包括段号或段名、段的起始地点、装入位、段的长度、主存占用区域表和主存可用区域表等,以便于举行段式管理。
在Intel处置惩罚器中,段选择符存储在段寄存器中。通过段选择符,可以获取对应段的首地点。
段选择符的结构如下:
图 42 段选择符结构
其包含三部分:索引,TI,RPL
索引:用来确定当前利用的段描述符在描述符表中的位置;
TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地点,虚拟地点。
7.3 Hello的线性地点到物理地点的变更-页式管理
线性地点(VA)到物理地点(PA)的转换是通太过页机制对虚拟地点内存空间举行分页来实现的。虚拟地点(VA)由虚拟页号(VPN)和虚拟页偏移量(VPO)两部分构成,其位数取决于计算机系统的特性。由于虚拟内存与物理内存的页大小相同,VPO与物理页偏移量(PPO)是同等的。
物理页号(PPN)需要通过访问页表中的页表条目(PTE)来获取。当PTE的有用位为1时,表示发生了页命中,可以直接获取到PPN,PPN和PPO共同构成了物理地点。然而,如果PTE的有用位为0,意味着对应的虚拟页没有缓存在物理内存中,这时就会发生缺页故障。
在发生缺页故障时,系统会调用操作系统内核的缺页处置惩罚程序。该程序会确定要断送的页,并将新的页面调入内存。然后返回到原来的进程,再次执行导致缺页的指令。此时会发生页命中,获取到PPN,PPN和PPO一起构成了物理地点。通过这种方式,线性地点终极被转换成物理地点,从而完成内存的访问。
图 43 页式管理
7.4 TLB与四级页表支持下的VA到PA的变更
7.4.1相关概念先容:
TLB的概念:
我们留意到,每次在举行虚拟地点翻译的过程中都会有访问PTE的操作,如果在比力极端的情况下,就会存在访存的操作,如许的服从是很低的。TLB的运用,就可以将PTE上的数据缓存在L1中,也就是TLB如许一个专用的部件,他会将差别组中的PTE缓存在差别的位置,提高地点翻译的服从。
多级页表的概念:
一级页表有一个弊端,就是对于每一个程序,内核都会给他分配一个固定大小的页表,如许有一些比力小的程序会用不到开出的页表的一些部分,就造成了空间的浪费,多级页表就很好的解决了这个问题。以二级页表为例,首先我们先开一个比力小的一级页表,我们将完备的页表分组,分别对应到开出来的一节页表的一个PTE中,在执行程序的过程中,如果我们用到了一个特定的页表,那么我们就在一级页表后面动态的开出来,如果没用到就不开,如许就大大的节省了空间。
7.4.2转换过程:
如图44,当CPU生成虚拟地点VA后,该地点被传送给内存管理单位(MMU)。MMU利用VA的前36位虚拟页号(VPN)作为索引,在转换后的转换后备缓冲器(TLB)中举行匹配。TLB是一个高速缓存,用于存储最近访问的虚拟页和物理页的映射关系。如果TLB中存在匹配的条目,MMU可以直接从TLB中获取物理页号(PPN)(40位)和虚拟页偏移量(VPO)(12位),将它们组合成物理地点(PA)(52位)。
如果TLB中没有匹配的条目,MMU将根据页表举行查询。首先,通过CR3确定第一级页表的起始地点。然后,利用VPN的第一级索引(9位)在第一级页表中查找对应的页表目(PTE)。如果PTE表示的页表条目在物理内存中存在且访问权限符合要求,MMU确定第二级页表的起始地点,以此类推。终极,在第四级页表中找到匹配的PTE,得到物理页号(PPN)和虚拟页偏移量(VPO),将它们组合成物理地点(PA)。
在举行页表查询时,如果发现PTE在物理内存中不存在或访问权限不符合要求,将引发缺页异常,操作系统需要介入来处置惩罚该异常。同时,如果在页表查询的过程中,找到了正确的PTE并生成了PA,MMU将在TLB中添加一个新的条目,以便未来对相同虚拟页的访问可以直接命中TLB,提高访问服从。
7.5 三级Cache支持下的物理内存访问
首先,利用组索引CI来定位缓存中的组。每个组有8路,每一路对应一个块。接下来,根据CT(前40位)来逐路匹配块,如果匹配成功且块的valid标记位为1,则命中(hit)。此时,根据数据偏移量CO(后6位)可以直接获取所需的数据并返回。
然而,如果匹配失败或者匹配成功但块的标记位为0,则发生不命中(miss)。在这种情况下,需要向下一级缓存举行数据查询(L2缓存->L3缓存->主存)。一旦查询到数据,需要确定数据的放置位置。
一种简单的放置策略是,如果映射到的组内有空闲块,则将数据直接放置在该空闲块中。然而,如果组内的全部块都是有用块,就会发生冲突(evict)。为相识决冲突,接纳最近最少利用策略(LRU)举行更换。LRU策略选择最近最少被利用的块举行更换,以便为新的数据腾出空间。
通过如许的组织和更换策略,可以有用地管理缓存中的数据,提高数据的访问服从和命中率。
7.6 hello进程fork时的内存映射
在当进步程"hello"调用fork函数时,内核会为新进程"hello"创建各种数据结构,并分配给它一个唯一的PID。为了为新的"hello"进程创建虚拟内存,内核会创建当进步程的mm_struct(内存描述符)、区域结构和页表的副本。
在创建完副本后,内核将两个进程中的每个页面标记为只读,并将两个进程中的每个区域结构标记为私有的写时复制。这意味着当fork函数在新进程中返回时,新进程的虚拟内存与调用fork时的进程的虚拟内存完全相同。
当这两个进程中的任何一个进程举行写操作时,写时复制机制就会被触发,它会创建新的页面。因此,每个进程都保持了私有地点空间的抽象概念。如许做的好处是,进程之间可以独立地举行写操作,而不会相互干扰,从而确保了进程之间的隔离性和安全性。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有用的替代了当前程序。加载并运行hello需要以下几个步骤:
删除已存在的用户区域。删除shell虚拟地点的用户部分中的已存在的区域结构。
映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。全部这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是哀求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是哀求二进制零的,初始长度为零。
映射共享区域。如果hello程序与共享对象(或目标)链接,好比标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地点空间中的共享区域内。
设置程序计数器(PC) 。execve 做的最后一件事变就是设置当进步程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页停止处置惩罚
缺页征象的发生是由于页表只充当磁盘的缓存,无法保存磁盘中的全部信息,因此某些信息的查询大概会失败,即发生缺页。当访问虚拟内存的指令遇到缺页征象时,CPU会触发缺页异常。缺页异常会调用内核中的缺页异常处置惩罚程序,该程序会选择一个断送页,如果其已经被修改,则先将其存回磁盘中。
一旦确定了要存储的页,内核会从磁盘中将需要访问的内存放入先前已利用的页中,并更新页表项(PTE)中的信息。如许,物理地点就成功地缓存在页表中。当异常处置惩罚程序返回时,CPU会重新执行对虚拟内存的访问操作,此时可以正常访问,不会发生缺页征象。
7.9本章小结
本章主要先容了hello 的存储器地点空间、段式管理、hello 的页式管理,VA 到PA的变更、物理内存访问,hello进程fork、execve时的内存映射、缺页故障与缺页停止处置惩罚。
结论
hello程序的一生履历了如下过程:
1.预处置惩罚阶段:
在预处置惩罚阶段,将hello.c文件中全部外部头文件的内容直接插入程序文本中,举行字符串的更换,以便后续处置惩罚。
2.编译阶段:
通过词法分析和语法分析,将正当指令转换为等效的汇编代码。编译器将hello.i翻译为汇编语言文件hello.s。
3.汇编阶段:
将hello.s汇编程序翻译为机器语言指令,并将这些指令打包成可重定位目标程序格式,保存在hello.o目标文件中。
4.链接阶段:
通过链接器,将hello程序的编码与动态链接库等网络整理为单一文件,生成完全链接的可执行目标文件hello。
5.加载和运行:
打开Shell,运行程序,终端会为其fork新的进程,并通过execve将代码和数据加载到虚拟内存空间,程序开始执行。
在进程被调理时,CPU为hello进程分配时间片。在每个时间片内,hello进程可以完全占用CPU资源。程序计数器(PC)渐渐更新,CPU不绝取指令,并按次序执行控制逻辑流程。
内存管理单位(MMU)将逻辑地点渐渐映射为物理地点,并通过三级高速缓存系统访问物理内存/磁盘中的数据。
进程时刻等待信号的到来。如果在运行过程中键入ctrl-c或ctrl-z,Shell会调用相应的信号处置惩罚函数,执行停止或挂起等操作。对于其他信号,也会有相应的处置惩罚方式。
Shell父进程等待并接纳hello子进程,内核删除为hello进程创建的全部数据结构。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |