本文以 Linux 环境下的 Hello 程序为研究对象,从计算机系统整体视角出发,对程序从源代码到进程终止的完整生命周期进行系统分析。论文分析了预处理、编译、汇编、链接等静态阶段,以及进程管理、存储管理和 I/O 管理等动态阶段。实验过程中,借助 gcc、ld、objdump、readelf、gdb 等工具,对中间文件和汇编指令进行详细分析,重点探讨了 ELF 文件格式、符号解析与重定位、虚拟地址空间布局、函数调用过程及标准 I/O 的实现原理。通过实验,加深了对计算机系统运行机制的理解,为系统级编程和操作系统原理学习提供实践基础。
关键词:Hello 程序;编译系统;ELF;进程管理;存储管理
自媒体发表截图
目 录
第1章 概述................................................................................... - 5 -
1.1 Hello简介............................................................................ - 5 -
1.2 环境与工具........................................................................... - 5 -
1.3 中间结果............................................................................... - 5 -
1.4 本章小结............................................................................... - 5 -
第2章 预处理............................................................................... - 6 -
2.1 预处理的概念与作用........................................................... - 6 -
2.2在Ubuntu下预处理的命令................................................ - 6 -
2.3 Hello的预处理结果解析.................................................... - 6 -
2.4 本章小结............................................................................... - 6 -
第3章 编译................................................................................... - 7 -
3.1 编译的概念与作用............................................................... - 7 -
3.2 在Ubuntu下编译的命令.................................................... - 7 -
3.3 Hello的编译结果解析........................................................ - 7 -
3.4 本章小结............................................................................... - 7 -
第4章 汇编................................................................................... - 8 -
4.1 汇编的概念与作用............................................................... - 8 -
4.2 在Ubuntu下汇编的命令.................................................... - 8 -
4.3 可重定位目标elf格式........................................................ - 8 -
4.4 Hello.o的结果解析............................................................. - 8 -
4.5 本章小结............................................................................... - 8 -
第5章 链接................................................................................... - 9 -
5.1 链接的概念与作用............................................................... - 9 -
5.2 在Ubuntu下链接的命令.................................................... - 9 -
5.3 可执行目标文件hello的格式........................................... - 9 -
5.4 hello的虚拟地址空间......................................................... - 9 -
5.5 链接的重定位过程分析....................................................... - 9 -
5.6 hello的执行流程................................................................. - 9 -
5.7 Hello的动态链接分析........................................................ - 9 -
5.8 本章小结............................................................................. - 10 -
第6章 hello进程管理.......................................................... - 11 -
6.1 进程的概念与作用............................................................. - 11 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 11 -
6.3 Hello的fork进程创建过程............................................ - 11 -
6.4 Hello的execve过程........................................................ - 11 -
6.5 Hello的进程执行.............................................................. - 11 -
6.6 hello的异常与信号处理................................................... - 11 -
6.7本章小结.............................................................................. - 11 -
第7章 hello的存储管理...................................................... - 12 -
7.1 hello的存储器地址空间................................................... - 12 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 12 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 12 -
7.4 TLB与四级页表支持下的VA到PA的变换................... - 12 -
7.5 三级Cache支持下的物理内存访问................................ - 12 -
7.6 hello进程fork时的内存映射......................................... - 12 -
7.7 hello进程execve时的内存映射..................................... - 12 -
7.8 缺页故障与缺页中断处理................................................. - 12 -
7.9动态存储分配管理.............................................................. - 12 -
7.10本章小结............................................................................ - 13 -
第8章 hello的IO管理....................................................... - 14 -
8.1 Linux的IO设备管理方法................................................. - 14 -
8.2 简述Unix IO接口及其函数.............................................. - 14 -
8.3 printf的实现分析.............................................................. - 14 -
8.4 getchar的实现分析.......................................................... - 14 -
8.5本章小结.............................................................................. - 14 -
参考文献....................................................................................... - 17 -
第1章 概述
1.1 Hello简介
1.1.1hello的P2P过程
程序从源代码到可执行进程的转换过程是计算机系统层次化设计的典型体现。Hello程序的生命周期始于人类可读的C语言源文件hello.c,这是一个高级抽象层面的程序表示。
编译系统通过四个有序阶段完成程序形态的转换:
1.预处理阶段:处理所有以#开头的指令,展开宏定义,包含头文件内容,删除注释,生成扩展的C源文件hello.i
2.编译阶段:将高级语言转换为与目标架构相关的低级表示,进行语法语义分析、优化和汇编代码生成,输出汇编文件hello.s
3.汇编阶段:将符号化的汇编指令转换为机器可执行的二进制编码,生成可重定位目标文件hello.o,但外部引用地址尚未确定
4.链接阶段:合并多个目标模块,解析符号引用,重定位地址,添加运行时启动代码,生成最终的可执行文件hello
此过程体现了计算机系统将高级抽象逐步降级为底层硬件可执行指令的设计哲学。
1.1.2hello的020过程
进程的动态生命周期展示了操作系统的进程管理机制。当用户在Shell中执行./hello命令时,程序从磁盘上的静态二进制文件转变为内存中动态执行的进程。
进程生命周期包含以下关键阶段:
1.进程创建:Shell通过fork()系统调用创建子进程,复制父进程的地址空间、文件描述符等上下文信息
2.程序加载:子进程通过execve()系统调用将hello程序映像加载到自己的地址空间,替换当前进程映像
3.指令执行:CPU从程序入口点开始执行机器指令,操作系统调度器在就绪队列中分配时间片
4.系统调用:程序通过系统调用接口访问操作系统服务(如I/O操作)
5.进程终止:main函数返回或调用exit()触发进程终止,操作系统回收分配的所有资源
这一过程展示了程序如何从磁盘存储的"静态存在"转变为内存中"动态执行"的完整生命周期。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
本次大作业的实验环境如下:
硬件平台:处理器架构:x86-64 (Intel i9)
操作系统:Ubuntu Linux 20.04 LTS(64 位)
编译工具:gcc
调试与分析工具:gdb、objdump、readelf
Shell:bash
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:C语言源程序
hello.i:预处理后的源代码
hello.s:编译生成的汇编程序
hello.o:可重定位目标文件
hello:可执行目标文件
hello_o.elf: hello.o经readelf分析得到的文件
hello_o.s:hello.o经objdump反汇编得到的文件
hello.elf:hello经readelf分析得到的文件
hello_dis.s:hello经objdump反汇编得到的文件
1.4 本章小结
本章确立了本报告的分析框架和研究方法。以Hello程序为研究对象,通过程序生命周期理论构建了P2P(从程序到进程)和020(从零到零)两个分析维度。P2P维度关注程序的静态转换过程,即从源代码到可执行文件的编译系统处理流程;020维度关注程序的动态执行过程,即从进程创建到资源回收的运行时行为。
实验环境选择了主流的x86-64 Linux平台,配合GNU工具链,这些工具提供了从源代码到二进制执行的全链路分析能力。中间结果文件的系统性保存确保了分析过程的可复现性和可验证性,为后续章节的深入分析提供了数据基础。
本报告将沿着编译系统的处理流程,逐章分析预处理、编译、汇编和链接各阶段的工作原理;随后从操作系统视角分析进程管理、存储管理和I/O管理的实现机制;最终形成对计算机系统协同工作的整体理解。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是编译系统处理C语言程序的第一个逻辑阶段,发生在实际的语法分析之前。这个阶段的核心任务是文本层面的处理,不涉及语法检查或代码优化。
预处理的主要作用包括:
1.宏展开:将程序中定义的宏标识符替换为对应的文本内容,包括带参数的宏函数.
2. 头文件包含:将#include指令指定的头文件内容完整插入到指令位置
3. 条件编译:根据预定义的条件选择性地包含或排除代码段,如#ifdef、#ifndef、#endif等指令
4. 注释删除:移除所有单行注释(//)和多行注释(/* */)
5. 行号标记:添加特殊的#line指令,便于编译器在错误报告中引用原始源文件位置
预处理阶段完成后的输出仍然是纯文本文件,但已经是完整的、自包含的C语言源代码,适合直接交给编译器进行语法分析。
2.2在Ubuntu下预处理的命令
在Linux环境下,使用GCC编译器进行预处理的标准命令如下:
gcc -E hello.c -o hello.i
命令参数解析:
1.-E:指示gcc仅执行预处理,停止在预处理阶段
2.hello.c:输入的C源文件
3.-o hello.i:指定输出文件名,约定使用.i扩展名表示预处理后文件


从行数对比可以看到,预处理后的文件通常比原始源文件大很多,因为包含了所有被引入的头文件内容。
2.3 Hello的预处理结果解析
2.3.1头文件展开分析
打开hello.i文件我们可以发现里面内容增加了,之前的#include对应的头文件都被替换了相应的内容,并且原文件中所对应的注释和多余空白都被删除了

2.4 本章小结
预处理阶段将模块化的C程序转换为完整的单一源文件,为后续编译阶段提供了标准化的输入。通过宏展开和头文件包含,实现了代码的复用和模块化设计。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译阶段是编译系统的核心转换过程,负责将预处理后的C语言源代码转换为目标机器架构的汇编语言。这一阶段执行了真正的语言转换工作,包括词法分析、语法分析、语义分析、中间代码生成、优化和代码生成等复杂过程。
编译器的主要任务:
1. 语法检查:验证程序是否符合C语言语法规则
2. 语义分析:检查类型匹配、作用域等语义正确性
3. 代码优化:在保证语义不变的前提下改进代码效率
4. 目标代码生成:生成与具体CPU架构相关的汇编指令
3.2 在Ubuntu下编译的命令
使用GCC从预处理文件生成汇编代码的命令:gcc -S hello.i -o hello.s
![]()
参数详解:
1.-S:指示GCC执行编译到汇编阶段后停止
2.hello.i:预处理后的输入文件
3.-o hello.s:指定输出的汇编文件名,通常使用.s扩展名
执行效果验证:

3.3 Hello的编译结果解析


3.3.1 数据类型处理
1.常量处理

字符串常量存储在.rodata只读数据段,通过标签.LC0、.LC1引用,中文字符串以UTF-8编码存储为字节序列
2.局部变量处理
3. 参数变量处理

argc通过%edi传递,保存到栈偏移-20处,argv通过%rsi传递,保存到栈偏移-32处,遵循System V AMD64调用约定
3.3.2赋值操作与表达式
1.简单赋值
![]()
2.复合赋值操作
虽然没有显式的+=等操作,但循环中有:
![]()
自增操作编译为addl $1指令,直接在内存位置执行算术运算
程序中的字符串常量被放置在只读数据段中,通过符号地址在运行时被访问。
3.3.3类型转换处理
1. 显式类型转换

atoi将字符串显式转换为整数,转换结果通过%eax寄存器返回
2.隐式类型转换

atoi返回int,sleep接收unsigned int,隐式转换通过寄存器传递实现
3.3.4 算术运算
1.基本算数运算
![]()
加法:i++编译为addl $1
3.3.5关系运算与逻辑运算
1.关系运算

关系表达式argc != 5编译为cmpl和条件跳转,实际实现为:argc == 5则跳转。
2.循环条件
![]()
i<=9编译为cmpl $9和jle(小于等于跳转)
3.3.6数组与指针操作
1.数组索引访问

数组索引转换为地址计算:argv[1] = argv + 1*8,64位系统中指针大小为8字节
3.3.7控制转移结构
1.条件分支(if/else)

使用cmpl比较和je条件跳转,没有显式else块,跳转后直接执行循环
使用call指令调用函数,返回地址压栈。
2.循环结构for

典型的for循环结构:1.初始化:i = 0 2.条件判断:i <= 9 3.循环体执行4.迭代:i++ 5.跳转回条件判断。
3.3.8函数操作
函数调用

使用call指令调用函数,返回地址压栈
2.参数传递
![]()
![]()
可变参数函数需要设置%eax为浮点参数数量
3.返回值处理

整数返回值通过%eax传递,函数返回前设置movl $0, %eax返回0
4.栈帧管理

pushq %rbp # 保存旧帧指针
movq %rsp, %rbp # 设置新帧指针
subq $32, %rsp # 分配栈空间
...
leave # 恢复栈帧
函数入口建立栈帧,出口恢复,为局部变量分配栈空间
3.3.9编译优化特点
1.常量传播:循环次数10被优化为9(因为i从0到9)
2.强度削弱:循环变量i存储在内存,但每次访问
3.循环展开:未进行循环展开优化(可能因调用外部函数)
4.寄存器分配:频繁使用的变量分配在寄存器中
3.4 本章小结
编译阶段将高级C语言结构映射为目标机器的汇编指令。通过分析hello.s,可以看到编译器如何实现数据类型、控制结构、函数调用等语言特性。关键发现包括:局部变量分配在栈上,参数通过寄存器传递,控制流转换为标签和跳转指令,函数调用遵循特定ABI约定。编译器在保持语义前提下进行了一定优化,为后续汇编阶段提供了标准化的输入。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编阶段将编译生成的汇编语言程序(hello.s)转换为机器语言二进制代码,生成可重定位目标文件(hello.o)。这是从人类可读的符号化代码到机器可执行二进制代码的关键转换过程。
汇编器(如GNU as)的核心功能:
- 指令编码转换:将助记符形式的汇编指令转换为对应的二进制操作码
- 符号地址解析:将标签(label)和符号转换为具体的地址偏移
- 数据转换:将数据定义指令(如.string、
.string、'.word.word)转换为二进制表示 - 重定位信息生成:为外部引用和地址相关操作生成重定位条目
- 格式封装:按照目标文件格式(ELF)组织代码和数据节区
汇编阶段的输出是一个可重定位目标文件,包含了机器指令但尚未确定最终运行时地址,这为后续链接阶段的地址绑定提供了基础。
4.2 在Ubuntu下汇编的命令
使用gcc -c hello.s -o hello.o进行汇编


1.文件类型为REL(可重定位文件)
2.目标架构为x86-64(AMD64)
3.采用小端字节序
4.没有程序入口点(entry point address为0)
5.包含14个节区(section)


重定位节是可重定位目标文件的重要组成部分,在程序最终变成可执行文件前需要将一些符号进行应用和解析、同时要将一些特定的外部函数的地址进行定位,这是链接器最后所做的,在这之前,汇编器会事先在相应的节中生成重定位条目(重定位节),以便链接器不加甄别地进行重定位操作。可以看到elf文件中.rlea.text以及.rela.eh.frame所对应的重定位条目,里面记录着一些需要重定位的符号puts、exit等以及他们的偏移量.
符号表是由汇编器生成的,使用编译器输出到汇编语言.s中的符号。其中可以看到一些符号的偏移量(value)、大小(size)、类型(type)等信息,值得注意的是,它标识了一些需要重定位函数和变量的信息
4.4 Hello.o的结果解析

通过对反汇编文件的分析可以看出,其内容与上一章的 .s文件既存在相似之处,也存在一定差异。在反汇编结果中,左侧列对应机器码,右侧则为经过调整的汇编语言表达。具体变化包括:立即数由十进制转为十六进制表示;控制转移相关的跳转指令改为基于地址计算的相对偏移寻址方式;同时部分指令后缀(如 b、l 等)被省略。
整体而言,这些调整使得反汇编后的代码更接近机器层面的表达方式。
4.5 本章小结
本章介绍了汇编器生成可重定位目标文件的过程,重点说明了该.o文件的elf格式文件的结构和内容,同时对比分析了由该.o文件生成的反汇编文件的关于立即数以及函数调用、控制转移的变化,展现了汇编阶段的重要作用,为下一章链接打下基础。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将多个可重定位目标文件合并生成可执行目标文件的关键过程。链接器(如ld)主要完成以下核心任务:
1.符号解析:将每个符号引用与一个唯一的符号定义关联起来
2.地址重定位:为每个符号定义分配运行时地址,并修正所有引用这些符号的指令
3.节区合并:将多个目标文件的相同类型节区合并为连续的段
4.运行时环境准备:添加程序启动代码和终止代码
链接过程分为两个阶段:
1.静态链接:在程序运行前完成所有符号解析和地址绑定
2.动态链接:部分符号解析推迟到程序加载时或运行时
链接使得模块化编程成为可能,允许将大型程序分解为多个独立的源文件分别编译,最后合并为完整可执行程序。
5.2 在Ubuntu下链接的命令

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

打开ELF文件分析,可见其总体结构与上一章所述基本一致,但内容体量显著增加。具体表现为:ELF头部中的部分参量已发生变更。
同时,文件中新增了程序头与 dynamic 段,其中 dynamic 段内记录了共享库等动态链接信息。
最重要的是,节头部表中的节增多,重定位条目和字符表发生了更新
1. init - 初始化代码段,起始地址:0x401000,大小:0x1b (27字节)
2. .plt - 过程链接表,起始地址:0x401020,大小:0x70 (112字节)
3.plt.sec - 安全PLT段,起始地址:0x401090,大小:0x60 (96字节)
4. .text - 主代码段(包含程序入口点),起始地址:0x4010f0(程序入口点),大小:0xd8 (216字节)
5. .fini - 终止代码段,起始地址:0x4011c8,大小:0xd (13字节)
6. .rodata - 只读数据,起始地址:0x402000,大小:0x48 (72字节)
7. .eh_frame - 异常处理框架,起始地址:0x402048,大小:0xa0 (160字节)
8. .interp - 程序解释器路径,起始地址:0x4002e0,大小:0x1c (28字节)
9. .dynamic - 动态链接信息,起始地址:0x403e38,大小:0x1a0 (416字节)
10. .dynsym - 动态符号表,起始地址:0x4003a8,大小:0xd8 (216字节)
11. .dynstr - 动态字符串表,起始地址:0x400480,大小:0x67 (103字节)
12. .rela.dyn - 动态重定位表,起始地址:0x400530,大小:0x30 (48字节)
13. .got - 全局偏移表,起始地址:0x403fd8,大小:0x10 (16字节)
14. .got.plt - PLT相关的全局偏移表,起始地址:0x403fe8,大小:0x48 (72字节)
15. .data - 已初始化数据,起始地址:0x404030,大小:0x4 (4字节)
16. .comment - 编译器信息,起始地址:0x0(不加载到内存),大小:0x2b (43字节)
17. .symtab - 符号表,起始地址:0x0(不加载到内存),大小:0x270 (624字节)
18. .strtab - 字符串表,起始地址:0x0(不加载到内存),大小:0x12e (302字节)
5.4 hello的虚拟地址空间

链接完成后,生成的可执行文件在反汇编视角下主要呈现三方面变化:
1.函数数量增加。链接产生的 hello2.asm 中包含若干新增函数,例如 .plt 及
.plt 及 'puts@puts@plt、printf@plt、getchar@plt 等以 @plt 结尾的动态链接桩函数。这些函数对应于源代码中对库函数的调用,由链接器从共享库中提取并插入最终可执行文件,以支持运行时动态解析。
2.函数调用指令的参数被重定位。对于源代码中的函数调用,链接器会解析重定位条目,并直接修改对应 call 指令后的机器码。具体而言,链接器计算目标地址与 call 指令下一条指令地址之间的相对偏移,并将该偏移值填入操作数中,从而使得 call 能够正确跳转至目标函数。
3.跳转指令的参数同样经过重定位处理。链接过程中,跳转指令的操作数也会被重新计算并改写。链接器依据重定位信息确定目标地址,进而得到目标与当前指令的相对距离,最终将该相对地址写入跳转指令对应的二进制位置,确保其能准确转向 PLT 中的对应函数。
5.5 链接的重定位过程分析

经链接后,可执行文件中的函数数量有所增加。这在反汇编文件 hello_dis.s 中表现为新增了 .plt、.init、.fini 等节,且各函数均具备了具体的实现代码。例如,文件中出现了 .plt 以及 exit@plt、sleep@plt 等函数的代码段。这一变化源于动态链接器将 hello.c 所调用的共享库函数提取并合并至最终的可执行文件中。

对比链接前后 main 函数的反汇编内容可见,原本标记为重定位类型 R_X86_64_32 与 R_X86_64_PC32 的机器码字段,其初始的零值已被替换为具体的虚拟地址。具体变化包括:控制跳转指令的目标地址通过函数地址加上偏移量计算得出;对 .rodata 节中数据的引用采用了绝对地址;而函数调用则使用相对于下一条指令的偏移量进行寻址。
5.6 hello的执行流程
在 Ubuntu 下使用 EDB 调试 hello。程序加载后,控制权首先到达 ELF 入口点 0x4010f0,对应 _start 函数;_start 进行初始化并调用 __libc_start_main;
随后 __libc_start_main 调用 main、初始化和清理;main → 参数检查 → 循环 printf/sleep → getchar → return,__libc_start_main → 调用 exit → _exit → 程序终止

5.7 Hello的动态链接分析
动态链接器会重定位全局偏移量表(GOT)中的每个条目,使得它包含正确的绝对地址,所以结合课程内容,我们得知可以通过观察got.plt节的内容变化来简单说明dl_init前后动态链接项目的变化。

运行hello后[email protected]的值发生了变化,说明动态链接项目都发生了变化。
5.8 本章小结
本章主要阐述了程序编译的最终阶段——链接。通过对比上一章内容,我们具体分析了链接器输出的 hello 可执行文件的 ELF 格式结构及其反汇编代码,重点剖析了链接器在符号解析与重定位中所起的关键作用,并跟踪了程序运行过程中函数的调用关系及动态链接项的变化。至此,hello 已形成一个完整的可执行实体,接下来它将作为进程被加载执行,完成其运行时生命周期。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是正在运行的程序的一个具体实例,它代表程序在特定数据集上的一次动态执行过程。在计算机系统中,进程作为资源分配和调度的基本单位,是操作系统构建与运行的底层支撑。在传统操作系统的设计中,进程同时承担着资源分配单元与执行单元的双重角色,负责承载并推进程序的执行。
从抽象视角看,进程为程序的执行营造了一种独占处理器和内存资源的假象,使人感觉程序指令在连续不断地被执行。实际上,处理器和内存是由多个进程共享的;而进程机制为每个程序提供了独立、隔离的运行环境,确保它们能在各自的上下文中执行,互不干扰。因此,进程不仅是执行中程序的实体化表现,也是操作系统实现多任务并行与资源管理的核心基础。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash 作为交互式命令行解释器,是用户与操作系统之间的桥梁,负责将用户输入的命令解析为系统指令并调度执行。对于内置命令,Shell会直接执行;若为外部程序,则通过 fork 创建子进程来运行。程序可以前台或后台方式执行:前台程序会阻塞Shell直到运行结束,后台程序则允许Shell立即返回并继续接收用户输入。在整个过程中,Shell始终监听键盘信号(如终止、挂起等),并能做出实时响应,从而为用户提供灵活的命令操作环境。
6.3 Hello的fork进程创建过程
Shell-bash 是一个交互式命令行解释器,它作为用户与操作系统之间的桥梁,将用户输入的命令转换为系统可执行的指令,并调度相应程序运行,从而为用户提供便捷的文件管理、程序执行和系统操作环境。
当用户在 Shell 中输入命令后,Shell 会首先读取并解析该命令。若命令属于 Shell 内置命令,则直接执行;否则,Shell 会调用 fork 函数创建一个子进程,并在该子进程中加载并执行用户指定的程序。
执行前,Shell 会根据命令判断程序应运行于前台还是后台。若是前台程序,Shell 会等待其执行完成后再接收下一条命令;若是后台程序,则 Shell 会立即返回并允许用户继续输入,程序在后台异步执行。
在整个过程中,Shell-bash 始终保持对键盘输入的监听,以便实时响应用户发出的信号(如终止、挂起程序等),并依据信号类型进行相应处理。
6.4 Hello的execve过程
execve 是 Unix/Linux 系统中用于加载并执行新程序的系统调用。它接收可执行文件的路径 filename、参数列表 argv 以及环境变量列表 envp,并将当前进程的映像替换为指定程序的映像。成功调用后,新程序从 main 函数开始执行,且 execve 不会返回原调用者;仅当发生错误(如文件不存在或不可执行)时,它才会返回并设置错误码。
与 fork(创建新进程并返回两次)不同,execve 是在当前进程的上下文中直接替换程序映像,并不创建新进程。
当 main 函数开始执行时,用户栈已被组织为包含命令行参数、环境变量及函数调用的栈帧结构,这些信息共同维护了程序执行所需的上下文,确保函数调用与返回能正确进行。
6.5 Hello的进程执行
在 hello 程序运行时,操作系统通过两个核心抽象为其提供执行支持:
逻辑控制流:每个进程都拥有独立的逻辑控制流,形成其独占处理器的假象,使得程序指令能够按顺序执行而不受其他进程干扰。调试器中观察到的程序计数器(PC)序列便是这一逻辑流的具体体现;多个逻辑流在时间上重叠执行时,称为并发流。
私有地址空间:操作系统为每个进程分配一个私有的虚拟地址空间,使其如同独占了整个主存,从而保证进程间的隔离与安全。
为实现多任务,操作系统通过上下文切换在进程间快速切换:内核为每个进程维护一套上下文(包含重启所需的所有状态),切换时保存当前进程上下文并载入下一进程的上下文。为公平利用 CPU,系统采用时间片调度:每个进程分得固定长度的时间片执行,时间片用尽即被暂停,CPU 转去执行其他进程。
CPU 通过控制寄存器中的模式位区分用户模式与内核模式。用户模式下进程只能执行部分指令且不能直接访问内核内存;内核模式下则可执行特权指令并访问全部内存空间,以此保障系统安全。
在 hello 的执行过程中,调用 execve 后系统为其分配新虚拟地址空间。程序始于用户模式,执行如 printf 输出等操作;调用 sleep 时则陷入内核模式执行信号处理程序,结束后返回用户模式继续执行。整个过程被切分为多个时间片,CPU 通过不断的上下文切换,使 hello 与其他进程交替执行,实现多任务调度与并发。
6.6 hello的异常与信号处理
6.6.1执行过程中的异常类型及处理

6.6.2信号处理机制

6.6.3运行结果及相关命令
(1)正常运行状态
在程序正常运行时,打印10次提示信息,以输入回车为标志结束程序,并回收进程。

(2)按下ctrl+c

对应6.2所给图片,按下ctrl + c,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
(3)按下ctrl+z

按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
(4)对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1和2。

(5)在Shell中输入pstree命令,可以将所有进程以树状图显示:

(6)输入kill命令,则可以杀死指定(进程组的)进程:

(7)当在 Shell 中输入 fg 2
命令时,此前在后台挂起的 hello 进程会被重新调至前台执行。此时 Shell 会先显示出该进程对应的命令行信息,随后 hello 进程从之前挂起的位置继续运行,输出剩余的内容。最终程序能够顺利执行完毕,并由系统完成进程资源的回收。

6.7本章小结
本章重点探讨了计算机系统中的进程与 Shell。以简单的 hello 程序为例,首先概述了进程的基本概念及其在计算机系统中的核心作用,并介绍了 Shell 的主要功能与基本处理流程。在此基础上,深入分析了 hello 程序从进程创建、启动到执行完毕的完整生命周期,具体阐释了 Shell 如何响应用户命令、通过 fork 创建子进程,并由子进程加载并执行目标程序。
同时,本章也关注了程序执行过程中可能出现的异常情形,如程序文件不存在、权限不足等问题,并对这些异常的原因与处理机制进行了说明。此外,结合 hello 程序实际运行中可能出现的不同输出结果,对其行为与预期效果做了进一步分析,从而加深对进程执行机制的理解。
(第6章2分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
在具备地址变换功能的计算机中,访问指令所给出的地址(操作数)称为逻辑地址,亦称相对地址。该地址需经过特定寻址方式的转换,才能对应到内存中的物理地址。具体而言,逻辑地址通常由段标识符和段内偏移量组成,在程序 hello 执行过程中,所产生的地址即为这种与段相关的偏移地址部分。
7.1.2线性地址
线性地址是逻辑地址转换为物理地址过程中的一个中间地址形式。在 hello 程序的执行过程中,其代码所产生的地址均为逻辑地址(即段内偏移地址)。逻辑地址经过内存管理单元中的分段部件处理:段描述符中记录的基地址加上该偏移地址,便得到对应的线性地址。
7.1.3虚拟地址
程序访问存储器时使用的逻辑地址称为虚拟地址,它需通过地址翻译机制转换为实际的物理地址。虚拟地址空间的大小与实际物理内存容量无关,这一特性使得每个进程(如 hello)都能拥有独立而完整的地址视图,hello 程序执行过程中所生成和使用的地址均为虚拟地址。
7.1.4物理地址
存储器中信息以字节为单位进行存储,每个字节单元被赋予唯一的地址,这一地址称为物理地址。物理地址是程序(如 hello)在内存中的实际位置,也可称为绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理是一种将程序按逻辑划分为不同部分(如代码段、数据段、共享段等)进行存储和访问的技术。每个段具有独立的逻辑完整性,并通过段表记录其属性,包括段号、内存起始地址、状态及长度等。
在段式管理中,逻辑地址由段标识符(段选择符)和段内偏移量两部分组成。段选择符通常为16位,其中高13位作为索引,在段描述符表中定位对应的段描述符,低3位则存储硬件控制信息。
段描述符表是存储段描述符的数据结构,每个描述符定义了段的基址、长度及访问权限等关键属性。通过段选择符的索引,可快速检索到目标段的具体信息。
系统层面,全局描述符表(GDT)存放操作系统核心段的描述符,同时也包含各个任务局部描述符表(LDT)的入口。每个任务拥有独立的LDT,其中存储该任务私有的代码、数据、堆栈段描述符,以及用于任务切换和调用的门描述符(如任务门、调用门)。
综上所述,段式管理通过逻辑分段、段表与描述符表的协同机制,实现了灵活高效的内存组织与访问控制,既贴合程序逻辑结构,也支持系统级的多任务隔离与调度。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被精心组织成一个数组结构,这个数组由存放在磁盘上的多个连续字节大小的单元组成,总数为N个。为了更有效地管理这些内存资源,VM系统会将虚拟内存分割成多个固定大小的块,这些块被称为虚拟页。与此同时,物理内存也被相应地分割成物理页,以便与虚拟页进行映射。
为了跟踪和管理这些虚拟页与物理页之间的映射关系,系统引入了页表的概念。页表本质上是一个包含多个页表条目(PTE)的数组,每个PTE负责记录一个虚拟页的状态信息。PTE通常由两个关键字段组成:一个是有效位,另一个是地址字段。
有效位的作用是指示该虚拟页当前是否已经被加载到DRAM(动态随机存取存储器)中。如果有效位被设置,那么意味着该虚拟页的内容已经缓存在DRAM中,此时地址字段就派上了用场,它存储着该虚拟页在DRAM中对应物理页的起始位置信息。
然而,当系统尝试访问一个尚未加载到DRAM中的虚拟页时(即有效位未被设置),就会发生所谓的“缺页”情况。这时,VM系统会从磁盘中读取该虚拟页的内容,并将其加载到DRAM中的某个物理页中,同时更新页表中相应的PTE,设置有效位并填写正确的地址字段。
内存管理单元(MMU)是负责实现虚拟地址到物理地址翻译的关键组件。它利用页表作为翻译的依据,根据虚拟地址中的页号索引到页表中相应的PTE,然后根据PTE中的有效位和地址字段来确定实际的物理地址。这样,程序就可以通过虚拟地址来透明地访问内存,而无需关心底层物理内存的具体布局和管理。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7处理器采用四级页表层次结构来管理虚拟地址到物理地址的映射。当CPU生成虚拟地址(VA)后,内存管理单元(MMU)首先利用该地址的高位部分作为标签(TLBT)与索引(TLBI),在翻译后备缓冲区(TLB)中进行查找。若TLB命中,则可直接获得物理地址(PA),从而极大提升翻译效率。
若TLB未命中,则需查询页表。CR3寄存器提供了第一级页表的基址。MMU依次使用虚拟地址中的VPN1至VPN4作为索引,逐级查询四级页表,最终在第四级页表的页表项(PTE)中获得物理页号(PPN)。将此PPN与虚拟地址中的页内偏移量(VPO)组合,即得到最终的物理地址(PA)。该结果随后被缓存至TLB中,以供后续快速访问。
通过上述机制,四级页表结构与TLB协同工作,在保证系统灵活支持大规模内存管理的同时,实现了高效可靠的地址翻译。
7.5 三级Cache支持下的物理内存访问
MMU 首先从虚拟地址中提取虚拟页号(VPN),并查询 TLB 是否已缓存对应的页表项(PTE)。TLB 利用 VPN 中的索引位(TLBI)和标记位(TLBT)进行组相联查找:若命中,则直接返回缓存的 PTE;否则,MMU 需从主存中获取相应 PTE。
获取物理地址后,MMU 将其发送至缓存。缓存从物理地址中解析出缓存偏移(CO)、组索引(CI)与标记(CT),在对应组内比对标记。若标记匹配且有效位为真,则根据 CO 读出相应数据字节,并经由 MMU 返回给 CPU。
7.6 hello进程fork时的内存映射
当进程调用 fork() 函数时,内核会为其创建一个全新的进程环境。这包括分配一个独立的进程控制块(PCB)和唯一的进程标识符(PID),并复制父进程的虚拟内存管理结构——包括 mm_struct、内存区域描述表和页表。通过这种方式,子进程在创建之初就拥有一份与父进程完全相同的虚拟地址空间映射,从而能够在 fork() 返回后从与父进程完全一致的内存状态开始执行。
为了在维持高效率的同时确保进程间的内存隔离,Linux 采用了 写时复制(Copy-on-Write, COW) 机制。在创建初期,父子进程的对应虚拟页均指向相同的物理内存页,系统将这些共享页标记为只读。当任一进程试图对某个共享页面执行写入操作时,会触发页保护异常。此时内核异常处理程序会介入:为该进程分配一个新的物理页,将原页内容复制到新页,并更新该进程的页表映射关系,使其指向新的私有物理页,随后再重新执行被中断的写入指令。
这种机制具有两大显著优势:一是 资源效率高,大部分情况下无需立即复制整个地址空间的物理内存,极大加快了 fork() 的执行速度并降低了瞬时内存开销;二是 隔离性完整,尽管初始时内存内容相同,但后续任一进程的修改都不会影响另一方,从而严格实现了进程私有地址空间的抽象语义。
因此,通过写时复制,fork() 不仅快速创建了一个逻辑独立、内存状态初始一致的新进程,也为后续进程可能执行的 exec() 或独立运算铺平了道路,成为 Unix 风格进程创建和高效并发执行的基石。
7.7 hello进程execve时的内存映射
execve 是 Unix/Linux 系统中用于执行新程序的系统调用,它在当前进程的上下文中加载并运行指定的可执行目标文件(如 hello),完全替换原有的进程映像。整个过程可分为以下步骤:
(1)清除原有用户空间
系统首先清空当前进程虚拟地址空间中的用户区域结构,为新程序提供一个干净、无冲突的运行环境。
(2)建立私有内存区域
随后,系统为新程序的代码段(.text)、数据段(.data)、未初始化数据段(.bss)、栈和堆等创建私有的内存区域结构,并采用写时复制技术映射物理内存。代码段与数据段直接映射自 hello 文件的对应段;.bss、栈和堆则初始化为二进制零,其中栈和堆的长度可在运行时动态增长。
(3)映射共享库
若 hello 链接了共享库(如 libc.so),系统会将其映射到进程虚拟地址空间的共享区域,允许多进程共用同一份物理代码与数据,以提升内存利用率。
(4)设置执行起点
最后,execve 将当前进程的程序计数器设置为新程序代码区域的入口(通常是 main 函数)。至此,原进程映像已被完全替换,新程序开始执行。
通过上述机制,execve 能够在保持进程外壳不变的情况下,实现程序映像的彻底切换,为进程执行新任务提供了标准而高效的内核支持。
7.8 缺页故障与缺页中断处理
缺页故障发生时,内核的缺页处理程序将启动一个系统化流程,以诊断问题并恢复进程的正常执行。具体过程如下:
首先,处理程序会验证触发缺页的虚拟地址的有效性。它检查该地址是否位于进程虚拟地址空间中已分配且有效的区域。如果虚拟地址不属于任何有效区域(例如,访问了未分配的内存或内核空间),则判定为非法访问,缺页处理程序会立即触发一个段错误,并向该进程发送 SIGSEGV 信号,通常导致进程终止。
若地址有效,处理程序接着检查访问权限。它会比对进程的操作模式(用户/内核)和操作类型(读、写、执行)与页表项(PTE)中记录的页面权限(读、写、执行、用户/超级用户)是否匹配。例如,在用户模式下试图写入一个标记为只读的页面,或试图执行一个没有执行权限的页面,都会被判定为权限违规。此时,处理程序会触发一个保护异常,并向进程发送 SIGSEGV 或 SIGBUS 信号,同样可能导致进程终止。
通过以上两层验证后,内核判定这是一个合法的缺页,并开始执行物理页的分配与映射。处理程序会从物理内存中选择一个牺牲页面(例如,通过近似最近最少使用算法或其他页替换策略)。如果该牺牲页面内容自上次加载后已被修改(脏页),则需先将其内容写回交换空间或对应的磁盘文件,以保持数据一致性,并释放其物理帧。
接下来,内核从后备存储加载目标页面。根据页面内容来源不同,可能从交换分区、原始可执行文件的对应段或共享库文件中读取数据。数据被载入到刚刚腾出的物理帧后,内核会更新页表:修改对应页表项,使其指向新的物理帧,并设置有效位、权限位等标志。
最后,缺页处理程序完成其工作:它将控制权返还给用户进程,并重新执行那条触发缺页故障的指令。由于所需虚拟页面现已常驻物理内存且映射已建立,指令得以正常访问内存,进程从被中断处继续执行,对上层应用而言,整个过程(除可能的性能延迟外)是透明的。
这一系列精密的步骤共同确保了虚拟内存系统的正确性与效率,使得应用程序能够在远超物理内存容量的地址空间中安全、透明地运行。
7.9动态存储分配管理
在程序开发中,动态内存管理是核心技术之一,开发者可以根据应用场景和语言特性选择不同的策略。
1. 基础手动管理(C语言风格)
使用 malloc 函数在堆上分配指定大小的内存块,并返回指向该内存的指针;使用 free 函数释放已分配的内存。这种方法将内存管理的责任完全交给程序员,若分配后未正确释放则会导致内存泄漏,若重复释放或访问已释放内存则会产生野指针等问题。
2. 构造器/析构器自动管理(C++风格)
使用 new 操作符在堆上分配内存并调用对象的构造函数进行初始化;使用 delete 操作符调用对象的析构函数并释放其内存。相比C语言的方式,new/delete 与对象生命周期绑定,更安全,但仍需手动配对使用。
3. 基于RAII的自动管理(现代C++)
通过智能指针(如 std::unique_ptr、std::shared_ptr)自动管理动态对象的生命周期。这些类模板封装了原始指针,并在析构时自动释放所持有的内存,从而极大降低了内存泄漏和悬挂指针的风险,是资源管理的推荐做法。
4. 专用分配器优化
对于频繁分配/释放小块内存或对性能有严苛要求的场景,可采用内存池技术。该技术预先分配一大块内存(池),程序从中进行次分配和回收,避免了频繁向操作系统申请内存的开销,能显著提升分配效率并减少内存碎片。
5. 定制化内存管理
在操作系统、游戏引擎或高频交易等特殊场景中,可自定义内存管理器。通过实现符合应用特点的分配策略(如分区分配、伙伴系统),可以精细控制内存布局、优化碎片问题,并满足特定的实时性或空间约束。
此外,printf 等标准库函数通常使用内部缓冲区(一般为静态或栈内存)进行格式化处理,一般不直接调用 malloc。仅在处理极长或动态格式等特殊情况时,其内部实现才可能临时使用堆内存。
7.10本章小结
本章聚焦于 hello 程序的存储器地址空间,系统阐述了 Intel 平台下的段式内存管理机制,并深入分析了 hello 程序所采用的页式内存管理。以 Intel Core i7 处理器在 Linux 环境下的运行实例为背景,详细揭示了从虚拟地址(VA)到物理地址(PA)的转换流程,以及如何通过物理地址访问实际内存。
此外,本章还探讨了 hello 进程在执行 fork 与 execve 操作时内存映射的动态调整过程,并专门分析了内存访问中可能出现的缺页异常。文中说明了操作系统如何通过缺页中断处理机制来透明地解决此类问题,从而保障程序运行的稳定性与效率。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux 的 I/O 设备管理建立在其“一切皆文件”的核心抽象之上。它将所有硬件设备(如磁盘、键盘、网络接口等)统一模型化为文件,通过位于 /dev 目录下的特殊设备文件进行表示。这使得应用程序可以使用与操作普通文件完全相同的 Unix I/O 接口(如 open、read、write、close 等系统调用)来访问和控制设备,实现了访问接口的高度统一。
在此抽象之下,Linux 通过设备驱动模型管理具体硬件。各类设备的驱动程序作为内核模块,负责封装硬件的所有底层操作细节,向上提供标准的文件操作接口。这种设计将应用程序与物理硬件彻底隔离,开发者无需关心设备的具体实现,只需通过文件描述符进行通用 I/O 操作,从而让系统能够灵活支持种类繁多的硬件,并让应用程序专注于业务逻辑。
8.2 简述Unix IO接口及其函数
Unix/Linux I/O 接口的核心设计哲学是“一切皆文件”。它将所有输入输出资源(硬件设备、磁盘文件、网络套接字等)统一抽象为文件,通过一组简洁的系统调用进行访问。
其核心是五个基本函数:
1.open:创建或打开文件,返回一个文件描述符(一个代表该打开文件的非负整数)。
2.read:从文件描述符读取数据到缓冲区。
3.write:将缓冲区数据写入文件描述符。
4.lseek:移动文件的读写偏移量。
5.close:释放文件描述符。
此外,ioctl 用于对设备执行特殊的控制操作。
该接口特点是无缓冲,直接执行系统调用,并提供文件描述符作为所有I/O操作的统一句柄,是底层、高效且通用的I/O模型基础。
8.3 printf的实现分析
printf实现分析
1. 格式化与系统调用
用户调用printf()后,C库中的vfprintf()解析格式字符串,vsprintf()将参数格式化为字符串。随后调用write()系统调用,通过int 0x80或syscall指令陷入内核,传递文件描述符、缓冲区地址和长度。
2. 内核处理与驱动
内核接收数据后,终端/TTY驱动处理控制字符。对于图形终端,帧缓冲区驱动将字符转换为字模数据。字模库存储每个字符的点阵信息(如8×16位图),驱动根据字符ASCII码索引获取对应字模。
3. 写入显存
驱动将字模点阵写入显存(VRAM)。每个像素用若干字节表示(如32位ARGB),通过位运算将字模的每个比特映射为前景色或背景色,计算显存中的对应位置进行写入。
4. 显示输出
显示控制器按固定频率(如60Hz)逐行扫描VRAM。每个像素的RGB分量通过数字信号接口传输至显示器。液晶显示器根据信号控制每个像素的液晶单元透光率,配合背光形成最终图像。
总结:printf历经用户层格式化、系统调用、内核驱动处理、字模转换、显存写入,最终由硬件扫描输出至屏幕,体现了软件与硬件的协同。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
用户层:getchar()调用getc(stdin),检查标准输入缓冲区。若缓冲区为空,则调用read(0, buf, size)系统调用,通过int 0x80或syscall进入内核。
内核层:系统调用由VFS转发至TTY驱动。若无输入字符,进程进入睡眠状态(TASK_INTERRUPTIBLE),加入等待队列,调度器切换其他进程运行。
硬件交互:键盘按键触发IRQ1中断,键盘驱动读取扫描码并转换为ASCII字符,放入TTY输入队列。随后唤醒等待进程,进程重新被调度。
数据返回:唤醒的进程从TTY缓冲区读取字符,经系统调用返回到用户空间。标准I/O库将字符放入缓冲区,getchar()返回该字符。
特殊处理:行规则处理退格、Ctrl+C等控制字符;Ctrl+D产生EOF(read返回0),getchar()返回EOF;终端模式影响阻塞行为(规范模式行缓冲,非规范模式立即返回)。
总结:getchar通过用户层缓冲、系统调用、内核TTY驱动和键盘中断处理,实现阻塞式字符输入,体现了Unix I/O的分层协同设计。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 中基于“一切皆文件”理念的 I/O 管理方法,阐述了标准的 Unix I/O 接口及相关系统调用函数。在此基础上,本章还对 printf 与 getchar 这两个常用函数的内部处理机制进行了简要分析,揭示了其底层如何通过 Unix I/O 接口实现数据缓冲与终端交互。
(第8章 1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello 程序的诞生,始于程序员在文本编辑器中的代码编写。这段由字符构成的源代码,需要经历一系列系统化的转换,才能成为可执行程序。
首先,预处理器(cpp)对 hello.c 文件进行处理,展开所有头文件与宏定义,生成扩展后的 hello.i 文件。接着,编译器(ccl)将其编译为汇编代码 hello.s。随后,汇编器(as)把汇编代码转换为可重定位目标文件 hello.o。最后,链接器(ld)将 hello.o 与所需库文件进行链接,生成最终的可执行目标文件 hello。
在终端中输入 ./hello 2024112996 cyz 138 1 后,Shell 识别该命令需要执行外部程序,于是调用 fork() 创建子进程。子进程通过 execve() 加载 hello 程序,将其代码、数据等内容映射到虚拟内存,并进入 main 函数开始执行。
在运行过程中,CPU 为进程分配时间片,使其按控制流顺序执行指令。每次内存访问均通过内存管理单元(MMU)配合页表,将虚拟地址转换为物理地址,确保程序能正确读写数据。信号机制也参与其中:例如按下 Ctrl+C 会发送 SIGINT 信号终止前台进程,Ctrl+Z 则发送 SIGTSTP 信号将其挂起。
当程序执行完毕,内核会安排父进程回收子进程资源,删除相关数据结构,完成一次完整的进程生命周期。整个过程展现了从源代码到进程执行、从内存映射到信号调度等一系列系统层级的协同运作,体现了计算机系统精密而高效的设计。
通过这次实验,我感受到了计算机系统的美妙。在每个简单的表面背后都藏着许多精密而复杂的操作。我希望以后能在更加了解它的基础上更好的使用计算机,让它绽放属于自己的光彩。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2402_87687507/article/details/156545718




