【程序人生-Hello’s P2P】

计算机系统原理

大作业

题 目 程序人生-Hello's P2P

计算学部

摘 要

本文以hello程序为研究载体,围绕"程序人生"全流程展开计算机系统原理探究,旨在具象化呈现编译链路、进程管理、存储管理与 I/O 管理的核心机制。研究过程中,通过 GCC、GDB、Readelf 等工具,追踪hello从预处理、编译、汇编、链接的生成过程,解析其在Ubuntu系统下的进程创建、程序替换、地址转换、内存映射及I/O交互逻辑。研究清晰展现了"预处理→编译→汇编→链接"的 P2P 链路与"用户态→内核态→硬件层"的 O2O 执行链路,揭示了写时复制、四级页表、TLB 缓存、中断驱动等关键优化机制的工作原理。本文通过小型程序串联系统底层知识,深入理解计算机系统的分层设计思想与资源管控逻辑,对深化系统原理认知、提升底层开发与问题排查能力具有实践意义。

关键词: hello 程序;计算机系统原理;编译链路;存储管理;I/O 管理

第1章 概述

1.1 Hello简介

Hello 的程序人生遵循"P2P"与"O2O"全流程。从源码经预处理、编译、汇编、链接生成可执行文件,再由 bash 通过 fork 创建子进程、execve 替换地址空间启动运行;运行中通过存储管理实现地址转换与内存访问,借助 I/O 管理完成字符串打印与键盘输入,最终进程终止并回收资源,全程体现计算机系统的分层协作逻辑。

1.2 环境与工具

软硬件环境:Legion Y9000P IRX9 笔记本电脑(x86-64 架构);Ubuntu 20.04 LTS 操作系统

开发与调试工具:GCC 9.4.0;GDB 9.2、Readelf 2.34、Objdump 2.34、Perf 5.4.0

1.3 中间结果

hello.c:源文件,包含hello程序核心逻辑。

hello.i:预处理文件,由hello.c经头文件展开、注释删除后生成。

hello.s:汇编文件,由hello.i编译生成,包含x86-64架构的汇编指令。

hello.o:可重定位目标文件,由hello.s汇编生成,含二进制机器码、符号表与重定位信息。

hello:可执行文件,由hello.o链接系统库生成,具备独立运行能力。

asm.txt/asm2.txt:反汇编文件,分别存储hello.o与hello的反汇编结果,用于指令分析。

hello_elf.txt:ELF 解析文件,记录hello的 ELF 文件头、节头表等结构信息。

1.4 本章小结

本章明确了hello程序的全生命周期流程与实验研究框架,梳理了实验所需的软硬件环境、工具链及中间产物。通过"P2P"编译链路与"O2O"执行链路的核心划分,为后续深入分析预处理、编译、汇编、链接、进程管理、存储管理及 I/O 管理等环节奠定了基础,确保实验按流程有序开展。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理是 C 语言程序编译流程的第一步,核心作用是文本替换与清理------ 处理代码中的预处理指令、删除注释、处理条件编译等,最终生成纯 C 代码(.i文件)

预处理的作用:

1.头文件展开:将#include <stdio.h>这类指令替换为对应头文件(如/usr/include/stdio.h)的全部内容

2.宏替换:用实际的常量或者字符串来替换宏定义的符号(如#define MAX)

3.注释删除:将// 注释内容或/* 多行注释 */全部删除

4.条件编译处理:如#if DEBUG ... #endif,根据条件决定是否保留某段代码。

2.2在Ubuntu下预处理的命令

1.预处理命令:

gcc -E hello.c -o hello.i

2.得到的hello.i文件如下所示

2.3 Hello的预处理结果解析

打开hello.i后,会发现文件变成了编译器友好的输入文件 ------ 原始hello.c中的注释全部消失了,无任何 #include 指令 ,所有 #include 都被替换为对应头文件的完整文本内容。头部包含大量展开的头文件内容。文件有三千多行,只有在文件末尾能看见原hello.c的代码。

2.4 本章小结

本章主要介绍了预处理的功能,核心是将hello.c转化为hello.i,并且通过将.c文件和.i文件进行对比,了解展开头文件、删除注释等预处理的功能。这一过程实现了将人类友好代码到编译器友好输入的转化,为后续编译环节奠定基础。

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译是 C 语言编译流程的核心环节,其本质是将预处理后完整的 C 代码(.i文件)翻译成机器可理解的汇编语言代码(.s文件),同时完成语法检查、语义分析与初步代码优化。

编译的作用:

1.语法与语义检查:验证hello.i的代码是否符合 C 语言规则,若有错误编译会直接报错并终止。

2.代码分析与转换:拆解C代码的逻辑结构,将语法翻译成对应架构的汇编指令。

3.初步代码优化:在不改变代码语义的前提下,提升后续执行效率。

3.2 在Ubuntu下编译的命令

1.编译的命令:

gcc -S hello.i -o hello.s -Og -m64

3.3 Hello的编译结果解析

3.3.1数据类型

(1)常量

1.字符串常量

中文字符串被转换为UTF-8编码的ASCII序列,存储于.rodata.str1.8段

2.数值常量在汇编中以 "立即数"(前缀$)形式直接嵌入指令

(2)变量 (局部变量int i)

初始化i=0,用%ebp寄存器存储局部变量i

(3)数据类型(int,char*、char*\[\])

字符串常量本质是char*类型(指向字符序列的指针),编译器将其存储在.rodata段(只读数据段)

argv本质是char**(指针的指针),x86-64 架构中指针占 8 字节,因此通过 64 位寄存器(% rsi、% rbx)存储;

3.3.2赋值

赋初值(i=0),通过movl实现"立即数"到寄存系的传输

3.3.3算数操作

自增赋值(i++),通过addl,将%ebp与立即数 $1 相加后,结果存回%ebp

3.3.4关系操作

cmpl $5, %edi ; 比较argc(%edi)与5

jne .L6 ; 不相等则跳至.L6(错误处理分支)

3.3.5数组/指针操作

指针数组的内存布局:argv 是 char** 类型,存储为连续的指针序列,每个指针占 8 字节,argv 0、argv 1、...、argv n 的地址依次相差 8 字节

3.3.6控制转移

(1)if/else分支

cmpl $5, %edi ; 比较argc与5

jne .L6 ; argc!=5 → 跳至.L6(if分支)

movq %rsi, %rbx ; argc==5 → 执行else分支(循环初始化)

(2)for循环

cmpl $9, %ebp ; 条件判断:i<=9?

jg .L7 ; 不满足则跳至.L7(循环结束)

jmp .L2 ; 跳转回条件判断

3.3.7函数操作

(1)参数传递

第一个printf:leaq .LC0(%rip), %rdi

无参数函数无显式参数传递指令,编译器直接生产call指令

(2)函数调用

循环内的printf 含%s占位符,必须保留原函数以解析动态参数(argv 1-argv 3

错误分支的printf 是 "固定字符串 + 换行",无格式解析需求,优化为puts可提升执行效率、相比 printf,省去格式符解析和可变参数处理的步骤。 这也体现了编译有初步优化代码的功能,这种优化完全不改变程序语义,却能降低系统资源开销,

3.4 本章小结

本章围绕计算机的编译阶段展开,阐述了从预处理文件hello.i到汇编文件hello.s的转化过程,通过分析 C 语法与汇编指令的精准映射,呈现了高级语言到底层指令的完整链路,深入理解了计算机系统分层协作的核心逻辑,为后续汇编阶段生成二进制机器码奠定了基础。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:

汇编是指汇编器将文本文件(.s)转换成二进制的机器语言指令,并把这些指令打包成可重定位从目标程序格式(.o)的过程。

汇编的作用:

1.指令翻译:将汇编指令逐行翻译成对应的二进制机器码,每个汇编指令对应一条机器码。

2.生成 ELF 格式文件:将翻译后的机器码、数据、符号表等按 ELF标准组织,生成.o文件,确保文件能被后续链接器识别和处理。

4.2 在Ubuntu下汇编的命令

汇编的命令:

gcc -C hello.s -o hello.o

4.3 可重定位目标elf格式

hello.o是二进制文件,,需用readelf等工具查看其结构和内容

可以在命令行中输入:

readelf -h -S hello.o

本次实验中,由于系统编译环境默认启用位置无关代码(PIC),生成的 hello.o 并非标准的可重定位目标文件(REL 格式),而是共享目标文件(DYN 格式)。两者的核心差异在于:

REL 格式:为静态链接设计,无程序头表,节地址为 0,仅包含重定位信息;

DYN 格式:支持动态加载,含程序头表,节地址为相对地址,保留重定位信息。

得到结果如下:

(1)ELF 文件头

文件头是 ELF 文件的 "总目录",记录文件的基本信息,确保系统和工具能正确识别

(2)节头表

节头表记录每个节的名称、类型、偏移量、大小等信息,链接器通过节头表定位各类数据。

(3)重定位节

重定位是 "可重定位目标文件" 的核心特性 ------hello.o中引用的printf、atoi等函数定义在外部库中,汇编阶段无法获取其真实地址,因此用 占位地址 + 重定位条目标记,链接器需通过重定位条目替换为真实地址。

(4)符号表

符号表记录所有符号的属性(是否定义、所属节、类型),重定位条目通过符号索引关联到符号表,明确需要重定位的符号是什么类型。

4.4 Hello.o的结果解析

通过命令objdump -d -r hello.o >asm.txt将hello.c的反汇编保存在文件asm.txt中

4.4.1机器语言的架构

x86-64 架构的机器语言是可变长度的二进制指令序列(十六进制表示),核心由 "操作码+ 寻址方式码+ 操作数+ 前缀" 四部分组成。

4.4.2常量

(1)数字常量

数字从十进制变成了16进制,无前缀标识($)

(2)字符串常量

4...4.3分支转移

汇编指令用 符号标签(.L6/.L2)来表示跳转目标,而在hello.o的反汇编中,直接用绝对地址或者相对地址表示跳转位置

4.4.4函数调用

汇编指令用函数名+@PLA表示调用目标和链接类型,而在反汇编中在call指令后用真实偏移量表示跳转到 .plt 节的函数入口

4.5 本章小结

本章围绕汇编过程展开,首先明确汇编是将.s汇编文件转换为.o可重定位目标文件的过程,接着通过查看 ELF 的文件结构、objdump解析反汇编结果,解析了 ELF 格式与功能,了解了机器语言在反汇编中的表现形式。通过本章学习,可掌握汇编的基本流程、目标文件结构及反汇编结果的解析方法,理解重定位机制对后续链接过程的关键意义。

第5章 链接

5.1 链接的概念与作用

链接的概念

链接是指链接器将一个或多个可重定位目标文件(.o)、系统库文件合并,完成符号解析与重定位,最终生成可执行目标文件的过程。

链接的作用

1.符号解析:解析目标文件中未定义的符号(如printf、atoi等外部库函数),将其关联到系统库中的实际定义。

2.重定位:根据符号解析结果,将.o文件中占位地址 + 重定位条目替换为可执行文件中的真实虚拟地址。

3.合并文件:将多个目标文件的.text(代码节)、.rodata(只读数据节)等合并为可执行文件的对应节,并生成.plt(过程链接表)、.got(全局偏移表)等动态链接相关节。

5.2 在Ubuntu下链接的命令

用 ld 命令,需要在链接时添加 crt1.o、crti.o、crtn.o 等启动文件

ld -o hello

/usr/lib/x86_64-linux-gnu/crt1.o

/usr/lib/x86_64-linux-gnu/crti.o

hello.o

-lc

/usr/lib/x86_64-linux-gnu/crtn.o

-dynamic-linker /lib64/ld-linux-x86-64.so.2

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

使用命令查看 ELF 文件头(-h)、节头表(-S)、程序头表(-l)

readelf -h -S -l hello > hello_elf.txt

(1)ELF 文件头,与hello.o类似

Type类型从REL(可重定位文件)转为EXEC(可执行文件)

(2)节头表

对比 hello.o,hello 的节头表中无重定位节(如 .rela.text、.rela.data),因为链接阶段已完成所有重定位,地址已确定。

(3)程序头表

hello.o 无此部分,hello会显示多个段

5.4 hello的虚拟地址空间

使用gdb加载hello,于5.3的ELF程序头表对照分析

ELF 程序头表的 LOAD 段直接映射为程序自身的内存段,虚拟起始地址、文件偏移、大小、对齐大小 完全一致,非 LOAD 段(INTERP、DYNAMIC、GNU_STACK)则指导动态库加载、栈属性定义等关键行为

5.5 链接的重定位过程分析

用命令objdump -d -r hello > asm2.txti

将可执行文件hello反汇编到文本文件asm2中

5.5.1. 内部重定位

重定位前:

指令中的偏移为0x0,重定位标记R_X86_64_PC32 .rodata-0x4表示:需将该地址修正为.rodata节的PC相对地址,偏移量为-0x4

重定位后:

得到逻辑相同的leaq指令,目标地址402000是.rodate节的真实虚拟地址。

5.5.2.外部重定位(printf函数调用等)

重定位前:

重定位标记 R_X86_64_PLT32

重定位后:

占位地址被替换为真实地址,目标地址是.plt节中的入口地址

重定位的核心流程:

1.链接器读取 hello.o 的重定位节,获取每个占位地址的重定位信息;

2.结合 hello.o 的符号表和 libc.so 的符号表,解析目标地址;

3.修正 hello.o 中 .text 节的占位地址,生成最终的可执行文件 hello;

4.动态链接相关的外部符号(如 printf),通过 PLT/GOT 机制延迟绑定,运行时由动态链接器完成最终地址解析。

5.6 hello的执行流程

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

子程序名 真实程序地址

_init(初始化入口) 0x401000

.plt(PLT 节起始) 0x400ff0

printf@plt(动态链接入口) 0x4010a0

__cxa_finalize@plt 0x4010b0

_start(ELF 入口) 0x4010f0

deregister_tm_clones 0x401100

register_tm_clones 0x401110

__do_global_dtors_aux 0x401118

frame_dummy 0x401120

main 0x401125

__libc_csu_init 0x4011f0

__libc_csu_fini 0x401260

_fini 0x401268

_exit 0xffffffff810a3b

5.7 Hello的动态链接分析

5.7.1获取动态链接真实地址

在终端执行readelf命令,获取hello程序 PLT/GOT 节的真实地址

5.7.2动态链接前的初始状态

用gdb查看.got.plt节的初始内容

动态链接前,所有 GOT 表项的初始值均为 .plt 节内的随机指令地址

printf@plt 的初始指令为:

首先执行 endbr64 边界检查,然后通过 bnd jmpq 跳转到 printf@got.plt 存储的地址 0x401040;

由于 0x401040 是 .plt 节的 endbr64 指令地址,执行后会触发动态链接器的介入,开始解析 printf 的真实地址

5.7.3动态链接后PLT/GOT的内容

5.7.4前后 printf@got.plt 的内容变化

状态 printf@got.plt地址 内容(真实值) 地址归属

动态链接前 0x404020 0x0000000000401040 .plt 节占位地址

动态链接后 0x404020 0x00007ffff7df6c90 libc.so 真实函数地址

5.8 本章小结

本章分析hello程序的链接过程,明确链接的符号解析、重定位与文件合并核心功能。通过工具对比可重定位与可执行 ELF 文件格式差异,梳理虚拟地址映射、重定位流程及程序执行链路,重点验证了动态链接的 PLT/GOT 延迟绑定机制。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程是操作系统中正在运行的程序的执行实例,是操作系统进行资源分配和调度的基本单位。

进程的作用:

1.资源隔离:每个进程拥有独立的虚拟地址空间,保证程序运行的安全性。

2.并发执行:操作系统通过调度多个进程,提升 CPU 利用率。

3.资源分配载体:操作系统的内存、文件、网络等资源,都以进程为单位分配,每一个进程能独立使用自己的内存空间存储变量、读取命令行参数,不会和其他进程争抢资源。

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

Shell 的作用:Shell是用户与操作系统内核之间的桥梁,本质是一个命令解释器。

bush的处理流程:(以hello程序为例)

1.命令解析:bash 接收命令后,先拆分指令,识别出./hello是要执行的可执行程序

2.创建子进程:bash 通过fork()系统调用创建一个新的子进程,避免执行hello时自身被覆盖 ;

3.程序替换:子进程调用execve()系统调用,将hello程序加载到自己的地址空间,覆盖原来从 bash 复制来的代码和数据,此时子进程的 PID 不变,但执行内容变成hello;

4.父进程等待:bash(父进程)调用wait()或waitpid()系统调用,暂停自身执行,等待hello子进程运行结束;

5.资源回收与结果反馈:hello执行完毕后,内核回收其资源并向 bash 发送SIGCHLD信号,bash 收到信号后回收子进程,然后恢复自身运行,等待用户输入下一条命令。

6.3 Hello的fork进程创建过程

1.父进程准备(bash 作为父进程)

你在 bash 终端输入./hello后,bash(父进程)解析命令,确定需要启动hello程序,此时 bash 会调用fork()系统调用,准备创建子进程。

2.内核创建子进程

内核接收到fork()调用后,为子进程分配新的 PID,并复制父进程的核心资源:

复制虚拟地址空间映射表,文件描述符和代码与数据

3.fork 的两次返回

父进程(bash):fork()返回子进程的 PID,bash 通过这个 PID 感知子进程的存在;

子进程:fork()返回 0,子进程据此判断自己是新创建的进程,准备加载hello程序。

4.子进程的程序替换(execve)

子进程不会执行 bash 的代码,而是调用execve()系统调用:

释放从 bash 复制来的地址空间,将hello的 ELF 文件加载到子进程的虚拟地址空间,设置程序入口为hello的_start地址,子进程从此执行hello的代码。

5.父子进程的并发执行

父进程(bash)调用wait()等待子进程(hello)执行完毕;

子进程(hello)独立执行指令,完成打印、计算等功能,与 bash 并发运行。

6.4 Hello的execve过程

1.释放旧地址空间

子进程原本复制了 bash 的代码段、数据段等资源,execve 会先释放这些资源的虚拟地址映射。

2.映射 hello 的段到虚拟地址空间

内核根据hello的程序头表,将LOAD段通过mmap系统调用映射到子进程的虚拟地址空间。

3.设置程序入口

内核将程序计数器(PC)设置为hello的 ELF 入口点_start(0x4010f0),子进程从此开始执行hello的代码,而非 bash 的代码。

4.传递参数与环境变量

execve 会将命令行参和环境变量存入子进程的栈空间,供hello的main函数读取。

6.5 Hello的进程执行

6.5.1基本概念

控制流:程序计数器值的序列叫做控制流。每个进程对应一个逻辑控制流,内核通过调度让多个逻辑控制流交替执行。

进程上下文:内核重启被抢占进程所需的全部状态,包括通用寄存器值、程序计数器(PC)、用户栈、页表、打开的文件描述符等。

进程时间片:调度器为进程分配的单次 CPU 占用时间

6.5.2进程调度

进程调度的核心是内核调度器通过时间分片实现多任务并发,过程如下

1.控制流的切换:内核调度器从就绪队列选中hello,恢复其进程上下文(加载保存的寄存器、PC 等),hello的逻辑控制流开始执行,此时占用 CPU 的时间片。

2.时间片分配:当hello的时间片耗尽,定时器中断触发内核抢占,内核保存hello的上下文,将其逻辑控制流暂停,切换到其他进程的控制流执行。

3.后续调度:待hello再次被调度器选中,内核恢复其保存的上下文,hello的控制流从暂停处继续执行,直到时间片再次耗尽或进程终止。

6.5.3用户态与核心态的转换

用户态:hello执行自身代码时,处理器的模式位未置位,进程运行在用户态。此时hello只能执行非特权指令,仅能访问自身的私有地址空间,无法直接访问内核内存或硬件资源。

核心态:当hello调用printf时,会通过异常机制触发内核陷阱,处理器置位模式位,hello切换到核心态,此时进程可执行特权指令。

6.6 hello的异常与信号处理

6.6.1hello执行过程中会出现的异常

异常分类 触发场景 对应信号 处理方式

异步中断 按下Ctrl+C SIGINT 终止进程

异步中断 按下Ctrl+Z SIGTSTP 暂停进程

同步陷阱 调用exit()函数 SIGCHLD 通知父进程(bash)

同步故障 非法内存访问 SIGSEGV 终止进程并生成core转储文件

同步故障 除零错误 SIGFPE 终止进程

同步终止 执行非法指令 SIGILL 终止进程

6.6.2正常运行

程序会打印第 1 条Hello 2024113125 wangjunbo 18249877079,然后进入4秒休眠,然后再次打印,循环打印十条

6.6.3不断乱按数字字母,回车

在运行过程中不断乱按,程序会正常循环输出Hello 2024113125 wangjunbo 18249877079,但是前面会出现乱按的数字字母

6.6.4键入Ctrl+z

Ctrl+Z触发 Linux 的SIGTSTP信号,属于 异步中断,系统暂停进程

(1)运行ps

ps 命令查看hello进程状态

PID表示进程ID,CMD显示程序是否运行

(2)运行jobs

jobs命令查看暂停的hello作业

(3)运行pstree

pstree命令查看hello的进程树

可以看到hello是bash的子进程

(4)运行fg

键入回车,输出会直接换行,但是循环结束后getchar()函数只会读取第一个回车,剩下的在终端当作输入

程序会继续执行,恢复进程运行

(5)运行kill

用ps获取hello的PID,然后kill+信号编号+PID即可杀死程序,进程立即终止。

6.6.5键入Ctrl-c

Ctrl-C直接触发程序终止

6.7本章小结

本章围绕hello程序,,分析了进程管理的核心机制。从进程的概念与作用出发,随后梳理了 bash 启动hello的流程,以及用户态与核心态的权限隔离与切换机制。异常与信号处理部分, 呈现了hello进程从创建、执行到终止的全流程

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.概念:

逻辑地址:程序代码中使用的地址,需经段式管理转换为线性地址。

线性地址:段式管理的输出、页式管理的输入,是连接逻辑地址与物理地址的中间地址。

虚拟地址:进程看到的独立地址空间,与物理地址无关,需经页式管理映射到物理地址。

物理地址:内存硬件实际的地址,是数据真正存储的位置。

2.hello的的地址空间

地址转换关系:hello的逻辑地址(代码段偏移0x10f0)

→ 线性地址(0x400000 + 0x10f0 = 0x4010f0)

→ 物理地址(内核通过页表映射得到)

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

段式管理的核心是通过段选择子和段描述符表(GDT/LDT),将 48 位逻辑地址拆分为16 位段选择子和32 位段内偏移,最终转换为 32 位线性地址:

1.逻辑地址拆分:48 位逻辑地址中,高 16 位是段选择子,低 32 位是段内偏移(有效地址);

2.段描述符查找:段选择子的TI位决定查找 GDT或 LDT,索引值指向描述符表中的具体段描述符;

3.线性地址计算:从段描述符中取出 32 位段基地址,与 32 位段内偏移相加,得到最终的 32 位线性地址。

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

页式管理是将线性地址与物理地址建立映射的核心机制,流程如下:

1.地址拆分:x86-64将 48 位虚拟地址拆分为四级页表索引(PML4/PDPT/PD/PT,各 9 位) + 页内偏移(12 位)。

2.页表映射:内核通过页表项(PTE) 记录虚拟页 (VP) 与物理页 (PP) 的映射关系,PTE 的有效位标识虚拟页是否缓存在物理内存中。

3.地址转换:若 PTE 有效(VM 页命中),则从 PTE 中提取物理页框号,与页内偏移拼接得到物理地址;若无效则触发缺页中断,从磁盘加载虚拟页到物理内存后再完成映射。

hello的页式转换:

运行hello并获取PID,查看虚拟地址映射,得到00401000-00402000是hello的代码段虚拟页(VP)

虚拟页对应的磁盘文件位置为08:01 728408,验证了虚拟页未缓存时从磁盘加载的逻辑,该虚拟页已被内核映射到物理内存(PTE 有效位为 1)

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

x86-64采用PML4→PDPT→PD→PT四级页表,将虚拟地址拆分为 9+9+9+9+12 位(四级索引+页内偏移),大幅减少页表占用的内存空间。

1.四级页表的地址拆:

虚拟地址拆分为VPN1~VPN4(虚拟页号)和VPO(页内偏移),对应 x86-64 将 48 位虚拟地址拆分为四级索引 + 页内偏移

2.四级页表的转换流程:

从页表基址寄存器 (PTBR)指向的PML4(一级页表)开始,依次用VPN1~VPN4查找四级页表,最终从 PT(四级页表)中提取PPN(物理页号),与VPO拼接得到物理地址。

3.TLB 的加速作用:

TLB 缓存最近使用的VPN→PPN映射关系,若查询命中则直接获取PPN,跳过图解中的多级页表遍历流程,大幅提升地址转换速度。

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

三级 Cache(L1/L2/L3)是 CPU 与物理内存之间的高速缓存层级,基于时间局部性(近期访问的内存会被重复访问)和空间局部性(访问内存时会同时访问相邻数据),将物理内存中的数据缓存到 Cache 中,减少 CPU 访问物理内存的延迟:

Cache 层级:

L1 Cache:CPU 核心独占,容量最小、速度最快;

L2 Cache:CPU 核心独占,容量中等、速度中等;

L3 Cache:CPU 所有核心共享,容量最大、速度最慢;

访问流程:

CPU 访问物理内存时,依次查询 L1→L2→L3→物理内存:若命中则直接读取;若未命中则从下一级加载数据到当前 Cache。只有在所有三级缓存都未命中的情况下,才会真正发起对主存的访问。主存的访问往返之间远高于L1/L2/L3缓存的访问延迟,取回的缓存行首先被装载到L3,再逐级上反,满足CPU的读写需求。

7.6 hello进程fork时的内存映射

bash 通过fork()创建hello子进程时,内核采用写时复制(COW)优化内存映射:

1.共享物理页:fork()执行时,内核不为hello子进程分配新物理内存,而是让父子进程(bash 为进程 1,hello为进程 2)共享所有物理页框,仅复制父进程的页表(PTE)。

2.页表项权限标:内核将共享页的 PTE权限设为只读,并标记为 "写时复制页"。3.写时复制触发:若hello子进程仅读取内存(如读取命令行参数),则持续共享物理页框,无内存复制; 若hello尝试修改内存(如修改局部变量),则触发保护故障(缺页中断),内核执行图解中的三步操作:

1.为该页分配新物理页框,复制原数据;

2.更新hello的 PTE,指向新物理页框并修改权限为可写;

3.重新执行写指令,完成数据修改。

二、hello进程 fork 时的内存映射

1.fork 阶段:bash(进程 1)与hello(进程 2)的虚拟地址空间中,0x400000~0x402000代码段指向同一物理页框,PTE 标记为只读 + COW。

2.execve 阶段:hello调用execve()后,会释放原有共享页的映射,重新建立自身 ELF 文件的虚拟地址映射,因此写时复制机制在hello进程中作用时间极短,但仍避免了 fork 时大量物理内存的复制。

3.若触发写操作:若在execve()前修改内存,则触发图解中的保护故障,内核为hello分配新物理页框,完成写时复制。

7.7 hello进程execve时的内存映射

hello子进程在fork()后调用execve()加载自身 ELF 文件时,内核会完全重建子进程的虚拟地址空间,核心流程如下:

1.释放旧地址空间:销毁fork()时从 bash 继承的所有虚拟地址映射,释放对应的页表项,但物理页框若为共享(COW)则保留;

2.解析 ELF 程序头:读取hello的 ELF 文件头和程序头表,识别LOAD段的虚拟地址、权限、偏移等信息;

3.建立新虚拟映射:通过mmap()系统调用,将hello的LOAD段映射到固定虚拟地址(如0x400000),设置对应权限;

4.加载动态库与初始化:若hello依赖动态库(如libc.so),则为动态库分配虚拟地址并映射,同时初始化栈空间(传入命令行参数、环境变量);

5.设置程序入口:将程序计数器(PC)指向hello的_start入口(0x4010f0),子进程从此执行hello代码而非 bash 代码。

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

hello程序运行时,内核采用按需分页策略 ------ 仅当进程访问某虚拟页且该页未映射到物理内存时,才触发缺页故障,进而执行缺页中断处理,核心流程如下:

1.缺页故障触发:CPU 访问hello的虚拟地址(如首次访问0x4010f0)时,若对应的页表项(PTE)有效位为 0(虚拟页未缓存到物理内存),则触发缺页异常。

2.缺页中断处理:

(1)内核检查虚拟地址合法性:若为非法地址(如越界访问),触发SIGSEGV信号终止hello; 若地址合法,为该虚拟页分配物理页框;

(2)更新页表项:将 PTE 的有效位设为 1,关联新分配的物理页框;

(3)重启故障指令:CPU 重新执行触发缺页的指令,此时虚拟页已映射到物理内存,指令正常执行。

7.9本章小结

本章以hello程序为核心载体,层层递进解析了操作系统存储管理的核心逻辑:首先明确逻辑地址、线性地址、虚拟地址与物理地址的定义及转换关系,构成存储管理的基础框架;接着阐释段式管理将hello的逻辑地址转换为线性地址的核心流程;随后通过页式管理实现线性地址到物理地址的映射,四级页表减少页表内存占用,TLB 则缓存映射关系大幅提升地址转换效率。在此基础上,三级 Cache 降低 CPU 访问延迟,fork 创建hello子进程时优化内存共享,execve 则重建地址空间完成程序替换。整体而言,本章呈现了hello程序从地址转换到内存映射、异常处理的全链路存储管理逻辑,体现了操作系统对内存资源的精细化管控与性能优化思路。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux 将所有 I/O 设备抽象为文件,核心管理方法如下:

1.设备文件化:每个 I/O 设备对应/dev目录下的一个设备文,hello的标准输出(stdout)对应设备文件/dev/tty,文件描述符为 1。

2.统一接口封装:内核通过文件系统接口(open/read/write/close)屏蔽设备硬件差异,hello无需区分终端、磁盘等设备类型,只需调用统一系统调用即可完成 I/O 操作。

3.设备驱动分层:内核通过设备驱动程序实现硬件控制,驱动分为字符设备驱动、块设备驱动等,hello的打印操作最终由tty驱动完成硬件交互。

可见hello的 stdout(文件描述符 1)映射到终端设备文件/dev/pts/0,验证了 Linux "设备文件化" 的管理机制,hello通过操作该设备文件实现字符串打印。

8.2 简述Unix IO接口及其函数

Unix 提供统一的 I/O 接口,hello的打印与输入操作依赖以下核心函数,按调用流程分类如下:

1.核心系统调用(内核态接口)

write(int fd, const void *buf, size_t count):向文件描述符fd对应的设备写入count字节数据,hello的printf最终调用该函数向 stdout(fd=1)写入字符串。

read(int fd, void *buf, size_t count):从文件描述符fd读取count字节数据,hello的getchar最终调用该函数从 stdin(fd=0)读取键盘输入。

open(const char *pathname, int flags):打开设备文件或普通文件,返回文件描述符,hello运行时由内核自动打开 stdin/stdout/stderr(fd=0/1/2)。

close(int fd):关闭文件描述符,释放内核资源,hello退出时自动关闭所有打开的文件描述符。

2.标准库函数(用户态封装)

printf(const char *format, ...):格式化输出函数,封装vsprintf(字符串格式化)和write(系统调用),hello通过它实现带参数的字符串打印。

getchar(void):读取单个字符,封装read系统调用,从 stdin 读取字符直至回车触发返回,hello通过它等待用户输入。

puts(const char *s):输出字符串并换行,hello错误分支的printf被编译器优化为puts,减少格式解析开销。

3.调用关系梳理

hello的 I/O 操作链路:printf/getchar(标准库)→ vsprintf/read(底层封装)→ write/read(系统调用)→ 设备驱动 → 硬件(终端)。

8.3 printf的实现分析

一、用户态

格式化字符串生成:

hello中printf("Hello %s %s %s\n", argv1, argv2, argv3)调用后,printf会调用 vsprintf函数,解析格式化字符串中的%s占位符,将命令行参数argv1/argv2/argv3与固定字符串拼接,生成完整的 ASCII 码字符串。

行缓冲触发条件:

printf默认启用行缓冲,缓冲区大小通常为 4096 字节,满足以下条件之一时触发数据写入:

输出字符串包含换行符\n(hello的打印逻辑满足此条件);

缓冲区被写满;

程序调用fflush(stdout)主动刷新;

程序正常退出或调用exit。

二、系统调用

缓冲区刷新后,printf通过 write系统调用 发起 I/O 请求,传入三个核心参数:文件描述符fd=1, 用户态缓冲区地址, 字符串长度。

write系统调用的底层实现依赖 syscall指令 ,触发以下流程:CPU 执行syscall指令后,将用户态寄存器值保存到内核栈,切换特权级; 通过系统调用号查找内核态的系统调用表,定位sys_write函数并执行。

三、内核态

1.sys_write函数处理

内核收到write请求后,通过文件描述符fd=1找到对应的终端设备(如/dev/pts/0),进而调用终端驱动程序(tty驱动)的写接口。

2.字符显示驱动的转换逻辑:

终端驱动程序收到内核缓冲区的 ASCII 码后,通过字模库完成 ASCII 码到显示点阵的转换:字模库存储每个 ASCII 字符的点阵数据 ,驱动程序根据 ASCII 码查找字模库,提取对应的点阵数据,再转换为显存(VRAM)支持的 RGB 颜色信息。

四、硬件层

1.数据写入显存(VRAM):

终端驱动将转换后的 RGB 颜色信息写入显存(VRAM),显存是一块专门用于存储图像数据的内存区域,每个存储单元对应屏幕上一个像素点的 RGB 分量。例如hello打印的字符'H',其点阵对应的像素点 RGB 信息会被写入显存的对应位置,形成字符的可视化图像。

2.显示芯片扫描与屏幕输出:

显示芯片(GPU)按固定刷新频率(如 60Hz)逐行扫描显存:从显存起始地址开始,依次读取每个像素点的 RGB 分量数据;通过信号线将 RGB 数据传输到液晶显示器(LCD),显示器根据数据控制每个像素点的发光强度,最终在屏幕上呈现出hello的打印字符串。

hello的printf实现链路可概括为:

vsprintf格式化 ASCII 字符串 → 行缓冲触发 → write系统调用 → syscall陷阱切换 → 内核sys_write处理 → tty驱动字模转换 → 数据写入 VRAM → GPU 扫描显示 → 屏幕呈现。

8.4 getchar的实现分析

一、内核态

1.键盘中断触发:

当用户按下键盘按键时,键盘控制器会向 CPU 发送异步中断请求(IRQ1),打断当前正在执行的进程(包括hello),触发 CPU 的中断响应机制。

2.中断服务子程序(ISR)处理:

CPU 响应中断后,切换到内核态,执行键盘中断服务子程序:

读取按键扫描码;

扫描码转 ASCII 码;

存入键盘缓冲区。

3.中断返回:

中断处理完成后,CPU 恢复被打断进程(hello)的上下文,hello从暂停处继续执行,整个中断处理过程对用户态程序透明。

二、用户态

1.getchar 的函数封装:

hello调用getchar()时,本质是调用标准库封装的输入函数,核心是同步调用read系统调用,传入参数:

文件描述符fd=0(对应标准输入 stdin,绑定键盘设备);

缓冲区地址(仅存储1个字符,因getchar仅读取单个字符);

读取长度1(仅获取一个ASCII码)。

2.阻塞等待输入:

若调用read时,内核的键盘缓冲区为空,hello进程会进入阻塞状态,无数据则使进程休眠,直至有中断事件唤醒。

3.唤醒与数据返回:

按下按键,触发键盘中断,内核会唤醒等待队列中的hello进程,将其移回运行队列:

内核从键盘缓冲区读取第一个ASCII码,复制到hello的用户态缓冲区;

read系统调用返回读取的字符数(1),getchar从缓冲区取出该字符并返回,hello恢复执行。

4.回车触发的特殊处理:

getchar默认采用行缓冲输入模式,仅当用户按下回车键时,read系统调用才会返回完整输入(仅取第一个字符),剩余字符仍保留在键盘缓冲区,供后续输入函数读取。

8.5本章小结

本章通过hello的 I/O 操作,呈现了操作系统对 I/O 设备的抽象封装、中断驱动的异步处理、进程阻塞与唤醒等机制,体现了计算机系统分层设计的思想 ------ 通过用户态、内核态、硬件层的协同,在保证安全性与兼容性的同时,实现了 I/O 操作的高效可靠。

结论

hello的程序人生,是计算机系统分层协作的生动缩影。从源码形态出发,它历经预处理的文本规整、编译的高级语言到汇编指令转换、汇编的二进制编码生成,再到链接的符号解析与重定位,最终形成具备独立执行能力的可执行文件,完成了从"人类可读"到"机器可懂"的蜕变。启动阶段,bash通过fork创建子进程,写时复制机制以资源共享优化进程创建效率,execve则重建虚拟地址空间,将hello的代码与数据载入内存,开启其运行之旅。运行中,段式与页式管理协同完成地址转换,TLB与三级Cache层层加速内存访问,缺页中断按需加载物理内存,让每一次指令执行都高效有序;而printf的格式化输出与getchar的键盘输入,通过系统调用陷阱切换特权级,依托设备抽象与中断驱动,实现了用户态与硬件层的顺畅沟通。异常与信号处理机制则如同隐形护盾,在Ctrl+C 终止、Ctrl+Z暂停等场景中,保障系统与程序的稳定可控,直至进程完成使命,资源被内核回收,为这段 "人生旅程" 画上句点。

这段旅程让我深刻体悟到计算机系统的设计智慧------分层架构是贯穿始终的核心逻辑,用户态与内核态的隔离保障安全,抽象封装屏蔽硬件差异,优化机制贯穿全流程:写时复制延迟资源消耗,按需分页平衡内存利用率与性能,Cache缓存挖掘局部性原理价值。这些设计以"协同"与"权衡"为核心,让复杂的硬件资源被高效调度,让简单的程序能借助系统之力实现丰富功能。计算机系统的魅力在于,它以严谨的逻辑构建底层基石,又以开放的设计承载无限创新,而hello这样的简单程序,正是解锁这份魅力的钥匙,让我们得以窥见底层世界的精妙与宏大。

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c:源文件,包含hello程序核心逻辑。

hello.i:预处理文件,由hello.c经头文件展开、注释删除后生成。

hello.s:汇编文件,由hello.i编译生成,包含x86-64架构的汇编指令。

hello.o:可重定位目标文件,由hello.s汇编生成,含二进制机器码、符号表与重定位信息。

hello:可执行文件,由hello.o链接系统库生成,具备独立运行能力。

asm.txt/asm2.txt:反汇编文件,分别存储hello.o与hello的反汇编结果,用于指令分析。

hello_elf.txt:ELF 解析文件,记录hello的 ELF 文件头、节头表等结构信息。

参考文献

1博主 Pianist. printf 函数实现的深入剖析 EB/OL. https://www.cnblogs.com/pianist/p/3315801.html.

2Bryant R E, O'Hallaron D R. 深入理解计算机系统(原书第 3 版)M. 龚奕利,贺莲,译。北京:机械工业出版社,2016.

3Kerrisk, M. (2010). The Linux Programming Interface. No Starch Press.

4GCC: The GNU Compiler Collection. (n.d.). GCC Official Documentation. Retrieved from https://gcc.gnu.org/onlinedocs/

5The Linux Kernel Documentation. (n.d.). Process Address Space. Retrieved from https://www.kernel.org/doc/html/latest/

相关推荐
通信小小昕19 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
CedarQR1 天前
万字长文:从零在 RK3588 上部署 PaddleSpeech 中文 TTS 全流程(FastSpeech2 + HiFiGAN)
开发语言·c++·嵌入式硬件·ubuntu·json
2501_916008891 天前
HTTPS 抓包遇到证书绑定怎么办,使用 TraceEagle 解除 App 证书校验
网络协议·计算机网络·http·网络安全·ios·adb·https
雷工笔记1 天前
Ubuntu迁移记录
笔记·ubuntu
zx1154501 天前
前后端开发必备的计算机网络知识
计算机网络
一叶龙洲1 天前
ubuntu26.04 xfce美化成mac
服务器·ubuntu·macos
一叶龙洲2 天前
wslg打开Ubuntu24.04默认打开图形界面
linux·服务器·数据库·ubuntu
敖行客 Allthinker2 天前
Parallels Ubuntu虚拟机项目如何让手机访问?完整解决方案
linux·运维·ubuntu
keyipatience2 天前
线程栈与TLS和线程互斥
java·linux·服务器·开发语言·ubuntu