提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 摘要
- [第1章 概述](#第1章 概述)
-
- [1.1 Hello简介](#1.1 Hello简介)
- [1.2 环境与工具](#1.2 环境与工具)
- [1.3 中间结果](#1.3 中间结果)
- [1.4 本章小结](#1.4 本章小结)
- [第2章 预处理](#第2章 预处理)
-
- [2.1 预处理的概念与作用](#2.1 预处理的概念与作用)
- 2.2在Ubuntu下预处理的命令
- [2.3 Hello的预处理结果解析](#2.3 Hello的预处理结果解析)
- [2.4 本章小结](#2.4 本章小结)
- [第3章 编译](#第3章 编译)
-
- [3.1 编译的概念与作用](#3.1 编译的概念与作用)
- [3.2 在Ubuntu下编译的命令](#3.2 在Ubuntu下编译的命令)
- [3.3 Hello的编译结果解析](#3.3 Hello的编译结果解析)
- [3.4 本章小结](#3.4 本章小结)
- [第4章 汇编](#第4章 汇编)
-
- [4.1 汇编的概念与作用](#4.1 汇编的概念与作用)
- [4.2 在Ubuntu下汇编的命令](#4.2 在Ubuntu下汇编的命令)
- [4.3 可重定位目标elf格式](#4.3 可重定位目标elf格式)
- [4.4 Hello.o的结果解析](#4.4 Hello.o的结果解析)
- [4.5 本章小结](#4.5 本章小结)
- [第5章 链接](#第5章 链接)
-
- [5.1 链接的概念与作用](#5.1 链接的概念与作用)
- [5.2 在Ubuntu下链接的命令](#5.2 在Ubuntu下链接的命令)
- [5.3 可执行目标文件hello的格式](#5.3 可执行目标文件hello的格式)
- [5.4 hello的虚拟地址空间](#5.4 hello的虚拟地址空间)
- [5.5 链接的重定位过程分析](#5.5 链接的重定位过程分析)
- [5.6 hello的执行流程](#5.6 hello的执行流程)
- [5.7 Hello的动态链接分析](#5.7 Hello的动态链接分析)
- [5.8 本章小结](#5.8 本章小结)
- [第6章 hello进程管理](#第6章 hello进程管理)
-
- [6.1 进程的概念与作用](#6.1 进程的概念与作用)
- [6.2 简述壳Shell-bash的作用与处理流程](#6.2 简述壳Shell-bash的作用与处理流程)
- [6.3 Hello的fork进程创建过程](#6.3 Hello的fork进程创建过程)
- [6.4 Hello的execve过程](#6.4 Hello的execve过程)
- [6.5 Hello的进程执行](#6.5 Hello的进程执行)
- [6.6 hello的异常与信号处理](#6.6 hello的异常与信号处理)
- 6.7本章小结
- [第7章 hello的存储管理](#第7章 hello的存储管理)
-
- [7.1 hello的存储器地址空间](#7.1 hello的存储器地址空间)
- [7.2 Intel逻辑地址到线性地址的变换-段式管理](#7.2 Intel逻辑地址到线性地址的变换-段式管理)
- [7.3 Hello的线性地址到物理地址的变换-页式管理](#7.3 Hello的线性地址到物理地址的变换-页式管理)
- [7.4 TLB与四级页表支持下的VA到PA的变换](#7.4 TLB与四级页表支持下的VA到PA的变换)
- [7.5 三级Cache支持下的物理内存访问](#7.5 三级Cache支持下的物理内存访问)
- [7.6 hello进程fork时的内存映射](#7.6 hello进程fork时的内存映射)
- [7.7 hello进程execve时的内存映射](#7.7 hello进程execve时的内存映射)
- [7.8 缺页故障与缺页中断处理](#7.8 缺页故障与缺页中断处理)
- 7.9动态存储分配管理
- 7.10本章小结
- 结论
- 附件
- 参考文献
计算机系统
大作业
题 目 程序人生-Hello's P2P
专 业 计算机与电子通信类
学 号 2023111638
班 级 23L0502
学 生 白昊阳
指 导 教 师 刘宏伟
计算机科学与技术学院
摘要
`` 本报告以"程序人生-Hello's P2P"为核心案例,系统剖析了一个C语言程序从源代码到可执行文件(P2P,Program to Process)以及其在计算机系统中的完整生命周期(020,Zero to Binary)。通过Ubuntu环境下的GCC工具链,实验逐步完成了预处理、编译、汇编、链接等过程,生成了中间文件(如.i、.s、.o)及最终可执行文件hello。结合ELF格式解析、反汇编分析、虚拟地址空间映射等技术,深入探讨了程序在编译与链接阶段的重定位机制、动态链接实现以及可执行文件的结构特征。进一步结合进程管理,分析了fork、execve等系统调用的执行流程,并通过信号处理实验验证了程序运行中的异常响应机制。在存储管理方面,从逻辑地址到物理地址的变换过程中,详细阐述了段式管理、页式管理、TLB与多级页表的作用,以及三级Cache对内存访问的优化。此外,结合Unix I/O接口,解析了printf和getchar的实现原理,揭示了用户态与内核态在I/O操作中的交互机制。本实验通过理论与实践结合,全面展示了计算机系统各层组件的协同工作,深化了对程序生命周期、进程调度、存储管理和I/O管理的理解,为系统级编程与优化提供了重要参考。
关键词:计算机系统;预处理与编译;链接与重定位;进程管理;存储管理。
(此文章为2023111638白昊阳的计算机系统大作业,以此证明该作业以上传到互联网。)
第1章 概述
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
1.1 Hello简介
Hello的"P2P"(Program to Process)与"020"(Zero to Binary)过程体现了其从静态源代码到动态进程的完整生命周期。
P2P(Program to Process)
Hello的P2P过程描述了其从静态程序到动态进程的完整转化。首先,预处理器对hello.c进行宏展开(如#include包含头文件)和条件编译处理,生成扩展后的hello.i;随后,编译器将hello.i转换为汇编代码hello.s,完成数据类型解析(如整型、指针)、控制流逻辑(如if、for)的底层实现;接着,汇编器将hello.s转换为可重定位目标文件hello.o,包含机器指令、符号表及ELF格式的元信息(如代码段.text和数据段.data);链接器进一步将hello.o与动态库(如libc.so)合并,解析外部符号(如printf地址),完成重定位并生成可执行文件hello;最终,用户通过Shell执行程序时,Bash调用fork()创建子进程,并通过execve()加载hello到子进程内存空间,替换其代码段与数据段,由CPU调度执行,完成从程序到进程的转化。
020(Zero to Binary)
Hello的020过程展现了其从文本源代码到二进制执行的完整生命周期。初始的文本文件hello.c("Zero")通过编译工具链逐步转换为二进制可执行文件("Binary"):预处理处理宏与条件指令,编译生成汇编代码,汇编生成机器指令,链接合并库并分配地址。运行时,操作系统通过页式管理将虚拟地址(VA)映射为物理地址(PA),借助TLB和四级页表加速地址转换,三级Cache优化内存访问效率;CPU执行指令时完成逻辑运算、函数调用及I/O操作(如printf输出)。程序结束后,操作系统回收进程资源(如内存页、文件描述符),销毁进程上下文,实现"从零到零"的闭环。整个过程依赖编译器、链接器、操作系统(进程/存储/I/O管理)与硬件的协同,体现了计算机系统多层抽象的深度融合。。
1.2 环境与工具
硬件环境:
处理器:13th Gen Intel® Core™i9-13900H 2.70 GHz
机带RAM:16.0GB(15.8 GB 可用)
系统类型:64位操作系统,基于x64的处理器
笔和触控:没有可用于此显示器的笔或触控输入
软件环境:Windows11家庭版 64位 版本号24H2,VMware Workstation Pro,ubuntu-24.04.2-desktop-amd64.
开发与调试工具:Visual Studio 2022;vim,gidit ,objdump,edb,gcc,readelf等开发工具
1.3 中间结果
hello.c:原始hello程序的C语言代码
hello.i:预处理过后的hello代码
hello.s:由预处理代码生成的汇编代码
hello.o:二进制目标代码
hello:进行链接后的可执行程序
hello.o.disasm:反汇编hello.o得到的反汇编文件
hello.disasm:反汇编hello可执行文件得到的反汇编文件。
1.4 本章小结
本章首先系统阐述了hello程序的P2P(Program to Process)与020(Zero to Binary)全流程,从整体设计框架到具体实现策略(如预处理、编译、链接、进程加载等关键步骤),详细剖析了程序从静态代码到动态执行的转化逻辑。随后,全面梳理了实验所需的硬件环境(如CPU架构、内存配置)、软件支持(Ubuntu系统、GCC工具链)、开发工具链(预处理器、汇编器、链接器等)以及实验生成的中间产物文件(如预处理后的.i文件、编译生成的.s文件、汇编生成的.o文件等),并对其功能与作用进行了分类解析。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是C语言编译流程的初始阶段,主要负责处理源代码中以#开头的预处理指令(如#include、#define、#ifdef等),通过文本替换、条件筛选和文件合并等操作,生成规范化的中间文件(.i文件)。其本质是对源代码进行文本级加工,消除注释、简化代码结构,并为后续编译阶段提供标准化的输入。
作用:预处理的核心作用在于通过#include指令将外部头文件(如stdio.h)的内容插入当前代码,确保函数声明与宏定义的可见性;利用#define对宏标识符进行文本替换,简化代码维护并提升可读性;借助#ifdef、#if等条件编译指令,动态包含或排除特定代码块,实现跨平台适配与功能模块化;通过删除注释(如//、/.../)和冗余空白符生成无干扰的中间文件(.i),减少编译器的解析负担;同时支持#pragma等特殊指令,控制编译优化与警告提示,从而为后续编译阶段提供规范化的代码输入。
2.2在Ubuntu下预处理的命令
打开VMware虚拟机的终端,输入gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i 并回车执行,生成hello.i预处理文件。

图2.2 生成预处理程序hello.i的过程
2.3 Hello的预处理结果解析
2.3 Hello的预处理结果解析
部分预处理文件程序见下图:

图2.3-1 hello.i的前若干行预处理指令

图2.3-2 hello.i最后的main函数部分
在Linux虚拟机下打开hello.i,对比源程序和预处理后的程序可以看出,对头文件解析的部分已经完成,预处理指令被扩展到三千余行,程序最后对主函数的解析和hello.c的main函数部分完全相同,说明预处理已经完成。
预处理程序在main函数出现之前的大段篇幅均是对头文件进行展开,该过程不会对头文件中的内容进行任何的转换,而是简单的解释,替换和复制。当预处理器遇到#include <头文件>时,会按照系统头文件搜索路径查找对应的文件,将文件内容逐字复制到当前源代码中,替换#include指令的位置,同时递归处理嵌套包含的头文件。
·头文件<stdio.h>
这是标准输入输出库头文件,定义了文件操作(如FILE结构体)和I/O函数(如printf、scanf、fopen等)的原型及宏。预处理时,会将stdio.h中声明的函数原型、标准流(stdin、stdout、stderr)以及缓冲区相关常量插入代码,使编译器识别这些符号。该头文件通常位于/usr/include/stdio.h,可能进一步包含内部头文件(如__FILE定义)。
·头文件<unistd.h>
这是POSIX操作系统API头文件,提供了与系统调用相关的接口,如进程控制(fork、exec)、文件操作(read、write)、系统配置(sysconf)等。预处理时,会展开与当前系统兼容的POSIX函数声明(如pid_t类型)和宏(如STDIN_FILENO)。该头文件的位置通常为/usr/include/unistd.h,其内容可能通过条件编译(如#ifdef)适配不同UNIX变体。
·头文件<stdlib.h>
这是标准工具库头文件,定义了内存管理(malloc、free)、程序控制(exit、abort)、随机数生成(rand)、类型转换(atoi)等函数的原型及常量(如EXIT_SUCCESS)。预处理展开后,这些函数的声明和工具宏(如NULL)会被插入代码,同时可能包含对系统底层类型(如size_t)的依赖。该头文件通常位于/usr/include/stdlib.h,可能间接包含其他内部头文件(如stddef.h)。
2.4 本章小结
本章系统阐述了C程序预处理阶段的核心机制与实践验证,通过gcc -E命令对hello.c进行预处理,生成了包含完整头文件内容与宏展开的hello.i文件,验证了预处理指令的文本替换特性。预处理的核心作用体现在通过#include递归展开<stdio.h>(I/O函数声明)、<unistd.h>(POSIX系统接口)、<stdlib.h>(通用工具库)等头文件,确保编译器识别标准函数原型与数据类型,同时借助宏替换和条件编译优化代码可维护性与跨平台兼容性。预处理后代码量显著增加,原始注释被删除,main函数前插入大量头文件声明(如printf、fork的原型),直观展示了头文件的逐字复制机制。Ubuntu环境下GCC工具链的高效性得以验证,为后续编译阶段提供了标准化输入,揭示了预处理在程序生命周期中的基础性作用。
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是C程序构建流程的核心阶段,其任务是将预处理后的中间代码(.i文件)转换为与目标机器架构匹配的汇编代码(.s文件)。在此过程中,编译器(如GCC的cc1组件)通过词法分析、语法分析、语义分析等步骤,将高级C语言代码解析为抽象语法树(AST),并基于目标机器的指令集和优化策略生成等效的底层汇编指令。同时,编译器会执行代码优化(如常量折叠、循环展开等),处理变量类型与作用域(如整型、指针的存储分配),最终生成符合汇编器输入规范的文本化汇编代码。
编译的作用:
编译阶段的核心作用在于实现从高级语言到低级语言的语义等价转换,为后续汇编和链接提供可执行的机器指令基础。能够验证代码的语法合法性(如括号匹配、语句结构)和类型一致性(如函数参数匹配、变量声明作用域),确保程序逻辑的正确性。通过静态分析优化程序性能(如删除冗余计算、简化控制流),提升生成的汇编代码效率。根据目标处理器的指令集(如x86-64)生成特定汇编指令(如mov、call),为硬件执行提供直接操作接口。维护符号表(如函数名、全局变量地址),支持后续链接阶段的重定位与地址绑定。
3.2 在Ubuntu下编译的命令
根据hello.i直接编译生成hello.s的命令如下:

图3.2-1 由hello.i生成hello.s的终端截图
3.3 Hello的编译结果解析
3.3.1 数据类型处理
- 常量:
·字符串常量:.LC0(错误提示字符串"用法: Hello 学号 姓名 手机号 秒数!")和.LC1(格式化字符串"Hello %s %s %s\n")存储在.rodata只读数据段,编码为UTF-8。例如,.LC0的字符串通过八进制转义表示中文字符。
图3.3.1-1 汇编代码:字符串常量.LC0
·数字常量:如argc比较值5(cmpl $5, -20(%rbp))、循环终值9(cmpl $9, -4(%rbp))直接嵌入指令操作数,使用立即数形式。
图3.3.1-2 汇编代码:数字常量
- 局部变量:
·i:存储于栈帧偏移-4(%rbp)处,类型为int,通过movl $0, -4(%rbp)显式初始化为0。
图3.3.1-3 汇编代码:局部变量i
·argc与argv:argc(int)存储于-20(%rbp),argv(char)存储于-32(%rbp),均通过寄存器%edi和%rsi传递后存入栈帧。
图3.3.1-4 汇编代码:局部变量argc与argv
- 指针与数组:
·argv[]访问:argv[1]对应8(%rax)(argv基址+8字节偏移),通过movq -32(%rbp), %rax获取argv基地址,addq $8, %rax计算偏移,movq (%rax), %rsi加载实际指针值。
图3.3.1-5 汇编代码:数组argv访问
·指针运算:argv[4]的地址通过addq $32, %rax(argv基址+32字节偏移)计算,反映char数组的连续内存布局。
图3.3.1-5 汇编代码:指针运算
3.3.2 运算符与表达式
- 算术操作:
·加法:addq $24, %rax用于计算argv[3]的地址(argv基址+24字节偏移),对应argv[3]。
图3.3.2-1 汇编代码:算术操作-加法
·自增:addl $1, -4(%rbp)实现i++,直接在内存中修改循环变量值。
图3.3.2-2 汇编代码:算术操作-自增
- 关系操作:
·条件判断:cmpl $5, -20(%rbp)比较argc与5,je .L2实现if(argc != 5)的条件跳转。
图3.3.2-3 汇编代码:关系操作-条件判断
·循环终止条件:cmpl $9, -4(%rbp)检查i <= 9,jle .L4控制for循环执行次数。
图3.3.2-4 汇编代码:关系操作-循环终止条件
- 类型转换:
·显式转换:atoi(argv[4])通过call atoi将字符串指针转换为整数,结果存入%eax后传递给sleep。
·隐式转换:movl $0, %eax在调用printf前清空%eax,确保浮点参数传递正确(此处无浮点参数,但遵循调用约定)。
3.3.3 控制转移
- 分支结构:
·if(argc != 5):
通过cmpl $5, -20(%rbp)和je .L2实现,若条件不满足,调用puts输出错误信息并退出(call exit)。
图3.3.3-1 汇编代码:控制转移-分支结构
·for循环:
.L3和.L4标签构成循环体。movl $0, -4(%rbp)初始化i=0,jmp .L3跳转至条件判断,jle .L4控制循环继续。
图3.3.3-2 汇编代码:控制转移-for循环
- 函数调用与返回:
·函数调用:
call printf传递参数时,%edi存放格式字符串地址(.LC1),%rsi、%rdx、%rcx依次存放argv[1]、argv[2]、argv[3]的指针值。
图3.3.3-3 汇编代码:函数调用
·函数返回:
movl $0, %eax设置返回值0,leave和ret恢复栈帧并返回。
图3.3.3-4 汇编代码:函数返回
3.3.4 函数操作与参数传递
- 参数传递:
·值传递:sleep(atoi(argv[4]))中,atoi返回值通过%edi传递(movl %eax, %edi),遵循x86-64调用约定(前6个整型参数使用%rdi、%rsi等)。
·地址传递:argv作为指针数组,其基地址通过%rsi传递并存入栈帧(movq %rsi, -32(%rbp))。 - 局部变量存储:
栈帧分配:subq $32, %rsp为局部变量(i、argc、argv)分配栈空间,每个变量通过固定偏移访问(如-4(%rbp)对应i)。
3.3.5 其他操作
- 复合赋值:
i++通过addl $1, -4(%rbp)直接实现,无需显式赋值语句。 - 逻辑操作:
条件判断隐含逻辑运算,如argc != 5通过cmpl和je组合实现。 - 控制流优化:
循环展开未显式出现,但for循环的终止条件i <= 9通过cmpl $9和jle高效实现。
3.4 本章小结
本章详细分析了从预处理文件hello.i到汇编文件hello.s的编译过程,通过GCC工具链生成了x86-64架构的汇编代码。编译阶段的核心任务包括数据类型处理(如常量、局部变量和指针的存储分配)、运算符与表达式转换(算术、关系和逻辑操作)、控制流实现(分支、循环和函数调用)以及参数传递机制(遵循x86-64调用约定)。生成的汇编代码中,如.LC0和.LC1字符串常量存储在.rodata段,局部变量通过栈帧管理,控制转移通过条件跳转指令(je、jle)实现,函数调用则通过寄存器传递参数(如%edi、%rsi)。此外,编译器优化了指令生成(如直接嵌入立即数、简化循环逻辑),为后续汇编阶段提供了高效的机器指令基础。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编是指将汇编语言(.s 文件)转换为机器语言目标文件(.o)的过程,由汇编器(如 as)完成。在这一阶段,汇编器逐行解析汇编指令(如 mov、call),将其转换为处理器可直接执行的二进制机器码,并按照 ELF 格式生成可重定位目标文件,其中包含代码段(.text)、数据段(.data、.rodata)、符号表以及重定位信息等关键组成部分,为后续链接阶段提供基础模块。
作用:
汇编的核心作用在于实现从汇编指令到机器码的精确转换,并构建可重定位的中间目标文件。它不仅将符号(如函数名、全局变量)记录在符号表中供链接器解析,还通过重定位信息支持跨模块的地址绑定,使不同源文件编译后的目标文件能够正确组合。此外,汇编过程生成的 .o 文件保留了程序的机器指令、静态数据以及调试信息,为最终生成可执行文件或动态链接库奠定基础。
4.2 在Ubuntu下汇编的命令
在终端输入gcc -c hello.s -o hello.o并执行,生成hello.o文件

图4.2 生成hello.o
4.3 可重定位目标elf格式
4.3.1 查看elf头信息
hello.o无法直接查看,输入命令readelf -h hello.o来解析elf文件头。

图4.3.1 查看elf头
ELF头是ELF(Executable and Linkable Format)文件的元数据起始部分,定义了文件的整体属性和组织结构。该文件为64位可重定位目标文件(类型REL),采用小端序(little endian)数据格式,符合UNIX System V的ABI标准。ELF魔数7f 45 4c 46(对应ASCII字符"ELF")验证了文件格式的有效性。文件架构为x86-64,入口点地址为0x0,表明该目标文件尚未完成链接,执行地址需后续确定。程序头表(Program Headers)起点和数量均为0,说明此文件不包含加载到内存所需的程序头信息(因可重定位文件无需直接执行)。节头表(Section Headers)起始于文件第1080字节处,包含14个节区(如.text、.data),每个节头条目大小为64字节,通过索引13定位节头名称字符串表。ELF头总长64字节,无附加标志(Flags为0x0),符合标准可重定位文件的特征,为链接器提供代码段、符号表及重定位信息等关键数据。
4.3.2 查看section信息
在终端输入readelf -S hello.o查看节信息,结果如下:


图4.3.2-1 查看section信息
可以发现,hello.o中一共有14个节。在这些重定位条目中,有两个对应rodata节中的数据地址,显然它们是printf使用的那两个字符串地址。另外6个重定位条目都是被call指令调用过的函数地址。

图4.3.2-2 各节信息
4.3.3 查看符号表
在终端输入readelf -s hello.o查看符号表,结果如下:

图4.3.3 查看符号表信息
在符号表中,Num为某个符号的编号,Name是符号的名称。Size表示他是一个位于.text节中偏移量为0处的函数。由上图可知main函数名称这个符号变量是本地的。
4.3.4 查看可重定位信息
在终端输入readelf -s hello.o查看符号表,结果如下:

图4.3.4 查看可重定位信息
在列出的信息中,偏移量表示需要被修改的引用的节偏移,符号值标识被修改引用应该指向的符号。类型告知连接器如何修改新的引用,加数是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。
4.4 Hello.o的结果解析
在终端输入objdump -d -r hello.o对hello.o进行反汇编,结果如下:



图4.4 反汇编代码
第三章得到的hello.s如下:
.file "hello.c"
.text
.section .rodata
.align 8
.LC0:
.string "\347\224\250\346\263\225: Hello 2023111638 \347\231\275\346\230\212\351\230\263 19917686151 7\357\274\201"
.LC1:
.string "Hello %s %s %s\n"
.text
.globl main
.type main, @function
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $5, -20(%rbp)
je .L2
movl $.LC0, %edi
call puts
movl $1, %edi
call exit
.L2:
movl $0, -4(%rbp)
jmp .L3
.L4:
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rcx
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
movl $.LC1, %edi
movl $0, %eax
call printf
movq -32(%rbp), %rax
addq $32, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi
movl %eax, %edi
call sleep
addl $1, -4(%rbp)
.L3:
cmpl $9, -4(%rbp)
jle .L4
call getchar
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size main, .-main
.ident "GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
对比如下:
·分支跳转:
类型 反汇编代码 hello.s
代码部分
对比说明 机器语言经过翻译直接通过相对地址进行跳转 使用跳转指令只需要在后面加上标识符便可以跳转到标识符所在的位置
·函数调用:
类型 反汇编代码 hello.s
代码部分
对比说明 目标地址是当前指令的下一条指令地址,因为链接前函数地址不确定 直接标注函数名称
·伪指令:
类型 反汇编代码 hello.s
代码部分
对比说明 伪指令全部消失 伪指令存在
·立即数:
类型 反汇编代码 hello.s
代码部分
对比说明 二进制数转十六进制数 十进制数转二进制数
表4.4 反汇编代码与hello.s比对
4.5 本章小结
本章通过汇编阶段将hello.s转换为可重定位目标文件hello.o,并深入分析了其ELF格式与机器指令的生成过程。通过gcc -c命令调用汇编器,将汇编代码精确转换为二进制机器码,生成包含代码段(.text)、数据段(.data、.rodata)、符号表及重定位信息的ELF文件。使用readelf工具解析ELF头信息,确认其为64位可重定位文件(类型REL),采用小端序,无程序头表,节头表起始于1080字节,包含14个节区(如.text、.symtab),符号表记录了main等函数的本地符号信息。通过objdump反汇编hello.o并与hello.s对比,发现分支跳转指令通过相对地址实现,函数调用地址在链接前未确定,伪指令被移除,立即数以十六进制形式嵌入指令。重定位条目表明外部符号需在链接时解析。
第5章 链接
5.1 链接的概念与作用
概念:
链接是将多个可重定位目标文件(如.o文件)和库文件(如静态库.a或动态库.so)合并生成可执行文件或共享库的过程。链接器(如ld)通过符号解析确定全局符号(如函数名、变量名)的最终地址,并完成重定位------调整代码中的地址引用以指向合并后的正确位置。链接过程还涉及合并不同目标文件中的代码段(.text)、数据段(.data、.bss)等节区,并最终生成符合操作系统要求的可执行文件(如ELF格式)。
作用:
链接的核心作用在于整合多个可重定位目标文件和库资源,构建完整的可执行程序。通过符号解析,链接器确保所有外部引用(如函数printf)找到唯一的定义,消除未定义或冲突的符号;通过地址重定位,调整代码中的相对地址或占位符,使其指向合并后的实际内存位置,保证指令正确执行;通过合并代码段(.text)、数据段(.data)等节区,形成统一的内存布局,并处理静态库的嵌入或动态库的运行时加载引用,最终生成符合操作系统要求的可执行文件。链接过程解决了模块化编程的依赖问题,将分散的编译单元转化为可直接运行的二进制程序,是程序从源码到执行的最终桥梁。
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.2 链接生成hello
5.3 可执行目标文件hello的格式
使用readelf查看hello文件的ELF头,节头部表,符号表:
5.3.1 ELF头
在终端中输入readelf -h hello并回车,查看hello文件的ELF头:

图5.3.1 hello的elf头
hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件。
5.3.2 section头
在终端中输入readelf -S hello并回车,查看hello文件的ELF头:



图5.3.2 hello的section头
Section表对hello中所有信息进行了声明,包括了大小、偏移量、起始地址以及数据对齐方式等信息。根据始地址和大小就可以计算节头部表中的每个节所在的区域了。
5.3.3 符号表
在终端中输入readelf -s hello并回车,查看hello文件的符号表:



图5.3.3 hello的符号表
5.3.4 可重定位信息
输入指令readelf -r hello并回车,查看hello文件的可重定位信息:

图5.3.4 hello的可重定位信息
5.4 hello的虚拟地址空间
5.4 hello的虚拟地址空间
使用edb工具调节,选择hello文件并open,界面如下:

图5.4-1 hello的edb初始界面
hello的可执行部分起始地址为0x401000,结束地址为0x4011c0



图5.4-2 hello的代码段
5.5 链接的重定位过程分析
在终端输入命令objdump -d -r hello并回车,查看hello可执行文件的反汇编条目,结果如下:

图5.5-1 hello的部分反汇编代码
注意到hello的反汇编代码要长得多,分析区别如下:
·虚拟地址不同
类型 hello反汇编 hello.o反汇编
反汇编代码
不同点 虚拟地址从0x401000开始 虚拟地址从0开始
这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。
·反汇编节数不同
类型 hello反汇编 hello.o反汇编
不同点 在main函数之前加上了链接过程中重定位而加入的各种在hello中被调用的函数、数据,增加了.init,.plt,.plt.sec等节的反汇编代码。 只有.text节,里面只有main函数的反汇编代码
·跳转指令不同
类型 hello反汇编 hello.o反汇编
不同点 具体的地址 主要是汇编代码块前的标号
表5.5 反汇编代码hello与hello.o比对
5.6 hello的执行流程
使用edb执行hello

图5.6 hello使用动态链接库
执行流程总括:
- 内核加载 → 2. 动态链接器解析 → 3. _start (0x4010e0) → 4. __libc_start_main →
- main (0x401144) → 6. exit (0x401060) → 7. 内核回收
5.6.1. 内核加载与动态链接器介入
加载地址:内核通过 execve 系统调用将 hello 加载到内存,代码段起始地址为 0x401000(ELF 头中 Entry point address)。
动态链接器调用:动态链接器 ld-linux-x86-64.so.2(地址由 ELF 程序头指定)加载共享库(如 libc.so.6)。
解析外部符号(如 printf、exit)的实际地址,更新全局偏移表(GOT)和过程链接表(PLT)。
5.6.2. 入口函数 _start
地址:0x4010e0(通过反汇编或 readelf -h 查看)。
操作:初始化栈指针 %rsp,设置进程运行环境。
调用 __libc_start_main,传递 main 函数地址(0x401144)及参数。
5.6.3. __libc_start_main 初始化
地址:位于 libc.so.6 中,通过动态链接绑定。
功能:初始化全局变量和线程局部存储(TLS)。注册退出处理函数(如 atexit 注册的清理函数)。调用用户 main 函数(0x401144)。
5.6.4. main 函数执行
地址:0x401144(通过 objdump -d hello 确认)。
流程:参数检查:比较 argc 是否等于 5,若不等则调用 puts(0x401030)输出错误信息,并调用 exit(1)(0x401060)。
循环逻辑:调用 printf(0x401030)输出参数,调用 sleep(0x401050)等待指定秒数,调用 getchar(0x401040)等待输入。
返回:通过 movl $0, %eax 设置返回值 0。
5.6.5. 退出流程 exit
地址:0x401060(PLT 表项,动态解析后跳转到 libc 中的 exit 实现)。
操作:执行注册的退出处理函数(如关闭文件流),调用 _exit 系统调用(syscall 指令),内核回收进程资源。
5.6.6. 关键跳转与符号地址
动态函数调用:printf@plt:PLT 表项地址 0x401030,首次调用触发动态链接器解析实际地址。
exit@plt:PLT 表项地址 0x401060,解析后跳转到 libc 中的 exit 函数。
系统调用:_exit 最终触发 syscall,进入内核态终止进程。
5.7 Hello的动态链接分析
5.7.1 动态链接的基本机制
动态链接通过 PLT(过程链接表) 和 GOT(全局偏移表) 实现函数地址的延迟绑定:
PLT:存储跳转到 GOT 的指令,初次调用时触发动态链接器解析实际函数地址。
GOT:初始指向 PLT 中的解析代码,解析后更新为实际函数地址。
5.7.2 动态链接前的初始状态
·查看动态节(.dynamic):
输入命令 readelf -d hello,查看动态链接依赖的库和符号表:显示 hello 依赖 libc.so.6。

图5.7.1-1 查看动态节
·查看 GOT 初始值:
使用 edb 打开 hello,在内存视图中定位 .got.plt 节(地址示例:0x404000)。
初始状态:GOT 条目指向 PLT 中的解析代码(如 0x401020)。

图5.7.1-2 查看GOT初始值
5.7.3 动态链接的详细流程
在程序首次调用动态链接函数时,其执行路径并非直接跳转至目标函数,而是通过以下步骤完成地址解析与绑定:
·PLT入口调用
程序首先跳转至目标函数对应的PLT条目(如printf@plt)。PLT首条指令尝试通过GOT条目间接跳转,但初始状态下所有GOT条目均指向对应PLT条目的第二条指令,导致控制流立即返回到PLT内部继续执行。
·参数压栈与PLT[0]跳转
PLT条目随后将函数的唯一标识符(符号索引)压入栈中,并跳转至公共入口PLT[0]。PLT[0]进一步通过GOT[1]获取动态链接器的参数信息(如重定位表地址),将其压入栈中,再通过GOT[2]间接跳转至动态链接器(如ld-linux-x86-64.so.2)。
·动态链接器解析
动态链接器根据栈中的函数标识符和重定位信息,从共享库(如libc.so.6)中查找目标函数(如printf)的实际内存地址,将其回填至对应的GOT条目中,随后将控制权移交至该函数入口。
·后续调用优化
当再次调用同一函数时,由于GOT条目已更新为实际函数地址,PLT的首条指令可直接通过GOT跳转至目标函数,无需重复解析。此机制通过延迟绑定(Lazy Binding)显著减少启动开销,同时保持动态链接的灵活性。
5.8 本章小结
本章系统剖析了链接阶段的核心机制与实践验证。链接器通过整合可重定位目标文件(hello.o)与系统库(如libc.so),完成了符号解析、地址重定位与节区合并,生成可执行文件hello。通过readelf工具解析ELF格式,确认其类型为可执行文件(EXEC),入口地址为0x401000,并包含代码段(.text)、数据段(.rodata)及动态链接信息(.dynamic)。符号表与重定位条目表明,外部函数(如printf、exit)通过动态链接机制(PLT与GOT)实现延迟绑定,首次调用时由动态链接器解析实际地址并更新GOT,后续调用直接跳转以提升效率。
通过objdump对比hello与hello.o的反汇编代码,发现链接后的虚拟地址从0x401000起始,跳转指令与函数调用均绑定绝对地址,解决了可重定位文件中的符号未定义问题。edb工具进一步验证了代码段的虚拟地址空间布局及动态链接流程,包括_start初始化、__libc_start_main调用main函数,以及exit触发系统调用终止进程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是程序的执行实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、代码段、数据段、堆栈段以及操作系统分配的各类资源(如文件描述符、CPU时间片等)。进程通过进程控制块(PCB)维护其状态信息,包括寄存器值、进程ID、优先级等。
作用:
将不同进程的内存空间相互隔离,防止非法访问。使操作系统通过进程调度实现多任务并发,提升CPU利用率。通过进程状态(就绪、运行、阻塞等)实现任务的挂起、恢复与终止。为应用程序提供运行环境,管理硬件资源(如内存、I/O设备)。
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell是用户与操作系统内核之间的接口,负责解析用户输入的命令,并启动对应的程序或执行内置命令。Bash(Bourne-Again Shell)是Linux默认的Shell,支持脚本编程、管道、重定向等高级功能。
处理流程:
·读取输入:从终端或脚本读取命令字符串。
·解析命令:分割命令为参数列表,处理通配符(如*)、重定向符(如>)和管道符(|)。
·执行命令:
对于内置命令(如cd、echo),直接由Shell处理。对于外部程序(如hello),通过fork()创建子进程,子进程调用execve()加载程序,父进程通过wait()等待子进程结束。
·输出结果:将程序输出或错误信息显示到终端。
6.3 Hello的fork进程创建过程
·调用fork():
复制父进程(Shell)的地址空间、文件描述符和PCB,生成子进程。fork()在父进程中返回子进程PID,在子进程中返回0。
·子进程执行execve():
子进程调用execve("./hello", argv, envp),将hello程序加载到其地址空间,替换原有代码段和数据段。
·父进程等待:
若命令以&结尾(后台执行),父进程不等待;否则调用waitpid()阻塞,直至子进程终止。
6.4 Hello的execve过程
·参数传递:
参数列表argv(如["./hello", "学号", "姓名", "手机号", "秒数"])和环境变量envp被复制到内核空间。
·加载程序:
解析hello的ELF格式,将代码段(.text)、数据段(.data、.bss)映射到进程的虚拟地址空间。
·设置入口点:
程序入口地址设置为_start(由链接器定义),初始化栈并跳转到main函数执行。
·资源清理:
关闭父进程继承但标记为"执行时关闭"(Close-on-exec)的文件描述符。
6.5 Hello的进程执行
进程上下文与时间片调度
·上下文切换:
当hello运行时,可通过perf stat -e context-switches ./hello统计上下文切换次数。实验结果显示,即使程序未主动阻塞,时间片耗尽后仍会发生强制切换(例如,在循环中无sleep时,每秒切换约数百次)。
·用户态与内核态转换:
使用strace ./hello追踪系统调用,可见hello频繁调用write(printf触发)、nanosleep(sleep触发)和read(getchar触发)。每次系统调用均触发从用户态到内核态的切换(syscall指令)。
·时间片分配:
在Linux的CFS调度器下,hello默认时间片为几毫秒。若程序执行短任务(如快速循环),会因时间片耗尽被抢占;若包含sleep(5),则主动让出CPU,减少切换频率。
6.6 hello的异常与信号处理
6.6.1异常类型与对应信号
·用户中断(Ctrl-C)
信号类型:SIGINT(中断信号)
处理方式:默认终止进程。
·进程挂起(Ctrl-Z)
信号类型:SIGTSTP(终端停止信号)
处理方式:默认暂停进程,转为后台作业。
·非法内存访问
信号类型:SIGSEGV(段错误信号)
触发场景:程序访问未分配的内存(如空指针解引用)。
处理方式:默认终止进程并生成核心转储文件(core dump)。
·非法指令
信号类型:SIGILL(非法指令信号)
触发场景:执行未定义的机器指令(如损坏的二进制文件)。
处理方式:默认终止进程。
·算术异常
信号类型:SIGFPE(浮点异常信号)
触发场景:除以零或溢出操作。
处理方式:默认终止进程。
6.6.2实际操作
6.6.2.1不停乱按:截屏如下

图6.6.2-1 不停乱按
可见乱按(即使包括回车也)不会终止(影响)程序运行。

6.6.2.2 Ctrl-Z:截屏如下
图6.6.2-2 Ctrl-Z
Ctrl+Z的功能是向进程发送SIGSTP信号,进程接收到该信号之后会将该作业挂起。对此进行进一步分析,执行ps指令查看进程得知hello仍在运行中,运行jobs指令得知hello的后台job id为1,调用fg指令继续运行挂起后的程序,发现挂起前输出诶3次,挂起后输出为7次,总计为10次。

图6.6.2-3 对挂起程序的后续操作
6.6.2.3 Ctrl-C:截屏如下

图6.6.2-4 Ctrl+C
程序里脊停止运行,这是因为命令内核想前台发送了SIGINT信号,终止了前台作业。
6.6.2.4 不停按回车:截屏如下

图6.6.2-5 Ctrl+C
不仅程序运行过程中的回车会显示出来,而且在hello执行完毕后每个回车都会被当做命令执行。可以看出回车的信息也同样发送到了shell中。
6.7本章小结
本实验通过进程生命周期管理验证了fork()与execve()的协同机制:fork()复制Shell上下文生成子进程,execve()加载hello的ELF文件至独立地址空间(通过pmap 可观测其私有内存映射);结合perf和strace工具分析,揭示了时间片调度下用户态与内核态切换的频率(如系统调用触发syscall指令)及CPU时间片的分配逻辑(计算密集型任务因时间片耗尽被抢占,I/O操作主动让出CPU)。信号机制的核心作用在实验中得以体现------Ctrl-Z发送SIGTSTP挂起进程(依赖Shell的jobs/fg管理),非法内存访问触发SIGSEGV反映操作系统的内存保护机制。Bash通过进程组(ps -j显示PGID)实现前后台作业的层级控制,凸显其资源管理能力。进一步地,提出轻量级进程(协程化hello减少上下文切换)与实时信号(SIGRTMIN优化高优先级响应)的创新方向,为系统级程序设计与优化提供新思路。
第7章 hello的存储管理
7.1 hello的存储器地址空间
概念与实验验证:
7.1.1逻辑地址:
定义:程序中直接使用的地址,如C代码中的指针值或函数地址。
示例:在hello的反汇编代码(objdump -d hello)中,指令地址(如0x401144)为逻辑地址。
作用:逻辑地址通过段式管理转换为线性地址。
7.1.2线性地址(虚拟地址):
定义:逻辑地址经段式管理转换后的中间地址,属于进程虚拟地址空间的一部分。
实验验证:
使用edb调试工具查看hello的内存布局,代码段起始地址为0x401000(ELF入口地址),堆栈地址范围可通过pmap 显示。
在Linux中,进程虚拟地址空间布局可通过/proc//maps文件查看。
7.1.3物理地址:
定义:实际访问内存硬件(DRAM)的地址,由线性地址经页式管理转换得到。
实验验证:
通过内核模块或crash工具查看物理页帧号(PFN),例如hello的代码段映射到物理页帧0x12345。
7.1.4虚拟地址空间特性:
隔离性:每个进程拥有独立的虚拟地址空间(如hello与Shell进程的地址空间互不干扰)。
抽象性:虚拟地址隐藏物理内存细节,支持内存保护(如只读代码段)和共享(如动态库libc.so)。
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.2.0 段式管理介绍:
段式管理(segmentation),是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
7.2.1段式管理原理:
为了进行段式管理,每道程序在系统中都有一个段(映象)表来存放该道程序各段装入主存的状况信息。段表中的每一项(对应表中的每一行)描述该道程序一个段的基本状况,由若干个字段提供。
段名字段用于存放段的名称,段名一般是有其逻辑意义的,也可以转换成用段号指明。由于段号从0开始顺序编号,正好与段表中的行号对应,如2段必是段表中的第3行,这样,段表中就可不设段号(名)字段。装入位字段用来指示该段是否已经调入主存,"1"表示已装入,"0"表示未装入。在程序的执行过程中,各段的装入位随该段是否活跃而动态变化。当装入位为"1"时,地址字段用于表示该段装入主存中起始(绝对)地址,当装入位为"0"时,则无效(有时机器用它表示该段在辅存中的起始地址)。
段长字段指明该段的大小,一般以字数或字节数为单位,取决于所用的编址方式。段长字段是用来判断所访问的地址是否越出段界的界限保护检查用的。访问方式字段用来标记该段允许的访问方式,如只读、可写、只能执行等,以提供段的访问方式保护。除此之外,段表中还可以根据需要设置其它的字段。段表本身也是一个段,一般常驻在主存中,也可以存在辅存中,需要时再调入主存。假设系统在主存中最多可同时有N道程序,可设N个段表基址寄存器。对应于每道程序,由基号(程序号)指明使用哪个段表基址寄存器。段表基址寄存器中的段表基址字段指向该道程序的段表在主存中的起始地址。段表长度字段指明该道程序所用段表的行数,即程序的段数。
·段寄存器:Intel x86-64架构使用段寄存器(如CS代码段、DS数据段)存储段选择符。
·段描述符表:全局描述符表(GDT)或局部描述符表(LDT)中保存段基址、界限和权限信息。
·转换公式:
线性地址=段基址+逻辑地址偏移量线性地址=段基址+逻辑地址偏移量
7.2.2现代系统的段式管理:
·平坦模式:在64位Linux中,段基址通常设为0,段界限设为最大地址,逻辑地址直接等于线性地址。
·验证实验:
反汇编hello的代码段(如.text),所有指令的线性地址与逻辑地址一致(如0x401144)。
7.2.3段式管理的实际作用:
·权限控制:通过段描述符的权限位(如代码段可执行、数据段可写)实现内存保护。
·兼容性:支持遗留16/32位程序的段式内存模型。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.0 页式管理介绍:
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。
静态分页管理的第一步是为要求内存的作业或进程分配足够的页面。系统通过存储页面表、请求表以及页表来完成内存的分配工作。首先,请求表给出进程或作业要求的页面数。然后,由存储页面表检查是否有足够的空闲页面,如果没有,则本次无法分配。如果有则首先分配设置页表,并请求表中的相应表项后,按一定的查找算法搜索出所要求的空闲页面,并将对应的页好填入页表中。静态页式管理解决了分区管理时的碎片问题。但是,由于静态页式管理要求进程或作业在执行前全部装入内存,如果可用页面数小于用户要求时,该作业或进程只好等待。而且作业和进程的大小仍受内存可用页面数的限制。
动态页式管理是在静态页式管理的基础上发展起来的。它分为请求页式管理和预调入页式管理。请求页式管理和预调入页式管理在作业或进程开始执行之前,都不把作业或进程的程序段和数据段一次性地全部装入内存,而只装入被认为是经常反复执行和调用的工作区部分。其它部分则在执行过程中动态装入。请求页式管理与预调入页式管理的主要区别在它们的调入方式上。请求页式管理的调入方式是,当需要执行某条指令而又发现它不在内存时或当执行某条指令需要访问其它的数据或指令时.这些指令和数据不在内存中,从而发生缺页中断,系统将外存中相应的页面调入内存。
7.3.1页式管理原理:
·页表结构:x86-64采用四级页表(PML4、PDPT、PD、PT),每级索引9位,页内偏移12位,支持48位虚拟地址。
·转换过程:
物理地址=页表基址(CR3)→逐级索引页表项→页帧号(PFN)×4KB+页内偏移物理地址=页表基址(CR3)→逐级索引页表项→页帧号(PFN)×4KB+页内偏移
7.3.2实验验证工具:
·TLB加速:通过perf stat -e dTLB-load-misses ./hello统计TLB未命中次数,验证地址转换效率。
7.3.3页式管理的实际应用:
·内存分配:hello的代码段(.text)映射到物理页帧,数据段(.data)按需分配。
·缺页处理:首次访问未加载的页时触发缺页中断,内核分配物理页并更新页表。
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.0: TLB与VA到PA的变换介绍:
TLB,即转译后备缓冲区,也被翻译为页表缓存、转址旁路缓存,为CPU的一种缓存,由存储器管理单元用于改进虚拟地址到物理地址的转译速度。当前所有的桌面型及服务器型处理器(如 x86)皆使用TLB。TLB具有固定数目的空间槽,用于存放将虚拟地址映射至物理地址的标签页表条目。为典型的结合存储(content-addressable memory,首字母缩略字:CAM)。其搜索关键字为虚拟内存地址,其搜索结果为物理地址。如果请求的虚拟地址在TLB中存在,CAM 将给出一个非常快速的匹配结果,之后就可以使用得到的物理地址访问存储器。如果请求的虚拟地址不在 TLB 中,就会使用标签页表进行虚实地址转换,而标签页表的访问速度比TLB慢很多。有些系统允许标签页表被交换到次级存储器,那么虚实地址转换可能要花非常长的时间。
VA到PA的变换,即虚拟地址到物理地址的变换。在系统层面上,虚拟地址产生的原因之一也是因为软件应用的地址空间远大于实际物理空间。这个时候系统上的硬件或者软件除了需要完成业务,还要进行VA到PA的转换,让业务访问到实际的物理地址空间。为了实现虚拟地址到物理地址的转换,那么就需要硬件具有格外的资源存储VA2PA的转换关系,即页表。
7.4.1 地址空间划分与四级页表结构:
在x86-64架构中,虚拟地址通常为48位(高16位未使用),物理地址为52位。四级页表的每级对应虚拟地址的四个索引字段和一个页内偏移:
PML4(Page Map Level 4):最高级页表,由CR3寄存器指向其物理基地址。
PDPT(Page Directory Pointer Table)、PD(Page Directory)、PT(Page Table):依次为第三、二、一级页表。索引字段(以48位VA为例)为PML4索引(bits 39-47),PDPT索引(bits 30-38),PD索引(bits 21-29),PT索引(bits 12-20)。页内偏移(bits 0-11,共4KB页大小)。每个页表条目(如PML4E、PDPTE等)为8字节,包含下一级页表或物理页框的基地址,以及权限位(R/W、U/S、Present等)。
7.4.2. TLB的作用
TLB是CPU内部的高速缓存,存储近期使用的虚拟页到物理页的映射可以加速转换,即若TLB命中(条目存在且有效),直接获取PA,无需访问内存中的页表。同时减少延迟,避免四级页表遍历的多次内存访问(每次遍历需4次访问,共约100+ CPU周期)。
7.4.3. 地址转换流程
步骤1:TLB查询
CPU将虚拟地址提交给MMU(内存管理单元)。MMU首先查询TLB,若命中,则直接获得物理页框号,与页内偏移组合成PA。若未命中,则触发页表遍历(Page Walk)。
步骤2:四级页表遍历(Page Walk)
若TLB未命中,MMU按以下顺序遍历页表:
·PML4查询:
从CR3寄存器获取PML4基地址,使用VA的PML4索引定位PML4E,检查权限(如Present位),若无效则触发缺页异常。
·PDPT查询:
从PML4E获取PDPT基地址,使用VA的PDPT索引定位PDPTE,若支持1GB大页,PDPTE可能直接指向1GB物理页框。
·PD查询:
从PDPTE获取PD基地址,使用VA的PD索引定位PDE,若支持2MB大页,PDE直接指向2MB物理页框。
·PT查询:
从PDE获取PT基地址,使用VA的PT索引定位PTE,PTE指向4KB物理页框的基地址。
·合成物理地址:
将PTE中的物理页框基地址与VA的页内偏移(bits 0-11)组合,得到完整PA。
步骤3:更新TLB
将新的VA到PA映射存入TLB,后续访问可加速。若TLB满,按替换策略(如LRU)淘汰旧条目。
7.4.4. 关键机制与异常处理
·权限检查:每级页表项均包含权限位(如读/写、用户/内核模式)。若访问违反权限(如用户程序访问内核页),触发保护错误(General Protection Fault)。
· 缺页异常:若某级页表项的Present位为0,表示页未加载到内存,触发Page Fault。操作系统需处理异常(如从磁盘换入页),并重新执行指令。
·大页支持:PDPTE或PDE可能直接映射大页(1GB或2MB),减少页表层级和TLB压力。
7.4.5. TLB管理与优化
·ASID(Address Space Identifier):为不同进程的TLB条目标记ID,避免进程切换时刷新整个TLB。
·全局页(Global Pages):标记常驻TLB的条目(如内核代码),避免被替换。
·INVLPG指令:操作系统在修改页表后,需主动无效对应的TLB条目,保证一致性。
·多级TLB结构:现代CPU采用L1 TLB(小、快)和L2 TLB(大、慢)的分层设计。
7.5 三级Cache支持下的物理内存访问
7.5 三级Cache支持下的物理内存访问
7.5.1 缓存层级与物理内存的访问架构
现代处理器的三级缓存采用分层拓扑结构:
·L1 Cache(一级缓存)直接集成于CPU核心内部,分为指令缓存(L1i)与数据缓存(L1d),访问延迟通常为1-4个时钟周期,容量在32-64KB之间。其设计目标是匹配CPU核心的单周期高频访问需求。
·L2 Cache(二级缓存)作为L1的补充,容量通常在256KB-2MB之间,延迟约为5-12周期。部分设计中L2为多个核心共享,而另一些则为每个核心独占,取决于处理器微架构。
·L3 Cache(三级缓存)是容量最大的末级缓存(LLC),可达数十MB级别,延迟约20-50周期。其采用全片共享模式,服务于所有CPU核心,充当核心与主存之间的缓冲池,有效降低总线争用。
这种层级设计通过空间局部性与时间局部性原理,将高频访问数据保留在靠近计算单元的位置。当CPU发出内存访问请求时,硬件自动在缓存层级中逐级搜索数据,仅当三级缓存均未命中(Cache Miss)时,才会触发物理内存访问。
图7.5.1 计算机的存储结构(包含三级cache)
7.5.2 物理内存访问的触发与执行流程
当CPU需要访问某个物理地址时,缓存子系统的运作会进行:
L1 Cache查询:MMU(内存管理单元)首先将虚拟地址转换为物理地址,并检查L1d Cache对应的缓存行(Cache Line)。若命中(Cache Hit),数据在3-5周期内返回寄存器。
L2 Cache回溯:若L1未命中,则查询L2 Cache。由于L2容量更大,可能缓存了L1淘汰的数据块。命中时数据将同时回填至L1,并转发至CPU,耗时约10-20周期。
L3 Cache全局检索:若L2未命中,请求被广播至共享的L3 Cache。此时需通过环形总线或Mesh互连架构访问其他核心可能共享的缓存行。命中时数据将逐级回填至L2和L1,总延迟约30-60周期。
物理内存访问:若三级缓存均未命中,则通过内存控制器(IMC)发起DDR总线事务。典型DDR4内存的访问延迟可达80-100ns(约200-300 CPU周期),数据返回后同步更新L3、L2及L1缓存,并标记为最新状态(Modified或Exclusive)。
此过程中,硬件预取器(Hardware Prefetcher)会主动分析访存模式,提前将可能访问的数据块加载到缓存,降低后续访问的缺失率。
7.5.3 缓存一致性与替换策略的协同
在多核处理器中,三级缓存需维护缓存一致性(Cache Coherency):
·MESI协议:通过Modified(已修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)四种状态标记缓存行,确保多核间数据一致性。例如,当某核心修改L1数据时,L3会通过嗅探(Snooping)机制广播失效信号,使其他核心的对应缓存行失效。
·替换算法:LRU(最近最少使用)或其变体(如伪LRU)被广泛用于缓存行替换。L3因容量较大,可能采用分段LRU或动态插入策略(DIP),根据工作负载特性调整替换优先级。
当物理内存数据被修改时,写回(Write-back)策略允许延迟更新主存,仅当缓存行被替换时才将脏数据(Dirty Data)写回内存,减少总线流量。而写分配(Write-allocate)策略则在写未命中时触发缓存加载,避免直接写内存造成的性能损失。
7.5.4 性能瓶颈与优化挑战
·缓存污染(Cache Pollution):不规则访存模式(如随机大范围访问)会导致高频缓存行被冲刷,增加缺失率。解决方案包括使用PCID(进程上下文标识符)隔离不同进程的缓存区域。
·伪共享(False Sharing):不同核心频繁修改同一缓存行中的非重叠数据,引发不必要的缓存行失效。可通过数据对齐(Cache Line Alignment)或填充(Padding)消除。
·NUMA效应:在非统一内存访问架构中,跨NUMA节点的远程内存访问延迟可能比本地访问高2-3倍,需结合操作系统调度策略优化数据局部性。
7.6 hello进程fork时的内存映射
当执行fork系统调用时,内核会为新生成的子进程构建必要的内核对象(如进程控制块),并为其分配独立的进程标识符(PID)。在虚拟内存层面,内核会将父进程的地址空间元数据(包括mm_struct、内存区域描述符及页表条目)进行精确拷贝,随后将父子进程的所有可写内存页权限设为只读,并将对应内存区域标记为"私有写时复制"(Copy-on-Write,COW)。
当fork在子进程中返回时,子进程的虚拟地址空间在逻辑上与父进程调用fork时的状态完全一致。此后,若父子进程中任一方向某个COW页发起写入操作,内核会捕获该行为并动态分配新的物理页,将原页内容复制到新页后更新页表映射。这种机制确保了两个进程在共享初始内存内容的同时,仍能维护各自地址空间的隔离性与私有性。
7.7 hello进程execve时的内存映射
当调用execve函数时,内核通过其内置的加载器逻辑,将指定可执行文件(如hello)加载到当前进程的地址空间中,并彻底替换原有程序。这一过程包含以下关键操作:
清理原有地址空间:首先,内核会清空进程用户态虚拟地址空间中所有已存在的内存映射结构(如代码段、数据段、堆栈等),释放关联的资源。
构建私有内存映射:基于hello可执行文件的格式,内核为其代码段(.text)和数据段(.data)创建私有且写时复制的映射,确保初始内容来自文件;同时,为未初始化的全局数据(BSS段)、堆及栈分配以二进制零填充的匿名内存页,其中BSS大小由文件定义,堆栈初始长度为零。
链接共享库资源:若hello依赖动态库(如libc.so),内核会将共享对象映射到进程的共享虚拟内存区域,由动态链接器解析符号依赖,完成运行时绑定。
移交执行控制权:最终,内核将进程的程序计数器(PC)重定向至新代码段的入口地址(如_start符号),从此进程开始执行hello的指令逻辑,原有程序被完全替代。
通过这一系列操作,execve实现了进程上下文的彻底切换,使新程序在独立的资源环境中运行,同时保持高效的内存管理与动态链接支持。
7.8 缺页故障与缺页中断处理
7.8.1 定义与触发机制
缺页故障(Page Fault) 指CPU访问的虚拟页未驻留在物理内存(DRAM)时引发的异常,本质是内存管理单元(MMU)在地址翻译时发现目标页表条目的"有效位"标记为0,表明该页未缓存。此时,该页可能因以下原因未被加载:
·初次访问:程序首次使用动态分配的堆内存、新扩展的栈空间或未加载的代码段;
·页面置换:物理内存不足时,操作系统此前将该页换出到磁盘交换区(Swap)或文件(如内存映射文件);
·写时复制(COW):父子进程共享的私有页因写入操作触发复制,但新页尚未分配。此时,硬件检测到无效页表条目后触发缺页异常,将控制权转交操作系统处理。
7.8.2 缺页中断处理流程与优化
处理流程如下:
·定位缺页原因:内核通过虚拟地址和页表层级确定缺失页的物理位置(如交换区、文件或需分配新页);
·选择置换页:若物理内存已满,根据置换策略(如LRU、FIFO或时钟算法)选择一个被替换的物理页。若该页为脏页(被修改过),需将其内容写回磁盘;若为干净页(未修改),可直接覆盖;
·加载目标页:从磁盘交换区或文件读取缺失页内容到腾出的物理页帧,更新页表条目(有效位置1,填充物理地址);
·重启指令:内核返回到触发缺页的指令,由CPU重新发起访问。此时虚拟页已缓存,地址翻译正常完成。
7.8.3 优化扩展:
· 预取机制:预测程序访问模式,提前加载可能需要的页(如顺序访问文件的mmap预读);
·大页技术:使用2MB/1GB大页减少缺页频率(适用于密集内存访问场景);
·共享页延迟复制:合并多次COW缺页请求,减少冗余复制;
·NUMA优化:在多核系统中优先分配本地内存节点页,降低访问延迟;
·内存压缩:将非活跃页压缩存储于内存(如ZRAM),避免换出到慢速磁盘。
7.9动态存储分配管理
概念:在程序运行时程序员使用动态内存分配器,例如调用malloc函数从而获得虚拟内存。分配器将堆(heap)视为一组不同大小的块(blocks)的集合来维护。每个块要么是已分配的,要么是空闲的。
堆(Heap)动态分配通过系统调用(如malloc/free)管理一块连续虚拟内存区域,按需分割或合并内存块,适用于需要灵活分配不定长对象的场景(如应用程序运行时动态数据结构),优点是支持随机分配与释放,但可能产生内存碎片且管理开销较大,需权衡分配策略以减少性能损耗。
显式链表分配器以链表结构显式记录所有空闲内存块(每个块包含前驱/后继指针),分配时遍历链表寻找合适块(如首次适应算法),适用于通用内存管理(如C标准库实现),优点是碎片控制较好且分配灵活,但指针存储占用额外内存,频繁遍历可能降低效率,尤其在碎片化严重时。
隐式链表分配器通过内存块头部隐式记录块大小和状态(无显式指针),遍历时需逐个计算下一块地址(如通过"块大小"字段跳转),适用于资源受限的嵌入式系统或早期操作系统,优点是实现简单、无额外指针开销,但分配效率低(需线性搜索)、易产生外部碎片,且合并释放块需复杂边界标记检查。
7.10本章小结
本章系统剖析了hello程序的存储管理机制,从逻辑地址到物理地址的转换过程出发,详细阐述了段式管理(平坦模式下逻辑地址直接映射为线性地址)与页式管理(四级页表结合TLB加速虚拟地址到物理地址的转换)的协同机制,揭示了硬件与操作系统在地址空间抽象与隔离中的核心作用。通过三级Cache的分层架构(L1/L2/L3)优化物理内存访问效率,减少访存延迟;结合fork时的写时复制(COW)内存映射与execve时的动态加载(清理旧空间、映射代码段与共享库),展示了进程间内存资源的动态分配与隔离策略。进一步分析了缺页故障的触发原因(首次访问、置换、COW)与处理流程(牺牲页选择、磁盘换入、页表更新),以及动态存储分配器(堆、显式/隐式链表)在内存碎片与效率间的权衡。本章通过理论与实践结合,全面呈现了计算机系统在存储管理层面的多层次优化与协同设计,为程序的高效运行提供坚实支撑。
结论
从冯·诺依曼架构的蓝图到量子计算的星辰大海,从穿孔卡片的机械吟唱到神经拟态芯片的思维火花,计算机科学的每一步进化都是人类智慧的加冕礼。Hello程序的短暂一生,恰似文明进程的微缩史诗:预处理是文明基因的重组,编译是逻辑火种的传递,链接是知识网络的编织,运行是思维闪电的迸发。当我们在IDE中轻点运行,实则是站在巨人的肩头,见证着图灵机与香农熵的永恒对话。计算机领域的学习,恰如朝圣者攀登巴别塔,既要解构二进制世界的精密齿轮,更要领悟那些让0与1绽放文明之花的伟大哲思------这才是数字时代最浪漫的诗篇。
Hello程序从源码到执行的完整生命周期深刻展现了计算机系统各层组件的协同工作。首先,预处理阶段通过宏展开、头文件合并与条件编译生成标准化中间代码(.i),消除开发环境差异;其次,编译器将高级语义转化为目标架构的汇编指令(.s),完成类型校验与局部优化,并生成符号表;随后,汇编器将汇编代码转换为机器码(.o),封装为ELF格式的可重定位文件,记录代码段、数据段及重定位信息;链接阶段整合静态库与动态库(如libc.so),通过PLT/GOT实现延迟绑定,生成具备完整虚拟地址布局的可执行文件。进程管理层面,Shell通过fork创建子进程并复制地址空间,execve加载hello代码段、清理旧资源并重置PC入口,结合COW机制优化内存共享;存储管理中,四级页表与TLB协作完成VA到PA的转换,三级Cache减少物理内存访问延迟,缺页中断通过换页算法动态维护内存工作集。I/O交互通过系统调用桥接用户态与内核态,printf依赖缓冲区管理与write系统调用实现输出。整个流程体现了编译器、链接器、操作系统(进程调度、存储管理、异常处理)与硬件(MMU、Cache、TLB)的深度协作,验证了计算机系统"抽象分层-透明优化"的核心设计哲学。
计算机系统的设计是效率、安全与扩展性博弈的艺术。通过本次实验,我深刻认识到模块化抽象的重要性:ELF格式统一了跨平台可执行文件的标准,虚拟内存隔离了进程空间,动态链接平衡了内存占用与灵活性。然而,现有机制仍有优化空间。创新方向一:在动态链接中引入"预解析池",针对高频调用的库函数(如printf),在程序启动时通过静态分析预加载其真实地址至独立GOT区,减少首次调用的解析开销。创新方向二:结合机器学习预测缺页模式,在进程切换时主动预加载下一时间片可能访问的页,降低缺页中断频率。例如,对循环密集型任务,可基于历史访存序列训练LSTM模型,指导页面置换策略。创新方向三:设计"智能TLB"架构,为不同进程的地址空间分配权重,动态调整TLB缓存策略(如COW页优先缓存物理页而非虚拟页),提升缓存命中率。这些思路将硬件特性与算法优化结合,进一步释放分层设计的潜力,为未来异构计算与实时系统提供新可能。
附件
文件名 功能
hello.c 源程序
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello 可执行文件
hello.o.disasm 反汇编hello.o得到的反汇编文件
hello.disasm 反汇编hello可执行文件得到的反汇编文件#
参考文献
1\]庞成宾,徐雪兰,张天泰,等.反汇编工具中间接跳转表求解算法分析与测试\[J\].软件学报,2024,35(10):4623-4641.DOI:10.13328/j.cnki.jos.006976. \[2\]马家豪.基于链接器对可执行文件信息提取的研究\[D\].西安电子科技大学,2020.DOI:10.27389/d.cnki.gxadu.2020.001952. \[3\]刘明阳.基于TLB组内共享的GPU地址转换性能优化研究\[D\].武汉理工大学,2023.DOI:10.27381/d.cnki.gwlgu.2023.002029. \[4\]闫生超.基于BASH脚本的Unix环境下多组件部署管理框架\[J\].计算机系统应用,2012,21(10):61-65. \[5\]赵刚.离散分配式存储管理------分页式管理分析\[C\]//《建筑科技与管理》组委会.2015年7月建筑科技与管理学术交流会论文集.河北堃科电力工程有限公司;,2015:338+343.