计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术模块
学 号 2023111843
班 级 23WL018
学 生 麻皓然
指 导 教 师 吴锐
计算机科学与技术学院
2025年5月
本文以基础的hello.c代码为例,系统阐述了该程序在Linux环境中的完整执行流程。内容涵盖源代码预处理、编译转换、汇编生成、链接构建、进程调度、内存分配及输入输出控制等七大核心环节,完整呈现了从代码编辑、存储到执行终止的全过程。通过深入解析hello.c的运行轨迹,生动展现了程序从诞生到结束的完整生命周期。
关键词:C程序、Linux系统、执行流程、生命周期分析
目 录
第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 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
1.P2P(Program to Process)阶段
(1)编码(Program)
用户通过编辑器(如vim)创建hello.c源文件,代码被写入磁盘。此时Hello作为静态文本(Program)存在,尚未获得生命。
(2)编译流程
预处理:gcc -E展开头文件与宏,生成.i文件。
编译:gcc -S将C代码转为汇编指令(.s文件),接近机器语言。
汇编:gcc -c生成可重定位目标文件(.o),含二进制机器码。
链接:ld合并库函数(如printf),生成可执行文件(a.out),此时Hello成为完整的可执行程序。
(3)进程创建(Process)
用户在Shell中输入./a.out后:
Fork:Bash调用fork()创建子进程,复制父进程上下文。
Execve:子进程通过execve()加载a.out,替换为Hello的代码段、数据段,正式成为进程。
2. O2O(Zero to Zero)阶段
(1)从无到有:从磁盘上的静态程序(Zero)到内存中的活跃进程(One)。
(2)从有到无:进程终止后,所有资源归零(Zero),仅留下日志或输出痕迹。
1.2 环境与工具
(1)硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk;
(2)软件环境:Windows10 64位; Vmware 11; Ubuntu 16.04 LTS 64位;
(3)开发工具:CodeBlocks;vi/vim/gpedit+gcc;gdb;edb;readelf;objdump
1.3 中间结果
hello.i: hello.c经预处理得到的文本文件
hello.s: hello.i 经编译程序的汇编文件
hello.o: hello.s经得到的可重定位目标文件
hello_elf.txt: hello.o经readelf分析得到的文本文件
hello: hello.o经链接得到的可执行目标文件
hello1_elf.txt: hello经readelf分析得到的文本文件
hello1_dis.txt: hello经objdump反汇编得到的文本文件
1.4 本章小结
本章以Hello程序为例,详细剖析了其从源代码到进程的完整生命周期,包括编码、预处理、编译、汇编、链接、进程创建等关键步骤,并阐述了程序从无到有再到资源释放的全过程,同时列出了实验所需的软硬件环境及生成的中间文件,为后续深入分析计算机系统底层机制奠定了基础。
第2章 预处理
2.1 预处理的概念与作用
1.预处理的概念
预处理是C/C++程序编译流程的第一个阶段,由预处理器对源代码进行文本级的处理。它不涉及语法分析或代码优化,仅执行基于指令的文本替换和文件整合操作,生成一个经过修改的中间代码文件(通常为.i文件),供后续编译阶段使用。
2.预处理的作用
(1)宏展开:处理#define定义的宏,将其直接替换到代码中。
(2)头文件包含:将#include指定的头文件内容完整插入到当前文件中。
(3)条件编译:根据#ifdef、#if等指令决定是否保留特定代码块。
(4)注释删除:清除源代码中的所有注释,减少编译负担。
(5)特殊指令处理:执行#pragma等编译器特定指令,实现特殊功能。
2.2在Ubuntu下预处理的命令


在终端输入gcc -E hello.c -o hello.i,即可在目录中生成hello.i文件
2.3 Hello的预处理结果解析
预处理阶段对hello.c进行了文本级的处理,主要包括宏展开、头文件包含、条件编译、注释删除等操作。以下是几个关键部分的分析和截图说明:
1. 预处理命令执行
分析:
(1)使用gcc -E命令对hello.c进行预处理,生成hello.i文件。
(2)执行ls -l命令对比源文件和预处理后文件的大小差异,可观察到预处理后的文件显著增大。

2. hello.i 文件头部
分析:
(1)预处理后的文件开头包含大量#开头的行号标记,用于调试和错误定位。
(2)随后引入stdio.h等头文件,展开标准库的宏定义和函数声明。

3. 系统头文件展开
分析:
(1)预处理后,#include <stdio.h>被替换为完整的标准I/O函数声明(如printf、scanf等)。
(2)类似地,#include <unistd.h>和#include <stdlib.h>也被展开,包含大量系统调用和库函数定义。

4. 原始代码保留部分截图
分析:
(1)预处理不会修改原始代码逻辑,main函数及其内部结构保持不变。
(2)注释被删除,但printf、sleep、getchar等函数调用仍然保留。

2.4 本章小结
本章通过hello.c的预处理过程,展示了C程序编译的第一阶段。预处理将源代码中的指令(如#include和#define)转换为纯C代码,生成hello.i文件。这一阶段完成了头文件包含、宏展开等工作,为后续编译阶段做好准备。预处理后的文件体积显著增大,主要原因是系统头文件的内容被完整插入到源文件中。通过分析hello.i,可以清晰地看到预处理对源代码的转换过程。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是将预处理后的源代码(.i文件)转换为汇编语言程序(.s文件)的过程。这一阶段的核心任务是进行词法分析、语法分析和语义分析,将高级语言代码转化为更接近机器语言的低级表示。编译器通过优化算法对代码进行结构调整和效率提升,同时生成与目标平台相关的汇编指令,为后续的汇编和链接阶段奠定基础。
3.1.2 编译的作用
编译的作用在于搭建高级语言与机器语言之间的桥梁,使程序能够在特定硬件平台上运行。它将与机器无关的代码转化为依赖于处理器架构的汇编指令,同时确保转换后的代码保持原始逻辑的正确性。这一过程不仅提高了程序的可移植性,还通过代码优化显著提升了执行效率,为生成最终的可执行文件提供了必要的中间表示。
3.2 在Ubuntu下编译的命令
编译指令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 汇编代码的元信息记录
在汇编代码的开头部分,编译器会生成一些用于链接和调试的元信息。这些信息包括:
1. .file:指明源文件名(如hello.c)
- .text:标识代码段的开始
- .section .rodata:定义只读数据段,存放字符串常量等不可修改的数据
- .align 8:指定8字节对齐,确保数据访问效率
- .string:存储字符串常量(如"Hello %s %s\n")
- .global main:声明main为全局符号,使链接器可以识别
- .type main, @function:指明main是一个函数
这些元信息不直接参与程序运行,但在编译和链接过程中起到关键作用。

3.3.2 局部变量的存储与操作
局部变量(如int i)在函数调用时分配在栈上。以下是具体处理方式:
- 栈空间分配:
进入main函数后,编译器通过调整栈指针%rsp预留空间(如subq $16, %rsp),用于存储局部变量。 - 变量初始化:
变量i的初始值0通过movl $0, -4(%rbp)存入栈中(-4(%rbp)为变量地址)。 - 生命周期管理:
函数返回前,栈空间会被释放(通过leave或addq $16, %rsp)。

3.3.3 字符串常量的处理
字符串常量(如"Hello %s %s\n")被放置在只读数据段(.rodata),并通过标签(如LC0)引用。在代码中,通过movq $.LC0, %rdi将字符串地址加载到寄存器,作为printf的参数。

3.3.4 立即数的使用
立即数直接嵌入指令中,以$前缀标识。

3.3.5 函数参数的传递
main函数的参数argc和argv通过寄存器传递(遵循System V AMD64调用约定):
- 寄存器分配:
argc(int)存入%edi
argv(char**)存入%rsi
- 栈备份:
若后续需要使用这些参数,编译器会将其保存到栈中(如movq %rsi, -32(%rbp))

3.3.6 数组的访问
ldr x0, [sp, 16] 加载argv基址
add x0, x0, 8 argv[1](偏移8字节)
ldr x1, [x0] 读取argv[1]到x1(printf参数2)
类似地,argv[2]和argv[3]通过偏移16和24访问。

3.3.7 函数的调用与返回
参数通过x0(格式串)、x1-x3(变量)传递。
mov w0, 0:返回值0(w0为32位返回值寄存器)。
ldp x29, x30, [sp], 48:恢复帧指针和返回地址,并释放栈空间。
ret:返回调用者。
(1)printf的调用

(2)sleep的调用

3.3.8 循环结构的实现
循环变量初始化:str wzr, [sp, 44]将局部变量(i)初始化为0(wzr为零寄存器)。
条件检查:cmp w0, 9 + ble .L4(若i ≤ 9继续循环)。
迭代更新:add w0, w0, 1 + str w0, [sp, 44](i++)。

3.3.9 赋值操作
1.赋值核心指令:mov(寄存器间)、str(到内存)、ldr(从内存加载)。
2.特点:ARM使用str/ldr显式操作内存,而x86的mov可同时处理寄存器和内存。
3.应用场景:局部变量初始化、参数传递、循环控制等均依赖赋值操作。

3.4 本章小结
本章系统阐述了编译阶段的核心原理与实现过程。编译器的核心任务是通过词法分析和语法分析,在确保所有指令符合语法规范的前提下,将其转换为等价的中间表示或目标架构的汇编代码。我通过具体的函数示例,深入分析了C语言结构到汇编指令的转换机制。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是将汇编语言程序(.s文件)转换为机器语言二进制程序(.o文件)的过程。这一阶段的核心任务是将人类可读的汇编指令逐条翻译成处理器可直接执行的机器码,并生成符合目标文件格式(如ELF)的可重定位目标文件。汇编器会处理符号引用、地址计算等底层细节,同时保留必要的重定位信息供后续链接使用。
4.1.2 汇编的作用
汇编的作用在于建立汇编指令与机器码之间的精确映射,为程序的可执行奠定基础。它通过解析助记符、寄存器名称等符号化表示,生成对应的二进制操作码和操作数,同时维护符号表、重定位表等元数据,确保代码和数据能够被正确加载和执行。这一过程是连接高级语言与计算机硬件的关键桥梁。
4.2 在Ubuntu下汇编的命令
汇编指令:gcc hello.s -c -o hello.o

4.3 可重定位目标elf格式
4.3.1. ELF 文件头分析
- 关键字段:
(1)Magic: 7f 45 4c 46(ELF文件标识)
(2)Class: ELF64(64位架构)
(3)Type: REL (Relocatable file)(可重定位目标文件)
(4)Machine: AArch64(ARM64架构)
(5)Entry point: 0x0(无入口地址,需链接后确定)
(6)Section headers: 共13个节区头(如.text、.data、.rodata等)

4.3.2 节头分析
节头部分集中记录了各节的名称、类型、地址、偏移量、大小、旗标、链接、信息、对齐等信息。使用节头表中的字节偏移信息可以得到各节在文件中的起始位置,以及各节所占空间的大小,这样便于重定位。

4.3.3 重定位节的分析
重定位类型:
R_AARCH64_ADR_PREL_LO21: 用于生成PC相对地址(如加载字符串地址)。
R_AARCH64_CALL26: 处理26位偏移的函数调用指令。
重定位节中包含.text 节中需要进行重定位的信息,可以观察到需要重定位的函数有: .rodata, puts, exits, printf, atoi, sleep, getchar等

4.3.4 符号表的分析
符号表中存放了程序中定义和引用的函数和全局变量的信息。
关键符号:
| 值 | 类型 | 绑定 | 可见性 | 名称 | 节区 |
| 0x000000 | FUNC | GLOBAL | DEFAULT | main | .text |
| 0x000000 | NOTYPE | GLOBAL | DEFAULT | printf | UND |
| 0x000000 | NOTYPE | GLOBAL | DEFAULT | sleep | UND |

4.4 Hello.o的结果解析
1. 机器语言与汇编语言的映射关系
(1) 指令编码
示例:
hello.s (汇编) hello.o (机器码 + 反汇编)
------------------- ----------------------------------------
stp x29, x30, [sp, #-48]! → 0x0000: a9be7bfd (stp x29, x30, [sp, #-48]!)
机器码 a9be7bfd 对应 ARM64 的 STP 指令,操作数编码为:
a9:操作码前缀
be:偏移量(-48)和寄存器编号(x29/x30)
7bfd:栈指针操作标志
(2) 操作数差异
立即数编码:
hello.s 中的 #0x5 在机器码中被编码为指令的一部分(如 cmp w0, #0x5 → 7100141f)。
地址引用:
hello.s 使用标签(如 .LC0),而 hello.o 中地址被替换为 0x0 占位符,需重定位:
hello.s: adrp x0, .LC0 → hello.o: adrp x0, 0x0 (待重定位)
2. 分支与函数调用的关键差异
(1) 分支指令
hello.s:
b.eq .L2 ; 跳转到标签.L2
hello.o:
b.eq 0x30 ; 跳转到固定偏移量0x30(实际地址需重定位)
(2) 函数调用
hello.s:
bl printf ; 直接调用printf
hello.o:
bl 0x6a ; 临时占位地址(实际需链接时修正为printf的PLT条目)
-
- 重定位条目:R_AARCH64_CALL26 标记需修正的 bl 指令。
3. 与第3章 hello.s 的对照分析
| 特性 | hello.s (汇编源码) | hello.o (反汇编) | 说明 |
| 符号引用 | 使用标签(如 .LC0, printf) | 替换为 0x0 或自引用地址 | 链接器需重定位 |
| 分支偏移 | 基于标签的跳转(如 .L2) | 基于固定偏移量(如 b.eq 0x30) | 链接时计算实际偏移 |
| 函数调用 | 直接调用函数名(如 bl puts) | 使用临时地址(如 bl 0x24) | 依赖重定位表修正 |
| 立即数编码 | 直观数值(如 #0x5) | 嵌入指令机器码(如 7100141f) | 指令集规定的编码格式 |
4. 异常点分析
(1)自跳转问题:
hello.o: bl 0x24 <main+36> ; 错误的自调用(应为puts)
原因:反汇编时未解析重定位条目,显示为相对偏移。实际链接后会修正为 bl puts@PLT。
(2)地址占位符:
adrp x0, 0x0 和 add x0, x0, #0x0 应为字符串地址加载,链接时填充。

4.5 本章小结
本章系统讲解了从汇编代码到可重定位目标文件的转换过程。首先阐述了汇编阶段的核心作用,即将符号化的汇编指令转换为机器可执行的二进制代码,同时生成包含重定位信息的ELF格式文件。通过实际操作,我们使用汇编器将hello.s文件转换为hello.o目标文件,并借助readelf工具深入解析了该文件的组织结构。
在技术实现层面,本章重点探讨了以下内容:
- 使用readelf工具全面分析ELF文件结构,包括:
(1)文件头信息解析
(2)节区头表详细解读
(3)重定位条目分析
(4)符号表内容研究
- 通过对比hello.s汇编源码与hello.o目标文件,揭示了:
(1)符号引用到地址占位符的转换过程
(2)分支指令和函数调用的重定位需求
(3)机器码与汇编指令的精确对应关系
- 深入理解可重定位目标文件的关键特性:
(1)地址空间的临时占位机制
(2)外部符号的未决引用
(3)重定位信息的记录方式
通过本部分的学习,我对编译器后端的工作机制有了更深刻的认识,特别是掌握了以下关键知识点:
(1)汇编阶段在编译流程中的承上启下作用
(2)ELF文件格式中各数据结构的组织方式
(3)重定位信息对后续链接阶段的重要性
(4)从高级语言到机器指令的完整转换链条
这种从理论到实践的贯通理解,不仅加深了对计算机系统底层工作原理的认识,也为后续学习链接器和加载器的工作机制奠定了坚实基础。通过实际操作和分析,我对程序从源代码到可执行文件的完整生命周期有了更全面的把握。
第5章 链接
5.1 链接的概念与作用
5.1.1 连接的概念
链接是将多个可重定位目标文件(如hello.o)和库文件合并生成最终可执行程序的过程。这一阶段的核心任务包括符号解析(确保所有符号引用都能找到定义)和重定位(将目标文件中的地址引用修正为最终内存地址)。链接器通过合并代码段、数据段,并处理外部依赖关系,构建出完整的可执行文件,使其能够被操作系统加载执行。
5.1.2 连接的作用
链接的作用在于将分散编译的模块整合为统一的执行实体。它通过地址空间分配、符号决议和重定位计算,解决跨文件函数调用和全局变量访问的问题,同时将运行时所需的动态库依赖信息嵌入可执行文件。这一过程实现了从"分而治之"的模块化开发到"完整可运行"程序的转换,是程序构建的最后关键步骤。
5.2 在Ubuntu下链接的命令
连接命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
/usr/lib/x86_64-linux-gnu/start.o \
/usr/lib/x86_64-linux-gnu/crt.o \
hello.o -lc

5.3 可执行目标文件hello的格式
5.3.1 ELF头的分析
可执行目标文件hello与可重定位文件hello.o在ELF格式上存在几个关键区别。hello作为最终生成的可执行文件,其ELF头中的e_entry字段明确指定了程序入口地址,而hello.o中该字段保持为0,因为它尚需链接处理。hello还包含了程序头表(段头表),这个结构数组详细描述了程序加载时各段的内存布局信息,同时在.text节前新增了.init节,用于定义初始化函数。相比之下,hello.o保留了.rel.text和.rel.data等重定位节区,这些在hello中已不再需要,因为所有地址引用都已完成重定位。通过readelf工具查看可以发现,hello的ELF头明确标记为EXEC类型,表明其可执行属性,且包含的节区数量(30个)也显著多于hello.o,这反映了链接过程中对多个目标文件和库的整合结果。

5.3.2 节头表的分析
通过分析hello文件的节头表,可以清晰地看到30个节区的完整信息,每个节区都详细记录了其在文件中的存储位置和内存映射关系。Size字段精确表明了各节区的大小,Offset则指出其在文件中的物理偏移位置,而Address字段尤为重要,它定义了该节区被加载到虚拟地址空间时的起始地址。这些信息共同构成了程序运行时的内存布局蓝图,例如.text节的Address指向代码段的起始虚址,.data节标记了初始化数据的加载位置。与可重定位文件相比,这些地址值都已完成了最终的重定位计算,不再保留中间过程的临时占位符,确保了程序加载时各段能准确映射到指定的虚拟内存区域。


5.3.3 程序头表的分析
通过分析hello的程序头表可以看到,这个可执行文件包含12个段表项,其中4个是LOAD类型的可加载段。这些段的虚拟地址(VirtAddr)和物理地址(PhysAddr)完全一致,都采用4KB对齐方式(Align=0x1000)。以第一个可加载段为例,它对应文件中0x00000到0x005f0的区间,将被映射到虚拟地址空间0x400000起始的位置,占用0x5f0字节空间。这个段设置了只读权限(Flags=R),主要包含程序的代码段。这种精细的内存布局设计确保了程序运行时各段能正确加载到指定位置,同时通过权限控制实现了代码和数据的保护机制。

5.3.4 重定位节的分析
重定位表展示了hello可执行文件中的动态链接信息,主要包含两类重定位条目:'.rela.dyn'和'.rela.plt'。在'.rela.dyn'段中,R_AARCH64_RELATIV类型占主导(6/10),用于处理相对地址重定位,如代码中的位置无关地址计算;而R_AARCH64_GLOB_DAT类型的4个条目则对应全局符号(如_ITM_deregisterTMCloneTable)的绝对地址解析。'.rela.plt'段则专门处理过程链接表(PLT)相关的跳转,所有10个条目均为R_AARCH64_JUMP_SL类型,涉及关键库函数如printf、exit、sleep等的动态绑定。值得注意的是,这些重定位条目都标记为GLIBC_2.17版本,表明程序依赖于特定版本的C库实现。Offset字段精确指出了需要修正的地址位置,为动态链接器提供了关键的修正信息。

5.3.5 符号表的分析
符号表展示了hello可执行文件的动态链接符号('.dynsym')和完整符号('.symtab')信息。动态符号表中包含15个条目,其中大部分(9/15)是未定义的全局函数(UND),包括关键库函数如printf、exit、sleep等,均标记为GLIBC_2.17版本依赖。值得注意的是,这些外部符号的Value值均为0,需通过动态链接器在加载时解析。完整符号表则包含98个条目,详细记录了各节区的起始地址(如.text节0x238)和局部符号信息。对比可见,动态符号表专注于外部依赖,而完整符号表则包含了更丰富的调试和链接信息,为程序运行和调试提供了全面的符号上下文。特别地,_ITM_deregisterTMCloneTable等弱符号(WEAK)的存在表明程序支持事务内存等高级特性。

5.4 hello的虚拟地址空间
使用gdb加载hello,指令为:gdb ./hello
5.4.1 查看进程虚拟地址空间布局

5.4.2 查看.inerp段的信息
用readelf -S hello | grep .interp命令得到.inerp的地址: [ 1] .interp PROGBITS 0000000000000238 00000238,从而在gdb中实现查看.inerp段的信息

5.4.3 查看.text段的信息
步骤同5.4.2

5.4.4 查看.rodata段的信息
步骤同5.4.2

5.5 链接的重定位过程分析
反汇编截图如下:

5.5.1.主要区别
- 地址不同:
(1)hello.o中的地址是相对地址(从0开始)
(2)hello中的地址是绝对地址(如0x00000000000000c)
- 外部引用处理:
(1)hello.o中对puts、exit、printf、atoi、sleep、getchar的调用目标地址未确定(调用自身地址)
(1)hello中这些调用已被解析为实际地址(如puts@plt在0x790)
- 重定位项目:
(1)hello.o中的字符串地址(如adrp x0, 0x0)未确定
(2)hello中字符串地址已被解析(如add x0, x0, #0xa18)
5.5.2.链接过程分析
链接器执行了以下主要操作:
- 地址分配:为所有节区分配最终的内存地址
- 符号解析:解析所有未定义的符号引用
- 重定位:修改代码和数据以反映新的地址
5.5.3重定位项目分析
从hello.o中可以看到多处需要重定位的地方:
- 函数调用:
(1)bl 0x24 <main+36> → 在hello中变为bl 0x790 <puts@plt>
(2)bl 0x78 <main+120> → 在hello中变为bl 0x740 <atoi@plt>
- 数据引用:
(1)adrp x0, 0x0 → 在hello中变为adrp x0, 0x0(但后面有add x0, x0, #0xa18)
(2)字符串地址从相对0变为绝对地址
- 分支指令:
(1)b.eq 0x30 <main+48> → 在hello中变为b.eq 0x8fc <main+48>
(2)b.le 0x38 <main+56> → 在hello中变为b.le 0x904 <main+56>
链接器通过重定位表(.rela.text等节区)知道哪些位置需要修改,并根据符号表确定最终地址,然后相应地修改指令或数据。
5.6 hello的执行流程
5.6.1 程序加载阶段
_start (通常由内核加载器直接跳转到这里)
├─> __libc_start_main (libc的入口函数)
├─> __libc_csu_init (全局构造器初始化)
│ ├─> _init (程序初始化)
│ │ ├─> frame_dummy (帧处理)
│ │ └─> __do_global_ctors_aux (全局构造函数)
├─> main (我们的主程序)
├─> __libc_csu_fini (全局析构器清理)
│ ├─> _fini (程序终止处理)
│ │ └─> __do_global_dtors_aux (全局析构函数)
└─> exit (程序退出)
5.6.2 具体函数调用序列
- 加载器阶段:
_start (默认入口点,地址通常如0x400000)
_dl_start (动态链接器入口)
_dl_init (动态链接初始化)
- libc初始化:
__libc_start_main (libc主入口)
__libc_csu_init (全局构造器)
_init (程序初始化)
frame_dummy (栈帧初始化)
__do_global_ctors_aux (调用全局构造函数)
- 主程序执行:
main (用户程序入口)
puts@plt (如果条件不满足)
exit@plt (如果条件不满足)
printf@plt (打印消息)
atoi@plt (字符串转整数)
sleep@plt (睡眠函数)
getchar@plt (等待输入)
- 程序终止:
__libc_csu_fini (全局析构器)
fini (程序终止处理)
__do_global_dtors_aux (调用全局析构函数)
exit (程序退出)
_dl_fini (动态链接器清理)
_exit (系统调用退出)
5.6.3 关键地址示例
| 函数名 | 说明 | |
| 0x0000555555555050 | _start | 程序入口点,由内核加载后跳转到这里 |
| 0x00007ffff7df5a90 | __libc_start_main | glibc 的主入口函数,负责初始化并调用 main |
| 0x00005555555550e0 | _init | 程序初始化阶段,调用全局构造函数 |
| 0x0000555555555130 | __libc_csu_init | 全局构造器初始化(C++ 全局对象、静态变量等) |
| 0x00005555555551b0 | main | 用户程序的 main 函数 |
| 0x0000555555555060 | puts@plt | 调用动态链接的 puts 函数(通过 PLT/GOT 机制) |
| 0x0000555555555090 | printf@plt | 调用动态链接的 printf 函数 |
| 0x0000555555555070 | atoi@plt | 调用动态链接的 atoi 函数 |
| 0x0000555555555080 | sleep@plt | 调用动态链接的 sleep 函数 |
| 0x00005555555550a0 | getchar@plt | 调用动态链接的 getchar 函数 |
| 0x0000555555555140 | __libc_csu_fini | 全局析构器清理(C++ 全局对象析构等) |
| 0x0000555555555110 | _fini | 程序终止处理阶段,调用全局析构函数 |
| 0x00007ffff7e05ec0 | exit | 程序退出,最终调用 _exit 系统调用 |
5.7 Hello的动态链接分析
程序调用共享库函数时,由于函数地址在编译时未知,系统采用延迟绑定技术,通过PLT和GOT实现动态链接。PLT是16字节代码数组,PLT[0]跳转到动态链接器,其他条目对应具体函数。GOT存储8字节地址,前三个条目供链接器使用,其余对应需解析的函数。
首次调用函数时,控制流经过PLT条目跳转到GOT,初始时GOT指向PLT的第二条指令,随后压入函数ID并跳转到PLT[0],最终通过GOT[1]和GOT[2]进入动态链接器完成地址解析。链接器确定函数地址后更新GOT,后续调用直接跳转到目标函数。
hello程序在动态链接器加载前仅包含未解析的PLT/GOT模板,加载后完成重定位,GOT被更新为实际函数地址。这种机制实现了高效的运行时绑定,避免了启动时的全部解析开销。



5.8 本章小结
链接是将多个目标文件合并生成可执行文件的关键过程。在Ubuntu系统中,可以通过ld命令完成链接操作。本章以hello程序为例,详细分析了链接的各个环节。首先探讨了可执行文件的ELF格式结构,使用edb调试工具查看了程序的虚拟地址空间布局和关键节区内容。随后通过重定位条目的分析,深入研究了地址重定位的具体实现过程。借助edb的单步调试功能,完整追踪了程序中各个子函数的执行流程和控制转移。最后利用edb的内存查看功能,重点分析了动态链接的实现机制,包括PLT和GOT的工作过程。经过链接处理后,hello.o与其依赖的所有库函数被整合成一个完整的可执行文件,其中所有代码和数据的运行时地址都已确定,可以直接加载到内存中执行。这个过程不仅完成了符号解析和地址重定位,还实现了高效的动态链接机制,确保程序能够正确调用共享库中的函数。通过本章的分析,可以清晰地理解链接器如何将分散编译的模块整合为可执行的程序映像。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是操作系统进行资源分配和调度的基本单位,它代表一个正在执行的程序实例。每个进程都拥有独立的地址空间、代码段、数据段以及系统资源,如打开的文件描述符和信号处理器等,进程之间通过进程间通信(IPC)机制进行数据交换和同步。操作系统通过进程控制块(PCB)来维护进程的状态信息,包括进程ID、优先级、寄存器状态等,从而实现多任务环境下进程的创建、切换和终止等管理功能。
6.1.2 进程的作用
进程的作用主要体现在隔离性、并发性和资源共享三个方面。隔离性确保每个进程运行在受保护的内存空间中,避免相互干扰;并发性使得多个进程可以交替执行,提高系统资源利用率;而通过进程间通信机制,不同进程又能安全地共享数据和资源。操作系统通过进程调度算法合理分配CPU时间片,使多个进程能够高效并发执行,同时提供进程同步机制来协调它们对共享资源的访问,从而构建出稳定可靠的计算环境。
6.2 简述壳Shell-bash的作用与处理流程
Shell(如Bash)是用户与操作系统内核之间的命令解释器,主要作用包括:
1)接收用户输入的命令并解释执行;
2)提供脚本编程环境实现自动化任务;
3)管理进程和作业控制;
4)处理输入输出重定向和管道
它作为用户界面,将人类可读的指令转换为系统调用,同时提供变量、循环等编程功能扩展交互能力。
处理流程分为四个阶段:
1)读取命令(从终端或脚本获取输入);
2)解析命令(分割令牌、处理特殊字符如通配符*);
3)执行命令(内置命令直接处理,外部命令通过fork-exec创建子进程);
4)反馈结果(显示输出或错误)
对于管道操作,会串联多个进程的输入输出;遇到脚本时则逐行解释执行,期间会进行变量替换、流程控制等处理,形成完整的自动化工作流。
6.3 Hello的fork进程创建过程
当hello程序执行fork()系统调用时,会触发以下典型流程:
- 系统调用触发
CPU执行fork()汇编指令(如x86的int 0x80或ARM的svc)
处理器切换到内核态,跳转到内核预设的系统调用入口
- 内核进程复制
内核调用do_fork()(Linux 5.x+为kernel_clone())
创建新的task_struct结构体,复制父进程的:
-
-
- 虚拟内存空间(通过COW机制)
- 文件描述符表
- 信号处理表
-
分配新的PID和内核栈
- 用户态资源准备
对父子进程区分处理:
父进程:fork()返回子进程PID
子进程:
返回0
重置未继承的资源(如pending signals)
保持相同的程序计数器位置
关键特性:
写时复制(COW):物理内存页仅在写入时复制,优化性能
资源继承:子进程继承父进程的:
环境变量
打开文件描述符(包括套接字)
工作目录
执行独立性:子进程后续可通过exec系列调用加载新程序映像
调试观察方法:
- 使用strace -f ./hello跟踪系统调用
- GDB设置catch fork断点
- 通过/proc/[pid]/maps对比父子进程内存映射
6.4 Hello的execve过程
当hello程序通过execve()加载新程序时,会经历以下关键步骤:
6.4.1. 系统调用触发
(1)用户态调用execve("./hello", argv, envp),触发软中断(如int 0x80或syscall)进入内核态。
(2)内核验证可执行文件的权限、路径有效性及ELF格式。
6.4.2. 内存空间替换
(1)释放旧资源:内核销毁当前进程的代码段、数据段、堆栈等用户空间内存映射(但保留PID、文件描述符等属性)。
(2)加载新程序:
解析hello的ELF头部,建立代码段(.text)、数据段(.data/.bss)的内存映射。
动态链接器(如ld-linux.so)处理依赖库,完成GOT/PLT重定位(若存在动态链接)。
6.4.3. 执行环境初始化
(1)寄存器重置:将程序计数器(PC/IP)指向hello的入口地址(_start或ELF头指定的e_entry)。
(2)堆栈重构:
压入命令行参数(argv)和环境变量(envp)。
栈顶布局符合ABI规范(如x86-64的argc|argv|envp序列)。
6.4.4. 用户态执行转移
(1)内核返回用户态后,CPU从hello的入口点开始执行,动态链接器(若需)优先运行,完成库函数地址解析。
(2)最终跳转到main()函数,hello程序正式运行。
-
- Hello的进程执行
6.5.1进程上下文
用户态上下文:寄存器(PC/SP等)、栈、代码段(.text)、数据段(.data/.bss)。
内核态上下文:进程控制块(PCB)保存线程信息、调度参数、文件描述符表等。
6.5.2时间片调度
CPU为Hello分配时间片(如10ms),时钟中断触发时:
-
-
- 若时间片耗尽,内核调用调度器(如CFS)选择新进程。
- 否则继续执行Hello,直到主动放弃CPU(如I/O阻塞)。
-
6.5.3用户态↔内核态转换
系统调用(如write()):
-
-
- Hello触发syscall指令,保存用户态寄存器。
- CPU切换到内核态,执行内核服务例程。
- 返回用户态前恢复寄存器,继续执行Hello。
-
中断/异常:时钟中断或缺页异常强制进入内核态处理。
6.5.4进程切换流程
Hello运行 → 时间片耗尽/阻塞 → 保存上下文 → 调度器选新进程 → 加载新上下文 → 执行新进程

进程切换示意图
6.6 hello的异常与信号处理
6.6.1 异常的分类
异常可分为以下四类:
①中断:异步事件。hello程序执行时,处理器外部I/O设备通过芯片引脚发送信号触发。设备将异常号置于系统总线,处理器在当前指令完成后检测到电压变化,读取异常号并调用对应中断处理程序。处理完成后继续执行下条指令,整个过程对程序透明。
②陷阱:同步异常。hello程序主动触发,如执行sleep()系统调用时。处理器执行特定指令产生陷阱,转入内核态处理请求,完成后返回到陷阱指令的下一条指令继续执行。
③故障:潜在可恢复错误。hello运行中可能出现缺页故障,处理器检测到错误后转交故障处理程序。若处理成功则重新执行故障指令,否则终止程序。
④终止:不可恢复错误。通常由硬件故障引发,如内存位损坏导致的奇偶错误。处理器直接终止hello程序运行,不提供恢复机会。
6.6.2 运行结果
(1)正常运行

(2)输入任意字符串

运行时输入任意字符串,只是被缓存到缓冲区,待进程结束后又作为命令行进行输入。
(3)运行时输入回车:

getchar读回车,回车前的字符串会被当作shell输入的命令
(4)运行时输入Ctrl+C

Ctrl+C使用SIGINT信号终止了前台进程
(5)运行时输入Ctrl+Z

Ctrl+Z使用SIGTSTP信号终止了前台作业。
(6)Ctrl-Z后运行ps、jobs、pstree、fg、kill等命令





当输入Ctrl+Z时,hello进程收到SIGTSTP信号,该信号会将进程挂起至后台。
通过ps命令可以查看到hello进程仍然存在,其进程号为2312,状态显示为"T"(停止状态)。使用jobs命令可以看到该作业的ID为1,状态标记为"stopped"。
输入pstree命令后,系统会以树状结构展示所有进程的层级关系。此时执行fg命令,停止的进程将收到SIGCONT信号,重新回到前台继续运行。
最后使用kill -9 2380命令,向指定进程发送SIGKILL信号(9号信号),该信号会强制终止目标进程的执行。
6.7本章小结
本章系统讲解了进程管理的核心机制,重点分析了shell-bash的工作流程,并以hello程序为例深入剖析了进程控制的关键系统调用。通过实验演示了fork创建子进程、execve加载程序映像的具体过程,详细追踪了进程执行中的上下文切换和用户态/内核态转换。
在异常处理部分,实验对hello进程进行了多种信号操作测试(如SIGTSTP挂起、SIGCONT恢复、SIGKILL终止等),结合ps、jobs、pstree等工具观察进程状态变化。通过将CSAPP第八章的异常控制流理论与实际操作相结合,使我对进程调度、信号处理和异常响应机制有了更直观的认识,特别是理解了内核如何通过中断上下文保存/恢复来实现透明的进程管理。
这些实践不仅验证了教材中的关键概念,还培养了对进程生命周期管理的系统性思维,为后续学习操作系统进程调度算法打下了坚实基础。
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址(Logical Address)
(1)在hello的汇编代码中(如mov 0x8048000, %eax),0x8048000就是逻辑地址
(2)这是CPU指令直接使用的地址形式,由段选择符和偏移量组成
(3)在hello的gdb调试中,disassemble main显示的地址就是逻辑地址空间
- 线性地址(Linear Address)
(1)当hello程序访问全局变量时,编译器生成的地址会经过段式管理转换
(2)在x86架构中,通过GDT(全局描述符表)将逻辑地址转换为线性地址
(3)例如:hello中printf函数的逻辑地址0x804841d经过段式转换后,线性地址可能仍为0x804841d(现代系统通常平坦模式)
- 虚拟地址(Virtual Address)
(1)hello进程运行时,/proc/[pid]/maps中显示的地址范围(如0x8048000-0x8050000)
(2)这是经过页表转换前的地址,在Linux中通常等同于线性地址
(3)当hello调用malloc时返回的指针就是虚拟地址(如0x9a13000)
- 物理地址(Physical Address)
(1)hello进程的.text段最终被加载到物理内存的某个页面(如0x35a7000)
(2)通过sudo cat /proc/[pid]/pagemap可以查询虚拟到物理地址的映射
(3)例如:hello的入口地址0x8048000可能映射到物理地址0x35a7000
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.2.1 基本概念
在Intel x86架构中,段式管理是将逻辑地址转换为线性地址的关键机制。这种转换过程涉及以下几个关键组件:
- 逻辑地址:由段选择符(16位)和偏移量(32位)组成
- 段描述符:描述段的属性、基址和界限
- 段寄存器:存储段选择符(CS, DS, SS, ES, FS, GS)
- 全局描述符表(GDT)和局部描述符表(LDT):存储段描述符的系统表
7.2.2 转换过程
逻辑地址到线性地址的转换步骤如下:
- 从段寄存器(如CS)中获取段选择符
- 使用段选择符中的TI位决定查询GDT(0)还是LDT(1)
- 从GDTR或LDTR寄存器中获取GDT/LDT的基地址
- 使用段选择符中的索引值在GDT/LDT中找到对应的段描述符
- 检查段描述符的有效性和访问权限
- 将段描述符中的基地址与逻辑地址中的偏移量相加,得到线性地址
7.2.3保护模式下的段式管理
在保护模式下,段式管理提供了内存保护和多任务支持:
每个段有基址、界限和访问权限
特权级检查确保系统安全
不同的任务可以使用不同的LDT实现内存隔离
段式管理是现代x86架构的重要组成部分,虽然分页机制更为常用,但段式管理仍然在保护模式操作系统中发挥着基础性作用。

7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1基本概念
在Intel x86架构中,页式管理是将线性地址转换为物理地址的关键机制。这种转换过程涉及多级页表结构和MMU(内存管理单元)的硬件支持。
7.3.2转换过程(以32位系统为例)
基本转换步骤:
- 获取页目录基址:从CR3寄存器获取页目录的物理基地址
- 解析线性地址:将32位线性地址分为三部分:
页目录索引(10位)
页表索引(10位)
页内偏移(12位)
- 查找页目录项:
使用页目录索引在页目录中找到页表基址
- 查找页表项:
使用页表索引在页表中找到物理页框基址
- 组合物理地址:
将物理页框基址与页内偏移组合得到物理地址
7.3.3扩展机制
1. 物理地址扩展(PAE)
允许32位系统访问超过4GB物理内存:
页目录项和页表项扩展为64位
增加一层页目录指针表(4项)
CR3指向PDPT(页目录指针表)
2. 64位分页模式
使用4级或5级页表结构
支持48位或57位虚拟地址空间
每级页表使用9位索引
7.3.4页式管理特点
- 内存保护:通过页表项中的权限位实现
- 页面共享:多个线性地址可映射到同一物理页
- 按需分页:通过"存在位"实现虚拟内存
- TLB加速:转换后备缓冲器缓存常用地址映射

虚拟页面分配示意图
页式管理是现代操作系统内存管理的核心机制,与段式管理共同构成了x86架构完整的内存管理方案。
7.4 TLB与四级页表支持下的VA到PA的变换
现代处理器通过页表机制完成虚拟地址到物理地址的转换,这一过程涉及多个关键组件和步骤。页表本质上是一个由页表条目(PTE)组成的数组,每个PTE都包含有效位和地址字段。有效位表示该虚拟页是否已缓存在物理内存中,地址字段则存储对应的物理页信息。虚拟地址被划分为虚拟页号(VPN)和页内偏移量(VPO)两部分,其中VPN用于在页表中查找对应的PTE,而VPO则直接对应物理地址中的偏移量。
为了提高地址转换效率,现代处理器都配备了TLB(转换后备缓冲器)。TLB是MMU中的一个专用高速缓存,用于存储最近使用过的PTE。当CPU需要转换地址时,首先查询TLB,如果命中就能立即获得物理地址。这种设计大幅提升了地址转换速度,因为TLB的访问速度远快于访问内存中的页表。当TLB未命中时,处理器就需要通过多级页表结构来查找PTE。
多级页表是现代处理器常用的页表组织形式,比如Intel Core i7采用四级页表结构。36位的虚拟页号被分成四个9位的字段,分别用于索引各级页表。CR3寄存器存储着第一级页表的物理地址,通过逐级查询最终在第四级页表中找到对应的PTE。这种分级结构虽然增加了查询步骤,但能显著节省内存空间,因为只需要为实际使用的虚拟地址范围分配页表空间。
当PTE的有效位为0时,表示对应的页面不在物理内存中,这时会触发缺页异常。操作系统需要介入处理,将所需的页面从磁盘调入内存,并更新页表。这个过程完成后,原先引发缺页的指令会被重新执行。整个地址转换机制通过TLB和多级页表的配合,既保证了转换效率,又实现了对大规模内存的高效管理。

页命中/页不命中流程示意图
7.5 三级Cache支持下的物理内存访问
在现代计算机体系结构中,处理器通过三级缓存(L1、L2、L3)的层级结构来加速对物理内存的访问,这一机制与虚拟内存管理紧密结合,共同构成了高效的内存访问体系。当CPU需要访问某个内存地址时,首先会进行虚拟地址到物理地址的转换,这个转换过程涉及TLB和多级页表。转换完成后得到的物理地址会进入缓存查询流程:处理器首先检查L1缓存,若未命中则继续查询L2缓存,仍未命中则查询L3缓存,最后才会访问主内存。
这种多级缓存架构的设计遵循了访问速度与容量的平衡原则:L1缓存最小但速度最快,通常只需3-5个时钟周期;L2缓存容量较大但速度稍慢,约需10个时钟周期;L3缓存容量最大但速度更慢,可能需要30-50个时钟周期。当所有缓存都未命中时,访问主内存可能需要数百个时钟周期。缓存系统采用各种优化策略,如预取机制会根据程序访问模式预先将可能用到的数据加载到缓存中,写回机制则延迟对主内存的写入操作,这些技术共同提高了内存访问的整体效率。
缓存与虚拟内存系统的协同工作体现在多个方面:TLB缓存了虚拟到物理地址的转换结果,而数据缓存则存储了物理内存内容。当发生上下文切换时,处理器可能需要刷新TLB,但缓存内容通常可以保留。现代处理器还采用了物理地址标记(PIPT)的缓存组织方式,这种方式虽然实现复杂,但可以避免虚拟地址别名问题。在多核处理器中,L3缓存通常由所有核心共享,这既提高了缓存利用率,也带来了缓存一致性的挑战,需要通过MESI等协议来维护各个核心缓存间的数据一致性。

三级缓存与主存层次示意图
7.6 hello进程fork时的内存映射
当进程调用 `fork()` 创建子进程时,内核并不会立即复制父进程的物理内存,而是采用写时复制(Copy-On-Write)机制来优化性能。子进程首先获得与父进程完全相同的虚拟内存映射,包括代码段、数据段、堆和栈等,但所有这些内存页都被标记为只读。此时父子进程实际上共享相同的物理内存页,只有当其中任一方尝试写入这些共享页时,才会触发缺页异常,内核这时才会真正复制该内存页,使得父子进程各自拥有独立的副本。
对于不同的内存区域,处理方式也有所不同。普通的堆栈等匿名内存采用标准的COW机制,而通过`mmap`映射的共享内存区域则会继续保持共享,即使发生写入也不会触发复制。文件映射则取决于映射方式:`MAP_PRIVATE`方式的映射会像普通内存一样采用COW机制,而`MAP_SHARED`方式的映射则始终保持共享,任何修改都会立即反映到文件中。
这种机制使得`fork()`系统调用非常高效,因为大多数情况下只需要复制页表而无需立即复制内存内容。只有在确实需要写入时才会进行实际的物理内存复制,这特别适合那些`fork()`后立即调用`exec()`的场景。更极端的优化是`vfork()`,它连页表都不复制,直接让子进程共享父进程的整个地址空间,但会阻塞父进程直到子进程执行完毕,专门为`fork()+exec()`组合优化。
7.7 hello进程execve时的内存映射
当进程调用 `execve()` 执行新程序时,内核会彻底重构该进程的内存映射。首先,原有的所有内存映射(包括代码段、数据段、堆、栈以及各种动态库映射)都会被完全清除。接着,内核会为新程序建立全新的内存布局:将可执行文件的代码段和数据段映射到进程地址空间,同时设置新的堆栈区域。动态链接器会被映射到内存中,负责后续的共享库加载工作。
这个过程中,内核采用了高效的内存映射技术。对于可执行文件的代码段,通常使用私有只读映射(MAP_PRIVATE),而数据段则使用私有可写映射。共享库则通过文件映射方式加载,其中代码段使用共享只读映射(MAP_SHARED),数据段使用私有可写映射。所有新映射的页面初始时都标记为未加载状态,采用按需分页机制,只有当程序实际访问时才会从磁盘读入内存。
特别值得注意的是,`execve()` 会保留原进程的某些属性,如进程ID、文件描述符(除非设置了FD_CLOEXEC标志)以及信号处理设置等。但整个地址空间被完全重置,所有原先的内存内容都会丢失。这个设计确保了新程序能在干净的环境中启动,同时避免了不必要的内存拷贝操作,提高了程序加载的效率。
7.8 缺页故障与缺页中断处理
当进程访问的虚拟页面尚未加载到物理内存时,会触发缺页故障(Page Fault)。CPU捕获到这个异常后,会暂停当前指令的执行,保存现场信息,并跳转到操作系统预设的缺页中断处理程序。处理程序首先检查缺页地址的合法性,若属于进程的合法地址空间,则进入正式的缺页处理流程。
内核会根据页表项中的信息判断缺页原因:可能是访问了未分配的页面、被换出到磁盘的页面,或是写保护触发的写时复制。对于需要从磁盘加载的情况,处理程序会选择一个物理页帧,发起磁盘I/O读取页面内容。如果是写时复制场景,则会复制原页面内容到新分配的页帧。在此期间,当前进程会被挂起,调度器选择其他就绪进程运行。
当I/O完成后,内核更新页表项,将虚拟页面映射到新的物理页帧,并标记为有效。最后恢复进程的执行,重新执行触发缺页的指令。整个过程对应用程序完全透明,是现代操作系统实现虚拟内存和按需分页的关键机制。通过这种机制,系统可以更高效地利用物理内存,支持运行比实际内存大得多的应用程序。

缺页异常处理流程图
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)
7.10本章小结
本章详细探讨了Hello程序与操作系统的交互机制,重点剖析了内存管理的核心原理。在存储器地址空间方面,Hello程序运行在操作系统提供的虚拟地址环境中,通过段式管理和页式管理的协同工作实现地址转换。Intel处理器的段式管理机制首先将逻辑地址转换为线性地址,随后页式管理机制通过多级页表结构完成线性地址到物理地址的最终转换,这个过程中TLB缓存显著提升了地址翻译效率。
在内存访问优化方面,现代处理器采用的多级缓存架构(L1/L2/L3)有效缓解了CPU与主存之间的速度差异。动态内存管理机制则通过malloc/free等接口为Hello程序提供灵活的内存分配能力,底层依赖操作系统的brk和mmap系统调用来扩展进程的堆空间。特别值得注意的是,动态存储分配管理器使用隐式或显式空闲链表等数据结构来跟踪内存块状态,在分配和释放时执行分割与合并操作以优化内存利用率。这些机制共同构成了Hello程序运行时的内存访问基础,确保其能够高效安全地使用系统内存资源。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux采用统一的设备文件抽象来管理I/O设备,所有设备都被抽象为特殊的文件节点存放在/dev目录下,遵循"一切皆文件"的设计哲学。内核通过设备驱动程序与硬件交互,驱动程序分为字符设备、块设备和网络设备三大类,分别对应不同的访问方式:字符设备提供字节流式访问(如键盘、串口),块设备支持随机访问并按块操作(如磁盘),网络设备则通过套接字接口访问。内核使用设备号(主次设备号)唯一标识每个设备,并通过虚拟文件系统(VFS)层为应用程序提供统一的open()、read()、write()等系统调用接口,同时采用缓冲机制、DMA传输和中断处理来优化I/O性能,对于块设备还实现了复杂的I/O调度算法(如CFQ、deadline等)来优化磁盘访问顺序,提高吞吐量。
8.2 简述Unix IO接口及其函数
Unix系统将所有I/O设备抽象为文件,提供统一的接口进行操作。最基本的I/O函数包括open()、read()、write()和close(),它们构成了文件操作的核心。open()用于打开或创建文件,返回文件描述符;read()和write()分别用于读写数据;close()则关闭文件释放资源。这些函数都是系统调用,通过文件描述符这个整数值来标识打开的文件。
除了基本操作外,Unix还提供了更高级的I/O控制函数。
lseek()可以改变文件偏移量,实现随机访问;
ioctl()用于设备特定的控制操作;
fcntl()提供了文件控制功能,如修改文件状态标志。
对于非阻塞I/O,可以通过fcntl()设置O_NONBLOCK标志,或者使用select()、poll()和epoll()等I/O多路复用机制来同时监控多个文件描述符的就绪状态。
Unix I/O接口的设计遵循"一切皆文件"的哲学,使得应用程序可以用相同的方式处理磁盘文件、管道、套接字和设备文件等不同类型的I/O对象。这种统一性简化了编程模型,同时保持了足够的灵活性来支持各种特殊的I/O需求。底层实现上,内核通过虚拟文件系统(VFS)层将这些操作路由到相应的设备驱动程序。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
结论
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 2023111843 麻皓然 13144566863 3 并回车。
6、创建进程:终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。
7、加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入内存,然后进入main函数。
8、执行指令:CPU为进程分配时间片,在一个时间片中,hello有CPU资源,可以顺序执行自己的控制逻辑流。
9、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。
10、信号管理:程序运行时输入Ctrl+C,内核会发送SIGINT信号给进程并终止前台作业。输入Ctrl+Z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
Hello程序的生命周期完美诠释了计算机系统精妙的层次化设计。从源代码到可执行程序,它经历了预处理器的宏展开、编译器的语法分析、汇编器的指令转换,最终由链接器完成模块整合。当我们在终端输入执行命令时,shell通过fork-exec机制为其创建独立的进程空间,加载器精心构建虚拟内存布局,CPU则通过时间片轮转为其分配计算资源。
在运行过程中,内存管理单元动态翻译虚拟地址,页表机制和TLB缓存协同工作;当用户触发控制信号时,内核精确捕获并处理中断事件;程序终止后,系统又严谨地回收所有资源。这一系列操作在毫秒级时间内完成,背后却是硬件架构、操作系统、编译工具链数十年演进的智慧结晶。
现代编程环境的高度抽象让"Hello World"变得简单,但这行简短代码所触及的计算机体系深度令人敬畏。从冯·诺依曼架构的奠基,到Unix哲学的形成,再到RISC指令集的革新,每一层抽象都凝结着工程师们对效率与优雅的不懈追求。这提醒我们:在计算机领域,真正的精通既要能享受高级语言的便利,也要理解底层系统的精妙,方能在技术演进中继往开来。
附件
hello.i: hello.c经预处理得到的文本文件
hello.s: hello.i 经编译程序的汇编文件
hello.o: hello.s经得到的可重定位目标文件
hello: hello.o经链接得到的可执行目标文件
参考文献
[1]冯依嘉,王雷,孟丽平.在Windows上使用虚拟机安装Linux操作系统[J].电脑编程 技巧与维护,2023,(04):60-63+73.DOI:10.16184/j.cnki.comprg.2023.04.037.
[2] 赵 明立.虚拟化项目中 Linux 虚拟机置备技巧[J].网络安全和信息 化,2023,(01):162-164.
[3] Linux 常用命令全拼 | 菜鸟教程 Linux常用命令
[4] http://docs.huihoo.com/c/linux-c-programming/ C汇编Linux手册
[5] http://csapp.cs.cmu.edu/3e/labs.html CMU的实验参考

1104

被折叠的 条评论
为什么被折叠?



