计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 生命科学和医学学院
学 号 2023113108
班 级 2352001
学 生 杜若林
指 导 教 师 刘松波
计算机科学与技术学院
2025年5月
本报告系统剖析 Hello 程序从源代码到进程执行的完整生命周期,深入阐释计算机系统底层机制。通过预处理、编译、汇编及链接流程,解析 Hello.c 转换为可执行文件的技术细节,包括 ELF 格式解析、符号重定位与动态链接实现。在进程管理层面,重点分析 fork-exec 机制如何实现进程创建与程序加载,结合 CPU 调度、上下文切换及信号处理机制,揭示多任务并发执行的底层逻辑。存储管理部分围绕逻辑地址到物理地址的转换展开,涵盖段式管理、页式管理、TLB 与四级页表协同机制,以及三级缓存架构下的内存访问优化。研究通过理论分析与实验验证,完整呈现程序在计算机系统中的执行原理,为理解操作系统核心机制提供具体案例。
关键词:Hello 程序;预处理;进程管理;存储管理;地址转换;动态链接;三级缓存
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(program to process),从程序到进程的生命周期:
在program阶段,程序员将hello world等代码写入文本文件(hello.c),这是静态程序。
在编译当中,hello.c首先经过预处理展开头文件,宏变换(#include <stdio.h>等),生成.i文件;然后是经过编译,将c代码变为汇编语言,生成.s文件;其次是汇编阶段,汇编器将汇编语言变为机器语言成.o文件,文件内包含二进制指令和符号表;最后一步是链接阶段,链接器将目标文件与标准库(如libc)合并,生成可执行程序hello(该文件存储在磁盘上。此时是非运行状态下。
加载和执行时,用户在shell中输入./hello,进程进入CPU的调度队列中,操作系统通过fork()和execeve()函数加载hello程序到数据内存当中;操作系统为其分配独立的虚拟空间,通过内存管理单元(MMU),将虚拟物理地址(VA)映射到物理地址(PA)上;CPU从内从上读取指令,执行hello world的输出逻辑。
020(Zero to Zero),从零到零的完整闭环:
程序加载时,全零初始化,执行的未初始化全局变量被加载到内存时被自动清零,初始数据以0填充;程序结束时,资源归零,main函数执行完毕,进程进入僵尸状态,等待父进程通过wait()函数将其回收,操作系统通过虚拟内存管理回收占用的内存页面
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:AMD Ryzen 7 7840HS with Radeon 780M Graphics 3.80 GHz
软件:window11及x86_64 linux ubuntu 24.04.2
开发及调试工具:vscode,gcc,gdb,odjdump
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
Hello.i:预处理后的源文件;
Hello.s:汇编程序文件;
Hello.o:可重定位目标文件;
Hello:可执行目标文件;
1.4 本章小结
本章首先以计算机系统术语,详细阐述了 Hello 程序从 P2P 到 O2O 的完整生命周期,揭示了从代码编写到程序执行及资源回收的底层原理;接着介绍了编写论文过程中使用的软硬件环境与开发调试工具,体现实践的技术支撑;最后列出各中间结果文件及其作用,展示程序从源代码到可执行文件的转换过程。这些内容为后续深入探讨 Hello 程序在计算机系统中的运行机制奠定了理论与实践基础。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理(Preprocessing)是编译流程中的第一个阶段,发生在编译器对源代码进行语法分析和语义分析之前。它的主要任务是对源代码中的预处理指令(以#开头的指令)进行处理,生成供后续编译阶段使用的预处理后的源文件(通常以.i为扩展名)。
作用:头文件包含,将指定头文件(如标准库头文件stdio.h、自定义头文件mylib.h)的内容直接插入到预处理指令所在的位置,形成一个完整的源文件;宏定义和替换:将代码中所有出现的NAME标识符替换为指定的token-sequence(文本序列);
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
应截图,展示预处理过程!
图 1预处理指令
2.3 Hello的预处理结果解析
从预处理后的文件中可以看出,主要是处理以#开头的指令,将文件内容展开并进行宏替换,头文件当中包含诸如#include <stdio.h>,#include <unistd.h>,#include <stdlib.h>的函数调用;里面含有大量的系统底层类型和结构体,诸如__u_char(unsigned char),__u_int(unsigned int)等用于底层内存操作,FILE 结构体用于文件流操作,包含缓冲区指针(如 _IO_read_ptr、_IO_write_ptr)、文件描述符(_fileno)、锁机制(_lock)等字段,是 printf 等函数的底层实现基础;同时预处理后的文件包含函数说明和库依赖,如标准I/O函数,系统调用函数等
2.4 本章小结
预处理是 C 语言编译流程的第一步,主要处理以#开头的指令。展开头文件,将#include指定的头文件内容递归替换到源文件中,使编译器能识别标准库函数、系统调用接口及相关类型定义;处理宏定义,将代码中用#define声明的宏名称替换为对应的文本内容。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译是将经过预处理后的高级语言编写的源代码转换为汇编程序的过程。这一过程由编译器完成,需经过词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成等多个阶段,每个阶段逐步将源代码解析、转换并优化,最终生成可汇编文件。
编译的作用:语法与语义验证,捕获预处理后代码中的语法错误和语义错误,确保代码逻辑的正确性;架构适配,根据目标硬件生成特定架构的汇编指令;生成可调式的中间产物,汇编代码(.s)保留了函数调用关系、变量分配和控制流结构。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图 2编译指令
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1 基本数据类型处理
3.3.1.1 整数类型(int)
在 C 语言源码中,int 类型在汇编中主要通过movl(move long)指令操作。如下:
movl $0, -4(%rbp) ; 将立即数0存入rbp-4位置(对应局部变量i)
addl $1, -4(%rbp) ; 将rbp-4位置的值加1(对应i++)
cmpl $9, -4(%rbp) ; 比较rbp-4的值与9(对应i <= 9的比较)
内存分配:局部变量i被分配在栈帧中(%rbp-4)
操作指令:使用movl(32 位整数操作)、addl(加法)、cmpl(比较)
3.3.1.2 指针类型(char*)
命令行参数(argv)作为指针数组处理:
movq -32(%rbp), %rax ; 将argv的地址加载到rax
addq $16, %rax ; 偏移到argv[2](姓名参数)
movq (%rax), %rdx ; 加载argv[2]的值(字符串地址)
内存访问:通过基址 + 偏移量访问指针数组
多级间接寻址:(%rax)表示解引用指针
3.3.1.3 函数指针
标准库函数(如printf、sleep)通过 PLT(Procedure Linkage Table)调用:
call printf@PLT ; 通过PLT调用printf函数
call sleep@PLT ; 通过PLT调用sleep函数
动态链接:运行时解析函数地址
参数传递:遵循 x86-64 ABI 规范(通过寄存器传递参数)
3.3.2 控制流操作处理
3.3.2.1 条件判断(if 语句)
cmpl $5, -20(%rbp) ; 比较argc与5
je .L2 ; 如果相等则跳转到.L2
比较指令:cmpl比较两个值
跳转指令:je(jump if equal)实现条件分支
3.3.2.2 循环结构(for 循环)
.L3:
cmpl $9, -4(%rbp) ; 比较i与9
jle .L4 ; 如果i <=9则跳转到.L4
...
.L4:
... ; 循环体
jmp .L3 ; 无条件跳转到.L3
循环条件:通过比较和条件跳转实现
循环体:通过无条件跳转形成闭环
3.3.3 函数调用处理
3.3.3.1 标准库函数调用(如 printf)
leaq .LC1(%rip), %rax ; 加载格式化字符串地址
movq %rax, %rdi ; 将格式化字符串地址存入rdi(第一个参数)
movq %rdx, %rsi ; 将姓名参数存入rsi(第二个参数)
movl $0, %eax ; 浮点参数个数(这里为0)
call printf@PLT ; 调用printf函数
参数传递:前 6 个参数通过寄存器(%rdi, %rsi, %rdx, %rcx, %r8, %r9)传递
返回值:通过%rax返回
3.3.3.2 系统调用(如 exit)
movl $1, %edi ; 将退出状态码1存入edi
call exit@PLT ; 调用exit函数
参数规范:与标准库函数一致
终止流程:直接终止当前进程
3.3.4 字符串处理
3.3.4.1 字符串常量
.LC1:
.string "Hello %s %s %s\n"
只读数据段:字符串常量存储在.rodata段
地址引用:通过标签(如.LC1)引用字符串地址
3.3.4.2 字符串操作
movq (%rax), %rcx ; 加载argv[3](手机号)
movq %rcx, %rdx ; 将手机号存入rdx
指针操作:通过指针传递字符串地址
格式化输出:由printf函数处理格式化逻辑
3.3.5 栈帧管理
3.3.5.1 栈帧结构
pushq %rbp ; 保存上一层栈帧指针
movq %rsp, %rbp ; 设置新的栈帧指针
subq $32, %rsp ; 为局部变量分配32字节空间
栈帧布局:
%rbp:基址指针(指向当前栈帧底部)
%rsp:栈顶指针
局部变量:存储在%rbp下方
3.3.5.2 局部变量访问
movl %edi, -20(%rbp) ; 保存argc到%rbp-20位置
movq %rsi, -32(%rbp) ; 保存argv到%rbp-32位置
相对寻址:通过%rbp的偏移量访问局部变量
3.3.6 类型转换处理
3.3.6.1 atoi 函数调用
movq (%rax), %rax ; 加载argv[4](秒数字符串)
movq %rax, %rdi ; 将字符串地址存入rdi
call atoi@PLT ; 调用atoi进行字符串转整数
movl %eax, %edi ; 将返回的整数存入edi
函数调用:通过标准库函数atoi完成转换
返回值处理:atoi的返回值通过%eax寄存器传递
3.3.7 内存操作
3.3.7.1 栈空间分配
subq $32, %rsp ; 为局部变量分配32字节栈空间
动态分配:通过调整%rsp指针实现
对齐要求:栈指针需 16 字节对齐(x86-64 规范)
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
编译作为 C/C++ 程序构建的核心环节,负责将预处理后的中间代码(.i 文件)转化为汇编语言(.s 文件)。这一过程通过词法分析识别代码单元,借助语法分析验证结构合法性,利用语义检查确保类型匹配,并最终通过代码优化生成高效的机器指令。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编阶段是将汇编程序(.s 文件)转换为机器语言二进制程序(.o 可重定位目标文件)的过程。这一过程由汇编器完成,主要工作是将汇编指令(如mov、add)翻译为对应的机器码(二进制指令),并解析符号引用(如函数名、变量名),生成可重定位的目标文件。目标文件包含机器码、数据和链接信息。
汇编的作用:指令翻译,每个汇编指令对应唯一的机器码模式,由硬件架构决定;符号解析与地址分配,记录符号表(Symbol Table),存储函数名、变量名及其地址,供链接阶段使用;生成可重定位代码,这些地址在链接阶段会被调整为实际内存地址。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -C hello.s -o hello.o
图 3汇编指令
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
以下是分析elf头信息:
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 ,其中 7f 45 4c 46 是 ELF 文件的魔数 ,用于标识该文件是 ELF 格式;ELF 即 Executable and Linkable Format(可执行与可链接格式) ,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的标准文件格式 。由 UNIX 系统实验室(USL)开发并发布,用作应用程序二进制接口(ABI) ,也是 Linux 等类 Unix 操作系统主要的可执行文件格式 。
图 4elf头信息
节头表是一个数据结构数组 ,其中每个元素(节头)描述了 ELF 文件中一个节(Section )的属性 。它相当于 ELF 文件中各个节的 “目录” ,通过节头表可以快速定位和了解每个节的相关信息,便于链接器、加载器等工具对文件内容进行处理 。包含的信息有:节名称、节标志、节在文件中的偏移、节的大小、节的地址、节头表字符串偏移。
图 5elf节头表
4.4 Hello.o的结果解析
函数结构与初始化
11e9: f3 0f 1e fa endbr64 ; Intel CET安全指令
11ed: 55 push %rbp ; 保存旧栈帧指针
11ee: 48 89 e5 mov %rsp,%rbp ; 设置新栈帧
11f1: 48 83 ec 20 sub $0x20,%rsp ; 分配32字节栈空间
11f5: 89 7d ec mov %edi,-0x14(%rbp) ; 保存argc
11f8: 48 89 75 e0 mov %rsi,-0x20(%rbp) ; 保存argv
栈帧结构:为局部变量分配 32 字节(0x20),argc存放在-0x14(%rbp),argv存放在-0x20(%rbp)。
参数检查(argc == 5)
11fc: 83 7d ec 05 cmpl $0x5,-0x14(%rbp) ; 比较argc和5
1200: 74 19 je 121b <main+0x32> ; 相等则跳转
1202: 48 8d 05 ff 0d 00 00 lea 0xdff(%rip),%rax ; 加载错误信息地址
1209: 48 89 c7 mov %rax,%rdi ; 设置puts参数
120c: e8 8f fe ff ff call 10a0 <puts@plt> ; 打印错误信息
1211: bf 01 00 00 00 mov $0x1,%edi ; 设置exit参数(1)
1216: e8 c5 fe ff ff call 10e0 <exit@plt> ; 异常退出
功能:检查命令行参数数量是否为 5,否则打印错误信息并以状态码 1 退出。
循环初始化
121b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) ; 初始化循环变量i=0
1222: eb 56 jmp 127a <main+0x91> ; 跳转到循环条件检查
循环变量:i存放在-0x4(%rbp),初始化为 0。
循环体(打印参数并休眠)
1224: 48 8b 45 e0 mov -0x20(%rbp),%rax ; 加载argv
1228: 48 83 c0 18 add $0x18,%rax ; 计算argv[3]地址
122c: 48 8b 08 mov (%rax),%rcx ; 加载argv[3]
122f: 48 8b 45 e0 mov -0x20(%rbp),%rax ; 再次加载argv
1233: 48 83 c0 10 add $0x10,%rax ; 计算argv[2]地址
1237: 48 8b 10 mov (%rax),%rdx ; 加载argv[2]
123a: 48 8b 45 e0 mov -0x20(%rbp),%rax ; 再次加载argv
123e: 48 83 c0 08 add $0x8,%rax ; 计算argv[1]地址
1242: 48 8b 00 mov (%rax),%rax ; 加载argv[1]
1245: 48 89 c6 mov %rax,%rsi ; 设置printf第2参数(argv[1])
1248: 48 8d 05 e9 0d 00 00 lea 0xde9(%rip),%rax ; 加载格式字符串地址
124f: 48 89 c7 mov %rax,%rdi ; 设置printf第1参数(格式串)
1252: b8 00 00 00 00 mov $0x0,%eax ; 清空varargs寄存器
1257: e8 54 fe ff ff call 10b0 <printf@plt> ; 调用printf
功能:打印命令行参数argv[1](格式串中可能包含argv[2]和argv[3])。
125c: 48 8b 45 e0 mov -0x20(%rbp),%rax ; 加载argv
1260: 48 83 c0 20 add $0x20,%rax ; 计算argv[4]地址
1264: 48 8b 00 mov (%rax),%rax ; 加载argv[4]
1267: 48 89 c7 mov %rax,%rdi ; 设置atoi参数
126a: e8 61 fe ff ff call 10d0 <atoi@plt> ; 字符串转整数
126f: 89 c7 mov %eax,%edi ; 设置sleep参数
1271: e8 7a fe ff ff call 10f0 <sleep@plt> ; 休眠指定秒数
1276: 83 45 fc 01 addl $0x1,-0x4(%rbp) ; i++
功能:将argv[4]转换为整数并调用sleep休眠相应秒数,然后递增循环变量。
循环条件检查(i <= 9)
127a: 83 7d fc 09 cmpl $0x9,-0x4(%rbp) ; 比较i和9
127e: 7e a4 jle 1224 <main+0x3b> ; i<=9则继续循环
循环逻辑:使用jle(小于等于跳转)实现i从 0 到 9 的 10 次循环。
收尾操作
1280: e8 3b fe ff ff call 10c0 <getchar@plt> ; 等待用户输入
1285: b8 00 00 00 00 mov $0x0,%eax ; 返回值设为0
128a: c9 leave ; 恢复栈帧
128b: c3 ret ; 返回
功能:等待用户按键后,返回状态码 0 退出程序。
图 6hello.o反汇编
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
在计算机程序的构建过程中,汇编扮演着承上启下的关键角色。作为编译流程的核心阶段,它将人类可读的汇编指令精准翻译为计算机能够直接执行的二进制机器码,并以此为基础构建出可重定位目标文件(.o)。这些文件如同精密的拼图组件,为后续的链接和加载过程提供了必要的基础。
汇编的核心功能主要体现在四个方面:指令编码、符号解析、节组织以及重定位信息生成。在指令编码环节,汇编器如同一个精准的翻译家,将每一条符号化的汇编指令按照特定的规则转换为对应的二进制操作码。符号解析则负责处理程序中出现的各种符号,将它们与实际的内存地址关联起来。节组织工作会将程序的不同部分,如代码、数据、全局变量等,分类整理到不同的节(Section)中,每个节都有其特定的用途和属性。而重定位信息生成则为后续的链接过程提供了必要的信息,使得不同的目标文件能够正确地组合在一起。
第5章 链接
5.1 链接的概念与作用
链接的概念:在程序的编译过程中,链接是将多个目标文件(.o)或库文件组合成可执行文件的过程,其本质是解决符号引用,合并数据代码段。
链接的作用:符号解析:连接器会在指定的库中查找printf的定义将hello.o的符号引用与符号定义绑定;地址与空间分配:链接器为每个段分配实际的内存地址空间,将相对地址转化为绝对地址;重定位和合并段位符号。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
Ld-ohello-dynamic-linker/lib64/ld-linux-x86-64.so.2/usr/lib/x86_64-linux-gnu/crt1.o/usr/lib/x86_64-linux-gnu/crti.zhello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
图 7hello的elf头信息
图 8hello的elf节头表信息(部分)
图 9 hello的elf符号表
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
从图中信息可以看出,hello的虚拟空间地址映射情况;
0x400000 - 0x401000:该区间是hello程序的一个映射区域,对应程序的只读代码段(.text段的一部分),存放程序的指令代码 ,一般具有读权限(r)。
0x401000 - 0x402000:通常是程序的可执行代码段,存放实际执行的机器指令,具有读和执行权限(r-x)。
0x402000 - 0x403000 以及后续几个相邻的0x1000大小的区间:是程序的只读数据段(.rodata等),存放常量等只读数据,只有读权限(r)。
0x404000 - 0x405000:可能是程序的读写数据段(.data等),存放已初始化的全局变量和静态变量等,具有读写权限(rw)
图 10hello的虚拟地址空间
使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
5.5.1 hello.o与hello的不同以及链接过程
文件类型与功能:
hello.o:是可重定位目标文件,它包含了编译后的机器指令代码和数据,但其中的地址等信息还未完全确定。它是编译过程中源文件(hello.c)经过预处理、编译和汇编阶段生成的中间产物,此时符号地址还未最终确定,不能直接运行。
hello:是可执行文件,它是通过将一个或多个可重定位目标文件(hello.o)以及所需的库文件,经过链接器处理后生成的。它包含了完整的、可在操作系统上直接运行的程序映像,地址已经重定位完成,具备了运行所需的全部信息。
地址信息:
hello.o:内部的符号地址是相对地址或者未确定的,如函数调用地址、变量地址等,只是在目标文件内部有相对的偏移量表示,因为此时还不知道最终在内存中的位置。
hello:地址是绝对地址,链接器在链接过程中会根据目标文件和库文件的布局,确定每个符号在最终可执行文件中的具体地址,使得程序在加载到内存后能够正确执行。
链接过程主要包括以下几个步骤:
符号解析:链接器会扫描所有输入的可重定位目标文件(如hello.o)和库文件,将每个目标文件中未定义的符号(如函数调用、外部变量引用等)与其他目标文件或库中定义的符号进行匹配。例如hello.o中如果调用了puts函数,链接器会在标准库中找到puts函数的定义,并记录其所在位置。
重定位:在确定了所有符号的位置后,链接器会修改目标文件中对这些符号的引用地址。因为在可重定位目标文件中,符号地址是相对的,链接器会根据最终的布局将这些相对地址转换为绝对地址。例如在hello.o中对某个函数的调用地址,链接器会根据该函数在可执行文件中的实际位置进行调整。
合并段:链接器会将输入文件中的各个段(如.text代码段、.data数据段等)进行合并。它会根据段的属性和地址分配规则,将相同类型的段合并在一起,并调整段内的偏移量等信息,形成最终可执行文件的段布局。
生成可执行文件:完成上述步骤后,链接器会将合并和重定位后的段写入到一个新的文件中,即生成可执行文件hello,这个文件包含了程序运行所需的全部代码和数据,并且地址等信息都已经正确设置。
5.5.2 hello对hello.o的重定位
重定位表的作用:hello.o中有一个重定位表,它记录了哪些指令中的地址需要在链接时进行修改。例如,对于hello.o中调用外部函数(如puts)的指令,重定位表会记录该指令的位置以及所引用的符号(puts)。
符号地址确定:链接器在符号解析阶段确定了puts函数在标准库中的实际地址。
重定位操作:链接器根据重定位表中的信息,找到hello.o中调用puts函数的指令位置,即将原来的未确定地址替换为puts函数的实际地址。这样,当hello程序运行时,调用puts函数的指令就能正确跳转到标准库中puts函数的实现代码处。
图 11hello的反汇编代码(部分)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
gdb查找_start的地址及内容情况:
图 12gdb操作查找_start相关内容
后续的主要函数及地址如下表所示:
地址 | 名称 |
0x401000 | _init |
0x4010f0 | _start |
0x401120 | _dl_relocate_static_pie |
0x401125 | main |
0x4011c8 | _fini |
0x404038 | _end |
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
图 13got表内容
从图中可以看出GOT的起始地址0x403fd8
在_start和main处设置断点的时候运行程序,分别去查看0x403fd8处的数值变化情况,所示如下图所示:
图 14程序执行前
图 15程序执行后
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
将hello.o(可重定位目标文件)与库文件(如libc.so)通过符号解析、地址分配、段合并,生成hello(可执行文件),解决符号引用和地址重定位。通过ELF格式分析,重定位过程,动态链接与GOT/PLT,执行流程,掌握了链接的内在运行
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是程序的一次执行实例,是操作系统资源分配和调度的基本单位。它不仅包含程序的代码和数据,还包括程序执行时的运行环境
进程的作用:一方面,是资源分配的基本单位,操作系统通过进程分配 CPU 时间、内存空间、I/O 设备等资源;另一方面,实现并发进程,多进程系统通过 CPU 时间片轮转,让多个进程 “同时” 运行,提升系统利用率。
6.2 简述壳Shell-bash的作用与处理流程
1.作用:Shell 是操作系统的用户接口,负责解释用户输入的命令并与系统内核交互。Bash(Bourne-Again SHell)作为 Linux/Unix 系统中最常用的 Shell,其核心作用包括:命令解释与执行,脚本编程与自动化,环境进程与管理,输入与输出重定向与管道,兼容性与扩展性
2.处理流程:用户在 Bash 中输入一条命令或执行脚本时,接收用户输入的命令行(如ls -la /etc),按空格分割为标记(Token),识别重定向符、管道符等特殊符号;解析并替换命令中的变量(如$HOME替换为用户主目录路径)和命令替换(如$(date)替换为当前日期);查找此处的命令并执行;重定向输入输出并处理管道;最后子进程执行完毕后,返回状态码。Bash 继续等待用户的下一条命令,或处理脚本中的后续逻辑。
6.3 Hello的fork进程创建过程
fork()用于创建一个与父进程几乎相同的子进程,子进程复制父进程的内存空间(代码段、数据段、堆、栈)、文件描述符、环境变量等,但拥有独立的进程 ID。
子进程从 fork() 调用的下一条指令开始执行。当 hello 程序执行 fork() 时,用户态代码通过软中断(如 int 0x80 或 syscall 指令)切换到内核态。内核根据系统调用号(如 __NR_fork)定位到 sys_fork() 内核处理函数。内核为子进程分配新的 task_struct 结构,包含进程 ID(PID)、状态、优先级等元数据。内核将子进程的父进程 ID(ppid)设置为调用者的 PID,并将子进程添加到进程调度队列中。子进程创建完成后,内核可能选择立即调度子进程执行,或继续执行父进程。
6.4 Hello的execve过程
execve函数的功能是在当前进程的执行上下文中加载并启动一个新的可执行程序实体。该函数通过指定的可执行文件路径、参数列表及环境变量列表完成程序的加载与初始化。与fork不同,execve在正常执行时不会返回至调用处,仅在加载失败时返回错误状态。当execve成功执行时,将触发如下操作序列:首先由加载器调用启动代码,该代码负责初始化运行时环境(含程序栈结构的配置);随后将可执行文件的代码段与数据段从存储设备载入内存;最终通过跳转至程序入口地址,将执行控制权移交至新程序的main函数,实现程序的正式启动。这一过程彻底替换原进程的执行映像,使调用进程转变为运行全新的程序实体。
6.5 Hello的进程执行
操作系统通过进程调度机制实现多任务并发执行,为各进程构建独占中央处理器的虚拟执行环境。当利用调试器进行单步跟踪时,程序计数器(PC)的跳转序列构成逻辑控制流,其指令来源包括可执行文件或动态链接库。由于中央处理器资源有限,进程需通过分时机制共享处理单元:各进程在获取时间片运行后被系统抢占,由调度器选择其他就绪进程执行。该过程包含核心操作:其一为调度决策,内核调度器基于进程优先级、时间片耗尽或 I/O 阻塞等事件,决定暂停当前进程并选择就绪进程;其二为上下文切换,当进程切换时,内核需保存当前进程的完整执行状态(上下文),并恢复目标进程的上下文信息,涵盖寄存器状态、内存管理数据及内核数据结构等。
上下文切换的具体流程如下:当内核作出调度决策时,按如下流程实施上下文切换操作:首先保存当前进程上下文,将其寄存器状态、程序计数器等执行状态信息存入进程控制块(PCB);继而更新调度队列,将当前进程移入相应状态队列(就绪或阻塞),并选定下一个待执行进程;随后恢复目标进程上下文,从其 PCB 中加载寄存器配置及内存映射等状态数据;接着切换地址空间,通过更新 CR3 寄存器切换页表,保障进程内存空间隔离;最终跳转至目标进程的程序计数器指向的指令,完成执行控制权移交。
用户态与内核态的转换机制如下:
用户态向内核态的转换通过系统调用或中断事件触发。中央处理器硬件自动将用户态寄存器内容保存至内核栈,并跳转至内核预定义的中断处理程序入口。
内核态向用户态的转换发生于系统调用或中断处理完成后,内核恢复用户进程的上下文环境,中央处理器切换至用户态并继续执行后续指令。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
异常的种类:中断,陷阱,故障,终止
信号的种类:SIGINT,SIGQUIT,SIGTSTP,SIGHUP,SIGKILL,SIGSTOP,SIGCONT,SIGCHLD,SIGSEGV,SIGFPE等
处理的方式:忽略,捕获或默认某种行为
图 16乱输入,按回车时运行过程
图 17键入ctrl-c时运行过程
图 18键入ctrl-z时运行过程
图 19各进程PID
图 20所有作业信息
图 21进程树状图
图 22重新进行进程
图 23杀死进程
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章以可执行程序hello为研究对象,系统解构其从加载到终止的过程,深入阐释进程管理的核心技术机制。程序执行过程遵循三个层次分明的技术阶段:首先通过fork-exec机制链完成进程实体创建与程序映像加载,继而依托 CPU 调度器与内存管理单元实现指令流的有序执行,最终通过exit系统调用链完成资源释放与进程回收。
在启动阶段,fork系统创建子进程;随后execve调用触发内存映像替换,将hello的 ELF 格式程序数据载入虚拟地址空间,完成代码段、数据段的重定位与动态链接。运行阶段,内核为进程分配时间片,借助上下文切换机制保存与恢复进程执行状态,同时内存管理单元通过页表映射实现虚拟地址到物理地址的转换。终止阶段,进程通过exit系统调用清理资源,内核回收进程并向父进程发送SIGCHLD信号,完成进程生命周期的闭环管理。
整个执行周期中,系统底层机制形成协同工作网络:硬件中断处理键盘输入等异步事件,信号机制传递进程间异步通知,异常处理模块捕获段错误(SIGSEGV)等运行时故障。内存管理单元保障进程地址空间独立性,动态链接器则在程序加载阶段完成共享库符号解析与重定位。这些机制共同构建了包含进程创建、资源分配、指令执行、异常处理、资源回收的过程,确保hello程序在复杂计算环境中实现确定性执行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是程序中使用的相对地址,由段选择器和段内偏移量组成。在编译时,编译器将代码中的符号地址转换为逻辑地址。
线性地址:线性地址是逻辑地址经分段机制转换后的中间地址。在纯分页系统中,线性地址与虚拟地址等价;在分段 + 分页系统中,线性地址是分段转换后的结果,再通过分页机制转换为物理地址。
虚拟地址:虚拟地址是操作系统为每个进程提供的独立地址空间,通过页表映射到物理内存。在纯分页系统中,虚拟地址与线性地址完全等价;在分段 + 分页系统中,虚拟地址是线性地址的同义词。
物理地址:物理地址是内存芯片中实际存储单元的地址,由硬件内存控制器管理。虚拟地址通过页表(Page Table)转换为物理地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel x86 架构中,段式管理是将程序使用的逻辑地址转换为线性地址的核心机制。这一机制在保护模式下尤为重要,它不仅实现了内存隔离,还为多任务操作系统提供了基础支持。逻辑地址由两部分组成:一个是段选择器,16 位,指定当前使用的段。高 13 位为段索引(Segment Index),指向 GDT(全局描述符表)或 LDT(局部描述符表)中的条目。第 2 位为描述符表指示位。低 2 位为请求特权级(RPL):指定访问该段的特权级别(0-3 级)。另一种是段内偏移量(Offset):32 位或 64 位,表示段内的相对地址。当逻辑地址向线性地址转换时,通过段索引在 GDT/LDT 中定位段描述符(其中段描述符是每个描述符占 8 字节(64 位),包含Base Address,Limit,G(Granularity)位,D/B 位,Access Rights)。检查 RPL 与段描述符的 DPL(描述符特权级),确保权限匹配。计算线性地址:线性地址=段基址+段内偏移量
7.3 Hello的线性地址到物理地址的变换-页式管理
在hello程序的执行过程中,地址空间的转换涉及段式与页式管理。首先,逻辑地址经段式管理(利用段选择器和描述符表)转换为线性地址,实现内存分段与特权保护。随后,线性地址(虚拟地址)通过页式管理(多级页表、TLB 缓存)转换为物理地址,页目录索引、页表索引和页内偏移分别解析,查找页表项计算物理地址,写时复制和缺页中断优化内存使用与加载。
7.4 TLB与四级页表支持下的VA到PA的变换
在 TLB 与四级页表的支持下,虚拟地址(VA)到物理地址(PA)的转换过程如下:首先,VA 被分解为四级页表索引(如页目录指针表、页目录表、页表、页帧偏移)。CPU 优先查询 TLB,若 TLB 命中(缓存中存在对应页表项),直接提取物理页框基址,与页内偏移计算 PA,快速完成转换。若 TLB 未命中,则遍历四级页表:依次访问页目录指针表项(确定页目录表基址)、页目录表项(确定页表基址)、页表项(获取物理页框基址),最终计算 PA。同时,将该页表项存入 TLB,供后续访问加速。四级页表通过分级索引,仅加载当前使用的页表部分,减少内存开销;TLB 缓存高频访问的页表项,提升转换效率。对于hello程序,其虚拟地址(如代码段、数据段地址)在转换时,先经 TLB 快速处理或四级页表遍历,映射到物理内存,确保指令执行和数据访问的正确性。这一机制支撑了程序在虚拟地址空间内的高效运行,实现内存隔离与资源共享,是现代操作系统内存管理的核心,保障了多任务并发与程序的稳定执行。
7.5 三级Cache支持下的物理内存访问
在三级缓存架构下,物理内存的访问过程如下:当 CPU 得到物理地址后,依据访问内容的类型(数据或指令),分别指向 L1 数据缓存(L1 d-cache)或 L1 指令缓存(L1 i-cache)。以数据访问为例,物理地址被拆分为标记、组索引和偏移三部分。CPU 利用组索引找到 L1 缓存中对应的组,然后在该组的缓存行里,将每行的标记与目标地址的标记进行比对。若标记一致且有效位为 1,就表明缓存命中,直接从偏移位置取出数据。要是 L1 缓存未命中,就接着查询 L2 缓存,同样进行标记比对;若 L2 也没命中,再去查询 L3 缓存。如果三级缓存都没命中,最后就通过内存控制器从物理内存(像 DRAM)中读取数据,并且按照缓存替换策略将其存入缓存,以便后续使用。指令访问的流程与之类似,只是使用的是 L1 i-cache。这种分层的缓存结构,借助局部性原理(时间局部性和空间局部性),加快了数据和指令的访问速度,减少了 CPU 直接访问内存的次数,提高了hello程序等应用的执行效率。比如,hello程序中的循环指令和常用数据(如字符串常量)会被缓存起来,多次访问时就不需要从内存读取,大大降低了访问延迟。
7.6 hello进程fork时的内存映射
当hello进程调用fork时,内核首先为子进程构建虚拟地址空间,初始阶段与父进程共享物理内存页,采用写时复制(COW)策略。具体而言,父子进程的页表项指向相同的物理页框,且这些页标记为 “写时复制”(COW 标志置位)。此时,hello的代码段(.text段)、只读数据段(.rodata段)以及未修改的可读数据段(.data段)在物理层面完全共享,无需复制内存,大幅减少fork的开销。
当子进程首次对共享内存(如栈中的局部变量、可写数据段的全局变量)执行写操作时,CPU 触发页错误(缺页异常)。内核处理该异常时,检查页表项的 COW 标志:若为真,则为子进程分配新的物理页,复制原页内容(如栈页或数据页),更新子进程的页表项指向新页,并清除 COW 标志。此后,子进程的写操作直接访问新页,而父进程仍使用原页,实现内存隔离。例如,hello中main函数内的局部变量在栈上,子进程修改时会触发 COW,复制栈页以确保父子进程的栈数据独立。
7.7 hello进程execve时的内存映射
当 hello 进程调用 execve 时,内核会首先卸载当前进程的原有内存映射,为新程序的加载做准备。此时,原进程的代码段、数据段、堆和栈等内存区域会被逐步释放,仅保留必要的进程上下文。接着,内核解析目标可执行文件的 ELF 格式,通过读取 ELF 头和程序头表,确定代码段(.text)、数据段等段的虚拟地址范围、大小及访问权限。
内核根据 ELF 程序头中的信息,为新程序创建虚拟内存区域。例如,将代码段映射到 0x400000 起始的只读区域,数据段映射到 0x404000 附近的可读写区域,并为堆栈分配高地址空间。这些映射不立即加载物理内存,而是通过页表建立虚拟地址与物理页的关联,首次访问时触发缺页中断加载。对于动态链接的程序,内核会进一步定位动态链接器,将其映射到内存,并由链接器负责解析共享库依赖,通过修改 GOT 表建立符号地址映射。
7.8 缺页故障与缺页中断处理
当进程访问的目标页未加载至物理内存时,会触发缺页故障。这种情况下,操作系统需通过按需加载机制将目标页从外存调入主存。特别地,若该页属于内存映射文件(如可执行程序的代码段或数据段),则对应的磁盘文件会作为分页后备存储,直接承担页数据的读取来源,而非通过独立的交换分区。
缺页中断发生时,CPU 会将控制权转交至内核的中断处理例程。该例程首先保存当前进程上下文,随后启动页面置换流程:依据预设的淘汰策略(如 LRU、FIFO 或 LFU 算法)选定物理内存中待替换的页框。若目标页存在修改痕迹(脏页标记),需先将其写回磁盘以保证数据一致性。接着从外存(内存映射文件或交换分区)读取所需页数据至选定的物理页框,并更新进程页表项(PTE)的物理地址映射及状态位(如有效位、访问位、脏页位)。完成上述操作后,内核恢复进程上下文,原指令将重新执行,此时因目标页已载入内存,访问得以正常完成。整个过程中,内存映射文件作为分页交换载体,直接参与缺页时的数据读写,形成 “虚拟地址 - 物理内存 - 外存文件” 的三级数据映射体系。
7.9本章小结
本章聚焦 hello 程序的存储管理机制,深入探讨其内存地址空间体系,以及 fork、execve 操作触发的内存映射重构过程。此外,针对 TLB 与四级页表协同下的虚拟地址到物理地址转换逻辑,结合三级缓存架构下的物理内存访问流程进行了系统解析。文末还对缺页故障的处理机制及动态内存分配策略展开说明,完整呈现了从地址空间抽象到物理内存访问的全链路技术实现,揭示现代操作系统存储管理中地址转换、缓存优化与内存调度的核心原理。
结论
一、程序构建的全流程解析
预处理与编译:hello.c经gcc -E预处理展开头文件与宏定义,生成hello.i;通过gcc -S编译为汇编代码hello.s,完成词法分析、语法验证及架构适配。
汇编与链接:gcc -c将汇编代码转换为可重定位目标文件hello.o,包含 ELF 格式的代码段、数据段与符号表;链接阶段通过ld合并标准库,解析符号引用(如printf),生成可执行文件hello,完成地址重定位与段合并。
二、进程管理的核心机制
进程创建与程序加载:fork通过写时复制(COW)创建子进程,共享父进程物理内存页,仅在写操作时触发页复制;execve卸载原进程内存映射,解析 ELF 格式并重新映射新程序的代码段、数据段,实现进程内容替换。
调度与异常处理:内核通过时间片轮转与优先级调度分配 CPU 资源,上下文切换时保存 / 恢复进程状态(寄存器、页表等);键盘输入触发SIGINT(Ctrl+C)、SIGTSTP(Ctrl+Z)等信号,进程可捕获或忽略信号以实现优雅终止或暂停。
三、存储管理的地址转换体系
逻辑地址到物理地址转换:Intel 段式管理将逻辑地址(段选择器 + 偏移量)转换为线性地址,页式管理通过四级页表将虚拟地址映射到物理地址,TLB 缓存加速转换过程。例如,hello的.text段虚拟地址0x401000经页表解析后映射到物理页框。
缓存与内存优化:三级缓存(L1/L2/L3)按标记 - 组索引 - 偏移结构加速物理内存访问,未命中时触发缺页中断,内核通过 LRU 等策略置换页面,从磁盘加载目标页并更新页表。
四、关键技术与实验验证
通过readelf分析 ELF 格式、gdb调试动态链接过程(GOT 表更新)、ps/pstree监控进程状态,验证了hello从代码到进程的全生命周期。实验结果表明,写时复制、动态链接、多级缓存等机制显著提升了程序执行效率与系统资源利用率,体现了计算机系统设计中抽象、分层与优化的核心思想。
附件
hello.c:原始hello程序的C语言代码
hello.i:预处理过后的hello代码
hello.s:由预处理代码生成的汇编代码
hello.o:二进制目标代码
hello:进行链接后的可执行程序
参考文献
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.