摘 要
本文以"Hello"程序为研究对象,系统性地剖析了一个C语言程序在现代计算机系统中的完整生命周期。通过P2P(Program to Process)和020(From Zero to Zero)两条主线,详细阐述了hello程序从源代码到进程执行的全过程,涵盖了预处理、编译、汇编、链接等构建阶段,以及进程管理、存储管理、I/O管理等运行时机制。研究采用GCC工具链在Ubuntu环境下进行实践验证,结合readelf、objdump、gdb等工具深入分析了ELF文件格式、机器指令生成、虚拟地址空间映射、动态链接机制等底层原理。本文不仅揭示了高级语言程序与底层系统硬件之间的协同工作机理,更通过hello这一简单案例生动展现了计算机系统各层次间的精密配合与抽象设计哲学,为深入理解计算机系统整体架构提供了典型范例。
****关键词:****计算机系统;进程管理;存储管理;ELF格式;动态链接
(摘要0分,缺失-1分, 根据内容精彩称都酌情加分0-1分 )
目 录
[++++第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.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的异常与信号处理)
[++++第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 缺页故障与缺页中断处理)
[++++第8章 hello的IO管理++++](#第8章 hello的IO管理)
[++++8.1 Linux的IO设备管理方法++++](#8.1 Linux的IO设备管理方法)
[++++8.2 简述Unix IO接口及其函数++++](#8.2 简述Unix IO接口及其函数)
[++++8.3 printf的实现分析++++](#8.3 printf的实现分析)
[++++8.4 getchar的实现分析++++](#8.4 getchar的实现分析)
第1章 概述
1.1 Hello简介
Hello,一个看似简单却承载着计算机系统完整生命旅程的程序。作为每一位程序员初识编程世界的第一位朋友,Hello不仅是一段输出问候语的代码,更是一个贯穿从源程序到进程(P2P)、从零到零(020)全过程的经典范例。
1.1.1所谓 P2P(Program to Process),是指 hello.c 从一个静态的 C 语言源文件(Program)转变为操作系统中一个活跃运行的进程(Process)的全过程。这一转变并非一蹴而就,而是经历四个关键阶段:
(1)预处理:预处理器处理所有以 # 开头的指令,包括展开 #include <stdio.h> 等头文件、替换宏定义、处理条件编译,并删除注释,最终生成扩展名为 .i 的预处理文件 hello.i。
(2)编译:编译器对 hello.i 进行词法分析、语法分析、语义检查与优化,将其翻译为与目标架构相关的汇编语言代码,输出为 hello.s 文件。
(3)汇编:汇编器将 hello.s 中的汇编指令逐条转换为对应的机器码,生成可重定位的目标文件 hello.o。此文件包含二进制代码、符号表及重定位信息,但尚不能独立运行。
(4)链接:链接器将 hello.o 与 C 标准库(如 libc 中的 printf、sleep、atoi 等函数实现)进行符号解析与地址重定位,最终生成完整的可执行文件hello.out。
至此,hello.c 完成了从人类可读源码到机器可执行程序的转变。
随后,在 Shell中执行 ./hello 学号 姓名 手机号 秒数 命令时,Shell 调用 fork() 创建子进程,并通过 execve() 系统调用加载 hello 可执行文件。操作系统为其分配虚拟地址空间,建立页表映射,将程序段、数据段等载入内存,并跳转至入口点,通常为 _start,最终调用 main 函数。此时,Hello 正式成为一个进程。
1.1.2所谓020(From Zero-0 to Zero-0),描述的是 Hello 在内存中的生命周期:始于无,终于无。
初始状态(Zero):
在执行前,系统内存中不存在与 hello 相关的任何代码或数据。磁盘上的 hello 文件仅是一段静态字节序列。
加载与运行:
当 execve() 被调用,内核为 Hello 进程创建进程控制块(PCB)、分配虚拟地址空间,并通过内存映射(mmap)机制将可执行文件的各段(.text, .data, .bss 等)映射到虚拟内存。MMU(内存管理单元)配合 TLB、多级页表与 Cache,完成虚拟地址(VA)到物理地址(PA)的高效转换。CPU 依据时间片调度策略,取指、译码、执行 Hello 的指令流,使其在硬件(CPU/RAM/IO)上"驰骋"。
终结与回收(Zero):
当 main() 函数返回或调用 exit(),Hello 进程终止。其父进程(Shell)通过 wait() 或类似机制回收子进程资源,内核释放其虚拟内存空间、页表项、文件描述符等所有相关数据结构。至此,Hello 在系统中不留痕迹,真正实现了 From Zero to Zero。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
- 硬件环境:
处理器:Intel(R) Core(TM) i9-14900HX
机带RAM:16.0GB
-
系统类型:64位操作系统,基于x64的处理器
-
软件环境:Windows11 64位,VMware,Ubuntu 22.04.4 LTS
-
开发与调试工具:gcc,gdb,readelf,vim,objdump
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
|------------|-----------------------------|
| 文件名 | 作用 |
| hello.i | 预处理后得到的文本文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello | 完整的可执行文件hello.out |
| hello.asm | 反汇编hello得到的反汇编文件 |
| hello.elf | 用readelf读取hello得到的ELF格式信息 |
| hello2.asm | 反汇编hello.o得到的反汇编文件 |
| hello2.elf | 用readelf读取hello.o得到的ELF格式信息 |
表1-1 中间结果文件
1.4 本章小结
本章系统介绍了"hello"程序作为理解计算机系统完整生命周期的典型案例,重点阐述了P2P和020两个核心概念,完整呈现了从源代码到进程执行的完整技术路径。本章还列出了研究过程中使用的软硬件环境和开发调试工具以及生成的中间结果文件及其在程序构建过程中的具体作用。这些内容为后续深入分析hello程序在计算机系统各层次的实现奠定了基础。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是C语言程序编译流程中的第一个阶段,由C预处理器在真正编译之前对源代码进行文本层面的处理。它并不理解C语言的语法结构,而是依据以字符 # 开头的预处理指令,对源文件执行一系列纯文本操作,最终生成一个不含任何预处理命令、但内容已被扩展和替换的中间C程序文件(通常以 .i 为扩展名)。
2.1.1预处理的概念
预处理本质上是一个宏展开与文本替换的过程。对于 hello.c 这样的源文件,预处理器会逐行扫描代码,识别并执行所有预处理指令。经过预处理后,原始的 hello.c 被转换为一个完整的、自包含的C源文件 hello.i,其中已内联了所有依赖的头文件内容,且不再包含任何 # 指令。该文件仍为纯文本格式,可直接作为编译器的输入。
2.1.2预处理的作用
预处理在软件开发中扮演着重要的角色,其主要作用包括:
头文件展开:将外部声明(如标准库函数 printf、sleep、atoi 等)通过 #include 引入当前源文件,实现代码模块化和接口复用,避免重复定义。
宏替换:通过 #define 定义常量或函数式宏,在预处理阶段完成文本替换,提高代码可读性、可维护性,并减少硬编码。
条件编译支持:利用 #if、#ifdef、#else、#endif 等指令,根据编译环境(如操作系统、调试模式)动态包含或排除代码段,增强程序的可移植性和灵活性。
清理源码:自动删除所有注释和多余的空白字符,生成更紧凑、规范的代码文本,为后续编译阶段提供干净的输入。
特殊指令处理:支持如 #pragma(向编译器传递特定指令)、#error(在预处理阶段强制报错)等高级功能,辅助编译控制与错误检查。
综上所述,预处理虽不涉及语义分析或代码优化,却是连接程序员编写逻辑与编译器理解代码之间不可或缺的桥梁。对于 hello.c 而言,正是预处理将其从一个简洁的源文件扩展为包含完整标准库声明的可编译单元,为后续的编译、汇编和链接奠定基础。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i

图2-1预处理命令
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3061行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3048行到3061行,hello.i文件主函数之前的部分涉及到了对头文件stdio.h,unistd.h,stdlib.h进行依次展开:
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
|
|
| 图2-2 hello.i部分内容 | 图2-3 hello.i部分内容 |
另一方面hello.c的主函数源代码,移除了注释行:

图2-4 hello.i部分内容
2.4 本章小结
本章阐述了预处理器对宏定义、头文件包含及条件编译的处理机制。通过 gcc -E 生成的 hello.i 文件,验证了预处理阶段执行的文本替换、注释移除等操作,展现了模块化编程的底层实现基础
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是C程序构建过程中的第二个关键阶段,特指将预处理生成的 .i 文件(如 hello.i)转换为与目标机器架构相关的汇编语言程序(.s 文件,如 hello.s)的过程。该阶段由编译器(如 GCC 中的 cc1 组件)完成,其核心任务是将高级语言描述的程序逻辑,系统地翻译为低级但人类可读的汇编指令序列。
3.1.1编译的概念
编译是一个结构化的语言转换过程。编译器首先对 hello.i 进行词法分析,将字符流切分为关键字、标识符、常量等记号(token);随后进行语法分析,依据C语言文法规则构建抽象语法树;接着通过语义分析检查类型一致性、变量声明合法性等语义约束。在此基础上,编译器生成中间表示(IR),并根据优化级别(如 -Og)执行一系列代码优化,最终将优化后的中间代码映射为目标平台(如 x86-64)的汇编指令,输出为文本格式的 hello.s 文件。该文件虽仍为文本,但每一条语句已对应具体的机器操作,如寄存器使用、内存访问、函数调用等。
3.1.2编译的作用
语法与语义验证:在翻译过程中,编译器严格检查源代码是否符合C语言规范。例如,若 hello.c 中存在未声明的变量或参数数量错误,编译阶段将报错并终止,从而在早期发现程序缺陷。
生成平台相关汇编代码:编译器根据指定的目标架构(本例中为 -m64 指定的 x86-64),将高级语言操作(如 printf 调用、for 循环、atoi 转换)转化为对应的汇编指令(如 call printf@PLT、jmp 循环控制、mov 数据搬运等),确保程序能在特定硬件上运行。
代码优化:在保证语义等价的前提下,编译器对中间代码进行优化,如常量折叠、死代码消除、循环不变量外提等。虽然本实验使用 -Og(优化以调试友好为主),但仍会进行基础优化,提升程序执行效率。
抽象与隔离硬件细节:编译过程将程序员关注的逻辑(如"输出十次问候")与底层硬件实现(如栈帧布局、寄存器分配、调用约定)解耦。开发者无需了解x86-64指令集细节,即可编写可移植代码,而编译器负责生成符合ABI(应用二进制接口)规范的汇编输出。
综上,编译是从高级语言迈向机器可执行代码的关键桥梁。对于 hello.c 而言,正是通过编译,其简洁的C逻辑被精确、高效地转化为 x86-64 汇编指令,为后续的汇编与链接阶段提供正确且平台适配的输入,奠定了程序最终可运行的基础。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s

图3-1 编译命令
3.3 Hello的编译结果解析

图3-2 hello.s部分内容

图3-3 hello.s部分内容

图3-4 hello.s部分内容

图3-5 hello.s部分内容
3.3.1 数据类型的处理机制
在汇编层面,不同类型的变量和常量有着截然不同的存储和处理方式:
字符串常量的处理
在.rodata只读数据段中,字符串常量被静态存储。hello.s中定义了两个标签:
.LC0:存储程序使用说明字符串"用法: Hello 学号 姓名 手机号 秒数!\n"
.LC1:存储格式化输出字符串"Hello %s %s %s\n"
在main函数中,通过leaq(加载有效地址)指令获取这些字符串的地址,而非内容本身。这种设计既保证了数据的只读安全性,又通过地址引用提高了访问效率。
变量的存储管理
源程序中的局部变量int i不占用静态存储空间,而是在函数调用时动态分配于栈上。当控制流进入main函数,栈指针%rsp会向下调整,为局部变量、函数参数和临时数据开辟存储区域。这种栈上分配方式实现了高效的自动内存管理。
3.3.2 赋值与初始化操作
局部变量i的初始化i = 0被编译为指令:
movl $0, -4(%rbp)
这条指令将立即数0移动到相对基址指针%rbp偏移-4字节的位置,该位置正是变量i在栈帧中的存储单元。这种基于偏移的寻址方式是实现栈帧变量的关键技术。
3.3.3 类型转换机制
类型转换在汇编层面体现为不同的数据解释和处理方式:
(1)显式类型转换
对atoi(argv[4])的调用将char*指针指向的字符串转换为整数。汇编代码通过call atoi@PLT调用标准库函数,该函数解析数字字符串并返回整数值。这种显式转换需要专门的库函数支持。
(2)隐式类型转换
指针运算中隐含着类型转换。例如访问argv[3]时,编译器自动计算指针偏移:
addq $24, %rax
这里argv作为char**类型,每个指针占8字节,访问第三个元素需要偏移3×8=24字节。编译器在编译时完成了这个类型相关的偏移计算。
3.3.4 算术操作实现
基本的算术操作直接映射为CPU的算术指令。循环变量自增i++编译为:
addl $1, -4(%rbp)
addl指令将栈帧中存储的变量值加1,结果存回原处。与高级语言的简洁表达不同,汇编层面需要显式地加载、运算和存储。
3.3.5 关系操作与条件判断
关系比较通过cmp(比较)指令实现,该指令设置标志寄存器的状态:
相等判断
cmpl $5, -28(%rbp)
je .L2
比较argc与5,若相等则跳转。je(相等时跳转)指令依赖cmp设置的标志位。
循环条件判断
cmpl $9, -4(%rbp)
jle .L4
比较循环变量i与9,若i <= 9则继续循环。这种有符号整数比较通过jle(小于等于时跳转)实现。
3.3.6 数组访问模式
数组访问遵循"基址+偏移"的统一模式,但每次访问都需重新计算:
movq -32(%rbp), %rax ; 加载argv基地址
addq $24, %rax ; 计算argv[3]偏移
movq (%rax), %rcx ; 获取argv[3]的值
值得注意的是,编译器没有优化重复的基址加载操作,每次数组访问都从头开始计算。这种保守策略保证了正确性但牺牲了部分效率。
3.3.7 控制流转移
控制结构被转换为条件跳转和无条件跳转的组合:
(1)条件分支(if语句)
通过cmp和条件跳转指令实现。当参数检查失败时,程序跳转到错误处理代码块,执行错误信息输出和异常退出。
(2)循环结构(for语句)
循环被编译为标准的"初始化-判断-执行-递增"模式:
movl $0, -4(%rbp) ; 初始化: i = 0
jmp .L3 ; 跳转到条件判断
.L4: ; 循环体开始标签
; ... 循环体代码 ...
addl $1, -4(%rbp) ; 递增: i++
.L3: ; 条件判断标签
3.4 本章小结
本章系统阐述了编译阶段的核心概念与作用,即编译器将预处理后的 hello.i 文件翻译为汇编代码 hello.s 的过程。通过 gcc -S 生成的 hello.s 文件,以hello.s为例深入分析了编译器如何处理 C 语言中的基本数据类型、变量声明、算术与关系运算、控制流结构(如循环与条件判断)、函数调用约定以及类型转换等关键要素,不仅解释了高级语言语义如何被映射为具体的汇编指令(如 mov、call、jmp、cmp 等),还探讨了寄存器分配、栈帧管理、参数传递等底层实现机制,并对比了源代码逻辑与汇编实现之间的对应关系,为理解程序从高级语言到机器可执行形式的转化奠定了坚实基础。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指将汇编语言编写的源程序转换为等价的机器语言程序的过程。在这一过程中,汇编程序作为转换工具,其输入为汇编语言源程序(通常以.s为扩展名),输出则是机器语言形式的目标程序(通常以.o为扩展名)。汇编语言是机器语言的符号化表示,每条汇编指令都与特定的机器指令直接对应,而汇编过程正是完成这种符号到二进制机器码的映射。
4.1.2 汇编的作用
汇编器(如 GNU 汇编器as)的核心作用是将.s格式的汇编程序翻译为机器可识别的二进制指令,并将这些指令按照可重定位目标程序的格式打包到.o目标文件中。汇编过程中,汇编器具体会:
(1)将汇编语言翻译为机器可执行的二进制代码。
(2)生成符号表与重定位信息为函数、全局变量等生成符号,记录其名称与地址(或偏移)对尚未确定最终地址的引用(如调用外部库函数 printf)打上重定位标记。
(3)组织程序的逻辑结构到目标文件格式汇编器按照目标文件格式(如 ELF)将代码、数据、符号、重定位表等组织成标准结构:text 段:存放可执行指令;.data / .bss 段:存放已初始化/未初始化的全局变量;.rodata 段:存放只读数据(如字符串常量 "Hello %s %s %s\n")。
汇编为链接阶段生成完整可执行程序奠定基础,是连接人类可读指令与机器可执行代码的关键桥梁。
4.2 在Ubuntu下汇编的命令
1.使用as汇编器直接汇编
as hello.s -o hello.o
2.使用gcc间接调用汇编器:
gcc -c hello.s -o hello.o

图4-1 汇编命令
4.3 可重定位目标elf格式
可重定位目标文件通常采用 ELF(Executable and Linkable Format)格式。通过readelf工具可以分析hello.o的 ELF 结构,执行以下命令可将详细信息输出到文件:readelf -a hello.o > hello.elf
4.3.1 ELF 文件整体结构
典型的可重定位目标文件的 ELF 格式包含以下关键部分:
ELF 头(ELF Header)
节头部表(Section Header Table)
各个节(Sections),如.text(代码节)、.data(已初始化数据节)、.bss(未初始化数据节)、.rela.text(重定位节)、.symtab(符号表)等。
4.3.2 ELF 头分析

图4-2 ELF头
ELF 头是 ELF 文件的起始部分,包含描述文件整体信息的元数据,具体包括:
开头 16 字节的 "魔术数" 序列,用于标识文件为 ELF 格式,并指定系统的字长(如 64 位)和字节顺序(如小端序)。
文件类型:对于hello.o,类型为 "可重定位目标文件(REL)"。
机器类型:针对hello.c生成的hello.o,机器类型为 "x86-64"。
节头部表的位置(文件偏移)、条目大小和数量:用于后续定位和解析各个节。
4.3.3 节头部表

图4-3节头部表
节头部表是 ELF 文件中描述各个节的结构,每个节在表中对应一个条目,包含以下关键信息:
节名称(如.text、.rela.text)
节的类型(如代码、数据、重定位信息等)
节在文件中的偏移量和大小
节的内存对齐要求
通过节头部表,链接器可以准确找到并操作各个节的内容。
4.3.4 重定位节

图4-4重定位节
重定位节的每个条目包含 偏移量、信息、类型、符号值、符号名称 + 加数 5 个核心字段,是链接器修正地址的说明书,逐字段拆解如下:
(1)偏移量(Offset)
需要重定位修正的位置,在对应目标节(如 .rela.text 对应 .text)中的字节偏移地址,示例:
.rela.text 中第一条条目偏移量 0x1c:表示 .text 节中偏移 0x1c 的位置需要修正;
.rela.eh_frame 中偏移量 0x20:表示 .eh_frame 节中偏移 0x20 的位置需要修正;
作用:链接器通过偏移量精准定位 "需要修改的字节位置"。
(2)信息(Info)
64 位数值(x86-64 架构),拆分为两部分:
高 32 位:符号表索引 → 指向符号表(.symtab)中目标符号的索引,告诉链接器 "要修正为哪个符号的地址";
低 32 位:重定位类型 → 告诉链接器 "如何计算最终地址"(如 PC 相对寻址、绝对寻址),示例:.rela.text 中 puts 对应的 Info 为 0050000000000002:
低 32 位:0x2 → 对应重定位类型 R_X86_64_PLT32(见 "类型" 字段);
高 32 位:0x5 → 表示符号表中索引为 5 的符号(即 puts);
核心作用:同时指定目标符号和地址计算规则,是重定位的核心字段。
(3)类型(Type)
重定位的寻址规则,决定链接器计算最终地址的方式,x86-64 常用的类型:
|----------------|-----------------------------|-----------------------------|
| 型名称 | 含义 | 适用场景 |
| R_X86_64_PC32 | 基于 PC(程序计数器)的 32 位相对寻址 | 调用共享库函数(如 printf、puts)、跨节跳转 |
| R_X86_64_PLT32 | 基于 PLT(过程链接表)的 32 位 PC 相对寻址 | 共享库函数的延迟绑定(动态链接) |
| R_X86_64_PC64 | 64 位 PC 相对寻址 | 64 位程序的跨节地址引用 |
表4-1 重定位类型
示例:.rela.text 中所有条目类型为 R_X86_64_PC32/R_X86_64_PLT32:表示通过 PC 相对寻址修正共享库函数地址;.rela.eh_frame 中类型为 R_X86_64_PC32:表示对异常帧地址做 32 位 PC 相对修正;
以 R_X86_64_PLT32 为例:最终地址 = 目标符号的 PLT 地址 - 当前指令的下一条指令地址(PC 值) + 加数。
(4)符号值(Symbol Value)
定义:符号表中对应符号的地址(汇编阶段暂为 0,链接阶段由链接器填充);
示例:你提供的示例中符号值均为 0000000000000000,因为汇编阶段无法确定 puts、printf 等外部函数的实际地址,需链接器从共享库中查找并填充。
(5)符号名称 + 加数(Symbol Name + Addend)
符号名称:对应符号表中的符号名,即重定位的目标(如 puts、.rodata、.text);
加数(Addend):地址计算的偏移补偿值,链接器计算最终地址时需加上该值;
示例解析:puts -4:加数 -4 是因为 call 指令占 4 字节,PC 指向 call 的下一条指令,需补偿偏移以确保寻址正确;.rodata + 2c:表示重定位到 .rodata 节偏移 0x2c 的位置,加数 0x2c 用于精准定位字符串常量;.text + 0:表示重定位到 .text 节起始地址,加数 0 无需额外偏移。
4.3.5 符号表

图4-5符号表
符号表(.symtab)用于保存程序中所有符号的定义和引用信息,是重定位的核心依据。符号包括:
全局符号:如外部函数(printf、exit等)和全局变量。
局部符号:如函数内的局部变量、标号(.L2等)。
节符号:每个节(如.text、.data)本身也是符号,用于标识节的起始地址。
符号表中的每个条目包含符号名称、值(地址或偏移)、大小、类型(函数、数据等)和绑定属性(全局、局部等)。
4.4 Hello.o的结果解析
通过objdump工具对hello.o进行反汇编,可观察机器语言与汇编语言的对应关系:objdump -d hello.o > hello.asm

图4-6 hello.asm
objdump -d -r hello.o 输出包含三部分核心信息:第一列是虚拟地址,第二列是可变长机器码,第三列是机器码还原的汇编指令,即对应 hello.s 的指令,机器码和汇编指令二者存在直接映射关系。
4.4.1 机器语言的组成
x86-64 架构的机器语言是 CPU 可直接执行的二进制编码,在反汇编中以十六进制字节序列呈现,核心由 4 部分组成(变长特性,1~15 字节不等):
(1)前缀字节:标识指令的扩展属性(如 64 位模式、安全机制)如f3 0f 1e fa 是 endbr64 指令的安全前缀 + 操作码(64 位程序分支保护);48 是 64 位指令前缀(如 48 89 e5 中的 48 标识操作 64 位寄存器)。
(2)操作码:1~2 字节,标识指令核心功能(如 0x55=push %rbp、0x75=jne、0xe8=callq)。
(3)ModR/M 字节:1 字节,拆分为 Mod(2位)+Reg(3位)+R/M(3位),用于定义操作数的寻址方式(寄存器 / 内存),例如:mov %rsp, %rbp 的机器码 48 89 e5 中,89 是 mov 操作码,e5 是 ModR/M 字节(标识 "% rsp → % rbp" 的寄存器寻址)。
(4)位移量 / 立即数:补充操作数的数值或内存偏移(1/2/4/8 字节,适配 32/64 位),例如:sub 0x20,%rsp 的机器码 48 83 ec 20 中,20 是立即数 0x20(对应 hello.s 中的 32);mov %rsi,-0x20(%rbp) 的 e0 是 -32 的补码(内存偏移)。
4.4.2 机器语言与汇编语言映射关系
(1)汇编语言中的助记符(pushq/movq/jne/call)映射为特定的操作码(0x55/0x48 0x89/0x75/0xe8)。
(2)操作数表示方面由汇编语言中的十进制 / 符号($32、-32(%rbp)、.L2、printf)映射为十六进制 / 补码 / 数值偏移(0x20、0xe0、0x06、0x00000000):
立即数:直接嵌入机器码的数值编码。汇编中的立即数(以开头的数值,如0、32、9)会被汇编器转换为十六进制 / 补码形式,直接作为机器码的一部分嵌入,长度适配操作数位数。正数直接转换为十六进制(如32→0x20);负数转换为补码(如-4→0xfc,8 位补码);32 位int立即数占 4 字节,64 位立即数占 8 字节(x86-64 中 32 位立即数自动零扩展为 64 位)。
寄存器:编码表映射为数字(ModR/M 字节)x86-64 架构定义了固定的寄存器编码表,汇编中的寄存器名(如%rbp、%rsp、%rsi)会被转换为 3 位二进制数字,封装在ModR/M字节中(Mod(2位)+Reg(3位)+R/M(3位)),硬件解析时通过编码表还原寄存
|------|----------|-------|
| 寄存器 | 3 位二进制编码 | 十进制数值 |
| %rax | 000 | 0 |
| %rcx | 001 | 1 |
| %rdx | 010 | 2 |
| %rbx | 011 | 3 |
| %rsp | 100 | 4 |
| %rbp | 101 | 5 |
| %rsi | 110 | 6 |
| %rdi | 111 | 7 |
表4-2 核心寄存器编码表(x86-64)
内存地址:偏移量 + 基址寄存器的组合编码。汇编中的内存地址(如-32(%rbp)、8(%rax)),在机器码中通过ModR/M字节 + 位移量/立即数组合表示,核心分为两类:
基址 + 偏移:disp(%reg) → ModR/M 字节标识基址寄存器,偏移量(补码 / 十六进制)嵌入机器码;
基址 + 比例 + 索引:disp(%reg1,%reg2,scale) → 额外的 SIB 字节(比例 - 索引 - 基址),适配数组 / 指针访问。
4.4.3 分支转移的差异
hello.s 中分支跳转的目标是 编译器生成的标号(如 .L2/.L3/.L4),机器码中是 PC 相对偏移量(当前指令下一条地址到目标地址的字节差),无任何符号:
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| hello.s跳转指令 | hello.asm机器码 |
|
|
|
|
|
|
表4-3 分支转移对比
4.4.4 函数调用的差异
hello.s 中函数调用的目标是 函数名(如 call printf@PLT、call sleep@PLT),机器码中是重定位占位值(全 0),在汇编阶段无法确定函数实际地址,需链接阶段修正,以printf函数为例:
call printf@PLT # 调用 printf 函数(hello.s)
7b: e8 00 00 00 00 callq 0x80 <main+0x80> (对应asm反汇编对应)
4.5 本章小结
本章围绕汇编过程展开,首先明确了汇编的概念,将汇编语言转换为机器语言和作用(生成可重定位目标文件,为链接做准备);其次介绍了 Ubuntu 下的汇编命令(as和gcc -c);接着通过readelf工具分析了hello.o的 ELF 格式,包括 ELF 头、节头部表、重定位节和符号表的核心作用;最后通过反汇编对比了机器语言与汇编语言的差异,重点说明了分支转移和函数调用在两种形式下的不同表现。通过本章内容,可清晰理解从汇编源程序到可重定位目标文件的转换过程及目标文件的内部结构,为后续链接过程做基础。
(第4章1分)
第 5 章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是指链接器(Linker,如 GNU ld)将一个或多个可重定位目标文件(.o)、系统启动文件(crt 系列.o)、共享库 / 静态库整合,收集并整理程序的指令编码与数据块,最终生成单一、可直接加载运行的可执行目标文件(Linux 下无后缀,Windows 为.exe)的过程。
链接的核心是解决两个关键问题:
符号解析:将程序中引用的外部符号(如printf、exit等库函数)绑定到其实际定义的位置;
重定位:修正可重定位目标文件中 "占位" 的地址(如函数调用、分支跳转的偏移),赋予指令 / 数据最终的虚拟地址。
5.1.2 链接的作用
模块化开发支撑:程序可拆分为多个源文件(如main.c、utils.c)分别编译为.o文件,链接阶段合并,降低代码复杂度,便于单独修改某一模块;
代码复用与库依赖:通过链接系统库(如libc.so)或自定义库,复用成熟的函数实现(如printf、sleep),无需重复编写;
地址规范化:为程序指令 / 数据分配最终的虚拟地址,确保 CPU 能按地址正确寻址执行;
动态 / 静态灵活适配:支持静态链接(库代码嵌入可执行文件)或动态链接(运行时加载共享库),平衡文件大小与内存占用。
5.2 在Ubuntu下链接的命令
Linux 下 C 程序的链接分为静态链接和动态链接,其中动态链接是默认方式(依赖共享库libc.so)。使用ld完成动态链接,命令如下:
ld -o hello \
-dynamic-linker /lib64/ld-linux-x86-64.so.2 \
/usr/lib/x86_64-linux-gnu/crt1.o \
/usr/lib/x86_64-linux-gnu/crti.o \
hello.o \
/usr/lib/x86_64-linux-gnu/libc.so \
/usr/lib/x86_64-linux-gnu/crtn.o

图5-1 链接命令
5.3 可执行目标文件hello的格式
可执行目标文件hello仍遵循 ELF 格式,但与可重定位目标文件hello.o的 ELF 结构存在核心差异(新增程序头、入口地址、动态链接段等)。
5.3.1ELF 格式核心部分分析
(1)ELF 头
hello.elf的 ELF 头与hello2.elf核心差异如下,是区分可执行与可重定位文件的关键:(hello.elf为用readelf读取hello得到的ELF格式信息见图5-2,hello2.elf为用readelf读取hello.o可重定位文件得到的ELF格式信息见第四章)
|--------------|---------------|------------|----------------------------------------------------------------------------------------|
| 字段 | hello.o(可重定位) | hello(可执行) | 区别与含义 |
| 文件类型(Type) | REL(可重定位) | EXEC(可执行) | 这是最根本的区别。REL表明该文件是编译器生成的中间文件,需链接后才能执行;EXEC表明是链接器生成的、可直接由系统加载执行的程序。 |
| 入口地址(Entry) | 0x0(无运行入口) | 0x4010f0 | 这是可执行文件的核心标志。可重定位文件没有固定的执行入口,因此为0。可执行文件必须指定程序第一条指令的虚拟地址 0x4010f0,操作系统加载后将从此地址开始执行。 |
| 程序头数量(Phnum) | 0(无程序头) | 12 | 程序头表描述了加载和执行文件所需的段 (Segment) 信息。可重定位文件无需加载,故无程序头;可执行文件有12个段(如代码段、数据段),供操作系统加载时建立内存映射。 |
| 程序头偏移(Phoff) | 0 | 64(字节) | 此字段指示程序头表 (Program Header Table) 在文件中的起始偏移。可重定位文件无此表,故为0;可执行文件的程序头表紧接在ELF头(64字节)之后开始。 |
| 节区头起点 | 1264 | 14208 | 指示节区头表 (Section Header Table) 的起始偏移。可执行文件通常更大,链接合并了多个.o文件,其节区头表位置更靠后。 |
| 节区头数量(Shnum) | 14 | 27 | 可执行文件经历了链接过程,合并、重定位了来自多个可重定位文件的节区,并添加了链接器生成的节区(如.init, .plt),因此节区数量更多。 |

表5-1 ELF头差异
图5-2 hello.elf ELF头
(2)节头(Section Header)
可执行文件的节头保留.text、.data、.rodata等核心节,但新增和修改关键属性:
节的虚拟地址(Address):从 0x400000 开始(x86-64 默认加载基址),而非hello.o的 0x0;新增.plt(过程链接表)、.got(全局偏移量表):支撑动态链接的延迟绑定;
节的可加载属性:标记.text/.data等节需映射到内存,.comment等辅助节无需加载。

图5-3 hello.elf节头
(3)程序头(Program Header)
程序头是可执行文件特有的结构,描述 "段(Segment)" 的加载规则(操作系统加载程序的核心依据):
每个程序头对应一个内存段(如代码段、数据段);
核心字段:
p_vaddr:段的虚拟起始地址(如代码段 0x401000);
p_filesz:段在 ELF 文件中的大小;
p_flags:段的访问权限(代码段R E、数据段R W)。

图5-4 程序头
- 动态段(Dynamic Section)
该段是 ELF 文件中动态链接的核心配置表,位于文件偏移 0x2e50处,共包含 21 个条目。它指明了程序运行时的关键信息:程序依赖的共享库为 libc.so.6;代码的初始化入口在 0x401000,结束处理在 0x401248;动态符号表和字符串表分别位于 0x400398和 0x400470;全局偏移表(GOT)的地址是 0x404000;重定位表位于 0x400500和 0x400530。这些信息共同引导动态链接器在程序启动时正确加载、重定位并执行程序。

图5-5 动态段
- 动态重定位节
重定位节是链接过程中用于修正地址引用的关键数据结构,它告诉链接器在程序加载时哪些位置的地址需要被替换。确保程序在内存中加载时,所有对函数和变量的引用,特别是那些定义在共享库中的能够指向正确的内存地址。
.rela.dyn:位于文件偏移 0x500,包含2个条目,用于处理数据段(.data、.bss 等)中全局变量的地址重定位。例如,其中一条是 R_X86_64_GLOB_DAT类型的重定位,针对偏移 0x403ff0处的符号,这通常是全局偏移表(GOT)中某个条目的位置,需要在加载时填入对应变量的真实地址。
.rela.plt:位于文件偏移 0x530,包含 6 个条目,专门用于过程链接表(PLT) 的重定向。这6个条目分别对应像 puts、printf这样的动态库函数,其类型为 R_X86_64_JUMP_SLOT。在程序启动时,动态链接器会根据这些条目,将 PLT 中相应桩代码的跳转目标修改为共享库中函数的真实地址。

图5-6 动态重定位节
(6)符号表(Symbol Table)
已定义符号(如 main):
hello.o 中 main 的 Value=0x0(.text 节内偏移);
hello 中 main 的 Value=0x401125(最终虚拟地址,由链接器分配,指向.text 节的实际位置),Ndx=15(属于.text 节)。
未定义符号(如 printf):
hello.o 中 printf 的 Value=0x0(占位,等待链接);
hello 中 printf 仍为 Ndx=UND(未定义),但运行时动态链接器会将其绑定到 PLT/GOT 地址(如 0x400410),实现延迟绑定。

5-7 符号表
5.4 hello的虚拟地址空间
gdb 调试(命令行查看)
启动 gdb:gdb ./hello;
运行程序:break main后run ;
查看内存映射:info proc mappings(输出进程的虚拟地址段分布):

图5-8 内存映射
gdb 显示的代码段起始地址(0x401000)与 ELF 程序头中代码段的p_vaddr完全一致;
数据段(.data/.got)的虚拟地址(0x404000)与程序头中数据段的p_vaddr一致;
共享库(如libc-2.31.so)会加载到高地址(0x7ffff7dbe000),属于运行时动态加载,不在 ELF 程序头中定义。
可执行文件的 ELF 程序头定义了 "段的虚拟地址规划",操作系统加载程序时,会严格按照程序头的规则将 ELF 中的节映射到虚拟地址空间的对应位置,因此调试工具看到的虚拟地址与 ELF 程序头的p_vaddr完全匹配
5.5 链接的重定位过程分析
重定位是链接的核心环节之一,链接器会读取hello.o的重定位节(如.rela.text),修正其中 "占位" 的地址偏移,为指令和数据分配最终的虚拟地址,使hello.o从 "可重定位" 变为 "可执行"。以下通过objdump对比hello与hello.o的反汇编差异,解析重定位的完整过程。
5.5.1 hello 与 hello.o 反汇编的核心差异:
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
|
|
| 图5-9 hello 反汇编hello.asm | 图5-10 hello.o 反汇编hello2.asm |
(1)地址基准:从相对偏移到绝对虚拟地址
hello.o的反汇编地址以0x0为基准(仅表示.text节内的相对偏移,无实际运行意义)如00000000 <main>: 55 push %rbp;
hello的反汇编地址为最终虚拟地址(以 0x401000 为基准,与 ELF 程序头定义的代码段起始地址一致)如0000000000401125 <main>: 55 push %rbp。
(2)符号解析:从 "占位符" 到 "实际地址"
hello.o中引用的外部函数(如printf、puts)无实际地址,反汇编中call指令后为全 0 占位:示例(hello.o):68: e8 00 00 00 00 callq 6d <main+0x6d>;
hello中链接器已解析符号,call指令后为指向PLT(过程链接表)的实际偏移:示例(hello):40118d: e8 0e ff ff ff callq 4010a0 <printf@plt>。
(3)跳转指令:从 "节内偏移" 到 "虚拟地址偏移"
hello.o中分支跳转指令(如jne、jle)的目标为节内相对偏移(如jle 38 <main+0x22>);hello中跳转目标修正为最终虚拟地址的偏移(如jne 40115d <main+0x38>),链接器已计算出目标地址与当前指令的实际字节差。
(4)函数完整性:新增库函数相关代码段
hello.o仅包含main函数的汇编指令;hello中链接器会整合libc.so中printf、puts等函数的 PLT/GOT 入口代码,新增.plt段(如401090 <puts@plt>),使程序能调用系统库函数:

图5-11 部分hello.asm内容
5.5.2 重定位核心逻辑:
链接器的重定位过程本质是"地址修正闭环":
读:读取hello.o重定位节中的 "待修正位置 + 目标符号 + 修正规则";
查:从系统库 / 启动文件中查找符号的实际地址(PLT/GOT 入口);
算:基于最终虚拟地址计算指令的相对偏移;
写:将计算后的偏移写入hello的机器码,替换原占位值;
验:确保修正后的地址符合虚拟地址空间的权限和布局规则。
通过这一过程,hello.o中 "无意义的相对偏移" 被替换为 "可执行的绝对虚拟地址偏移",程序具备了运行的基础。
5.6 hello的执行流程
程序的执行流程从操作系统加载hello到内存开始,直至进程终止,核心分为 "加载初始化→入口执行→main 函数调用→资源清理" 四个阶段,以下通过 gdb 调试解析完整流程。
|------------|-------------------|----------------|-----------------------------------------------------|
| 阶段 | 函数 / 入口 | 地址(示例) | 核心作用 |
| 1. 程序加载 | 操作系统加载器 | - | 按 ELF 程序头将.text/.data等段映射到虚拟地址空间(0x401000/0x404000) |
| 2. 动态链接初始化 | _start(程序真正入口) | 0x4010f0 | 初始化栈、调用动态链接器ld-linux.so.2完成共享库加载 |
| 3. 运行时初始化 | __libc_start_main | 0x7ffff7de1f90 | 初始化 libc 库、调用init函数完成程序初始化 |
| 4. 初始化函数 | _init | 0x4003c0 | 执行程序的构造函数、动态链接重定位收尾 |
| 5. 主函数调用 | main | 0x401125 | 执行业务逻辑(如参数判断、printf 输出、sleep 等待等) |
| 6. 资源清理 | __libc_csu_init | 0x4011d0 | 初始化 C 运行时环境,清理临时资源 |
| 7. 收尾函数 | __libc_csu_fini | 0x401240 | 执行程序的析构函数 |
| 8. 终止函数 | _fini | 0x401248 | 释放动态链接资源、调用exit终止进程 |
| 9. 进程终止 | exit | GOT@0x404038 | 回收进程资源,返回退出码给操作系统 |
表5-2 hello关键函数信息
核心流程说明:
_start是程序的入口(ELF 头的 Entry 字段指向此地址),而非main;
__libc_start_main是 libc 库的核心函数,负责封装main的调用(传递 argc/argv、处理返回值);
动态链接相关函数(如_init)在main前执行,完成printf/puts等函数的 GOT/PLT 绑定;
main执行完毕后,exit函数会回收进程的内存、文件描述符等资源,确保进程正常终止。
5.7 Hello的动态链接分析
动态链接的核心是 "延迟绑定"------ 程序加载时不直接解析所有库函数地址,而是在首次调用时由动态链接器(ld-linux.so.2)完成绑定,通过PLT(过程链接表)+GOT(全局偏移量表)实现。以下通过gdb调试分析动态链接前后GOT表的内容变化。
PLT:存放跳转指令,每个库函数对应一个 PLT 入口(如printf@plt),首次调用时触发动态链接器解析;
GOT:全局偏移量表,存放函数的实际地址,未解析时指向 PLT 入口,解析后指向库函数的真实地址;
动态链接器:ld-linux.so.2,负责查找libc.so中函数的地址并更新GOT。
(1)查看puts@plt和GOT条目

图5-11 hello.asm部分内容
2)动态链接前的GOT状态
启动gdb,在main处设置断点,运行程序,查看首次调用前的GOT,指向PLT 解析代码。

图5-12首次调用前的GOT
3)动态链接后的 GOT 状态
首次调用 puts 后,动态链接器解析实际函数地址并更新GOT,指向实际函数地址。

图5-13首次调用后的GOT
可见,动态链接前,GOT表仅保存 PLT 入口地址,函数未绑定到真实地址;
首次调用库函数时,PLT 指令触发动态链接器解析函数地址,更新GOT表;
后续调用同一函数时,直接从GOT表读取真实地址,无需重复解析(延迟绑定提升效率)。
5.8 本章小结
本章围绕 "从 hello.o 到 hello 的链接过程" 展开,系统讲解了链接的概念与作用,明确了链接是符号解析与重定位的核心闭环;通过 Ubuntu 下的链接命令生成可执行文件hello,并基于readelf分析其 ELF 格式(对比hello.o的核心差异:新增程序头、入口地址、动态段等);结合gdb 调试,验证了虚拟地址空间与 ELF 程序头的映射关系,解析了重定位过程中地址修正的完整逻辑;跟踪了程序从_start到exit的完整执行流程,明确了动态链接通过 PLT/GOT 实现延迟绑定的机制。
通过本章分析可知,链接是 "可重定位目标文件" 到 "可执行文件" 的关键桥梁,其核心是解决 "地址未知" 和 "符号未解析" 两大问题,而动态链接的延迟绑定机制则平衡了程序加载效率与内存占用,是 Linux 系统程序运行的核心基础。
(第5章1分)
第 6 章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是程序的动态执行实例,是操作系统资源分配与调度的基本单位。当用户执行./hello时,静态的hello可执行文件(代码、数据的集合)被加载到内存,操作系统为其分配独立的虚拟地址空间、CPU 时间片、文件描述符等资源,并创建进程控制块(PCB,Process Control Block) 记录核心状态信息,此时hello从静态程序转变为动态进程。
PCB 包含的核心信息(以 hello 进程为例):
标识信息:进程 ID(PID)、父进程 ID(PPID,bash 的 PID);
状态信息:运行(R)、就绪(S)、阻塞(D)、挂起(T)等;
资源指针:虚拟地址空间的页表指针、文件描述符表(继承 bash 的 stdin/stdout/stderr);
调度信息:优先级、时间片剩余量、调度队列指针;
上下文信息:CPU 寄存器值(如 PC、% rsp)、堆栈指针,用于上下文切换。
6.1.2 进程的作用
(1)资源隔离与安全保障:hello 进程的虚拟地址空间与其他进程完全隔离,其代码段(.text)设为只读,数据段(.data)仅自身可写,避免非法访问其他进程内存,保障系统稳定。
(2)多任务并发支撑:操作系统通过时间片轮转调度,让 hello 进程与其他进程(如终端、浏览器)交替占用 CPU,实现 "伪并行";多核 CPU 下可实现真正并行,提升系统整体吞吐量。
(3)用户与硬件的交互桥梁:hello 通过printf调用write系统调用请求内核写入屏幕,通过getchar等待键盘输入,通过信号接收内核或其他进程的事件通知,如 Ctrl+C 终止信号,成为用户操作与硬件资源的中间载体。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash 的核心作用
bash 是用户与操作系统之间的命令解释器,也是 hello 进程的父进程,核心作用包括:
(1)命令解析与执行:将用户输入的./hello解析为可执行文件路径,触发进程创建流程;
(2)环境管理:为 hello 进程传递环境变量,控制其执行上下文;
(3)任务控制:管理 hello 进程的前台 / 后台运行状态,处理信号(如 Ctrl+Z 挂起);
(4)IO 重定向与管道:支持./hello > output.txt(输出重定向)、./hello | grep Hello(管道),灵活控制进程 IO。
6.2.2 Shell-bash 处理./hello的完整流程
以用户在终端输入./hello 2024111660 杨凯焱 15537227963 3为例,bash 的处理步骤如下:
读取输入:从终端读取命令行字符串,存入 bash 的输入缓冲区;
解析命令:分割命令为单词(./hello、2024111660、杨凯焱、15537227963、3),识别无元字符(如|、>),无需重定向或管道;
变量与通配符处理:无环境变量(如$VAR)或通配符(如*),直接进入下一步;
查找可执行文件:因命令以./开头,直接定位当前目录下的hello文件,验证其可执行权限;
创建子进程:调用fork()系统调用,创建 bash 的子进程,子进程继承 bash 的文件描述符、环境变量;
加载目标程序:子进程调用execve("./hello", argv, envp),清空自身地址空间,加载hello的代码段、数据段,初始化argc=5、argv数组(存储命令行参数);
父进程等待:bash(父进程)调用waitpid(),挂起等待 hello 子进程执行完毕;
回收资源:hello 进程退出后,内核回收其 PCB、内存等资源,bash 接收退出状态码,输出结果(如Hello 2024111660 杨凯焱 15537227963),返回终端提示符。
6.3 Hello的fork进程创建过程
fork()是 bash 创建 hello 子进程的核心系统调用,是 "进程复制 + 写时复制(COW)优化",确保高效创建且不浪费内存资源。
6.3.1 fork () 的核心执行步骤
内核分配 PCB:内核为 hello 子进程分配新的 PCB,初始化核心字段:
PID:分配唯一进程 ID;PPID:设为 bash 的 PID;
文件描述符表:复制 bash 的 stdin(0)、stdout(1)、stderr(2),指向终端设备;
信号处理表:继承 bash 的默认信号处理动作(如 SIGINT 终止、SIGTSTP 挂起)。
虚拟地址空间复制:
子进程复制 bash 的页表,但所有物理内存页标记为只读;
hello 子进程此时与 bash 共享物理内存(代码段、数据段、栈),未实际复制数据,仅占用页表空间。
资源引用计数更新:共享资源(如终端设备、打开的文件)的引用计数 + 1,避免内核重复分配。
6.3.2 父子进程分离与 COW 触发
返回值区分:
父进程(bash):fork()返回子进程 PID,进入waitpid()等待状态;
子进程(hello):fork()返回 0,准备执行execve()加载新程序。
写时复制的实际触发:若 hello 子进程在execve()前修改内存(如临时变量),会触发缺页异常:
内核检测到 "只读 + COW" 标记的物理页;
分配新物理页,复制原页数据到新页;
更新子进程页表,将该页标记为 "可写",解除 COW 状态;
父进程(bash)仍使用原物理页,实现父子进程内存隔离。
6.4 Hello的execve过程
execve()是 hello 子进程 "替换自身为目标程序" 的核心系统调用,它会清空 fork 继承的 bash 地址空间,加载hello可执行文件的代码与数据,完成从bash 子进程到hello进程的转变,execve () 的核心执行步骤:
(1)清空旧地址空间:销毁子进程继承的 bash 代码段、数据段、栈、堆;
释放对应的页表项,仅保留文件描述符表、信号处理表(未显式修改的部分)。
(2)加载 hello 的 ELF 格式:解析hello的 ELF 头,定位代码段(.text,0x401000)、数据段(.data,0x404000)、.bss 段(未初始化数据);为各段分配虚拟地址,设置权限(.text:R/E,.data:R/W);触发按需加载:仅建立虚拟地址与文件的映射,物理页在首次访问时通过缺页异常加载。
(3)初始化执行上下文:传递命令行参数:argc=5,argv[0]="./hello"、argv[1]="2024111660"等,存入栈空间;传递环境变量:envp(如PATH=/usr/bin、LANG=en_US.UTF-8);设置程序计数器(PC)指向hello的_start入口(如 0x4010f0),而非main函数。
(4)跳转至 main 函数:_start是 ELF 的入口,由 C 运行时库(crt1.o)提供,负责初始化栈、调用__libc_start_main;__libc_start_main调用main(argc, argv),hello 正式开始执行业务逻辑。
6.5 Hello的进程执行
hello 进程创建后,操作系统通过调度算法分配 CPU 时间片,实现与其他进程的并发执行,核心涉及调度策略、上下文切换、用户态与内核态转换。
6.5.1 进程调度(时间片分配)
调度算法:Linux 采用 CFS(完全公平调度器),为 hello 进程分配时间片,根据进程优先级动态调整权重;调度流程:
hello 进程初始进入 "就绪队列",等待 CPU 空闲;
调度器选中 hello 后,将其状态改为 "运行态(R)",分配 CPU 时间片;
时间片耗尽时,时钟中断触发,内核保存 hello 的上下文(寄存器、PC),将其放回就绪队列,切换至其他进程。
6.5.2 用户态与内核态转换
hello 进程的执行过程中,会频繁在用户态与内核态之间切换,核心触发点为系统调用:
用户态执行:main函数中的循环逻辑、i++算术运算等,无需内核干预,直接在用户态执行;
内核态切换:
调用printf时,底层触发write系统调用(软中断int 0x80或syscall指令);
CPU 切换至内核态,执行内核的sys_write函数,操作终端设备;
系统调用完成后,内核恢复 hello 的用户态上下文,返回用户态继续执行。
6.5.3 上下文切换
当 hello 进程时间片耗尽或触发系统调用时,内核会执行上下文切换:
保存现场:将 hello 的寄存器值(% rax、% rsp、PC)、进程状态存入 PCB;
加载新现场:从目标进程(如 bash)的 PCB 中读取上下文,更新 CPU 寄存器、页表(切换虚拟地址空间);
跳转执行:设置 PC 为目标进程的下一条指令,切换完成。
6.6 hello的异常与信号处理
6.6.1 异常类型
(1)中断
定义:由外部硬件设备(如键盘、定时器)触发的异步事件,强制中断当前程序执行。
触发场景:用户按下 Ctrl+C、系统时钟中断等。
对hello的影响:终止进程或导致时间片切换。
(2)陷阱
定义:程序主动触发的同步异常,通常用于系统调用或调试。
触发场景:调试器设置断点、代码执行int 0x80指令等。
对hello的影响:跳转到内核态执行系统调用。
(3)故障
定义:程序执行中的可恢复错误,通常由于资源未就绪或权限不足(如缺页、非法访问)。
触发场景:非法内存访问(解引用空指针)、除零错误等。
对hello的影响:终止进程或重试指令。
(4)终止
定义:不可恢复的严重错误(如硬件故障、程序主动中止)。
触发场景:hello执行abort()函数、硬件校验错误等。
对hello的影响:进程强制终止并生成core dump。
6.6.2 核心信号类型(与 hello 相关)
|------|---------|-------------------|---------------------|
| 信号编号 | 信号名 | 触发场景 | 默认处理动作 |
| 2 | SIGINT | 按下 Ctrl+C | 终止进程(hello 退出) |
| 19 | SIGTSTP | 按下 Ctrl+Z | 挂起进程(状态变为 T) |
| 9 | SIGKILL | 执行kill -9 PID | 强制终止进程(不可捕获) |
| 18 | SIGCONT | 执行fg/kill -18 PID | 恢复挂起的进程 |
| 11 | SIGSEGV | 非法内存访问(如空指针) | 终止进程 + 生成 core dump |
表6-1 核心信号
6.6.3 实际操作与信号处理流程
正常运行

图6-1 正常运行
不停乱按

图6-2 不停乱按
在运行中乱按并没有改变printf的输出,程序正常运行。
按Ctrl-Z

图6-3 按Ctrl-Z
按下Ctrl+Z后,内核会向前台发送SIGSTP信号,从而终止前台作业。
运行ps

图6-4 运行ps
可以看到hello进程没有被回收,而是被挂起,其进程号是8742。
运行jobs

图6-5 运行jobs
可以看到job ID为1,状态为停止。
运行pstree


图6-6 6-7运行pstree
以树状图形式显示所有进程。
运行fg

图6-8 运行fg
输入fg后,内核会向目标作业发送SIGCONT信号,让程序重新在前台运行
运行kill

图6-9 运行kill
输入kill -9 8732,表示给进程8742发送9号信号(SIGKILL),终止进程。
按Ctrl-C

图6-10 按Ctrl-C
按下Ctrl+C后,内核会向前台发送SIGINT信号,从而终止前台作业。
6.7本章小结
本章围绕 hello 进程的完整生命周期(创建→加载→执行→终止),解析了进程管理的核心机制:bash 通过fork()创建子进程,利用写时复制(COW)优化内存使用;execve()清空旧地址空间,加载 hello 的 ELF 格式,完成从 "bash 子进程" 到 "hello 进程" 的转变;操作系统通过 CFS 调度算法分配时间片,上下文切换实现多进程并发;信号与异常处理则保障了进程对外部事件(如 Ctrl+C、非法访问)的响应。
hello 进程的管理是 "用户态程序→Shell→内核→硬件" 协同的典型体现:Shell 是命令解析与进程创建的入口,内核是资源分配与调度的核心,信号与系统调用是用户态与内核态交互的桥梁。结合ps、jobs、strace等工具的实践,清晰展现了进程从创建到终止的每一个关键环节,深化了对操作系统进程管理本质的理解。
(第6章 2 分)
第 7 章 hello的存储管理
7.1 hello的存储器地址空间
存储器地址空间是程序从编译到运行过程中地址形态的核心载体,结合hello程序的执行场景,可清晰厘清逻辑地址、线性地址、虚拟地址、物理地址的定义与转换关联,四者共同构成了 "软件地址→硬件地址" 的完整链路:
7.1.1 核心地址概念(结合 hello 实例)
|------|----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| 地址类型 | 定义与 hello 场景映射 | 具体示例 |
| 逻辑地址 | 程序编译 / 汇编后生成的段内相对地址,由 "段选择符 + 段内偏移" 组成,是 CPU 执行指令时直接生成的地址。Linux 为简化管理,将用户态段基址统一设为 0,因此逻辑地址与线性地址在用户态完全等价。 | hello.s 中main函数的push %rbp指令,在汇编文件中标记为.text节内偏移0x0(纯逻辑地址);反汇编后main函数入口地址0x401125,既是逻辑地址也是线性地址。 |
| 线性地址 | 逻辑地址经段式管理转换后的中间地址,是 CPU 视角下的连续地址空间,不依赖物理内存布局。无分页机制时直接等价于物理地址,启用分页后需通过页表进一步转换。 | hello 的.text节线性地址范围为0x402000~0x401000,由 ELF 程序头定义,是段式转换后的统一地址,与物理内存的实际位置无关。 |
| 虚拟地址 | 进程专属的私有地址空间(x86-64 架构为 48 位有效地址),通过分页机制映射到物理内存或磁盘交换空间。进程无法直接感知物理内存布局,所有访问均通过虚拟地址发起,实现内存隔离与按需扩展。 | hello 运行时,main函数虚拟地址0x401125、栈地址0x7ffffffde000-0x7ffffffff000、动态库libc.so加载地址0x7ffff7dbe000,均属于虚拟地址空间,仅本进程可见。 |
| 物理地址 | 内存控制器直接访问的硬件地址,对应 DRAM 芯片上的实际存储单元,是地址转换的最终结果。物理地址由操作系统统一分配管理,通过页表与虚拟地址绑定,确保不同进程的内存访问互不干扰。 | hello 的0x401125虚拟地址经页表转换为物理地址,CPU 通过该地址直接读取内存中的指令数据。 |
7.1.2 地址转换链路(hello 的完整流程)
hello 程序的地址转换遵循 "逻辑地址→线性地址→虚拟地址→物理地址" 的链路(Linux 用户态下逻辑地址 = 线性地址 = 虚拟地址,简化为 "虚拟地址→物理地址"):
编译阶段:hello.c编译为hello.s,指令地址以段内偏移(逻辑地址)表示;
链接阶段:生成hello可执行文件,为各段分配虚拟地址(如.text节0x401000);
运行阶段:CPU 执行指令时生成虚拟地址,经 MMU(内存管理单元)通过页表转换为物理地址,最终访问内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是 x86 架构实现内存隔离与权限控制的基础机制,核心目的是将不同性质的内存(代码、数据、内核空间)划分为独立段,通过段描述符管控访问权限。Linux 对其进行了简化,用户态进程的段基址均设为 0,使逻辑地址直接等价于线性地址。
7.2.1 逻辑地址的构成
逻辑地址由 "16 位段选择符 + 32/64 位段内偏移" 组成,针对 hello 进程:
段选择符:hello 运行在用户态(特权级 3),代码段选择符为0x2b(二进制0000000000101011),拆解为:
索引(13 位):0x5,用于定位全局描述符表(GDT)中的用户代码段描述符;
表指示位(TI):0,表示使用 GDT(而非局部描述符表 LDT);
请求特权级(RPL):11,对应用户态特权级 3。
段内偏移:hello 的main函数逻辑地址偏移为0x401125,因段基址为 0,偏移量即逻辑地址本身。
7.2.2 段式转换的核心流程
段描述符查找:CPU 通过段选择符的索引0x5,在 GDT 中定位用户代码段描述符,该描述符包含段基址、段限长、访问权限;
权限校验:当前特权级(CPL=3)与段描述符的特权级(DPL=3)一致,校验通过,避免用户态访问内核段;
线性地址计算:线性地址 = 段基址(0x0)+ 段内偏移(0x401125)=0x401125,与逻辑地址完全重合。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是实现虚拟内存的核心机制,通过将线性地址(虚拟地址)与物理地址划分为固定大小的 "页"(x86-64 默认 4KB),利用多级页表建立映射,实现内存的灵活分配与复用。hello 程序的虚拟地址到物理地址转换,依赖 x86-64 的四级页表(PML4→PDP→PD→PT)完成。
7.3.1 线性地址的四级页表拆解(以 hello 的 main 函数 0x401125 为例)
x86-64 架构的 48 位虚拟地址按 "9+9+9+9+12" 位拆分,对应四级页表索引与页内偏移:
|-------------|--------------|-----------|-------------------------|
| 页表层级 | 索引位范围(47~0) | 索引值(十六进制) | 作用 |
| PML4(顶级页表) | 47~39 | 0x0 | 定位 PML4 表项,指向 PDP 表物理基址 |
| PDP(页目录指针表) | 38~30 | 0x0 | 定位 PDP 表项,指向 PD 表物理基址 |
| PD(页目录表) | 29~21 | 0x1 | 定位 PD 表项,指向 PT 表物理基址 |
| PT(页表) | 20~12 | 0x0 | 定位 PT 表项,指向目标物理页框基址 |
| 页内偏移 | 11~0 | 0x526 | 定位物理页内的具体字节位置 |
表6-1 四级页表
7.3.2 地址转换的完整流程
顶级页表查询:CPU 从 CR3 寄存器读取 PML4 表的物理基址,通过 PML4 索引 0x0 找到对应表项,获取 PDP 表的物理基址;
二级页表查询:访问 PDP 表基址 0x101000,通过 PDP 索引 0x0 找到表项,获取 PD 表的物理基址;
三级页表查询:访问 PD 表基址 0x102000,通过 PD 索引 0x1 找到表项,获取 PT 表的物理基址;
四级页表查询:访问 PT 表基址 0x103000,通过 PT 索引 0x0 找到表项,获取物理页框基址;
物理地址合成:物理地址 = 物理页框基址+ 页内偏移
7.3.3 页表项的核心属性(hello 场景)
hello 的代码段(.text)对应的 PT 表项包含以下关键属性:
存在位(P=1):表示该页已加载到物理内存;
权限位(R/W=0,U/S=1):只读、用户态可访问;
脏位(D=0):该页未被修改(代码段只读,脏位始终为 0)。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(快表)是 MMU 中的高速缓存,专门存储近期使用的虚拟地址→物理地址映射关系,核心目的是避免频繁遍历四级页表导致的访存延迟,提升 hello 程序的地址转换效率,其核心工作流程(hello 的 VA→PA 转换)如下:
TLB 查询(优先执行):CPU 读取 hello 的 0x401125 虚拟地址,提取高 36 位(四级页表索引)作为 TLB 标签,查询 TLB 缓存:
命中:直接从 TLB 中获取物理页框号,与页内偏移合成物理地址,无需访问页表,耗时仅 1~2 个 CPU 周期;
未命中:触发 TLB 缺失,执行四级页表遍历(见 7.3.2 流程)。
TLB 更新:页表遍历完成后,将 0x400526→物理地址的映射关系写入 TLB,后续访问该地址时可直接命中,提升效率。
权限与一致性校验:TLB 表项同步页表项的权限属性(如 R/E),若 hello 尝试修改代码段地址,会触发权限异常;页表项更新时(如缺页处理后),需刷新 TLB,避免旧映射导致地址错误。
7.4.2 TLB 对 hello 性能的影响
hello 程序的循环指令(printf+sleep)会频繁访问相同虚拟地址,TLB 可缓存这些映射,使后续转换无需遍历页表:
无 TLB 时:每次地址转换需访问 4 次物理内存(四级页表),耗时约 40~80 个 CPU 周期;
有 TLB 时:命中后仅需 1~2 个周期,性能提升数十倍。

图7-1 TLB与四级页表支持下的VA到PA的变换示意图
7.5 三级Cache支持下的物理内存访问
现代 x86-64 CPU 内置 L1、L2、L3 三级 Cache,通过空间局部性和时间局部性原理优化物理内存访问延迟,是程序高效运行的关键硬件支撑。三级 Cache 呈现 "容量递增、延迟递增" 的层级特征:L1 Cache 分为指令缓存(I-Cache)和数据缓存(D-Cache),总容量约 32KB,访问延迟仅 1~3 个 CPU 周期,用于缓存 hello 近期执行的循环指令和频繁修改的循环变量 i;L2 Cache 为统一缓存,容量 256KB~1MB,延迟 10~15 个周期,用于缓存 L1 未命中的指令和数据,作为中间缓冲层级;L3 Cache 为多核心共享缓存,容量 8MB~32MB,延迟 30~50 个周期,用于缓存 L2 未命中的内容,支撑多进程并发时的缓存共享。hello 的物理内存访问遵循逐级查询流程:CPU 生成物理地址后,先通过物理地址的组索引位定位 L1 Cache 的缓存行,检查标签是否匹配,命中则直接获取指令或数据;未命中则依次查询 L2、L3 Cache,若均未命中则向内存控制器发起请求,从 DRAM 读取,读取后将数据逐级加载到 L3→L2→L1 Cache,供后续访问复用。Cache 的优化策略进一步提升效率:时间局部性让 hello 的循环指令长期保留在 Cache 中,空间局部性使.text 节的连续指令被预加载,写回策略则让栈变量修改仅更新 L1 Cache,脏数据延迟刷回内存,减少内存写操作开销。
7.6 hello进程fork时的内存映射
fork () 系统调用创建 hello 子进程时,Linux 采用写时复制机制,在保障父子进程内存隔离的同时,最大化进程创建效率。fork 执行时,内核首先为 hello 子进程分配唯一的 PID,复制父进程的 PCB 元数据,包括文件描述符表、信号处理规则、虚拟地址空间描述符等,但共享资源的引用计数会相应递增;随后复制父进程的页表,将所有物理内存页的页表项标记为 "只读",并启用 COW 标志,此时父子进程共享同一物理内存空间,代码段、数据段、栈、堆均未发生实际复制,仅占用少量页表存储空间。父子进程的分离通过写操作触发:hello 的代码段(.text)、只读数据段(.rodata)为只读属性,父子进程永久共享物理页,无需复制;若子进程修改数据段的全局变量或栈上局部变量,会触发缺页异常,内核检测到该页的 COW 标记后,分配新的物理页,将原物理页的数据复制到新页,更新子进程的页表项为 "可写",解除 COW 标志,而父进程的页表项保持不变,仍指向原物理页,实现数据独立。这种机制既避免了 fork 时无意义的全量内存复制,降低进程创建开销,又通过缺页异常延迟复制时机,确保父子进程的内存隔离性,hello 子进程修改全局变量时的物理内存占用变化,可通过 ps 命令查看 RSS(物理内存常驻大小)的增长得以验证。
7.7 hello进程execve时的内存映射
execve ("./hello", argv, envp) 的核心作用是 "替换进程镜像",销毁当前进程的地址空间并构建 hello 程序的全新虚拟内存布局。执行时,内核首先销毁原进程的代码段、数据段、栈、堆及内存映射文件,回收对应的物理页和页表,仅保留 PID、文件描述符表等核心元数据;随后解析 hello 的 ELF 文件,重构虚拟地址空间:将.text 节映射为只读可执行(R/E)的代码段,虚拟地址从 0x401000 开始,物理页采用按需加载机制,首次访问时通过缺页异常从磁盘载入;将.data 节(初始化数据)和.bss 节(未初始化数据,初始化为 0)映射为可读可写(R/W)的数据段,虚拟地址起始于 0x404000;初始化栈空间(虚拟地址 0x7ffffffde000),将 argc、argv、环境变量存入栈帧;重置堆指针,为动态内存分配预留空间;动态链接器将 hello 依赖的共享库(libc-2.31.so)映射到高地址区域(0x7ffff7dbe000),建立 PLT/GOT 表映射以支撑动态链接。地址空间构建完成后,程序计数器(PC)指向 hello 的_start 入口(如 0x4010f0),通过__libc_start_main 初始化 C 运行时环境后,最终调用 main 函数,hello 程序正式开始执行业务逻辑,完成从原进程镜像到 hello 程序的完全替换。
7.8 缺页故障与缺页中断处理
缺页故障是 hello 程序运行时常见的内存异常,指 CPU 访问的虚拟地址对应的页表项 "存在位(P=0)" 或权限不匹配,触发内核的缺页中断处理流程,分为 "合法缺页" 和 "非法缺页" 两类。合法缺页的触发场景包括 hello 首次访问.text 节(物理页未加载)、首次分配堆内存(物理页未分配)等,地址属于进程虚拟地址空间;非法缺页则是由于访问 0x0 等非法地址,或尝试修改.text 节等只读区域,权限不匹配或地址超出进程虚拟地址空间范围。缺页中断的处理流程清晰有序:CPU 检测到故障后,保存当前进程的上下文(寄存器、PC),切换至内核态,将错误虚拟地址存入 CR2 寄存器,触发缺页中断;内核接收中断后,通过进程的虚拟地址空间描述符校验 CR2 中的地址合法性,若为非法地址,向 hello 进程发送 SIGSEGV 信号,进程终止并提示段错误;若为合法地址,进一步判断故障原因,若为页未加载则从磁盘读取对应数据到物理页,若为堆 / 栈访问则分配新的物理页,随后更新页表项,设置存在位(P=1)、写入物理页框号、同步权限属性,刷新 TLB 以清除旧映射;最后内核恢复 hello 的用户态上下文,返回故障指令重新执行,此时地址转换正常,程序继续运行。缺页中断处理既实现了内存的按需扩展,又通过权限校验保障了系统安全,是虚拟内存机制的核心支撑。

图7-2 页面命中和缺页的操作图
7.9动态存储分配管理
动态存储分配管理的核心是高效管控进程的堆空间,为 printf 等函数提供灵活的内存分配与释放服务,Linux 下的 malloc 基于 glibc 的 ptmalloc 实现。堆空间的扩展与收缩采用两种方式:小内存分配(<128KB)通过 brk 系统调用抬高堆顶指针,扩展连续的堆空间;大内存分配(≥128KB)则通过 mmap 系统调用分配匿名页,独立于连续堆空间,避免碎片化。内存释放时,mmap 分配的大内存直接通过 munmap 归还给内核,brk 分配的小内存则被标记为空闲,存入按大小分类的空闲链表(如 16B、32B、64B 等),供后续 malloc 复用,减少系统调用开销。分配策略采用首次适配或最佳适配:首次适配快速查找第一个满足需求的空闲块,效率较高;最佳适配查找最小匹配的空闲块,减少内存浪费。释放内存时,会检查相邻内存块是否空闲,合并为大内存块,降低外部碎片产生。printf 函数底层会调用 malloc 分配 4KB 左右的格式化输出缓冲区,用于存储格式化后的字符串,输出完成后通过 free 释放或由 glibc 自动管理,若缓冲区分配失败则降级使用栈上临时缓冲区,确保输出功能正常,动态存储管理机制平衡了分配效率与内存利用率。
7.10本章小结
本章围绕 hello 程序的存储管理核心机制展开总结:从逻辑地址到物理地址的转换链路(段式管理简化、四级页表与 TLB 加速、三级 Cache 优化访存延迟),到进程生命周期中的内存映射策略(fork 的写时复制复用内存、execve 的地址空间重构与懒加载),再到缺页中断的合法 / 非法区分处理,以及动态存储分配对堆空间的高效管理,全程体现了硬件(MMU/TLB/Cache)、操作系统(页表、缺页处理、权限控制)与应用程序的深度协同,既实现了内存的高效利用、进程隔离与按需扩展,又保障了程序运行的稳定性与安全性,构成了现代计算机系统存储管理的完整体系
(第7章 2分)
第 8 章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
8.2.1Unix I/O接口:
(1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
(2)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
(3)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(4)关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
8.2.2Unix I/O函数:
(1)int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2)int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(3)ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

图8-1 printf函数体
先看参数部分:"..."是可变形参的一种写法,如果传递的参数的个数不确定时,那就使用这种方法来表示
va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。进一步查看windows系统下的vsprintf函数体:

图8-2 vsprintf函数体
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
再进一步对write进行追踪:

图6-3 write部分
这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:

图6-4 sys_call实现
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章系统阐述了 Linux 对 I/O 设备的统一管理机制:通过"一切皆文件"的抽象模型,将键盘、显示器等设备映射为文件,并借助 Unix I/O 接口提供一致的操作方式。在此基础上,深入剖析了 hello 程序所依赖的两个关键 I/O 函数------printf 与 getchar 的实现原理。整个 I/O 过程贯穿用户空间、系统调用、内核驱动与硬件设备,充分展现了操作系统在简化应用开发与高效管理硬件之间的精妙平衡。
(第8章 1分)
结论
hello所经历的过程总结:
从文本到token(预处理):hello.c经过预处理展开头文件、宏替换,蜕变为自包含的hello.i。
从高级到低级(编译):编译器将C代码转化为x86-64汇编指令,完成语法树构建与优化。
从符号到二进制(汇编):汇编器将助记符翻译为机器码,生成包含重定位信息的可重定位目标文件。
从孤立到完整(链接):链接器通过符号解析和地址重定位,将分散的代码段与库函数编织成可执行整体。
从静态到动态:Shell通过fork创建进程副本,execve将程序加载到内存,开启hello的运行时旅程。
从虚拟到物理:MMU借助页表与TLB,将虚拟地址空间映射到物理内存,实现隔离与共享。
从指令到交互:printf通过系统调用链将数据送达屏幕,getchar等待键盘中断完成人机对话。
从存在到消亡:进程执行完毕,系统回收所有资源,回归"零"状态,完成生命轮回。
计算机系统就像一场精心编排的交响乐,每个层次既是独奏者又是合奏者。hello程序的旅程让我深刻体会到"抽象"是系统设计的灵魂 - 从高级语言到机器指令,从虚拟内存到物理地址,每一层抽象都隐藏了复杂性,却暴露了恰到好处的接口。这种分层设计让程序员能够站在巨人的肩膀上创新,而不必关心底层硬件的具体实现。
最令人惊叹的是系统各组件间的无缝协作:编译器与链接器的默契配合,操作系统与硬件的深度耦合,动态链接的延迟绑定巧妙平衡了效率与灵活性。这种协同设计启示我们,优秀的系统不是单个组件的简单叠加,而是有机的整体,每个部分都在为整体目标服务。
hello的剖析过程启发我们,未来的系统设计可以更加智能化。比如引入机器学习优化编译策略,根据程序特征自动选择最佳优化方案;设计更细粒度的内存管理机制,实现按需分配与实时回收;构建自适应I/O调度器,动态调整缓冲区策略提升交互体验。更重要的是,我们应该致力于打造更加透明可控的系统,让开发者能够直观理解从代码到执行的完整链条,从而创造出更高效、更可靠的软件系统。
计算机系统的精妙之处在于,它用严谨的逻辑和工程方法,将冰冷的硬件赋予了执行人类智慧的能力。每一个hello程序的运行,都是对人类创造力与工程智慧的一次致敬。
(结论 0 分,缺失- 1 分)
附件
|------------|--------------------------------|
| 文件名 | 作用 |
| hello.i | 预处理后得到的文本文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello | 完整的可执行文件hello.out |
| hello.asm | 反汇编hello得到的反汇编文件 |
| hello.elf | 用readelf读取hello可执行文件得到的ELF格式信息 |
| hello2.asm | 反汇编hello.o得到的反汇编文件 |
| hello2.elf | 用readelf读取hello.o得到的ELF格式信息 |
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
1\] 林来兴. 空间控制技术\[M\]. 北京:中国宇航出版社,1992:25-42. \[2\] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集\[C\]. 北京:中国科学出版社,1999. \[3\] 赵耀东. 新时代的工业工程师\[M/OL\]. 台北:天下文化出版社,1998 \[1998-09-26\]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5). \[4\] 谌颖. 空间交会控制理论与方法研究\[D\]. 哈尔滨:哈尔滨工业大学,1992:8-13. \[5\] KANAMORI H. Shaking Without Quaking\[J\]. Science,1998,279(5359):2063-2064. \[6\] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era\[J/OL\]. Science,1998,281:331-332\[1998-09-23\]. http://www.sciencemag.org/cgi/ collection/anatmorp. ****(参考文献0分,缺失 -1分)****