摘 要
本陈诉以Hello程序为载体,体系剖析了计算机体系中程序从静态代码到动态进程的全生命周期管理机制(P2P),以及进程从创建到终止的资源闭环管理(020)。通过GCC工具链在Ubuntu情况下完成预处理、编译、汇编、链接四阶段实验,天生.i、.s、.o及可实行文件,联合readelf、objdump、edb等工具分析ELF格式、进程内存布局与动态链接行为。陈诉深入探讨了Shell的进程管理逻辑(fork与execve)、CPU调度机制、虚拟内存到物理地址的页式转换,以及缺页停止处理流程。通过信号控制实验,展现了操纵体系对进程异常与资源回收的核心策略。本研究完备呈现了程序运行背后的体系级协作机制,为明白进程调度、内存管理及工具链协同提供了实践范本。
关键词:计算机体系;进程生命周期;ELF格式
第1章 概述
1.1 Hello简介
P2P(Program to Process)形貌了程序向进程转化的完备技能链路。其核心路径可概括为:开辟者编写Hello.c源文件后,经过预处理(宏展开/头文件插入)、编译(天生汇编代码)、汇编(转为呆板码对象文件)、链接(整合库函数天生ELF可实行文件)四阶段编译流程。当用户在Shell终端实行"./hello"时,操纵体系的进程管理机制将被激活:Shell首先通过fork()体系调用创建子进程副本,随后利用execve()加载器将hello可实行映像置换到子进程地址空间,最终完成从静态程序到动态进程的实体化变化。
020(Zero to Zero)则构建了进程生命周期的哲学隐喻。从虚无状态(0)起步,源代码经物化过程成为可实行实体;当进程通过execve()被装载实行时到达存在峰值(2);随着main()函数返回或exit()调用,进程进入终止态(第一个0),此时内核发送SIGCHLD信号通知父进程,由Shell通过waitpid()回收进程形貌符与资源,体系资源状态完成闭环回归初始(第二个0),形成完备的生命周期轮回。该数字序列不但形貌技能过程,更隐喻计算体系资源管理的本质规律。
1.2 情况与工具
1.2.1 硬件情况
操纵体系:Windows11
Ubuntu 20.04.6 LTS
1.2.2 软件情况
VMware workstation;
Visual studio
1.2.3 开辟工具
gcc :用于编译和链接程序
GDB :用于程序调试
EDB调试器:用于分析程序实行过程
objdump:用于反汇编和目的文件分析
readelf:用于分析ELF格式文件
strace:用于跟踪体系调用
1.3 中心结果
(图1.1)全部创建出的文件
预处理阶段创建了hello.i文件:
作用:展示宏展开和头文件包罗后的完备代码
编译阶段创建了hello.s文件:
作用:展示C代码转换后的汇编语言表示
汇编阶段创建了hello.o文件:
作用:包罗呆板码但未完成最终地址重定位
链接阶段创建了hello可实行文件
作用:可直接在Linux体系上运行的程序
利用readelf读取hello.o文件的ELF信息,重定位输出天生helloelf.txt文件
反汇编hello可实行文件,将反汇编结果重定位创建了excutabledump.txt文件
利用readelf读取hello可实行文件的ELF信息,重定位输出到excutable.txt文件
1.4 本章小结
本章体系探讨了从程序到进程(P2P)的技能实现与进程生命周期(020)的完备闭环,联合开辟情况、工具链及中心产物分析, 为后续章节深入分析程序的预处理、编译、汇编、链接、进程管理和存储管理等细节奠定了底子。
第2章 预处理
2.1 预处理的概念与作用
预处理是编译过程的初始阶段,在编译器正式实行词法分析和语法分析之前完成。其主要作用是对源代码进行文本层面的加工处理:首先删除全部单行表明(//)和多行表明(/ ... /),消除对编译无意义的冗余信息;其次剖析以开头的预处理指令,例如将define定义的宏常量或函数进行直接文本替换,将include引用的库文件内容完备复制到当前文件中,同时根据ifdef、ifndef等条件编译指令动态选择保留或排除特定代码块。
预处理完成后会天生扩展名为.i的中心文件,其中包罗原始代码经表明清理、宏展开、头文件融合后的完备文本。这一阶段本质上是通过代码重组与简化,将开辟者编写的“易读型”源代码转化为“编译器友好型”的纯净输入,为后续编译阶段的语法剖析和代码优化扫除干扰,奠定结构化底子。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
(图2.1)预处理阶段实行的指令
2.3 Hello的预处理结果剖析
(图2.2)预处理天生的.i文件的部分信息
(图2.2)预处理天生的.i文件的部分信息
(图2.3)预处理天生的.i文件中的Main函数部分
预处理完成后,天生的.i文件与原始.c文件相比,呈现出显著的结构变革:首先,全部程序开头的表明(包括单行表明和多行表明)均被彻底扫除,确保代码中不再包罗非实行性文本;其次,通过剖析include指令,预处理器会将全部被引用的头文件(如标准库stdio.h或自定义头文件)内容逐字复制到当前文件中,形成头文件代码与原代码的完备合并。因此,最终的.i文件虽然移除相识释,却因插入大量头文件代码而体积膨胀,本质上成为一份“纯净”且可直接编译的完备代码集合。
2.4 本章小结
预处理通过代码净化(删除表明)、符号替换(宏展开)、模块集成(头文件合并),将分散的、多层次的源代码转化为单一、一连且无冗余的编译单元,为词法分析与语法分析阶段扫清告终构性障碍,是程序从“人类可编辑”到“呆板可编译”转化的第一道桥梁。
第3章 编译
3.1 编译的概念与作用
编译是从高级语言代码向底层呆板指令转化的核心过程,其本质是通过编译器对预处理后的代码进行词法分析、语法剖析、语义查抄及代码优化,最终天生与特定硬件架构匹配的汇编代码或直接输出二进制呆板指令。
该过程的关键作用在于实现“一次编写,多平台运行”的跨平台兼容性:高级语言的代码逻辑与硬件细节解耦,开辟者只需维护同一份源代码,而不同操纵体系或硬件架构可通过适配各自的编译器(如x86平台的GCC、ARM平台的交织编译器),将雷同的高级语言代码分别编译为目的平台专用的汇编指令或呆板码,从而屏蔽底层差异,显著提升代码的可移植性和开辟服从。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
(图3.1)编译过程指令
3.3 Hello的编译结果剖析
(3.2).s文件部分内容
(3.3.1)函数操纵
首先,main函数含有参数argc以及argv[],前者是int范例的数据,存储在rdi寄存器中,后者是char范例的数组,存储在rsi寄存器中。此时,创建了main的栈帧并修改了rbp以及rsp的值。此后,进行对函数参数的保存操纵,将rdi压栈到rbp-20的地址去,同时将rsi压栈到rbp-32的地址。
第二处函数操纵为调用printf函数。
(3.3).c文件中有关printf函数的输出模式串
(3.4)printf段在.s文件中被翻译成的汇编指令
(3.5)printf输出模式串在.s文件中的体现
调用printf函数进行以下步骤:向rax寄存器传递.LC0的地址;将rax寄存器的内容复制给rdi寄存器;call调用puts@PLT。图中给出.LC0段内容为 printf要输出的语句格式串。此时可以看到hello:因为是ASCII字符的原因,以是可以直接显示出来,而中文则必须转为编码。
(3.6)exit函数在.s汇编文件中的体现
在输出之后,继续实行exit(1)操纵,将立刻数1移到edi寄存器中作为调用函数的参数,然后用call调用exit@PLT。
(3.7)hello.c中循环输出源代码
(3.8)for循环在.s文件中的体现
第三处函数操纵在.s文件中对应于.L4部分,对应for循环内部printf函数以及sleep函数。首先,将rbp-32的内容存放至rax寄存器,令rax加24,由于argv是一个char范例的字符型指针数组,数组内部每一个元素都占用8个字节,此操纵让rax指向了argv[3]。此处调用printf中参数的设置是4321,即首先把arv[3]传递给printf函数的第四个参数,把arv[2]传递给printf函数的第三个参数,将argv[1]传递给第二个参数,末了将printf的模式串传递给printf的第一个字符串。
对于sleep函数的调用流程为:将argv[4]传递给rdi寄存器并call调用atoi@pLT函数,并将函数的返回值作为sleep函数的参数调用sleep函数,在完成这一切后进行循环计数:rbp-4地址的值加一。
(3.3.2)关系操纵
(3.9)if循环在本来.c文件中的内容
源文件中应有四个输入的参数,在汇编语言中体现为cmpl操纵,将立刻数5与argc比较,假如相当就跳转到L2.代码段,假如不等就继续下面的操纵。
(3.10)if条件判断在汇编文件中的体现
(3.11)for循环在.c文件中的内容
(3.12)for循环在汇编文件中的循环条件判断片断
第二处关系操纵为for循环的输出,将用户输入的第一个以及第二个命令行参数循环打印10次。跟踪这一部分的操纵,程序将立刻数9与rbp-4地址处的值比较(初值为0),假如为小于等于关系则跳转到.L4部分,实行for语句内部的内容。从0到9循环进行10次,满足本来.s文件的循环要求。
(3.3.3)控制转移
if条件判断,以argc不等于5为判断条件,在.s文件翻译为如下语句:
(3.13)if条件判断在汇编文件中的全内容
通过条件判断,依赖je语句跳转到L2语句段,完成控制的转移改变程序计数器的值。此处L2语句段对应的是源程序中if没有实行时的情况。
(3.3.4)数组、指针操纵
.s文件中对指针的调用先将地址存放到rax寄存器,再调用这个地址的,详细方式如下:
(3.14).s文件中的取址操纵
对于数组的操纵(argv数组),首先把数组的第一个元素的地址取出,然后直接对这个地址加上一个数字,用来搜刮数组的不同位置的元素。本程序中argv是字符指针数组,是8字节的元素,因此搜刮索引时以8为倍数加减的。
(3.3.5)数据
字符串:这里主要指printf调用时利用的模式串,主要被存放在.rodata段中,在main函数书写之前就出现在.s文件中,对这些字符串的调用也是直接将字符串对应段落的地址传递到寄存器中利用。
循环计数器i,在栈中(rbp - 4)开辟了一个4字节空间来存放程序中的循环计数器,并且在每次循环结束时增加这个参数的值。
命令行输入argc与argv。argc是一个int范例的参数,在.s文件中体现出,main函数与一样平常的函数雷同,也是将这两个作为平凡参数存放在rdi与rsi中。由于这两个参数要经常利用,以是一开始就把这两个参数存放到栈中。
3.4 本章小结
编译将高级语言翻译为汇编语言程序,这使得高级语言文件具有可跨平台的属性,不同的体系可以摆设不同的编译器,将同样的高级语言文件编译成不同的顺应相应体系的汇编语言文件。
汇编代码中有许多伪指令(汇编器指令),这时汇编语言中利用的操纵符和助记符,还包括一些宏指令。这些指令告诉汇编程序如何进行汇编,既不控制呆板的操纵也不被汇编成呆板代码,只能为汇编程序所辨认并引导汇编如何进行。[1]
第4章 汇编
4.1 汇编的概念与作用
汇编是编译流程中的关键阶段,负责将汇编语言文件(.s)转换为呆板语言目的文件(.o)。其本质是通过汇编器(如as)将人类可读的助记符指令(如mov, add)逐条翻译为二进制形式的呆板码,天生包罗代码段(.text)、数据段(.data)等结构的可重定位目的文件。
天生的.o文件虽包罗呆板指令,但因未完成链接过程,存在两大关键缺陷:其一,外部符号(如库函数或跨文件变量)尚未剖析,仅以占位符形式存在;其二,代码与数据的虚拟地址未完成重定位,仍基于相对偏移量。因此,目的文件无法直接载入内存独立运行,必须通过链接器整合多模块并绑定现实地址后,才能形成完备的可实行程序。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
(4.1)汇编指令
4.3 可重定位目的elf格式
(4.2)利用readelf的代码
实行命令如上,将readelf获得的全部信息输出到helloelf文件中
(4.3)readelf的结果:ELF头
根据CSAPP,ELF头应该有一个16字节的开头用来指示程序的字节信息(大端还是小段),可以瞥见这里Magic恰有16个字节。ELF头中标识了一些体系信息,例如是UNIX操纵体系的虚拟机,给出了体系架构。并且给出处理的hello.o是一个可重定位文件,给出了入口点地址、程序头起点等各种信息,还指出了这个ELF头的大小是64个字节,没有程序头表,统共14个节头,节头表索引在第十三位。
(4.4)readelf结果:节信息表
恰如ELF头所述,共计14个节,节头部表的索引是13。
4.6)反汇编文件中的重定位条目信息
重定位节.rela.text里存储重定位符号的信息,与反汇编文件联合可以发现这里的信息正是.text段中各种等候被重定位的内容,.rodata-4,.puts-4,.exit-4。出现的顺序正如反汇编程序中重定位条目的出现顺序。
(4.7)readelf结果:.symtab
.symtab节中给出了一些符号的信息,指明白它们是函数抑或是其他的什么内容,并且给出他们的作用域。
4.4 Hello.o的结果剖析
(4.8)反汇编结果
将hello.o反汇编的结果与hello.s比较:
①许多.s文件中的汇编器指令被删除了。
②.o文件为各个段分配了虚拟的地址
③文件被翻译为了二进制(本来的hello.o文件完全是二进制文件,经objdump处理后得到反汇编后的汇编语言代码)
④以是.s文件中的.L2之类的标识不再存在, call函数调用的地址也不再是exit@PLT这样的标识,变为现实的地址,如.L2那边的代码在反汇编文件中具有地址0x32。
⑤指令的翻译结果稍有不同,.s文件中指令都有操纵的位数,例如subq到反汇编中就直接是sub
4.5 本章小结
汇编是编译流程中将汇编语言文件(.s)转换为呆板语言目的文件(.o)的关键阶段。其核心是通过汇编器(如as)将助记符指令(如mov, call)逐条翻译为二进制呆板码,天生包罗代码段、数据段等结构的可重定位目的文件。这一过程本质上实现了从“人类可读的符号化指令”到“处理器可直接解码的二进制指令集”的转化。
呆板语言与汇编语言的本质差异体现在指令表示和操纵数处理上:
1. 呆板语言由二进制字符串构成,严酷遵循处理器指令集编码规则。例如,常数在反汇编结果中统一以十六进制形式呈现(如0x1F),但其底层存储均为二进制位流(如00011111)。
2. 汇编语言利用助记符和符号化规则加强可读性:
指令表示:函数跳转通过助记符(如call <func_name>)标记目的位置,而呆板语言则需硬编码目的地址(如E8 00 00 00 00)。
操纵数格式:汇编语言保留源代码中的数值表示方式(十进制10或十六进制0xA),但汇编器会将其统一转换为二进制形式嵌入呆板指令。例如,汇编指令mov eax, 10会被翻译为二进制码B8 0A 00 00 00,其中0A即十进制10的十六进制编码。
天生的.o文件虽包罗呆板码,但因未完成链接(符号剖析与地址重定位),尚不具备独立实行能力。
第5章 链接
5.1 链接的概念与作用
链接是程序构建的关键阶段,负责将多个目的文件(.o)和静态库(.a)中的代码与数据整合为单一可实行文件。这一过程通过剖析模块间的符号引用关系,并重新计算内存地址,将分散的代码片断组合成完备的、可被操纵体系加载运行的程序实体。
静态链接的核心作用在于办理多模块协同问题:首先通过符号剖析匹配函数和变量的定义与引用(例如将main中调用的printf绑定到库函数实现),消除未定义符号;其次通过重定位将全部代码段和数据段按虚拟内存布局重新分配绝对地址(例如修正函数跳转指令中的偏移量为现实内存地址),最终天生包罗完备指令、全局数据及内存映射信息的可实行文件(如ELF格式)。链接后的文件可直接由操纵体系加载到内存,形成进程映像并实行,实现从静态代码到动态运行的转化。
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的格式
(5.2)readelf查察指令
利用readelf剖析hello的ELF格式,将输出信息重定位到excutable.txt文件中。
(5.3)readelf结果:ELF头
hello的ELF格式与hello.o的ELF 的magic雷同,关于操纵体系、版本之类的信息雷同。程序入口点地址以及程序头起点在可实行程序经过链接之后被重定位,函数被映射到了虚拟空间地址,入口点地址不再是0,并且有了程序头。节头起点相较于hello.o而言增大了许多。在标志中,新增了程序头,并且节的数量增加了。
5.4 hello的虚拟地址空间
(5.4)edb调试界面
进入程序,实行完init加载之后,程序进入main函数,地址为4010f0,正为ELF中指出的.text的地址。
(5.5)虚拟内存信息
.text段在401ff0结束,接下来为 402000的.rodata段,而.text段也恰如ELF中指出的那样占用1ff0字节的单元。
(5.6).text段信息
在402000存放的就是.rodata的内容,内有Hello的printf字符串信息。
5.5 链接的重定位过程分析
(5.7)反汇编过程指令
(5.8)反汇编结果.plt.sec段
反汇编hello可实行程序,在.plt.sec段中,调用的函数被存放,例如printf,puts,getchar。
(5.9)反汇编结果:main函数部分
main函数的起始地址是401125而不再是0,说明main函数被重定位了。.o文件中的重定位条目被处理, 40113e地址的lea此时已经有明白的地址,取到的地址在.rodata段中。观察call函数调用, call指令编码为e8 46 ff ff ff,为PC寻址方式,而在.o文件的反汇编中它的重定位条目也确实是记录为PC寻址方式。
5.6 hello的实行流程
利用edb调试,实行了一系列操纵之后,jmp到r12,即 _start函数的地址。
(5.10)edb调试界面
继续实行操纵,改变了寄存器的值,依赖call调用了Main函数,地址为401125。
(5.11)edb调试界面(寄存器内容)
(5.12)edb调试界面(栈信息)
进入main函数后,观察寄存器以及stack,发现此时rdi和rsi存放argc的数量5以及各种字符串的首地址。
(5.13)edb调试界面(虚拟内存信息)
在输入的参数数量恰好为4个时,正常运行循环的过程
(5.14)edb调试界面(程序输出结果)
第一个调用Printf函数,地址位于4010a0。
(5.15)edb调试界面
程序实行完循环体之后,调用存放于4010b0地址的getchar函数。输入一个数字结果程序,地址又变回7f开头的地址了。
5.7 Hello的动态链接分析
动态链接的核心在于将程序分解为多个独立模块(如主程序与共享库),在程序启动或运行时(而非编译时)完成模块间的链接操纵。其核心机制办理了共享库加载地址不确定性问题:由于共享库(如.so文件)在运行时可能被加载到内存任意位置,编译器无法预先确定其函数的内存地址。为此,动态链接接纳耽误绑定策略——在编译阶段仅为外部函数引用天生重定位标记(如call printf@PLT),而非直接写入绝对地址。
当程序加载时,动态链接器(如ld-linux.so)首先根据共享库的依赖关系将其载入内存,随后遍历重定位表,将函数调用地址修正为现实加载的库函数入口地址(例如通过全局偏移表GOT与过程链接表PLT协作实现)。这种运行时地址剖析机制不但支持库的灵活加载,还答应多进程共享同一库的物理内存副本,显著减少资源占用,同时实现库的独立更新(替换.so文件无需重新编译主程序)。
根据hello.elf文件可知,GOT起始表位置为:0x404000:
(5.16)GOT起始表位置
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
(5.17)Data Dump中的GOT表(其一)
调用了dl_init之后字节改变:
(5.18)Data Dump中的GOT表(其二)
在动态链接机制中,变量和库函数的地址剖析接纳不同的策略:
变量的地址绑定依赖于模块内部段结构的稳定性。编译器在天生目的文件时,会基于代码段(.text)与数据段(.data)的相对偏移量计算变量地址。由于这些段在链接后的相对位置保持不变,即使模块被加载到内存的任意位置,变量地址仍可通过“基址(段加载地址)+ 固定偏移量”准确计算,无需运行时重定位。
库函数的动态剖析则通过过程链接表(PLT)与全局偏移量表(GOT)的协作实现:
每个外部函数(如printf)在PLT中对应一个条目,初始代码逻辑为:跳转至GOT中该函数对应的表项地址(首次调用时,GOT表项默认指向PLT条目中的第二条指令)。调用动态链接器(如ld-linux),由其剖析函数现实地址并回填到GOT中。 动态链接器首次剖析函数后,会将真实函数入口地址写入GOT对应表项。后续再次调用该PLT条目时,跳转指令将直接通过GOT跳转到目的函数,绕过链接器,实现高效绑定(称为“耽误绑定”)。
5.8 本章小结
本章以hello程序为实践载体,体系剖析了链接的核心机制与应用方法。首先通过命令行工具(如ld)实行静态链接操纵,将目的文件与库文件整合天生ELF格式的可实行文件;随后借助readelf等工具分析ELF文件头、节头表、符号表等结构,明白代码段、数据段的布局与入口地址信息。同时,利用调试器edb加载hello程序,直观观测其被加载到虚拟地址空间后的内存映射状态,验证链接阶段预设的内存布局逻辑。 联合进程运行时的内存快照,动态剖析共享库加载地址修正、符号剖析的现实过程,完备呈现从静态ELF文件到内存可实行映像的动态链接生命周期。
第6章 hello进程管理
6.1 进程的概念与作用
进程是操纵体系进行资源管理与任务调度的核心实体,代表一个正在实行的程序实例。其本质是程序在内存中的动态实行状态,包罗代码段、数据段、堆栈、寄存器值以及打开的文件形貌符等运行时上下文。操纵体系通过进程抽象为每个程序提供两大虚拟化核心机制:一是独占逻辑控制流,使进程在时间片轮转中“独占”CPU实行权(通过上下文切换实现多任务并行假象);二是独立虚拟地址空间,通过页表映射将进程的线性虚拟地址转化为物理内存地址,实现进程间内存隔离。
进程的虚拟地址空间布局由操纵体系统一规范。这种计划既简化了程序开辟(开辟者无需关注物理地址变革),又通过内存管理单元(MMU)的地址转换机制,保障了不同进程的雷同虚拟地址指向独立的物理内存区域,彻底隔离进程间的内存访问冲突,确保体系稳定性和安全性。
6.2 简述壳Shell-bash的作用与处理流程
Bash作为Shell的一种实现,其核心功能是充当用户与操纵体系之间的交互接口。它的工作流程围绕一连读取并剖析用户输入的命令展开:当用户在终端输入指令后,Bash首先判断该指令范例——若为内置命令(如fg控制进程前台规复、bg转为后台运行、exit退出Shell等),则直接调用Shell内部预置的处理函数实行;若为外部程序(例如用户编写的hello可实行文件),则通过创建子进程的方式启动目的程序。
对于外部程序的实行,Bash默认接纳前台运行模式:此时Shell会暂停自身的指令读取循环,等候该程序完全实行完毕并回收其资源后,才重新规复终端提示符,继续接收下一条命令输入。若用户显式指定程序在后台运行(例如输入./hello &),Bash则不会阻塞等候,而是立刻返回交互状态,答应用户并发实行其他操纵。这一机制既保障了任务实行的灵活性,又通过进程管理实现了多任务的调和控制。
6.3 Hello的fork进程创建过程
Bash进程依赖fork创建一个子进程,与bash在同一个进程组里,fork得到与bash雷同的上下文。
子进程与父进程最大的区别就是具有不同的PID。在父进程中,fork返回子进程的PID,而在子进程中fork返回0,返回值提供一个明白的方法来分辨程序是父进程还是在子进程中实行。
6.4 Hello的execve过程
当Bash通过fork创建子进程后,立刻在该子进程中调用execve函数。此函数会彻底替换进程的原有上下文:将子进程的代码段、数据段、堆栈等全部替换为目的程序(如hello)的对应内容,同时继续原进程的PID(进程ID)与部分属性(如文件形貌符)。由于execve实行后直接加载新程序并跳转至其入口点,原进程的全部逻辑被完全覆盖,函数调用永不返回,子进程由此蜕变为一个全新的、独立运行的hello程序实例。
6.5 Hello的进程实行
进程通过独立逻辑控制流与私有虚拟地址空间两大抽象,为每个运行中的程序(如hello)制造出“独占”CPU与内存的假象。当Bash通过fork创建子进程并调用execve加载hello后,该子进程即成为独立的实行实体:其虚拟内存空间完全隔离于其他进程,且内核通过时间片轮转调度机制,让hello在逻辑上“独占”CPU。
当hello自动调用sleep()或时间片耗尽时,内核触发上下文切换:首先保存hello的寄存器状态、程序计数器等上下文至其进程控制块(PCB),随后从就绪队列中选择另一进程(如原Bash或其他任务),将其PCB中保存的上下文载入CPU并规复实行。若此时发生异常(如缺页、体系调用),CPU会从用户模式切换至内核模式,由内核处理异常后再返回用户模式继续实行原进程或调度新进程。这一机制在保障进程资源独占性的同时,实现了多任务的高效并发与体系资源的动态分配。。
6.6 hello的异常与信号处理
Hello在实行的过程四种异常都有可能出现。由于hello中没有fork任何子进程,以是不会产生任何SIGCHLD信号,但是仍有可能会产生别的信号,体系会依照接受到信号时默认的行为处理这些信号。
(6.1)在实行程序时输入ctrl + c
在运行的时候输入ctrl+c,程序接收到SIGINT信号立刻就停止了, Shell结束并回收hello进程。
(6.2)在实行程序时胡乱输入
在程序循环输出的过程中胡乱输入,结果发现对程序的实行没有任何影响,输入的全部内容都被缓存了起来,包括换行符,在程序结束之后一条一条的运行。
(6.3)在实行程序时输入ctrl + z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
(6.3)在输入ctrl + z后输入ps和jobs指令
由ps和jobs命令查察对hello进程的挂起,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
(6.4)在实行程序时开启另一个终端实行ps
尝试开两个终端,在正常源程序运行时查察那个程序的信息,结果发现连pid都查询不到。
(6.6)ctrl+z后实行pstree
在Shell中输入pstree命令,可以将全部进程以树状图显示
(6.6)ctrl+z后实行kill
输入kill命令,则可以杀死指定(进程组的)进程:
输入kill -CONT PID(这里的PID值要根据需要再次启动的进程的PID值调整,图中例子为8262,以是输入的指令为kill -CONT 8262)则命令将hello进程再次调到前台实行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍旧可以正常结束,并完成进程回收。
6.7本章小结
本章围绕计算机体系中进程与Shell的核心机制展开,以hello程序为实例串联核心知识点。说明Shell(如Bash)作为用户与内核的“翻译官”,如何剖析命令、管理前后台任务——好比输入./hello时,Shell像调度员一样启动流程,先fork克隆自身,再通过execve让子进程“洗手不干”酿成hello程序。
详细到hello的生命周期,本章拆解了从进程创建到灭亡的全链路:fork复制出的子进程如何继续Shell的“基因”却拥有独立PID,execve彻底替换代码与数据;实行时CPU时间片轮转让hello看似独占资源,而遇到sleep或停止时,内核像交通指挥员一样切换进程,保存现场再规复其他任务。末了,还探讨了程序运行中可能遇到的异常(如段错误)和输入输出的处理逻辑,展现Shell如何调和程序与体系的互动,确保整个过程稳定高效。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址是程序(如hello)天生的地址,由段标识符和段内偏移量构成。例如程序中的函数调用或变量访问指令中的地址,需通过分段机制(段基址+偏移量)转换为线性地址,属于CPU指令直接操纵的地址形式。
线性地址是逻辑地址经分段部件处理后的中心形态。例如在x86架构中,段寄存器存储的基址加上指令中的偏移量天生线性地址,该地址若未启用分页机制则直接作为物理地址利用,否则需进一步转换。
虚拟地址是程序视角中的内存地址,与物理内存解耦。例如hello程序中的指针变量值即为虚拟地址,操纵体系通过页表将其转换为物理地址,使得程序可访问宏大于物理内存的地址空间(如64位体系的48位虚拟地址)。
物理地址是内存芯片上的现实寻址单元,每个字节对应唯一物理地址。例如当hello进程访问某个虚拟地址时,MMU通过页表查询将其转换为物理地址,最终由内存控制器根据此地址定位到详细的DRAM存储单元完成数据读写。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(7.1)段-偏移地址的结构
Intel处理器的段式管理机制通过分别程序为多个逻辑段(如代码段、数据段)实现内存隔离与访问控制。每个逻辑地址由段选择符(16位)和段内偏移量(32/64位)组成:段选择符的前13位作为索引,从全局形貌符表(GDT)或局部形貌符表(LDT)中定位对应的段形貌符,该形貌符存储段的基地址、长度限制、权限属性等关键信息。体系通过唯一的GDT管理内核段及各任务的LDT指针,而每个任务独立的LDT则形貌其私有段(如用户代码段)和门结构。地址转换时,硬件将段形貌符中的基地址与偏移量相加,天生线性地址,完成逻辑地址到线性地址的映射,为后续分页机制或直接物理寻址提供底子。
7.3 Hello的线性地址到物理地址的变换-页式管理
(7.2)虚拟地址的寻址方式
虚拟地址到物理地址的转换由CPU的内存管理单元(MMU)通过页式管理实现。虚拟内存被组织为磁盘上的一连字节数组,每个字节通过唯一的虚拟地址索引。磁盘数据按固定大小的页(如4KB)分别,作为与物理内存间的传输单元。物理内存同样分别为雷同大小的物理页(页框),虚拟页通过页表映射到物理页。当程序(如hello)访问虚拟地址时,MMU将其拆分为虚拟页号和页内偏移量,通过页表查询虚拟页对应的物理页框号,最终将物理页框号与偏移量拼接形成物理地址。若该虚拟页未缓存在物理内存(页表标记无效),则触发缺页异常,由操纵体系将磁盘中的目的页加载到物理页框并更新页表,完成地址映射后继续实行。
(7.3)页的结构
页表将虚拟页映射到物理页。每次地址翻译软件将一个虚拟地址转换为物理地址时,都会读取页表。操纵体系负责维护页表的内容,以及在磁盘与物理内存之间来回传送页。
7.4 TLB与四级页表支持下的VA到PA的变换
(7.4)Intel Core i7 处理器内存体系
该处理器接纳的就是TLB与四级页表支持的翻译方案。虚拟地址(VA)48位,物理地址(PA)52位,虚拟地址的0~11位是VPO(虚拟页偏移量),12~47位是VPN(虚拟页号)。利用VPN到页表中查找对应的页,页表被分为四级,每级对应12~47位中的9位,在一级页表中查找与VPN的27~35位对应的二级页表,再在这个二级页表中查找VPN的18~26位对应的三级页表,以此类推,末了在四级页表中找到目的页表条目,这个页表条目中存储的就是PPN(物理页号),VPO与PPO(物理页偏移量)相当,直接将这两部分合起来就得到了物理地址(PA),即PA=PPN+PPO。
7.5 三级Cache支持下的物理内存访问
(7.5)Intel Core i7 总的地址翻译及访存流程
在三级Cache(L1/L2/L3)架构下,CPU访问物理内存的流程如下:当CPU需要读取数据时,首先查询速率最快的L1 Cache(一级缓存,通常集成在CPU核内),若数据存在(命中),则直接返回;若未命中,则逐级向下查询容量更大但速率较慢的L2 Cache(二级缓存,核内或核间共享)和L3 Cache(三级缓存,多核共享)。若三级Cache均未命中,则需访问主内存(DRAM)读取数据,同时将数据按缓存行(如64字节)逐级回填至L3→L2→L1,供后续快速访问。此层级计划以空间换时间,利用局部性原理显著减少内存访问耽误。
7.6 hello进程fork时的内存映射
当 hello 程序实行时,shell 调用 fork 来创建一个新的进程。在 fork 之后,内核会为新进程分配必要的数据结构并为它分配一个唯一的 PID(进程ID)。为了给新进程分配虚拟内存,内核会创建一个父进程(即当前进程)的 mm struct、内存区域结构和页表的副本。
在此过程中,内核将父进程和子进程的内存页面都标记为只读,并将内存区域结构标记为私有的写时复制(COW)。这意味着,在 fork 之后,父进程和子进程共享雷同的物理内存页面,但这块内存是只读的,并且只有在其中一个进程尝试修改内存时才会触发复制。
详细地,当子进程修改某个虚拟内存页时,内核会将该页从共享的物理内存中复制出来,并将其映射到一个新的物理内存页上。这样,子进程和父进程的内存就不再共享,确保了进程间的内存隔离,同时通过写时复制机制提升了创建新进程的服从
7.7 hello进程execve时的内存映射
当 execve 函数加载并运行 hello 程序时,实行过程涉及以下几个内存映射的步骤:
- 删除已存在的用户区域
在调用 execve 时,当前进程的用户空间会被扫除,包括已存在的虚拟地址空间区域结构。这是为了消除进程之前运行程序的痕迹,为新程序的加载做准备。
- 映射私有区域
接下来,execve 会为新程序创建新的虚拟内存区域结构,映射出程序的不同部分:代码区(.text) 和 数据区(.data)会被映射到 hello 文件中的 .text 和 .data 区域,它们是私有且写时复制的。bss区是哀求二进制零的区域,会映射到一个匿名文件,并且大小由 hello 程序中的 bss 段确定。栈和堆这两个区域也被哀求为二进制零,其初始长度为 0。
- 映射共享区域
假如 hello 程序与共享库(如标准 C 库 libc.so)链接,execve 会动态加载这些共享对象,并将它们映射到用户虚拟地址空间中的共享区域。这些共享区域会被映射为只读,供多个进程共享。
- 设置程序计数器(PC)
末了,execve 会设置进程的程序计数器(PC),使其指向 hello 程序的代码区域的入口点。这意味着,下一次调度时,进程将从该入口点开始实行。
通过这些步骤,execve 函数乐成地替换当前进程的地址空间,加载并运行新的 hello 程序。
(7.6)execve设置程序计数器示意图
7.8 缺页故障与缺页停止处理
当程序访问的虚拟地址对应的物理页未加载到内存时,CPU触发缺页停止,操纵体系内核启动处理流程:首先查抄虚拟地址是否属于进程正当地址空间(如代码段、堆、栈等),若越界访问则触发段错误(SIGSEGV)终止进程;其次验证操纵权限是否匹配内存区域属性(如尝试写入只读页),权限不符同样终止进程。若校验通过,内核从磁盘加载目的页到物理内存:若内存已满,根据置换算法(如LRU)选择一个牺牲页,若该页被修改则先写回磁盘;新页加载后更新页表项并建立映射,末了CPU重新实行触发缺页的指令,此时物理页已就绪,程序继续运行。
(7.7)缺页故障示意图
缺页处理的核心机制在于按需加载和透明管理:操纵体系仅在页面被现实访问时从磁盘加载,减少内存占用;进程无需感知物理内存分配或磁盘I/O,由内核与MMU协作完成地址转换。通过脏页回写、置换算法优化及预读策略(如访问局部性预测),体系在保障进程隔离性与稳定性的同时,最大限度降低缺页频率,提升团体实行服从。
7.9本章小结
本章主要讨论了“hello”程序的存储器地址空间管理,包括Intel的段式管理和页式管理。通过以Intel Core i7为例,详细先容了虚拟地址(VA)到物理地址(PA)的转换过程,以及物理内存访问。章节还分析了在不同操纵过程中,hello进程的内存映射情况,例如进程fork时的内存映射变革,以及execve时的内存映射情况。别的,还探讨了缺页故障和缺页停止的处理机制,并扼要先容了动态存储分配管理的相干内容。
结论
Hello的一生:从代码到进程的旅程
Hello的生命始于程序员的指尖,承载着简单的任务:向世界问好。
- 预处理使Hello的逻辑结构渐渐清晰,天生.i文件;
- 编译通过编译器将C代码翻译为汇编语言(.s),赋予Hello与硬件对话的能力 。
- 汇编器将助记符(mov, jmp)转化为呆板码(.o),Hello蜕变为由0和1构成的机械生命。
- 链接:链接器将分散的.o文件与库函数焊接,剖析printf的真实地址,完成重定位。 Hello披上可实行文件的外衣,拥有虚拟内存入口(_start),终获独立行走于世间的资格。
- Fork: 当用户在终端输入./hello时,Bash以fork()分裂自身,诞生一个与父进程孪生的子进程。
- Execve:子进程调用execve(),清空原有的代码与数据,将Hello的ELF文件加载至虚拟地址空间。 main()成为新的心脏,命令行参数(argv)与情况变量(envp)如血液般流入栈中,Hello今后不再是Bash的影子,而是独立的魂魄。
- MMU为Hello构筑私有的虚拟王国,Hello在虚拟内存的世界配备代码数据。在Unix IO体系帮助下,展示自己风采
- 当main()返回或exit(1)被调用,Hello的任务闭幕。内核向其父进程(Shell)发送SIGCHLD,是落叶归根的讯号。
- Shell以waitpid()回收Hello的遗产:开释PID,清空页表,将物理内存归还体系池。曾经的代码、数据与堆栈,化作虚无(020)。
附件
全部创建出的文件
预处理阶段创建了hello.i文件:
作用:展示宏展开和头文件包罗后的完备代码
编译阶段创建了hello.s文件:
作用:展示C代码转换后的汇编语言表示
汇编阶段创建了hello.o文件:
作用:包罗呆板码但未完成最终地址重定位
链接阶段创建了hello可实行文件
作用:可直接在Linux体系上运行的程序
利用readelf读取hello.o文件的ELF信息,重定位输出天生helloelf.txt文件
反汇编hello可实行文件,将反汇编结果重定位创建了excutabledump.txt文件
利用readelf读取hello可实行文件的ELF信息,重定位输出到excutable.txt文件
参考文献
[1] MIPS指令集:汇编源程序(.S文件)编写_.s文件编写-CSDN博客
[2] 谭浩强. (2010). C 程序计划(第4版). 北京: 清华大学出版社.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |