摘 要
本报告以经典的"Hello"程序为研究载体,系统剖析了计算机系统中程序从源代码到进程执行的完整生命周期。通过构建完整的实验环境,运用GCC编译器套件、GDB调试器、readelf和objdump等分析工具,逐步追踪了hello.c程序经历的预处理、编译、汇编、链接、加载和执行等关键阶段。在预处理阶段,头文件被展开形成完整的翻译单元;编译阶段将高级语言转换为优化的汇编指令;汇编阶段生成包含重定位信息的可重定位目标文件;链接阶段整合多模块解决符号引用生成可执行文件。进程管理章节探讨了Shell的解析机制、fork和execve系统调用、进程调度以及信号处理。存储管理部分深入分析了地址空间转换机制,包括段页式管理、TLB与缓存体系、内存映射和缺页处理。IO管理章节揭示了Unix IO接口的实现原理,特别是printf和getchar函数的完整执行路径。本报告不仅验证了计算机系统原理的核心概念,还通过实际操作加深了对系统各层次协同工作机制的理解。
关键词: 计算机系统;程序构建;进程管理;存储管理;IO管理
目 录
第1章[概述 - 5 -](#概述 - 5 -)
[1.1 Hello](#1.1 Hello)[简介 - 5 -](#简介 - 5 -)
1.2[环境与工具 - 5 -](#环境与工具 - 5 -)
1.3[中间结果 - 5 -](#中间结果 - 5 -)
1.4[本章小结 - 5 -](#本章小结 - 5 -)
2.1[预处理的概念与作用 - 6 -](#预处理的概念与作用 - 6 -)
2.2[在Ubuntu下预处理的命令 - 6 -](#在Ubuntu下预处理的命令 - 6 -)
[2.3 Hello](#2.3 Hello)[的预处理结果解析 - 6 -](#的预处理结果解析 - 6 -)
2.4[本章小结 - 6 -](#本章小结 - 6 -)
3.1[编译的概念与作用 - 7 -](#编译的概念与作用 - 7 -)
3.2[在Ubuntu下编译的命令 - 7 -](#在Ubuntu下编译的命令 - 7 -)
[3.3 Hello](#3.3 Hello)[的编译结果解析 - 7 -](#的编译结果解析 - 7 -)
3.4[本章小结 - 7 -](#本章小结 - 7 -)
4.1[汇编的概念与作用 - 8 -](#汇编的概念与作用 - 8 -)
4.2[在Ubuntu下汇编的命令 - 8 -](#在Ubuntu下汇编的命令 - 8 -)
4.3[可重定位目标elf格式 - 8 -](#可重定位目标elf格式 - 8 -)
[4.4 Hello.o](#4.4 Hello.o)[的结果解析 - 8 -](#的结果解析 - 8 -)
4.5[本章小结 - 8 -](#本章小结 - 8 -)
5.1[链接的概念与作用 - 9 -](#链接的概念与作用 - 9 -)
5.2[在Ubuntu下链接的命令 - 9 -](#在Ubuntu下链接的命令 - 9 -)
5.3[可执行目标文件hello的格式 - 9 -](#可执行目标文件hello的格式 - 9 -)
[5.4 hello](#5.4 hello)[的虚拟地址空间 - 9 -](#的虚拟地址空间 - 9 -)
5.5[链接的重定位过程分析 - 9 -](#链接的重定位过程分析 - 9 -)
[5.6 hello](#5.6 hello)[的执行流程 - 9 -](#的执行流程 - 9 -)
[5.7 Hello](#5.7 Hello)[的动态链接分析 - 9 -](#的动态链接分析 - 9 -)
5.8[本章小结 - 10 -](#本章小结 - 10 -)
第 6 章hello [进程管理 - 11 -](#进程管理 - 11 -)
6.1[进程的概念与作用 - 11 -](#进程的概念与作用 - 11 -)
6.2[简述壳Shell-bash的作用与处理流程 - 11 -](#简述壳Shell-bash的作用与处理流程 - 11 -)
[6.3 Hello](#6.3 Hello)[的fork进程创建过程 - 11 -](#的fork进程创建过程 - 11 -)
[6.4 Hello](#6.4 Hello)[的execve过程 - 11 -](#的execve过程 - 11 -)
[6.5 Hello](#6.5 Hello)[的进程执行 - 11 -](#的进程执行 - 11 -)
[6.6 hello](#6.6 hello)[的异常与信号处理 - 11 -](#的异常与信号处理 - 11 -)
6.7[本章小结 - 11 -](#本章小结 - 11 -)
第 7 章hello [的存储管理 - 12 -](#的存储管理 - 12 -)
[7.1 hello](#7.1 hello)[的存储器地址空间 - 12 -](#的存储器地址空间 - 12 -)
[7.2 Intel](#7.2 Intel)[逻辑地址到线性地址的变换-段式管理 - 12 -](#逻辑地址到线性地址的变换-段式管理 - 12 -)
[7.3 Hello](#7.3 Hello)[的线性地址到物理地址的变换-页式管理 - 12 -](#的线性地址到物理地址的变换-页式管理 - 12 -)
[7.4 TLB](#7.4 TLB)[与四级页表支持下的VA到PA的变换 - 12 -](#与四级页表支持下的VA到PA的变换 - 12 -)
7.5[三级Cache支持下的物理内存访问 - 12 -](#三级Cache支持下的物理内存访问 - 12 -)
[7.6 hello](#7.6 hello)[进程fork时的内存映射 - 12 -](#进程fork时的内存映射 - 12 -)
[7.7 hello](#7.7 hello)[进程execve时的内存映射 - 12 -](#进程execve时的内存映射 - 12 -)
7.8[缺页故障与缺页中断处理 - 12 -](#缺页故障与缺页中断处理 - 12 -)
7.9[动态存储分配管理 - 12 -](#动态存储分配管理 - 12 -)
7.10[本章小结 - 13 -](#本章小结 - 13 -)
第 8 章hello [的 IO 管理 - 14 -](#的IO管理 - 14 -)
[8.1 Linux](#8.1 Linux)[的IO设备管理方法 - 14 -](#的IO设备管理方法 - 14 -)
8.2[简述Unix IO接口及其函数 - 14 -](#简述Unix IO接口及其函数 - 14 -)
[8.3 printf](#8.3 printf)[的实现分析 - 14 -](#的实现分析 - 14 -)
[8.4 getchar](#8.4 getchar)[的实现分析 - 14 -](#的实现分析 - 14 -)
8.5[本章小结 - 14 -](#本章小结 - 14 -)
结论 [- 15 -](#- 15 -)
附件 [- 16 -](#- 16 -)
[参考文献 - 17 -](#参考文献 - 17 -)
第1章 概述
### 1.1 Hello简介
P2P过程描述了hello从一个静态的文本文件演变为动态执行进程的完整历程:最初,程序员编写了hello.c源文件,这是一个存储在磁盘上的普通文本文件。随后,预处理器处理其中的#include指令,将头文件内容包含进来,生成扩展后的hello.i文件。编译器对hello.i进行词法、语法和语义分析,生成x86-64架构的汇编代码hello.s。汇编器将汇编指令转换为机器码,生成可重定位目标文件hello.o,此时程序已变为二进制格式但尚未解决外部依赖。链接器将hello.o与标准库和其他必要组件结合,解析所有符号引用,生成最终的可执行文件hello。当用户在Shell中输入命令执行hello时,操作系统通过fork系统调用创建新进程,execve系统调用加载程序映像,hello由此完成从静态程序到动态进程的变化。
020过程则展现了hello从无到有再到无的完整生命周期:起点是程序员构思并创建hello.c源文件,从"零"状态开始。经过预处理、编译、汇编和链接等一系列构建步骤,hello.c转变为可执行文件hello,这是"从零到一"的创造过程。当程序被执行时,进程被创建并开始运行,在虚拟地址空间中加载代码和数据,这是生命的开始。进程执行期间,它输出问候信息,等待用户输入,这是生命的绽放。最终,无论是因为正常执行完毕还是被信号终止,进程的所有资源都被操作系统回收,内存被释放,文件描述符被关闭,进程标识符被注销,hello彻底消失,回归到"零"的状态。
1.2 环境与工具
硬件环境:
处理器:12th Gen Intel(R) Core(TM)i5-12500H 2.50 GHz
机带RAM:16.0GB
系统类型:64位操作系统,基于x64的处理器
**软件环境:**Ubuntu 20.04.6 LTS
**开发与调试工具:**VS Code; objump gdb gcc readelf等工具
1.3 中间结果
1)hello.c :源程序
2)hello.i : 预处理后得到的文本文件
3)hello.s : 编译后得到的汇编语言文件
4)hello.o : 汇编后得到的可重定位目标文件
5)hello.elf : 用readelf读取hello.o得到的ELF格式信息
6)hello1.elf : 用readelf解析hello的ELF格式
7)hello.asm : 反汇编hello.o得到的反汇编文件
8)hello2.asm : 反汇编hello可执行文件得到的反汇编文件
9)hello : 可执行文件
1.4 本章小结
通过对P2P和020过程的阐述,建立了对程序生命周期的宏观理解,认识到一个简单C程序背后所涉及的复杂系统过程。详细说明了本实验所需的硬件配置、软件平台、开发工具。中间结果的列举则为我们后续章节的深入分析提供了具体的对象和路径。
(第 1 章 0.5 分)
第2章 预处理
### 2.1 预处理的概念与作用
概念:
预处理是C语言编译过程中的第一阶段,它在实际编译之前对源代码进行初处理。预处理器(C Preprocessor, CPP)负责处理所有以#开头的预处理指令,生成一个"纯净"的C源代码文件,供编译器进行后续处理。
作用主要包括:
头文件包含 :#include指令将被指定头文件的全部内容复制到当前文件中。这使得我们可以在程序中使用标准库函数和其他模块中定义的函数和数据结构,而无需在每个源文件中重复声明。
宏展开:#define指令定义的宏会在源代码中被展开。宏可以是简单的常量(如#define PI 3.14)或带参数的函数式宏(如#define MAX(a,b) ((a)>(b)?(a):(b)))。预处理时,所有宏调用都会被替换为其定义的内容。
条件编译:通过#if、#ifdef、#ifndef、#else、#elif和#endif等指令,可以根据特定条件决定哪些代码参与编译。这在跨平台开发和调试时特别有用。
删除注释:预处理器会移除源代码中的所有注释(包括//单行注释和/* */多行注释),简化后续编译步骤。
行号标记:预处理器会在输出中添加行号信息,便于编译器在出错时定位到原始源文件的正确位置。
对于hello.c程序,预处理的主要任务是处理#include指令,将stdio.h、unistd.h和stdlib.h这三个头文件的内容包含进来,从而为编译器提供printf、sleep、atoi、getchar和exit等函数的声明。
2.2在Ubuntu下预处理的命令
-
预处理的命令:gcc -E hello.c -o hello.i
图 1: 预处理指令
### 2.3 Hello的预处理结果解析
-
打开hello.i文件,我们可以看到预处理的具体结果:
图 2: hello.i 文件
-
头文件包含的展开: hello.i文件的开头部分包含了从stdio.h、unistd.h和stdlib.h等头文件展开的内容。这些内容主要包括:大量函数声明(如printf、fprintf、scanf等),类型定义(如size_t、FILE等),宏定义(如NULL、EOF等),常量定义。
-
原始代码的保留: 在hello.i文件的最后部分,3000多行处,我们可以看到原始的main函数代码(如图二)
-
关键变化分析:注释已被删除 :原始hello.c中的注释//
大作业的 ``hello.c ``程序等已被移除。预处理指令消失 :#include指令已被替换为对应头文件的实际内容。行号标记 :# 710 "hello.c"是预处理器添加的行号指令,表示以下代码来自原始hello.c文件的第710行(这个行号是预处理后的相对位置)。代码保持不变 :main函数的逻辑代码与原始代码一致,包括变量定义、控制结构和函数调用。 -
综上,预处理过程并没有改变程序的逻辑结构,只是将外部依赖(头文件)内联到源文件中,创建了一个完全自包含的翻译单元。这使得编译器在后续步骤中无需查找外部文件即可完成编译工作。
### 2.4 本章小结
-
本章通过实际操作和文件分析深入探讨了hello.c程序的预处理阶段。我们使用gcc -E命令成功生成了预处理文件hello.i,并对其内容进行了详细解析。预处理阶段主要完成了头文件包含、注释删除和行号标记等关键任务,将分散的源代码和头文件整合为统一的翻译单元。预处理后的文件虽然体积显著增大,但程序的逻辑结构保持不变。
-
(第****2 章 0.5 分)
## **第3章 编译**
1.
### 3.1 编译的概念与作用
-
编译将预处理后生成的中间文件转换为特定处理器架构的汇编语言代码。这一过程由编译器核心组件完成,负责进行词法分析、语法分析、语义分析、中间代码生成、代码优化以及目标代码生成等一系列复杂操作。编译器首先对源代码进行结构化分析,检查其是否符合C语言语法规范,随后生成与机器无关的中间表示形式,经过多层次的优化处理后,最终输出目标平台的汇编指令。
-
编译阶段的核心作用在于将高级语言描述的算法逻辑转化为低级的、面向硬件的指令序列,同时在这一过程中实施多种优化策略以提升程序运行效率。这些优化包括但不限于删除冗余计算、简化表达式结构、重组循环体以及调整内存访问模式等。
### 3.2 在Ubuntu下编译的命令
-
编译的命令:gcc -S hello.i -o hello.s

-
图 3: 编译
3.3 Hello的编译结果解析
-
这部分将按照类型和操作一块块说明
3.3.1汇编初始部分
-
在最开始部分,在main函数前一部分展示了节名称:
 图 4: 汇编初始字段
解析:
.file 声明出源文件
-
.text 表示代码节
-
.section .rodata 表示只读数据段
-
.align 声明对指令或者数据的存放地址进行对齐的方式
-
.string 声明一个字符串
-
.globl 声明全局变量
-
.type 声明一个符号的类型
3.3.2 数据部分
-
1)字符串程序的两个字符串LC0,LC1存放在只读数据段
这里以LC0为例,与LC0相关的字段已在图5中标黄。其中第一次标黄表明其存储在只读数据段。
-
hello.c中唯一的数组是main函数中的第二个参数(即char**argv),数组的每个元素都是一个指向字符类型的指针。由知数组起始地址存放在栈中-32(%rbp)的位置,被两次调用作为参数传到printf中。
-
第二次标黄是将rdi设置为字符串的起始地址。
 图 5: LC0的存储与调用
1.
2.
3.
### 2)参数argc
-
参数argc是main函数的第一个参数,被存放在寄存器%edi中
图 6: argc由图6第22,23两行,可见寄存器%edi地址被压入栈中,可知该地址上的数值与立即数5判断大小,从而得知argc被存放在寄存器并被压入栈中。
2.3)局部变量
程序中的局部变量是i

5. 图 7: 局部变量i根据可图7知局部变量i是被存放在栈上-4(%rbp)的位置。
3.3.3全局函数
6.
7. 图 8: 全局函数hello.c中只声明了一个全局函数int main(int arge,.char*argv[]),通过图8可知。
3.3.4赋值操作
8.
9. 图 9: 赋值操作hel1o.c中的赋值操作贝有for循环开头的i-0,该赋值操作体现在汇编代码上,则是用mov指令实现,如图9。由于int型变量i是一个32位变量,使用movl传递双字实现。
#### 3.3.5算术操作 -
hello.c中的算术操作为for循环的每次循环结束后i++,该操作体现在汇编代码则使用指令add实现。由于变量i为32位,使用指令addl。指令如图10。

-
图 10: 算术操作
3.3.6关系与跳转操作
-
1)条件判断语句if(argc!=5):汇编代码将这条代码翻译如图11。

图 11: 判断与跳转1
-
使用了cmp指令比较立即数5和参数argc大小,并且设置了条件码。 根据条件码,如果不相等则执行该指令后面的语句,否则跳转到.L2。
-
2)在for循环每次循环结束要判断一次i<10,判断循环条件翻译如图12。

图 12: 判断与跳转2
1. 同(1),设置条件码,并通过条件码判断跳转到什么位置。
2.

3. 图 13: 跳转3
1. 3)跳转到L2后进行完赋值,会无条件跳转到L3,如图13。
4.
5.
6.
#### 3.3.7函数操作
-
1)main函数
-
参数传递:该函数的参数为int argc,,char*argv[]。具体参数传递地址和值都在前面阐述过。
-
函数调用:通过使用call内部指令调用语句进行函数调用,并且将要调用的函数地址数据写入栈中,然后自动跳转到这个调用函数内部。main函数里调用了printf、exit、sleep函数。
-
局部变量:使用了局部变量i用于for循环。具体局部变量的地址和值都在前面阐述过。
-
2)printf函数
-
参数传递:printf函数调用参数argv[1],argv[2]。
-
函数调用:该函数调用了两次。第一次将寄存器%rdi设置为待传递字符串"用法:Hello学号 姓名 手机号 秒数!\n"的起始地址;第二次将其设置为"Hello %s %s\n"的起始地址。具体已在前面讲过。使用寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递。
-
3)exit函数
-
参数传递与函数调用见图14。

-
图 14: exit函数
-
将rdi设置为1,再使用call指令调用函数。
-
4)atoi、sleep函数
-
参数传递与函数调用见图15。

-
图 15: atoi与sleep函数
-
可见,atoi函数将参数argv[3]放入寄存器%rdi中用作参数传递,简单使用call指令调用。然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。
-
5)getchar函数
无参数传递,直接使用call调用即可。
3.4本章小结
-
本章深入分析了hello.c程序从预处理文件到汇编代码的编译转换过程。我们通过gcc -S命令成功生成了hello.s汇编文件,并对其内容进行了分块详细解析。编译阶段完成了从高级语言到低级汇编语言的语义转换,同时实施了多种代码优化策略以提升执行效率。生成的汇编代码严格遵循目标平台的调用约定和指令集架构,为后续的汇编阶段提供了精确的机器指令蓝图。
-
(第 3 章 2 分)
## **第4章 汇编**
1.
### 4.1 汇编的概念与作用
汇编是程序构建流程中的关键转换阶段,负责将人类可读的汇编语言源代码转换为机器可直接执行的二进制指令代码。这一转换过程由专门的汇编器程序完成,它逐行解析汇编指令,根据处理器指令集架构将其编码为对应的二进制操作码和操作数,同时生成包含代码、数据和元信息的可重定位目标文件。
汇编阶段的核心作用在于搭建高级语言与机器硬件之间的桥梁,将符号化的汇编指令转化为具体的二进制表示,为后续的链接和执行提供基础。与编译阶段不同,汇编过程相对直接,大部分汇编语句与机器指令存在一一对应关系,但汇编器仍需处理标签解析、地址计算和指令编码等细节任务。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统下,对hello.s进行汇编的命令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o,见图16。

图 16: 汇编
4.3 可重定位目标elf格式
在终端中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式,见图17。

图 17: ELF 格式
下为对 ELF 文件的结构分析:
(1)ELF头
ELF头包含了目标文件的基本元数据信息,通过执行"readelf -h hello.o"命令可以查看详细的头信息。头信息显示这是一个64位x86-64架构的可重定位目标文件,采用小端字节序,ELF头大小为64字节,节区头表从第1136字节开始,共包含13个节区头条目。文件类型标识为REL(可重定位文件),机器类型为Advanced Micro Devices X86-64,这些信息共同定义了目标文件的基本属性和运行平台要求,为后续的链接和加载过程提供必要指导。ELF头展示如图18。

图18: ELF头
(2)节头
节区头表详细描述了hello.o文件中各个节区的组织结构和存储布局。主要的节区包括.text节存放机器指令代码,其起始偏移为0x40,大小为0x7a字节;.data节和.bss节分别用于已初始化全局变量和未初始化变量,但hello程序中未使用这些特性故大小为零;.rodata节保存只读数据如字符串常量,包含程序中使用的两个格式字符串;.symtab符号表节和.strtab字符串表节存储符号名称和对应字符串;.shstrtab节则保存节区名称字符串。每个节区头条目记录了节区类型、地址对齐要求、大小和偏移等关键属性,这些信息共同构建了目标文件内部的数据组织结构。节头展示如图19。
(

图 19: 节头
3)重定位节
重定位节.rel.text记录了代码段中所有需要链接器修正的外部引用位置。通过执行"readelf -r hello.o"命令可以查看具体重定位条目,这些条目对应着对printf、sleep、atoi、getchar和exit等库函数的调用位置。每个重定位条目包含需要修正的指令在.text节中的偏移量、符号索引以及重定位类型信息。例如对于printf调用,其重定位类型为R_X86_64_PLT32,这表明链接器需要创建一个过程链接表条目并计算相对调用地址。这些重定位信息在链接阶段将指导链接器如何修改目标代码,将临时占位地址替换为实际函数地址。重定位展示如图20。

图20: 重定位节
(4)符号表
符号表.symtab包含了hello.o文件中定义和引用的所有符号信息,包括函数名称和节区名称等。通过执行"readelf -s hello.o"命令可以查看完整的符号表内容,其中main函数作为全局符号定义在.text节中,printf等库函数作为未定义符号标记为外部引用。符号表的每个条目记录了符号名称、类型、绑定属性、所在节区索引和值等信息。本地符号如循环计数器i和字符串常量标签通常不会出现在最终符号表中,因为编译器可能将其优化或使用局部符号绑定方式。符号表为链接器提供了符号解析的基础数据,使得链接器能够将多个目标文件中的符号引用与定义正确关联起来。符号表展示如图21。

图 21: 符号表
4.4 Hello.o的结果解析
在终端中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件。见图22。

图 22: 反汇编文件
与第 3 章的 hello.s 文件进行对照分析:
1)增加机器语言
每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如,在hello.s中的一个cmpl指令表示为图23:

图 23: cmpl在hello.s中
-
-
-
-
而在反汇编中表示为图24,多了83 7d ec 05:
-
-
-
-
2

图 24: cmpl在反汇编文件中
)操作数进制- 反汇编文件中的所有操作数都改为十六进制。如(1)中的例子,立即数由hello.s中的5变为了0x5,地址表示也由-20(%rbp)变为-0x14(%rbp)。
-
3)跳转指令
- 反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L3)。例如下面的jmp指令,反汇编文件中为图25:

图 25: 反汇编中的跳转指令
而hello.s中,则是段名称,如图26:

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

-
图27: hello.s中的函数调用
-
而在反汇编文件中调用函数见图28:

-
图28: 反汇编文件的函数调用
-
在可重定位文件中call后面不再是函数名称,而是一条重定位条目指引的信息。
### 4.5 本章小结
本章全面分析了hello程序从汇编代码到目标文件的汇编转换过程。我们通过gcc -c命令成功生成了hello.o可重定位目标文件,并使用readelf和objdump工具对其结构内容进行了细致研究。汇编阶段完成了从文本指令到二进制机器码的直接转换,同时构建了包含代码、数据和重定位信息的标准ELF格式文件。这一过程虽然不涉及复杂的语义分析和优化,但准确实现了指令编码和符号记录,为多个目标文件的合并与最终执行奠定了必要基础。通过本章的探讨,我们深入理解了可重定位目标文件的结构组成和生成机制,掌握了分析二进制文件的基本方法,这些知识对于理解完整的程序构建流程和后续的链接阶段分析具有重要价值。
(第 4 章 1 分)
第5章 链接
### 5.1 链接的概念与作用
链接是将多个可重定位目标文件及库文件合并生成可执行目标文件的关键构建阶段,这一过程由链接器负责执行。链接的核心任务包括符号解析和重定位两大主要操作。符号解析旨在将程序中每个符号引用与相应的符号定义正确关联,确保每个函数调用和变量引用都有明确的定义来源。重定位则将不同目标文件中的代码和数据节区整合到统一的地址空间中,并修正符号引用的具体内存地址,使程序能够在加载后正确执行。
链接的作用不仅在于组合多个编译模块,还能实现代码复用、库函数集成和地址空间的有效组织,最终生成可被操作系统直接加载执行的完整程序。对于hello程序而言,链接阶段将hello.o目标文件与C标准库及必要的系统启动代码进行整合,解决所有外部引用,生成可以直接运行的hello可执行文件。
5.2 在Ubuntu下链接的命令
在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
见图29:

图 29: 链接
5.3 可执行目标文件hello的格式
使用readelf解析hello的ELF格式,文件名记为hello****1.elf,得到hello的节信息和段信息:
1 ) ELF 头
hello1.elf 中的 ELF 头与 hello.elf 中的 ELF 头包含的信息种类基本相同,以描述了生成该文件的系统的字的大小和字节顺序的 16 字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与 hello.elf 比较, hello1.elf 中的基本信息未发生改变(如 Magic ,类别等),而类型改变为 EXEC ,表明这是一个可直接执行的文件 程序。头大小和节头数量增加,并且获得了入口地址。见图 30 :
2

图 30: hello1.elf的ELF头
)节头
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且以它的大小以及偏移量重新设置各个符号的地址。展示见图 31 :


图 31:hello1.elf的节头
3 )程序头
1.
1. 程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息这个部分是hello.elf没有的。见图32:
2.

图 32: 程序头
( 4 )动态段
存储动态链接信息,使程序能在运行时加载所需的共享库并解析依赖。 这个部分也是 hello.elf 没有的。见图****3 3 :

图 33: 动态段
-
( 5 )重定位节
-
见

图 34: hello1.elf的重定位节
图 34 : -
( 6 )符号表
-
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。见图 35 :


图 35: hello1.elf的符号表
5.4 hello的虚拟地址空间
使用gdb调试工具执行info hello mappings命令,就能查看hello的虚拟地址空间布局以及对应的进程编号,见图36:

图 36: hello的虚拟地址
从显示结果可以看出,整个虚拟地址空间都被划分成大小为0x1000(即4096字节)的页。结合程序头表进行分析,前五页对应主程序中的各个段:第一页对应第一个LOAD段,第二页对应第二个LOAD段(即代码段),第三页对应第三个LOAD段(只读数据),而第四页和第五页共同对应第四个LOAD段,因为该段加载的内存范围跨过了两页,地址从0x403de8延伸到0x404050。此外,bss段虽然不占用实际内存,但仍需分配16字节的空间。
在这些段中,第二页的代码段标记为r-xp,即可读、可执行;第五页的数据段标记为rw-p,即可读写、可修改。这些段的内容都是从可执行文件中直接加载到内存的。
参考csapp教材的图7-15,继续向上观察虚拟地址空间,可以看到共享库的内存映射区域位于地址空间的顶端。中间预留的大片空间是留给运行时堆(例如通过malloc动态分配的内存)使用的。
从地址0x7ffff7c00000开始,直到7e05000,分布着数百页C标准库的映射段,从上到下依次包括文件头、代码段、只读数据段和数据段。而从7fc5000到7fff000的区间,则包含了动态链接库以及加载器的映射段,结构与之类似。正如书中示意图所示,内存映像的最高地址区域属于用户栈,这里为主线程分配了33页的可读写栈空间,主要用于保存函数调用链、局部变量等数据。
5.5 链接的重定位过程分析
在终端中使用命令objdump -d -r hello > hello2.asm生成反汇编文件hello2.asm,见图37:
与

图37: 生成反汇编文件hello2.asm
图 37: 生成反汇编文件hello2.asm
第四章中生成的 hello.asm 文件进行比较,其不同之处如下:
1)多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码(见图38)。这是因为动态链接器将共享库中hello.c用到的函数加入可
执行文件中。
图38: 链接后的函数相关代码
2)函数调用指令call的参数发生变化
在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码,展示见图39。

图39: call的变化
(3)跳转指令参数发生变化
在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码,具体见图40。

图40: 跳转的变化
重定位过程:
合并节并分配地址 : 链接器首先将来自不同目标文件中类型相同的节(例如多个代码节)合并成统一的完整块。接着,它会给这些新合并的完整块、以及每个输入模块中定义的各个节和符号分配具体的运行时内存地址。完成这一步后,程序中的每条指令和每个全局变量都将拥有一个唯一的运行地址。
修正符号引用 : 链接器随后会逐一修改代码节和数据节中对所有符号的引用(例如函数调用和全局变量访问),使它们指向上一步所确定的正确运行时地址。为了实现这一步修改,链接器需要借助目标文件中名为"重定位条目"的特殊数据结构,这些条目指明了哪些地方需要调整以及如何调整。
引用地址的计算方式**:** 在修正符号引用时,通常的地址计算方法是:最终运行地址 = 符号在合并后节内的偏移量 + 该节被装载到内存的起始地址。链接器正是根据重定位条目提供的信息,运用此方法对每个引用进行修正。
5.6 hello的执行流程
首先,在子程序处设置断点,查看断点位置即可看到子程序名与其地址,见图41。

图 41: 子程序名及地址
接着,执行程序,查看调用过程,见图42:

图 42: 调用过程
5.7 Hello的动态链接分析
为了深入观察 Hello 程序在动态链接后的内存状态变化,我们需要重点关注几个可写的内存段:.got段、.got.plt段、.data段和.bss段。这些段都包含在程序头表的第四个 LOAD 段中(不包含 .dynamic 段)。虽然该 LOAD 段在程序头表中被标记为可读写,但其初始映射的虚拟页(例如地址范围 0x403000-0x404000)可能被设置为只读。当动态链接器尝试向该区域(例如地址 0x403ff0)写入数据时,CPU 会触发页错误。内核随后会将该可写区域重新映射到一个新的、具有写权限的物理页帧上。这相当于动态链接器在初始化阶段暂时调整了内存区域的权限以完成必要的写入操作,之后可能再恢复其原始保护属性。
在 GDB 中,如果对 _start 设置断点(b _start),通常会看到两个子断点。这是因为现代 Linux 程序通过动态链接器加载:阶段 1(断点 1.2)是动态链接器自身的入口点,阶段 2(断点 1.1)才是用户程序 _start 的入口点。因此,符号 _start 实际上对应两个不同的内存地址范围。观察从断点 1.2到断点 1.1的过程,可以发现 .got、.got.plt 和 .bss 段的内容发生了变化,见图43。

图 43: 断点1.2到断点1.1的变化
具体变化如下:
-
动态链接器在 .got 段中写入了动态符号表的地址(例如 0x00007ffff7c7a200),该地址位于 C 标准库映射的代码段内,用于运行时的符号解析。
-
在 .got.plt 段的特定位置(例如 0x403ff0)填入了动态链接器内部某个数据结构的地址(例如 0x00007ffff7ffe2e0),该地址位于动态链接器自身的数据段中。
-
.bss 段被初始化,例如写入了 stdin 全局变量的地址 0x00007ffff7e038e0,该地址位于 C 标准库映射的数据段。对于可执行文件,.bss 段在磁盘上不占空间,但在程序加载到进程内存后,它会占用内存并成为重定位操作的一部分。
-
.got.plt 段中地址 0x404000-0x404030 范围内的内容在此时尚未改变,它们仍然指向延迟绑定桩代码(地址如 0x4010x0)。这些桩代码在生成可执行文件时就已存在,其对应的函数真实地址将在第一次被调用时才进行解析。相关的 PLT32 重定位类型支持这种延迟绑定机制,由动态链接器在运行时处理,有助于提升程序的启动速度。
从断点 1.1 到在 main 函数处设置的断点 2 之间,动态链接相关的这些条目没有进一步变化。当程序第一次运行到断点 3(printf 调用处)时,实际调用的是 __printf_chk 函数,此时触发了延迟绑定过程。其地址被解析为 0x00007ffff7d37960(位于 C 标准库的代码段),并回填到对应的 .got.plt 条目中。这一过程的完整步骤是:跳转到 PLT 中的桩代码 (0x401050) → 触发动态链接器进行符号解析 → 将解析得到的真实函数地址填入 .got.plt → 跳转到该真实函数执行。此后再次调用该函数时,就可以直接通过 .got.plt 中的地址跳转,无需重复解析,变化见图44。

图 44: 断点2到断点3的变化
程序在循环中第二次进入断点 3 时,由于执行了 sleep(atoi(argv[4])) 语句,strtol 和 sleep 函数也被调用,因此它们在 .got.plt 段中的对应条目被填入了各自在 C 标准库代码段中的真实地址。在后续的第 3 到第 10 次循环中,其他未被调用的函数条目则保持不变,具体见图45。

图 45: 第二次到断点3的变化
程序运行结束前,在 exit 处设置的断点 4 可以看到,由于执行了 getchar,getc 函数的地址也被填入 .got.plt。而程序错误处理部分用到的 puts 函数由于始终未被调用,其条目内容一直保持为初始的桩代码地址。具体见图46:

图 46: 到断点4的变化
当 exit 继续执行后,进程终止,hello 程序所占用的虚拟内存被释放,内存状态回归到操作系统的管理之下。可见于图47:

图 47: exit后的变化
5.8 本章小结
本章全面分析了hello程序从目标文件到可执行文件的链接过程。我们通过链接命令生成可执行文件hello,并利用readelf、objdump和gdb等工具深入研究了可执行文件的格式结构、虚拟地址空间布局、重定位机制以及动态链接原理。链接阶段完成了符号解析和地址重定位两大核心任务,将多个编译单元整合为统一的执行实体,添加必要的启动代码和动态链接信息,最终生成可由操作系统直接加载运行的程序文件。通过本章的探讨,我们理解了链接在程序构建中的关键作用,掌握了可执行文件的组织结构和加载执行原理,这些知识对于深入理解计算机系统如何运行程序具有重要意义。
(第 5 章 1 分)
第6章 hello进程管理
### 6.1 进程的概念与作用
进程是计算机系统中程序执行的基本单元,它代表了一个正在运行的程序实例,包含了程序执行所需的所有状态信息。进程不仅包含程序的代码和数据,还包括程序计数器、寄存器、堆栈以及打开的文件描述符等运行时环境。
操作系统通过进程抽象为每个程序提供了独立的虚拟地址空间,使得多个程序能够并发执行而互不干扰。进程管理是操作系统核心功能之一,负责进程的创建、调度、同步和终止等操作。对于hello程序而言,当我们在Shell中输入命令执行它时,操作系统会创建一个新的进程来运行这个程序,该进程拥有独立的资源,并按照程序的逻辑执行指令,完成信息输出和等待输入等任务。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash作为用户与操作系统内核之间的命令行接口,承担着命令解释和执行的重要角色。
当用户在终端输入命令时,Shell首先读取命令行输入,然后进行解析,将命令分解为可执行的程序名及其参数。Shell接着查找该程序的可执行文件,通常通过PATH环境变量指定的目录搜索。找到程序后,Shell通过系统调用创建子进程来运行该程序,并可能根据命令中的重定向或管道符号调整标准输入输出。对于前台命令,Shell会等待子进程执行完毕后再提示用户输入新命令;对于后台命令,Shell则立即返回提示符,允许用户继续输入其他命令。
在整个处理流程中,Shell还负责环境变量管理、作业控制、命令历史记录以及脚本执行等高级功能。
6.3 Hello的fork进程创建过程
当用户在Shell中输入"./hello 1 2 3"命令后,Shell首先调用fork系统调用创建一个子进程。
fork调用会创建一个与父进程几乎完全相同的副本,包括代码段、数据段、堆栈以及打开的文件描述符等。在fork调用返回时,父进程和子进程从同一位置继续执行,但fork的返回值不同:父进程中返回子进程的进程ID,而子进程中返回0。这一差异使得两个进程能够区分彼此并执行不同的代码路径。子进程继承了父进程的环境变量和命令行参数,为后续执行hello程序做好了准备。fork进程创建过程采用了写时复制技术,初始时父子进程共享物理内存页,只有当任一进程尝试修改内存时,才会复制相应的页面,这种机制显著提高了进程创建的效率。
6.4 Hello的execve过程
在fork创建子进程后,子进程通过execve系统调用加载并执行hello程序。execve调用会用指定的可执行文件hello的内容完全替换当前进程的地址空间,包括代码段、数据段和堆栈等。execve接受三个主要参数:可执行文件的路径名、命令行参数数组以及环境变量数组。对于hello程序,命令行参数包括程序名、学号、姓名、手机号和秒数。execve成功执行后,操作系统的程序加载器将hello文件的代码和数据加载到进程的虚拟地址空间中,设置好程序入口点,并开始执行hello的main函数。execve调用不会创建新进程,而是用新程序替换当前进程的映像,因此进程ID保持不变,但进程的代码和数据已经完全改变。
6.5 Hello的进程执行
hello程序在运行时,进程提供给应用程序的抽象有:(1)一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占地使用处理器;(2)一个私有的地址空问,它提供一个假象,好像我们的程序独占地使用CPU内存。
操作系统提供的抽象有:
1)逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
2)上下文切换。操作系统内核使用一种称为上下文切换的叫高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需状态。
3)时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
4)用户态和核心态。处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。
5)上下文信息。上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。hello程序执行过程中,在进程调用execve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf函数输出"Hello 2024111665 毛京轩",之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
在hello程序的执行过程中,可能遭遇多种类型的异常,这些异常根据其产生原因和处理方式可归为四大类别。首先是陷阱(Trap),这是程序有意触发的同步异常,用于主动请求操作系统服务,例如printf调用write系统输出字符、sleep调用睡眠系统暂停执行、getchar调用read系统读取输入,这些操作均通过专门的陷阱指令实现,处理完毕后总是返回到下一条指令继续执行。其次是故障(Fault),表现为潜在可恢复的同步错误,典型代表是页故障,当hello进程访问的虚拟地址未映射物理内存时触发,操作系统会尝试从磁盘加载缺失页面,成功后重新执行当前指令,若无法恢复则可能升级为终止。第三类是中断(Interrupt),源自外部设备的异步信号,例如定时器中断驱动进程调度,键盘中断捕获Ctrl-C等用户输入,这些事件随机发生且处理完成后总是返回到原指令流的下一条指令。最后是终止(Abort),属于不可恢复的同步错误,如非法指令或严重硬件故障,一旦发生操作系统将直接终止hello进程且无法返回。
在程序执行过程中,内核还会产生信号。例如,Ctrl-C会产生SIGINT信号,终止子进程;Ctrl-Z会产生SIGTSTP信号,暂停子进程;运行fg命令会发送SIGCONT恢复子进程运行,但是因为返回到下一条指令处,所以未执行完的睡眠过程不再执行,而是直接进入下一次printf循环。Hello终止后会产生SIGCHLD信号给父进程并要求父进程回收资源。
下面展示运行结果及相关命令:
1)Ctrl + C与Ctrl + Z
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程;按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。结果如图48(涉及个人信息部分打码,下文中打码原因皆同图48,不再说明):

图 48: Ctrl+Z与Ctrl+C
2)对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。结果见图49:

图49: 进程挂起的查看
3)在终端中输入pstree命令,可以将所有进程以树状图显示,直观展示了进程之间的父子关系,部分结果见图50:

图 50: pstree
4)输入kill命令, kill PID表示直接终止某一进程,向其发送SIGTERM信号进行终止,则可以杀死指定(进程组的)进程,如图51:

图 51: kill
5) 输入fg 1则命令将hello进程再次调到前台执行,hello再从挂起处继续运行,程序仍然可以正常结束,并完成进程回收。结果如图52:

图 52: fg重新执行被挂起的进程
6.7本章小结
本章全面探讨了hello程序的进程管理过程。从Shell解析命令开始,通过fork创建子进程,再通过execve加载hello程序,进程进入执行阶段,在操作系统调度下交替运行和睡眠。我们分析了进程执行中的上下文切换、用户态与内核态转换,以及异常和信号处理机制。通过实际操作演示了hello进程在运行过程中对键盘信号的反应,以及相关进程管理命令的使用。进程管理是操作系统核心功能的集中体现,它通过抽象和隔离为每个程序提供了独立的运行环境,通过调度和同步实现了系统资源的高效利用。理解进程管理机制对于编写健壮的程序和深入理解计算机系统工作原理具有重要意义。
(第 6 章 2 分)
第7章 hello的存储管理
### 7.1 hello的存储器地址空间
在hello程序的执行过程中,涉及多种不同层次的地址概念,这些地址共同构成了程序的存储访问体系。
逻辑地址是程序内部看到的地址形式,通常表现为"段选择子:偏移量"的组合,在hello的代码中所有变量和函数引用最初都使用逻辑地址。
线性地址(也称为虚拟地址)是逻辑地址经过段式管理转换后得到的地址,在大多数现代操作系统中,段式管理被简化,逻辑地址往往直接映射为虚拟地址。 虚拟地址是进程视角中的连续地址空间,hello程序运行在独立的虚拟地址空间中,从0x400000开始的代码段到高地址的栈空间构成了完整的虚拟内存布局。 物理地址则是实际内存硬件上的地址,通过页式管理机制将虚拟地址转换而来,当hello进程访问内存时,最终需要通过硬件将虚拟地址转换为物理地址才能完成实际的数据读写。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器通过段式管理机制将逻辑地址转换为线性地址,这一过程涉及段选择子和段内偏移量的配合使用。在保护模式下,段选择子指向全局描述符表(GDT)或局部描述符表(LDT)基地址、界限和访问权限等信息。处理器将段基地址与偏移量相加得到线性地址,在x86-64架构中,段式管理被大大简化,大多数段的基地址被设置为0,界限设置为最大值,使得逻辑地址与线性地址几乎等价。段式管理框架见图53:

图53: 段式管理
对于hello程序而言,代码段、数据段和栈段等都具有相应的段描述符,但这些段的基地址通常为0,因此逻辑地址中的偏移量直接成为线性地址,这种简化设计提高了地址转换效率,同时保持了系统的兼容性。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理机制负责将hello进程的线性地址转换为实际的物理地址,这是现代操作系统内存管理的核心。
操作系统将物理内存划分为固定大小的页框,将进程的虚拟地址空间划分为相同大小的页面,通过页表建立页面到页框的映射关系。当hello进程访问内存时,处理器中的内存管理单元根据当前进程的页表基址寄存器找到页表,然后利用线性地址中的页号索引页表项,获取对应的物理页框号,最后结合页内偏移得到完整的物理地址。页表项中还包含了访问权限、修改标志和存在标志等重要信息,确保内存访问的安全性和正确性。图54为页式管理示意图:

图54: 页式管理
通过页式管理,hello进程的连续虚拟地址空间可以映射到物理内存中不连续的页框,实现了内存的高效利用和进程间的隔离保护。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。工作原理如图55:

图 55: TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
获得物理地址后,处理器通过三级缓存系统访问实际的内存数据,这个层级结构旨在弥合处理器与主存之间的速度差距。L1缓存分为指令缓存和数据缓存,直接集成在处理器核心内部,访问速度最快,hello程序的指令和频繁使用的数据会被缓存在这里。L2缓存容量较大,作为L1缓存的补充,存储近期访问的内存内容。L3缓存由多个核心共享,容量更大但速度较慢。当hello进程需要访问内存数据时,首先查询L1缓存,若未命中则依次查询L2和L3缓存,若三级缓存

图 56: 高速缓存层次结构
均未命中则需访问主内存。高速缓存层次结构如图56:
缓存系统采用组相联映射和LRU等替换策略管理缓存行,对于hello程序中的循环访问模式和局部性特征,缓存能够显著减少平均内存访问时间,提升程序执行效率。
7.6 hello进程fork时的内存映射
根据6.3小节,当Shell通过fork系统调用创建hello子进程时,操作系统采用写时复制技术实现内存映射的高效管理。初始时,子进程与父进程共享相同的物理内存页,这些页面被标记为只读。当任一进程尝试写入共享页面时,会触发页保护异常,操作系统内核捕获该异常后,为写入进程复制一个独立的页面副本,并更新页表映射,使两个进程拥有各自的物理内存页。
这种机制避免了fork时立即复制整个地址空间的开销,对于hello进程而言,其代码段作为只读内容可以继续共享,而数据段和堆栈区域在需要写入时才会进行实际复制。写时复制优化了进程创建性能,特别适用于fork后立即执行execve的场景。
7.7 hello进程execve时的内存映射
execve系统调用加载hello可执行文件时,操作系统会为进程建立全新的内存映射关系。程序加载器首先释放进程原有的地址空间,然后根据ELF文件中的程序头表信息,为代码段、数据段等建立内存映射。
代码段通常映射为只读且可执行,数据段映射为可读写,这些映射初始时可能并未分配实际物理页框,而是标记为延迟分配的匿名映射或文件映射。动态链接器部分也被映射到地址空间中,负责处理共享库的加载和重定位。堆区域通过brk系统调用设置初始大小,栈区域则由内核自动分配。execve完成后,hello进程拥有了独立的地址空间布局,其中代码和数据来自可执行文件,而堆和栈则在运行时动态扩展。这种按需映射机制减少了程序启动时的内存开销。
7.8 缺页故障与缺页中断处理
在hello进程执行过程中,当访问的虚拟地址对应的页表项标记为不存在时,会触发缺页故障这一异常。处理器保存当前状态后转入内核态的缺页处理程序,该程序首先检查访问是否合法,若为非法访问则向进程发送SIGSEGV信号终止进程。对于合法访问,处理程序根据页表项中的信息区分不同类型的缺页。对于代码段或文件映射的数据段,内核从磁盘上的可执行文件中读取相应页面到物理内存;对于堆或栈的匿名映射,内核分配新的物理页框并初始化为零;对于写时复制页面,内核复制原页面并建立新映射。页面加载完成后,页表项被更新为有效状态,进程从触发缺页的指令处重新执行。这种按需调页机制使得hello进程无需在启动时就加载全部内存内容,提高了内存利用率和启动速度。
缺页异常处理如图57:

图57: 缺页异常处理
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf 会调用 malloc ,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)
7.10本章小结
本章深入探讨了hello程序的存储管理机制,从地址空间概念到具体的地址转换过程,再到内存映射和缓存系统。我们分析了逻辑地址到物理地址的完整转换路径,包括简化的段式管理、四级页表与TLB协同的页式管理,以及三级缓存体系。通过研究hello进程fork时的写时复制和execve时的内存映射建立,理解了进程内存空间的动态变化。缺页故障处理展示了操作系统如何实现按需调页,动态存储分配则揭示了堆内存管理的内部原理。这些存储管理技术共同为hello程序提供了高效、安全、隔离的运行环境,体现了现代计算机系统在内存管理方面的精巧设计。理解这些机制对于编写高效程序和深入分析系统行为至关重要。
(第 7 章2分)
第8章 hello的IO管理
### 8.1 Linux的IO设备管理方法
### 8.1.1 设备的模型化:文件
Linux系统采用"一切皆文件"的设计哲学,将各种输入输出设备抽象为文件进行处理。这种设计为用户和程序提供了统一的访问接口。
设备文件分为三种类型:
1)字符设备文件:以字符为单位进行数据传输,如键盘、鼠标、串口等
2)块设备文件:以数据块为单位进行数据传输,如硬盘、光盘、U盘等
3)网络设备:通过网络套接字接口进行访问
所有设备文件都存放在dev目录下,例如devtty表示终端设备,devsda表示第一块硬盘。用户程序可以使用与普通文件相同的系统调用如open、read、write和close来操作设备文件,这种统一性极大地简化了编程模型。
8.1.2设备管理:Unix IO接口
Linux继承了Unix的IO管理框架,采用分层架构管理设备:
第一层是用户程序层,调用标准IO库函数
第二层是标准IO库层,提供带缓冲的IO操作
第三层是系统调用层,提供内核服务接口
第四层是设备驱动层,直接控制硬件设备
第五层是物理设备层,包括各种硬件设备
这种分层设计使得各层之间的耦合度降低,提高了系统的可维护性和可扩展性。
8.2 简述Unix IO接口及其函数
Unix提供了一套简洁而强大的IO系统调用接口,这些接口作为用户程序与内核之间交互的桥梁,保证了设备访问的一致性和安全性。
主要系统调用函数 :
1)文件描述符机制
文件描述符是一个非负整数,用于标识打开的文件或设备。系统预定义了三个标准文件描述符:
0:标准输入
1:标准输出
2:标准错误输出
2)基本IO系统调用
open函数用于打开或创建文件:
函数原型:int open(const char pathname, int flags, mode_t mode)
功能说明:打开指定路径的文件或设备
参数说明:pathname为文件路径,flags为打开标志,mode为文件权限
返回值:成功返回文件描述符,失败返回-1
close函数用于关闭文件:
函数原型:int close(int fd)
功能说明:关闭指定的文件描述符
参数说明:fd为要关闭的文件描述符
返回值:成功返回0,失败返回-1
read函数用于读取数据:
函数原型:ssize_t read(int fd, void buf, size_t count)
功能说明:从文件描述符读取数据到缓冲区
参数说明:fd为文件描述符,buf为缓冲区地址,count为要读取的字节数
返回值:成功返回实际读取的字节数,0表示文件结束,-1表示错误
write函数用于写入数据:
函数原型:ssize_t write(int fd, const void buf, size_t count)
功能说明:将缓冲区数据写入文件描述符
参数说明:fd为文件描述符,buf为缓冲区地址,count为要写入的字节数
返回值:成功返回实际写入的字节数,-1表示错误
lseek函数用于调整文件偏移量:
函数原型:off_t lseek(int fd, off_t offset, int whence)
功能说明:重新定位文件读写位置
参数说明:fd为文件描述符,offset为偏移量,whence为基准位置
返回值:成功返回新的文件偏移量,-1表示错误
3)IO多路复用机制
对于需要同时监控多个文件描述符的场景,Unix提供了select和poll系统调用:
select函数允许程序同时监控多个文件描述符的状态变化
poll函数提供了比select更灵活的监控机制
系统调用流程:
当用户程序调用IO函数时,首先调用标准库函数,标准库函数再调用相应的系统调用。系统调用通过软中断或专用指令陷入内核,内核处理请求后调用相应的设备驱动程序,驱动程序控制硬件设备完成操作,最后结果沿原路返回给用户程序。
8.3 printf的实现分析
printf函数的实现是一个从用户空间到硬件显示设备的完整调用链。首先,printf函数接受格式字符串和可变参数,调用vsprintf函数进行格式化处理。vsprintf函数解析格式字符串,根据格式说明符(如%d、%s、%x等)将可变参数转换为相应的字符串表示,并将结果存入缓冲区。
格式化完成后,printf调用write系统函数输出字符串。write函数通过系统调用陷阱进入内核态,在x86架构中传统上使用int 0x80指令触发软中断,现代x86_64架构则使用syscall指令。系统调用号标识要调用的服务,write对应的系统调用号为4(x86)或1(x86_64)。
进入内核后,系统调用处理程序根据调用号执行相应的服务,最终调用字符显示驱动程序。驱动程序将每个字符根据ASCII码在字模库中查找对应的字形点阵数据,然后将这些点阵数据写入显存(VRAM)的相应位置。显存中存储了屏幕上每个像素的RGB颜色信息。
最后,显示芯片按照固定的刷新频率(如60Hz)逐行读取VRAM中的数据,通过信号线将每个像素的RGB分量传输到液晶显示器。显示器根据接收到的RGB值调整每个液晶单元的透光率,背光透过这些液晶单元形成最终的彩色图像,从而完成整个显示过程。
8.4 getchar的实现分析
异步异常处理:
当按键被按下时,键盘控制器会产生一个扫描码,并向中断控制器发送中断请求。CPU检测到中断后,保存当前执行上下文,然后跳转到键盘中断处理程序执行。
键盘中断处理程序的主要工作包括:
1)从键盘控制器的数据端口读取扫描码
2)将扫描码转换为对应的ASCII字符
3)将转换后的字符存入键盘缓冲区
4)向中断控制器发送中断结束信号
5)恢复被中断的程序继续执行
键盘缓冲区管理:
键盘缓冲区通常采用环形缓冲区实现,有两个指针分别指向缓冲区的读位置和写位置。中断处理程序作为生产者向缓冲区写入数据,read系统调用作为消费者从缓冲区读取数据。当缓冲区满时,新输入的字符可能被丢弃;当缓冲区空时,读取操作会阻塞等待。
getchar 函数实现:
getchar函数实际上是对read系统调用的封装。它从标准输入读取一个字符,如果读取成功则返回该字符的ASCII码,如果遇到文件结束或错误则返回EOF。
getchar的基本实现逻辑:
1)声明一个字符变量用于存储读取的字符
2)调用read系统函数从标准输入读取一个字节
3)检查返回值,如果小于等于0则返回EOF
4)否则将读取的字符转换为无符号字符并返回
终端输入处理:
当用户程序调用read读取终端输入时,终端驱动程序会管理一个行缓冲区。用户输入的字符会先存储在这个缓冲区中,直到遇到换行符或达到缓冲区大小。这种设计允许用户使用退格键修改输入,也使得程序能够一次读取整行输入。
特殊按键处理:
1)回车键:产生换行符,触发read返回
2)退格键:删除前一个字符,可能需要更新屏幕显示
3)控制键组合:如Ctrl+C产生中断信号,由信号处理程序处理
8.5本章小结
本章系统分析了hello程序涉及的输入输出管理机制。我们探讨了Linux系统将设备统一抽象为文件的管理方法,以及Unix IO接口提供的标准化操作函数。通过深入分析printf和getchar两个关键函数的实现原理,揭示了从用户空间函数调用到底层硬件操作的完整路径。printf的输出过程包括字符串格式化、系统调用传递、驱动渲染和硬件显示等多个环节;getchar的输入过程则涉及键盘中断处理、缓冲区管理和同步读取等机制。这些IO管理技术共同构建了程序与外部环境的交互桥梁,使得hello程序能够接收用户输入并在屏幕上显示输出信息。理解IO管理机制对于编写高效可靠的系统程序具有重要意义。
(第 8 章1分)
结论
hello程序的生命周期完整展现了计算机系统将高级语言代码转换为实际运行进程的精妙过程:
1)从程序员编写的C语言源文件开始,程序首先经历预处理阶段,头文件被展开,注释被移除,生成完整的翻译单元。
2)编译阶段将高级语言转换为汇编指令,期间进行了多种优化以提升执行效率。
3)汇编器随后将符号化指令编码为机器码,形成可重定位目标文件,其中包含了代码、数据和未解析的符号引用。
4)链接阶段将这些目标文件与库文件结合,解析所有符号,分配最终地址,生成可执行文件。
5)当用户在Shell中执行该程序时 ,操作系统通过fork创建新进程,execve加载程序映像,建立起包含代码段、数据段、堆栈的完整虚拟地址空间。
6)进程执行过程中,处理器通过段页式管理将虚拟地址转换为物理地址,TLB和缓存系统加速了这一访问过程。
7)程序通过系统调用与外界交互,printf将格式化输出经过多层传递最终显示于屏幕,getchar则通过中断机制获取键盘输入。
8)当程序执行完毕或收到终止信号时,进程资源被回收,生命周期终结。
这一从Program到Process的完整转换,以及从无到有再到无的循环,充分体现了现代计算机系统各层次协同工作的复杂性与美感。
通过深入分析hello程序的完整执行过程,我对计算机系统的设计与实现有了更为深刻的理解。计算机系统最令人赞叹之处在于其多层次抽象与协同机制,每一层都向上提供简洁接口,同时隐藏下层复杂性。从编译器优化到硬件加速,从进程隔离到虚拟内存,这些设计无不体现了效率与安全的平衡考量。系统设计者通过标准化接口和约定,使得软硬件能够独立演进又完美配合,这种模块化思想值得所有工程领域借鉴。如果有机会进行创新设计,我设想可以构建更加透明的系统观察工具,将程序执行过程中的所有状态转换------从源代码到二进制、从逻辑地址到物理地址、从系统调用到底层中断------以可视化方式实时展现,使学习者能够直观理解系统内部运作。此外,基于程序行为分析的智能优化系统也值得探索,它能够根据程序特征动态调整编译策略、内存布局和调度策略,实现个性化的系统优化。计算机系统的深邃与优美激励我们不断探索,在理解现有设计的基础上,思考未来系统的可能性。
(结论 0 分,缺失 -1 分)
附件
1)hello.c :源程序
2)hello.i : 预处理后得到的文本文件
3)hello.s : 编译后得到的汇编语言文件
4)hello.o : 汇编后得到的可重定位目标文件
5)hello.elf : 用readelf读取hello.o得到的ELF格式信息
6)hello1.elf : 用readelf解析hello的ELF格式
7)hello.asm : 反汇编hello.o得到的反汇编文件
8)hello2.asm : 反汇编hello可执行文件得到的反汇编文件
9)hello : 可执行文件
参考文献
为完成本次大作业你翻阅的书籍与网站等
1\] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016. \[2\] [https://www.cnblogs.com/pianist/p/3315801.html](https://www.cnblogs.com/pianist/p/3315801.html "https://www.cnblogs.com/pianist/p/3315801.html") \[3\] [https://blog.csdn.net/m0_74985290/article/details/148429423](https://blog.csdn.net/m0_74985290/article/details/148429423 "https://blog.csdn.net/m0_74985290/article/details/148429423") \[4\] ++[https://www.cnblogs.com/fanzhidongyzby/p/3519838.html](https://www.cnblogs.com/fanzhidongyzby/p/3519838.html "https://www.cnblogs.com/fanzhidongyzby/p/3519838.html")++. \[5\] ++[https://www.cnblogs.com/diaohaiwei/p/5094959.html](https://www.cnblogs.com/diaohaiwei/p/5094959.html "https://www.cnblogs.com/diaohaiwei/p/5094959.html")++ **(参考文献** **0** **分,缺失****-1****分)**