HITICS大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   计算机与电子通信集群  

学     号      2023111579         

班     级        23L0502           

学       生         周钰彭        

指 导 教 师          刘宏伟          

计算机科学与技术学院

2024年5月

摘  要

本文以“Hello World”程序为例,系统分析了从源代码到进程终止的完整生命周期(P2P: Program to Process),深入探讨了计算机系统核心机制在程序运行中的作用。通过预处理、编译、汇编、链接等阶段,揭示了高级语言如何转换为可执行目标文件;结合进程管理、存储映射、动态链接和异常处理,阐述了操作系统如何协调硬件资源执行程序。实验基于Ubuntu环境,使用GCC、EDB等工具逐步解析了ELF文件格式、虚拟地址空间、重定位过程及信号处理机制,完整呈现了程序在计算机系统中的运行原理。研究表明,现代计算机系统通过多层级抽象(如虚拟内存、进程隔离)和硬件-软件协同设计(如TLB、Cache),实现了高效、安全的程序执行环境,为理解复杂软件系统奠定了基础。

关键词:预处理,编译,汇编,链接,进程管理,虚拟地址,存储管理                           

目  录

第1章 概述……………………………………………………………………….. – 4 –

1.1 Hello简介…………………………………………………………………. – 4 –

1.2 环境与工具………………………………………………………………… – 4 –

1.3 中间结果……………………………………………………………………. – 4 –

1.4 本章小结……………………………………………………………………. – 4 –

第2章 预处理……………………………………………………………………. – 5 –

2.1 预处理的概念与作用………………………………………………….. – 5 –

2.2在Ubuntu下预处理的命令………………………………………… – 5 –

2.3 Hello的预处理结果解析……………………………………………. – 5 –

2.4 本章小结……………………………………………………………………. – 5 –

第3章 编译……………………………………………………………………….. – 6 –

3.1 编译的概念与作用……………………………………………………… – 6 –

3.2 在Ubuntu下编译的命令……………………………………………. – 6 –

3.3 Hello的编译结果解析……………………………………………….. – 6 –

3.4 本章小结……………………………………………………………………. – 6 –

第4章 汇编……………………………………………………………………….. – 7 –

4.1 汇编的概念与作用……………………………………………………… – 7 –

4.2 在Ubuntu下汇编的命令……………………………………………. – 7 –

4.3 可重定位目标elf格式……………………………………………….. – 7 –

4.4 Hello.o的结果解析……………………………………………………. – 7 –

4.5 本章小结……………………………………………………………………. – 7 –

第5章 链接……………………………………………………………………….. – 8 –

5.1 链接的概念与作用……………………………………………………… – 8 –

5.2 在Ubuntu下链接的命令……………………………………………. – 8 –

5.3 可执行目标文件hello的格式……………………………………. – 8 –

5.4 hello的虚拟地址空间………………………………………………… – 8 –

5.5 链接的重定位过程分析………………………………………………. – 8 –

5.6 hello的执行流程……………………………………………………….. – 8 –

5.7 Hello的动态链接分析……………………………………………….. – 8 –

5.8 本章小结……………………………………………………………………. – 9 –

第6章 hello进程管理…………………………………………………. – 10 –

6.1 进程的概念与作用……………………………………………………. – 10 –

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

6.3 Hello的fork进程创建过程…………………………………….. – 10 –

6.4 Hello的execve过程……………………………………………….. – 10 –

6.5 Hello的进程执行…………………………………………………….. – 10 –

6.6 hello的异常与信号处理…………………………………………… – 10 –

6.7本章小结…………………………………………………………………… – 10 –

第7章 hello的存储管理……………………………………………… – 11 –

7.1 hello的存储器地址空间…………………………………………… – 11 –

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

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

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

7.5 三级Cache支持下的物理内存访问………………………….. – 11 –

7.6 hello进程fork时的内存映射………………………………….. – 11 –

7.7 hello进程execve时的内存映射………………………………. – 11 –

7.8 缺页故障与缺页中断处理…………………………………………. – 11 –

7.9动态存储分配管理…………………………………………………….. – 11 –

7.10本章小结…………………………………………………………………. – 12 –

第8章 hello的IO管理………………………………………………. – 13 –

8.1 Linux的IO设备管理方法…………………………………………. – 13 –

8.2 简述Unix IO接口及其函数………………………………………. – 13 –

8.3 printf的实现分析…………………………………………………….. – 13 –

8.4 getchar的实现分析…………………………………………………. – 13 –

8.5本章小结…………………………………………………………………… – 13 –

结论………………………………………………………………………………….. – 14 –

附件………………………………………………………………………………….. – 15 –

参考文献…………………………………………………………………………… – 16 –

第1章 概述

1.1 Hello简介

从源程序代码到成为一个进程“跑”起来,一个Hello程序到底经历了什么呢?这也就是我们口中Hello程序的P2P历程(Program to Process)。一个程序的P2P历程到底是怎么样的呢:用户编写hello.c后,经预处理、编译、汇编、链接生成可执行文件;Shell通过fork创建子进程,execve加载Hello到内存,OS为其分配虚拟地址空间并映射物理页,CPU通过流水线执行指令,MMU借助TLB和四级页表完成地址转换,Cache加速数据访问。运行结束后,OS回收资源,进程从零诞生最终归零即zero to zero,也就是完成了我们口中的020生命周期。整个过程涉及程序翻译、进程管理、存储层次、异常控制等计算机系统核心机制,短暂却完整展现了CS的协同运作。

1.2 环境与工具

硬件环境:

处理器:13th Gen Intel(R) Core(TM)i7-13700H 

系统类型:64位操作系统,基于x86-64的处理器

软件环境:Windows11 64位 ,泰山服务器 Ubuntu 20.04.6 LTS,VMware

开发与调试工具:gdb,edb,objdump,gcc,readelf等开发工具

1.3 中间结果

文件名称

作用

hello.c

储存hello程序源代码

hello.i

源代码经过预处理产生的文件(包含头文件等工作)

hello.s

hello程序对应的汇编语言文件

hello.o

可重定位目标文件

hello.out

二进制可执行文件

1.4 本章小结

本章简述了Hello程序的一生,概括了从P2P到020的整个过程,可以发现整个计算机系统的学习和Hello的生命轨迹基本重合一致。本章还简要说明了实验的软、硬件环境以及编译处理工具,是整体文章的布局脉络。  

第2章 预处理

2.1 预处理的概念与作用

2.1.1. 预处理的概念

预处理(Preprocessing)是C/C++程序编译的第一个阶段,由预处理器(Preprocessor)对源代码进行文本级处理,生成经过修改的源代码(.i 文件),供后续编译阶段使用。

2.1.2. 预处理的作用

预处理的主要任务包括:

宏展开:处理 #define 定义的宏,进行文本替换。例如:#define PI 3.14 → 所有 PI 替换为 3.14。

头文件包含:处理 #include 指令,将指定的头文件内容插入到当前文件。例如:#include <stdio.h> → 插入 stdio.h 的内容。

条件编译:根据 #if、#ifdef、#ifndef 等指令决定是否编译某段代码。例如:

图1 条件编译举例

删除注释:移除所有 // 和 /* … */ 注释,减少编译负担。

特殊指令处理:如 #pragma(编译器特定指令)、#error(强制报错)、#line(修改行号信息)等。

2.2在Ubuntu下预处理的命令

图2 在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

图3 .c与.i文件大小对比

我们可以看到,对Hello.c文件进行与处理后生成了Hello.i文件,是一个可以打开的文本文档,相对与Hello.c文件,.i文件内容多了很多,.c文件只有1KB而.i文件有63KB大小,分析.i文件的内容可以看到预处理后生成的 `.i` 文件与原始的 `.c` 文件主要有以下差异: 

1. 头文件展开:`#include` 指令被替换为对应头文件的所有内容,导致 `.i` 文件显著增大; 

2. 宏替换:所有 `#define` 定义的宏被直接替换为实际值或代码; 

3. 注释删除:源代码中的 `//` 和 `/*…*/` 注释被完全移除; 

4. 条件编译生效:`#ifdef`、`#if` 等条件编译指令被解析,仅保留符合条件的代码块; 

5. 行标记插入:预处理器添加 `# <行号> <文件名>` 的标记,便于编译器定位原始代码位置。 

最终,`.i` 文件是纯粹的展开后文本,不含预处理指令,可直接交给编译器进行后续编译阶段。

2.4 本章小结

本章是关于对源程序文件进行预处理方面内容的探索,对包括预处理的概念和作用,预处理使用的命令以及预处理的结果和预处理后生成的文件在内的内容进行了学习和探究,预处理作为编译运行的第一步,对后续进行编译有重要作用。

第3章 编译

3.1 编译的概念与作用

3.1.1. 编译的概念

该阶段指编译器将预处理后的纯C/C++代码(已展开宏、包含头文件等)通过词法分析、语法分析、语义分析、中间代码生成和优化后,最终生成目标平台相关的汇编代码的过程,是编译器实现”高级语言→机器可执行代码”转换的关键步骤。

3.1.2. 编译的作用

1.平台适配桥梁:将高级语言转换为底层汇编,使代码能与特定CPU指令集对接。

2.优化执行效率:在汇编层面实现寄存器分配、指令选择等优化。例如将循环展开(Loop Unrolling)减少分支判断开销。

3.调试与分析支持:生成带调试信息的汇编代码(如GCC的-g选项),便于结合源码排查问题。

4.安全与兼容性控制:通过编译选项(如-fPIC)生成位置无关代码,适应动态链接需求;通过插入栈保护指令(如-fstack-protector)增强安全性。       

3.2 在Ubuntu下编译的命令

图4 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1 数据类型处理

                                                        图5 Hello编译结果截图

1. 常量(Literal Values)

字符串常量:存储在.rodata段(只读数据段),如.LC0和.LC1。

编译器将中文字符编码为UTF-8的八进制转义序列。

2. 变量(Variables)

局部变量:存储在栈中(通过sp寄存器偏移访问)。

未显式初始化的变量如wzr(零寄存器)用于清零:

3.3.2 操作符处理

1. 赋值操作

简单赋值

复合赋值:

2. 算术/逻辑操作

加法:

比较:

3. 关系操作

条件分支:

表示如果小于等于9时,将跳转到循环体。

3.3.3 复合数据结构操作

1. 指针操作

指针解引用:

//加载argv[1](x0为argv+8地址)

//argv + 16(等价于argv[2])

指针算术:

2. 数组访问

//加载argv地址

命令行参数(argv):argv为指针数组,通过偏移量访问:

//解引用

//argv + 24(argv[3])

3.3.4 控制转移

1. 条件分支(if-else)

argc检查:

2. 循环(for)

循环结构:

循环结束;

循环体代码;

3.3.5 函数操作

1. 参数传递

寄存器传参:x0~x7用于传递参数(如printf):

栈传参:超过寄存器数量的参数通过栈传递。

2. 返回值

整数返回值:

3. 函数调用

标准库调用:从标准库中调用puts,atoi转换字符串为整数,sleep

3.3.6 其他关键操作

1. 栈管理

栈帧分配:分配48字节栈空间

栈帧释放:恢复栈指针

2. 系统调用

程序退出:

3.3.7总结表

C语言构造

编译器处理(ARM64汇编)

关键指令示例

字符串常量

存储在.rodata段

.string “…”

局部变量

栈偏移访问

str w0, [sp, 28]

指针算术

基地址 + 偏移量

add x0, x0, 8

循环控制

cmp + 条件跳转

ble .L4

函数调用

bl指令跳转

bl printf

返回值

通过x0/w0传递

mov w0, 0

3.4 本章小结

在对源程序文件进行预处理后,通过编译工作将预处理生成的.i文本文件编译生成.s汇编语言程序文件,通过对汇编语言程序文件(.s文件)内容的分析可知,编译器将C语言的高级抽象(如循环、函数调用)转换为底层的寄存器操作、内存访问和条件跳转,同时严格遵循ARM64的调用约定和指令集规范。

第4章 汇编

4.1 汇编的概念与作用

4.1.1. 汇编的概念

汇编是将汇编语言(`.s`文件)转换为机器语言目标文件(`.o`)的过程,由汇编器(如`as`)完成。

4.1.2. 汇编的作用

汇编的核心作用包括: 

1. 指令转换:将人类可读的汇编指令编码为CPU可执行的二进制机器码; 

2. 符号管理:解析标签和外部符号,生成符号表供链接器使用; 

3. 生成重定位信息:标记需链接阶段修正的地址引用(如函数调用)。 

最终输出的目标文件(`.o`)包含机器指令、数据段及元数据(如节头表、重定位条目),为后续链接阶段生成可执行程序奠定基础。这一过程是编译流程中衔接高级语言与机器码的关键环节,直接决定程序能否在特定硬件(如ARM64)上正确运行。

4.2 在Ubuntu下汇编的命令

图6 Ubuntu下汇编的命令

4.3 可重定位目标elf格式   

4.3.1 ELF

我通过 readelf 分析 hello.o 的 ELF 格式,可以看到这是一个 ARM64 架构的可重定位目标文件(Type: REL),采用 ELF64 格式。ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了生成文件的系统的字的大小和字节顺序。

下面分析重定位项目:对 printf、puts 等外部函数的调用指令在 .rela.text 中被标记为 R_AARCH64_CALL26 类型,表示需链接器填充函数实际地址。字符串常量(.LC0/.LC1)的地址引用需重定位为最终内存位置。无程序头表数量为0(Number of program headers: 0):因尚未链接,不包含可执行段信息。

图7 .o.文件的ELF头

4.3.2 Section

在泰山服务器上使用readelf -S hello.o查看Section头得到结果如下:

图8 .o文件的Section头

关键信息如下:

.text:包含 main 函数的机器指令。

.rodata:存储只读字符串常量。

.symtab:符号表,记录全局函数、局部只读数据等符号信息。

.rela.text:重定位节,标记需链接阶段修正的地址引用。

.data: 已初始化的全局和静态C变量

.bss:   未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量

.rel.text :一个.tex节中位置的列表

.rel.data :被模块引用或定义的所有全局变量的重定位信息

.debug:一个调试符号表

.line:  原始C源程序中的行号和.text节中机器指令之间的映射

.strtab:一个字符串表(包括.symtab和.debug节中的符号表)

4.3.3 符号表

使用readelf -s hello.o查看符号表,结果如下

图9 .o.文件的符号表

Num为符号的编号,Name是符号的名称。Size表示是一个位于.text节中偏移量为0处的146字节函数。Bind表示符号是本地的还是全局的。

4.3.4 可重定位段信息

使用readelf -r hello.o查看可重定位段信息,结果如下:

图10 .o文件的可重定位段信息

Offset是需要被重定位的指令或数据在目标文件 .text 节中的偏移地址;Info是重定位信息;Type(重定位类型)指定如何计算重定位后的值; Sym. Value(符号值)是符号在它所在节中的偏移地址(通常是符号定义的地址);Sym. Name(符号名)需要重定位的目标符号名称; Addend(加数)是一个常数偏移值,用于调整重定位后的最终地址。

4.4 Hello.o的结果解析

我通过objdump -d -r hello.o命令对hello.o进行反汇编得到结果如下:

图11 hello.o反汇编结果

机器语言是CPU直接执行的二进制指令,由操作码和操作数构成。操作码指定操作类型(如加法、跳转),操作数则提供操作对象(如寄存器编号、内存地址或立即数)。指令格式和长度因架构而异(如ARM64的固定长度4字节,x86的变长指令)。分支和函数调用指令的操作数通常是相对偏移量,由链接器在重定位时填充实际地址。

汇编语言是机器语言的文本表示,通过助记符(如MOV、CALL)和符号(如标签、寄存器名)提高可读性。但汇编指令的操作数与机器码可能不一致:

分支/调用指令中,汇编中直接使用标签,而机器码需计算相对偏移。

内存操作中,汇编中可用符号地址,而机器码需转换为PC相对地址或绝对地址。

这种差异由汇编器和链接器处理,将符号化操作数转换为二进制可执行代码。

4.5 本章小结

本章学习了实现从hello.o到hello.s的汇编过程,通过readelf等系列指令查看了可重定位目标ELF格式中ELF头、Section头、符号表和可重定位段信息,并查看了hello.o的反汇编结果,并对结果进行分析,对汇编的概念和作用有了更直观深刻的理解。

第5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是将多个目标文件(如`hello.o`)和库文件合并生成最终可执行程序(如`hello`)的过程,由链接器(如`ld`)完成。其核心任务是解析符号引用(如未定义的`printf`函数)、合并代码与数据段(如将多个`.text`节合并),并处理重定位信息(如修正`bl`指令中的跳转地址)。链接分为静态链接(直接嵌入库代码)和动态链接(运行时加载共享库),确保程序中所有外部依赖被正确绑定。

5.1.2 链接的作用

在`hello.o`到`hello`的生成过程中,链接器具体完成以下工作: 

地址分配:为合并后的代码段(`.text`)、数据段(`.data`等)分配最终内存地址。 
符号解析:将目标文件中的未定义符号与库中的实现关联。 
重定位修正:根据实际地址调整指令中的偏移量。

最终生成符合操作系统格式(如ELF)的可执行文件,包含可直接加载运行的机器码和运行时信息。

5.2 在Ubuntu下链接的命令

这里我直接使用ld静态链接,在链接之前需要先手动查找必要的系统库和使用文件(否则会报错),这里我使用-print-file-name=命令在泰山服务器中查找必要的系统库和文件。

在完成查找后通过ld命令手动链接所有组件,过程截图如下:

图12 Ubuntu下链接的命令

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

可执行目标文件(.out)的格式类似于可重定位目标文件(.o)的格式,但稍有不同。 与查看hello.o的elf文件相似,我同样使用readelf查看hello文件的ELF头,节头部表,符号表。

5.3.1 ELF

在VMware虚拟机上使用readelf -h hello.out获取ELF头的内容,得到结果如下:

图13 .out文件的ELF头

ELF头中字段e_entry给出执行程序时的第一条指令的地址,而在可重定位文件中,此字段为0,并且可以看到文件的Type变成了Executable file(可执行文件)。

5.3.2 Section

使用readelf -S hello.out可以获取可执行文件的Section头部分内容,得到结果如下:

图14 .out文件的Section头

节头部表对hello中所有信息进行了声明,包括了大小、偏移量、起始地址以及数据对齐方式等信息。根据始地址和大小,我们就可以计算节头部表中的每个节所在的区域。

5.3.3 符号表

使用readelf -s hello.out可以查看符号表的内容,得到结果如下:

图15 .out文件的符号表

可以发现经过链接之后符号表的符号数量陡增,说明经过连接之后引入了许多其他库函数的符号,一并加入到了符号表中。

5.3.4 可重定位段信息

同第四章,使用readelf -r hello.out可以查看符号表的内容,得到结果如下:

图16 .out文件的可重定位段信息

5.4 hello的虚拟地址空间

我们在虚拟机终端中输入edb –run hello,在File-Open中选择hello文件,点击open,界面显示如下:

图17  edb显示界面

从EDB程序中可以找到,hello的起始地址为00007ffe:b2cfa5f8,显示界面如下:

图18  hello地址显示

5.5 链接的重定位过程分析

在虚拟机上使用objdump -d -r hello.out命令查看hello.out可执行文件的反汇编条目,得到下面的结果:

图19 hello.out文件的反汇编代码

可以看到,hello的反汇编代码与hello.o的返汇编代码在结构和语法上是基本相同的,只不过hello的反汇编代码多了非常多的内容,我们通过比较不同来看一下区别:

虚拟地址不同:hello.o的反汇编代码虚拟地址从0开始,而hello的反汇编代码虚拟地址从0x001000开始。这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。

图20 hello.out文件的反汇编代码

反汇编节数不同:hello.o只有.text节,里面只有main函数的反汇编代码。而hello在main函数之前加上了链接过程中重定位而加入的各种在hello中被调用的函数、数据,增加了.init,.plt,.plt.sec等节的反汇编代码。

下面我们对hello的重定位进行分析。我们可以将重定位分为两大步:第一步是重定位节和符号定义,在这一步中,连接器将所有相同类型的节合并成为同一类型的新的聚合节。例如,来自所有输入模块的.data节全部被合并成一个节,这个节成为输出的可执行目标文件的.data节。第二步就是重定位节中的符号引用,在这一步中,连接器依赖于可重定位条目及得到的数据,修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。

5.6 hello的执行流程

1.使用edb执行hello后,可以看到最初的程序地址会在0x7dd0:4fca0290处,这里是hello使用的动态链接库ld-2.2.27.so的入口点_dl_start:

2.然后,程序跳转到_dl_init,在经过了一系列初始化后,跳到hello的程序入口点_start;

3.然后程序通过call指令跳到动态链接库ld-2.27.so的_libc_start_main 处,这个函数会进行一些必要的初始化,并负责调用main函数;

4.下一步,程序调用动态链接库中的__cxa_atexit函数,它会设置在程序结束时需要调用的函数表;

5.然后会返回到__libc_start_main继续运行,然后调用hello可执行文件中的__libc_csu_init函数,这函数是由静态库引入的,也是做一些初始化的工作;

6.然后程序返回到__libc_start_main继续,紧接着程序调用动态链接库里的_setjmp函数,设置一些非本地跳转;

7.然后返回到__libc_start_main继续,正式开始调用main函数;

8.由于我们在edb运行hello的时候并未给出额外的命令行参数,因此它会在第一个if处通过exit(1)直接结束程序;

9.通过hello本身携带的exit函数,程序会跳转;

最后在完成若干操作后,程序退出。

5.7 Hello的动态链接分析

我们从实际目的出发,动态共享库是现代操作系统为解决静态库固有缺陷而发展出的创新解决方案。与静态库不同,共享库作为独立的目标模块,可以在程序运行时被动态加载到任意内存地址,并与主程序进行链接,这一过程称为动态链接。把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

.plt(过程链接表):PLT 是一个代码段数组,其中每个条目为 16 字节的可执行指令,其主要功能是实现延迟绑定机制:PLT[0] 是特殊条目,负责跳转至动态链接器;其余每个条目(如 PLT[1]、PLT[2])对应一个需动态解析的库函数(如 printf),在首次调用时触发地址解析。

.got(全局偏移表):GOT 是一个数据段数组,每个条目存储 8 字节地址,与 PLT 协同工作:GOT[0] 和 GOT[1] 保存动态链接器解析函数地址所需的元数据;GOT[2] 指向动态链接器(如 ld-linux.so)的入口点;其余条目(如 GOT[3])存储外部函数的最终地址,首次调用后由动态链接器填充。

hello在动态连接器加载前后的重定位是不一样的,在加载之后才进行重定位,我们在EDB中查询到重定位后的_init显示结果如下:

图21 edb中查询到重定位后的_init

5.8 本章小结

这章内容带着我们完整走过了程序链接的奇妙旅程:从理解链接的基本概念和在Ubuntu下的实际操作指令开始,到深入分析hello可执行文件的ELF格式结构;通过edb调试工具,我不仅观察了程序在虚拟地址空间中的内存布局和各节内容,还追踪了重定位条目的具体处理过程,逐步理清了程序中各个子函数的执行路径;最让我着迷的是借助edb对虚拟内存的探查,让我直观看到了动态链接的整个过程 – 原来hello.o就是这样和它依赖的各种库完美融合,最终形成一个所有运行时地址都已确定的完整可执行文件,随时准备好被加载到内存中运行。这段探索让我真正理解了从源代码到可执行程序的魔法转变,每个步骤都环环相扣,整个过程既精妙又严谨~~

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程是操作系统资源分配和调度的基本单位,指一个正在执行的程序实例。每个进程拥有独立的内存空间(代码段、数据段、堆栈等)和系统资源(如文件句柄、CPU时间片),操作系统通过进程控制块(PCB)记录其运行状态、优先级等信息。进程的创建通常通过“fork-exec”机制实现,使得程序能够以隔离环境运行,避免相互干扰,体现操作系统的多任务处理能力。

6.1.2 进程的概念

进程的核心作用在于实现并发执行与资源管理。一方面,通过时间片轮转、优先级调度等机制,操作系统允许多个进程“同时”运行(伪并行),提升CPU利用率及用户体验(如边下载文件边编辑文档)。另一方面,进程隔离性保障了系统稳定性:单一进程崩溃不会波及其他进程或内核,结合权限控制可防御恶意程序。此外,进程间通信(IPC)机制(如管道、共享内存)支持数据交换,为复杂分布式计算提供基础支撑。

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

6.2.1  Shell(Bash)的作用

Shell是用户与操作系统内核之间的桥梁,主要作用为解释并执行用户输入的命令或脚本。作为命令行接口,Bash(Bourne Again Shell)支持交互式操作(如终端输入)和批处理模式(如执行脚本),提供环境变量管理、文件操作、进程控制等功能。它还允许定制化工作流,通过管道(`|`)、重定向(`>`/`>>`)、通配符(`*`/`?`)等语法,将多个命令组合成复杂任务。此外,Bash内置脚本编程能力,支持条件判断、循环、函数定义,实现自动化运维与高效系统管理。

6.2.2  Shell(Bash)的处理流程

当用户在终端输入命令后,Bash首先解析输入内容,按空格分割参数并处理特殊符号(如变量替换`$VAR`)。接着,检查命令类型:若是内置命令(如`cd`),直接由Shell自身执行;否则,通过`PATH`环境变量搜索可执行文件,启动子进程运行外部程序。执行过程中,Bash会处理I/O重定向(如将输出写入文件),管理前后台进程(`&`/`jobs`),并在命令结束后返回状态码(`$?`)。对于脚本文件,Bash逐行解释执行,支持流程控制(如`if`/`for`)和错误处理,最终将结果输出到终端或指定目标。

6.3 Hello的fork进程创建过程

当程序调用`fork()`系统调用时,操作系统会复制当前进程(父进程)的代码段、数据段和堆栈,生成一个几乎完全相同的子进程。`fork()`在父进程中返回子进程的PID(正数),在子进程中返回0,通过此返回值区分父子进程的执行分支。例如,若子进程分支(`pid == 0`)中调用`printf(“Hello”)`,则子进程将输出“Hello”,而父进程可继续执行其他逻辑(如等待子进程结束)。 

父进程和子进程从`fork()`之后并行执行,但具体运行顺序由内核调度决定。例如:

1. 父进程可能先打印“Parent process starts”;

2. 子进程随后执行并输出“Hello”;

3. 父进程通过`wait()`等待子进程结束。 

由于进程独立性,两者内存空间隔离,子进程对变量的修改不会影响父进程。若未同步,父子进程的输出可能交错(如同时操作终端),需通过信号或管道协调。

6.4 Hello的execve过程

当进程调用`execve(“./Hello”, argv, envp)`时,操作系统首先验证`Hello`可执行文件的权限及格式,随后加载其代码段、数据段至当前进程的内存空间,替换原有程序内容(但保留进程PID、文件描述符等属性)。命令行参数(`argv`)和环境变量(`envp`)被复制到新程序的栈中,进程从`Hello`的`main`函数开始执行,原进程的后续代码(`execve`之后的指令)仅在调用失败时恢复执行。例如,原程序若在`execve`后调用`printf`,该代码将被`Hello`的程序逻辑完全覆盖。

6.5 Hello的进程执行

6.5.1 逻辑控制流与并发流

每个进程的执行表现为一个独立的逻辑控制流——即程序计数器(PC)按指令序列顺序或跳转执行的轨迹。操作系统通过时间片轮转实现多进程的并发执行:将CPU时间划分为小片段(如10ms),依次分配给就绪队列中的进程。例如,进程A执行完一个时间片后,CPU被强制切换到进程B,宏观上形成“并行”假象(并发流)。这种机制依赖硬件定时器中断触发调度器,确保公平性。

图22 “进程的并发”示意图

6.5.2 用户态与内核态的切换

进程通常在用户态执行应用程序代码(如`hello`的`printf`调用),当需要操作系统服务(如文件读写)时,通过系统调用主动进入内核态。此时CPU权限提升,内核代为完成操作后返回结果。例如,`sleep()`会触发调度器挂起当前进程,切换到其他就绪进程,期间涉及多次用户态-内核态切换。

6.5.3 上下文切换的细节

当时间片耗尽或进程阻塞(如等待I/O),内核启动上下文切换: 

保存现场:将当前进程的寄存器状态(PC、SP等)、页表指针存入其PCB; 

恢复现场:从目标进程的PCB加载上下文到寄存器,切换地址空间(CR3寄存器); 

切换堆栈:内核栈替换为用户栈,确保执行流无缝衔接。 

例如,`hello`调用`getchar()`等待输入时,内核会立即切换到其他进程,输入到达后再恢复`hello`的执行。 

图23 “进程的切换”示意图

6.6 hello的异常与信号处理

6.6.1异常类型

异步异常(中断):由处理器外部I/O设备引起的异常,包括时钟中断、外部设备的I/O中断
同步异常:包括陷阱、故障和终止。陷阱是有意的,程序执行的结果,如系统调用;故障不是有意的,但有可能被修复,如缺页故障(可恢复),保护故障(不可恢复),浮点异常;终止是由非故意的不可恢复的致命错误造成,包括非法指令,奇偶校验错误,机器检查到致命的硬件错误
被零除、算术运算溢出、缺页、 I/O请求完成、键盘输入等

6.6.2. 产生的信号

图24 Linux信号表

SIGINT:当用户按下Ctrl+C时发送,通常用于中断程序。

SIGTSTP:当用户按下Ctrl+Z时发送,用于暂停程序。

SIGTERM:请求程序终止的正常信号。

6.6.3具体信号处理与命令

我在hello.out文件的运行过程中从键盘输入不同的信号,得到如下结果:

1.按Ctrl-Z

图25

2.按Ctrl-C

图26

3.乱输入/不停按回车

图27

6.7本章小结

本章概述了hello进程大致的执行过程,阐述了进程、shell、fork、execve等相关概念,之后从逻辑控制流、时间分片、用户模式/内核模式、上下文切换等角度详细分析了进程的执行过程。并在运行时尝试了不同形式的命令和异常,每种信号都有不同处理机制,针对不同的shell命令,hello会产生不同响应。

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1. 逻辑地址

逻辑地址是程序直接使用的内存地址,由段选择符和偏移量组成,通常表现为汇编代码中的符号地址。在x86架构中,CPU的段式管理单元通过段描述符表(GDT/LDT)将逻辑地址转换为线性地址。

7.1.2. 线性地址

线性地址是逻辑地址经过段式转换后的中间地址形式,表现为连续的32位或64位地址空间。在平坦内存模型下,操作系统通过将段基址设为0来禁用段机制,逻辑地址直接成为线性地址。

7.1.3. 虚拟地址

虚拟地址是用户进程视角的分页式地址,与线性地址在分页机制下通常视为同一概念。操作系统通过页表(Page Table)将虚拟地址映射到物理地址,实现内存隔离与动态分配。

7.1.4. 物理地址

物理地址是计算机物理内存(RAM)的实际存储单元地址,由内存控制器直接操作。MMU(内存管理单元)通过页表将虚拟地址(或线性地址)转换为物理地址。

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

在Intel架构的段式管理机制中,逻辑地址通过段选择符和偏移量的组合转换为线性地址:

1.段选择符解析:段选择符的高13位作为索引,通过TI位选择全局描述符表或局部描述符表,从中获取对应的段描述符(包含段基址、段限长、权限等);

2.地址计算:将段描述符中记录的段基址(32/64位)与逻辑地址的偏移量相加,生成线性地址;

3.权限校验:检查偏移量是否超出段限长,以及操作是否符合段类型(如代码段不可写),若非法则触发通用保护异常。

图28  Intel X86架构存储器寻址示意图

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

分页机制中使用一个页表来记录这些关系,页表也是存储在内存中的,是由操作系统维护的。

每个进程都有一个页表,页表中的每一项,即PTE(页表条目),记录着该对应的虚拟地址空间的那一页是否有效(即是否有对应的物理内存上的页),物理页的起始位置或磁盘地址,访问权限等信息。PTE根据不同的映射状态也被划分为三种状态:未分配、未缓存、已缓存。

未分配:虚拟内存中未分配的页

未缓存:已经分配但是还没有被缓存到物理内存中的页

已缓存:分配后缓存到物理页块中的页

图29 虚拟页面分配示意图

7.4 TLB与四级页表支持下的VA到PA的变换
   当CPU访问虚拟地址(VA)时,首先查询TLB(快表):若命中(缓存中存在VA对应的物理页框号),则直接合成物理地址(PA);若未命中,则启动四级页表遍历:

地址分解:将64位VA分为四级索引(PML4、PDPT、PD、PT,各占9位)和页内偏移(12位)。

逐级查表:

1.从CR3寄存器获取PML4基地址,结合PML4索引找到PDPT基地址;

2.依次查询PDPT→PD→PT,最终获取PTE中的物理页框号(如0x1F3A);

地址合成:将页框号左移12位与偏移相加,得到PA ;

TLB更新:将本次VA→PA映射缓存至TLB,加速后续访问;

异常处理:若某级页表项标记为“不存在”或权限不符(如写只读页),触发缺页异常或段错误,由内核处理(如分配物理页或终止进程)。

图30 页命中/不命中流程示意图

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

当CPU访问物理地址(PA)时,数据访问优先通过三级缓存(L1→L2→L3)逐级检索: 

1. L1 Cache查询:根据PA的索引(Index)和标签(Tag)匹配缓存行(Cache Line),若命中(Tag匹配且有效),直接返回数据; 

2. L2 Cache查询:若L1未命中,PA发送至L2 Cache,检查标签与状态位(如MESI协议),命中则返回数据并填充L1; 

3. L3 Cache查询:L2未命中时访问共享的L3 Cache,命中后数据逐级回填至L2和L1; 

4. 访问主存:若三级缓存均未命中,通过内存控制器(IMC)访问物理内存(DRAM),数据读取后填充L3→L2→L1,并更新缓存行的标签与状态; 

5. 写策略:写操作根据缓存策略(写回/写直达)更新缓存行,脏数据回写主存时机由替换算法(如LRU)决定。     

我们通过分析可以知到提升程序执行效率的关键点在于利用三级设计平衡速度与容量,缓存通过空间局部性和时间局部性减少访存延迟。

图31 三级缓存与主存层次结构示意图

7.6 hello进程fork时的内存映射

我们知道,程序利用fork()函数创建子进程,当hello进程调用fork()创建子进程时,内核通过写时复制技术延迟物理内存的实际复制:

1.页表复制:子进程复制父进程的页表结构,所有页表项标记为只读,并指向相同的物理页框;

2.COW触发:若父子进程任一试图修改共享页(如修改变量),触发缺页异常,内核分配新物理页,复制原页内容,更新修改进程的页表项为可写,并解除共享;

3.内存隔离:未修改的页(如代码段)保持共享,修改后的页独立映射,实现高效内存利用与快速进程创建。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。

3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

4.设置程序计数器(PC)。execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

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

7.8.1. 缺页故障

缺页故障是当进程访问虚拟内存中一个有效但未加载到物理内存的页时,由MMU(内存管理单元)触发的同步异常。该故障表明目标页可能位于磁盘交换区(Swap)或文件映射区,需由操作系统介入处理。

图32 “缺页异常处理”流程图

7.8.2. 缺页中断处理

处理缺页中断时,内核首先检查访问合法性:若地址无效(如越界),则终止进程;若有效,则从磁盘读取缺失页到物理内存,更新页表并重试故障指令。

7.9动态存储分配管理

首先明确动态存储分配管理的概念:在程序运行时程序员使用动态内存分配器,例如malloc获得虚拟内存。分配器将堆视为一组不同大小的 块(blocks)的集合来维护每个块要么是已分配的,要么是空闲的。

7.9.1

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。

在内存中的碎片和垃圾被回收之后,内存中就会有空余的空间被闲置出来。这些空间有时会比较小,但是积少成多,操作系统不知道怎么利用这些空间,就会造成很多的浪费。为了记录这些空闲块,采用隐式空闲链表和显式空闲链表的方法实现这一操作。

7.9.2隐式空闲链表

首先了解几个概念:

首次适配 (First fit): 从头开始搜索空闲链表,选择第一个合适的空闲块: 搜索时间与总块数(包括已分配和空闲块)成线性关系。在靠近链表起始处留下小空闲块的“碎片”。

下一次适配 (Next fit): 和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快: 避免重复扫描那些无用块。一些研究表明,下一次适配的内存利用率要比首次适配低得多。

最佳适配 (Best fit): 查询链表,选择一个最好的空闲块;适配,剩余最少空闲空间。保证碎片最小——提高内存利用率,通常运行速度会慢于首次适配。

在隐式空闲链表工作时,如果分配块比空闲块小,可以把空闲块分为两部分,一部分用来承装分配块,这样可以减少空闲部分无法使用而造成的浪费。隐式链表采用边界标记的方法进行双向合并。脚部与头部是相同的,均为 4 个字节,用来存储块的大小,以及表明这个块是已分配还是空闲块。同时定位头部和尾部,是为了能够以常数时间来进行块的合并。无论是与下一块还是与上一块合并,都可以通过他们的头部或尾部得知块大小,从而定位整个块,避免了从头遍历链表。但与此同时也显著的增加了额外的内存开销。他会根据每一个内存块的脚部边界标记来选择合并方式,如下图:

图33 “隐式空闲链表”示意图

7.9.2显式空闲链表

显式空闲链表只记录空闲块,而不是来记录所有块。它的思路是维护多个空闲链表,每个链表中的块有大致相等的大小,分配器维护着一个空闲链表数组,每个大小类一个空闲链表,当需要分配块时只需要在对应的空闲链表中搜索。

7.10本章小结

本章深入探讨了Hello程序的存储管理机制,揭示了从逻辑地址到物理地址的完整转换过程。通过段式管理(Intel架构)和页式管理(TLB与四级页表)的分析,阐明了虚拟内存如何通过地址变换实现隔离性与高效性。重点剖析了缺页故障的处理流程:内核通过合法性检查、磁盘加载和页表更新完成动态内存分配,支撑按需分页机制。此外,结合写时复制(COW)技术,说明了fork和execve时进程内存映射的优化策略,以及三级Cache如何加速物理内存访问。最后,对比了隐式与显式空闲链表的动态存储分配方法,凸显了内存碎片管理的设计权衡,系统展示了操作系统如何协调硬件与软件,实现安全、高效的内存管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

以下格式自行编排,编辑时删除

8.3 printf的实现分析

以下格式自行编排,编辑时删除

[转]printf 函数实现的深入剖析 – Pianistx – 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

以下格式自行编排,编辑时删除

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

以下格式自行编排,编辑时删除

(第81分)

结论

Hello程序的生命周期  

Hello程序从源代码到进程终止的完整生命周期可分为以下几个关键阶段: 

1. 预处理(cpp):将`hello.c`中的宏、头文件等展开,生成`hello.i`文件。 

2. 编译(ccl):将`hello.i`翻译为汇编代码`hello.s`。 

3. 汇编(as):将`hello.s`转换为可重定位目标文件`hello.o`。 

4. 链接(ld):将`hello.o`与库文件链接,生成可执行文件`hello`。 

5. 运行:在Shell中输入`./hello.out 2023111579 zyp 1234567890 9 `,启动程序。 

6. 创建进程:Shell调用`fork()`创建子进程。 

7. 加载程序:通过`execve()`加载`hello`,映射虚拟内存并跳转到`main`函数。 

8. 执行指令:CPU分配时间片,顺序执行程序逻辑。 

9. 访问内存:MMU通过页表将虚拟地址转换为物理地址。 

10. 信号管理:用户输入`Ctrl+C`(SIGINT终止)或`Ctrl+Z`(SIGTSTP挂起)时,内核处理信号。 

11. 终止:进程结束后,父进程回收资源,内核清理进程数据结构。 

这一过程涵盖了程序翻译、进程管理、存储映射和信号处理等计算机系统核心机制,完整展现了从代码到执行再到终止的计算机系统运作原理。

在当今的集成开发环境中,初学者仅需输入几行代码、轻点“运行”,便能瞬间见证“Hello, World!”跃然屏上。这“一键即达”的魔法背后,实则是计算机系统层层抽象的精妙演绎:从硅基晶体管的物理开关到指令集的逻辑跃迁,从机器码的冰冷二进制到高级语言的语义丰碑,从单核串行到多核并行的时空折叠——每一步跨越都是工程师对“不可能三角”(性能、通用、易用)的破局。冯·诺依曼架构的奠基、操作系统的资源博弈、虚拟化技术的时空延展……无数先贤以数学为刀、工程为尺,将混沌的硬件之力驯化为可推演的确定性逻辑,让信息革命的火种从实验室蔓延至万物互联的浩瀚人间。

附件

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

文件名称

作用

hello.c

储存hello程序源代码

hello.i

源代码经过预处理产生的文件(包含头文件等工作)

hello.s

hello程序对应的汇编语言文件

hello.o

可重定位目标文件

hello.out

二进制可执行文件

     

            

            

     

     

     

     

     

参考文献

[1] Bryant R E, O'Hallaron D R. Computer Systems: A Programmer's Perspective (3rd Edition). Pearson, 2016.

[2] GNU Compiler Collection (GCC) Manual.

[3] Levine J R. Linkers and Loaders. Morgan Kaufmann, 2000.

[4] Tanenbaum A S, Bos H. Modern Operating Systems (4th Edition). Pearson, 2014.

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容