这篇论文的目的是探讨程序Hello从编写到执行的整个过程,主要关注Hello程序在各个阶段的转换和处理。通过预处理、编译、汇编、链接和执行,展示了一个程序如何被转化为计算机可以执行的机器代码,并在操作系统的支持下运行。文章详细描述了每个步骤的概念、使用的命令以及生成的中间结果,分析了每个阶段的关键点和注意事项。论文结果表明,理解程序的生命周期不仅有助于提高编程效率,还能加深对计算机系统工作原理的理解。
关键词:程序生命周期;预处理;编译;汇编;链接;进程管理;存储管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 6 -
2.1 预处理的概念与作用........................................................... - 6 -
2.2在Ubuntu下预处理的命令................................................ - 6 -
2.3 Hello的预处理结果解析.................................................... - 6 -
2.4 本章小结............................................................................... - 8 -
第3章 编译................................................................................... - 9 -
3.1 编译的概念与作用............................................................... - 9 -
3.2 在Ubuntu下编译的命令.................................................... - 9 -
3.3 Hello的编译结果解析........................................................ - 9 -
3.4 本章小结............................................................................. - 12 -
第4章 汇编................................................................................. - 13 -
4.1 汇编的概念与作用............................................................. - 13 -
4.2 在Ubuntu下汇编的命令.................................................. - 13 -
4.3 可重定位目标elf格式...................................................... - 13 -
4.4 Hello.o的结果解析........................................................... - 15 -
4.5 本章小结............................................................................. - 16 -
第5章 链接................................................................................. - 17 -
5.1 链接的概念与作用............................................................. - 17 -
5.2 在Ubuntu下链接的命令.................................................. - 17 -
5.3 可执行目标文件hello的格式......................................... - 17 -
5.4 hello的虚拟地址空间....................................................... - 19 -
5.5 链接的重定位过程分析..................................................... - 19 -
5.6 hello的执行流程............................................................... - 22 -
5.7 Hello的动态链接分析...................................................... - 23 -
5.8 本章小结............................................................................. - 24 -
第6章 hello进程管理.......................................................... - 25 -
6.1 进程的概念与作用............................................................. - 25 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 25 -
6.3 Hello的fork进程创建过程............................................ - 25 -
6.4 Hello的execve过程........................................................ - 25 -
6.5 Hello的进程执行.............................................................. - 26 -
6.6 hello的异常与信号处理................................................... - 26 -
6.7本章小结.............................................................................. - 29 -
第7章 hello的存储管理...................................................... - 30 -
7.1 hello的存储器地址空间................................................... - 30 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 30 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 30 -
7.4 TLB与四级页表支持下的VA到PA的变换................... - 31 -
7.5 三级Cache支持下的物理内存访问................................ - 31 -
7.6 hello进程fork时的内存映射......................................... - 33 -
7.7 hello进程execve时的内存映射..................................... - 33 -
7.8 缺页故障与缺页中断处理................................................. - 33 -
7.9动态存储分配管理.............................................................. - 33 -
7.10本章小结............................................................................ - 34 -
参考文献....................................................................................... - 37 -
第1章 概述
1.1 Hello简介
Hello 的 P2P(Program to Process)过程始于菜鸟在文本编辑器中编写 hello.c 程序,经过预处理器处理源代码后,编译器将其转换为汇编代码,再由汇编器转为目标机器码生成目标文件,随后链接器将多个目标文件和库文件结合生成可执行文件。操作系统在命令行界面运行该程序,通过创建新进程(fork)和 execve 系统调用将可执行文件加载到内存中并开始执行,同时内存管理单元处理虚拟地址到物理地址的映射,操作系统分配 CPU 时间片使其在硬件上运行,管理输入输出操作和信号处理,程序执行完毕后操作系统清理资源并终止进程。整个过程展示了程序从编写、预处理、编译、汇编、链接、执行到结束的完整生命周期,体现了计算机系统的工作原理,即 O2O(From Zero-0 to Zero-0)从零开始编写程序到执行完毕后回归零状态。
1.2 环境与工具
X64 CPU AMD Ryzen 7 5800H@ 2.30Hz,16G RAM,512G SSD
Windows 11 21H2 64位,Ubuntu 18.04 LTS 64位 双系统
VMware,gcc,vim
1.3 中间结果
附件1 hello.c
作用:高级语言源程序
附件2 hello.i
作用:预处理生成的.i文件
附件3 hello.s
作用:编译生成的.s文件(汇编语言文件)
附件4 hello.o
作用:汇编生成的.o文件(可重定位的目标文件)
附件5 hello
作用:链接生成的可执行的目标文件
1.4 本章小结
本章简要介绍了一下Hello的一生,并列出了撰写本文所需要的环境与工具,以及列出了中间结果的名字和作用,以便对照查看。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是编译过程中重要的初步步骤,主要作用是对源代码进行文本替换和宏扩展,以便编译器能够更容易地进行后续处理。在预处理阶段,预处理器根据指令对源代码进行操作,包括替换宏定义、包含头文件、删除注释、处理条件编译指令等。这些操作确保源代码的简洁性和模块化,提高了代码的可读性和可维护性。通过预处理,程序员可以方便地管理代码的重复使用和平台相关性,为编译器生成更为统一和简洁的代码输入,从而提高编译效率和质量。
2.2在Ubuntu下预处理的命令
在Ubuntu下可通过gcc –m64 –no-pie –fno-PIC -E hello.c -o hello.i对hello.c进行预处理,并把结果保存在hello.i中。
图 1 Ubuntu下预处理过程
2.3 Hello的预处理结果解析
打开预处理文件hello.i后发现,首先是涉及到的头文件的信息,对应预处理包含头文件的功能。
图 2 头文件信息
然后是用typedef定义的数据类型别名
图 3 数据类型别名
在这之后是包含的头文件的主体内容,有更多的typedef以及结构体,还有外部变量声明和函数声明。
图 4 头文件的主体内容
最后才是hello.c的内容。
图 5 hello.c的内容
2.4 本章小结
本章中介绍了预处理的概念和作用,并给出了ubuntu下怎么对hello.c进行预处理,并通过生成的预处理文件hello.i,解析了预处理的结果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将预处理后的源代码文件(通常以 .i 结尾)转换为汇编代码(通常以 .s 结尾)的过程,主要作用是将高级语言的语法结构转化为低级的汇编语言指令。在编译阶段,编译器首先对预处理后的源代码进行语法分析和语义分析,检查代码的语法和逻辑正确性。然后,编译器将代码转换为中间表示形式,并进行各种优化处理,以提升代码的执行效率。最终,编译器生成对应的汇编代码,该汇编代码可以更直接地映射到具体的机器指令。这一步骤确保了代码从高级抽象层次转化为更接近机器层次的表示形式,为后续的汇编和链接步骤做好准备,同时也为程序的高效执行打下基础。
3.2 在Ubuntu下编译的命令
在Ubuntu下可通过gcc -S hello.i -o hello.s将hello.i编译并把结果保存在hello.s中。
图 6 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据
常量:字符串常量保存在只读数据段即.rodata
图 7 字符串常量
变量:没有全局变量和静态变量。局部变量储存在栈中,通过%rbp进行访问,局部变量i储存在-4(%rbp)中。
图 8 局部变量i
main函数的参数argc和argv储存在%edi和%rsi中,并分别被储存在栈中-20(%rbp)和-32(%rbp)中。
图 9 main参数argc和argv
表达式:hello.c中有两个表达式:argc!=5和i<10。argc!=5在编译文件中为
图 10 表达式argc!=5
i<10在编译文件中为
图 11 表达式i<10
类型:类型通过汇编指令和寄存器进行区分。比如movq是移动八字节的数据,而movl是移动四字节的数据。%rdi是六十四位寄存器,适用于六十四位数据,%edi是%rdi的低三十二位寄存器,适用于三十二位数据。
3.3.2赋值
如图8,将局部变量i赋初值0.
3.3.3算数操作
i++在循环末尾由addl $1, -4(rbp)实现。
图 12 i++
3.3.4关系操作
关系操作argc!=5和i<10如图10和图11所示。
3.3.5数组/指针/结构操作
argv数组通过基地址加上偏移量的方式取数值,并放到%rcx、%rdx和%rax(最终放到%rsi)中。
图 13 数组操作
3.3.6控制转移
if(argc!=5)通过以下操作实现:比较argc与5的值,如果相等则跳转到.L2。
图 14 if(argc!=5) 实现
for(i=0;i<10;i++)的实现方法是先将i赋初值0,然后执行循环内的内容,将i加一,然后将i与9比较,若相等,则继续执行,若不相等,跳到.L4,重新进行循环。
图 15 for的实现
3.3.7函数操作
通过call指令调用puts、exit、printf、atoi、sleep等函数。
图 16 puts函数调用
图 17 exit函数调用
图 18 printf函数调用
图 19 atoi函数调用
图 20 sleep函数调用
3.4 本章小结
本文介绍了编译的概念和作用并给出了Ubuntu下编译的指令,之后根据hello.s文件对编译结果在数据、赋值、算术操作、关系操作、数组操作、控制转移、和函数操作等方面进行了解析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言代码文件(通常以 .s 结尾)转换为目标机器语言二进制文件(通常以 .o 结尾)的过程,主要作用是将人类可读的汇编语言指令转化为计算机可以直接执行的机器码。汇编器在此过程中解析汇编代码,将每条汇编指令翻译成相应的机器指令,并生成包含这些指令的目标文件。这个目标文件还包括数据段、符号信息和重定位信息等,为链接阶段准备好必要的内容。通过汇编,代码从较高层次的汇编语言转换为更底层的机器语言,确保程序可以在计算机硬件上高效执行,并为最终生成可执行文件做好准备。
4.2 在Ubuntu下汇编的命令
通过gcc -c hello.s -o hello.o命令进行汇编。
图 21 Ubuntu下汇编命令
4.3 可重定位目标elf格式
4.3.1 ELF头
通过命令readelf -h hello.o查看ELF头,ELF头包含字大小,字节序、文件类型等信息。
图 22 ELF头
4.3.2程序头表
通过命令readelf -l hello.o查看程序头表,发现其为空。
图 23 程序头表
4.3.3节头表
通过命令readelf -S hello.o查看节头表,节头表记录了各个节在文件中的偏移量、大小等。
图 24 节头表
4.4.4节
可以通过命令readelf -p num hello.o查看各个节的内容。
图 25 部分节的内容
4.4.5可重定位代码
通过readelf -p 9 hello.o可查看可重定位代码,可重定位代码是在可执行文件中需要修改的指令地址。。
图 26 可重定位代码
4.4 Hello.o的结果解析
图 27 hello.o的反汇编
对照分析:
首先是反汇编结果中每一行前面都有一串十六进制代码,这是机器语言代码,而hello.s中没有。
其次是hello.s中跳转指令跳转到的是.L3等,而反汇编结果中则是跳转到用偏移量表示的具体的地址。
图 28 反汇编结果中的跳转
另外hello.s中函数调用是call加具体函数名字,而反汇编结果中函数调用是call加main的地址加上偏移量。其中R_X86_64_PC32 是一种重定位类型,用于在 x86-64 架构上进行位置无关代码的地址计算。它表示 32 位相对偏移,即地址相对于当前指令地址的 32 位偏移量。
图 29 反汇编结果中的函数调用
4.5 本章小结
本章介绍了汇编的概念和作用,并具体分析了elf文件格式的构成,然后将hello.o反汇编并将反汇编结果与第三章中的hello.s进行了对照分析。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将一个或多个目标文件(例如 hello.o)组合成一个最终可执行文件(例如 hello)的过程,主要作用是解决符号引用、重定位地址并生成完整的可执行文件。链接过程涉及将各个目标文件中的代码和数据段合并,并处理符号解析和重定位。
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进行链接。
图 30 Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
ELF头:
图 31 hello的ELF头
节头表:
图 32 hello的节头表
程序头表:
图 33 hello的程序头表
5.4 hello的虚拟地址空间
由于无法安装edb,在网上查询到可以使用以下方法查询hello的虚拟地址空间,首先输入./hello 2022112672 梁智恒 15017333723 &让hello在后台运行,然后通过ps -u查询hello的pid,之后通过cat /proc/pid/maps查询hello的虚拟地址空间。
图 34 hello的虚拟地址空间
5.5 链接的重定位过程分析
图 35 hello的反汇编结果1
图 36 hello的反汇编结果2
图 37 hello的反汇编结果3
通过和hello.o的对比可以发现,hello的反汇编结果要比hello.o长很多,这是因为hello多出了很多除了main函数之外的函数,而hello.o只有main函数的部分,说明链接过程中将main函数与其他文件代码合并到了一起。并且hello.o中main函数的地址全为0,之后的代码的地址是相对于main的偏移量,但在hello的反汇编中,所有函数和代码都有了确定的地址,说明在链接过程中给每行代码都分配了虚拟空间地址。
以图29为例,本来具有重定位类型R_X86_64_PC32,且具有相对于main的偏移量,但是hello反汇编中没有,而是变成了一个具体的地址,说明链接过程中对其进行了重定位。
5.6 hello的执行流程
hello的执行结果为
图 38 hello的执行结果
在gdb中执行hello,用set 2022112672 梁智恒 15017333723 3设置参数,要想知道从加载hello到_start,到call main,以及程序终止的所有过程,在_start和main处设置断点,然后执行,可以知道_start地址为0x400520
图 39 断点1_start
之后可以根据stepi,continue,backtrace,disassemble查看其执行过程。
想要知道其调用与跳转的各个子程序名或程序地址,可以通过objdump -t ./hello命令进行查看。
图 40 hello的各个子程序名和程序地址
5.7 Hello的动态链接分析
当 hello 程序调用由共享库(如 libc.so)定义的函数(如 printf)时,编译器无法预测这些函数的运行时地址,因为共享库可以在运行时加载到任意位置。为了处理这一点,GNU 编译系统使用延迟绑定技术,通过过程链接表(PLT)和全局偏移量表(GOT)来解析函数地址。PLT 是代码段的一部分,每个被调用的库函数都有对应的 PLT 条目,通过这些条目进行跳转。GOT 是数据段的一部分,包含函数的目标地址。当程序第一次调用一个函数时,动态链接器会通过 GOT 和 PLT 的协作来解析并绑定函数地址,此后调用会直接跳转到该函数的实际地址。
首先通过readelf找到.got函数的位置。
图 41 .got函数的位置
然后通过gdb执行hello,同样在_start和main处设置断点。执行到_start后通过命令x/16gx 0x0000000000600930查看.got段的内容。
图 42 动态链接前的.got
然后继续执行到断点main,同样用命令x/16gx 0x0000000000600930查看.got段的内容。
图 43 动态链接后的.got
可以发现在0x600960处发生了变化。
5.8 本章小结
本章首先介绍了链接的概念和作用,给出了Ubuntu下链接的命令。并分析了可执行文件hello的格式,通过对比hello的反汇编结果和hello.o对重定位过程进行了分析,然后执行hello,分析其执行过程和各子程序名称与地址,最后进行hello的动态链接分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是操作系统中一个运行中的程序实例,它不仅包含了程序的代码,还包括了当前活动的资源、数据和状态信息,进程的主要作用是实现多任务处理,使得多个程序可以在同一时间段内并发执行,尽管实际上每个处理器在任意时刻只能处理一个任务,通过进程的切换,操作系统能够在短时间内迅速交替运行多个进程,从而提升系统的整体效率和响应速度,确保资源的合理分配和有效利用。
6.2 简述壳Shell-bash的作用与处理流程
Shell(壳)是用户与操作系统之间的命令行解释器,它的主要作用是接受用户输入的命令,将其解析后传递给操作系统内核执行,Bash是一种常见的Shell类型,通过提供一个用户友好的界面,Bash允许用户运行命令、启动程序、执行脚本和管理系统资源。它的处理流程通常为
1.启动:用户启动Shell后,它显示提示符等待输入。
2.读取:Shell读取用户输入的命令行。
3.解析:Shell解析输入,分解为命令和参数。
4.执行:Shell根据解析结果执行命令,可能是内部命令或外部程序。
5.返回结果:执行完成后,Shell显示输出或错误信息。
6.循环:Shell再次显示提示符,等待下一次用户输入。
6.3 Hello的fork进程创建过程
程序开始运行时,操作系统会加载程序代码并分配所需的资源,接着执行main函数中的代码。假设在该程序的主循环中加入了fork调用,fork函数会创建一个子进程,该子进程是父进程的一个副本,拥有相同的代码和数据段。当fork被调用时,父进程会得到子进程的进程ID,而子进程会得到一个返回值为0,这样父进程和子进程就可以通过检查fork的返回值来执行不同的代码路径。父进程可能会继续执行其他任务或等待子进程完成,而子进程则可能会执行特定的任务。
对于hello程序中,如果在主循环的每次迭代中调用fork,每个子进程都会打印一次消息并调用sleep函数,暂停指定的秒数。这样,每次调用fork后,父进程和子进程都会打印消息并暂停,直至循环结束。在这种情况下,子进程会继承父进程的资源,如文件描述符,但它们是独立的执行实体,拥有各自的进程ID。
6.4 Hello的execve过程
在Hello程序的执行过程中,通过调用fork函数创建子进程,然后在子进程中使用execve函数来加载并运行Hello程序,execve函数会将当前子进程的内容替换为Hello程序的内容,通过虚拟内存机制映射代码段和数据段,并加载所需的共享库,最终将控制权转移到Hello程序的入口点,从而实现上下文切换和新程序的执行。
6.5 Hello的进程执行
在操作系统中,Hello程序的执行涉及进程创建、上下文切换、进程调度以及用户态与核心态的转换。当Hello程序启动时,操作系统将其加载到内存中并在用户态运行。程序执行过程中,调用fork函数创建子进程,操作系统会复制当前进程的资源,并为新进程分配新的PID和时间片。进程调度器会根据调度算法,在时间片结束时进行上下文切换,保存当前进程状态并加载下一个进程状态。用户态程序调用系统函数时,会触发陷阱指令,将控制权切换到核心态,操作系统内核处理请求后再返回用户态。当Hello程序中的子进程调用execve函数加载并执行新程序时,操作系统会替换当前进程的内容,加载新程序的代码段和数据段,并将控制权转移到新程序的入口点,开始执行新程序的代码。整个过程中,操作系统不断进行用户态与核心态的转换、上下文切换和进程调度,确保所有进程高效运行。
6.6 hello的异常与信号处理
1.不停乱按
程序照常执行,每过一段时间输出Hello 2022112672 梁智恒 15017333723.不受不停乱按的影响。
图 44 不停乱按的结果
2.Ctrl-C
按下Ctrl+C时,终端会发送SIGINT信号给程序,默认处理方式是立即终止程序。
图 45 Ctrl-C的结果
3.Ctrl-Z
当用户按下Ctrl+Z,操作系统会发送一个SIGTSTP
信号给程序,默认情况下,SIGTSTP
信号会导致进程暂停执行。
图 46 Ctrl-Z的结果
4.ps、jobs
ps
命令显示当前终端会话中的进程状态。jobs命令显示当前会话中的后台作业。
图 47 ps、jobs的结果
5.pstree
pstree
命令显示以树状结构展现的进程状态。
图 48 pstree的结果
6.fg
fg
命令将进程移回前台继续执行,恢复进程的执行会发送SIGCONT
信号给进程,进程收到SIGCONT
信号后,恢复运行。
图 49 fg的结果
7.kill
kill
命令用于向指定的进程发送信号,可以用于终止进程、暂停进程、继续进程等。比如kill -9 <pid>可以强制终止进程。
图 50 kill -9 <pid>的结果
6.7本章小结
本章介绍了进程的概念和作用,并介绍了shell-bash的作用和工作流程,随后讲解了hello的fork进程创建过程和execve过程,以及hello的进程执行和hello遇到各种异常与信号的处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址、线性地址、虚拟地址和物理地址在内存管理和程序执行过程中发挥着重要作用。逻辑地址是程序在编译时产生的地址,通常由程序员或编译器指定。在hello程序中,当我们编写代码并编译生成可执行文件时,所有的地址引用都是逻辑地址。这些地址没有直接映射到实际物理内存,而是作为程序的一部分存储在可执行文件中。线性地址是操作系统通过内存管理单元(MMU)将逻辑地址转换而来的地址。在程序执行过程中,CPU通过MMU将逻辑地址转换为线性地址。这一过程通常涉及地址变换机制,如分段或分页。在现代操作系统中,线性地址通常与虚拟地址等同,因为线性地址空间提供了一个连续的地址视图,即使物理内存是分散的。虚拟地址是操作系统提供给程序的一种抽象地址空间,使得每个进程都认为自己独占整个内存。在hello程序执行时,程序员定义的逻辑地址通过操作系统的地址映射机制转换成虚拟地址。虚拟地址空间的设计不仅简化了内存管理,还提供了内存保护机制,防止进程间的互相干扰。物理地址则是内存芯片上的实际地址。在程序执行时,操作系统最终需要将虚拟地址映射到物理地址,从而访问实际的硬件内存。内存管理单元负责将虚拟地址转换成物理地址。比如在hello.c程序运行过程中,系统会将每次调用printf函数输出的信息和sleep函数暂停的时间转换成相应的物理地址进行处理。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel处理器中,逻辑地址到线性地址的转换通过段式管理机制实现。逻辑地址由段选择器和段内偏移量组成。段选择器指示段描述符在全局描述符表(GDT)或局部描述符表(LDT)中的位置,而段描述符包含段的基地址、段界限和段属性。当程序使用逻辑地址时,处理器首先通过段选择器找到段描述符。段选择器包含段描述符表索引、表指示符(GDT或LDT)以及请求特权级(RPL)。处理器利用段描述符表索引定位到段描述符,读取段的基地址。然后,将段基地址与段内偏移量相加,形成线性地址。计算公式为:
线性地址=段基地址+段内偏移量。
例如,如果段基地址是0x1000,段内偏移量是0x0020,那么线性地址就是0x1020。
7.3 Hello的线性地址到物理地址的变换-页式管理
hello中,从线性地址到物理地址的转换通过页式管理机制实现。页式管理将内存划分为固定大小的页和页框,通过页表进行地址映射。当hello生成一个线性地址时,处理器将其分为页目录、页表和页内偏移三部分。线性地址的格式如下:
线性地址=[页目录索引∣页表索引∣页内偏移]
以下是地址转换的具体步骤:
首先是页目录查找:处理器首先读取控制寄存器CR3中的页目录基地址,然后利用线性地址中的页目录索引(高10位)定位到页目录表中的页目录项(PDE)。页目录项包含页表的基地址。
其次是页表查找:接着处理器利用页目录项中的页表基地址和线性地址中的页表索引(中间10位)找到对应的页表项(PTE)。页表项包含物理页框的基地址。
最后是页内偏移:最后处理器使用页表项中的物理页框基地址和线性地址中的页内偏移(低12位)计算出最终的物理地址。计算公式为:
物理地址=物理页框基地址+页内偏移
举个例子,比如0x12345678这个线性地址中,页目录索引为0x1,页表索引为0x23,页内偏移为0x45678。处理器首先通过CR3找到页目录表,使用页目录索引0x1定位到页目录项,得到页表的基地址。然后使用页表基地址和页表索引0x23找到页表项,获取物理页框的基地址。最后,通过物理页框基地址加上页内偏移0x45678,计算得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
当CPU要访问某个虚拟地址时,首先会在TLB中查找是否存在该地址的映射。如果找到了映射,CPU会直接返回对应的物理地址,完成转换。如果未找到,则需要进行页表查找。若TLB未命中,CPU会使用四级页表进行地址转换。四级页表包括页目录指针表(PML4)、页目录(PDPT)、页目录(PDT)和页表(PT)。虚拟地址被分为五部分:PML4索引、PDPT索引、PDT索引、PT索引和页内偏移。从控制寄存器CR3获取PML4表的基地址。然后,使用虚拟地址中的PML4索引(高9位)查找PML4表中的PML4条目。接着,从PML4条目中获取PDPT表的基地址,再利用PDPT索引(中间9位)查找PDPT表中的PDPT条目。然后,从PDPT条目中获取PDT表的基地址,并使用PDT索引(中间9位)查找PDT表中的PDT条目。接着,从PDT条目中获取页表(PT)的基地址,使用PT索引(中间9位)查找页表中的页表条目(PTE)。最后,从页表条目中获取物理页框的基地址,将其与页内偏移(低12位)相加,得到最终的物理地址。
由图39可知hello中的printf函数地址是0x4004b0。假设CR3中的PML4基地址为0x1000。页目录索引可以通过以下计算得到:页目录索引为0x1。处理器通过页目录索引0x1查找页目录表中的页目录项。假设找到的页目录项包含页表的基地址为0x2000。接着,处理器使用页目录项中的页表基地址0x2000和线性地址0x4004b0中的页表索引0x4找到对应的页表项。假设找到的页表项包含物理页框的基地址为0x3000。最后,处理器使用页表项中的物理页框基地址0x3000和线性地址0x4004b0中的页内偏移0x4b0计算出最终的物理地址为0x34b0。
7.5 三级Cache支持下的物理内存访问
在计算机系统中,三级缓存(L1、L2、L3)共同支持物理内存的高效访问。缓存是一种高速存储器,存放着频繁访问的数据,以提高数据访问的速度。缓存层级从L1到L3,容量逐渐增大,但访问速度逐渐变慢。处理器首先尝试从最近的缓存层获取所需数据,逐层往外查找。
当CPU需要访问数据时,首先会检查L1缓存。L1缓存是最接近处理器的一级缓存,速度最快,但容量最小。它分为指令缓存和数据缓存。如果所需数据在L1缓存中找到,即称为命中,CPU可以快速获取数据并继续执行。然而,如果数据不在L1缓存中,称为未命中,CPU会继续查找L2缓存。
L2缓存比L1缓存稍远,访问速度较慢,但容量更大。L2缓存通常是统一的,存储指令和数据。如果数据在L2缓存中找到,即L2缓存命中,CPU可以从L2缓存获取数据。如果L2缓存未命中,CPU会继续查找L3缓存。
L3缓存是最后一级缓存,离处理器最远,访问速度最慢,但容量最大。L3缓存通常是共享的,供处理器的多个核心使用。如果数据在L3缓存中找到,即L3缓存命中,CPU可以从L3缓存获取数据。如果L3缓存也未命中,CPU只能访问主内存。
访问主内存相比缓存要慢得多。处理器从主内存获取数据后,会将数据存入各级缓存,以便下次访问时能更快地获取。这个缓存填充过程使得频繁使用的数据在缓存中有更高的命中率,从而提高了系统整体的性能。
图 51 三级缓存示意图
7.6 hello进程fork时的内存映射
调用fork时,操作系统会为子进程创建一个新的进程控制块,并复制父进程的页表结构,但不立即复制实际的物理内存。写时复制机制让父子进程共享相同的物理内存页面,并将这些页面标记为只读。当父或子进程尝试写入共享页面时,操作系统会为尝试写入的进程分配一个新的物理页面,并将原页面内容复制到新页面中。这样,只有在写操作发生时才会实际复制内存,从而节省资源。
7.7 hello进程execve时的内存映射
当hello进程调用execve时,首先,操作系统会销毁当前进程的内存映像。这包括释放进程的代码段、数据段、堆和栈等所有内存区域。然后,操作系统会为新程序建立一个新的内存映像。新的内存映像包括加载可执行文件的代码段、数据段、只读数据段和其他必要的内存区域。操作系统会从磁盘中读取新的可执行文件,并将其映射到进程的虚拟地址空间。例如,新程序的代码段会被映射到低地址区域,数据段和BSS段会被映射到稍高的地址区域,堆和栈也会重新初始化。
7.8 缺页故障与缺页中断处理
当缺页故障发生时,处理器会产生一个缺页中断,并暂停当前的程序执行。操作系统的内存管理单元(MMU)会捕获这个中断,并开始处理缺页故障。首先,操作系统会检查导致缺页故障的虚拟地址。如果该地址是合法的,但当前没有映射到物理内存,操作系统会进行下一步处理。操作系统会查找虚拟地址对应的页面在磁盘上的位置。这个信息通常存储在页表或其他数据结构中。找到页面位置后,操作系统会选择一个空闲的物理内存页框。如果没有空闲页框,操作系统可能会使用页面置换算法(如LRU)选择一个当前不常用的页框,将其内容写回磁盘,并将其分配给新的页面。
接着,操作系统会将需要的页面从磁盘加载到选定的物理页框中,并更新页表项,将虚拟地址映射到新的物理页框。页表项还会设置相关的访问权限和状态信息,如页面是否为只读或可写。在完成这些操作后,操作系统会恢复被中断的程序执行。处理器重新尝试访问引发缺页故障的虚拟地址,这次访问应该会成功,因为所需的页面已经被正确加载到内存中。
7.9动态存储分配管理
在程序hello中,printf函数可能会调用malloc来分配动态内存。malloc函数用于在堆上分配指定大小的内存块,并返回指向该内存块的指针。当程序调用malloc时,操作系统或运行时库会在堆中查找一个合适大小的空闲内存块。堆是一块较大的内存区域,专门用于动态分配内存。为了管理堆中的空闲和已分配内存块,操作系统使用各种内存管理策略:
一种常见的策略是首次适应算法。在这种策略中,malloc从堆的起始位置开始查找第一个足够大的空闲内存块。找到后,该内存块被分割为已分配部分和剩余的空闲部分,然后返回已分配部分的指针。首次适应算法简单且速度快,但容易造成内存碎片。
另一种策略是最佳适应算法。malloc遍历整个堆,查找最接近所需大小的空闲内存块。虽然这种方法可以减少内存碎片,但查找过程可能会比较慢。
最坏适应算法则相反,它查找最大的空闲内存块,并将其分割以满足分配请求。这个策略也有助于减少内存碎片,但和最佳适应算法一样,查找过程可能比较慢。
7.10本章小结
本章讲解了hello是如何进行内存管理的,包括hello的存储器地址空间有什么,如何进行逻辑地址到线性地址的变换、线性地址到物理地址的变换,以及TLB与四级页表支持下的VA到PA的变换等。
(第7章 2分)
结论
Hello程序通过一系列的步骤从源代码最终变成可执行文件,包括预处理、编译、汇编和链接,每一步都涉及到特定的操作和转换。预处理阶段主要是进行宏扩展和文件包含,编译阶段将高级语言转换为汇编代码,汇编阶段生成目标机器码,而链接阶段则将多个目标文件和库文件合并成一个可执行文件。最后,操作系统通过fork和execve系统调用创建新进程并执行程序。通过对Hello程序的研究,我们深刻认识到计算机系统的复杂性和精妙设计,理解了各个阶段的具体实现和其在计算机系统中的重要作用。创新理念在于对每个步骤进行更高效的优化和改进方法,使得程序的执行更为快速和稳定。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
附件1 hello.c
作用:高级语言源程序
附件2 hello.i
作用:预处理生成的.i文件
附件3 hello.s
作用:编译生成的.s文件(汇编语言文件)
附件4 hello.o
作用:汇编生成的.o文件(可重定位的目标文件)
附件5 hello
作用:链接生成的可执行的目标文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 北岛寒沫. 用Ubuntu编写第一个C程序并预处理、编译、汇编、链接. https://blog.csdn.net/hanmo22357/article/details/123693595
[2] Tsingke. 深度解析程序从编译到运行,1999. https://www.cnblogs.com/tsingke/p/9747141.html
[3] 乐行僧丶. Linux中查看进程的虚拟地址空间内存布局. https://blog.csdn.net/ASJBFJSB/article/details/81429576
[4] madao756. 段页式访存——逻辑地址到线性地址的转换. https://www.jianshu.com/p/fd2611cc808e
[5] mfditxkj. 段页式内存管理中,逻辑地址,线性地址,物理地址的差别. https://www.cnblogs.com/daqiang/articles/3054774.html#:~:text=%E6%9C%BA%E7%90%86%20%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80%EF%BC%88%E6%88%96%E7%A7%B0%E4%B8%BA%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80%EF%BC%89%E5%88%B0%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E6%98%AF%E7%94%B1CPU%E7%9A%84%E6%AE%B5%E6%9C%BA%E5%88%B6%E8%87%AA%E5%8A%A8%E8%BD%AC%E6%8D%A2%E7%9A%84%E3%80%82,%E5%A6%82%E6%9E%9C%E6%B2%A1%E6%9C%89%E5%BC%80%E5%90%AF%E5%88%86%E9%A1%B5%E7%AE%A1%E7%90%86%EF%BC%8C%E5%88%99%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E5%B0%B1%E6%98%AF%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%E3%80%82%20%E5%A6%82%E6%9E%9C%E5%BC%80%E5%90%AF%E4%BA%86%E5%88%86%E9%A1%B5%E7%AE%A1%E7%90%86%EF%BC%8C%E9%82%A3%E4%B9%88%E7%B3%BB%E7%BB%9F%E7%A8%8B%E5%BC%8F%E9%9C%80%E8%A6%81%E5%8F%82%E5%92%8C%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E5%88%B0%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%E7%9A%84%E8%BD%AC%E6%8D%A2%E8%BF%87%E7%A8%8B%E3%80%82%20%E5%85%B7%E4%BD%93%E6%98%AF%E9%80%9A%E8%BF%87%E8%AE%BE%E7%BD%AE%E9%A1%B5%E7%9B%AE%E5%BD%95%E8%A1%A8%E5%92%8C%E9%A1%B5%E8%A1%A8%E9%A1%B9%E8%BF%9B%E8%A1%8C%E7%9A%84%E3%80%82
(参考文献0分,缺失 -1分)