本文以“程序人生-Hello's P2P”为主题,系统探究了从C语言源程序到进程执行的完整生命周期(Program to Process, P2P)及其内存管理机制(Zero to Zero, 020)。通过分析Hello程序的预处理、编译、汇编、链接、进程创建、存储管理与I/O处理等核心阶段,揭示了计算机系统底层的工作原理。实验基于Ubuntu 22.04环境,使用GCC工具链生成中间文件(如hello.i、hello.s、hello.o),结合readelf、objdump等工具解析ELF格式与重定位过程,并通过edb/gdb动态调试验证了虚拟地址空间分配、动态链接(PLT/GOT机制)及进程Fork/Execve的实现细节。研究表明,程序执行涉及多级地址转换(段式/页式管理)、缓存优化与异常处理等关键技术,最终由Shell通过进程管理完成资源回收,实现从“零痕迹”到“归零”的全周期闭环。本文为理解程序在计算机系统中的完整生命周期提供了实践性范例。
关键词:程序生命周期;ELF格式;动态链接;进程管理;虚拟内存;页式管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:用户编写hello.c文件(Program),包含高级语言代码;预处理器(cpp)处理宏定义、文件包含等,生成hello.i文件;编译器(cc1)将hello.i翻译为汇编代码hello.s;汇编器(as)将hello.s转换为机器指令,生成可重定位目标文件hello.o;链接器(ld)合并hello.o与库函数(如printf),生成可执行文件hello,在 shell 中输入./hello 命令,系统为 hello 程序 fork 一个子进程。这样,hello.c 就从程序变成了进程。
020:程序启动前,内存中无hello的痕迹(Zero),进程创建后,OS为其分配虚拟内存空间,按需加载到物理内存,CPU执行指令,利用缓存(L1/L2/L3)和页表加速访问,然后程序运行并完成输出,进程结束后,父进程(Shell)回收资源,内核清除进程描述符、释放内存,所有痕迹被抹除,回归初始状态(Zero)。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2.20GHz;16G RAM;1 TB 固态硬盘
软件环境:Windows11 64位;Vmware 17;Ububtu 22.04
开发与调试工具:gcc,edb,readelf,gedit,objdump,vi
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 源程序
hello.i 预处理后的修改的C程序
hello.s 汇编程序
hello.o 可重定位目标文件
hello 可执行目标文件
Objdump_hello_o hello.o的反汇编文件
Objdump_hello hello的反汇编文件
elf_hello hello的ELF格式
elf_hello_o hello.o的ELF格式
1.4 本章小结
对P2P和020的有了大概了解,同时大致了解了后续需要做的工作。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指在正式编译之前对源代码进行的文本级处理,由预处理器(Preprocessor)执行。它主要处理以#开头的指令,不涉及语法分析或编译优化,仅完成简单的文本替换和条件控制。
作用:通过#include将头文件内容插入当前文件,避免重复编写;使用#define定义宏(常量或函数式替换),提升代码通用性;通过#ifdef等条件编译实现跨平台或功能开关;预处理阶段删除注释和多余空白,生成纯净的中间代码供编译器处理。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

可以发现,hello.i文件扩展到了三千多行,相对于源程序,hello.i中的代码量剧增。这是因为#预处理命令将头文件的程序、宏变量、特殊符号等插入到代码中。
原来的C代码在文本的最末端。在此之前,则是stdio.h unistd.h stdlib.h的源代码的依次展开。

最开始的代码是hello.c拼接的各种库文件。

中间对很多内部函数进行了声明。

2.4 本章小结
本章介绍了预处理的概念和作用,并在Ubuntu中对hello.c进行了预处理和解析,浏览了hello.i的代码,对hello.i的内容有了认识。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译阶段(从 .i 到 .s)是将预处理后的高级C代码转换为汇编语言的关键步骤,由编译器(如GCC的cc1组件)完成。编译过程中,首先进行词法分析,将源代码拆解为记号(Token),如标识符、关键字等,然后进行语法分析,构建抽象语法树(AST),检查代码结构合法性(如括号匹配、语句格式),之后进行语义分析,验证类型匹配、变量声明等语义规则(如未定义变量的报错),再进行中间代码生成与优化,生成与机器无关的中间表示(如LLVM IR或三地址码),并进行冗余消除、循环优化等(受-O1/-O2等优化选项影响),最后生成汇编代码,根据目标架构(如x86、ARM)将优化后的中间代码翻译为汇编指令(.s文件)。
作用:进行语法/语义检查以确保代码逻辑正确。拥有跨平台适配,同一C代码可生成不同CPU架构的汇编。可以优化性能,通过中间代码优化提升执行效率。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 数据处理
3.3.1.1常量
字符串常量被存入.rodata(只读数据段)。编译器将格式化字符串和中文提示文字提取为静态地址标签 .LC0, .LC1。

3.3.1.2局部变量
局部变量通常使用栈指针(%rsp)和基址指针(%rbp)进行访问。在此段汇编代码中,可以看到通过基址指针(%rbp)来访问局部变量。
movl %edi, -20(%rbp) 用-20(%rbp)存储从main函数参数argc传入的值。
movq %rsi, -32(%rbp) 用-32(%rbp)存储从main函数参数argv传入的值。
movl $0, -4(%rbp) 用-4(%rbp)存储i的值。

![]()
3.3.1.3表达式
hello.c中的表达式 argc!=5 的汇编语言表示为:

i<10表示为:

3.3.1.4类型
根据变量类型使用不同的mov指令。
3.3.2赋值
将立即数0赋值给局部变量i,编译器将变量i分配在栈帧的-4(%rbp) 位置:
![]()
函数参数的赋值:
![]()
3.3.3类型转换
atoi(argv[4]) 中的类型转换由 atoi 完成(字符串转整型),返回值在 %eax,然后传递给 sleep 函数。
![]()
3.3.4算术操作
将局部变量 i 加1,即i++。
![]()
3.3.5关系操作
实现 if (argc != 5) 判断逻辑,cmpl 对比两个操作数,je 表示“相等跳转”,即 != 5 时不跳转,执行错误提示:
![]()
i <= 9,用于 for 循环条件判断:
![]()
3.3.6数组/指针操作
每个 argv[i] 相距8字节,编译器直接计算偏移,采用指针加偏移(argv + i)访问数组:

3.3.7控制转移
if (argc != 5) 分支:

for (i = 0; i < 10; i++) 循环:

3.3.8函数操作
3.3.8.1函数调用
所有函数调用通过call xxx@PLT完成,参数通过%rdi,%rsi,%rdx,%rcx,%r8,%r9顺序传递:
call printf@PLT
call atoi@PLT
call sleep@PLT
call getchar@PLT
call exit@PLT
3.3.8.2参数传递

3.3.8.3函数返回
主函数返回 0:
![]()
![]()
3.4 本章小结
本章分析了hello.s,看到了C程序的编译结果,汇编语言,并与C语言代码结合分析,增强了对汇编语言的理解。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编阶段是C程序编译过程的第三步,负责将编译器生成的汇编代码(.s文件)转换为机器可执行的目标代码(.o文件)。汇编器(如GCC的as工具)逐条解析汇编指令(如movl %eax, %ebx),将其翻译为二进制机器码,并生成包含代码段(.text)、数据段(.data)等结构的可重定位目标文件。此阶段还会处理符号表(记录函数、变量地址)和调试信息,为后续链接阶段提供基础。
作用:生成机器指令,将人类可读的汇编指令转换为CPU直接执行的二进制码;符号管理,记录全局变量、函数名等符号的地址,供链接器解析跨模块引用;平台适配,不同架构(如x86、ARM)的汇编器生成对应的机器码,确保程序能在目标硬件上运行;调试支持,嵌入源代码与机器码的映射信息,辅助调试器定位错误。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式
ELF(Executable and Linkable Format)文件是一种常见的可执行文件和可链接文件格式,用于在Unix和类Unix系统上存储程序、库和其他相关数据。包括文件头、程序头表、节表、节、重定位表、符号表。
4.3.1文件头
首先查看文件头(ELF Header)。其包含了描述ELF文件整体结构和属性的信息,包括ELF标识、目标体系结构、节表偏移、程序头表偏移等。

4.3.2程序头表
对于目标文件,程序头表(Program Header Table)可能为空。

4.3.3节表
节表描述了ELF文件中各个节的信息,包括节的名称、类型、偏移、大小等。ELF文件的数据和代码通常存储在各个节中,比如.text节存储代码段、.data节存储数据段等。其中.text和.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。

之后用readelf -a hello.o探查ELF文件中能探查的其他节,
.rela.text 包含 8 个条目。以下是这些条目的详细信息:

重定位节 .rela.eh_frame 记录了需要进行地址重定位的异常处理框架(exception handling frame)信息。以下是查看 .rela.eh_frame 的输出。

符号表 .symtab 是目标文件中非常重要的一部分,它列出了所有定义的符号,包括函数、变量和节。符号表在链接和调试过程中起着至关重要的作用。以下是符号表 .symtab 的具体内容和解释。

.note.gnu.property 节包含了一些描述目标文件属性的注释,特别是与特定平台或编译器相关的属性。

4.4 Hello.o的结果解析
objdump -d -r hello.o,得:

对照分析:
每行代码末尾指令基本相同,但在每条指令前面都会有一串十六进制的编码。hello.s是由汇编语言组成的,相对于计算机能识别的机器级指令,汇编代码仍是抽象语言;而反汇编得到的代码不仅仅有汇编代码,还有机器语言代码。机器语言代码是计算机可识别执行的,是一种纯粹的二进制编码。
1.分支转移时,hello.s文件会跳转到像.L2的代码段:

在反汇编文件中,会直接跳转到当前过程的起始地址加上偏移量得到的直接目标地址:

- 函数调用时,汇编文件中,call指令直接调用函数,函数名在后:
call printf@PLT
call atoi@PLT
call sleep@PLT
call getchar@PLT
call exit@PLT
反汇编文件中,9c是重定位条目所影响的地址偏移量,这是call指令中地址字段的起始位置。R_X86_64_PLT32:重定位类型,这里是R_X86_64_PLT32,表示这是一个32位的PLT重定位。PLT用于延迟绑定函数调用,在运行时解析函数地址。atoi-0x4: 表示重定位的目标符号是 atoi,加数是 -4。call 指令执行时,会跳转到由重定位条目所指示的目标地址。重定位类型 R_X86_64_PLT32 指示链接器,地址字段需要被填充为指向 PLT 表项的偏移,这个表项在运行时会解析为 atoi 函数的实际地址。atoi-0x4 中的 -0x4 是因为 call 指令的目标地址是相对于下一条指令的,所以需要减去 4 个字节来调整指令位置。

4.5 本章小结
本章我们把 hello.s 文件汇编成了 hello.o 可重定位目标文件,分析了它的 ELF文件格式和内容,并反汇编比较了它与 hello.s 文件格式的不同之处。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接阶段由链接器(如ld)完成,主要解决目标文件之间的符号引用问题。例如,当hello.o调用了printf函数(定义在标准库libc.so中),链接器会找到该函数的实际地址并将其嵌入到最终的可执行文件中。链接过程包括:符号解析,确定每个符号(如函数、全局变量)的定义位置;重定位,将目标文件中的符号引用替换为实际的内存地址。
作用:合并代码与数据,将分散在多个目标文件(如hello.o、库文件)中的代码段(.text)、数据段(.data)等合并为一个完整的可执行文件;解决外部依赖,解析跨文件的函数或变量引用(如printf来自标准库);优化资源,通过静态链接(合并所有代码)或动态链接(运行时加载共享库)平衡执行效率与内存占用。
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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1ELF头信息

5.3.2节头
描述了各个节的大小、起始位置和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

5.3.3程序头

5.4 hello的虚拟地址空间
使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在EDB中,可以通过数据转储功能查看加载到虚拟地址空间的程序代码。ELF格式文件的程序头表(Program Header)为链接器提供了运行时加载内容的指引,并包含动态链接相关信息。每个表项详细描述了各段在虚拟地址空间和物理地址空间中的属性。程序头表主要包含以下部分:
- PHDR:保存程序头表自身的地址信息
- INTERP:指定程序加载后需要调用的解释器路径
- LOAD:标识需要从二进制文件映射到虚拟地址空间的段,包含常量数据和程序目标代码等
- DYNAMIC:存储动态链接器所需的元数据
- NOTE:包含辅助性的注释信息
- GNU_STACK:用于设置栈内存的可执行权限标志
通过对比5.3节中列出的各节起始地址,可以在EDB中准确定位并查看对应的内存信息。如图所示,这些程序头信息完整描述了可执行文件在内存中的布局结构。

5.5 链接的重定位过程分析
在反汇编结果中,hello可执行文件的每条指令都带有唯一的虚拟地址,而hello.o目标文件只显示相对于.text段的偏移地址,这是因为目标文件尚未经过链接过程,其内存布局尚未确定,所有地址引用仍保持相对形式;而经过链接器处理后,hello已完成重定位,所有指令和数据都被分配了最终的虚拟内存地址,地址引用关系也全部解析为绝对地址,从而形成完整的可执行内存映像。
hello:


Hello已经完成链接,所以其反汇编的地址关系已经确定,直接给出。
hello.o:


Hello.o并没有链接,所以需要告诉链接器在链接时需要执行的动作。
6f: R_X86_64_PLT32 printf-0x4告诉链接器(linker)在链接时需要执行的动作。6f 是一个字节偏移量,指示了在某个特定位置发生了重定位动作。
R_X86_64_PLT32 是一个重定位类型(relocation type),表示这是一个32位的重定位项。printf-0x4 表示需要修改的目标符号是 printf,并且要在链接时将其地址减去 0x4。
5.6 hello的执行流程
edb查看:

| 程序地址 | 程序名 |
| 0x00000000004010f0 | hello!_start |
| 0x0000000000401125 | hello!main |
| 0x0000000000401000 | hello!_init |
| 0x00000000004011c8 | hello!_fini |
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
在调用共享库函数时,由于编译器无法预知共享模块的加载位置,它首先生成重定位记录,由动态链接器在程序加载时解析地址。GNU编译系统采用延迟绑定技术,将函数地址绑定推迟到首次调用时,这是通过全局偏移表(GOT)和过程链接表(PLT)协同实现的:PLT作为代码段的跳转表,其首项指向动态链接器,后续每项对应一个库函数调用;GOT作为数据段的地址表,前两项存储动态链接信息,第三项是链接器入口,其余项保存待解析的函数地址。其中.got节支持位置无关代码,记录全局变量和函数的最终地址;而.got.plt节专用于延迟绑定,首次调用函数时动态链接器会更新其条目为真实地址,后续调用即可直接跳转,从而提升运行时效率。


分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章通过分析ELF文件结构、结合EDB动态调试以及对比链接前后的程序差异,我系统性地探究了链接的完整过程。在动态链接机制研究中,我深入理解了延迟绑定技术(PLT/GOT协作)的具体实现原理及其运行时行为。这一系列实践不仅让我清晰认识到静态链接与动态链接的技术差异,更深刻体会到链接器在程序地址空间分配、符号解析和重定位中的核心作用,从而全面掌握了链接过程对程序可执行性的关键影响。
(以下格式自行编排,编辑时删除)
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是程序在计算机系统中的动态执行实例,它包含了程序运行所需的完整执行环境,包括内存中的代码和数据、运行时栈空间、处理器寄存器状态、系统资源以及环境变量设置等,这些要素共同构成了确保程序正确运行的执行上下文。
作用:进程为应用程序提供了两个关键抽象:独立的逻辑控制流使程序看似独占处理器资源,指令执行过程连续不间断;私有的地址空间让程序仿佛独占了整个内存系统。通过这些抽象机制,进程为用户程序创造了一个虚拟的执行环境,使得每个程序都像是在系统中独立运行,无需考虑其他程序对资源的竞争和干扰。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 作用
shell是一个交互型应用级程序,代表用户运行其他程序。如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,也包括图形化的GNOME桌面环境。Shell是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。
6.2.2 处理流程
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把所有从处理的结果中用到的注释删除,并且按照下面的顺序实行命令的检查:
A.内建的命令
B. shell函数(由用户自己定义的)
C.可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.执行命令。
6.3 Hello的fork进程创建过程
当用户在命令行输入合法指令如"./hello 邢翔* 202311107* 1"时,系统会解析该命令并判定为需要执行程序,此时父进程(通常是shell)会通过调用fork函数创建一个新的子进程来运行目标程序。这个新创建的子进程会获得与父进程完全相同的用户级虚拟地址空间副本,包括代码段、数据段、堆内存、共享库以及用户栈等关键内存区域。在进程创建过程中,fork函数在子进程中返回0以标识其身份,而在父进程中则返回新创建子进程的PID以便管理。
新生成的子进程虽然几乎复制了父进程的所有状态,但两者并非完全相同。子进程不仅拥有独立于父进程的虚拟地址空间副本,还会继承父进程所有已打开的文件描述符副本。两者之间最本质的区别在于它们拥有各自独立的进程标识符(PID),这确保了操作系统能够正确区分和管理这两个进程。
具体到hello程序的执行场景,当用户在shell终端中输入"./hello"命令时,操作系统就会通过上述fork机制创建一个专门的子进程来负责运行这个hello程序,从而实现了程序的隔离执行和资源管理。
6.4 Hello的execve过程
当子进程创建后,操作系统会通过execve()系统调用将hello程序加载到子进程的地址空间。这个系统调用需要三个参数:可执行文件的路径(filename)、命令行参数数组(argv)以及环境变量数组(envp)。execve()函数在成功执行后不会返回,而是直接将控制权转移给新加载的程序。具体来说,操作系统会根据提供的文件路径,将hello程序的可执行文件内容完整地映射到子进程的地址空间,替换原有的程序映像,从而实现程序的执行环境切换。
6.5 Hello的进程执行
当在 shell 中运行 ./hello 程序时,操作系统会按照以下步骤创建和执行新进程:
1.路径解析
- 操作系统首先解析"./hello"路径,定位到磁盘上的可执行文件
- 检查文件权限,确认当前用户有执行权限
2.进程创建
- 父进程(通常是shell)调用fork()系统调用
- 内核创建子进程,复制父进程的上下文信息
- fork()在父进程中返回子进程PID,在子进程中返回0
3.进程上下文初始化
- 子进程获得父进程上下文的独立副本,包括:
- 寄存器状态
- 进程控制块(PCB)
- 虚拟内存映射
- 打开的文件描述符
4.执行环境准备
- 子进程调用execve()系统调用加载hello程序
- execve()需要三个参数:
- 可执行文件路径
- 命令行参数数组
- 环境变量数组
5.程序加载
- 内核验证可执行文件格式(如ELF格式)
- 用新程序替换子进程的地址空间
- 设置程序入口点为新程序的_start或main函数
6.进程调度
- 调度器将新进程加入就绪队列
- 根据调度算法(如时间片轮转)分配CPU时间
- 进程切换时保存和恢复上下文
7.执行阶段
- hello程序在用户态开始执行
- 需要系统服务时通过系统调用进入核心态
- 执行I/O操作时可能发生进程阻塞
8.进程终止
- 程序执行完毕时调用exit()系统调用
- 内核回收进程资源(内存、文件描述符等)
- 向父进程发送SIGCHLD信号
9.父进程处理
- 父进程通过wait()/waitpid()获取子进程状态
- 处理子进程退出状态和返回值
- 若父进程未及时wait,子进程可能成为僵尸进程
10.资源清理
- 内核最终释放进程描述符等剩余资源
- 更新系统进程表和资源统计信息
6.6 hello的异常与信号处理
按下Crtl+Z之后,进程收到SIGSTP信号,hello进程挂起并向父进程发送SIGCHLD。

通过ps命令查看进程运行状态。

jobs命令:

看到停止作业。
pstree:
显示所有进程的图。

kill:
进程被终止。

Ctrl+C发送SIGINT信号,Hello进程被终止。

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章学习了hello进程的创建、执行、种植和各个命令的执行。
(第6章2分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由程序产生的与段相关的偏移地址部分。一个逻辑地址由两部份组成,段标识符和段内偏移量。页式存储器的逻辑地址由两部分组成:页号和页内地址。[段标识符 : 段内偏移地址] 的表示形式,其中的段内偏移地址就是指逻辑地址;当我们调用printf函数时,编译器会生成一个调用printf函数的指令,这个指令中的地址就是一个逻辑地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。hello的代码产生的段中的偏移地址,加上相应段的基地址构成一个线性地址。Hello.o反汇编中每个函数可见这种表示方式。
虚拟地址是线性地址经过分页机制转换后得到的地址。操作系统使用页表将线性地址映射到虚拟地址。虚拟地址空间允许每个进程有自己独立的地址空间,从而提高了安全性和稳定性。当hello程序运行时,操作系统为其分配一个虚拟地址空间。Hello反汇编中可以看到都有固定的地址。
物理地址:物理地址是最终的内存地址,即实际的硬件内存地址。虚拟地址通过页表映射到物理地址,CPU通过内存管理单元(MMU)完成这个转换。例如,当hello程序调用printf函数时,虚拟地址通过页表转换为物理地址,CPU最终访问这个物理地址来执行函数。
7.2 Intel逻辑地址到线性地址的变换-段式管理
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)即段选择符。
在CPU中,跟段有关的CPU寄存器一共有6个:cs,ss,ds,es,fs,gs,它们保存段选择符。而同时这六个寄存器每个都有一个对应的非编程寄存器,保存的对应段描述符。
段描述符就是保存在全局描述符表或者局部描述符表中,当某个段寄存器试图通过自己的段选择符获取对于的段描述符时,会将获取到的段描述符放到自己的非编程寄存器中,这样就不用每次访问段都要跑到内存中的段描述符表中获取。
包括数据段描述符、代码段描述符等。
分段机制将逻辑地址转化为线性地址的步骤:
1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)
2)利用段选择符检验段的访问权限和范围,以确保该段可访问。
3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1 分页机制
分页机制将虚拟地址空间(也称为线性地址空间)划分为固定大小的页(通常是4KB)。操作系统维护一个页表,将虚拟地址空间中的页映射到物理内存中的页框。
7.3.2 地址转换过程
(1)地址分解
分页机制将线性地址分解为页表项(Page Table Entry, PTE)和页内偏移量。
对于一个32位的地址(假设页大小为4KB),地址分为三部分:目录项、页表项和页内偏移量。目录项为高10位,页表项为中间10位,页内偏移量为低12位。
(2)页目录和页表查找
操作系统维护一个页目录,每个进程有一个独立的页目录。页目录包含指向多个页表的指针。根据目录项查找页目录,得到对应的页表地址。在对应的页表中,根据页表项查找,得到物理页框地址。
(3)页框和页内偏移量
物理页框地址结合页内偏移量,得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
在现代操作系统中,地址转换过程中除了页表机制外,还使用了转换后备缓冲(Translation Lookaside Buffer, TLB)来加速虚拟地址(VA)到物理地址(PA)的转换。下面我们详细说明TLB和四级页表机制下虚拟地址到物理地址的转换过程。
7.4.1 TLB
TLB是一种高速缓存,用于存储最近使用的虚拟地址到物理地址的映射。通过TLB,可以避免每次地址转换都进行多级页表查找,从而加速地址转换过程。
7.4.2 四级页表机制
四级页表机制将虚拟地址转换为物理地址时,通过四级页表结构进行映射。假设我们使用64位地址空间,地址分解如下:
Page Map Level 4 (PML4):PML4是四级页表的顶层,每个进程有一个PML4表。
Page Directory Pointer Table (PDPT):PDPT是第二级页表,它的每个条目指向一个Page Directory (PD)。
Page Directory (PD):PD是第三级页表,它的每个条目指向一个Page Table (PT)。
Page Table (PT)::PT是第四级页表,它的每个条目指向一个物理页框。
每一级页表大小为512项,每项指向下一级页表或物理页。
64位虚拟地址分为以下几部分:PML4 索引高9位,PDPT 索引下一个9位,PD 索引再下一个9位,PT 索引最后一个9位,页内偏移12位
7.4.3 TLB和四级页表结合的地址转换过程
(1)从虚拟地址(VA)提取各级索引和页内偏移
假设虚拟地址为 VA,PML4 索引位于高9位,PDPT 索引位于下一个9位,PD 索引位于再下一个9位,PT 索引为最后一个9位,页内偏移& 0xFFF为低12位。
(2)CPU首先在TLB中查找虚拟地址的映射。如果命中(TLB hit),则直接使用缓存的物理地址。如果未命中(TLB miss),则需要进行页表查找。
(3)四级页表查找(在TLB miss的情况下):
使用PML4索引在PML4表中查找,找到对应的PDPT表地址。使用PDPT索引在PDPT表中查找,找到对应的PD表地址。使用PD索引在PD表中查找,找到对应的PT表地址。使用PT索引在PT表中查找,找到物理页框地址。
(4)计算物理地址:
物理地址 = 物理页框地址 + 页内偏移
7.5 三级Cache支持下的物理内存访问
在现代计算机系统中,为了提高内存访问的速度,通常会使用多级缓存(Cache)。
7.5.1 缓存层级结构
(1)一级缓存(L1 Cache)
位置:最接近CPU核心,通常分为两个部分:指令缓存(L1i)和数据缓存(L1d)。
大小:通常较小(几KB到几十KB)。
速度:非常快,延迟通常在1到3个时钟周期。
(2)二级缓存(L2 Cache)
位置:紧接L1缓存,可能是每个CPU核心独有,也可能是每两个核心共享。
大小:比L1大(几百KB到几MB)。
速度:稍慢于L1缓存,延迟通常在10到20个时钟周期。
(3)三级缓存(L3 Cache)
位置:通常为整个处理器共享,所有核心都可以访问。
大小:较大(几MB到几十MB)。
速度:慢于L2缓存,延迟通常在几十到上百个时钟周期。
7.5.2 缓存访问过程
当CPU需要访问某个物理地址时,三级缓存架构的访问过程如下:
(1)CPU发出内存访问请求
CPU生成一个物理地址来访问数据(假设地址为PA)。
(2)L1缓存查找
CPU首先在L1缓存中查找PA。如果命中(hit),L1缓存返回数据给CPU,访问结束。如果未命中(miss),请求发送到L2缓存。
(3)L2缓存查找
在L2缓存中查找PA。如果命中(hit),L2缓存返回数据给CPU,并且可能将数据复制到L1缓存。如果未命中(miss),请求发送到L3缓存。
(3)L3缓存查找
在L3缓存中查找PA。如果命中(hit),L3缓存返回数据给CPU,并且可能将数据复制到L2和L1缓存。如果未命中(miss),请求发送到主内存(DRAM)。
(4)内存访问
在L3缓存未命中的情况下,访问请求发送到主内存。主内存返回数据给L3缓存,并且可能复制到L2和L1缓存。最终,数据从L1缓存返回给CPU。
7.5.3 缓存一致性
为了确保多核处理器中所有核心对内存的一致视图,通常采用缓存一致性协议(如MESI、MOESI)。这些协议管理缓存之间的数据一致性,确保当一个核心修改缓存中的数据时,其他核心能够看到最新的数据。
7.6 hello进程fork时的内存映射
当一个进程调用fork()时,操作系统会创建一个新的进程,这个新进程被称为子进程。子进程几乎完全复制了父进程的虚拟地址空间,包括代码段、数据段、堆、栈等。现代操作系统使用“写时复制”(Copy-On-Write, COW)技术来优化这个过程。下面我们详细讨论fork()时内存映射的过程。
7.6.1 fork()时的内存映射
(1)创建子进程:操作系统为子进程创建一个新的进程控制块(PCB),并将父进程的所有资源(文件描述符、内存映射等)复制到子进程。
(2)虚拟地址空间复制:子进程获得父进程的完整虚拟地址空间的副本,但是这时候并不立即复制实际的物理内存。
(3)写时复制(COW):父进程和子进程共享相同的物理内存页,直到有一个进程试图修改某个页。只有在此时,操作系统才会复制该页。
7.6.2 写时复制(COW)具体过程
fork()调用时,子进程会继承父进程的虚拟地址空间,但并不会立即复制所有物理内存。通过写时复制技术,父子进程共享相同的物理页,直到有写操作发生时,才会分配新的物理页。这样可以高效地管理内存,并减少 `fork()` 的开销。这种机制对于创建新进程和高效利用内存资源非常重要。
(1)写操作触发页错误:尝试写入只读页会触发页错误。
(2)操作系统处理页错误:操作系统检测到这是写时复制情况。
(3)复制物理页:操作系统为写入操作分配一个新的物理页,并将旧页的数据复制到新页。
(4)更新页表:更新进程的页表,使得该虚拟页映射到新的物理页,并将页标记为可写。
(5)完成写操作:进程继续执行写操作。
7.7 hello进程execve时的内存映射
int execve(const char *pathname, char *const argv[], char *const envp[]);
pathname新程序的路径。
argv[]:传递给新程序的命令行参数。
envp[]:传递给新程序的环境变量。
1、 输入 ./hello lmy 2022113064 3
2、 execve加载hello程序后,设置栈,将控制传递给hello程序的主函数。
3、 删除已存在的用户区域
4、 映射新的私有区域。代码和初始化数据映射到.text和.data区(执行可执行文件提供),.bss映射到匿名文件,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点。栈中从栈底到栈顶是参数和环境字符串,再往上是指针数组,每个指针指向刚才的环境变量和参数字符串。栈顶是系统启动函数libc_start_main的栈帧和预留的未来函数的栈帧。
当hello进程调用execve()时,整个进程的内存映射发生了彻底的变化。旧的地址空间被清除,新的程序被加载到地址空间中。通过这种方式,execve()可以在当前进程的上下文中运行一个新的程序,而不需要创建新的进程。这样不仅节省了资源,还允许新程序继承当前进程的许多属性,如进程ID、环境变量等。该函数成功运行正确运行时不返回。逻辑控制流交给要运行的程序
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障(Page Fault)
虚拟内存在DRAM缓存不命中即为缺页故障。
7.8.2 缺页中断处理
缺页中断处理:触发缺页异常时启动缺页处理程序
1、缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
2、缺页处理程序页面调入新的页面,并更新内存中的PTE
3、缺页处理程序返回到原来的进程,再次执行导致缺页的命令。
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)
7.10本章小结
本章在hello的具体例子中查看了程序执行的内存管理,存储器从逻辑地址到线性地址到物理地址的变换。也了解了cache、动态存储分配管理等机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章 选做 0分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello 的一生中发生的重大事件:
1. 预处理。hello.c文本翻译为 hello.i 文本,预处理器 cpp 替换掉源码中的头文件和宏。
2. 编译。hello.i 转变为 hello.s,ccl 编译器把源码转变为汇编码。
3.汇编。as 汇编器把汇编码转换为机器码,生成了 hello.o 可重定位目标文件,
在这一阶段还不能知道对全局符号或外部函数的引用的最终位置,所以汇编器产生了重定位条目,方便下一阶段的链接器的工作。
4.链接。静态链接,把外部函数的代码(通常是后缀名为.lib 和.a 的文件),
添加到可执行文件中;动态链接的做法正好相反,它会设置过程链接表 PLT 和全局偏移量表 GOT 等,只在运行时动态引用相关代码。最后生成了 hello 可执行文件。
5.在系统上运行。hello 程序被 shell 父进程 fork、execve,内核为 hello 进程分配时间片执行逻辑控制流。hello 进程运行过程中响应可能的异常和信号处理。
6. Pass away. 程序运行结束后,shell 父进程回收这个僵死子进程,内核会从
系统中删除它的所有痕迹
(结论0分,缺失-1分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源程序
hello.i 预处理后的修改的C程序
hello.s 汇编程序
hello.o 可重定位目标文件
hello 可执行目标文件
Objdump_hello_o hello.o的反汇编文件
Objdump_hello hello的反汇编文件
elf_hello hello的ELF格式
elf_hell_.o hello.o的ELF格式
(附件0分,缺失 -1分)
参考文献
- https://github.com.
- Bryant, Randal E., and David R. O'Hallaron. "Computer Systems: A Programmer's Perspective." Pearson, 2016.
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2301_80405688/article/details/148052133




