第1章 概述
1.1 Hello简介
P2P(Program to Process)转化机制描述了从源代码文件(如hello.c)到可执行进程的动态生成过程。该过程涵盖预处理、编译、汇编、链接四个核心阶段,最终生成可在操作系统中加载执行的可执行文件。随后,shell通过进程创建机制将其载入内存,并为其分配独立的进程空间,实现程序的动态执行环境。
O2O(Zero to Zero)内存生命周期模型刻画了程序在内存中的完整存在周期。初始状态下,内存中不存在与hello程序相关的任何内容;shell通过execve系统调用启动程序,建立虚拟内存至物理内存的映射关系,并从程序入口点开始加载执行。在main函数执行完毕后,shell父进程负责回收子进程资源,操作系统内核则清除与该进程相关的所有数据结构,使系统回归初始状态。
1.2 环境与工具
硬件平台配置:
处理器:第14代Intel Core i9-14900HX @ 2.2GHz
内存容量:32GB DDR5
系统架构:x86-64兼容的64位体系结构
软件环境栈:
宿主操作系统:Windows 11 23H2 64位版本
虚拟化环境:VMware Workstation Pro 17
实验系统:Ubuntu 24.04 LTS
开发与调试工具链:
集成开发环境:Visual Studio 2022 17.8+ (64位)
Linux环境工具集:vim编辑器、gcc-13/g++-13编译套件、objdump反汇编工具、readelf分析工具、edb调试器、gdb增强插件(如pwndbg/gef)
1.3 中间结果
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.asm 反汇编hello.o得到的反汇编文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章系统构建了hello程序从静态代码到动态进程的完整生命周期框架。首先提出了P2P与O2O两个核心概念模型,阐述了程序转化与内存管理的理论机制;其次详细说明了实验所需的硬件架构、软件环境与专业工具链配置;最后列举了程序转换各阶段产生的关键中间文件及其在分析过程中的作用,为后续章节的深入技术分析奠定了实证基础。
第2章 预处理
2.1 预处理的概念与作用
****概念:****预处理是指编译器在语法分析之前,由预处理器对源代码进行的文本级处理过程。该过程主要处理以井号(#)开头的预处理指令,执行宏替换、头文件包含、条件编译等操作,同时移除程序注释和无关空白字符。预处理指令本身不参与后续编译,而是在预处理阶段被展开为符合C语言语法的实际代码内容。
****作用:****预处理阶段不对程序逻辑进行解析,而是对源代码文本进行结构化的重组与替换,其主要功能包括:
①头文件展开:将#include指令所指定的头文件内容递归插入源文件相应位置;
②宏替换:将所有宏标识符替换为预定义的文本或表达式;
③条件编译控制:根据预定义条件决定特定代码段是否参与后续编译;
④辅助处理:删除注释、添加行号标记及调试信息等。
本质上,预处理实现了源代码在编译前的文本级重构与规范化。
2.2在Ubuntu下预处理的命令
命令为:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
预处理生成的hello.i文件展现了源代码在编译前的完整形态。通过技术分析可见,该文件已从原始的十余行扩展至数千行,这一变化主要源于头文件的递归展开机制。
头文件展开的实质:当预处理器处理#include <stdio.h>指令时,它不仅插入stdio.h的内容,还递归展开该头文件所依赖的所有嵌套头文件。在生成的hello.i中,我们可以观察到标准输入输出函数(如printf)的完整原型声明、系统调用封装(如getpid)以及大量类型定义(如size_t)。这些内容共同构成了编译器理解用户代码所需的完整上下文环境。
预处理的技术特征:值得注意的是,所有预处理指令(如#define定义的宏)均已完成文本替换,而条件编译指令的判定逻辑也已执行完毕。文件末尾保留的#line标记为后续编译阶段提供了准确的源代码位置映射。整个预处理过程不涉及任何语法分析或类型检查,纯粹是在文本层面进行的复制、替换和删除操作。
工程意义:从工程视角看,hello.i文件创建了一个完全自包含的编译单元,消除了对外部头文件的运行时依赖,这使得编译过程具有确定性和可重复性。同时,通过分析预处理结果,开发者可以验证宏展开是否正确、头文件包含是否完整,为排查复杂的编译错误提供了重要线索。


2.4 本章小结
本章系统阐述了C语言程序预处理阶段的基本原理与功能,介绍了在Linux环境下使用GCC进行预处理的具体命令。通过分析hello.c程序预处理后生成的hello.i文件,直观展示了头文件展开、宏替换等操作的实现效果。实验结果表明,预处理后的文件已包含全部依赖库的函数原型、宏定义及条件编译指令,为后续编译阶段做好了文本层面的准备。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的基本定义
编译是将高级程序设计语言编写的源程序,通过系统化的翻译过程,转换为等价的低级汇编语言程序的转换机制。这一过程实现了从抽象算法描述到具体机器指令的语义保持映射。
3.1.2 编译的核心作用与流程
编译的核心价值在于建立高级语言与底层硬件架构之间的桥梁,其作用主要体现在:
- 抽象层次转换:将平台无关的高级语言语义映射为特定指令集架构的机器相关表示
- 执行效率优化:通过编译优化技术生成高效的目标代码
- 可移植性支持:分离程序逻辑与硬件实现细节
标准编译流程包含六个核心阶段:
- 词法分析:源程序字符流到词素序列的转换
- 语法分析:构建抽象语法树(AST)以表征程序结构
- 语义分析:类型检查与上下文相关约束验证
- 中间代码生成:生成平台无关的中间表示(如三地址码)
- 代码优化:对中间表示进行机器无关的性能优化
- 目标代码生成:最终汇编代码的生成与寄存器分配
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 汇编程序结构分析
生成的hello.s文件呈现标准的汇编程序结构,包含以下关键部分:
节区声明指令:
.file:源文件标识符
.text:代码段起始标记,存放可执行指令
.section .rodata:只读数据段,存储常量字符串
.globl main:声明全局符号main
.type:符号类型定义
.align:内存对齐约束声明

3.3.2 数据存储与访问模式
字符串常量处理:
程序中的字符串字面量"用法:Hello学号姓名 秒数!\n"和"Hello %s %s\n"被分配在.rodata只读数据段。编译器为每个字符串生成唯一标签(如.LC0、.LC1)并计算相对地址引用。

参数与局部变量布局:
argc参数:通过%edi寄存器传递,并存储于栈帧位置-20(%rbp)
argv参数数组:起始地址存放于栈帧位置-32(%rbp)
局部变量i:分配在栈帧位置-4(%rbp),符合自动存储期变量的典型布局
3.3.3 控制流实现机制
条件分支实现:
cmpl $4, -20(%rbp) # argc与立即数4比较
jne .L2 # 不相等时跳转至.L2标签
条件判断if(argc != 4)被翻译为比较-跳转指令对,基于EFLAGS寄存器中的条件码进行决策。

循环结构实现:
.L4:
addl $1, -4(%rbp) # i++
.L3:
cmpl $8, -4(%rbp) # i < 8比较
jl .L4 # 小于时继续循环
for(i=0; i<8; i++)循环被分解为初始化、条件测试、增量更新三个逻辑块,通过标签跳转实现迭代控制。
3.3.4 函数调用约定分析
调用栈管理:
- 函数调用前通过push指令保存调用者保存寄存器
- call指令自动将返回地址压栈并跳转至目标函数
- 被调用函数通过mov %rsp, %rbp建立新栈帧
参数传递规范:
根据System V AMD64 ABI调用约定:
- 前六个整型/指针参数通过%rdi、%rsi、%rdx、%rcx、%r8、%r9传递
- 浮点参数通过%xmm0-%xmm7传递
- 额外参数通过栈传递
具体函数调用实例:
printf("用法:Hello学号姓名 秒数!\n"):字符串地址通过%rdi传递
printf("Hello %s %s\n", argv[1], argv[2]):格式字符串地址通过%rdi,argv[1]通过%rsi,argv[2]通过%rdx传递
exit(1):立即数1通过%edi传递
3.3.5 类型转换实现
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdi # argv[3]地址加载到%rdi
call atoi # 字符串到整型转换
movl %eax, %edi # 转换结果传递至%edi
call sleep
atoi函数调用实现了从字符串到32位整型的显式类型转换,转换结果通过%eax返回,再传递至sleep函数。


3.4 本章小结
本章系统阐述了从预处理文件hello.i到汇编文件hello.s的编译转换过程。通过分析生成的汇编代码,揭示了高级语言结构到低级机器指令的映射关系,具体包括:
- 数据表示转换:高级语言变量到寄存器/内存位置的分配策略
- 控制流翻译:结构化控制语句到条件跳转指令的等价转换
- 函数调用实现:参数传递、栈帧管理和返回机制的规范实现
- 类型系统映射:抽象数据类型到具体机器表示的转换机制
编译阶段生成的汇编代码不仅保留了源程序的完整语义,还通过指令选择和优化为后续汇编阶段做好了准备,体现了现代编译器的分层设计和语义保持转换特性
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器(通常为as)将文本格式的汇编语言程序(.s文件)系统性地翻译为机器可执行的二进制指令序列,并将这些指令按照特定的目标文件格式进行封装,最终生成可重定位目标文件(.o文件)的过程。.o文件作为二进制文件,不仅包含机器指令编码,还包含了符号表、重定位信息等元数据。
4.2 在Ubuntu下汇编的命令
汇编阶段的主要功能包括:
- 指令翻译:将人类可读的汇编助记符转换为机器可直接解码执行的二进制操作码
- 地址解析:为符号引用分配临时地址,并记录需要链接时修正的位置
- 数据编码:将汇编指令中的立即数、偏移量等操作数编码为二进制格式
- 格式封装:按照目标平台的可重定位目标文件格式(如ELF)组织指令和数据
4.3 可重定位目标elf格式
4.3.1 ELF 文件结构概述
通过 readelf -a hello.o > hello.elf 命令可获取完整的 ELF 格式信息。典型的可重定位目标文件包含以下几个结构单元:
4.3.2 ELF 文件详细解析
(1)ELF 头(ELF Header)
ELF 头位于文件起始位置,包含 16 字节的魔数序列(7f 45 4c 46),用于标识该文件为 ELF 格式。头部中关键字段包括:
- 机器类型:EM_X86_64(0x3e),表明目标平台为 x86-64 架构。
- 文件类型:ET_REL(1),标识为可重定位文件。
- 节头表偏移:指示节头表在文件中的起始位置。
- 节头表条目大小与数量:描述节区元数据的组织结构。
(2)节头表(Section Header Table)
节头表记录了每个节区的元数据信息,主要包括以下节区:
- .text 节:类型为 PROGBITS(程序数据),包含机器指令,标志为 AX(可分配、可执行)。
- .data 节:类型为 PROGBITS,包含已初始化的全局变量和静态变量。
- .rodata 节:类型为 PROGBITS,包含只读数据(如字符串常量),标志为 A(可分配)。
- .symtab 节:类型为 SYMTAB,存储符号表信息。
- .strtab 节:类型为 STRTAB,存储符号名称字符串。
- .rela.text 节:类型为 RELA,包含 .text 节的重定位条目。
(3)重定位节区分析
.rela.text 节记录了链接时需要修正的位置信息,主要包括两类重定位:
绝对地址引用:例如对全局变量和函数的引用。
PC 相对地址引用:例如条件跳转的目标地址。
典型的重定位条目格式如下:
text
Offset Type Sym.Value Sym.Name + Addend
0000001b R_X86_64_PC32 00000000 printf - 4
0000002f R_X86_64_PC32 00000000 exit - 4
Offset:需要修改的指令在 .text 节中的偏移量。
Type:重定位类型,决定了地址计算方式。
Sym.Name:引用的符号名称。
(4)符号表(Symbol Table)解析
.symtab 节包含全局符号信息,关键符号类型包括:
- 全局函数:如 main 函数,类型为 FUNC,绑定类型为 GLOBAL。
- 外部引用:如 printf、exit、sleep、getchar 等库函数,类型为 NOTYPE,绑定类型为 GLOBAL,节索引为 UND(未定义)。
- 局部符号:如节区标签 .text、.data 等。
每个符号表条目包含符号值(在节内的偏移)、大小、类型、绑定信息和节索引等关键字段。
4.4 Hello.o的结果解析
4.4.1命令
在shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。


4.4.2与hel1o.s的对照分析
(1)增加机器语言
每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如,在hello.s中的一个cmpl指令表示为

而在反汇编文件中表示为

(2)操作数进制
反汇编文件中的所有操作数都改为十六进制。如(1)中的例子,立即数由hello.s中的4变为了0x4,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
(3)分支转移
反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L3)。例如下面的jmp指令,反汇编文件中为

而hello.s文件中为

(4)函数调用
反汇编文件中对函数的调用与重定位条目相对应。观察下面两个call指令调用函数,在hello.s中为

而在反汇编文件中调用函数为

在可重定位文件中call后面不再是函数名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章系统分析了从汇编语言源文件到可重定位目标文件的转换过程。通过对比hello.s汇编源文件与hello.o反汇编结果,揭示了汇编器在以下方面的处理机制:
- 指令编码转换:将文本指令映射为二进制机器码,同时保持语义一致性
- 符号解析处理:为局部符号分配临时地址,为外部符号创建重定位记录
- 地址计算模式:根据指令类型(绝对/相对寻址)采用不同的地址编码策略
- 重定位信息生成:记录链接阶段需要修正的所有位置及其计算方式
可重定位目标文件不仅包含机器指令,更重要的是提供了完整的元数据信息(符号表、重定位表),使得链接器能够将多个目标模块合并为最终的可执行文件。这一设计体现了现代编译系统模块化、分阶段处理的架构思想。
第 5 章 链接
5.1 链接的概念与作用
概念:链接是指将多个独立编译或汇编生成的目标模块中的代码和数据片段进行收集、组合与地址重定位,最终形成可在内存中加载执行的单一文件的过程。根据执行时机的不同,链接可分为三种类型:
- 编译时链接:在源代码翻译为机器码时完成
- 加载时链接:在程序被加载到内存时由加载器执行
- 运行时链接:程序执行过程中动态完成
作用:链接器作为自动化工具,实现了分离编译的工程范式,其主要作用包括:
- 模块化开发支持:允许将大型应用程序分解为可独立编译和维护的模块
- 库函数集成:将标准库和第三方库函数整合到最终可执行文件中
- 地址空间组织:为所有程序元素分配统一的虚拟内存地址
符号解析与重定位:解决跨模块的符号引用关系,完成地址绑定
5.2 在Ubuntu下链接的命令
命令: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.3 可执行目标文件hello的格式
使用readelf解析hello的ELF格式,得到hello的节信息和段信息:
(1)ELF头(ELF Header)
hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以描述了生成该文件的系统的字的大小和字节顺序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello1.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

(2)节头
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。


(3)程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

(4)Dynamic section

(5)Symbol table
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。


5.4 hello的虚拟地址空间
观察程序头的LOAD可加载的程序段的地址为0x400000。如图:

使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查
看各段信息。如图:

程序从地址0x400000开始到0x401000被载入,虚拟地址从0x4000000x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。
如.interp节,在hello.elf文件中能看到开始的虚拟地址:

我们可以找到如.text节的信息:


5.5 链接的重定位过程分析
5.5.1分析helo与helo.o区别
在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm

与第四章中生成的hello.asm文件进行比较,其不同之处如下:
(1)链接后函数数量增加
链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
(2)函数调用指令call的参数发生变化
在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
(3)跳转指令参数发生变化
在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。
5.5.2重定位过程
重定位由两步组成:
(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
5.6 hello的执行流程
5.6.1过程
通过edb的调试,一步一步地记录下call命令进入的函数。
(I)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
5.6.2子程序名或地址
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x4010a0
_sleep 0x4010e0
_getchar 0x4010b0
_exit 0x4010d0
5.7 Hello的动态链接分析
hello 依赖动态库 libc.so,通过 edb 可视化调试分析动态链接过程。
运行命令:
edb --run output/hello 学号 姓名 手机号 秒数
调试步骤:
-
点击「Breakpoints」→ 新增断点:地址 0x401030(printf@plt);
-
点击运行,命中断点后,查看「Memory」窗口,输入 .got.plt 的地址(通过 `readelf -S output/hello` 获取 .got.plt 的 Addr);
-
观察 .got.plt 中 printf 对应的条目:初始值为 PLT 表中 printf 的跳转地址(未解析),执行一次 printf 后,值变为 libc.so 中 printf 的实际地址(动态链接器填充)。
动态链接原理:程序首次调用 printf 时,通过 PLT 表跳转至动态链接器 ld-linux.so,链接器查找 libc.so 中 printf 的地址,更新 .got.plt 中的条目,后续调用直接从 .got.plt 获取地址,无需重复解析(延迟绑定)。

5.8 本章小结
本章通过hello程序的链接过程,系统阐述了现代链接系统的核心机制:
- 多阶段链接架构:展示了从目标文件到可执行文件的完整转换流程
- 地址空间管理:分析了虚拟内存空间的分配策略和布局原则
- 动态链接实现:揭示了PLT/GOT机制如何实现高效的延迟绑定
- 安全增强特性:说明了地址随机化等现代安全机制的实施
链接过程不仅完成了简单的代码合并,更实现了复杂的地址计算、符号解析和运行时支持,是现代操作系统可执行文件格式设计思想的集中体现。通过对链接过程的深入理解,开发者能够更好地掌握程序的内存布局、加载机制和运行时行为。
- hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程的经典定义就是一个执行中程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
6.1.2进程的作用
进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell是一个交互型应用级程序,也被称为命令解析器,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。
6.2.2 Shell-bash的处理流程
首先从终端读入输入的命令,对输入的命令进行解析,如果该命令为内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前台程序则等待程序执行结束,若为后台程序则将其放回后台并返回。在过程中shell可以接受从键盘输入的信号并对其进行处理。
6.3 Hello的fork进程创建过程
首先用户再shel1界面输入指令:./hel1o 2021113211 郑文翔
Shell判断该指令不是内置命令,于是父进程调用fork函数创建一个新的子进程,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程与父进程最大的区别就是具有不同的PID。在父进程中,fork返回子进程的PID,而在子进程中fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个程序。函数声明如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并不返回。main函数运行时,用户栈的结构如图所示:

6.5 Hello的进程执行
hello程序在运行时,进程提供给应用程序的抽象有:(1)一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占地使用处理器;(2)一个私有的地址空问,它提供一个假象,好像我们的程序独占地使用CPU内存。
操作系统提供的抽象有:
(1)逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
(2)上下文切换。操作系统内核使用一种称为上下文切换的叫高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需状态。
(3)时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
(4)用户模式和内核模式。处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。
(5)上下文信息。上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。hello程序执行过程中,在进程调用execve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf函数输出"Hello 2021113211 郑文翔",之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
6.6.1异常的分类

6.6.2异常的处理方式




6.6.3运行结果及相关命令
(1)正常运行状态
在程序正常运行时,打印8次提示信息,以输入回车为标志结束程序,并回收进程。

(2)运行时按下Ctrl + C
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

(3)运行时按下Ctrl + Z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

(4)对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。

(5)在Shell中输入pstree命令,可以将所有进程以树状图显示:

(6)输入kill命令,则可以杀死指定(进程组的)进程:
(7) 输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。
(8)不停乱按
在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个'\n'结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。
6.7本章小结
本章通过hello程序实例,系统分析了Linux环境下的进程管理机制:
- 进程生命周期管理:详细阐述了从fork()创建到execve()加载的完整过程
- 执行环境构建:分析了虚拟地址空间映射和运行上下文建立机制
- 并发控制机制:说明了时间片调度和上下文切换的实现原理
- 信号处理体系:通过实验验证了异常信号的处理流程和作业控制功能
进程作为操作系统资源管理的基本单元,其设计体现了虚拟化、隔离和并发执行的核心思想。通过对hello进程执行过程的分析,揭示了用户程序与操作系统内核之间的交互机制,为理解现代操作系统的工作原理提供了具体实例。
第 7 章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
在具备地址转换机制的计算机体系结构中,由程序中的访问指令所给出的地址(例如操作数地址)被称为逻辑地址,亦称相对地址。逻辑地址通常由段标识符与段内偏移量两部分构成,例如在程序hello中由代码生成的、与特定段相关联的偏移地址部分。该地址需要经过寻址方式计算或地址转换步骤,才能最终对应到内存中的实际物理地址。
7.1.2线性地址
线性地址是逻辑地址到物理地址变换之间的一步,程序hello的代码会产生逻辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
7.1.3虚拟地址
程序访问存储器所使用的逻辑地址称为虚拟地址。虚拟地址经过地址翻译得到物理地址。与实际物理内存容量无关,是hello中的虚拟地址。
7.1.4物理地址
在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是一种将程序划分为若干逻辑段进行存储的内存管理方式,每个段作为一个独立的逻辑实体存在。该机制通过段表实现管理,段表中记录了段号(或段名称)、段起始地址、加载状态以及段长度等信息。程序经过分段后被划分为多个区块,例如代码段、数据段、共享段等。
逻辑地址由两部分构成:段标识符与段内偏移量。段标识符为16位长的段选择符,其中高13位作为索引号,低3位包含若干硬件控制信息。该索引号用于在段描述符表中定位对应的段描述符,每个段描述符详细定义了一个内存段的属性,多个段描述符共同构成段描述符表。
全局描述符表(GDT)在整个系统中唯一存在,其内容包括:(1)操作系统自身使用的代码段、数据段和堆栈段的描述符;(2)各个任务或程序对应的局部描述符表(LDT)的描述符。
每个任务或程序拥有独立的局部描述符表(LDT),其中包含:(1)该任务私有的代码段、数据段及堆栈段的描述符;(2)该任务所使用的各类门描述符,例如任务门、调用门等。
段式管理的示意结构如下图所示:

7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存系统将主内存与磁盘存储空间统一抽象为一个连续的、大小为N字节的地址空间。该空间被划分为大小固定的单元,称为虚拟页。相应的,物理内存也被划分为同等大小的物理页框。虚拟页可映射至物理页框,亦可暂时存储在磁盘上。
系统通过页表来记录与维护各个虚拟页的状态信息。页表实质上是一个页表条目(PTE)数组,每个条目对应一个虚拟页。典型的页表条目包含有效位与地址字段两部分。有效位指示该虚拟页当前是否已加载至物理内存中;若有效位为1,则地址字段存储对应的物理页框起始地址;若有效位为0,则表示该页当前不在物理内存中(即未缓存),其内容存放于磁盘上。
当程序访问有效位为0的虚拟页时,将触发缺页异常。此时操作系统会介入处理:从磁盘中加载所需页至一个空闲物理页框,更新页表条目,并重新执行引发异常的指令。
内存管理单元(MMU)是专门负责动态地址转换的硬件部件。当CPU生成虚拟地址进行内存访问时,MMU会借助页表自动将该虚拟地址转换为物理地址。转换过程一般包括:从虚拟地址中提取虚拟页号(VPN),以其为索引查找页表、获取页表条目、检查有效性,最终将虚拟地址中的页内偏移与页表条目中的物理页框号组合,形成物理地址。这一机制为每个进程提供了独占整个地址空间的抽象视图,并实现了内存的共享、保护与高效利用。
页式管理的示意结构如下图所示:

7.4 TLB与四级页表支持下的VA到PA的变换
Intel Core i7 处理器采用四级页表结构实现虚拟内存地址转换。当CPU生成虚拟地址后,该地址被送入内存管理单元进行处理。MMU首先利用虚拟页号的高位信息生成TLB标签与索引,并在转译后备缓冲器(TLB)中查找对应的地址映射。若TLB命中,则可直接获得物理页框号,与虚拟地址中的页内偏移结合后生成物理地址。
若TLB未命中,MMU将启动完整的页表查询流程。该流程始于控制寄存器CR3所存储的顶级页表基地址,随后依次进行四级查询:第一级索引定位页全局目录项,获得下一级页表基址;第二级索引在页上级目录中定位页中间目录基址;第三级索引在页中间目录中定位页表基址;第四级索引在最终页表中获得目标页表条目,其中包含物理页框号。
得到物理页框号后,将其与原始虚拟地址中的页内偏移量组合,形成最终物理地址。转换完成后,系统会将本次查询得到的虚拟页号与物理页框号的映射关系存入TLB中,以加速后续相同地址的访问。这种分级页表机制在保持地址空间灵活性的同时,通过TLB缓存与多级索引实现了地址转换效率的优化。
完成地址转换后,系统会将本次查询得到的虚拟页号与物理页框号映射关系添加到TLB中,以加速后续相同虚拟地址的访问请求。这种分级查询机制在保证地址空间灵活性的同时,通过TLB缓存和分级索引实现了地址转换的效率优化。
多级页表的工作原理展示如下:

7.5 三级Cache支持下的物理内存访问
如图为高速缓存存储器组织结构:

高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位
在高速缓存中进行数据查找时,若根据地址中的组索引位所定位的缓存组内,存在有效位为1且标记位与地址中标记位完全匹配的缓存行,则发生缓存命中,所需数据可直接从该行读取。否则,发生缓存未命中。
缓存未命中时,系统需从存储层次的下级(如更大容量的缓存或主存)中提取目标数据块。取得数据块后,将其载入最初由组索引位确定的缓存组中。若该组内已无空闲行,则根据预设替换策略(如最近最少使用策略)选择一行进行替换,将新数据块写入该行并更新其标记与有效状态,从而实现缓存内容的更新与循环使用。
7.6 hello进程fork时的内存映射
当当前进程调用 fork 函数时,内核为新进程创建必要的管理数据结构,并分配唯一的进程标识符(PID)。为了构建新进程的虚拟内存空间,内核复制当前进程的 mm_struct 结构、内存区域结构以及页表。当 fork 调用在新进程中返回时,新进程拥有与调用 fork 时完全相同的虚拟内存副本。此后,若任一进程尝试执行写入操作,写时复制机制将创建新的物理页面,从而确保每个进程保持私有地址空间的抽象。
7.7 hello进程execve时的内存映射
execve 函数通过内核中的加载器代码,在当前进程中加载并运行可执行目标文件 hello 中的程序,实质上使用 hello 程序替换原有程序。该过程包含以下主要步骤:
(1)删除现有用户区域:清除当前进程虚拟地址空间中用户部分的所有已有内存区域结构。
(2)映射私有区域:为 hello 程序的代码、数据、.bss 及栈区域创建新的私有内存区域结构,这些区域均以写时复制方式建立。代码与数据区域映射至 hello 文件的 .text 和 .data 段;.bss 区域初始化为二进制零,映射到匿名文件;栈与堆区域同样初始化为零,初始长度为零。
(3)映射共享区域:若 hello 程序链接了共享库(如 libc.so),则将该共享对象动态映射到进程虚拟地址空间的共享区域中。
(4)设置程序计数器:execve 最后将当前进程上下文中的程序计数器设置为代码区域的入口地址,从而开始执行 hello 程序。
该过程如下图所示:

7.8 缺页故障与缺页中断处理
若程序执行过程中触发缺页异常,内核将调用缺页处理程序进行处理,步骤如下:
(1)检查触发缺页的虚拟地址是否合法,若不合法则触发段错误并终止进程。
(2)检查进程是否具有对该区域页面的读、写或执行权限,若无权限则触发保护异常并终止进程。
(3)若上述检查均通过,内核选择一个牺牲页面;若该页面已被修改,则将其写回磁盘。随后将目标页从磁盘加载至内存,更新页表条目,最后将控制权交还进程,重新执行引发缺页的指令。

7.9本章小结
本章系统阐述了 hello 程序的存储器地址空间概念,分析了 Intel 架构下的段式管理机制与 hello 程序的页式内存管理方式,并结合 Intel Core i7 环境详细说明了虚拟地址至物理地址的转换过程及其在物理内存中的访问机制。同时,探讨了 hello 进程在执行 fork 及 execve 时的内存映射行为,并解释了缺页异常的产生与处理流程。
7.9动态存储分配管理
7.10本章小结
本章系统阐述了hello程序运行所依赖的存储管理体系。首先解析了程序使用的逻辑地址、线性地址、虚拟地址与物理地址等不同层次的存储器地址空间概念。进而详细剖析了Intel架构采用的段式管理机制,以及在此基础上实现的页式管理方案。文章以Intel Core i7处理器在典型Linux环境下的运行为例,完整揭示了从虚拟地址到物理地址的转换流程,并说明了在此过程中物理内存的实际访问方式。此外,本章深入分析了hello进程生命周期中的两个关键内存操作:通过`fork`系统调用创建子进程时,操作系统如何复制父进程地址空间并采用写时复制技术进行优化;通过`execve`系统调用加载新程序时,加载器如何建立全新的地址空间映射。最后,探讨了缺页故障的触发条件、内核缺页中断处理程序的响应流程,以及操作系统如何通过页面调度机制在内存与磁盘之间透明地交换数据,从而支撑hello程序在有限物理资源下高效、可靠地运行。
第 8 章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux操作系统采用统一的I/O设备管理模型,其核心思想是将所有输入输出设备抽象为文件对象。这种"一切皆文件"的设计哲学使得应用程序可以通过统一的操作接口访问各种不同类型的设备,包括磁盘、键盘、显示器、网络接口等。
在Linux内核中,每个设备都被映射到文件系统的某个路径下,通常位于/dev目录中。设备文件分为两种主要类型:字符设备文件和块设备文件。字符设备以字节流的形式进行数据传输,适合键盘、串口等需要实时处理的设备;块设备则以固定大小的数据块为单位进行读写,适合磁盘等存储设备。
设备管理的核心是设备驱动程序,这些驱动程序在内核空间运行,直接与硬件交互。当应用程序发起I/O请求时,请求经过系统调用接口传递给内核,内核的I/O子系统将请求路由到相应的设备驱动程序,由驱动程序完成实际的硬件操作。这种分层架构使得应用程序不需要了解硬件的具体细节,只需要通过标准接口进行访问。
8.2 简述Unix IO接口及其函数
Unix系统提供了一套统一的I/O系统调用接口,这些接口构成了应用程序与I/O设备交互的基础。主要的Unix I/O函数包括:
open()函数用于打开文件或设备,接受文件路径和打开模式作为参数,返回一个文件描述符。这个描述符是一个非负整数,在后续的I/O操作中作为文件的标识符。close()函数用于关闭已打开的文件,释放相关的系统资源。
read()和write()函数分别用于从文件读取数据和向文件写入数据。它们都接受文件描述符、数据缓冲区指针和要传输的字节数作为参数。read()函数将数据从文件读入缓冲区,返回实际读取的字节数;write()函数将数据从缓冲区写入文件,返回实际写入的字节数。
lseek()函数用于改变文件的当前读写位置,支持相对位置和绝对位置的定位。ioctl()函数提供对设备的控制功能,可以执行设备特定的操作,如设置终端参数、控制磁盘驱动器等。
这些系统调用在设计上遵循了最小化接口原则,提供了足够的灵活性来处理各种I/O需求。所有I/O操作都是通过文件描述符进行的,这种一致性简化了应用程序的设计和实现
8.3 printf的实现分析
printf函数是C语言标准库中用于格式化输出的关键函数,其实现涉及多个软件层次的协作。当hello程序调用printf输出"Hello World"时,整个处理流程如下:
首先,printf函数内部调用vprintf或类似函数,处理格式字符串和可变参数。格式化过程包括解析格式说明符(如%s、%d)、将参数转换为字符串表示、处理宽度和精度指定符等。这个阶段在用户空间的C库中完成,生成一个包含完整显示信息的字符串缓冲区。
接下来,格式化后的字符串通过write系统调用传递给内核。在x86架构上,这通常通过int 0x80指令或syscall指令触发软件中断,使CPU从用户模式切换到内核模式。系统调用处理器根据传递的系统调用号识别write操作,并将控制权转交给相应的内核函数。
在内核中,write系统调用首先检查参数的合法性,包括文件描述符的有效性、缓冲区的可访问性等。对于标准输出(文件描述符1),内核需要将数据传递给终端或控制台子系统。这涉及终端驱动程序的调度,可能需要处理行规范、信号生成等终端特定逻辑。
对于字符显示,最终的数据需要转换为屏幕上可见的像素。这个过程涉及多个步骤:首先,字符的ASCII码需要从字模库中查找对应的点阵图案;然后,这些点阵信息被转换为像素颜色值,写入显示控制器的视频RAM中;最后,显示控制器按照固定的刷新频率(通常60Hz)读取VRAM内容,通过视频信号线将颜色信息传输到显示器。
在hello程序的例子中,printf输出的字符串经过上述完整流程,最终在用户的显示器上呈现出来。整个过程涉及用户空间库函数、系统调用接口、内核I/O子系统、设备驱动程序、硬件控制器等多个组件的协同工作。
8.4 getchar的实现分析
getchar函数用于从标准输入读取单个字符,在hello程序中用于等待用户按下回车键。其实现同样涉及复杂的系统交互:
当用户在键盘上按下按键时,硬件层面产生键盘中断。键盘控制器检测到按键动作,将按键的扫描码通过中断请求线发送给CPU。CPU响应中断,暂停当前执行的程序,保存上下文状态,然后跳转到键盘中断处理程序。
键盘中断处理程序位于内核中,它从键盘控制器的数据端口读取扫描码,将扫描码转换为对应的ASCII字符码。这个转换过程需要考虑键盘布局、修饰键状态(如Shift、Ctrl)等因素。转换后的字符被放入内核维护的输入缓冲区中,这个缓冲区通常组织为环形队列,支持多字符的缓存。
当hello程序调用getchar函数时,实际上是调用C标准库中的getchar实现,该函数内部调用read系统调用从标准输入(文件描述符0)读取数据。read系统调用检查输入缓冲区,如果缓冲区中有可用字符,则立即返回第一个字符;如果缓冲区为空,则调用进程可能被阻塞,进入睡眠状态,等待键盘输入。
一个重要的细节是,终端通常工作在规范模式下,在这种模式下,输入数据会经过行编辑处理,直到用户按下回车键,整行数据才变得对读取进程可用。这就是为什么hello程序中的getchar调用会等待回车键的原因。按下回车键后,终端驱动程序将换行符添加到输入缓冲区,并唤醒等待输入的进程。
getchar函数从read系统调用返回后,将读取的字符返回给调用者。如果读取的是换行符,hello程序继续执行后续代码;如果用户在输入过程中按下了Ctrl+C等特殊组合键,终端驱动程序会生成相应的信号,可能导致程序终止。
8.5本章小结
本章分析了hello程序涉及的I/O管理机制,重点关注了输出函数printf和输入函数getchar的实现原理。通过分析可以看出,一个简单的I/O操作背后是多个系统层次的复杂协作。
Linux的统一设备模型通过"一切皆文件"的抽象,为应用程序提供了简洁一致的I/O接口。Unix I/O系统调用作为用户空间与内核空间的桥梁,使得应用程序能够安全有效地访问各种硬件资源。
printf函数的实现展示了从格式化字符串到屏幕显示的完整数据通路,涉及用户空间库函数、系统调用、内核驱动程序、硬件控制器等多个组件。getchar函数的实现则揭示了从键盘中断到字符读取的完整输入流程,体现了异步事件处理、缓冲区管理、进程调度等系统机制。
这些I/O机制的设计体现了几个重要的系统原则:通过抽象隐藏复杂性,通过分层实现模块化,通过缓冲提高效率,通过中断实现实时响应。理解这些底层机制对于编写高效可靠的应用程序、进行系统性能调试和深入学习操作系统原理都具有重要意义。
hello程序的I/O操作虽然表面上简单,但实际上利用了现代计算机系统的完整I/O基础设施。从按键检测到屏幕刷新,从用户空间调用到硬件中断处理,每一个步骤都体现了精心设计的系统架构和高效的实现机制。
结论
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、运行。在shel1中输入./hello 2024112966 辛博 18804528791 1。
6、创建进程。终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。
7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
9、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。
10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
感悟:
通过本次实验我加深了对计算机系统这个事物的理解,更加能够明白平时运用的代码的运行原理。
附件
|-----------|-----------------------------|
| 文件名 | 功能 |
| hello.c | 源程序 |
| hello.i | 预处理后得到的文本文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
| hello.asm | 反汇编hello.o得到的反汇编文件 |
| hello | 可执行文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
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. \[7\] [https://www.cnblogs.com/pianist/p/3315801.html](https://www.cnblogs.com/pianist/p/3315801.html "https://www.cnblogs.com/pianist/p/3315801.html") \[8\] [https://www.cnblogs.com/fanzhidongyzby/p/3519838.html](https://www.cnblogs.com/fanzhidongyzby/p/3519838.html "https://www.cnblogs.com/fanzhidongyzby/p/3519838.html"). \[9\] [https://www.cnblogs.com/diaohaiwei/p/5094959.html](https://www.cnblogs.com/diaohaiwei/p/5094959.html "https://www.cnblogs.com/diaohaiwei/p/5094959.html") \[10\] https://blog.csdn.net/spfLinux/article/details/54427494