计算机系统
题 目 ++程序人生++ ++-Hello's P2P++
专 业 ++计算学部++
学 号 ++2022113719++
班 级 ++23L0517++
学 生 ++张双麒++
指 导 教 师 ++吴锐++
计算机科学与技术学院
2025 年5月
摘 要
本文以C语言程序设计课程中的经典示例------hello.c程序为切入点,阐述了该程序从源代码到可执行文件的转换过程,并分析了其作为操作系统中进程的运行机制。首先,源代码需经过一系列转换步骤,包括预处理、编译、汇编以及链接,最终生成机器可执行的二进制文件。其次,程序运行时,计算机硬件诸如CPU、输入输出设备和内存等协同工作,保障程序的正常执行。同时,操作系统承担着进程创建、调度与管理的重要职责。通过对hello.c程序的详细剖析,本文旨在帮助读者全面理解计算机系统中程序的生成与执行过程,深化对操作系统和硬件协作机制的认识。
关键词:计算机系统;预处理;编译;汇编;链接;进程管理;内存
目 录
[第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 程序通过简单的一行代码,展示了计算机系统从源程序到进程再到资源回收的完整生命周期。
1.1.1在 P2P(Program to Process) 过程中,hello.c 首先被编译器驱动程序处理,依次经过预处理(生成 hello.i)、编译(生成 hello.s)、汇编(生成 hello.o)和链接(生成最终可执行文件 hello)。随后,操作系统通过 shell 启动 hello,调用 fork() 创建子进程,再通过 execve() 加载执行,Hello 程序就从一个静态的代码文本成功转化为系统中的一个动态进程。
1.1.2在 O2O(Zero to Zero) 过程中,Hello 程序从无到有地被编写、编译并运行(从 0 到 1),运行结束后进程退出、资源释放,一切归于初始状态(从 1 回到 0)。整个过程体现了计算机系统资源生命周期的闭环管理,是理解系统运行机制的最佳范例。
1.2 环境与工具
1.2.1硬件环境
13th Gen Intel(R) Core(TM) i9-13980HX (X64 CPU); 2.20GHz; 32G RAM;
1.2.2 软件环境
Windows11 64位; Ubuntu20.04;
1.2.3 开发工具
Visual Studio Code; edb;gcc;vim;objdump;readelf
1.3 中间结果
hello.c 源程序
hello.i 预处理后的修改的C程序
Hello.s 汇编程序
Hello.o 可重定位目标文件
hello 可执行目标文件
Objdump_hello.o hello.o反汇编文件
Objdump_hello hello的反汇编文件
elf_hello hello的ELF格式
elf_hello.o hello.o的ELF格式
1.4 本章小结
本章详细阐述了 hello 程序的 P2P(点对点) 和 O2O(线上线下) 流程,并说明了其 软硬件运行环境 及 开发调试工具。此外,还列举了中间生成文件及其功能,帮助读者深入理解该程序的开发、编译、链接与执行机制。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 概念
预处理是程序编译前的首要阶段,负责对源代码进行文本级处理,但不涉及语法分析。其主要任务是对源文件(如 .c 文件)中所有以 # 开头的指令(如宏定义 #define、文件包含 #include、条件编译 #ifdef 等)进行解析和转换,最终生成修改后的中间文件(通常为 .i 文件),作为编译器的输入。
2.1.2 作用
预处理的核心功能是通过文本替换和代码扩展优化源代码结构,具体包括:
- 宏替换:
通过 #define 指令定义常量或代码片段(如 #define PI 3.14),预处理器会将所有宏标识符替换为指定值,减少重复代码。 - 文件包含:
使用 #include 指令(如 #include <stdio.h>)可将外部文件(如头文件)内容插入当前文件,实现函数声明、宏定义等代码的复用。 - 条件编译:
通过 #if、#ifdef、#endif 等指令控制代码段的编译条件,灵活适配不同平台或调试需求。
优势:提升代码可维护性(如通过宏避免硬编码)、增强模块化(通过头文件分离功能)、支持跨平台编译(条件编译)。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
图2.1最开头是hello.c文件涉及的头文件信息

图2.2文件末尾:

使用 gcc -E hello.c -o hello.i 命令对 hello.c 进行预处理后,生成的 hello.i 文件是一个展开后的 C 源文件,主要完成以下几项工作:
- 头文件展开:
所有 #include 指令被替换为对应头文件的全部内容。例如 #include <stdio.h> 会插入大量标准库函数声明和宏定义。 - 宏展开:
所有用 #define 定义的宏会被替换为实际值,例如 #define PI 3.14 会把所有 PI 替换成 3.14。 - 条件编译处理:
如 #ifdef、#ifndef 等条件编译语句会根据条件决定是否保留对应代码块。 - 注释删除:
所有 // 和 /* ... */ 的注释会被删除,保留纯净的代码内容。
2.4 本章小结
本章系统阐述了预处理阶段的核心概念与功能,并以 hello.c 及其预处理生成的 hello.i 文件为例,通过对比分析详细解析了预处理结果。具体包括:
- 头文件追踪:定位并分析 hello.i 中引入的头文件内容(如 stdio.h 的展开代码);
- 预处理机制解析:结合实例验证宏替换、条件编译等关键操作的实际效果;
- 知识衔接:通过实验结果深化对预处理流程的理解,为后续编译、链接等环节奠定基础。
第3章 编译
3.1 编译的概念与作用
概念:编译是指将高级编程语言编写的源代码转换为计算机可执行的低级语言(如汇编语言或机器码)的转换过程。这一过程由编译器完成,主要包括词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等阶段。
作用:编译的核心作用体现在三个方面:首先,进行严格的语法和语义检查,确保程序逻辑的正确性;其次,对代码进行多层次的优化,包括寄存器分配优化、冗余代码消除等,显著提升程序运行效率;最后,生成与特定硬件平台相匹配的高效目标代码。通过编译过程,不仅实现了跨平台的程序可移植性,还为后续的链接和执行环节奠定了基础,是整个软件开发流程中不可或缺的关键环节。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 数据与常量

- .LC0 和 .LC1:是两个字符串常量,用于 puts() 和 printf()。
- 存储在只读数据段 .rodata,表示这是常量数据。
3.3.2 main 函数基本框架

这是函数的标准起始部分:
main为全局符号,类型为函数。endbr64:是用于支持 Intel CET(Control-flow Enforcement Technology)的安全指令。
3.3.3 函数调用与参数处理
函数开头处理参数:

- 入栈保存
%rbp,设置新栈帧。 - 将主函数参数
argc(%edi)和argv(%rsi)保存到局部变量区域(偏移 -20 和 -32)。 - 所有局部变量和参数存储在栈上(即局部变量)。
3.3.4 if 语句与跳转(控制流)
C代码等价**:**

汇编:

· cmpl $5, -20(%rbp) 比较 argc 是否为 5。
· je .L2 如果等于则跳转(说明是 if 判断);
· 否则执行 puts() 打印提示并 exit(1) 退出。
3.3.5循环结构 (for / while)
等价C代码:

汇编体现:
初始化与跳转判断

循环体 .L4 :

· argv[i] 实际存储为一个字符串指针数组,所以访问 argv[1], argv[2], argv[3] 是通过偏移:
argv + 8,argv + 16,argv + 24
· 三个参数传给 printf("Hello %s %s %s\n", ...),分别放在 %rsi, %rdx, %rcx。
休眠时间:

将 argv[4] 转换为整数,调用 sleep(atoi(argv[4]))。
自增与跳转:

3.3.6函数调用总结
| 函数 | 参数传递方式 | 功能 |
|---|---|---|
| puts() | 常量地址(立即数)通过 %rdi | 打印提示语 |
| exit() | 整数值 1 通过 %edi | 立即退出 |
| printf() | 多个字符串地址,%rdi/%rsi/%rdx/%rcx | 格式化输出 |
| atoi() | 字符串地址 %rdi | 字符转整数 |
| sleep() | 秒数 %edi | 延时 |
| getchar() | 无 | 等待键盘输入 |
3.4 本章小结
本章介绍了编译器的概念和作用,ccl将hello.i转换为hello.s,得到一个汇编语言程序,通过分析汇编代码详细说明了编译器是怎么处理C语言的各个数据类型以及各类操作的。
第4章 汇编
4.1 汇编的概念与作用
基本概念
汇编(Assembly)是将汇编语言程序(.s文件)转换为可重定位目标文件(.o文件)的过程。这一过程由汇编器(as)完成,其核心任务是将人类可读的汇编指令逐条翻译为对应的机器指令,并按照特定格式(如ELF格式)组织成目标文件。
主要作用
- 指令转换:
- 目标文件生成:
- 重定位信息记录:
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
4.3 可重定位目标elf格式
- ELF头
ELF格式如下图:

通过readelf读取hello.o

ELF头是ELF(可执行与可链接格式)文件开头的固定长度数据结构,它包含了描述整个文件属性的关键元信息。通过16字节的魔数(0x7F+'E'+'L'+'F')标识文件类型,并记录了目标平台架构(如x86-64)、文件类型(可重定位/可执行/共享库)、程序入口地址以及段头表和节头表的位置信息等核心数据。在64位系统中,ELF头固定为64字节,包含大小端标识、ABI版本等控制字段,为后续的链接或加载过程提供必要的引导信息。使用readelf工具可以直观查看这些信息,例如在分析hello.o文件时,能看到其被标记为可重定位文件(REL),采用小端格式,目标平台为x86-64架构等关键属性。
2.节头部表

节头部表,用于描述目标文件的节,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
- 重定位节

.rela.text节是ELF目标文件中专门存储代码段(.text)重定位信息的特殊节区。它记录了在链接阶段需要修改的代码位置及其相关重定位信息。
- 符号表

.symtab节是ELF目标文件中存储符号定义和引用信息的关键节区。它记录了程序中定义和使用的所有全局符号,为链接过程提供必要的符号解析基础。
4.4 Hello.o的结果解析

- 伪指令消失:这些伪指令在汇编阶段由汇编器(如 as)解释为指令、符号、节等信息,用于生成目标文件 hello.o,不会出现在机器码中。

- 条件分支变化:标签变偏移地址
hello.s中的条件分支条件分支:

.L2 是一个符号标签,表示某段代码的位置。汇编器会将其换算成当前指令到 .L2 的相对距离,填入指令的操作数中。但在反汇编中,由于没有原始标签信息,转而显示为:
hello.o中条件分支对应的反汇编代码:

- 函数调用变化:call + 相对地址
汇编器把函数名保留为符号,真正的地址由链接器决定,机器码中先留空等重定位。
hello.s中的exit函数调用:

hello.o中exit函数对应的反汇编代码:

- 数据访问变化:助记符变为 %rip 相对寻址
汇编中用符号访问数据,机器码中使用的是 %rip + 偏移,相对定位,等待链接器修正。
hello.s中的常量数据访问:

hello.o中的对应的反汇编代码:

4.5 本章小结
本章通过将.s汇编为.o文件,了解从汇编程序到可重定位目标程序(二进制)的过程。同时通过查看ELF表,查看了其中的各项内容。又将.o反汇编,通过和汇编程序相对比,了解了他们的不同,也了解了机器代码的逻辑。
第5章 链接
5.1 链接的概念与作用
概念:
链接(Linking)是程序构建过程中的关键阶段,指将多个编译生成的可重定位目标文件(.o文件)和库文件合并,生成最终可执行文件或共享库的过程。这一过程由链接器(如ld)完成,主要处理目标文件中的符号引用与定义关系、内存地址分配以及代码重定位等问题。链接可分为静态链接(所有库代码被直接复制到可执行文件中)和动态链接(部分库代码在程序运行时加载)两种方式。
作用
符号解析与绑定,内存空间分配,重定位修正,库函数整合,生成可执行格式,优化程序结构
5.2 在Ubuntu下链接的命令

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

- ELF头信息

2 节头
描述了各个节的大小、起始位置和其他属性。


- 程序头表

5.4 hello的虚拟地址空间

| 静态段 (readelf) | 运行时段 (gdb) 映射地址 | 段类型 | 权限 | 说明 |
|---|---|---|---|---|
| .text | 0x555555558000 | LOAD | r-xp | 可执行代码段 |
| .rodata | 0x555555554000 | LOAD | r--p | 只读常量 |
| .data / .bss | 0x5555557f9000 | LOAD | rw-p | 已初始化/未初始化数据 |
| .interp | 0x00000000000002e0 | INTERP → 加载 ld-linux-x86-64.so.2 | ||
| 动态库 | 多段,如 libc.so | LOAD | 多种 | 动态链接的共享库 |

节头中的各段均能在gdb中找到,说明节头表中存储各段的起始地址与各段的虚拟地址之间存在对应关系.
-
- 链接的重定位过程分析

- 在hello.o中,main函数地址从0开始,hello.o中保存的是相对偏移地址;而在hello中main函数0xe9开始(如下图),即hello中保存虚拟内存地址,对hello.o中的地址进行了重定位。

- hello的反汇编代码含有更多的函数。

在完成符号解析后,链接器进入重定位阶段,首先将各目标文件的同类节区(如.text、.data)合并为统一的段,并基于进程虚拟地址空间为这些段分配运行时基地址,从而确定所有指令和数据的最终内存位置。随后,链接器遍历合并后的内容,将代码和数据中对符号的临时引用替换为计算得到的绝对地址,使程序中的所有内存引用都指向正确的运行时位置。这一过程将编译器生成的相对地址转换为可执行的绝对地址,最终生成具备完整内存映射结构的可执行文件,确保程序能够被操作系统正确加载和运行。
5.6 hello的执行流程





5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到 第一次调用该过程时。通过 GOT 和过程链接表 PLT 的协作来解析函数的地址。在加载时,动态链接器会重定位 GOT 中的每个条目,使它包含正确的绝对地址,而 PLT 中的每个函数负责调用不同函数。那么,通过观察 edb,便可发现 dl_init 后.got.plt 节发生的变化。
hello的节头部表中的.got.plt

执行init前

执行init后:

5.8 本章小结
链接中包含了许多提高程序执行效率、减小空间浪费的措施,如静态链接库、动态链接共享库等,为编写高效的程序提供了思路
经过链接,已经得到了一个可执行文件,接下来只需要在 shell 中调用命令就 可以为这一文件创建进程并执行该文件。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程(Process)是计算机中正在运行的程序实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、文件描述符、寄存器状态和系统资源,由操作系统内核管理。进程通过进程控制块(PCB)记录其状态(如运行、就绪、阻塞等),并通过上下文切换实现多任务并发执行。
作用:
-
资源隔离:不同进程的内存和资源相互独立,避免程序间干扰。
-
多任务并发:操作系统通过进程调度(如时间片轮转)实现多个程序"同时"运行。
-
程序执行载体:为应用程序提供运行环境,管理代码执行、内存分配和I/O操作。
-
安全控制:通过权限管理(如用户态/内核态)限制进程对系统资源的访问。
6.2 简述壳Shell-bash的作用与处理流程
Shell**(bash)的作用:**
Shell是操作系统提供的命令行解释器,负责接收用户输入的命令并将其传递给操作系统内核执行。它充当用户与操作系统之间的桥梁,支持命令解析、程序启动、任务管理等功能。
Shell 的处理流程:
- 读取命令:等待并接收用户输入的命令行字符串。
- 解析命令:将输入的命令分解成命令名称和参数。
- 执行命令 :
- 对于内置命令(如cd、exit),Shell直接执行相应操作。
- 对于外部命令,Shell创建子进程,通过exec族函数加载并运行对应程序。
- 后台执行判断:若命令末尾有"&",则将命令放到后台运行,Shell不会等待命令完成,继续接受新的输入。
这样,bash Shell实现了用户与系统的交互和资源管理。
6.3 Hello的fork进程创建过程
· 父进程开始执行
程序从主函数开始,执行打印"Hello"的代码前,会调用fork()系统调用。
· 调用fork()
fork()会复制当前进程(父进程)的内存空间,创建一个几乎完全相同的子进程。fork()返回两次:
- 父进程中,fork()返回新创建子进程的进程ID(PID)。
- 子进程中,fork()返回0。
· 父子进程分支执行
通过判断fork()返回值,程序可以区分父进程和子进程:
- 如果返回值为0,说明当前是子进程,子进程可以执行打印"Hello"的操作。
- 如果返回值大于0,说明当前是父进程,也可以选择执行自己的代码。
· 父子进程独立运行
父进程和子进程是两个独立的进程,后续操作相互独立,子进程可以打印"Hello",父进程可以等待子进程结束或继续执行其他任务。
6.4 Hello的execve过程
· 父进程或子进程调用execve()
一般是先通过fork()创建一个子进程,然后子进程调用execve()来加载并执行新的程序,比如打印"Hello"的程序。
· 参数传递给execve()
execve()接收三个参数:
- 要执行的程序路径(如"/bin/echo")
- 参数数组(例如{"echo", "Hello", NULL})
- 环境变量数组(通常传递environ)
· 系统调用执行
系统将当前进程映像替换成新程序映像,加载新程序的代码和数据段,初始化栈,开始执行新程序的入口函数。
· 新程序执行
新程序开始运行,执行打印"Hello"的操作。原进程的代码不再运行。
· 如果execve()执行失败
函数返回-1,子进程可以进行错误处理。
6.5 Hello的进程执行
1****用户态与核心态(内核态)的转换
- 用户态:程序(如Hello程序)运行在用户态,受限于操作系统的保护,不能直接访问硬件资源。
- 核心态(内核态):操作系统内核运行在核心态,有权访问所有硬件和资源。
- 当进程执行系统调用(如fork()、execve()、write()等)时,会发生从用户态切换到核心态,由内核完成对应操作。
- 系统调用执行完毕后,再切换回用户态,继续执行用户程序。
2. 进程上下文信息
- 包括CPU寄存器内容、程序计数器(PC)、堆栈指针、内存映像等,是进程运行状态的完整描述。
- 当CPU从一个进程切换到另一个进程时,操作系统会保存当前进程的上下文(称为上下文保存 ),并加载新进程的上下文(称为上下文恢复),使新进程得以继续执行。
3. 时间片与进程调度
- 多个进程共享CPU资源,操作系统采用时间片轮转调度 机制:
- 每个进程分配一个固定长度的CPU时间片。
- 进程在自己的时间片内执行,时间片用尽后,内核会强制切换到另一个进程执行。
- 进程调度指操作系统根据调度算法选择下一个执行的进程。
- 在Hello程序执行时,如果系统中有多个进程,CPU会轮流给Hello程序和其他进程分配时间片。
4. Hello 程序的执行过程示意
- Hello 程序启动,操作系统为其创建进程,初始化进程上下文,加载程序代码到内存,进程进入就绪状态。
- 调度器调度Hello进程,CPU加载Hello进程上下文,进程进入用户态开始执行。
- 执行系统调用(如write()打印Hello),进程发生用户态→内核态切换,内核完成打印操作后返回用户态。
- 时间片用尽,操作系统保存Hello进程上下文,切换到其他进程继续执行。
- 下一次调度Hello进程时,恢复其上下文,继续执行后续代码。
- Hello 进程执行结束,调用exit(),进程状态变为终止,操作系统回收资源。

6.6 hello的异常与信号处理
1. 执行过程中可能出现的异常和信号
- Ctrl-C (SIGINT):用户按下Ctrl-C时,系统发送中断信号给Hello进程,默认会终止该进程。
- Ctrl-Z (SIGTSTP):用户按下Ctrl-Z时,系统发送暂停信号,将Hello进程挂起,进入后台停止状态。
- 程序崩溃(SIGSEGV):当程序访问非法内存时,操作系统发送段错误信号,进程异常终止。
- 其他异常信号:如浮点错误(SIGFPE),子进程结束信号(SIGCHLD)等。
2. 常见命令及其作用和示例
- ps
查看当前系统运行的进程状态。
示例:运行ps可以看到Hello进程的PID和状态。 - jobs
显示当前shell管理的后台和暂停任务。
示例:在Hello程序按Ctrl-Z暂停后,执行jobs会显示暂停的Hello任务。 - pstree
以树状结构显示进程的父子关系。
示例:pstree -p显示bash父进程和它启动的Hello子进程。 - fg
将后台或暂停的作业调回前台继续执行。
示例:fg %1恢复暂停的Hello进程,使其回到前台继续运行。 - kill
向指定进程发送信号,通常用于终止进程。
示例:kill -SIGINT <PID>发送中断信号结束Hello进程。
3. 用户操作与信号处理流程
- 用户启动Hello程序,程序开始运行。
- 按下Ctrl-Z,shell发送SIGTSTP信号,Hello进程暂停,shell提示"Stopped"。
- 用户运行jobs查看暂停的Hello任务。
- 用户运行ps或pstree确认Hello进程存在且处于暂停状态。
- 用户执行fg命令恢复Hello进程,进程继续运行。
- 用户按Ctrl-C,shell发送SIGINT信号,Hello进程被终止。
- 用户通过ps确认Hello进程已退出。

正常运行

不停乱按:运行时按回车、随机字符串,发现没有影响
按下Crtl+Z,进程收到SIGSTP信号,hello进程挂起并向父进程发送SIGCHLD:

Ctrl-Z操作向进程发送了一个SIGTSTP信号,让进程暂时挂起,输入jobs、ps指令可以发现hello进程在后台挂起,通过fg指令可以恢复运行。

pstree

6.7本章小结
本章介绍了进程的概念和作用,观察了hello进程的创建,执行,终止以及各个命令的执行,如进程树,ps等。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1**、逻辑地址(Logical Address)**
逻辑地址是由程序生成的地址,也称为虚拟地址的一部分 ,它包括段选择子和段内偏移量。
在采用段式管理的系统中,CPU 首先处理的是逻辑地址。
2 、线性地址(Linear Address)
逻辑地址通过段寄存器的段基址 + 段内偏移量 转换为线性地址。
现代操作系统中段基址通常为 0,因此逻辑地址和线性地址多数时候是相同的。
3 、虚拟地址(Virtual Address)
虚拟地址是进程能"看到"的地址,它和线性地址几乎等价(对于非分页系统),但在分页系统中,虚拟地址经过页表转换后才映射到物理地址。
对于 hello 程序而言,比如 .text 段通常加载到虚拟地址 0x08048000 处,这个地址是程序在编译和链接阶段就决定的。
4 、物理地址(Physical Address)
物理地址是真正存在于内存中的硬件地址,是 CPU 通过 MMU(内存管理单元)将虚拟地址转换之后访问的内存单元地址。
操作系统和硬件通过页表将虚拟地址转换成物理地址,这个过程对用户是透明的。
5 、结合 hello 的存储器地址空间说明
当执行 ./hello 时,操作系统为该进程分配独立的虚拟地址空间,包括以下几个区域:
- 代码段(.text):包含程序的机器指令,比如 printf("hello") 所在的代码。
- 数据段(.data):存储已初始化的全局变量。
- BSS 段(.bss):存储未初始化的全局变量。
- 堆(heap):动态分配内存(如 malloc)使用的区域。
- 栈(stack):存储函数调用、局部变量等。
这些段都有自己的虚拟地址 ,但操作系统通过页表将它们映射到真实的物理地址中去。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel的段式管理中,逻辑地址由段选择子和段内偏移组成,CPU首先根据段选择子在全局描述符表(GDT)或局部描述符表(LDT)中查找对应的段描述符,提取出该段的基地址,然后将段基址与段内偏移相加,生成线性地址。这个过程完成了从逻辑地址到线性地址的转换,是x86保护模式下内存寻址的第一步。
7.3 Hello的线性地址到物理地址的变换-页式管理
当 hello 程序执行时,操作系统会为其分配一个独立的虚拟地址空间。程序运行过程中访问的地址首先通过段式管理转换为线性地址 ,接着该线性地址会通过页式管理 进一步转换为物理地址:CPU 根据当前进程的页目录表(CR3寄存器指向),将线性地址的高10位用于查页目录,中间10位用于查页表,得到对应页框的物理地址,再加上线性地址的低12位偏移,最终组合出实际的物理地址。这个转换过程是由硬件完成的,对用户程序完全透明,从而实现了内存隔离和虚拟内存管理。
7.4 TLB与四级页表支持下的VA到PA的变换

在现代 x86-64 架构(如运行 hello 程序的 Linux 系统)中,虚拟地址(VA)到物理地址(PA)的转换依赖于四级页表机制与TLB的协同工作。可以描述其过程:
当 hello 程序访问某个虚拟地址(VA)时,CPU 首先查询 TLB(快表),如果命中,直接获得该虚拟页对应的物理页框地址,快速完成虚拟地址到物理地址的转换;如果未命中,则启动页表遍历过程。x86-64 使用四级页表:页全局目录(PML4)、页上层目录(PDPT)、页中间目录(PD)、页表(PT)。CPU 将虚拟地址划分为五部分:每一级页表索引各占 9 位,最低的 12 位为页内偏移。通过这四级索引逐层找到页表项,最终定位物理页框,再加上偏移得到物理地址。这个结果会被缓存进 TLB,以加速后续访问。该机制支持虚拟内存、高效地址映射及内存保护。
7.5 三级Cache支持下的物理内存访问

在现代处理器中,如运行 hello 程序的系统,三级Cache(L1、L2、L3)用于加速对物理内存的访问。
当 hello 程序访问某个变量时,CPU 首先将虚拟地址转换为物理地址后,会按顺序在三级缓存中查找数据。L1 Cache 是速度最快、容量最小的一级缓存,分为指令缓存和数据缓存;若在 L1 命中,则直接返回数据,访问非常迅速。若 L1 未命中,则访问更大的 L2 Cache ;若 L2 也未命中,则查询共享的 L3 Cache(通常多个核心共享),再不命中时才访问主内存(DRAM)。这个过程由硬件自动完成,极大地提高了数据访问速度,降低了主存访问频率,从而提升程序整体执行效率。
7.6 hello进程fork时的内存映射

当 hello 程序调用 fork() 创建子进程时,操作系统不会立即复制父进程的全部内存内容 ,而是采用了一种高效的机制,叫做 写时复制(Copy-On-Write, COW)。
当 hello 进程通过 fork() 创建子进程时,子进程会继承父进程的整个虚拟地址空间结构,包括代码段、数据段、堆、栈等的内存映射关系,但父子进程最初共享相同的物理页面,并将这些共享页面的权限设为只读。此时父子进程在页表中指向相同的物理地址,避免了昂贵的内存复制操作。只有当父或子进程尝试写入共享页面时,操作系统才会触发页错误中断,复制该页面并将写权限赋给写进程,这就是"写时复制"机制。这样既保证了进程间的隔离,又显著提高了内存分配效率。
7.7 hello进程execve时的内存映射

当 hello 进程在执行 execve() 系统调用时,内核会彻底清空原有进程的虚拟地址空间 ,并重新加载新的可执行文件(如 ELF 格式)所需的内容,建立全新的内存映射关系。
当 hello 进程调用 execve() 执行新程序时,内核会销毁原进程地址空间的代码段、数据段、堆栈等所有映射,重新根据目标程序的可执行文件格式(如 ELF)建立新的内存映射。新的虚拟地址空间包括:代码段被映射为只读可执行、数据段映射为可读写、栈空间在高地址动态分配、堆空间根据实际运行动态扩展。此外,还会为动态链接库创建新的映射(如加载 libc.so),并将命令行参数、环境变量和栈初始化到新地址空间中。整个过程确保了执行新程序的地址空间干净、独立,与原程序无关。
7.8 缺页故障与缺页中断处理

在操作系统的虚拟内存管理中,缺页故障(Page Fault)是指进程访问了尚未映射到物理内存的虚拟地址,系统此时会触发一个异常,称为缺页中断,由内核来处理这一情况。
当 hello 程序访问一个尚未加载进内存的虚拟页面(例如,第一次访问堆空间、栈增长或按需加载的共享库页面),CPU 检测到该虚拟地址没有有效的物理映射,便触发缺页异常(#PF) ,进入内核态执行缺页中断处理程序。内核根据页表信息判断该访问是否合法(例如地址是否在进程合法地址空间内),若合法,则从磁盘(如 swap 区或可执行文件)中将对应页面加载到物理内存,为该页分配页框,并更新页表完成映射。之后恢复进程,重新执行触发缺页的指令。若访问非法(如访问空指针),则内核发送 SIGSEGV 信号终止程序。整个过程是现代操作系统实现按需分页和内存保护的关键机制。
7.9动态存储分配管理
动态存储分配管理是指在程序运行期间,操作系统或运行时库根据程序的实际需要,动态地为其分配和释放内存空间。这一机制广泛用于处理堆内存,如 C 语言中的 malloc/free,C++ 中的 new/delete,或 shell 程序执行中加载命令参数、环境变量、输入输出缓冲等。
以 hello 程序为例,当程序运行过程中调用如 malloc() 分配内存时,系统不会立刻向内核请求物理内存,而是先通过运行时库(如 glibc)在进程的堆区 维护一个空闲块链表(如空闲链表、空闲树、位图等算法结构 ),找到合适的内存块返回给程序。当内存不足时,运行时库会调用系统调用(如 brk() 或 mmap())向内核申请更多堆空间。释放内存时,运行时库会尝试将其合并入空闲块或缓存起来以供后续重用,而不是立即归还给操作系统。动态分配管理的核心目标是提高内存利用率 、减少碎片 、加快分配速度 ,同时保证程序的正确性与安全性。
7.10本章小结
本章在hello的具体例子中查看了程序执行的内存管理,查看了存储器从逻辑地址到线性地址到物理地址的变换。也了解了cache、动态存储分配管理等机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux 的 IO 设备管理采用了设备模型化为文件的设计思想,即把所有设备(硬盘、键盘、网络接口等)都抽象为文件,从而统一了设备访问接口。
设备模型化为文件 :在 Linux 中,设备被表示为特殊的文件,分为字符设备 和块设备。字符设备(如键盘、串口)按字符流方式访问,块设备(如磁盘)按块方式访问。设备文件通常位于 /dev 目录,通过文件系统提供统一的路径名访问。
设备管理------Unix I/O 接口:Linux 设备的读写操作使用 Unix 标准的文件操作接口,如 open(), read(), write(), close() 等系统调用。应用程序通过操作设备文件进行输入输出,而内核通过设备驱动程序实现具体硬件操作的封装,响应这些系统调用,将数据在设备和用户空间之间传递。
8.2 简述Unix IO接口及其函数
Unix IO接口是Unix及类Unix操作系统中用于文件和设备输入输出的标准接口,提供了一组系统调用函数,使程序能够对文件、设备进行统一的操作。主要函数包括:
- open():打开文件或设备,返回文件描述符。
- read():从文件描述符指向的位置读取数据到用户缓冲区。
- write():将用户缓冲区的数据写入文件描述符指向的位置。
- close():关闭文件描述符,释放资源。
- lseek():移动文件读写指针,实现随机访问。
- ioctl():设备控制接口,用于设备特定操作,如设置参数。
- select()/poll():多路复用函数,用于监控多个文件描述符的读写状态。
这些接口统一了对普通文件、设备文件、管道等的访问方式,使得程序对各种输入输出资源的操作具有一致性和灵活性。
8.3 printf的实现分析
Linux 字符显示的过程可以从用户层到硬件层这样理解:
用户程序首先调用类似 printf() 这样的函数,内部会通过 vsprintf 将格式化的字符串生成到缓冲区。然后调用 write() 系统调用将数据写入终端设备。
write() 是一个系统调用,通过陷阱指令(如 int 0x80 或 syscall)从用户态切换到内核态,内核根据文件描述符找到对应的字符设备驱动。
字符显示驱动程序将接收到的 ASCII 字符转换为点阵字模(字库),将字模对应的点信息写入显存(VRAM)。VRAM 中存储了屏幕上每个像素点的颜色数据(RGB分量)。
显示芯片以固定刷新频率逐行读取 VRAM 内容,通过信号线发送像素点的 RGB 信息给液晶显示器,液晶屏根据这些信号显示出对应的字符图像。整个过程实现了从软件文字信息到硬件图像显示的转换。
8.4 getchar的实现分析
getchar() 的实现涉及从硬件中断到系统调用的一整套流程:
当用户按下键盘时,键盘硬件产生键盘中断 ,CPU响应这个中断,进入内核态执行键盘中断处理程序 。该程序读取键盘控制器发送的扫描码,将扫描码转换成对应的 ASCII 码,并将字符存入内核维护的键盘缓冲区(一个环形缓冲区)。
用户空间的 getchar() 函数实际上会调用 read() 系统调用从标准输入(通常是键盘设备文件)读取数据。内核的 read 函数会从键盘缓冲区读取字符,若缓冲区为空,则进程被阻塞等待中断处理程序放入新数据。当用户按下回车键(换行符)时,read 调用才返回,getchar() 也随之返回读取到的字符。
这个过程体现了异步异常驱动的输入机制:键盘中断异步接收数据,缓冲区临时存储,系统调用同步读取,最终实现用户程序对键盘输入的阻塞式访问。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数。最后分析了printf、getchar两个标准化的IO函数。
结论
hello****程序经历的计算机系统过程总结
- 编译链接
源代码经过编译器翻译成汇编,再汇编成机器码,最后链接生成可执行文件(ELF格式),包括代码段、数据段、符号表和动态链接信息。 - 加载程序
操作系统加载器读取 ELF 文件,将代码段和只读数据映射到进程的虚拟地址空间,分配堆栈,建立程序的初始内存布局。 - 进程创建
由 Shell 通过 fork() 创建子进程,父子进程共享写时复制的内存映射,子进程调用 execve() 彻底加载 hello 程序映像,重建虚拟地址空间。 - 地址转换
程序运行时虚拟地址通过多级页表和 TLB 转换为物理地址,CPU缓存三级Cache加速内存访问。 - 执行指令
CPU根据指令流执行程序逻辑,涉及寄存器操作、算术运算和系统调用。 - 系统调用交互
例如 write() 输出字符串,触发陷阱指令切换到内核态,由内核完成设备驱动访问字符终端。 - 异常与中断处理
系统异步响应硬件中断(如键盘输入),软中断处理系统调用,实现用户态与内核态之间的切换。 - 进程调度
操作系统通过时间片轮转或优先级算法调度进程,分配CPU资源,切换进程上下文。 - 内存管理
运行时动态分配堆空间,堆栈扩展,缺页中断按需加载内存页,实现虚拟内存管理。 - 程序终止
hello 执行完毕调用 exit(),内核回收资源,更新进程状态,通知父进程。
对计算机系统设计与实现的深切感悟
- 现代计算机系统通过多层抽象(硬件、内核、库、应用)有效管理复杂性,使开发者能专注于高层逻辑,提升生产力。
- 设计中注重模块化、接口统一与硬件抽象,如Unix"一切皆文件"思想极大简化设备与文件操作。
- 系统的虚拟内存、进程调度、异步中断等机制,是软硬结合的经典范例,体现了效率与灵活性的平衡。
- 面向并发的设计挑战不断,如何更好支持多核、多线程和分布式系统,是未来的重点。
创新理念与设计思路
- 轻量级内核模块化:将传统庞大内核拆分成可动态加载的小型模块,按需扩展,提升系统安全和可维护性。
- 智能内存管理:结合机器学习动态调整内存分配策略,优化缓存命中率和减少碎片,提升程序性能。
- 异构计算资源统一调度:设计统一的调度框架,支持CPU、GPU、FPGA等多种计算单元,动态分配任务,实现高效异构计算。
- 软硬协同安全防护:通过硬件支持的安全模块与内核安全策略结合,实时监控与防御恶意攻击,保护用户数据和系统稳定。
- 用户态文件系统与零拷贝:减少内核态和用户态切换,提高文件IO效率,适应大数据与云计算场景。
附件
hello.c 源程序
hello.i 预处理后的修改的C程序
hello.s 汇编程序
hello.o 可重定位目标文件
hello 可执行目标文件
hello.d hello.o的反汇编结果
参考文献
1\] 兰德尔·E·布莱恩特等著;深入理解计算机系统\[M\]. 北京:机械工业出版社,2016.7. \[2\] Andrew S. Tanenbaum, Herbert Bos 《现代操作系统》 (Modern Operating Systems), 4th Edition, Pearson, 2014. \[3\] David A. Patterson, John L. Hennessy 《计算机体系结构:量化研究方法》 (Computer Architecture: A Quantitative Approach), 6th Edition, Morgan Kaufmann, 2017. \[4\] Abraham Silberschatz, Peter B. Galvin, Greg Gagne 《操作系统概念》 (Operating System Concepts), 9th Edition, Wiley, 2012. Science,1998,281:331-332\[1998-09-23\]. http://www.sciencemag.org/cgi/ collection/anatmorp.