本报告详细分析了程序“Hello”的完整生命周期,从源代码到进程的执行,再到最终资源的释放。内容涵盖预处理、编译、汇编、链接等构建过程,并深入探讨了进程管理、存储管理和I/O机制。通过实践操作和系统工具的使用,全面揭示了计算机系统底层运行原理,包括虚拟内存、地址转换、异常处理及设备管理等关键概念。报告旨在加深对操作系统和程序执行机制的理解,展示了程序如何在系统中高效、安全地运行。
关键词:程序生命周期;预处理;编译;汇编;链接;进程管理;虚拟内存;地址转换;I/O管理;ELF格式;
目 录
第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简介
P2P(从程序到进程)
Hello的P2P过程是指它从一段源代码变成一个运行中的程序。首先,程序员编写出hello.c源代码;接着,通过预处理、编译、汇编和链接等步骤,这段代码被一步步转化为可执行文件hello。当用户在终端输入命令运行它时,操作系统会创建一个新的进程来执行这个程序,这就完成了从程序到进程的转变
020(从零到零)
Hello的020过程描述了它作为一个进程的完整生命周期。从程序被启动开始,操作系统为它分配资源并加载到内存中,然后CPU开始执行它的指令。最终,当程序运行结束或出现错误时,操作系统会回收它所占用的所有资源,包括内存和文件描述符等,整个过程就像从“无”开始又回到“无”。
1.2 环境与工具
泰山服务器
虚拟机:VirtualBox/Vmware 11以上 + Ubuntu 18.04 LTS 64位/优麒麟 64位 以上;
软件平台:Linux、C/C++
调试工具:gdb、CodeBlocks(限虚拟机下使用,服务下不能使用)
1.3 中间结果
hello.c 原C语言文件
hello.i 预处理产生文件
hello.s 编译产生文件
hello.o 汇编产生文件
hello.out 链接产生可执行文件
1.4 本章小结
本章主要介绍了C语言程序“hello”的完整生命周期,从源代码到可执行程序,再到进程的运行与终止。通过“P2P”(Program to Process)和“020”(从零到零)两个概念,形象地描述了程序的生成与执行过程。
P2P过程 :展示了hello.c如何经过预处理、编译、汇编和链接四个阶段,逐步转化为可执行文件hello.out。随后,在用户输入运行命令后,操作系统创建新进程来加载并执行该程序,完成从静态程序到动态进程的转变。
020过程 :描述了hello程序作为进程的一生。它从无到有——由操作系统加载运行,使用系统资源;又从有到无——在运行结束后被系统回收所有资源,回归“零”状态。
此外,本章还列出了实验所需的软硬件环境及开发工具,包括泰山服务器、虚拟机平台、Linux操作系统以及相关的调试工具如gdb等,并给出了各阶段产生的中间文件及其作用。
总体而言,本章为后续深入理解程序的执行机制和系统运行原理奠定了基础。
第2章 预处理
2.1 预处理的概念与作用
预处理是指在程序正式编译之前,由预处理器对源代码进行的一系列初步处理操作。这个过程并不涉及对程序语法的解析,而是主要完成宏替换、文件包含、条件编译等任务。
通过预处理,可以将头文件中的内容插入到源文件中、展开宏定义、删除注释和多余的空白字符,从而为后续的编译工作准备好完整的源代码。
2.2在Ubuntu下预处理的命令
在Ubuntu系统中,我们可以使用GCC(GNU Compiler Collection)提供的选项来单独执行预处理步骤。具体命令如下:

gcc -E hello.c -o hello.i
上述命令中,-E选项告诉GCC只执行预处理阶段而不进行编译;hello.c是原始的C语言源文件;-o hello.i指定输出文件名为hello.i,这是预处理后的结果文件。
cpp hello.c > hello.i
这个命令将hello.c预处理为hello.i
经过这个命令处理,hello.c被预处理成为hello.i文件
2.3 Hello的预处理结果解析

经过预处理,hello.i文件的长度相对于hello.c文件大幅增长了。

这些以 # 开头的行是预处理器插入的标记,用于指示源文件的位置、嵌套包含的头文件以及宏定义等内容。它们帮助编译器跟踪代码来源,确保错误信息和调试信息能够正确关联到原始源文件中的具体位置。

编译器帮助完成了数据类型和机器内部代码的定义。

编译器导入外部定义的代码。
2.4 本章小结
本章主要介绍了C语言程序在编译前的重要阶段——预处理过程。预处理是由预处理器在编译之前对源代码进行的初步处理操作,主要包括宏替换、文件包含和条件编译等任务。
通过这一过程,源代码中的头文件内容被引入、宏定义被展开、注释和多余空白被清除,从而生成一个更完整、更适合后续编译的中间文件(如 .i 文件)。总的来说,预处理是程序构建过程中不可或缺的一环,它为后续的编译工作奠定了基础,并在很大程度上增强了C语言程序的灵活性与可维护性。
第3章 编译
3.1 编译的概念与作用
编译是指将预处理后的文本文件(如 .i 文件)翻译为包含汇编语言的文件(如 .s 文件)的过程。这个过程由编译器完成,是整个程序构建过程中最关键的一环之一。编译器不仅会解析源代码的语法和语义,还会将其转换为目标平台可识别的低级语言——汇编语言 。
在这一阶段,高级语言(如 C 语言)的抽象语法结构会被映射为具体的机器指令表示。例如,C 语言中的变量声明、控制结构和函数调用都会被转化为相应的汇编语句,以便后续的汇编和链接步骤能够生成最终的可执行程序 。
编译的作用包括:
语法检查 :确保源代码符合语言规范;
语义分析 :理解代码的逻辑并进行优化;
目标代码生成 :将高级语言转换为对应架构的汇编代码 。
通过编译,程序从易于理解的高级语言形式逐步过渡到机器可以直接执行的二进制代码,是连接软件与硬件的重要桥梁。
3.2 在Ubuntu下编译的命令
在 Ubuntu 系统中,可以使用 GCC 提供的选项来单独执行编译阶段,即从 .i 文件生成 .s 文件的过程。具体命令如下:

gcc -S hello.i -o hello.s
其中:-S 选项告诉 GCC 只执行编译阶段,不进行汇编或链接;hello.i 是经过预处理的中间文件;-o hello.s 指定输出文件为 hello.s,即生成的汇编语言文件 。
cc1 hello.i -o hello.s
执行这个指令可以单独进行这步编译,但我的虚拟机里它不在环境变量中,应该用:/usr/libexec/gcc/x86_64-linux-gnu/13/cc1 hello.i -o hello.s
3.3 Hello的编译结果解析
1、数字常量

数字常量被编译为立即数
2、字符串常量

字符串常量存放在.rodata区
3、局部变量

局部变量存放在栈里
4、表达式

表达式用cmpl的形式实现
5、类型

编译时不关注类型,直接将类型转化为对应内存空间大小
6、算术操作

算术操作时,直接转化为对应汇编代码
7、关系操作

关系操作时,先比较两数,并根据需要判断的关系调用不同跳转函数。
8、数组操作

数组操作时,直接根据数组首地址和数组类型确定偏移量,从栈对应位置取数。
9、控制转移

需要控制转移时会出现跳转函数。
10、函数操作

调用函数时先将需要使用的参数放在对应寄存器中,之后使用call调用函数,调出的函数会获得寄存器中的参数值。
3.4 本章小结
本章介绍了编译的基本概念及其在程序构建过程中的关键作用,详细说明了编译器如何将预处理后的源代码转换为汇编语言。同时,通过具体命令展示了在 Ubuntu 系统中如何进行编译操作,并以 hello.i 文件为例解析了编译结果 .s 文件的内容,涵盖了数据处理、表达式实现、控制转移及函数调用等常见编程元素的汇编表示。最后,强调了编译作为连接高级语言与机器指令的重要桥梁作用。
第4章 汇编
4.1 汇编的概念与作用
汇编是指将编译器生成的汇编语言文件(如 .s 文件)转换为机器语言的目标文件(如 .o 文件)的过程。这一过程由汇编器 完成,是程序构建流程中的关键步骤之一。
在该阶段,汇编语言的助记符(mnemonics)被翻译为对应的二进制机器指令,同时还会生成重定位信息和符号表等数据,以便后续链接器处理多个目标文件并最终生成可执行程序 。
汇编的主要作用包括:
翻译助记符为机器码 :将易于理解的汇编语句转化为计算机可直接执行的二进制代码;
生成重定位信息 :用于指示链接器哪些地址需要在链接时进行调整;
建立符号表 :记录函数名、变量名等符号及其地址偏移,便于链接过程中引用和解析。
4.2 在Ubuntu下汇编的命令
在 Ubuntu 系统中,可以使用 gcc 或 as 命令来进行汇编操作。

gcc -c hello.s -o hello.o
其中:-c 表示只进行汇编和链接前的准备工作,不进行链接;hello.s 是经过编译后的汇编文件;-o hello.o 指定输出为名为 hello.o 的目标文件。
使用 as(GNU 汇编器)进行汇编:
as hello.s -o hello.o
这样也可以对代码进行汇编
4.3 可重定位目标elf格式
1、ELF头部分

2、节头

3、其他信息

4、重定位节

这里的信息含义如下:
偏移量:需要重定位的信息的字节偏移位置(代码节/数据节)
信息:重定位目标在.symtab中的偏移量和重定位类型
类型:表示不同的重定位类型
符号名称:被重定位时指向的符号
加数:重定位过程中要使用它对被修改引用的值做偏移调整
其中.rela.text 为重定位条目,每项表示一个需要在链接阶段进行调整的地址引用,例如:
R_X86_64_PC32 :PC 相对引用,用于访问 .rodata 中的字符串;
R_X86_64_PLT32 :用于调用外部函数(如 puts, printf, exit 等),链接器会将其解析为 PLT 表中的跳转地址 。
.rela.eh_frame 为重定位条目,是异常处理框架的重定位信息,确保调试和异常处理机制能在最终可执行文件中正确工作。
4.4 Hello.o的结果解析
1、数字进制不同

hello.s中的数都是十进制的,而在反汇编代码中以十六进制表示,这对应着在机器中以二进制的形式存在。
2、对字符串常量的引用不同

hello.s中引用字符串常量是用全局变量所在的那一段的名称加上%rip的值,而hello.o中用的是0加%rip的值,因为当前为可重定位目标文件,之后还需经过重定位方可确定其具体位置,所以这里都用0来代替。
3、分支转移的不同

hello.s中列出了每个段的段名,分支转移时,跳转指令后用对应的段的名称表示跳转位置;而在hello.o的反汇编代码中每个段都有明确的地址,跳转指令后用相应的地址表示跳转位置。
4、函数调用的不同

在hello.s中调用函数时,在call指令之后直接引用函数名称,而在hello.o的反汇编代码中,在call指令后加上下一条指令的地址来表示。观察机器语言,发现其中操作数都为0,这是因为通过重定位信息,再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。
4.5 本章小结
本章介绍了汇编的基本概念与作用,详细说明了在 Ubuntu 系统中使用 gcc 或 as 命令将 .s 文件转换为可重定位目标文件 hello.o 的过程。通过对 ELF 格式的分析,了解了 ELF 头、节头表、符号表及重定位信息的结构和作用。最后,对比了 hello.s 和 hello.o 在表示形式上的差异,如进制变化、字符串引用方式、分支跳转和函数调用的不同表示方法。这些内容帮助我们理解从汇编语言到机器代码的转化机制及其在链接前的状态特征。
第5章 链接
5.1 链接的概念与作用
链接是指将一个或多个目标文件(如 hello.o)以及所需的库文件组合成一个可执行文件的过程。它是程序从源代码到最终运行的最后一步,决定了程序能否正确启动并与系统环境无缝衔接 。
在链接过程中,链接器(如 GNU 的 ld)主要完成以下任务:
符号解析(Symbol Resolution) :将每个符号引用与一个符号定义进行匹配,通常是函数名、全局变量等。
重定位(Relocation) :将各个目标文件中的代码和数据片段合并为统一的地址空间,并修正代码中因相对地址变化而失效的引用。
例如,在 hello.o 中对 puts、printf 等外部函数的调用是未解析的,链接器会将这些引用绑定到 C 标准库(如 libc.so)中的实际实现上,从而生成完整的可执行程序 。
链接的作用在于使得模块化编程成为可能,程序员可以将程序拆分为多个源文件分别编译,再通过链接整合成一个整体。同时,它也允许程序使用共享库,提高代码复用性和运行效率 。
5.2 在Ubuntu下链接的命令
在 Ubuntu 系统中,通常使用 gcc 命令来驱动链接过程,它会自动调用底层链接器 ld 并传入必要的参数和库文件:

gcc hello.o -o hello
其中:hello.o 是之前汇编生成的目标文件;-o hello 指定输出的可执行文件名为 hello。
实际上,gcc 会链接标准 C 库(如 libc)以及其他必要的启动文件(如 crt0.o),因此即使我们只提供了 hello.o,最终生成的 hello 可执行文件仍然能正常运行 。
ld -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 /usr/lib/gcc/x86_64-linux-gnu/13/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/13/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
也可以完成链接
5.3 可执行目标文件hello的格式

分析hello的ELF格式结果如图5-2。
5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,与5.3对照分析,发现两者并不一致但占用空间一致,说明操作系统进行了ASLR偏移。
5.5 链接的重定位过程分析

hello相较于hello.c多出很多系统的共享库的代码,说明链接的过程是将链接的库中的需要的代码添加到当前程序中的。

hello中根据其相对主函数的偏移定位链接后的程序的地址。
5.6 hello的执行流程
| _start | 0x401090 |
| __libc_start_main_impl | 0x7ffff7c2a200 |
| __GI___cxa_atexit | 0x7ffff7c471f0 |
| __internal_atexit | 0x7ffff7c471ff |
| __new_exitfn | 0x7ffff7c47000 |
| call_init | 0x7ffff7c2a28b |
| __libc_start_call_main | 0x7ffff7c2a150 |
| __GI__setjmp | 0x7ffff7c44fa0 |
| __GI___sigsetjmp | 0x7ffff7c44ed0 |
| __sigjmp_save | 0x7ffff7c44f50 |
| main | 0x401176 |
用gdb对hello逐步进行操作可得到上述内容
5.7 Hello的动态链接分析

先在反汇编代码中查找PLT表。

找到GOT地址

执行前

执行后
5.8 本章小结
本章深入探讨了链接(Linking)的概念、过程及其在程序构建中的核心作用。我们了解到,链接是将多个目标文件和所需的库文件组合成最终可执行程序的关键步骤,它使得模块化编程成为可能,并支持代码复用。
链接器(如 ld)在这一过程中扮演着重要角色,主要完成了符号解析和重定位两大任务。符号解析负责将代码中对外部函数或变量的引用与它们的实际定义进行匹配;重定位则负责调整代码和数据的地址,确保它们在合并后的统一地址空间中能够正确引用。
在 Ubuntu 环境下,通常通过 gcc 命令来驱动链接过程,gcc 会自动引入必要的系统启动代码和标准库(如 libc)。虽然 gcc 简化了操作,但我们也可以通过更底层的 ld 命令来观察链接过程的更多细节。
通过分析生成的可执行目标文件 hello 的 ELF 格式,我们了解了其内部结构,包括不同的段(如代码段、数据段等)。进一步通过调试工具查看其在内存中的虚拟地址空间布局时,我们发现了与 ELF 文件描述的不完全一致,这主要是由于操作系统为了安全考虑实施了地址空间布局随机化 (ASLR)。
重定位过程的分析揭示了链接器如何将库中的代码整合到最终程序中,并根据程序加载后的实际地址调整所有的地址引用。
我们还通过调试工具追踪了 hello 程序的执行流程,从入口点 _start 开始,经过系统启动函数 __libc_start_main_impl 等,最终到达用户编写的 main 函数。这展示了程序启动时所经历的一系列初始化和准备工作。
最后,本章对动态链接进行了初步分析,介绍了过程链接表 (PLT) 和全局偏移表 (GOT) 的概念。通过观察 GOT 在程序执行前后的变化,我们理解了动态链接如何实现函数的延迟绑定(Lazy Binding),从而提高了程序的启动速度和内存使用效率。
总而言之,链接是软件开发中不可或缺的一环,它负责将零散的代码和数据片段“链接”成一个完整的、可在特定操作系统环境下运行的程序。理解链接的概念和过程,对于深入理解程序的构建、加载和执行机制至关重要。
第6章 hello进程管理
6.1 进程的概念与作用
进程是程序执行的一个实例,它是操作系统进行资源分配和调度的基本单位。每个进程都有独立的地址空间、堆栈、寄存器状态以及文件描述符等上下文信息。在操作系统的管理下,进程通过时间片轮转等方式共享CPU资源,从而实现多任务并发执行。进程的存在使得多个程序能够同时运行而互不干扰,并且提供了良好的隔离性和安全性。
6.2 简述壳Shell-bash的作用与处理流程
Shell(如bash)是一个命令行解释器,它负责接收用户输入的命令并调用相应的程序来执行。Shell的主要作用包括解析用户输入、路径查找、进程创建与管理等。当用户在终端输入./hello时,Shell首先解析该命令,确认其为一个可执行文件后,通过调用fork()函数创建一个新的子进程,并在子进程中使用execve()加载并运行hello程序。此外,Shell还负责处理信号、重定向、管道等功能,以支持复杂的命令组合。
6.3 Hello的fork进程创建过程
在用户输入命令启动hello程序后,Shell会调用fork()函数创建一个新的进程。fork()会复制当前进程(即Shell进程)的所有上下文信息,包括内存映像、寄存器状态、文件描述符等,生成一个几乎完全相同的子进程。子进程与父进程的区别在于其进程ID(PID)不同。fork()返回值在父进程中为子进程的PID,在子进程中为0,因此可以通过判断返回值区分父子进程并执行不同的代码逻辑。
6.4 Hello的execve过程
在fork()成功创建子进程后,子进程会调用execve()函数来加载并运行新的程序hello。execve()会将新程序的可执行文件从磁盘读入内存,并替换当前进程的地址空间。这一过程包括设置新的代码段、数据段、堆栈段以及更新进程控制块(PCB)中的相关信息。execve()还会传递命令行参数和环境变量给新程序,确保其能够正确运行。一旦execve()成功执行,原来的子进程代码将被替换,开始执行hello程序。
6.5 Hello的进程执行
在hello程序开始执行后,它处于用户态,运行在用户空间中。当程序需要访问系统资源(如打印输出到终端)时,会触发系统调用,进入核心态,由内核代为完成相关操作。例如,printf函数最终会调用write系统调用,将字符串写入标准输出设备。
进程的执行受到操作系统的调度管理。操作系统根据进程的时间片分配CPU资源,采用时间片轮转或优先级调度算法决定哪个进程获得CPU使用权。进程在运行过程中可能会因等待I/O操作完成或资源不可用而进入阻塞状态,待条件满足后由调度器重新唤醒并继续执行。
在进程执行期间,用户态与核心态之间的切换频繁发生。这种切换由中断、异常或系统调用触发,涉及到保存当前上下文、切换页表、执行内核代码、恢复上下文等一系列复杂操作。通过这种机制,操作系统能够在保证安全性的前提下高效地管理系统资源。
6.6 hello的异常与信号处理

hello执行中不停乱按没有产生新的信号,程序执行不会被打断。

ctrlc输入后系统会给程序发送SIGINT信号,使得程序结束。

ctrlz输入之后会内核会发送SIGTSTP信号,使得程序被起。


使用fg后系统发送SIGCONT给程序使得程序能继续进行。
6.7本章小结
本章围绕进程管理和hello程序的执行,详细介绍了操作系统中进程的基本概念、Shell的作用与处理流程、进程的创建与加载机制、以及进程的执行和信号处理等内容。
首先,在进程的概念与作用 部分,我们了解到进程是程序执行的一个实例,是操作系统进行资源分配和调度的基本单位。每个进程都有独立的地址空间、堆栈、寄存器状态等信息,通过时间片轮转等方式共享CPU资源,实现多任务并发执行。
接着,在Shell(如bash)的作用与处理流程 中,Shell被描述为一个命令行解释器,负责接收用户输入并调用相应的程序来执行。它通过调用fork()创建子进程,并在子进程中使用execve()加载并运行程序。此外,Shell还支持复杂的命令组合,如信号处理、重定向和管道等功能。
在Hello的fork进程创建过程 中,Shell调用fork()函数创建一个新的进程,该进程复制父进程的所有上下文信息,并通过返回值区分父子进程。随后,子进程通过execve()加载并运行hello程序,替换当前进程的地址空间,完成程序的初始化执行。
关于Hello的进程执行 ,程序开始时运行在用户态,当需要访问系统资源时会触发系统调用进入核心态。操作系统的调度机制决定了进程如何获得CPU资源,并管理进程的状态变化(如运行、阻塞等)。同时,用户态与核心态之间的频繁切换,由中断、异常或系统调用触发,确保了系统的安全性和资源的高效管理。
最后,在异常与信号处理 部分,我们看到用户可以通过按键向程序发送特定信号。例如,Ctrl+C会发送SIGINT信号终止程序,Ctrl+Z会发送SIGTSTP信号暂停程序,而fg命令则通过发送SIGCONT信号恢复被暂停的程序。
总体而言,本章全面展示了从用户输入命令到程序执行结束的整个生命周期,涉及操作系统的核心机制,包括进程管理、调度、内存管理以及信号处理等关键内容。
第7章 hello的存储管理
7.1 hello的存储器地址空间
在操作系统中,程序的执行需要依赖不同的地址空间来访问内存资源。以 hello 程序为例:
逻辑地址 (Logical Address):是指由程序产生的与段相关的偏移地址部分,是程序视角下的地址表示。例如,在 hello.o 文件中,指令和数据的地址都是相对于某个段起始位置的偏移量 。
线性地址 (Linear Address):也称为虚拟地址(Virtual Address),是逻辑地址经过段式管理机制转换后的结果。它是一个连续的地址空间,供程序使用,但尚未映射到物理内存上 。
虚拟地址 (Virtual Address):与线性地址同义,是现代操作系统中用于抽象物理内存的一种机制,使得每个进程都拥有独立的地址空间 。
物理地址 (Physical Address):是实际存在于主存中的地址,由线性地址通过页式管理机制转换而来,最终指向具体的内存单元 。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 架构中,逻辑地址由段标识符(Segment Selector)和段内偏移量(Offset)组成。段标识符指向全局描述符表(GDT)或局部描述符表(LDT)中的段描述符,从而获取段的基地址。段基地址加上偏移量即为线性地址。这种机制称为段式管理 ,用于将逻辑地址转换为线性地址 。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址通过页式管理 机制转换为物理地址。操作系统将线性地址空间划分为固定大小的页(Page),并将这些页按需映射到物理内存中的页框(Page Frame)。页表记录了每个页对应的物理页框号,通过查找页表即可完成地址转换。这一过程由 CPU 的页式内存管理单元(MMU)自动完成 。
7.4 TLB与四级页表支持下的VA到PA的变换
在现代系统中,为了提高地址转换效率,引入了 TLB(Translation Lookaside Buffer) ,它是硬件高速缓存,用于缓存最近使用的页表项。当 CPU 需要进行地址转换时,首先查询 TLB;若命中,则直接获取物理地址;否则,需访问多级页表查找。
x86-64 架构采用四级页表结构 (PML4 → PDPT → PD → PT),每一级页表负责解析地址的一部分,最终定位到物理页框。这种设计既保证了地址空间的扩展性,又提高了地址转换效率 。
7.5 三级Cache支持下的物理内存访问
在物理内存访问过程中,CPU 并不会直接访问主存,而是先检查是否命中高速缓存(Cache)。现代处理器通常配备三级 Cache(L1、L2、L3),它们按层次结构组织,容量递增、速度递减。当 CPU 访问某个物理地址时,会依次查找 L1、L2、L3 缓存,若全部未命中,则从主存加载数据,并写入各级缓存中,以便后续访问更快 。
7.6 hello进程fork时的内存映射
当 Shell 执行 fork() 创建子进程时,新进程会继承父进程的虚拟地址空间及其页表。然而,为了节省资源,Linux 使用 写时复制(Copy-on-Write, COW) 技术,即父子进程共享相同的物理页,只有当任一进程尝试修改页面内容时,才会触发缺页异常并复制该页 。
7.7 hello进程execve时的内存映射
在 execve() 调用中,当前进程的虚拟地址空间被清空,并重新加载新的可执行文件(如 hello)的内容。操作系统根据 ELF 文件头的信息创建新的虚拟内存区域(VMA),包括代码段、数据段、堆栈等,并建立相应的页表映射。此时,程序尚未真正加载到物理内存,仅在首次访问时通过缺页中断按需加载 。
7.8 缺页故障与缺页中断处理
当进程访问的虚拟页面尚未映射到物理内存时,会触发 缺页故障(Page Fault) 。操作系统捕获该异常后,根据页表项判断该页是否合法。若合法但未加载,则从磁盘读取相应页面到物理内存,并更新页表;若不合法,则终止进程。此机制实现了按需分页(Demand Paging),有效降低了内存占用 。
7.9动态存储分配管理
动态内存分配通常由 malloc() 和 free() 函数实现,用于在程序运行期间按需申请和释放内存。基本策略包括:
首次适应 (First Fit):从空闲链表头部开始查找第一个足够大的块。
最佳适应 (Best Fit):查找最小且满足需求的块,减少碎片。
伙伴系统 (Buddy System):将内存划分为大小为 2 的幂次方的块,便于合并和分割。
Slab 分配器 :针对频繁申请和释放的小对象优化,预分配对象池。
此外,printf() 函数内部可能调用 malloc() 来分配缓冲区,确保输出操作顺利进行 。
7.10本章小结
本章围绕 hello 程序的存储管理机制,详细介绍了操作系统如何通过段式管理和页式管理实现从逻辑地址到物理地址的转换,以及 TLB 和多级页表在地址转换中的作用。同时,分析了 Cache 在物理内存访问中的加速机制,探讨了 fork() 和 execve() 过程中的内存映射变化,并深入讲解了缺页中断的处理流程及动态内存分配的基本方法。
通过本章学习,可以全面理解现代操作系统如何高效地管理内存资源,保障程序的正确执行,同时兼顾性能与安全 。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux系统中,所有的I/O设备都被模型化为文件。这种设计思想体现了“一切皆文件”(Everything is a file)的核心理念,使得设备的访问方式与普通文件的操作保持一致,简化了程序设计和系统管理的复杂性。
在Linux中,每个设备都对应一个特殊的文件节点,通常位于 /dev 目录下。这些设备文件提供了统一的接口,使得应用程序可以通过标准的文件操作函数(如 open()、read()、write()、close() 等)来访问不同的硬件设备。根据设备的读写特性,Linux将设备分为三类:
字符设备 :以字节流形式进行读写,没有缓存机制,如键盘、串口等。
块设备 :以数据块为单位进行访问,通常有缓存支持,如硬盘、SSD等存储设备。
网络设备 :用于处理网络通信,如网卡设备 。
通过这种方式,Linux内核实现了对多种外设的统一管理,并隐藏了底层硬件的复杂性,提高了系统的可移植性和扩展性 。
8.2 简述Unix IO接口及其函数
Unix I/O接口提供了一组标准化的系统调用函数,使得应用程序可以以统一的方式访问各种I/O资源,包括普通文件、设备文件、管道、套接字等。主要的Unix I/O接口函数包括以下几类:
打开/关闭文件 :
open():用于打开或创建一个文件或设备,返回一个文件描述符(file descriptor)。
close():关闭一个已打开的文件描述符 。
读写操作 :
read():从文件或设备中读取指定数量的字节到缓冲区。
write():将缓冲区中的数据写入文件或设备 。
定位操作 :
lseek():用于移动文件读写指针的位置,适用于支持随机访问的文件或设备 。
控制操作 :
ioctl():用于执行设备特定的控制命令,常用于配置设备参数或获取设备状态 。
此外,还有用于异步I/O、内存映射I/O(mmap())、多路复用I/O(select()、poll()、epoll())等高级接口,进一步增强了程序对I/O操作的控制能力 。
通过这些统一的接口函数,应用程序可以方便地操作各种I/O设备,而无需关心其具体实现细节,从而提升了代码的可移植性和开发效率。
8.3 printf的实现分析
printf 是 C 语言中最常用的输出函数之一,其核心功能是将格式化的字符串输出到标准输出设备(如终端)。它从 vsprintf 生成显示信息,调用 write 系统函数,最终通过陷阱(如 int 0x80 或 syscall)进入内核态完成实际的 I/O 操作 。
8.3.1从 vsprintf 到 write
printf 函数的实现大致可以分为两个步骤:格式化 和输出 。
1、格式化阶段 (vsprintf)
vsprintf 是 printf 的底层实现函数,负责将可变参数按照格式字符串 fmt 转换为一个完整的字符串。
它使用 va_list 类型来遍历所有可变参数,并根据格式说明符(如 %d, %s, %x)将参数转换为对应的字符串形式。
示例代码如下:
int i = vsprintf(buf, fmt, arg);
其中 buf 是用于存储格式化结果的缓冲区,fmt 是格式字符串,arg 是可变参数列表。这个函数返回格式化后的字符串长度 。
2、输出阶段 (write)
格式化完成后,printf 调用 write 函数将缓冲区中的内容写入标准输出文件描述符(通常是 1)。
示例代码如下:
write(1, buf, i);
这里的 1 表示标准输出(stdout),i 是之前 vsprintf 返回的字符串长度。write 是一个系统调用接口,会触发中断或系统调用指令(如 int 0x80 或 syscall),从而切换到内核态执行真正的 I/O 操作 。
8.3.2系统调用与中断处理
在用户态调用 write 后,程序需要通过系统调用 进入内核态,由操作系统完成实际的字符输出操作:
1、x86 架构下的中断调用
在早期的 x86 架构中,write 使用 int 0x80 指令触发中断,进入内核的系统调用处理程序。
寄存器中保存了系统调用号(如 _NR_write)以及参数(如文件描述符、缓冲区地址、数据长度),内核根据这些信息调用相应的驱动程序进行输出 。
2、现代架构下的 syscall 指令
在 64 位系统中,通常使用 syscall 指令替代 int 0x80,以提高系统调用效率。
内核依然通过寄存器获取调用号和参数,并调用相应的设备驱动程序 。
8.3.3字符显示驱动子程序
当内核接收到写入请求后,会调用字符显示驱动子程序 完成字符的显示过程:
1、ASCII 到字模库的映射
字符的 ASCII 码被映射到字模库中,字模库中存储了每个字符的点阵信息(即字符的图形表示)。
例如,字符 'A' 的 ASCII 码为 0x41,对应字模库中一段表示字母 A 的 8x16 或 8x8 的像素矩阵 。
2、写入显存 (VRAM)
字符的点阵信息会被写入显存(Video RAM),显存中存储的是每一个像素点的颜色信息(RGB 分量)。
显存的布局决定了字符在屏幕上的位置,通常由光标坐标控制当前写入的位置 。
3、显示芯片刷新
显示芯片会按照固定的刷新频率(如 60Hz)逐行读取显存中的数据,并将其转换为显示器可以识别的信号。
每个像素点的 RGB 分量通过信号线传输到液晶显示器,最终在屏幕上显示出字符 。
8.4 getchar的实现分析
getchar 是 C 标准库中的一个常用函数,用于从标准输入(通常是键盘)读取一个字符。它的底层实现依赖于操作系统提供的系统调用(如 read),并涉及到中断处理、设备驱动和缓冲机制等多个层面。
8.4.1异步异常 - 键盘中断的处理
当用户按下键盘按键时,会触发一个硬件中断 ,通知 CPU 有外部输入事件发生。这个中断由内核中的键盘中断处理子程序响应:
扫描码获取 :键盘控制器将按键转换为对应的扫描码(Scan Code),然后传递给 CPU。
扫描码转 ASCII 码 :内核根据当前键盘状态(如是否按下 Shift、Caps Lock 等)将扫描码转换为对应的 ASCII 码值 。
数据存入键盘缓冲区 :转换后的 ASCII 码被写入操作系统的键盘缓冲区 (通常是一个环形缓冲区),等待后续读取 。
这一过程是异步 进行的,意味着即使应用程序尚未调用 getchar(),用户输入的数据也会被暂存在缓冲区中,不会丢失。
8.4.2getchar 调用 read 系统函数
在用户空间,getchar() 函数实际上是标准 I/O 库(如 glibc)封装的一个接口。其底层通过调用 read() 系统调用 来从标准输入(文件描述符 0)中读取字符:
int c = read(0, &ch, 1);
其中:
0 表示标准输入(stdin)的文件描述符;
&ch 是用户提供的缓冲区地址;
1 表示每次读取一个字节。
read() 是一个阻塞式系统调用,如果当前没有可用的输入数据(即键盘缓冲区为空),它会一直等待,直到有数据可读 。
8.4.3机制与回车键
在大多数终端设置下,标准输入是以行缓冲 方式工作的。这意味着:
read() 不会立即返回,而是等到用户按下 Enter(回车)键 后才将整行数据返回给用户程序;
在此之前,用户输入的字符会被缓存在内核或终端驱动程序中,并支持基本的编辑功能(如退格);
只有当一行输入完成并以换行符结束时,getchar() 才能逐个读取每个字符,直到遇到换行符为止 。
例如:
char c = getchar(); // 若输入 "abc\n",则连续四次调用 getchar() 可分别得到 'a','b','c','\n'
8.5本章小结
本章围绕 hello 程序的 I/O 管理机制 ,深入探讨了 Linux 操作系统中 I/O 设备的管理方式、Unix I/O 接口及其函数实现,并结合 printf 和 getchar 函数分析了字符输入输出的具体实现过程。
首先,在 Linux 的 I/O 设备管理方法 中,介绍了“一切皆文件”的设计理念。Linux 将所有 I/O 设备(如键盘、磁盘、网卡等)抽象为文件节点,位于 /dev 目录下,并通过统一的文件操作接口(如 open()、read()、write() 等)进行访问。这种设计简化了设备的管理和程序开发,提高了系统的可移植性和扩展性 。
接着,Unix I/O 接口及其函数 部分详细说明了标准 I/O 操作的核心系统调用,包括文件的打开与关闭、数据读写、位置调整以及设备控制等。这些接口不仅适用于普通文件,也支持设备文件、管道和网络通信,使得应用程序能够以一致的方式处理各类 I/O 资源 。
在 printf 的实现分析 中,我们了解到其底层依赖于 vsprintf 进行格式化字符串处理,并通过 write 系统调用将数据写入标准输出。该过程涉及从用户态到内核态的切换,使用 int 0x80 或 syscall 指令触发系统调用,最终由字符显示驱动程序将 ASCII 字符映射为字模库中的图形信息,并写入显存(VRAM),由显示芯片刷新屏幕显示 。
随后,在 getchar 的实现分析 中,分析了用户按键输入的整个流程:当用户按下键盘时,会触发中断,操作系统将扫描码转换为 ASCII 码并缓存;getchar() 通过 read() 系统调用从缓冲区中读取字符。由于终端默认采用行缓冲机制,getchar() 通常要等到用户按下回车键才会返回有效字符 。
综上所述,本章全面展示了 hello 程序在执行过程中涉及的 I/O 管理机制,涵盖了从硬件中断处理、系统调用、内核缓冲,到用户空间函数封装的完整流程。通过理解这些内容,可以更深入地掌握操作系统如何协调软件与硬件资源,实现高效、可靠的输入输出操作 。
结论
通过本次实验,我们全面分析了 Hello 程序从源代码到最终执行结束的全过程。在这个过程中,我们使用了预处理、编译、汇编、链接、进程管理、存储管理和 I/O 管理等多个计算机系统核心机制,逐步揭示了一个简单程序背后所涉及的复杂系统行为。
Hello 的完整生命周期总结如下:
P2P(Program to Process)过程:
Hello.c 源文件经过预处理(gcc -E),生成 hello.i 文件;
通过编译器(gcc -S),生成汇编代码 hello.s;
再经过汇编(gcc -c 或 as),生成可重定位目标文件 hello.o;
最后通过链接(gcc hello.o -o hello),生成可执行文件 hello;
用户在 Shell 中输入命令运行程序时,Shell 使用 fork() 创建子进程,并通过 execve() 加载并执行该可执行文件,完成从静态程序到动态进程的转变 。
020(从零到零)过程:
Hello 进程启动后,操作系统为其分配内存空间、加载代码段和数据段;
程序开始执行,经历用户态与内核态的切换、系统调用、异常与信号处理等操作;
在运行过程中,涉及到虚拟地址到物理地址的映射、TLB 缓存加速、Cache 访问优化以及缺页中断处理等底层机制;
程序执行结束后,操作系统回收其占用的所有资源,包括内存、文件描述符等,整个生命周期回归“零”状态 。
I/O 管理与交互:
Hello 程序中使用 printf() 和 getchar() 进行标准输出和输入操作;
printf() 底层通过 vsprintf() 格式化字符串,再调用 write() 系统调用进入内核态,最终通过字符显示驱动写入显存;
getchar() 则依赖键盘中断和 read() 系统调用,实现了异步输入与缓冲处理;
整个 I/O 流程体现了操作系统如何协调硬件设备与用户程序之间的高效通信 。
对计算机系统设计与实现的深入理解:
模块化与分层抽象是系统设计的核心思想 。
Hello 程序的构建过程展示了从高级语言到机器码的逐层转换,每一阶段都对上一阶段进行封装和抽象,使得开发者无需关心底层细节即可编写高效程序。
进程与内存管理是多任务操作系统的基础 。
通过 fork、execve、虚拟内存、页表、TLB 等机制,操作系统实现了进程隔离、资源共享和高效的内存访问,为现代并发执行提供了坚实基础。
统一接口简化了设备管理 。
Linux 将所有 I/O 设备抽象为文件,通过 open(), read(), write() 等统一接口操作,极大降低了开发难度,提高了系统的可扩展性。
创新与展望:
如果未来能在操作系统层面引入更智能的内存调度策略(如基于 AI 的预测性内存分配),可以进一步提升程序执行效率;
在 I/O 管理方面,结合异步非阻塞 I/O 与事件驱动模型,有望实现更高吞吐量的交互体验;
随着 RISC-V 等开源指令集架构的发展,未来也可以尝试在更多平台上复现类似流程,探索跨平台兼容性与性能优化的新路径。
综上所述,本次实验不仅帮助我们深入理解了程序的生命周期和计算机系统的运行机制,也让我们体会到操作系统作为软硬件桥梁的重要作用。通过实践与分析,我们更加坚定了对计算机系统设计原理的理解,并对未来的技术发展方向有了更清晰的认识。
附件
| hello.c | hello源代码 |
| hello.i | 预处理之后的文本文件 |
| hello.s | hello的汇编代码 |
| hello.o | hello的可链接重定位文件 |
| hello | hello的可执行文件 |
| hello.o | 用readelf读取得到ELF格式信息 |
| hello.elf.elf | 由hello可执行文件得到的ELF格式信息 |
| hello.asm | 反汇编hello.o得到的反汇编文件 |
| hello.o.asm | 反汇编hello可执行文件得到的反汇编文件 |
参考文献
[1] https://chat.qwen.ai/
[2] https://chat.deepseek.com/
[3] https://gemini.google.com/app
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/xiao_bright/article/details/148016548



