本文旨在阐述C语言程序从源代码到可执行文件的完整转换过程。以经典的hello.c程序为例,研究系统地剖析了预处理、编译、汇编、链接以及进程管理等关键环节,展现了C语言程序的生命周期。通过理论分析与实践演示相结合的方式,本文不仅详细阐释了各阶段工具的工作原理和实现方法,还通过实际操作展示了具体的处理结果。这种理论与实践并重的研究方法,有助于读者深入理解计算机系统的运作机制和体系结构设计,从而更全面地掌握C语言程序的编译与执行全过程。
关键词:生命周期;计算机系统;体系结构;
目 录
第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(From Program to Process)
P2P 描述了从 hello.c(源程序) 到 运行时进程(Process) 的完整转换过程。这一过程包含四个关键阶段:
- 预处理(Preprocessing):处理宏定义、头文件展开等,生成 .i 文件。
- 编译(Compilation):将预处理后的代码翻译成汇编语言(.s 文件)。
- 汇编(Assembly):将汇编代码转换为机器指令,生成可重定位目标文件(.o 文件)。
- 链接(Linking):合并库函数和用户代码,生成可执行文件(a.out 或 hello)。
最终,Shell 加载该可执行文件,通过 fork() + execve() 创建新进程,分配虚拟内存空间,CPU 从程序入口(如 _start)开始执行,最终进入 main() 函数运行用户代码。
020(From Zero-0 to Zero-0)
020 描述了程序从 无(Zero) 到运行,再到 终止回收(Zero) 的完整生命周期:
- 加载阶段:Shell 调用 execve() 加载 hello,操作系统分配虚拟内存,建立页表映射到物理内存,并初始化进程控制块(PCB)。
- 执行阶段:CPU 从程序入口开始执行,加载代码和数据,调用 main() 运行目标逻辑。
- 终止阶段:程序结束后,父进程(Shell)通过 wait() 回收子进程资源,内核清除进程描述符、释放内存,系统回归初始状态(Zero)。
这一过程完整展现了程序如何在计算机系统中 从无到有,再从有到无 的完整生命周期。
1.2 环境与工具
硬件环境:
处理器:12th Gen Intel(R) Core(TM)i9-13900K
机带RAM:32.0GB
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位,VMware,Ubuntu 20.04 LTS
开发与调试工具:Visual Studio 2022 64位;vim objump edb gcc readelf等工具
1.3 中间结果
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.asm 反汇编hello.o得到的反汇编文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章首先系统阐述了hello程序的P2P(Program to Process)和020(Zero to Zero)流程,包括其核心设计理念和具体实现方法。随后,全面介绍了实验所需的硬件环境、软件平台和开发工具,并详细说明了实验过程中生成的各个中间文件(如预处理文件、汇编文件、目标文件等)的名称及其功能作用,为后续实验分析奠定基础。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
预处理步骤是指预处理器在程序运行前,对源文件进行简单加工的过程。预处理过程主要进行代码文本的替换工作,用于处理以#开头的指令,还会删除程序中的注释和多余的空白字符。预处理指令可以简单理解为#开头的正确指令,它们会被转换为实际代码中的内容(替换)。
2.1.2预处理的作用
预处理过程中并不直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换,主要有以下作用:
头文件包含:将所包含头文件的指令替代。
宏定义:将宏定义替换为实际代码中的内容。
条件编译:根据条件判断是否编译某段代码。
其他:如注释删除等。
简单来说,预处理是一个文本插入与替换的过程预处理器。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,我们对比了源程序和预处理后的程序。结果显示,除了预处理指令被扩展成了几千行之外,源程序的其他部分都保持不变,说明.c文件的确是被修改过了

在 main 函数执行前的代码主要由头文件 <stdio.h>、<unistd.h> 和 <stdlib.h> 依次展开生成。以 stdio.h 为例,其展开过程如下:
在预处理阶段,#include 指令的作用是将指定头文件的全部内容插入到当前源文件中。stdio.h 作为标准输入输出库的头文件,包含了文件读写、标准 I/O 操作相关的函数原型和宏定义。当预处理器扫描到 #include <stdio.h> 时,它会在系统头文件路径(通常位于 /usr/include)中查找该文件,并将其内容完整复制到源文件内。
值得注意的是,stdio.h 自身可能还嵌套包含其他头文件(如 <stddef.h> 或 <features.h>),这些文件也会被递归展开并插入。在整个预处理过程中,预处理器仅执行文本级的复制和替换,不会对头文件内容进行任何语法解析或计算。
2.4 本章小结
本章详细介绍了在Linux环境下对C语言程序进行预处理的操作方法和实际意义。通过一个简单的hello程序示例,我们完整演示了从源代码hello.c到预处理文件hello.i的转换过程,并深入解析了预处理后的文件内容。具体分析表明,生成的hello.i文件不仅包含了标准输入输出库stdio.h的全部内容,还涉及以下关键元素:
- 宏定义和常量声明
- 源代码行号标记信息
- 条件编译指令
- 其他头文件的递归展开内容
这一过程清晰地展现了预处理阶段对源代码的文本级处理机制,为后续的编译流程奠定了基础。通过实际代码对比分析,读者可以直观理解预处理在程序编译过程中的重要作用。
第3章 编译
3.1 编译的概念与作用
计算机程序编译的概念是指将用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式程序的翻译过程
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1汇编初始部分
在main函数前有一部分字段展示了节名称:

.file 声明出源文件
.text 表示代码节
.section .rodata 表示只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号的类型
3.3.2 数据部分
(1)字符串程序有两个字符串存放在只读数据段中,如图:

hello.c中唯一的数组是main函数中的第二个参数(即char**argv),数组的每个元素都是一个指向字符类型的指针。由知数组起始地址存放在栈中-32(%rbp)的位置,被两次调用作为参数传到printf中。
如图,分别将rdi设置为两个字符串的起始地址:
![]()
![]()
(2)参数argc
参数argc是main函数的第一个参数,被存放在寄存器%edi中,由语句
可见寄存器%edi地址被压入栈中,而语句
可知该地址上的数值与立即数5判断大小,从而得知argc被存放在寄存器并被压入栈中。
(3)局部变量
程序中的局部变量只有i,我们根据
可知局部变量i是被存放在栈上-4(%rbp)的位置。
3.3.3全局函数
hello.c中只声明了一个全局函数int main(int arge,.char*argv[]),我们通过汇编代码
可知。
3.3.4赋值操作
hel1o.c中的赋值操作贝有for循环开头的i-0,该赋值操作体现在汇编代码上,则是用mov指令实现,如图:
。由于int型变量i是一个32位变量,使用movl传递双字实现。
3.3.5算术操作
hello.c中的算术操作为for循环的每次循环结束后i++,该操作体现在汇编代码则使用指令add实现,问样,由丁变量i为32位,使用指令addl。指令如下:
![]()
3.3.6关系操作
hello.c中存在两个关系操作,分别为:
- 条件判断语句if(argc!=5):汇编代码将这条代码翻译为:
![]()
使用了cmp指令比较立即数4和参数argc大小,并且设置了条件码。根据条件码,如果不相等则执行该指令后面的语句,否则跳转到.L2。
- 在for循环每次循环结束要判断一次i<10,判断循环条件被翻译为:
![]()
同(1),设置条件码,并通过条件码判断跳转到什么位置。
3.3.7控制转移指令
设置过条件码后,通过条件码来进行控制转移,在本程序中存在两个控制转移:
(1) ![]()
判断argc是否为5,如果不为5,则执行if语句,否则执行其他语句,在汇编代码中则表现为如果条件码为1,则跳到.L2,否则执行cmpl指令后的指令。
(2) ![]()
在for循环每次结束判断一次i<10,翻译为汇编语言后,通过条件码判断每次循环是否跳转到.L4。而在for循环初始要对i设置为0,如下:
![]()
然后直接无条件跳转到.L3循环体。
3.3.8函数操作
(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函数
参数传递与函数调用:
![]()
将rdi设置为1,再使用call指令调用函数。
(4)atoi、sleep函数
参数传递与函数调用:
![]()
可见,atoi函数将参数argv[3]放入寄 s存器%rdi中用作参数传递,简单使用call指令调用。
![]()
然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。
(5)getchar函数
无参数传递,直接使用call调用即可。
3.3.9类型转换
atoi函数将宁字符中转换为sleep函数需要的整型参数.
3.4 本章小结
本章系统阐述了C编译器将预处理后的hello.i文件转换为汇编文件hello.s的完整过程。首先从理论层面解释了编译阶段的核心功能与作用机制,随后通过具体指令演示了实际的编译操作流程。
通过对生成的hello.s文件进行深入分析,本章重点剖析了以下几个关键方面的实现细节:
- 数据处理的底层表示方式
- 函数调用的实现机制
- 各类运算(包括赋值、算术、关系运算)的汇编实现
- 程序控制流(跳转结构)的转换过程
- 类型转换的底层处理方式
通过将原始C源代码与生成的汇编指令进行逐项对比,本章清晰地展现了高级语言特性到机器级指令的转换过程,使读者能够深入理解编译器的工作机制和代码优化的实现原理。这种对比分析方法不仅揭示了语言抽象背后的实现细节,也为后续的汇编和链接过程奠定了理论基础。
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编器(as)将包含汇编语言的.s文件转换为机器语言指令,并生成可重定位目标文件(.o文件)。这个.o文件是二进制格式,其中包含了main函数的指令编码。
4.1.2汇编的作用
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。 .o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统下,对hello.s进行汇编的命令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
(1)ELF头
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。ELF头展示如下:

(2)节头(section header)
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

(3)重定位节
.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需修改。可执行目标文件中不包含重定位信息。如图,需要重定位的内容如下:

(4)符号表
.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。符号表如下:

4.4 Hello.o的结果解析
4.4.1命令
在shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。

4.4.2与hel1o.s的对照分析
(1)增加机器语言
每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如,在hello.s中的一个cmpl指令表示为:
![]()
而在反汇编文件中表示为:
![]()
(2)操作数进制
反汇编文件中的所有操作数都改为十六进制。如(1)中的例子,立即数由hello.s中的$4变为了$0x4,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
(3)分支转移
反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L3)。例如下面的jmp指令,反汇编文件中为:

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

在可重定位文件中call后面不再是函数名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章系统地阐述了汇编语言的基本概念及其核心功能。以Ubuntu系统环境中的hello.s文件作为实例,我们完整演示了从汇编代码到目标文件的转换过程:首先使用汇编器将hello.s编译为可重定位目标文件hello.o,随后将其转换为ELF格式的可执行文件hello.elf。研究过程中,我们特别关注了目标文件格式转换带来的变化,对ELF文件中的各个节(section)进行了详细解析。
通过对比分析hello.o的反汇编结果(保存为hello.asm文件)与原始hello.s汇编代码,我们清晰地展现了汇编语言到机器语言的转换机制。研究发现,虽然两者在表现形式上存在差异,但本质上保持着指令的对应关系。同时,反汇编结果还揭示了目标文件为后续链接过程所做的各项准备工作,包括符号解析、地址预留等关键环节,这些都为深入理解程序构建过程提供了重要依据。
这一系列分析不仅验证了汇编器的工作原理,更直观地呈现了机器语言的组织形式,以及链接器在处理目标文件时所需的关键信息。整个研究过程构建了一个从高级表示到底层实现的完整认知路径。
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接(linkng)是将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行与编译时(compile time),也就是在源代码被翻译为机器代码时;也可以执行与加载时(load time),也就是程序被加载器加载到内存并执行时:甚至执行于运行时。
5.1.2链接的作用
在现代系统中,链接是由叫做链接器(1iker)的程序自动执行的,它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。
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
运行截图如下:
![]()
5.3 可执行目标文件hello的格式

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

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


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

(4)Dynamic section

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


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

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

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

在edb中找到对应的信息:

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


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

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

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

5.5.2重定位过程
重定位由两步组成:
(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
(3)重定位过程地址计算方法如下:

5.6 hello的执行流程
5.6.1过程
通过edb的调试,一步一步地记录下call命令进入的函数。

(I)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
5.6.2子程序名或地址
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x4010a0
_sleep 0x4010e0
_getchar 0x4010b0
_exit 0x4010d0
。
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。延迟绑定是通过GOT和PLT实现的,根据hello.elf文件可知,GOT起始表位置为:0x404000:

GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用了dl_init之后字节改变了:


对于变量而言,利用代码段和数据段的相对位置不变的原则去计算正确地址。
对于库函数而言,需要plt、got合作。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。接下来执行程序的过程中,就可以使用过程链接表plt和全局偏移量表got进行动态链接。
5.8 本章小结
\
本章从链接的基本原理入手,系统性地介绍了链接在程序构建过程中的关键作用。首先通过gcc链接命令的实操演示,完整展示了从目标文件到hello可执行文件的生成过程。随后采用readelf等工具对生成的ELF格式可执行文件进行了结构解析,详细考察了其文件组织格式。为进一步理解程序运行机制,使用edb调试器深入分析了hello程序加载后的虚拟地址空间分布情况。最后,本章以hello程序为典型案例,从三个维度展开深入剖析:详细解读了重定位过程中的地址修正机制,逐步跟踪了程序执行的完整流程,并对动态链接库的加载和绑定过程进行了系统分析。这一系列研究不仅验证了链接技术的实现原理,更完整呈现了程序从编译链接到加载执行的全生命周期。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念与特征
进程作为操作系统中最核心的概念之一,其经典定义是指一个正在执行的程序实例。从系统视角来看,进程本质上是程序在特定数据集合上的一次动态执行过程,它不仅是操作系统进行资源分配和调度的基本单位,更是构建整个操作系统架构的基础要素。在传统操作系统的设计理念中,进程具有双重身份:既是系统资源分配的最小单元,也是程序执行的基本载体。
6.1.2 进程的核心功能
进程机制为程序运行创造了一个关键的抽象层,通过虚拟化技术为每个程序营造出独占系统资源的假象。具体表现为:程序指令看似在处理器上连续不断地执行,内存空间也仿佛由程序独占使用。实际上,系统中的所有程序都运行在特定进程的上下文环境中,正是这种进程抽象机制,使得多个程序能够高效、安全地共享有限的硬件资源。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的功能特性
作为用户与操作系统交互的桥梁,Shell(以bash为例)本质上是一个交互式的应用层程序,其主要功能包括:
1)作为命令解释器解析用户指令
2)提供友好的用户操作界面
3)调度管理各类应用程序的执行
6.2.2 Shell-bash的工作机制
Shell的执行流程遵循以下处理逻辑:
1)输入处理阶段:从终端设备读取用户输入的命令字符串
2)命令解析阶段:对输入命令进行词法分析和语法解析
3)执行决策阶段:
- 若为内置命令(如cd、echo等),直接在当前进程执行
- 若为外部命令,则通过fork系统调用创建子进程执行
4)进程管理阶段: - 前台进程:阻塞等待执行完成
- 后台进程:立即返回并继续接收新命令
5)信号处理阶段:实时响应并处理键盘输入的中断信号
整个处理流程体现了Shell作为命令调度中枢的核心功能,既保证了用户交互的实时性,又实现了进程管理的灵活性。
6.3 Hello的fork进程创建过程
调用fork( ) 函数创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork函数只被调用一次,但会返回两次;一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到可执行目标文件,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次从不返回。在execve加载了可执行目标文件之后,调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
6.5.1进程上下文信息架构
操作系统通过上下文切换机制实现多任务处理,该机制构建在底层异常控制流基础之上。内核为每个进程维护完整的上下文信息,包含:
- 寄存器状态:通用寄存器、浮点寄存器、程序计数器
- 内存管理数据:用户栈、页表结构
- 系统资源描述:内核栈、进程描述符、文件描述表
这些上下文数据构成进程恢复执行所需的完整运行环境。
6.5.2时间片分配机制
进程时间片是CPU资源分配的基本单位,其特点包括:
- 动态分配性:由调度算法动态确定时长
- 强制中断性:到期后触发抢占式调度
- 公平性保障:确保多进程的并发执行体验
6.5.3进程调度全流程
调度过程包含三个关键阶段:
- 触发阶段:
- 时间片耗尽触发时钟中断
- 系统调用/缺页异常引发主动让出
- 更高优先级进程就绪
- 上下文切换阶段:
a. 保存当前进程的完整上下文
b. 加载目标进程的保存上下文
c. 更新内存管理单元(MMU)配置 - 执行转移阶段:
- 更新运行队列状态
- 设置新进程的时间片计数器
- 将CPU控制权移交目标进程
6.5.4执行状态转换模型
以hello程序执行过程为例:
- 用户态→内核态转换:
- 触发条件:系统调用(如read)、硬件中断
- 转换机制:通过陷阱门进入内核异常处理流程
- 特权级提升:CPU模式寄存器切换
- 内核态→用户态恢复:
- 完成系统服务后执行异常返回指令
- 恢复用户栈和程序计数器
- 重新启用用户空间内存映射
典型执行序列:

6.6 hello的异常与信号处理
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1异常
a. 中断
原因:来自I/O设备的信号
异步/同步:异步
返回行为:总是返回到下一条指令
处理方式:

b. 陷阱
原因:有意的异常
异步/同步:同步
返回行为:总是返回到下一条指令
处理方式:

c. 故障
原因:潜在可恢复的错误
异步/同步:同步
返回行为:可能返回到当前指令
处理方式:

d. 终止
原因:不可恢复的错误
异步/同步:同步
返回行为:不会返回
处理方式:

6.6.2信号
hello执行过程中可能产生的信号:
a.SIGTSTP信号,中断信号,默认行为是停止直到下一个SIGCONT。
b.SIGINT信号,终止信号,默认行为是终止。
6.6.3程序运行过程中各命令及运行结果
a.正常运行:程序每隔2秒输出一次,共输出10次

b.运行程序后在键盘上随意输入
屏幕输出缓存至缓冲区,不影响当前进程的运行。在键盘按回车
并不影响进程的执行

c.Ctrl-Z
在进程执行时按Ctrl-Z,进程停止。

e.Ctrl-Z后运行ps命令
hello进程被挂起,通过ps可查看进程的相关信息

f.Ctrl-Z后运行jobs命令
hello进程被挂起,通过jobs可查看任务列表及状态

g.Ctrl-Z后运行pstree命令
hello进程被挂起,通过pstree命令可查看进程间的关系,所有进程以树状图显示。

h.Ctrl-Z后运行fg命令
hello进程被挂起,fg命令使得被挂起的进程收到SIGCONT信号,进程继续运行。

i.Ctrl-Z后运行kill命令发送信号
hello进程被挂起,通过kill -9 进程号的命令,给hello进程发送SIGKILL信号,终止hello进程。

j.Ctrl-C
发送SIGINT信号,结束hello进程。

6.7本章小结
本章围绕计算机系统中的进程管理与Shell机制展开系统性探讨。首先以基础hello程序为切入点,分层解析了进程的核心概念与功能定位,详细阐述了Shell作为命令解释器的工作机制与处理流程。通过完整的案例跟踪,深入剖析了hello程序从进程创建、启动到执行的全生命周期,包括进程控制块初始化、地址空间分配、执行上下文构建等关键环节。最后,针对程序运行中可能触发的异常场景及多样化输出结果,从系统调用、信号处理等维度进行了全面的技术解读与原理说明,完整呈现了用户程序与操作系统交互的动态过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
在有地址变换功能的计算机中,访问指令给出的地址(操作数)叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,由程序hello产生的与段相关的偏移地址部分
7.1.2线性地址
线性地址是逻辑地址到物理地址变换之间的一步,程序hello的代码会产生逻辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
7.1.3虚拟地址
程序访问存储器所使用的逻辑地址称为虚拟地址。虚拟地址经过地址翻译得到物理地址。与实际物理内存容量无关,是hello中的虚拟地址
7.1.4物理地址
在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名)、段起点、装入位、段的长度等。程序通过分段划分为多个块,如代码段、数据段、共享段等。
一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。其中前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。
全局描述符表(GDT)整个系统只有一个,它包含:(1)操作系统使用的代码段、数据段、堆栈段的描述符(2)各任务、程序的LDT(局部描述符表)段。
每个任务程序有一个独立的LDT,包含:(1)对应任务/程序私有的代码段、数据段、堆栈段的描述符(2)对应任务/程序使用的门描述符:任务门、调用门等。
段式管理图示如下:

7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,如果发生缺页,则从磁盘读取。
MMU利用页表来实现从虚拟地址到物理地址的翻译。
下面为页式管理的图示:

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。工作原理如下:

多级页表的工作原理展示如下:

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

高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:

如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行取决于替换策略,例如LRU策略会替换最后一次访问时间最久远的那一行。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、.区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域。hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。如图所示:

7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:
(1)检查虚拟地址是否合法,如果不合法则触发一个段错误,终止这个进程。
(2)检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
(3)两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

7.9本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
(1)动态内存管理的基本方法
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接着未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
(2)动态内存管理的策略
分配器将堆视为一组大小不同的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
a. 显式分配器,要求应用显式地释放任何已分配的块。
b. 隐式分配器,另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程就叫做垃圾收集。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux系统采用统一的文件抽象模型实现设备管理,其核心设计理念可概括为以下两个层面:
- 设备抽象机制
系统通过"文件化"建模将所有I/O设备(包括存储设备、网络接口及终端设备)抽象为特殊文件,建立设备与文件的映射关系。这种设计使得所有输入输出操作均可转化为对相应文件对象的读写操作,实现了物理设备的逻辑抽象化。 - 统一接口规范
基于该抽象模型,内核导出标准化的Unix I/O接口,其特性表现为:
- 接口层级:提供底层基础操作接口
- 行为一致性:确保各类设备的访问方式统一
- 操作原子性:所有I/O操作均通过文件描述符机制执行
这种设计范式既屏蔽了设备物理特性的差异,又通过统一的文件操作语义(open/read/write/close)实现了设备访问的标准化,构成了Linux设备管理的核心架构基础。
8.2 简述Unix IO接口及其函数
(1)Unix IO接口:所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O
(2)Unix IO函数
a.open函数——打开文件
int open(char *filename, int flag, mode_t mode); 进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,也可以是一个或者更多位掩码的或,为写提供给一些额外的指示。mode参数指定了新文件的访问权限位。
b.close函数——关闭文件
int close(int fd);进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。
c.read函数——读文件函数
ssize_t read(int fd, void *buf, size_t n);应用程序通过调用read函数执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
d.write函数——写文件函数
ssize_t write(int fd, const void *buf, size_t n);应用程序通过调用write函数执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
e.lseek函数——修改字节偏移量的函数
off_t lseek(int fd, off_t offset, int whence); lseek函数可以修改文件的字节偏移量。
8.3 printf的实现分析

printf函数调用vsprintf函数进行格式化,接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出;调用write函数把buf中的i个元素的值写到终端。syscall函数不断地打印出字符,直到遇到:'\0'。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

用户从键盘输入,键盘接口得到对应的键盘扫描码,同时发送中断请求,通过键盘中断处理子程序,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar的返回值是用户输入字符的ascii码,若到文件结尾则返回-1(EOF),且将用户输入显示到屏幕。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法、Unix IO接口、Unix IO常见函数,以及对printf和getchar的实现分析等内容。
结论
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(1)hello所经历的过程
a.预处理——hello.c经过预处理得到文本文件hello.i
b.编译——hello.i经过编译得到汇编代码文件hello.s
c.汇编——hello.s经过汇编得到二进制可重定位文件hello.o
d.链接——hello.o经过链接得到可执行文件hello
e.输入运行命令./hello 2022110549 sunyuwen 2,shell接收命令并进行分析
f.调用fork函数创建新进程
g.调用execve函数在新的子进程中加载hello进程
h.从虚拟地址到物理地址的转换及访问内存
i.各种函数的执行及调用
j.在进程运行过程中的异常处理,接收相应信号并执行对应的异常处理
k.hello进程运行结束,被父进程回收,进行资源释放。
(2)深切感悟及创新理念
通过对hello程序执行全过程的深度剖析,我们可以系统性地理解计算机体系的核心运作机制。这个看似简单的程序从源代码到屏幕输出,实际上经历了完整的计算机系统处理链条:
- 程序构建阶段
- 预处理:宏展开与头文件包含(gcc -E)
- 编译:高级语言到汇编代码转换(gcc -S)
- 汇编:生成可重定位目标文件(gcc -c)
- 链接:地址解析与最终可执行文件生成(ld)
- 系统运行阶段
- 进程管理:fork/execve创建进程空间
- 存储管理:虚拟地址到物理地址转换
- I/O管理:系统调用与设备驱动交互
- 显示输出:从字符编码到像素渲染的全流程
- 系统优化维度
在掌握基础执行流程后,可以从以下方面进行深入优化:
- 安全性:加强边界检查,防范缓冲区溢出
- 高效性:优化系统调用频率,减少上下文切换
- 可靠性:完善错误处理机制
- 可维护性:增强代码可读性和模块化
这种从微观指令到宏观系统的完整认知路径,不仅揭示了"程序如何运行"的本质问题,更为我们提供了"如何编写更好程序"的方法论指导。深入理解计算机系统各层次的协同工作机制,是开发高性能、高安全性软件的基础,也是计算机专业能力提升的关键所在。
附件
| 文件名字 | 文件作用 |
| hello.i | hello.c经过预处理得到的文本文件 |
| hello.s | hello.i经过编译得到的文本文件 |
| hello.o | hello.s经过汇编得到的二进制文件 |
| hello | hello.o经过链接得到的可执行目标文件 |
| hello_elf.txt | hello.o的ELF格式文件 |
| hello_obj.txt | hello.o的反汇编文件 |
| hello_elf2.elf | hello的ELF格式文件 |
| hello_obj2.txt | hello的反汇编文件 |
参考文献
[1]程序预处理阶段,在做什么_预处理阶段主要做的是哪两件事-CSDN博客http://t.csdnimg.cn/Z5Vfu
[2] C语言的预处理阶段都能完成哪些工作? - 千锋实践训练营的文章 - 知乎https://zhuanlan.zhihu.com/p/142108566
[3]程序详细编译过程(预处理、编译、汇编、链接) - 知乎
[4]《深入理解计算机系统》 Randal E.Bryant & David R.O’Hallaron 机械工业出版社
[5] 逻辑地址、物理地址、虚拟地址_虚拟地址 逻辑地址-CSDN博客
[6] 编译和链接的过程_编译链接四个步骤-CSDN博客http://t.csdnimg.cn/L2vIw
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/ICEY_0814/article/details/148029435



