【HIT-CSAPP 哈尔滨工业大学计算机系统期末大作业】程序人生-Hello‘s P2P

计算机系统

大作业

题 目 ++程序人生++ ++-Hello's P2P++

专 业 ++计算机与电子通信类++

学   号 ++2023111846++

班   级 ++23L0503++

学 生 ++吴乃祺++

指 导 教 师 ++刘宏伟++

计算机科学与技术学院

2024 年5月

摘 要

本文以经典的"Hello World"程序为研究对象,通过构建完整的实验分析框架,系统性地剖析了程序从源代码到可执行文件的全生命周期。研究采用gcc、objdump、readelf、edb等专业工具链,深入分析了预处理阶段的宏展开、编译阶段的语法分析和代码优化、汇编阶段的指令转换以及链接阶段的符号解析和重定位等关键过程。

在系统运行机制方面,本文重点探讨了进程管理中的核心问题:通过fork系统调用时的写时复制(COW)机制展示了操作系统如何高效管理进程内存空间;详细解析了execve执行时的动态加载过程,包括代码段映射、数据段初始化和动态链接处理。在存储体系研究中,不仅阐述了Intel架构下段式管理与页式管理的协同工作机制,还论述了TLB快表和三级缓存对系统性能的关键优化作用。

特别值得关注的是,本文深入剖析了缺页异常从硬件触发到内核处理的完整路径,包括异常诊断、页面调度和恢复执行等关键环节。同时,对动态内存分配策略进行了系统分析,比较了不同分配算法在碎片处理和性能优化方面的特点。

这些研究内容有机串联起了编译系统、进程管理、存储体系等计算机系统核心组件,通过一个简单程序的深度分析,生动展现了现代计算机系统软硬件协同工作的复杂机制和精妙设计。

**关键词:**计算机系统;程序生命周期;链接;进程管理;动态内存分配

目 录

[第1章 概述................................................................................... - 4 -](#第1章 概述................................................................................... - 4 -)

[1.1 Hello简介............................................................................ - 4 -](#1.1 Hello简介............................................................................ - 4 -)

[1.2 环境与工具........................................................................... - 4 -](#1.2 环境与工具........................................................................... - 4 -)

[1.3 中间结果............................................................................... - 4 -](#1.3 中间结果............................................................................... - 4 -)

[1.4 本章小结............................................................................... - 4 -](#1.4 本章小结............................................................................... - 4 -)

[第2章 预处理............................................................................... - 5 -](#第2章 预处理............................................................................... - 5 -)

[2.1 预处理的概念与作用........................................................... - 5 -](#2.1 预处理的概念与作用........................................................... - 5 -)

[2.2在Ubuntu下预处理的命令................................................ - 5 -](#2.2在Ubuntu下预处理的命令................................................ - 5 -)

[2.3 Hello的预处理结果解析.................................................... - 5 -](#2.3 Hello的预处理结果解析.................................................... - 5 -)

[2.4 本章小结............................................................................... - 5 -](#2.4 本章小结............................................................................... - 5 -)

[第3章 编译................................................................................... - 6 -](#第3章 编译................................................................................... - 6 -)

[3.1 编译的概念与作用............................................................... - 6 -](#3.1 编译的概念与作用............................................................... - 6 -)

[3.2 在Ubuntu下编译的命令.................................................... - 6 -](#3.2 在Ubuntu下编译的命令.................................................... - 6 -)

[3.3 Hello的编译结果解析........................................................ - 6 -](#3.3 Hello的编译结果解析........................................................ - 6 -)

[3.4 本章小结............................................................................... - 6 -](#3.4 本章小结............................................................................... - 6 -)

[第4章 汇编................................................................................... - 7 -](#第4章 汇编................................................................................... - 7 -)

[4.1 汇编的概念与作用............................................................... - 7 -](#4.1 汇编的概念与作用............................................................... - 7 -)

[4.2 在Ubuntu下汇编的命令.................................................... - 7 -](#4.2 在Ubuntu下汇编的命令.................................................... - 7 -)

[4.3 可重定位目标elf格式........................................................ - 7 -](#4.3 可重定位目标elf格式........................................................ - 7 -)

[4.4 Hello.o的结果解析............................................................. - 7 -](#4.4 Hello.o的结果解析............................................................. - 7 -)

[4.5 本章小结............................................................................... - 7 -](#4.5 本章小结............................................................................... - 7 -)

[第5章 链接................................................................................... - 8 -](#第5章 链接................................................................................... - 8 -)

[5.1 链接的概念与作用............................................................... - 8 -](#5.1 链接的概念与作用............................................................... - 8 -)

[5.2 在Ubuntu下链接的命令.................................................... - 8 -](#5.2 在Ubuntu下链接的命令.................................................... - 8 -)

[5.3 可执行目标文件hello的格式........................................... - 8 -](#5.3 可执行目标文件hello的格式........................................... - 8 -)

[5.4 hello的虚拟地址空间......................................................... - 8 -](#5.4 hello的虚拟地址空间......................................................... - 8 -)

[5.5 链接的重定位过程分析....................................................... - 8 -](#5.5 链接的重定位过程分析....................................................... - 8 -)

[5.6 hello的执行流程................................................................. - 8 -](#5.6 hello的执行流程................................................................. - 8 -)

[5.7 Hello的动态链接分析........................................................ - 8 -](#5.7 Hello的动态链接分析........................................................ - 8 -)

[5.8 本章小结............................................................................... - 9 -](#5.8 本章小结............................................................................... - 9 -)

[第6章 hello进程管理.......................................................... - 10 -](#第6章 hello进程管理.......................................................... - 10 -)

[6.1 进程的概念与作用............................................................. - 10 -](#6.1 进程的概念与作用............................................................. - 10 -)

[6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -](#6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -)

[6.3 Hello的fork进程创建过程............................................ - 10 -](#6.3 Hello的fork进程创建过程............................................ - 10 -)

[6.4 Hello的execve过程........................................................ - 10 -](#6.4 Hello的execve过程........................................................ - 10 -)

[6.5 Hello的进程执行.............................................................. - 10 -](#6.5 Hello的进程执行.............................................................. - 10 -)

[6.6 hello的异常与信号处理................................................... - 10 -](#6.6 hello的异常与信号处理................................................... - 10 -)

[6.7本章小结.............................................................................. - 10 -](#6.7本章小结.............................................................................. - 10 -)

[第7章 hello的存储管理...................................................... - 11 -](#第7章 hello的存储管理...................................................... - 11 -)

[7.1 hello的存储器地址空间................................................... - 11 -](#7.1 hello的存储器地址空间................................................... - 11 -)

[7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -](#7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -)

[7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -](#7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -)

[7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -](#7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -)

[7.5 三级Cache支持下的物理内存访问................................ - 11 -](#7.5 三级Cache支持下的物理内存访问................................ - 11 -)

[7.6 hello进程fork时的内存映射......................................... - 11 -](#7.6 hello进程fork时的内存映射......................................... - 11 -)

[7.7 hello进程execve时的内存映射..................................... - 11 -](#7.7 hello进程execve时的内存映射..................................... - 11 -)

[7.8 缺页故障与缺页中断处理................................................. - 11 -](#7.8 缺页故障与缺页中断处理................................................. - 11 -)

[7.9动态存储分配管理.............................................................. - 11 -](#7.9动态存储分配管理.............................................................. - 11 -)

[7.10本章小结............................................................................ - 12 -](#7.10本章小结............................................................................ - 12 -)

[第8章 hello的IO管理....................................................... - 13 -](#第8章 hello的IO管理....................................................... - 13 -)

[8.1 Linux的IO设备管理方法................................................. - 13 -](#8.1 Linux的IO设备管理方法................................................. - 13 -)

[8.2 简述Unix IO接口及其函数.............................................. - 13 -](#8.2 简述Unix IO接口及其函数.............................................. - 13 -)

[8.3 printf的实现分析.............................................................. - 13 -](#8.3 printf的实现分析.............................................................. - 13 -)

[8.4 getchar的实现分析.......................................................... - 13 -](#8.4 getchar的实现分析.......................................................... - 13 -)

[8.5本章小结.............................................................................. - 13 -](#8.5本章小结.............................................................................. - 13 -)

[结论............................................................................................... - 14 -](#结论............................................................................................... - 14 -)

[附件............................................................................................... - 15 -](#附件............................................................................................... - 15 -)

[参考文献....................................................................................... - 16 -](#参考文献....................................................................................... - 16 -)

第1章 概述

1.1 Hello简介

Hello的一生完整诠释了程序从静态代码到动态进程、最终归于虚无的计算机系统生命周期,其过程可划分为P2P与020两个核心阶段。

P2P(从程序到进程):程序员编写的hello.c源代码历经预处理、编译、汇编和链接四个阶段完成静态构建。预处理阶段通过宏展开和头文件包含生成hello.i文件;编译器将hello.i翻译为汇编代码hello.s,将高级逻辑映射为底层硬件指令;汇编器进一步将hello.s转换为机器码,生成可重定位目标文件hello.o;链接器最终将hello.o与系统库合并,解析符号并生成可执行的hello文件,实现从代码到可执行程序的转化。

020(从零到零):当用户在Shell中执行./hello时,程序进入动态运行与消亡阶段。Shell通过fork()创建子进程,并通过execve()加载hello的代码段和数据段到内存中。运行期间,操作系统与硬件协同工作:MMU通过四级页表和TLB实现虚拟地址到物理地址的高效转换,CPU流水线并行处理指令,分级Cache减少内存访问延迟,信号处理与设备驱动管理输入输出。进程执行完毕后,操作系统回收内存、文件描述符等资源,移除进程表记录并销毁虚拟地址空间,使进程从运行时的动态实体彻底回归静态的磁盘文件状态,完成"从零(静态代码)到零(资源释放后无痕)"的完整循环。

1.2 环境与工具

1. 硬件环境

**处理器:**12th Gen Intel(R) Core(TM) i9-12900H 2.50 GHz

**系统架构:**x64

2. 软件环境

主操作系统:Windows 11 x64(提供基础开发环境)

Linux **子系统:**Ubuntu 22.04.5

Shell **环境:**Bash(GNU Bash 5.0+,支持fork/execve等进程控制)

编译器:GCC(GNU Compiler Collection)

标准库:glibc;

3. 开发与调试工具

集成开发环境( IDE **:**Code::Blocks 64位(C/C++代码编写与项目管理)

Visual Studio 2019 64位

虚拟化环境 **:**VMware Workstation 16 Pro

Linux 调试工具链 **:**Vim + GCC; GDB; Objdump; Hexedit

1.3 中间结果

1. hello.c

作用:原始C语言源代码文件

生成方式:手动编写,本次实验由附件得到

内容:包含main函数和程序逻辑的初始代码

2. hello.i

作用:预处理后的源代码文件

生成命令:gcc -E hello.c -o hello.i

内容:展开所有宏定义,处理所有条件编译指令,包含所有头文件内容,删除所有注释,添加行号和文件名标识。

3. hello.s

作用:编译生成的汇编代码文件

生成命令:gcc -S hello.i -o hello.s

内容:与硬件架构相关汇编指令,符号引用和函数调用,寄存器使用规划。

4. hello.o

作用:可重定位目标文件

生成命令:gcc -c hello.s -o hello.o

内容:ELF格式二进制文件,包含机器指令但未完成最终地址绑定,需要进一步链接才能执行。

5. hello

作用:最终可执行文件

生成命令: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

内容:完整的ELF可执行文件,已完成所有符号解析和地址重定位,包含程序入口点和执行权限。

6. elf.txt

作用:hello.o的ELF格式分析报告

生成命令:readelf -a hello.o > elf.txt

内容:ELF头信息,节头表,符号表,重定位信息。

7. hello.elf

作用:hello可执行文件的ELF格式分析报告

生成命令:readelf -a hello > hello.elf

内容:程序头表,动态链接信息,入口点地址,各段加载信息。

8. hello1.s

作用:hello.o的反汇编结果

生成命令:objdump -d hello.o > hello1.s

内容:展示机器码与汇编指令对应关系,显示未完成重定位的符号引用,有助于理解编译器优化

9. hello2.s

作用:hello可执行文件的反汇编结果

生成命令:objdump -d hello > hello2.s

内容:展示最终可执行指令,包含完整的地址信息,显示运行时内存布局,包含库函数调用实现。

1.4 本章小结

本章系统性地阐述了hello程序从创建到终止的完整生命周期,包括预处理、编译、汇编、链接等构建过程(P2P),以及进程加载、执行和资源回收的运行阶段(020),介绍了支持本次研究的开发环境配置以及使用的主要开发调试工具。此外,还完整列出了实验过程中生成的各类中间文件及其在分析程序转换过程中的具体作用和意义,为深入理解计算机系统的工作机制提供了完整的实验依据和分析基础。

第2章 预处理

2.1 预处理的概念与作用

预处理是C程序编译的第一个阶段,由预处理器(如cpp)对源代码进行文本级处理。它在编译器正式分析代码前,根据预处理指令(以#开头)对源文件进行转换,生成纯净的C代码供后续编译使用。

预处理具有以下作用:

1. 头文件包含(#include

将头文件(内容直接插入源文件,确保函数声明、宏定义等可用。

示例:#include <stdio.h> → 插入printf等函数的声明。

2. 宏展开(#define

替换代码中所有宏定义的符号。

示例:#define PI 3.14 → 代码中的PI替换为3.14。

3. 条件编译(#ifdef 、#if 等)

根据条件决定是否编译某段代码,实现跨平台或调试功能。

示例:#ifdef DEBUG

printf("Debug info");

#endif

4. 注释删除与文本处理

删除所有注释(//、/* */),减少编译器负担。

添加行号标记(#line),便于调试时定位错误。

2.2在Ubuntu下预处理的命令

通过gcc -E -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello.i命令生成hello.i文件,其内容为预处理后的纯C代码,可直接被编译器处理。

图1 预处理的命令与结果

2.3 Hello的预处理结果解析

1.头文件展开

预处理后文件开头包含:(此外还包含unistd.h和stdlib.h的内容)

图2 预处理文件开头内容

2.代码转换

图3 原始代码经预处理结果

3.关键处理细节

注释删除:移除 //大作业的hello.c程序 等所有注释

宏处理:本代码无自定义宏,故无宏展开

条件编译:未使用#ifdef等指令,无变化

特殊标记:保留#line标记辅助调试:

2.4 本章小结

本章通过预处理hello.c程序生成hello.i文件,完整展示了预处理阶段的关键作用:原始30行代码扩展至约3902行,主要源于stdio.h等系统头文件的插入;所有注释被移除但程序逻辑结构完整保留,printf等库函数声明通过头文件被正确定义,使得预处理后的hello.i文件可直接用于后续编译阶段。这一过程实现了从源代码到编译器可处理形式的转换,为程序构建奠定了基础。

第3章 编译

3.1 编译的概念与作用

编译是将预处理后的C代码(.i文件)转换为汇编代码(.s文件)的过程,由编译器(如gcc)完成。这是程序构建的第二个关键阶段,实现了从高级语言到底层机器相关代码的转换。

编译的作用包括:

1.语法语义分析

检查代码是否符合C语言规范;验证类型匹配、函数声明等语义规则。

2.代码优化

根据编译选项(如-Og)进行基础优化;消除冗余计算、简化控制流等。

3.汇编代码生成

生成与目标架构(x86-64)匹配的汇编指令;处理寄存器分配、栈帧布局等底层细节。

3.2 在Ubuntu下编译的命令

图4 编译的命令与结果

3.3 Hello的编译结果解析

3.3.1 数据

在编译过程中,编译器会针对代码中的不同元素采用差异化的处理策略。对于常量数据,编译器会将其直接转换为立即数并嵌入到生成的机器指令中,实现快速访问。变量则通过分配特定的内存地址进行存取操作,在汇编层面表现为寄存器或栈内存的寻址操作。面对各类表达式时,编译器会先进行严格的类型检查和语义分析,然后根据操作符特性生成对应的机器指令序列,同时应用多种优化手段(如常量传播、公共子表达式消除等)来提高执行效率。整个转换过程严格遵循类型系统的规则,确保不同数据类型的操作都能被正确映射到底层硬件指令。

图5 字符串常量

图6 常量与变量

这里常量5直接以立即数形式($5)嵌入指令,编译时确定值,运行时无需额外内存访问。变量存储在栈帧中(如-36(%rbp)),通过基址指针%rbp+偏移量访问,体现变量的内存地址特性。函数参数通过寄存器(如%edi、%rsi)传入,再保存到栈,遵循x86-64调用约定(整型参数用%edi,指针用%rsi)。

3.3.2 赋值

在hello.s文件中,赋值操作主要包括寄存器赋值、内存存储和立即数赋值。

图7 寄存器到内存的赋值

将argc(int 类型,存储在%edi)保存到栈帧-36(%rbp);将argv(char** 类型,存储在%rsi)保存到栈帧-48(%rbp)。

**movq %rax, -16(%rbp)**也是寄存器到内存的赋值,临时存储计算后的地址(如 argv[1] 的地址)。

图8 立即数到内存的赋值

初始化循环变量i = 0,$0是立即数,直接写入内存-20(%rbp)(i的栈位置)。

**addl 1, -20(%rbp)**是内存→运算→内存类型的赋值,实现了循环变量i++,addl 1直接在内存-20(%rbp)上执行+1 操作。

图9 寄存器到寄存器的赋值

%eax存储atoi 的返回值(int 类型),movl将其复制到 %edi(sleep的参数寄存器),将atoi(argv[4]) 的返回值传递给sleep()。

图10 立即数到寄存器的赋值

设置 printf 的浮点参数数量(0), 设置exit(1) 的参数, $1是立即数,直接写入%edi(exit的参数寄存器)。

3.3.3 算术操作

**addl 1, -20(%rbp)**是算术操作,实现了循环变量i++,addl 1直接在内存-20(%rbp)上执行+1操作。

3.3.4 关系操作与控制转移

在汇编代码中,关系操作与控制转移主要通过比较指令+条件跳转实现,用于控制程序流程(如if、for等)。

图11 整形比较与条件跳转

比较$5(立即数)与栈中的argc值(-36(%rbp)),:若cmpl 结果相等(ZF=1),则跳转到.L2(错误处理分支)。

图12 比较指令与条件跳转

比较循环变量i(-20(%rbp))与立即数9,若i <= 9(SF=OF或ZF=1),跳转到.L6(循环体)。

3.3.5 数组操作

在hello.s文件中,数组操作主要体现在对argv(char *argv[])的访问上。

图13 数组操作

数组操作通过基址寄存器加偏移量的方式实现,严格遵循C语言的指针运算规则。对于char *argv[]数组,编译器使用movq指令从栈中加载数组基地址到%rax寄存器,随后通过固定的字节偏移(8×索引值)访问各个元素:argv[1](偏移8字节)、argv[2](16字节)、argv[3](24字节)和argv[4](32字节)。每次访问通过addq计算元素地址,再用movq将指针值加载到目标寄存器(如%rsi、%rcx),最终传递给printf或atoi等函数。

3.3.6 函数操作

在hello.s文件中,函数操作主要包括函数调用约定、参数传递、栈帧管理和返回值处理。

图14 printf和exit调用

call printf@PLT通过PLT(过程链接表)实现动态链接,$1直接写入%edi,退出状态码调用exit。

图15 atoi和sleep调用

argv[4]通过%rdi传递,atoi的整型结果存储在%eax,随后移动到 %edi作为 sleep的参数。%edi存储休眠秒数,调用sleep。

图16 getchar调用

无需寄存器准备参数,调用getchar。

3.4 本章小结

本章解析了hello.c程序的完整编译过程。首先介绍了编译的核心作用,随后在Ubuntu环境下生成并分析了hello.s汇编文件。分析了数据类型、赋值操作、算术/关系运算、数组访问、控制转移以及函数调用等关键环节,揭示了编译器如何将高级语言结构高效转换为底层机器指令。

第4章 汇编

4.1 汇编的概念与作用

汇编是程序构建流程中的第三个阶段,紧随编译之后,主要由汇编器(如as)完成。其任务是将编译器生成的汇编代码(.s文件)翻译为机器指令并打包为目标文件(.o文件),为后续的链接做好准备。

汇编的主要作用包括:

1. 指令翻译

将汇编语言转为机器语言,把人类可读的汇编指令(如movq, call, cmp等)翻译成对应的二进制机器指令;精确映射CPU架构,输出的指令集严格符合目标架构(如x86-64)规范;处理伪指令和宏指令,解析.globl, .text, .data等汇编伪指令以生成正确的段结构。

2. 目标文件生成

构造可链接的中间文件(.o),将翻译后的机器指令、符号表、重定位信息等封装为 ELF(Executable and Linkable Format)格式的目标文件;支持多模块编程,多个.o文件可由链接器组合成完整程序,实现模块化开发。

3. 提供调试和优化基础

保留符号信息,为调试器(如 gdb)提供函数、变量名等符号;为链接做准备,输出中包含重定位条目,链接器可解析和重定地址;作为反汇编基础,程序逆向、调试分析均依赖于汇编视图。

4.2 在Ubuntu下汇编的命令

图17 汇编的命令与结果

4.3 可重定位目标FLF格式

图18 readelf命令与结果

可重定位目标ELF文件是一种中间文件格式,通常用于编译过程中生成的对象文件。它包含了程序的机器代码、数据以及用于后期链接的信息。该文件的格式遵循 ELF标准,旨在为链接器提供足够的信息以便将代码与数据模块结合成一个可执行文件或共享库。可重定位 ELF 文件中包括一个程序头表和一个节区头表,前者用于描述可执行段的内存布局,后者则描述数据和代码的不同段(如.text、.data、.bss等),并提供每个段的基本属性。在此格式中,尤其需要关注的是重定位信息,它帮助链接器在最终链接时调整目标代码和数据的地址。重定位条目通常存储在特定的节区中,标记了需要修改的代码位置、变量地址或函数调用。通过这些信息,链接器能够将不同的目标文件连接成一个统一的可执行文件或共享库,同时确保各模块之间的符号引用能够正确解析。

4.3.1 FLF

图19 ELF头

这是一个标准的64位ELF可重定位目标文件,采用小端字节序,文件头信息显示:Magic为7f 45 4c 46,表明是合法的ELF文件;架构为x86-64;包含14个节区,节区头表从文件偏移1096字节处开始,每个节区头大小为64字节;由于是可重定位文件,程序头表大小为0且入口地址为0x0。

4.3.2 节头

节头是ELF文件中的一个重要结构,它描述了文件中各个节区的属性和位置信息。该ELF文件采用模块化设计,通过节区头部表将程序划分为多个功能区块。包含存储指令的.text段、存放变量的.data段、保存常量的.rodata段,以及符号表等辅助信息区。每个节区都明确记录了位置、大小和访问权限,如.text段可执行、.data段可读写、.rodata段只读。这种结构既隔离了不同类型数据,又为程序加载提供了必要信息,共同构成完整的可执行文件。各节区通过编号唯一标识,其属性设置体现了操作系统的内存保护机制。

图20 节头

4.3.3 重定位节

重定位节节包含了重定位信息,用于修正代码和数据在加载过程中的地址。如图为.rela.text和.rela.eh_frame重定位节。

图21 重定位节

1..rela.dyn 节分析

该节区包含8个重定位条目,主要用于修正代码段(.text)中的地址引用,涉及函数调用和全局数据访问。具体分为两类:

(1) 数据引用修正(R_X86_64_PC32)

作用:修正对 .rodata(只读数据段)的引用。

如:偏移量 0x1c: 引用 .rodata - 4

偏移量 0x7d: 引用 .rodata + 0x2d

将指令中的占位地址替换为 .rodata 段的实际运行时地址(需减去/加上指定偏移)

(2) 函数调用修正(R_X86_64_PLT32)

作用:修正动态链接函数(如 printf、sleep)的调用地址。

例如:偏移量 0x3d: _printf_chk@GLIBC - 4

偏移量 0x11b7: getchar@GLIBC - 4

通过PLT(过程链接表)实现延迟绑定,首次调用时动态解析函数地址

2. .rela.eh_frame 节区分析

该节区包含 1 个条目,用于异常处理框架(EH Frame)的重定位

作用:修正 .eh_frame 对代码段(.text)的引用

例如:偏移量 0x20: 引用 .text + 0, 确保异常处理机制能正确关联到代码段的起始位置

3. 结论

(1 )动态链接支持

函数调用(如 printf、getchar)通过 R_X86_64_PLT32 实现懒绑定。

符号名称中的 @GLIBC 表明依赖 glibc 动态库。

(2 )数据与代码分离

.rodata 的引用需独立重定位,体现代码与只读数据的隔离设计。

(3 )异常处理机制

.eh_frame 的重定位确保栈展开等调试功能能定位到代码段。

重定位节区是链接阶段的核心数据结构,.rela.text 负责修正代码中的外部符号引用,而 .rela.eh_frame 确保调试信息的正确性。通过 R_X86_64_PLT32 和 R_X86_64_PC32 等类型,ELF 文件实现了灵活的地址绑定机制,支持动态链接与位置无关代码(PIC)

4.3.7 符号表

ELF文件的符号表(.symtab)记录了程序中定义的所有符号信息,包括函数名称(如main、puts、exit等)、变量以及它们的类型和绑定属性。每个符号条目详细描述了其在内存中的位置、大小范围以及作用域(全局/局部),为链接器和调试器提供了关键的符号解析依据。符号表通过系统化的组织方式,将程序中的各类标识符与其实际实现关联起来,是ELF文件实现模块化编译和动态链接的重要数据结构基础。

图22 符号表

4.4 Hello.o的结果解析

图23 反汇编结果(部分)

4.4.1 hello.o 反汇编结果与hello.s 的对照分析

1. 数据移动指令

汇编指令:movq %rsi, -48(%rbp) # 保存argv到栈

movl $0, -20(%rbp) # 初始化循环变量i=0

**机器码:**48 89 75 d0 mov %rsi,-0x30(%rbp) # 同义但偏移量编码不同

c7 45 ec 00 00 00 movl $0x0,-0x14(%rbp) # 立即数直接嵌入指令

**分析:**汇编代码使用符号化偏移(如-48),而机器码用补码编码(如0xd0)。立即数$0直接以00 00 00 00形式嵌入。

2. 函数调用指令

汇编指令:call printf@PLT # 显式标注动态链接

机器码:e8 00 00 00 00 call 0x0 # 临时占位

对应重定位条目:

R_X86_64_PLT32 printf-0x4 # 需链接时修正

分析: 汇编代码直接标注@PLT,机器码用e8操作码+全零占位,重定位类型R_X86_64_PLT32指示链接器修正为PLT条目地址。

3. 条件跳转指令

汇编指令:jle .L6 # 基于标签的跳转

机器码:7e 97 jle -0x69 # 计算为.L6的相对偏移

分析: 汇编器将标签.L6转换为当前指令地址与目标地址的差值(-0x69),以补码形式存储。

4.4.2 机器语言的构成

机器语言是计算机能够直接识别和执行的指令集,由一系列二进制代码组成。它的基本构成包括操作码、操作数、寻址模式,以及不同类型的指令,如数据处理指令、控制指令、分支跳转等。

操作码是机器指令的核心,告诉CPU当前这条指令要做什么操作,例如加法(ADD)、减法(SUB)、加载(LOAD)或跳转(JMP)等。它由特定的二进制位组成,每种操作都有唯一编号。处理器根据操作码判断如何解析和执行接下来的内容。

操作数是指令中参与操作的数据。它们可以是立即数(直接写在指令中)、寄存器编号,或是内存地址。在执行指令时,处理器通过操作数来访问数据并执行运算或传输。

寻址模式指定了操作数如何从内存或寄存器中定位。寻址模式可以是直接寻址、间接寻址、基址寻址、相对寻址等。

数据相关指令包括将数据从一个位置移动到另一个位置、执行加减乘除等运算,或加载/存储变量到内存。它们是实现逻辑功能的基础。

控制指令影响程序的运行逻辑或处理器状态,包括中断控制、函数调用、返回、暂停执行等。它们不直接处理数据,但对程序流程至关重要。

分支和跳转指令指令用于改变程序的执行顺序,如jmp、call、ret等,常用于实现循环、条件判断、函数调用。它们通常携带目标地址或偏移量,可以是绝对跳转,也可以是根据条件跳转(如等于则跳转)。

4.4.3 机器语言与汇编语言的映射关系

机器语言与汇编语言之间的关系为"一一对应但可读性不同的映射关系。机器语言是计算机硬件能直接识别和执行的二进制代码,由0和1组成,例如一条指令可能是 10110000 01100001,这对人类几乎不可读。而汇编语言是对机器语言的符号化表示,是介于高级语言和机器语言之间的一种低级语言,它使用助记符来代替二进制指令,使人类程序员更容易理解和编写。例如,上述机器码可以被写成汇编语言形式如:mov al, 0x61,意思是把十六进制的0x61赋值到al寄存器。

每一条汇编语言指令,基本上都可以与一条机器指令一一对应。汇编语言不是抽象层次上的提升,而是对机器指令的直观映射,因此不同的处理器架构拥有不同的机器指令集,也有各自专属的汇编语言风格和语法。编写汇编语言的程序需要汇编器来将其翻译成对应的机器码,生成目标文件或可执行文件。这个翻译过程本质上就是将人类可读的符号转化为硬件可执行的二进制指令序列。

二者的关键差异在于:

1. **操作数表示不同:**机器语言用二进制/十六进制编码,汇编语言用符号(寄存器名、标签等)。

2. 分支/ **调用地址不同:**机器语言使用 相对偏移量,汇编语言使用 符号标签。

4.5 本章小结

本章介绍了汇编语言的基本概念和作用,分析了可重定位目标文件ELF的结构以及ELF文件中各个节和重定位项的具体含义。通过反汇编hello.o文件并与hello.s中的汇编指令进行对比,揭示了汇编指令如何转换为机器码。这一过程有助于更好地理解编译、汇编和链接的工作机制,尤其是符号和内存地址的处理。

第5章 链接

5.1 链接的概念与作用

链接是在编译过程中将一个或多个目标文件(如 hello.o)合并生成最终可执行文件(如hello)的关键步骤。对于单个源文件生成的 hello.o 来说,它只是一个可重定位目标文件,包含了编译后的机器指令、数据、符号信息和重定位记录,但还不能独立运行。链接器的任务就是将这些信息整理、整合,处理符号解析和地址重定位,最终生成一个结构完整、可被操作系统加载执行的ELF可执行文件。

链接的核心作用包括:

1. 符号解析

将目标文件中未定义的外部符号(如调用的函数、使用的全局变量)与定义这些符号的代码进行匹配。

2. 地址重定位

将目标文件中各个代码和数据段的相对地址修正为绝对地址,以满足最终程序的内存布局要求。

3. 节与段的合并

将多个目标文件中的 .text、.data、.bss 等节按规则整合为最终可执行文件中的段结构。

5.2 在Ubuntu下链接的命令

图24 链接的命令与结果

5.3 可执行目标文件hello的格式

5.3.1 ELF

图25 ELF头

ELF 文件是一个面向 x86-64 架构的64位小端格式动态可执行文件(PIE),符合UNIX System V的标准。它的入口地址是0x4010d0,文件共包含27个节区,其中第26个节区保存了节区名称的字符串表。整体结构规整,符合现代Linux系统中常见的可执行文件或共享库的格式,说明它具备较强的可移植性和兼容性。

5.3.2 节头

图26 节头

节头是ELF文件中的一个重要结构,它描述了文件中各个节区的属性和位置信息。每个节区通常包含程序的不同组成部分,如代码段(.text)、数据段(.data)、未初始化数据段(.bss)等。节头的作用是帮助链接器和加载器识别并管理这些节区的内容和位置。节头包括每个节区的名称、类型、标志、大小、偏移量以及与其他节区的关联信息。这些节区在文件中的顺序和属性通过节头表进行管理。节头的存在使得 ELF 文件能够在编译和链接过程中灵活地分配内存、执行重定位操作并有效地组织代码和数据。

5.3.3 程序头

图27 程序头

程序头在 ELF 文件中定义了程序的各个段的属性和内存映射信息。它指示了文件中各个段如何加载到内存中,并在程序执行时如何映射到进程的虚拟地址空间。程序头表是由多个程序头条目组成,每个条目描述了一个段(如代码段、数据段等)的起始地址、大小、类型、权限等信息。程序头的作用主要是在文件加载时指导操作系统如何正确地将文件内容加载到内存,确保程序的各个部分能够正确访问和执行。

5.3.4 段节

图28 段节

段节则描述了ELF文件中的节区(section),这些节区用于存储与程序执行直接相关的数据和代码。每个节区可以包含不同类型的数据,如程序的代码、初始化数据、符号表、重定位表等。段节的作用是帮助链接器和调试器识别和操作这些节区,并在程序执行时组织和管理代码与数据。节头表提供了关于节区的具体信息,包括节区的名称、类型、大小、偏移量等。

5.3.5 Dynamic section

图29 Dynamic section

Dynamic section 是ELF文件中的一个特定节区,通常用于存储与动态链接相关的信息,如符号表、字符串表、重定位信息和动态链接库的路径等。它位于 ELF 文件中的一个特定位置,通过程序头中的动态链接部分指向。这部分信息主要用于在运行时进行符号解析、库加载以及链接操作。Dynamic section 的作用是为程序的动态链接提供必要的数据,确保在运行时能够正确地加载和链接共享库,支持功能的扩展和模块化。

5.3.6 重定位节

图30 重定位节

重定位节是ELF文件的关键部分,用于指导动态链接器在加载程序时修正代码和数据中的地址引用。它们包含两类重定位条目:.rela.dyn为处理数据段(如全局变量)的重定位;.rela.plt为处理过程链接表(PLT)中的函数跳转重定位。

R_X86_64_GLOB_DAT:处理全局变量和函数的地址引用。

R_X86_64_JUMP_SLOT:专用于 PLT 表的函数跳转修正。

重定位节是动态链接的核心数据结构,通过.rela.dyn和.rela.plt协作完成地址修正。R_X86_64_RELATIVE处理基址相关修正,R_X86_64_JUMP_SLOT 实现函数调用的懒绑定,而 R_X86_64_GLOB_DAT 确保全局符号的正确引用。这一机制平衡了加载效率和运行时灵活性,是动态链接库(如 glibc)能高效工作的关键。

5.3.7 符号表

图31 符号表

符号表是ELF文件中的一个关键结构,用于记录程序中所有函数、变量和其他标识符的名称、地址、大小、类型和绑定属性等信息。它的主要作用是在链接和调试过程中提供符号的定义与引用信息。每个符号表条目对应一个符号,可以是全局的、局部的,或是弱绑定的,支持静态和动态链接的不同需求。符号表通常与字符串表配合使用,后者保存符号名称的实际字符串,而符号表中的条目则通过索引引用这些字符串。链接器在链接多个目标文件时,通过符号表来解析符号之间的引用关系,实现代码和数据的整合;而在程序运行时,动态链接器也依赖符号表中的信息进行符号解析和重定位。符号表还为调试器提供了变量和函数的定位依据,使得开发者可以查看、设置断点或追踪程序行为。根据用途的不同,ELF文件中可能包含多个符号表,如静态链接使用的 .symtab和动态链接使用的.dynsym。

5.3.8 其他信息

图32 其他特殊段信息

5.4 hello的虚拟地址空间

图33 虚拟地址空间

在edb的虚拟地址Data Dump视图中,我们可以根据各节区的地址范围和偏移量定位它们在内存中的具体位置。以.init节区为例,这段存储程序初始化代码的机器指令,其虚拟地址起始于0x401000,占据0x1000的偏移空间。通过交叉比对edb显示的地址映射信息,我们就能准确找到程序中每个节区(如.text、.data等)在虚拟内存中的实际存储位置及其内容分布,此部分内容可以与5.3的节头的信息进行对照。

5.5 链接的重定位过程分析

图34 hello文件反汇编结果

在程序链接过程中,链接器会对目标文件进行符号解析和地址重定位处理。以hello程序为例,其反汇编代码起始地址为0x401000,而hello.o目标文件的反汇编代码则从0开始,这是因为链接前的目标文件尚未经过地址分配。hello.o中仅包含.text段和main函数的原始内容,函数调用和静态字符串引用使用临时地址0x0占位;而经过链接后的hello程序通过重定位过程,已将所有的函数调用和字符串引用替换为确定的内存地址,并添加了必要的库函数代码。链接过程主要包含三个关键步骤:首先进行符号解析,将各目标文件中的符号引用与定义进行匹配并建立符号表;随后通过地址重定位修改目标文件中的地址引用,使其指向正确的内存位置;最后合并重复定义的符号,确保最终可执行文件中每个符号只有唯一定义。这一过程使得分散编译的目标文件能够整合为完整的可执行程序。

5.6 hello的执行流程

图35 edb的机器语言

程序的加载和执行过程由操作系统加载器(Loader)和运行时库协同完成。加载器首先将磁盘中的可执行文件映射到内存空间,完成必要的动态链接后,将控制权转交给程序的入口点_start。作为程序执行的起点,_start函数会进一步调用C运行时库的__libc_start_main函数,该函数负责初始化全局变量、设置线程局部存储等运行时环境,并最终调用用户定义的main函数作为程序逻辑的入口。

在main函数执行期间,程序会通过动态链接机制调用各类库函数(如printf、sleep等)。当main函数执行完毕返回后,__libc_start_main会继续处理程序终止流程,包括调用注册的退出处理函数、释放资源等。最终,控制权通过系统调用交还给操作系统,由操作系统回收进程资源并处理退出状态码。

整个过程体现了从系统层到用户层,再从用户层返回系统层的完整程序生命周期管理,其中加载器和C运行时库在底层环境和用户程序之间建立了关键的桥梁作用。

5.7 Hello的动态链接分析

图36 在节头中找到.got.plt的位置

图37 edb中.got.plt的内容(链接前)

图38 edb中.got.plt的内容(链接后)

动态链接的过程可以通过调试工具清晰地观察到地址解析的变化。在程序启动初期,通过`readelf`和`ldd`可以看到`hello`依赖的动态库(如`libc.so.6`)和未解析的PLT/GOT条目。使用GDB/EDB调试时,首次调用如`printf`函数时,其PLT条目(如`0x401030`)会跳转到动态链接器进行符号解析,此时对应的GOT条目(如`0x404028`)初始值指向延迟绑定代码;解析完成后,动态链接器会将`libc`中`printf`的真实地址(如`0x7ffff7e3d200`)回填到GOT中,此后所有对该函数的调用均直接跳转至目标地址。这一机制通过PLT/GOT的协作,实现了动态库函数的延迟绑定和高效调用,调试时可通过对比函数调用前后GOT内存内容的变化(从初始地址到真实库函数地址)直观验证动态链接的过程。

5.8 本章小结

本章系统阐述了程序链接的核心概念及其在构建可执行文件过程中的关键作用。以Ubuntu环境下的hello程序为例,我们通过对比分析链接前后的目标文件(hello.o与hello),深入探究了ELF文件格式的显著差异。借助edb调试器,我们进一步验证了重定位后各子程序(如_start、main等)的最终虚拟地址信息,以及动态链接过程中PLT/GOT表从初始状态到函数地址解析完成的动态变化。

第6章 hello进程管理

6.1 进程的概念与作用

进程是操作系统中对正在运行程序的一种抽象,是程序执行的基本单位。它不仅包含了程序的代码,还包括程序运行时所需的资源,如内存空间、打开的文件、CPU 状态、堆栈、寄存器等。每当一个程序被执行,操作系统就会为其创建一个对应的进程,并为它分配必要的系统资源,使其能够独立运行。

进程的核心作用在于实现程序的并发执行与资源隔离。通过将每个运行中的程序封装为一个独立的进程,操作系统能够同时调度多个任务运行,提高系统的利用率和响应能力。此外,进程之间的地址空间互相隔离,保证了各自的内存和资源不会被随意访问,从而提升了系统的稳定性与安全性。进程还作为调度的基本单位,配合进程调度算法,使操作系统可以根据优先级、时间片等策略灵活控制 CPU 的使用。在用户视角下,一个运行中的程序就是一个进程;而在操作系统内部,进程则是管理系统资源和任务调度的核心对象。无论是简单的 hello 程序,还是复杂的图形界面应用,只有当它被操作系统加载为进程并运行时,它的功能才能真正发挥出来。

6.2 简述壳Shell-bash的作用与处理流程

Shell,特别是常用的 Bash(Bourne Again SHell),是操作系统中用户与内核之间的重要接口,主要作用是接收用户命令、解析并执行。它既是一种命令解释器,也是一个脚本环境,允许用户以交互或脚本的方式控制系统。Bash是用户与 Linux系统交互的桥梁,既能作为日常操作的接口,也能用于编写自动化脚本,在系统管理、开发调试中扮演着极其重要的角色。

Bash 的处理流程如下:

  1. 读取命令:Shell 启动后进入一个循环,等待用户输入命令(或从脚本中读取命令)。

  2. 词法与语法分析:将用户输入的字符串进行分词(tokenize),分析命令结构、参数、重定向、管道等。

  3. 变量替换与通配符展开:处理变量(如 $HOME)、命令替换(如 date)、通配符(如 .c)等。

  4. 查找命令:Shell 会判断命令是内部命令(如 cd)、外部命令(如 ls),还是用户自定义函数或脚本。

  5. 创建子进程执行命令:对外部命令,Shell 会通过 fork()创建一个子进程,然后用 exec()执行对应程序。主Shell进程可以选择等待命令执行完毕,或直接返回(如在后台运行时)。

  6. 处理返回值:Shell接收命令的退出状态码,并决定是否继续执行下一条命令,或根据逻辑控制(如 &&, ||)调整流程。

6.3 Hello的fork进程创建过程

当用户在shell中输入 ./hello命令时,shell首先对该命令进行解析,确认它是一个可执行文件。接着,shell 会通过调用 fork()创建一个新的子进程。fork()会将父进程的地址空间、文件描述符以及相关状态信息复制到子进程中。子进程因此拥有与父进程一致的用户虚拟地址空间副本,包括代码段、数据段、堆区和栈区,从而具备运行同样程序的能力。同时,子进程也会继承父进程打开的文件描述符副本,意味着它能够访问相同的文件资源。

fork()返回后,父子进程将分别沿各自的执行路径继续运行。在子进程中,系统随后调用exec系列函数将 ./hello可执行文件加载进其地址空间,原来的内存内容被新程序替换。最终,子进程便开始执行hello程序中的指令,完成整个从命令输入到程序启动的过程。

6.4 Hello的execve过程

父进程通过调用 fork创建一个新的子进程。与前面所述类似,fork会将父进程的地址空间、打开的文件描述符及其他运行状态复制一份给子进程。此时,子进程拥有与父进程几乎相同的执行上下文。随后,子进程调用execve函数以运行指定的可执行文件。execve需要指定要执行程序的路径以及对应的参数列表。

一旦execve被调用,子进程原有的地址空间和状态信息将被清除,取而代之的是新加载的可执行程序的内容。新的程序被加载至子进程的内存中,从其入口地址开始执行。此过程完成了子进程从"复制父进程"到"执行目标程序"的转换,使其真正成为一个独立运行的新进程。

6.5 Hello的进程执行

当用户在 shell 中输入 ./hello命令时,shell首先调用fork()创建一个子进程,然后该子进程通过execve()将hello程序加载进自身地址空间中,正式启动程序执行。此时,操作系统为这个新进程分配一块独立的用户空间,并将其加入到就绪队列,等待调度器分配CPU时间。

在进程被调度执行之前,操作系统会将该进程的上下文信息加载到CPU中。这些上下文包括程序计数器、通用寄存器、内存映射信息以及打开文件的描述符表。通过这些信息,CPU可以精确恢复并继续该进程的执行。

由于大多数现代操作系统使用的是抢占式多任务机制,进程只能在一个限定时间片内运行。在hello的循环中,每次打印 "Hello"并调用sleep(N) 进行休眠时,程序会主动放弃CPU,进入阻塞状态。在调用sleep的过程中,进程从用户态切换到内核态,由内核来处理定时等待功能。一旦设置的时间间隔过去,操作系统将该进程重新移入就绪队列,并等待再次被调度执行。

这种用户态与核心态之间的切换是操作系统正常管理资源的必经步骤。用户态运行普通应用程序的代码,而核心态拥有访问底层硬件和系统资源的权限。当程序需要进行诸如文件操作、等待计时、内存分配等特权行为时,必须通过系统调用进入内核态处理。

在整个程序执行过程中,hello进程不断被调度执行、进入休眠、重新调度,这一过程依赖操作系统的进程调度器维持公平性与响应性。每一次进程从休眠恢复,或因时间片耗尽而被挂起时,操作系统都必须完成一次进程上下文的切换,确保CPU能够在多个进程之间高效切换而不会造成错误执行。

最终,在循环执行完毕并经过一次阻塞等待后,hello程序会等待用户输入(如回车),再退出进程并由操作系统进行资源回收和清理工作。整个过程中,进程调度、上下文切换和态间转换机制共同保证了hello程序的有序、稳定运行。

6.6 hello的异常与信号处理

6.6.1 回车

图39 回车输出结果

程序会正常运行,运行输出结果出现换行,但当程序执行完毕后,先前输入的回车符会残留在系统输入缓冲区中。这些残留输入会被后续的进程自动捕获处理,出现连续换行。

6.6.2 Ctrl-Z

图40 Ctrl-Z输出结果

暂停当前前台进程,发送 SIGTSTP,程序正常暂停, 并返回shell。

6.6.3 Ctrl-C

图41 Ctrl-C输出结果

终止当前前台进程,发送 SIGINT,程序终止,强制结束程序。

6.6.4 ps

图42 Ctrl-Z后ps输出结果 图43 Ctrl-C后ps的输出结果

显示当前进程状态,Ctrl-Z后,hello程序被暂停,但未结束;Ctrl-C后,hello程序被强制结束。

6.6.5 jobs

图44 jobs输出结果

Jobs可以列出当前终端的后台任务或暂停任务,此处可以查看Ctrl-Z暂停的任务,而在Ctrl-C后,jobs无输出结果。

6.6.6 pstree

图45 pstree的输出结果

以树状结构显示进程关系。

6.6.7 fg

图46 fg输出结果

恢复暂停的进程到前台运行。

6.6.8 kill

图47 kill的输出结果

暂停程序后通过ps指令查看当前的hello程序的PID为5842,利用kill -9 PID指令,可以强制终止进程,默认SIGTERM信号。

6.7本章小结

本章介绍了进程的基本概念及其在计算机系统中的作用,重点分析了hello程序在Shell中创建和执行的过程,探讨了执行过程中可能遇到的异常情况和信号处理机制,包括同步异常、异步信号的处理方式以及进程终止等关键环节。

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1 逻辑地址

逻辑地址是程序直接使用的地址,如代码中的指针变量argv[1]和函数调用地址。它由编译器生成,存在于机器指令中。在程序运行时,CPU通过段式内存管理单元(MMU)先将逻辑地址转换为线性地址。例如,printf函数调用时,其指令地址和参数地址最初都是逻辑地址。

7.1.2 线性地址与虚拟地址

线性地址(32位后也称虚拟地址)是逻辑地址经段式转换后的结果,构成进程独立的地址空间。代码中sleep(atoi(argv[4]))等系统调用涉及的地址均为虚拟地址,通过页表映射实现进程隔离。例如,argv数组的存储位置在进程虚拟地址空间中连续,但实际物理内存可能分散。

7.1.3 物理地址

物理地址是数据在内存芯片上的实际位置。当执行printf时,虚拟地址经页表查询转换为物理地址,如0xffff880012345678→0x1a2b3c4d。sleep调用触发CR3寄存器切换页表,内核通过MMU完成地址转换,目标页未加载则引发缺页异常。

7.2 Intel逻辑地址到线性地址的变换-段式管理

7.2.1 逻辑地址结构

逻辑地址由16位段选择符(Segment Selector)和32位偏移量(Offset)组成:

段选择符(16位):高13位:索引(Index),用于在GDT(全局描述符表)或LDT(局部描述符表)中查找段描述符。

第2位(TI):0=使用GDT,1=使用LDT。

低2位(RPL):请求特权级(0=内核,3=用户)。

偏移量(32位):段内相对地址。

7.2.2 段描述符

段描述符是一个 8字节(64位) 的数据结构,存储在 GDT/LDT 中,包含:

段基址(Base Address):32位,段的起始线性地址。

段界限(Limit):20位,段的大小(单位由G位决定)。

访问权限(Type):代码/数据段、可读/可写、特权级等。

G位(Granularity):0=界限单位是字节,1=界限单位是4KB。

7.2.3 转换过程

CPU解析段选择符:根据TI位选择 GDT(TI=0)或LDT(TI=1)。用Index×8(因为每个描述符8字节)计算描述符在表中的位置。

从 GDT/LDT 加载段描述符:检查段描述符的有效性(如段是否存在、权限是否合法)。

计算线性地址:线性地址 = 段基址(来自段描述符)+偏移量

检查段界限:确保偏移量≤段界限,否则触发#GP(General Protection Fault) 异常。

图48 逻辑地址到线性地址转换流程图

7.3 Hello的线性地址到物理地址的变换-页式管理

7.3.1 页式管理核心概念

页表:多级结构(x86-64采用4级页表),存储虚拟页到物理页的映射。

页帧:物理内存被划分为固定大小的块(通常4KB)。

MMU(内存管理单元):硬件自动完成地址转换。

7.3.2 转换流程(以x86-64 为例)

在x86-64架构下,当hello程序访问变量argv[1]时,其虚拟地址(如0x7ffd12345678)到物理地址的转换通过多级页表机制完成。首先,CPU会将48位有效虚拟地址按位域拆分为五部分:[47:39]位作为PML4索引,[38:30]位作为页目录指针索引,[29:21]位作为页目录索引,[20:12]位作为页表索引,[11:0]位则保留为页内偏移。转换过程始于CR3寄存器中存储的当前进程PML4表物理地址,通过各级索引逐级查找下一级页表的物理地址,最终定位到具体的页表项(PTE)。PTE中存储的物理页帧号(PFN)与原始虚拟地址中的页内偏移量组合,形成最终的物理地址。这种分级查找机制既实现了灵活的内存管理,又通过硬件加速保证了地址转换效率。

图49 线性地址到物理地址的变换

7.3.3 关键机制

TLB(快表):缓存常用地址映射,加速转换。

缺页异常:若PTE标记为"不存在",触发缺页处理程序加载数据到内存。

写时复制:fork()创建的子进程共享父进程页表,写操作时复制新物理页。

7.3.4 与Hello 程序的关联

代码段:只读权限,物理页可能被多个进程共享(如动态库)。

堆栈段(argv):用户态可读写,COW机制保证进程隔离。

sleep()系统调用:切换到内核态时,使用内核页表(CR3切换)。

7.4 TLB与四级页表支持下的VA到PA的变换

7.4.1 TLB 加速查询

TLB是CPU内部的高速缓存,存储近期使用的虚拟页号→物理页帧号(VPN→PFN)映射。当CPU访问虚拟地址 VA 时,首先查询TLB,命中则直接获取PFN,与页内偏移组合成物理地址 PA,无需访问内存页表。未命中则触发页表遍历(Page Walk),通过四级页表查询映射,并将结果缓存到TLB。

图50 具有快表的地址变换

7.4.2 四级页表遍历

若TLB未命中,MMU按以下步骤查询四级页表(以48位虚拟地址为例)

1.CR3寄存器

存储当前进程的 PML4表(Page Map Level 4) 的物理地址。

2.逐级索引:

PML4(Level 4):用VA[47:39] 索引PML4表,获取PDPT表物理地址。

PDPT(Level 3):用VA[38:30] 索引PDPT表,获取PD表物理地址。

PD(Level 2):用VA[29:21] 索引PD表,获取PT表物理地址。

PT(Level 1):用VA[20:12] 索引PT表,获取页表项(PTE),其中包含目标物理页帧号(PFN)。

3.组合物理地址:

PA = (PFN << 12) | VA[11:0] // PFN左移12位(页大小4KB),加上页内偏移。

7.4.3 变换流程

假设访问虚拟地址 VA = 0x7ffd12345678:

1.TLB查询:若缓存中存在0x7ffd12345→0x8000,则直接得到PA = 0x800678。

2.TLB未命中时

PML4:VA[47:39]=0x1FF → 查PML4表得PDPT地址。

PDPT:VA[38:30]=0x1FE → 查PDPT表得PD地址。

PD:VA[29:21]=0x123 → 查PD表得PT地址。

PT:VA[20:12]=0x456 → 查PT表得PFN=0x8000。

PA:0x8000 << 12 | 0x678 = 0x8000678。

3.更新TLB:将0x7ffd12345→0x8000 缓存到TLB。

7.4.4 关键机制

TLB作用:避免每次地址转换都遍历四级页表,提升性能。

页表缓存:各级页表条目可能被CPU缓存(如PDPT/PD/PT)。

缺页处理:若PTE标记为"不存在",触发缺页异常,由操作系统加载数据到内存。

权限检查:PTE中的权限位(R/W/U/S)确保安全访问。

7.4.5 性能优化

大页:减少页表层级(如2MB页跳过PT层),降低TLB缺失率。

PCID(Process Context ID):避免进程切换时TLB刷新。

预取:硬件预判页表访问路径。

7.5 三级Cache支持下的物理内存访问

在x86-64架构中,当CPU通过页表转换获得物理地址(PA)后,实际访问内存数据需要经过 三级缓存(L1/L2/L3 Cache) 的协同操作。

7.5.1 缓存层级结构

表1 缓存层级结构表

缓存级别 位置 典型容量 访问延迟 特点
L1 Cache CPU核心内 32-64KB 1-3周期 分指令/数据缓存
L2 Cache CPU核心内 256-512KB 10-15周期 统一缓存
L3 Cache 多核心共享 2-32MB 30-50周期 共享缓存(Last-Level Cache)
主内存 内存条 GB级 100+周期 DRAM存储

7.5.2 物理地址访问流程

假设CPU要读取物理地址 PA = 0x8000678 的数据:

1.L1 Cache 查询

用PA的 索引位 查找L1数据缓存组(Set)

对比标签位(Tag): 命中:直接返回数据(1-3周期)

未命中:触发L2查询

2.L2 Cache 查询

查询统一缓存(不分指令/数据)

若命中,数据返回CPU并回填L1(10-15周期)

未命中则查询L3。

3.L3 Cache 查询

多核心共享,检查其他核心的缓存一致性(MESI协议)

命中则数据返回并逐级回填L2/L1(30-50周期)

未命中则访问主内存

4. 主内存访问

通过内存控制器(IMC)访问DRAM

加载数据到L3,再逐级回填(100+周期)

7.5.3 与hello 程序的关联

当hello程序执行时,CPU首先通过EIP寄存器获取下一条指令的地址,该地址对应的代码段数据会经过三级缓存体系进行高效加载------优先从速度最快的L1指令缓存中读取,若命中则立即执行,否则依次向L2、L3缓存查询,直至必要时从主内存加载指令数据。对于程序中访问的argv[1]参数数据,其经过页表转换后得到的物理地址会首先查询L1数据缓存,若为首次访问(冷未命中),则触发完整的缓存层级查询流程,最终从主内存获取数据并逐级回填至L3、L2和L1缓存,形成后续访问的加速基础。当执行printf输出时,对缓冲区的写入操作会触发处理器的"写分配"策略,即先将被修改数据所在的整个缓存行(通常64字节)从缓存或内存读取到L1,再进行局部修改,这种机制既保证了多核环境下缓存一致性(通过MESI协议),又避免了直接写内存带来的性能损耗。整个执行过程通过三级缓存与内存的协同,将平均内存访问延迟从100个时钟周期以上优化至个位数周期级别,显著提升了程序运行效率。

7.6 hello进程fork时的内存映射

7.6.1 fork 时的内存管理机制

当hello进程通过fork()创建子进程时,Linux内核采用写时复制(COW)机制管理内存。内核会复制父进程的进程控制块、内存描述符(mm_struct)、页表和虚拟内存区域(VMA)链表等关键结构,但这些副本初始时与父进程共享相同的物理内存页,仅当实际需要修改时才会进行真正的内存复制,从而显著提升fork效率。

7.6.2 写时复制的触发条件

COW机制根据不同的操作类型动态处理内存共享:

**读取操作:**父进程或子进程访问共享页时,直接读取原物理页,不触发复制。

**写入操作:**任一进程首次尝试修改共享页时,内核分配新物理页并复制数据,更新页表以解除共享关系。

execve() 调用:若子进程通过exec加载新程序,内核立即释放所有共享页,重新建立独立的内存映射。

7.6.3 实现要点

内核通过以下步骤实现COW机制:

页表标记:fork后,所有共享的页表项(PTE)被设为只读,并添加COW标志位(如x86的_PAGE_BIT_COW)。

异常处理:当进程尝试写入COW页时,触发缺页异常,内核分配新物理页、复制数据,并更新PTE的权限和物理地址。

引用计数:每个物理页通过_refcount记录被引用的进程数,归零时自动释放内存。

7.6.4 hello 程序的内存映射

代码段:父进程和子进程共享同一物理页(只读),无COW开销(如printf的指令代码)。

数据段(如argv[] **):**初始共享物理页,若子进程修改argv[1],则触发COW复制。

堆栈段:每个线程拥有独立栈空间,fork时完全复制,不共享。

7.7 hello进程execve时的内存映射

当hello进程调用execve()加载新程序时,Linux内核会完全重置进程的内存空间,建立全新的内存映射结构。整个过程可分为以下阶段:

1. 原内存空间的清理

释放所有现有内存映射(代码/数据/堆/栈段)

清除原页表(PML4/PDPT/PD/PT)

保留文件描述符表、信号处理等非内存资源

2. 新程序的内存映射建立

代码段:映射可执行文件的.text节(只读,共享物理页)

数据段:.data:初始化全局变量(私有可写)

.bss:未初始化全局变量(清零页)

堆空间:通过brk初始化堆起始地址

栈空间:新建用户态栈(包含argv/envp参数)

3. 动态链接处理(如使用glibc

映射动态链接器ld.so到内存

递归加载依赖的共享库(.so文件)

执行重定位和符号解析

4. 权限与特性设置

代码段:PROT_READ|PROT_EXEC

数据段:PROT_READ|PROT_WRITE

启用ASLR时随机化加载基址(通过mmap的MAP_RANDOMIZE)

5. 与fork 的协同

若通过fork()+execve()启动,fork保留COW映射,execve完全覆盖原有映射,原父进程内存不受影响

7.8 缺页故障与缺页中断处理

7.8.1 缺页异常的触发机制

当程序访问的虚拟地址对应的物理页面尚未加载到内存时,硬件内存管理单元(MMU)会立即检测到这一异常情况。这种情况可能由两种原因导致:一是该页面之前被操作系统置换到了磁盘交换区,二是程序首次访问这个特定的内存区域。此时处理器会产生缺页中断信号,暂停当前正在执行的指令流,保存程序现场信息,并将控制权转移到操作系统内核的缺页异常处理程序。

7.8.2 内核响应与诊断过程

操作系统内核接收到缺页中断后,首先会通过CR2寄存器获取引发异常的虚拟地址,然后查询该地址对应的页表项进行诊断。内核需要区分多种缺页类型:对于全新的页面访问需要初始化新页面;对于被换出的页面需要从磁盘重新加载;而对于权限违规的访问则会直接终止进程。这一诊断过程确保了系统能够针对不同类型的缺页采取正确的处理策略。

7.8.3 页面调度与加载流程

对于合法的缺页请求,内核会启动页面调度流程:首先从物理内存空闲列表中分配新的页帧,若内存不足则触发页面置换算法释放空间。接着根据页面类型进行数据加载:文件映射页从对应磁盘文件读取,匿名页则进行清零或写时复制处理。在Linux系统中,这一过程可能涉及I/O设备操作、文件系统交互等多个子系统协同工作。

7.8.4 页表更新与执行恢复

完成物理页加载后,内核会更新页表项,建立虚拟地址到物理页帧的正确映射,并刷新TLB缓存保证一致性。最后,内核恢复之前保存的进程上下文,将进程状态重新设为可运行状态。当该进程再次被调度执行时,CPU会重新执行原先触发缺页异常的指令,此时由于页面已加载到内存,指令能够正常执行下去。整个过程对应用程序完全透明,确保了虚拟内存系统的无缝运作。

7.9动态存储分配管理

动态内存管理是程序运行时通过malloc、free等函数在堆(Heap)区域分配和释放内存的过程。printf等函数在需要缓冲区时会调用malloc动态分配内存。

7.9.1 基本管理方法

1. 显式分配与释放

malloc:分配指定大小的内存块,返回起始地址

free:释放已分配的内存,供后续重用

calloc:分配并清零内存

realloc:调整已分配内存的大小

2. 底层实现依赖

系统调用:如brk/sbrk扩展堆空间,或mmap直接映射匿名页

内存管理器:如glibc的ptmalloc、Google的tcmalloc等

7.9.2 核心分配策略

1. 空闲链表管理

隐式空闲链表:通过内存块头部信息(如大小、使用标志)串联所有块

显式空闲链表:单独维护空闲块链表,加速分配(如fast bins)

图51 隐式空闲链表

图52 显式空闲链表

2. 分配算法

表2 分配算法对比表

算法 原理 特点
首次适应 从链表头部查找第一个满足的块 简单但易产生碎片
最佳适应 查找最小满足的块 碎片较小但搜索开销大
最差适应 查找最大的空闲块 减少小碎片但利用率低
伙伴系统 按2的幂次大小分割合并块 减少外部碎片,适合内核

3. 优化技术

分箱(Binning):按大小分类空闲块(如small/large bins)

内存池:预分配固定大小块,减少碎片(如obstack)

本地缓存:每线程维护独立缓存,避免锁竞争(tcmalloc特性)

7.9.3 对碎片的处理策略

1. 内部碎片

成因:分配块大于请求大小(如对齐要求)

缓解:使用更精细的大小分类(如ptmalloc的fast bins)

2. 外部碎片

成因:空闲内存分散无法合并解决:

解决方法:压缩:移动已分配块合并空闲区(需程序配合)

合并:释放时检查相邻块是否空闲并合并(coalescing)

7.9.4 与printf 的关联

当printf函数需要缓冲区来存储格式化输出内容时,其内存管理策略会根据输出规模动态调整:对于首次调用或少量输出,函数可能优先使用预分配的静态缓冲区以避免频繁调用malloc带来的性能开销;当输出内容超出静态缓冲区容量时,则会动态分配堆内存以满足需求;而在处理大量输出数据时,函数会通过realloc机制逐步扩展缓冲区大小,确保既能高效利用内存,又能适应不同规模的输出需求。这种分层缓冲策略有效平衡了内存使用效率和性能表现。

7.9.5 与现代分配器的对比

表3 现代分配器

分配器 策略特点 适用场景
ptmalloc 分箱+线程缓存 通用程序(glibc默认)
tcmalloc 线程本地缓存+全局堆 高并发服务
jemalloc 多arena减少竞争+低碎片 内存密集型应用

7.10本章小结

本章系统性地阐述了hello程序在计算机系统中的完整内存管理机制,包括从逻辑地址到物理地址的转换全过程。内容涵盖Intel架构下的段式地址转换、四级页表支持的页式管理、TLB加速机制以及三级缓存体系对物理内存访问的优化。同时详细分析了hello进程在fork和execve时的内存映射变化,探讨了缺页异常的处理流程,并介绍了动态内存分配的核心策略。这些机制共同构成了现代操作系统高效、安全的内存管理体系,为hello程序的运行提供了可靠的存储支持。

结论

一、hello 程序在计算机系统中的完整生命周期分析

  1. 编写阶段

程序员使用文本编辑器或IDE编写C语言源代码,保存为hello.c文件。这是程序生命周期的起点。

  1. 预处理阶段

预处理器处理源代码中的#include和宏定义等指令。它将头文件内容插入到源代码中,并展开所有宏。最终生成一个纯C代码的中间文件。

  1. 编译阶段

编译器将预处理后的C代码转换为汇编语言代码。这个过程包括词法分析、语法分析和语义分析。生成的汇编代码针对特定CPU架构优化。

  1. 汇编阶段

汇编器将汇编代码转换为机器码目标文件。这个目标文件包含二进制指令和符号表。此时代码已经是处理器可执行的格式,但还不完整。

  1. 链接阶段

链接器将目标文件与标准库合并,解析外部函数引用。它确定所有函数和变量的最终内存地址。最终生成可执行的二进制文件。

  1. 加载阶段

操作系统创建新进程并分配内存空间。加载器将可执行文件读入内存,建立运行环境。程序计数器指向main()函数开始执行。

  1. 执行阶段

CPU从内存读取并执行程序指令,程序按照指令顺序逐步执行。

  1. 终止阶段

main()函数返回后,程序调用退出系统调用。操作系统回收内存、文件描述符等资源。父进程接收程序的退出状态码。

  1. 系统支持

整个过程依赖CPU、内存管理单元和操作系统的协同工作。硬件执行指令,操作系统管理资源。各子系统共同确保程序正确运行。

二、学习感悟

在深入分析hello程序的完整生命周期后,我对计算机系统的设计与实现有了更深刻的认识。计算机系统通过精妙的分层抽象,将复杂的硬件细节隐藏在简洁的软件接口之下,这种设计哲学使得程序员能够专注于业务逻辑而无需关心底层实现。软硬件协同设计的思想贯穿始终,从编译器的代码优化到操作系统的内存管理,再到硬件的地址转换和缓存机制,每一层都在为提升系统性能而努力。特别是在资源管理方面,系统必须不断权衡性能与资源消耗的关系,比如写时复制机制虽然优化了fork性能,却可能增加后续的写入开销;TLB和缓存虽然加速了访问,但也带来了额外的管理复杂性。这种权衡取舍体现了系统设计的艺术性。

我深刻体会到计算机系统设计的精妙之处。在课堂上学习操作系统原理时,我们往往只关注理论概念,而通过这次对hello程序的完整分析,我才真正理解到这些理论是如何在实际系统中落地实现的。比如虚拟内存机制,在课本上可能只是简单的页表转换示意图,但实际系统中需要考虑TLB缓存、多级页表、缺页处理等诸多细节。这种理论与实践的结合让我对计算机系统的认识更加立体和全面。

通过这次研究,我还认识到计算机系统设计中的一些有趣现象。有时候为了提升某个特定场景的性能,可能会在其他方面做出妥协。就像写时复制机制,虽然极大地优化了fork操作的性能,但后续的写入操作却需要付出额外开销。这种现象在系统设计中随处可见,需要我们根据实际应用场景做出合理的选择。这也让我明白,优秀的系统设计不是追求某个指标的极致,而是要在多个维度上找到最佳的平衡点。

这次对Hello程序的分析经历,让我对计算机系统的认识从抽象的理论概念转变为具体的实现细节。这种转变不仅加深了我的理解,也激发了我进一步探索系统底层原理的兴趣。我认为这种将理论知识与实际系统相结合的学习方法非常重要,它帮助我建立了完整的知识体系,也为未来的研究和开发工作打下了坚实基础。

附件

所有的中间产物文件:

hello.c:hello源代码

hello.i:由hello.c预处理得到的文件

hello.s:编译hello.i后得到的汇编程序

hello.o:汇编后得到的可重定位目标文件

hello:可执行目标文件

hello.txt:hello的elf文件

elf.txt:hello.o的elf文件

hello1.s:hello.o的反汇编文件

hello2.s:hello的反汇编文件

参考文献

1\] ++[https://www.csdn.net/](https://www.csdn.net/ "https://www.csdn.net/")++ \[2\] https://chatgpt.com/c/67cd8d25-b608-800d-9226-b320f856ac24. \[3\] Randal E. Bryant David R. O' Hallaron. 深入理解计算机系统(第三版).机械工业出版社

相关推荐
nianniannnn4 小时前
HNU计算机系统期中题库详解(一)计算机组成原理(CPU、指令系统、存储器、运算基础)
计算机系统
人邮异步社区8 天前
为什么需要学习计算机组成原理?
程序员·计算机系统·计算机原理
CheerWWW10 天前
深入理解计算机系统——位运算、树状数组
笔记·学习·算法·计算机系统
jllws11 个月前
理解计算机系统_链接_重定位(二)
计算机系统
jllws11 个月前
理解计算机系统_链接_符号解析和重定位的原理(二):重定位
计算机系统
unicore1591 个月前
程序人生-Hello’s P2P
计算机系统
ZhangShuangqi2 个月前
哈工大计算机系统程序人生Hello’s P2P
计算机系统
默默无名的大学生2 个月前
01 GCC—从高级语言到机器语言的过程
计算机系统
曦月逸霜3 个月前
深入理解计算机系统——学习笔记(持续更新~)
笔记·学习·计算机系统