关注

CSAPP2025秋大作业-程序人生-Hello’s P2P

摘  要

本文以Hello程序的生命周期为主线,通过对一个简单C程序从源代码到进程执行完毕全过程的分析,系统探究了计算机系统底层的工作原理与协同机制。论文依次阐述了程序在Linux环境下的预处理、编译、汇编和链接过程,揭示了从高级语言到可执行文件的转换机制;深入分析了进程的创建、加载、执行和存储管理,包括虚拟内存、地址转换、缓存与缺页处理等核心系统支持;探讨了进程间的信号通信、异常处理以及I/O设备的管理与接口实现。通过结合理论分析与基于Ubuntu平台的动手实验,完整展现了Hello程序从“Program to Process”(P2P)的构建过程,以及最终资源回收、归于“Zero-0”的完整生命周期。本工作不仅加深了对计算机系统整体层次与交互机制的理解,也为系统级程序的开发、调试与优化提供了实践参考。

关键词:计算机系统;程序生命周期;编译链接;进程管理;虚拟内存;Linux                           

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.2 环境与工具... - 5 -

1.3 中间结果... - 5 -

1.4 本章小结... - 6 -

第2章 预处理... - 7 -

2.1 预处理的概念与作用... - 7 -

2.2在Ubuntu下预处理的命令... - 7 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 9 -

第3章 编译... - 10 -

3.1 编译的概念与作用... - 10 -

3.2 在Ubuntu下编译的命令... - 10 -

3.3 Hello的编译结果解析... - 10 -

3.4 本章小结... - 16 -

第4章 汇编... - 17 -

4.1 汇编的概念与作用... - 17 -

4.2 在Ubuntu下汇编的命令... - 17 -

4.3 可重定位目标elf格式... - 17 -

4.4 Hello.o的结果解析... - 20 -

4.5 本章小结... - 21 -

第5章 链接... - 22 -

5.1 链接的概念与作用... - 22 -

5.2 在Ubuntu下链接的命令... - 22 -

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

5.4 hello的虚拟地址空间... - 25 -

5.5 链接的重定位过程分析... - 27 -

5.6 hello的执行流程... - 28 -

5.7 Hello的动态链接分析... - 32 -

5.8 本章小结... - 33 -

第6章 hello进程管理... - 34 -

6.1 进程的概念与作用... - 34 -

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

6.3 Hello的fork进程创建过程... - 34 -

6.4 Hello的execve过程... - 35 -

6.5 Hello的进程执行... - 35 -

6.6 hello的异常与信号处理... - 35 -

6.7本章小结... - 38 -

第7章 hello的存储管理... - 40 -

7.1 hello的存储器地址空间... - 40 -

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

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

7.4 TLB与四级页表支持下的VA到PA的变换... - 41 -

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

7.6 hello进程fork时的内存映射... - 41 -

7.7 hello进程execve时的内存映射... - 42 -

7.8 缺页故障与缺页中断处理... - 42 -

7.9动态存储分配管理... - 42 -

7.10本章小结... - 43 -

第8章 hello的IO管理... - 44 -

8.1 Linux的IO设备管理方法... - 44 -

8.2 简述Unix IO接口及其函数... - 44 -

8.3 printf的实现分析... - 45 -

8.4 getchar的实现分析... - 45 -

8.5本章小结... - 46 -

结论... - 46 -

附件... - 48 -

参考文献... - 49 -

第1章 概述

1.1 Hello简介

Hello从“出生”到“死亡”,经历了P2P和020

P2P(From Program to Process):

Hello从程序员编写的C源程序(hello.c)开始,经过预处理、编译、汇编和链接成为可执行文件;随后在Shell中通过OS的fork()创建子进程,再经execve()加载到内存,由mmap()映射虚拟地址空间,最终成为一个活跃的进程。OS为它分配时间片,CPU通过取指、译码、执行的流水线运行其指令,在键盘、屏幕等I/O设备的协作下完成输出。

020(From Zero-0 to Zero-0):

Hello进程运行中,OS与MMU通过多级页表、TLB和Cache管理其虚拟地址到物理地址的转换;执行完毕后,进程通过exit()终止,OS回收其PCB、内存、文件描述符等所有资源,Shell等待并清理其状态。最终,Hello进程如同从未存在过,仅在计算机系统的协同运作中留下短暂痕迹,完整演绎了从创建到消亡的生命周期。

1.2 环境与工具

硬件环境:

处理器:Intel(R) Core(TM) i9-14900HX 2.20 GHz

机带RAM:16.0 GB

软件环境:

Windows 11 64位; Vmware 17; Ubuntu 22.04 LTS 64位

开发与调试工具:Visual Studio 2022 64位;gedit+gcc;edb;objdump

1.3 中间结果

hello.i:hello.c预处理后的输出文件。用于观察预处理效果。

hello.s:hello.i编译后的汇编代码文件。用于分析编译器生成的控制流与数据操作。

hello.o:hello.s汇编后的可重定位目标文件。可用 objdump、readelf 等工具分析其ELF结构。

hello:hello.o最终链接生成的可执行文件。可用 objdump、readelf 等工具分析其ELF结构,可直接在系统中加载运行。

o_asm.txt:hello.o的反汇编文本输出。展示目标文件的机器指令与汇编助记符对照,用于分析未链接时的代码布局与重定位需求。

out_asm.txt:hello可执行文件的反汇编文本输出。展示链接后具有实际虚拟地址的指令序列,便于与 o_asm.txt 对比,观察重定位与地址绑定的具体结果。

1.4 本章小结

本章对Hello程序的全生命周期进行了概述,介绍了从源代码到可执行文件、再到进程执行的完整流程,以及开发过程中使用的软硬件环境与工具。通过P2P(Program to Process)和020(Zero-0 to Zero-0)的视角,阐述了Hello从“出生”到“消亡”的整个过程,为后续章节的深入分析奠定基础。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

    预处理是C编译流程的初始阶段,本质是对源代码进行纯文本处理。 预处理器执行头文件包含(#include)、宏展开(#define)、条件编译(#ifdef/#endif)等指令,将程序员编写的带宏和包含语句的源代码转换为完全展开的“纯净”代码。这个过程会删除所有注释、合并空白字符,并根据条件编译指令选择性包含或排除代码段,生成供下一阶段使用的.i文件。

2.1.2 预处理的作用

    预处理的核心作用是实现代码模块化、可配置性和跨平台支持。 它允许通过头文件组织代码结构,借助宏定义实现常量管理和代码复用,利用条件编译针对不同平台或调试状态编译不同代码路径。预处理不进行语法检查或类型验证,仅是机械的文本替换,为后续的编译阶段准备标准化、展开完全的源代码文本。

2.2在Ubuntu下预处理的命令

图 1 Ubuntu下预处理指令gcc及结果

还可使用cpp命令预处理:cpp hello.c > hello.i

2.3 Hello的预处理结果解析

预处理后的hello.i为可读的文本文件,可使用gedit查看hello.i的内容。观察发现,hello.i共3902行,原本hello.c中的注释全部消失,前几行为有关源程序的一些信息:

图 2 hello.i的1-6行

随后为导入的头文件的具体展开:

图 3 hello.i的头文件展开示例

最后为hello.c的源代码,如下图,除注释与以“#”开始的语句被删除,其余部分与源代码保持一致

图 4 hello.i的最后部分

2.4 本章小结

本章详细介绍了C程序编译流程中的预处理阶段,阐述了预处理的概念、作用,并在Ubuntu环境下演示了预处理的命令与结果。通过对hello.i文件的分析,展示了头文件展开、宏替换和条件编译等预处理机制,说明了预处理如何为后续编译阶段准备“纯净”的源代码。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译阶段是C构建流程的核心环节,将预处理后的源代码转换为目标平台的汇编语言。

3.1.2 编译的作用

编译阶段的核心作用是实现高级语言到低级语言的语义转换和性能优化。 它将平台无关的C/C++代码转化为与硬件架构密切相关的汇编代码,同时进行常量传播、死代码消除、循环优化等编译优化,在保持程序逻辑不变的前提下提升运行时性能。生成的汇编代码为后续的汇编阶段提供了可直接翻译为机器指令的文本表示。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

图 5 Ubuntu下编译指令gcc及结果

还可使用cc1命令预处理:/usr/lib/gcc/x86_64-linux-gnu/11/cc1 hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 数据类型处理

字符串常量:

存储在.rodata只读数据段,中文使用UTF-8编码存储为八进制转义序列,通过标签.LC0、.LC1引用,如.L6错误处理部分,第一行将$.LC0存入%edi

图 6 字符串常量定义

图 7 字符串引用实例

整型常量:

作为立即数直接嵌入指令中,如下图判定main函数传入参数数量是否为四个时,将%edi(argc)与立即数$5比较

图 8 整数常量处理实例

局部变量:

高频使用的局部变量分配到寄存器中,遵循寄存器使用约定(调用者保存/被调用者保存),如下图局部变量i的处理,i存入寄存器%ebp

图 9 对局部变量i的操作

3.3.2 赋值操作

见图9第一个红框,使用movl指令将立即数$0存入寄存器%ebp,完成了对局部变量i赋初值0

3.3.3 类型转换

hello.c中的atoi(argv[4])为类型转换,具体实现如下图,第一个红框是将argv[4]存入寄存器%rdi,作为参数传入函数strtol,将原字符串类型转换为long,并存入%rax作为返回值,完成显式类型转换(字符串→long)。调用sleep时,将%eax的值(%rax的低32位)存入%edi作为参数,实现了隐式的类型转换(long→int)。最终实现了字符串→int的类型转换

图 10 类型转换实例

3.3.4 算术操作

见图9第二个红框,使用addl $1 %ebp实现i++操作

3.3.5 关系操作

判定main函数传入参数数量是否为四个时,通过不相等操作(判断%edi(argc)与立即数$5是否不相等),通过cmpl和jne实现,具体见下图

图 11 不相等操作实例

cmpl比较$5与%edi,jne为如果判断两者不等,则跳转.L6错误处理,否则继续执行

3.3.6 数组操作

编译器通过保存argv地址,并通过偏移量计算访问数组对应下标的元素

图 12 数组操作实例

从上图可以看出,寄存器先保存argv基地址到%rbx,然后通过偏移计算访问数组中别的元素(+8为地址+8字节,64位操作系统中即为数组下一元素)

3.3.7 控制转移

如下图所示,绿色标注为if判定,判断若$5与%edi(argc)不等,则跳转.L6错误处理,否则继续执行

红色和蓝色标注为for循环。首先给%ebp(局部变量i)存入初值0,然后跳转.L2,比较i与9,jle为如果i小于等于9则跳转.L3循环体部分,注意到循环体最后进行了i++操作后,再次进入.L2进行比较,若i不小于等于9,则不执行jle的跳转到循环体,而是如蓝色箭头指示继续执行

图 13 控制转移

3.3.8 函数操作

main函数参数传递:

图 14 main函数传参

根据上面的分析,可知%edi中为main函数第一个参数argc(int),%rsi为第二个参数argv(char*类型)

printf函数调用:

图 15 printf函数调用

由上图,传递参数依次为1(立即数,作为标志flag)、$.LC1(格式字符串常量)、argv[1](学号)、argv[2](姓名)、argv[3](手机号),返回值寄存器%eax存入立即数0,忽略返回值(后续未使用)

atoi函数调用(被编译器优化,实际调用函数为strtol):

图 16 atoi函数(strtol函数)调用

由上图,传递参数依次为argv[4](秒数,字符串)、0(立即数,实际为endptr=NULL的NULL)、10(立即数,作为基数),返回值存入寄存器%rax,返回值数据类型为long

sleep函数调用:

图 17 sleep函数调用

由上图,传递参数为%eax(此时strtol函数返回值存放在%rax中,%eax为%rax的低32位,实际进行了一次long→int的类型转换)(秒数,int)

其他函数调用(puts、exit、getchar):

puts和exit函数在.L6错误处理中调用,用于输出用法提示和退出程序

getchar函数可用于循环结束后等待输入,延迟程序的结束,输入回车后停止

3.4 本章小结

本章分析了编译阶段的概念与作用,展示了将预处理后的.i文件转换为汇编代码的过程。通过对hello.s文件的解析,详细说明了编译器如何处理数据类型、控制结构、函数调用等C语言特性,并展示了其在x86-64架构下的汇编实现,体现了编译阶段在语义转换和初步优化中的关键作用。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念

汇编是机器指令的符号化表示,通过助记符代替二进制编码直接对应CPU操作。

4.1.2 汇编的作用

汇编的核心作用是实现底层硬件操作与性能极限调优,提供无抽象层的精确控制。

4.2 在Ubuntu下汇编的命令

图 18 Ubuntu下汇编指令gcc及结果

还可使用as命令汇编:as hello.s -o hello.o

4.3 可重定位目标elf格式

4.3.1 elf

图 19 hello.o的elf头

在终端输入命令readelf -h hello.o即可解析elf头,结果如上图

ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。具体见上图展示的信息

4.3.2 节区头表

图 20 hello.o的节区头表

在终端输入readelf -S hello.o查看节头,结果如上图

通过分析节区头表可以发现,hello.o中一共有15个节,7个重定位条目,5个全局符号。在这些重定位条目中,有两个对应.rodata节中的数据地址,显然它们是printf使用的两个字符串地址。另外5个重定位条目都是被call指令调用过的函数地址,分别对应puts、exit、__printf_chk、strtol和sleep函数。

4.3.3 符号表

图 21 hello.o的符号表

在终端输入readelf -s hello.o查看符号表,结果如上图

根据符号表分析,hello.o包含13个符号条目,其中仅有main函数是已定义的全局函数,位于.text节区(Ndx=1),大小为135字节。其余7个符号均为未定义的外部引用(Ndx=UND),包括puts、exit、__printf_chk、strtol、sleep、stdin和getc,这些都需要在链接时从C标准库中解析地址。此外还有4个局部节区符号用于内部引用。

4.3.4 重定位表

图 22 hello.o的重定位表

在终端输入readelf -r hello.o可查看可重定位段信息,结果如上图

重定位表显示hello.o中共有10个重定位项,其中9个位于.text节区,1个位于.eh_frame节区。

4.4 Hello.o的结果解析

图 23 使用objdump查看hello.o的反汇编

图 24 使用objdump生成hello.o的反汇编文件

观察反汇编结果,左边为十六进制的机器代码,每行为一组,每组都是一条指令。右边为等价的汇编语言注释

4.4.1 立即数编码差异

图 25 hello.o反汇编立即数编码实例

hello.o的反汇编中,机器码:bf(mov edi, imm32)+ 01 00 00 00(小端),实际值:0x00000001(32位立即数)。hello.s语言movl $1, %edi中的立即数为$1

4.4.2 分支转移差异

图 26 hello.o反汇编分支转移编码实例

    hello.o的反汇编中,75为jne的操作码,0a为相对偏移,通过偏移计算分支转移。hello.s语言为jne  .L6,通过符号标签进行分支转移

4.4.3 函数调用指令差异

图 27 hello.o反汇编函数调用编码实例

hello.o的反汇编中,e8为call的操作码,后跟32位相对偏移,此处为00 00 00 00占位符(需要重定位)。hello.s语言为call puts

4.5 本章小结

本章介绍了汇编阶段的概念与作用,展示了将汇编代码转换为机器指令、生成可重定位目标文件的过程。通过分析hello.o的ELF格式、符号表与重定位表,说明了目标文件的结构及其在链接前的状态,并通过对比机器码与汇编指令,揭示了指令编码与符号引用的映射关系。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是将多个目标文件与库文件合并为单一可执行文件的关键过程,负责解析符号引用、分配内存地址并重定位代码数据。 

5.1.2 链接的作用

链接的核心作用是实现模块化程序构建与代码复用,在编译与运行之间架起桥梁。 它既完成多个编译单元的物理合并与地址绑定,也支持动态链接实现库函数共享,最终生成满足操作系统加载要求的可执行文件格式

5.2 在Ubuntu下链接的命令

图 28 Ubuntu下链接指令ld及结果

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

5.3.1 elf

图 29 hello的elf头

      查看hello的ELF头,发现类型为EXEC,说明hello为可执行文件,且有28个节头,与hello.o不同

5.3.2 节区头表

图 30 hello的节区头表

      hello可执行文件包含28个节区,采用非PIE固定地址布局:代码段从0x401000开始包含.text、.plt等节区,数据段从0x402000开始包含.rodata、.data和.bss。链接过程将hello.o的14个节区扩展为完整运行时结构,为所有符号分配了绝对地址并建立了动态链接所需的PLT/GOT机制。

5.3.3 程序头表

图 31 hello的程序头表

观察上图hello的程序头表,该可执行文件采用四段式经典内存布局:第一个LOAD段(0x400000)包含动态链接元数据,第二个LOAD段(0x401000)为可执行代码段包含.text和PLT,第三个LOAD段(0x402000)为只读数据段,第四个LOAD段(0x403e50)为可读写数据段包含.data、.bss和GOT。程序入口点位于0x401180,所有段均按4KB页面对齐,并通过GNU_RELRO机制保护动态链接数据。

5.4 hello的虚拟地址空间

使用edb加载hello,在视图中打开内存区域,总览如下

图 32 hello内存区域总览

使用edb加载hello,可以看到hello可执行部分起始地址为0x400000,与5.3信息一致

图 33 hello起始地址

    由5.3信息可知,.init段起始地址为0x401000,结束地址为0x40101b

图 34 .init段信息

    .interp段起始地址为0x4002e0,结束地址为0x4002fb

图 35 .interp段信息

    .interp段内容为动态链接器路径字符串 /lib64/ld-linux-x86-64.so.2

.text段起始地址为0x4010f0,结束地址为0x4011b5

图 36 .text段信息

    .text段信息实际为main函数代码,见下图

图 37 main函数部分代码

    .rodata段起始地址为0x402000,结束地址为0x402043

图 38 .rodata段信息

    .rodata段包含两个字符串常量,只可读,不可写入

5.5 链接的重定位过程分析

图 39 使用objdump查看hello的反汇编(部分)

图 40 使用objdump生成hello的反汇编文件

    hello的反汇编多了很多节,且4.4.3中函数调用的占位符替换为了具体的相对偏移,且被调用的puts函数也有了具体地址

图 41 函数调用偏移

    接下来以puts函数为例,介绍链接和重定位

首先,观察hello.o的重定向表(图22)见如下一行

00000000001f  000600000004 R_X86_64_PLT32   0000000000000000 puts - 4

其中000600000004说明此处应绑定第0x6个符号,同时编译器知道这里是相对寻址,查询hello.o的符号表(图21)第6行

6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

在此处绑定puts的地址

在hello的反汇编中查看puts函数

图 42 hello反汇编puts函数

见上图,puts的地址为0x401090,而见图41,call puts时地址为0x40110e

40110e: e8 7d ff ff ff         call   401090 <puts@plt>

而puts的地址为0x401090,偏移值为0xffffff7d,由于小端存储,重定位目标处填入7d ff ff ff

5.6 hello的执行流程

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

使用edb执行hello,正确输入参数,并在关键部分设置断点后,按F9继续执行,程序会先进入动态链接器,进行如下关键调用链ld.so:_start → ld.so:dl_start → ld.so:_dl_sysdep_start → ld.so:_dl_start_final → ld.so:_dl_relocate_object (重定位PLT/GOT) → ld.so:_dl_init (初始化) → 调用 hello:_init (0x401000)

图 43 动态链接器

继续按F9执行,进入hello的_start,设置栈、参数、清理函数,调用__libc_start_main初始化C运行时

图 44 hello的_start

继续按F9执行,进入main函数

图 45 进入hello!main函数

继续按F9,调用printf_chk函数

图 46 调用hello!printf_chk

继续运行,调用strtol函数

图 47 调用hello!strtol

之后会调用sleep函数

图 48 调用hello!sleep

上述三个调用为循环体内,循环结束后,调用getc函数

图 49 调用hello!getc

继续执行,输入回车后,程序结束运行

图 50 程序正常退出

若使用edb执行hello时,未正确输入参数

图 51 不正确输入参数

则不会进入循环体,而是调用puts

图 52 调用hello!puts

随后调用exit

图 53 调用hello!exit

之后程序退出

图 54 不正确输入参数程序退出

5.7 Hello的动态链接分析

查询hello的节区头表,得到.got.plt起始地址为0x404000                                                                                                                                                                                                                                         

图 55 hello节区头表部分

通过edb打开hello调试,观察动态链接前后该处信息的变化:

图 56 动态链接前

图 57 动态链接后

可以看出,调用函数后该处信息发生了变化

hello的反汇编中,函数名后有@plt后缀的均为动态链接,如下图所示:

图 58 hello程序的动态链接项目

5.8 本章小结

本章阐述了链接的概念与作用,分析了如何将多个目标文件与库文件合并为可执行文件。通过解析hello的ELF格式、虚拟地址空间布局、重定位过程与动态链接机制,揭示了链接器在符号解析、地址分配与运行时绑定中的关键角色,展示了Hello从目标文件到可执行进程的完整转换过程。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程是计算机系统中执行中程序的实例,是操作系统进行资源分配和调度的基本单位。

6.1.2 进程的作用

其作用在于实现程序的并发执行,隔离不同任务的资源与状态,从而提高系统利用率和可靠性。

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

6.2.1 Shell-bash的作用

    Shell(以Bash为代表)是操作系统的命令解释器与用户界面,核心作用在于充当用户与系统内核之间的桥梁。它接收并解释用户输入的命令或脚本,将其转化为系统内核能够理解的操作,从而启动、管理和协调程序(进程)的执行,实现文件管理、进程控制、环境配置等核心功能,同时也是一个强大的脚本编程环境,用于自动化任务。

6.2.2 Shell-bash的处理流程

首先读取并解析命令行,进行变量替换、通配符扩展和语法分析;然后根据解析结果,区分内部命令或外部程序,通过系统调用创建子进程来执行外部命令,并处理输入输出重定向与管道;最后,Shell等待命令执行完成,捕获其退出状态,并返回提示符等待下一条命令。这个过程循环往复,构成了交互式会话的基础。

6.3 Hello的fork进程创建过程

当用户在Shell中输入./hello并按下回车后,Shell(如Bash)会解析该命令行。当识别到./hello是一个外部可执行程序时,Shell会调用fork()系统调用以创建一个新的子进程。创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。父进程与创建的子进程之间最大的区别在于它们有不同的PID。fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程

fork() 创建子进程后,子进程通常立即调用 execve() 来加载并执行 hello 程序。execve() 会将 hello 的代码段(.text)、数据段等映射到新地址空间。同时,将命令行参数和环境变量压入新堆栈,并设置好寄存器状态(如 argc、argv),使其符合C语言 main 函数的调用约定。最后,将程序计数器(PC)设置为新程序的入口地址。只有当出现错误时,例如找不到filename,execve才会返回到调用程序,调用成功不会返回。与fork不同,fork一次调用两次返回,execve一次调用从不返回。

6.5 Hello的进程执行

Hello进程执行时,操作系统通过 调度、上下文切换和模式转换 来管理其运行。

(1)调度与时间片:Hello进程被放入就绪队列。操作系统为其分配一个 CPU时间片(如几毫秒)。在时间片内,它独占CPU执行指令。时间片耗尽、主动阻塞(如调用 sleep)或等待I/O时,CPU会被剥夺。

(2)上下文切换:当需要切换到另一进程时,内核保存Hello进程的 上下文(寄存器值、程序计数器等)至其进程控制块(PCB),并恢复下一进程的上下文到CPU。此过程对Hello进程透明。

(3)用户态与内核态转换:

用户态:Hello进程的普通指令(循环、计算等)在此态执行,权限受限。

核心态:当进行系统调用(如 write、exit)或发生异常时,CPU通过软中断(如 syscall)切换到内核态。内核执行特权操作(如I/O、进程管理)后,再返回用户态并继续执行Hello的下一条指令。

6.6 hello的异常与信号处理

6.6.1 异常类型

(1)中断:异步发生的。在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。

(2)陷阱:陷阱是有意的异常,是执行一条指令的结果。hello执行sleep函数的时候会出现这个异常。

(3)故障:由错误情况引起,可能能够被故障处理程序修正。在执行hello时,可能出现缺页故障。

(4)终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。

6.6.2 信号

(1)SIGINT:来自键盘的中断。当用户按下Ctrl+C时发送,通常用于中断程序。

(2)SIGTSTP:来自终端的停止信号。当用户按下Ctrl+Z时发送,用于暂停程序。

(3)SIGTERM:软件终止信号。

6.6.3 具体信号处理

(1)不停乱按

图 59 不停乱按键盘

乱按键盘没有改变原输出结果,不影响程序正常运行

(2)按回车

图 60 按回车

观察发现,不停按回车,没有影响循环的printf,循环结束后,输入的回车中其中一次被hello的getchar读取并结束了程序,其余回车被shell读取并进行刷新换行

(3)按Ctrl-Z

图 61 Ctrl-Z结果

按Ctrl-Z后向进程发送SIGSTP信号,进程接收到该信号之后会将该作业挂起,但不会回收。

图 62 ps命令及结果

由上图可知,PID为6634的进程hello仍在运行

图 63 jobs命令及结果

运行jobs指令,我们可以得知hello的后台job id=1。

图 64 fg命令及结果

fg命令用于将后台作业(在后台运行的或者在后台挂起的作业)放到前台终端运行。

图 65 kill命令及结果

kill命令用于结束进程

图 66 pstree命令及部分结果

(4)按Ctrl-C

图 67 按Ctrl-C结果

输入Ctrl+C,Ctrl-C命令内核向前台发送SIGINT信号,终止了前台作业。

6.7本章小结

本章探讨了Hello进程的管理机制,包括进程的概念、Shell的处理流程、进程的创建与执行,以及异常与信号的处理。通过分析fork、execve、进程调度与上下文切换,说明了操作系统如何管理Hello进程的生命周期,并展示了信号机制对进程行为的控制与影响。

(第62分)

7章 hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址是程序代码中的地址,表现为段选择符加偏移量;

(2)线性地址(即虚拟地址)是逻辑地址经段式转换后的结果,在x86-64平坦模型中等于偏移量;

(3)虚拟地址是hello进程看到的连续64位空间,从0x400000开始布局代码、数据和堆栈;

(4)物理地址是实际内存芯片上的硬件地址,由MMU通过页表将虚拟地址转换得到。

四种地址共同构成从程序到硬件的完整转换链。

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

当hello程序执行时,处理器会根据指令所在的代码段选择符(CS寄存器)从全局描述符表(GDT)中获取相应的段描述符。由于在64位模式下,代码段、数据段和堆栈段的基地址都被设置为0,段限长设置为最大值,因此逻辑地址中的偏移量部分直接成为线性地址,无需进行实际的基地址相加计算。

具体变换过程如下:hello程序中的逻辑地址包含16位的段选择符和偏移量。处理器使用段选择符的高13位作为索引,从GDT或LDT中找到对应的段描述符。该描述符包含段的基地址、限长和访问权限等信息。由于基地址为0,线性地址就等于偏移量值。这个过程就称作段式内存管理。

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

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

Linux系统为hello进程建立独立的多级页表结构,将连续的虚拟地址空间划分为固定大小的页面(通常为4KB),并映射到物理内存的页框中。当hello进程访问某个虚拟地址时,内存管理单元(MMU)自动执行地址转换,整个过程对应用程序透明。

地址转换的核心在于页表查找。以典型的4级页表为例,64位虚拟地址被划分为多个索引字段和页内偏移字段。MMU首先从进程的页表基址寄存器(CR3)获取顶级页目录的物理地址,然后依次使用虚拟地址的各级索引在页目录、页表等结构中查找。最终获得目标页框的物理基地址后,将其与虚拟地址中的页内偏移量组合,形成完整的物理地址。

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

当hello进程访问虚拟地址时,MMU首先在TLB中查找对应的转换条目。如果TLB命中,物理地址几乎可以立即获得,避免了耗时的多级页表遍历,这一过程通常在1-2个时钟周期内完成。

若TLB未命中,则需进行完整的四级页表遍历。在x86-64架构下,48位有效虚拟地址被划分为四个9位的索引字段和一个12位的页内偏移字段。MMU依次使用这些索引在各级页表中查找:从CR3寄存器开始,经过PML4、PDPT、PD和PT四级查找,最终获得目标页面的物理页框号。将页框号与12位页内偏移组合即得52位的物理地址。

完成四级页表遍历后,MMU不仅返回物理地址,还会将这一转换结果存入TLB中,以备后续访问。TLB作为关键的性能优化组件,对hello程序的性能有显著影响。当hello进程频繁访问相同或邻近的虚拟地址时(如循环处理数组),TLB能够保持高命中率,减少地址转换开销。

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

现代CPU通常采用三级缓存设计:每个核心独享的L1和L2缓存,以及多个核心共享的L3缓存。hello进程的数据访问首先在L1缓存中查找,若命中则可在几个时钟周期内返回数据;若未命中则逐级向L2、L3缓存查找,最后才访问主内存。

缓存查找基于物理地址进行。缓存控制器将物理地址划分为标记、索引和偏移字段。索引用于确定缓存组,标记用于匹配具体缓存行,偏移字段则定位缓存行内的具体字节。三级缓存形成了严格的包含关系,L3缓存包含L2缓存的内容,L2缓存包含L1缓存的内容。hello程序的空间局部性和时间局部性特征会影响缓存命中率,良好的访问模式可使数据驻留在高速缓存中,显著提升性能。

7.6 hello进程fork时的内存映射

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

当shell通过fork系统调用创建hello子进程时,内核首先为新创建的hello子进程复制父进程的虚拟内存描述结构,包括页表、虚拟内存区域(VMA)列表等元数据,但物理内存页面并不立即复制。

内核将父子进程共享的物理页面的页表项都标记为只读,并在相关数据结构中记录这些页面被多个进程共享。当hello子进程或父进程后续试图写入这些共享页面时,会触发页保护异常。异常处理程序识别到这是COW页面,内核会分配新的物理页面,复制原页面内容,更新故障进程的页表项指向新页面,并将其标记为可写。

这种机制使得fork操作非常高效,hello子进程的创建几乎只涉及内核数据结构的复制。对于hello程序而言,其代码段和只读数据段在整个生命周期可能保持共享状态,因为它们是只读的;而堆栈等可写区域则会在首次写入时分离。COW技术既节省了物理内存,也减少了进程创建的开销,是Unix类系统进程创建的重要优化手段。

7.7 hello进程execve时的内存映射

当hello子进程调用execve加载新程序时,操作系统会完全替换进程的虚拟地址空间。首先,内核释放hello进程原有的所有内存映射和页表,清空虚拟地址空间。然后根据hello可执行文件的程序头表,重新建立虚拟内存布局。

内核将hello文件的各个段映射到虚拟地址空间:.text代码段映射为只读可执行,.rodata只读数据段映射为只读,.data数据段映射为可读写。这些映射最初可能只是占位符,实际页面按需加载。对于动态链接的hello程序,内核还会将动态链接器映射到进程空间,并设置入口点为链接器的_start函数。

execve还会设置堆栈区域,将命令行参数和环境变量压入新栈顶,初始化堆起始位置(brk)。通过内存映射机制,不同进程可以共享相同的只读代码段(如C库函数),提高了内存利用率。execve完成映射后,进程拥有全新的、专为hello程序定制的地址空间,准备开始执行。

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

hello访问未加载或权限不足的页面时,MMU触发缺页中断。内核异常处理程序分析原因:若是合法缺页(如首次访问),则分配物理页框,必要时从磁盘加载内容,更新页表后恢复执行;若是非法访问(如越界),则向hello发送SIGSEGV信号终止进程。缺页机制实现了虚拟内存的按需调页。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

本章分析了hello程序的存储管理机制,涵盖地址空间概念、段式与页式地址转换、TLB与缓存优化、进程创建与加载时的内存映射,以及缺页处理。这些技术共同保障了hello进程内存访问的高效性、安全性和隔离性。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux系统采用统一的 “一切皆文件” 模型来管理输入输出设备。无论是磁盘、键盘、显示器还是网络接口,在用户空间都被抽象为文件描述符,可以通过标准的文件操作接口进行访问。这种设计极大简化了应用程序的开发,使得hello程序可以使用与操作普通文件相同的系统调用来处理输入输出。

在底层,Linux通过设备驱动程序、虚拟文件系统(VFS)和设备文件三层结构来管理IO设备。每个设备在/dev目录下都有对应的设备文件节点,这些节点关联到相应的设备驱动程序。当hello进程调用如write或read等系统调用时,VFS层将请求路由到适当的设备驱动,由驱动与硬件交互完成实际的数据传输。

8.2 简述Unix IO接口及其函数

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

Unix提供了五类基本的IO函数,构成了Unix IO接口的核心:

1. 打开与关闭文件

open():打开文件或设备,返回文件描述符。hello程序通过标准输入输出文件描述符(0,1,2)与终端交互。

close():关闭文件描述符,释放相关资源。

2. 读写操作

read():从文件描述符读取数据。当hello调用getchar()时,底层最终通过read从标准输入(文件描述符0)读取字符。

write():向文件描述符写入数据。hello中printf()函数最终调用write向标准输出(文件描述符1)写入格式化的字符串。

3. 改变文件位置

lseek():移动文件读写位置,适用于随机访问设备。

4. 元数据操作

stat():获取文件状态信息,如大小、权限等。

5. IO多路复用

select(), poll(), epoll():同时监控多个文件描述符的IO状态。

Unix IO接口将所有的IO操作都统一为对文件描述符的读写,这种一致性设计使得hello程序能够以相同的方式处理来自不同设备的输入输出。

8.3 printf的实现分析

printf是hello程序中使用最频繁的输出函数,其实现涉及多层软件栈的协作:

1. 格式化处理阶段

当hello调用printf("Hello %s\n", name)时,首先进入C标准库的printf实现。该函数解析格式字符串,根据格式说明符(如%s, %d)将参数转换为字符串形式。这一过程通过vsprintf或类似函数完成,它在内部缓冲区中构建完整的输出字符串。

2. 系统调用阶段

格式化后的字符串需要输出到终端。对于输出到标准输出的情况,printf最终调用write系统调用。在Linux x86_64架构上,这是通过syscall指令实现的,该指令触发从用户态到内核态的切换。

3. 内核处理与设备驱动

进入内核后,write系统调用经过VFS层,最终到达终端设备驱动。对于控制台输出,内核将字符串写入终端的输出缓冲区。如果输出目标是一个终端,内核还会处理特殊的控制字符(如换行符\n转换为回车换行序列)。

4. 硬件输出阶段

终端设备驱动程序通过硬件接口将字符数据传输到显示控制器。在字符模式下,每个字符的ASCII码被转换为字模点阵,写入显存(VRAM)的相应位置。显示控制器以固定的刷新频率(如60Hz)扫描VRAM,将像素信息通过视频信号发送到显示器,最终hello程序的输出呈现在屏幕上。

8.4 getchar的实现分析

getchar是hello程序用于等待用户输入的函数,其实现同样涉及复杂的异步处理机制:

1. 函数入口与缓冲

当hello调用getchar()时,C标准库首先检查输入缓冲区是否已有字符。如果缓冲区为空,则调用read系统调用从标准输入(文件描述符0)读取数据。默认情况下,标准输入采用行缓冲模式,意味着read会等待用户输入完整的行(以回车结束)后才返回。

2. 键盘中断处理

用户按键触发硬件中断。键盘控制器将扫描码通过中断请求线发送给CPU,CPU暂停当前执行的hello进程,切换到键盘中断处理程序。该处理程序读取扫描码,转换为ASCII字符或特殊键码,并存储在内核的输入缓冲区中。

3. 终端行规则处理

Linux内核的终端行规则模块对原始输入进行预处理,包括回显(echo)字符到屏幕、处理退格等编辑字符、识别控制字符(如Ctrl-C产生SIGINT信号)等。hello程序看到的是经过行规则处理后的规范输入。

4. 从内核到用户空间

当用户按下回车键时,整行字符从内核缓冲区复制到C标准库的缓冲区,read系统调用返回。getchar从缓冲区读取第一个字符并返回给hello程序。后续的getchar调用可能直接从缓冲区读取,而不需要进入内核。

5. 阻塞与非阻塞

默认情况下,getchar是阻塞的:如果没有任何输入可用,调用进程(如hello)会被置为睡眠状态,直到有数据可读。这通过内核的等待队列机制实现,允许其他进程在hello等待输入时使用CPU。

8.5本章小结

本章介绍了Linux的I/O设备管理方法与Unix I/O接口,并深入分析了printf和getchar函数的实现机制。通过阐述从用户态调用到内核态处理、再到硬件交互的完整流程,揭示了I/O子系统在Hello程序输入输出中的关键作用,体现了操作系统对设备资源的统一抽象与管理。

(第8 1分)

结论

Hello 程序作为计算机系统中一个简洁而完整的执行实例,其生命周期深刻体现了从高级语言到硬件执行的全栈协同机制。以下从系统视角逐层总结其所经历的过程:

一、程序构建阶段

Hello 的旅程始于 C 源代码 hello.c,经预处理器展开头文件与宏,生成纯净的 .i 文件;编译器进行词法、语法与语义分析,生成平台相关的汇编代码 .s;汇编器将助记符转换为机器指令,生成可重定位目标文件 .o,其中符号尚未绑定,地址均为相对偏移;链接器解析外部符号,合并多个 .o 文件与库,完成重定位与地址分配,生成可执行文件 hello。此时,程序已具备完整的 ELF 格式,包含代码段、数据段、符号表与动态链接信息,等待加载执行。

二、进程创建与加载

在 Shell 中输入 ./hello 后,bash 解析命令并调用 fork() 创建子进程,复制父进程的虚拟地址空间;子进程通过 execve() 系统调用加载 hello,操作系统清空原有内存映射,根据 ELF 程序头建立新的地址空间布局,将 .text、.rodata、.data 等段映射至虚拟内存,并设置堆、栈初始状态;动态链接器 ld.so 介入,解析共享库依赖,完成 GOT/PLT 的重定位,最终将控制权移交至程序入口 _start。

三、运行时执行与存储管理

CPU 从 _start 开始取指执行,初始化运行时环境后调用 main 函数。在指令执行过程中,MMU 借助页表完成虚拟地址到物理地址的转换,TLB 加速这一过程;多级缓存(L1/L2/L3)根据局部性原理缓存热点数据,减少内存访问延迟;若发生缺页,操作系统按需调页,从磁盘加载缺失页面。进程在执行中可能因系统调用(如 write、sleep)、异常或中断陷入内核态,内核处理完毕后返回用户态继续执行。

四、I/O 与交互处理

printf 调用经过格式解析、缓冲区处理,最终通过 write 系统调用将字符串送入标准输出流,经终端驱动程序、显卡缓冲,最终显示于屏幕;getchar 则通过 read 系统调用阻塞等待用户输入,键盘中断触发字符采集,经行规则处理后返回程序。信号机制(如 Ctrl+C 发送 SIGINT)允许用户或系统中断进程执行,操作系统调用相应的信号处理例程,实现进程的交互控制。

五、进程终止与资源回收

main 函数返回后,运行时环境调用 exit() 进行清理工作,操作系统回收进程占用的所有资源:释放虚拟内存空间、关闭打开的文件描述符、销毁内核数据结构(如 PCB),并向父进程发送 SIGCHLD 信号。最终,hello 进程从系统中彻底消失,完成其从零到零的生命周期。

通过对 Hello 程序全过程的梳理,我对计算机系统的设计与实现有以下深切感悟:

计算机系统本质上是一系列抽象的层次化结构,每一层都向其上层隐藏了下层的复杂性,同时提供简洁而强大的接口。从高级语言到机器指令,从虚拟内存到物理存储,从进程抽象到硬件执行,这种分层与封装不仅降低了软件开发的复杂度,也赋予系统极强的可扩展性与可移植性。此外,系统中处处体现了时空权衡的思想:缓存机制以空间换时间,延迟绑定以时间换空间,写时复制则在进程创建中取得平衡。这些设计并非偶然,而是计算机科学在资源约束下持续优化的智慧结晶。

在创新理念方面,我认为未来系统设计可朝着“智能感知与自适应优化”方向发展。例如,可引入轻量级运行时 profiling 机制,动态识别程序的执行模式(如计算密集、I/O 密集、内存访问模式),并自适应地调整调度策略、缓存分配、页面置换算法甚至编译优化选项。同时,借助硬件支持的可编程内存控制器与网络栈,系统可将存储与通信语义更高层次地暴露给应用程序,实现更细粒度的资源控制与协同优化,进一步提升整体能效与执行效率。

(结论0分,缺失-1分)

附件

hello.i:hello.c预处理后的输出文件。用于观察预处理效果。

hello.s:hello.i编译后的汇编代码文件。用于分析编译器生成的控制流与数据操作。

hello.o:hello.s汇编后的可重定位目标文件。可用 objdump、readelf 等工具分析其ELF结构。

hello:hello.o最终链接生成的可执行文件。可用 objdump、readelf 等工具分析其ELF结构,可直接在系统中加载运行。

o_asm.txt:hello.o的反汇编文本输出。展示目标文件的机器指令与汇编助记符对照,用于分析未链接时的代码布局与重定位需求。

out_asm.txt:hello可执行文件的反汇编文本输出。展示链接后具有实际虚拟地址的指令序列,便于与 o_asm.txt 对比,观察重定位与地址绑定的具体结果。

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.

[2]  https://www.csd.cs.cmu.edu

[3]  HIT-CSAPP2025大作业要求

(参考文献0分,缺失 -1分)

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/fumeng_l/article/details/156572724

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--