关注

哈工大2025CSAPP大作业——程序人生-Hello‘s P2P

计算机系统

 

大作业

题     目  程序人生-Hello’s P2P   

专       业  工科试验班(计算机与电子通信)                     

学     号      2023112265                   

班     级       23L0518                  

学       生                        

指 导 教 师        史先俊                

计算机科学与技术学院

20255

   

本文以“Hello's P2P”程序为研究对象,系统探讨了从源代码到可执行程序的全生命周期及其在计算机系统中的运行机制。通过预处理、编译、汇编、链接等阶段的分析,揭示了高级语言代码向机器指令的转换过程,并结合进程管理、存储管理等系统机制,深入剖析了程序从静态文件到动态进程的完整流程。实验基于Ubuntu环境,使用GCC工具链生成中间文件,结合反汇编、ELF格式解析等手段验证了各阶段的理论实现。此外,通过分析进程创建、虚拟内存映射、缺页中断处理等核心机制,阐述了操作系统对程序运行的底层支持。本文不仅完整呈现了程序“从代码到进程”(P2P)和“从零到零”(020)的全过程,还通过实践验证了计算机系统各层组件的协同工作原理,为深入理解系统级软件开发与优化提供了理论依据和实践参考。

关键词:程序编译;进程管理;虚拟内存;ELF格式;动态链接                           

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

  

第1章    概述

1.1       Hello简介

1.1.1    Hello的P2P

1.1.2    Hello的020

1.2       环境与工具

1.3       中间结果

1.4       本章小结

第2章    预处理

2.1       预处理的概念与作用

2.2       在Ubuntu下预处理的命令

2.3       Hello的预处理结果解析

2.4       本章小结

第3章    编译

3.1       编译的概念与作用

3.2       在Ubuntu下编译的命令

3.3       Hello的编译结果解析

3.4       本章小结

第4章    汇编

4.1       汇编的概念与作用

4.2       在Ubuntu下汇编的命令

4.3       可重定位目标elf格式

4.3.1        文件头

4.3.2    section头

4.3.3        符号表

4.3.4        重定位节

4.4       Hello.o的结果解析

4.4.1    hello.o的反汇编与hello.s的对照分析

4.4.2        机器语言的构成

4.4.3        汇编语言与机器语言的映射

4.4.4        操作数的不一致性:分支转移与函数调用

4.5       本章小结

第5章    链接

5.1       链接的概念与作用

5.2       在Ubuntu下链接的命令

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

5.3.1    ELF头

5.3.2    section头

5.3.3        程序头

5.4       hello的虚拟地址空间

5.4.1        虚拟地址空间信息

5.4.2        对照分析

5.5       链接的重定位过程分析

5.5.1    Hello的反汇编结果

5.5.2        对比分析

5.5.3        链接过程

5.5.4        重定位

5.6       hello的执行流程

5.7       Hello的动态链接分析

5.8       本章小结

第6章        hello进程管理

6.1       进程的概念与作用

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

6.2.1        作用

6.2.2        处理流程

6.3       Hello的fork进程创建过程

6.4       Hello的execve过程

6.5       Hello的进程执行

6.6       hello的异常与信号处理

6.6.1        正常运行

6.6.2        运行时按下Ctrl-C

6.6.3        运行时按下回车

6.6.4        运行时按下Ctrl-Z

6.7       本章小结

第7章        hello的存储管理

7.1       hello的存储器地址空间

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

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

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

7.4.1    TLB的命中流程与缓存机制

7.4.2        四级页表遍历流程

7.4.3    VA到PA的转换

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

7.5.1        三层Cache的层级结构

7.5.2        物理内存访问流程

7.6       hello进程fork时的内存映射

7.7       hello进程execve时的内存映射

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

7.8.1        缺页故障触发条件及解决方式

7.8.2        缺页中断解决方式

7.9       动态存储分配管理

7.10     本章小结

第8章        hello的IO管理

8.1       Linux的IO设备管理方法

8.2       简述Unix IO接口及其函数

8.3       printf的实现分析

8.4       getchar的实现分析

8.5       本章小结

结论

附件

参考文献

第1章         概述

1.1     Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

1.1.1  Hello的P2P

P2P指的是From Program to Process,在这里,就是从Program——hello.c变为运行时进程——Process。

整个过程如下:

预处理:hello.c经预处理器处理,展开宏(如#include、#define),生成 hello.i 文件。

编译:编译器(如gcc)将预处理后的代码编译为汇编代码,也即.s 文件。

汇编:汇编器将汇编代码转换为机器指令也即.o 目标文件,包含代码段hello.text、数据段hello.data、hello.bss等。

链接:链接器合并目标文件与库函数(如libc),解析符号地址,生成可执行文件也就是hello.out文件。此时程序具备完整的逻辑地址空间。

加载:执行时,操作系统通过execve系统调用加载可执行文件,分配虚拟内存空间,映射代码段、数据段,并初始化堆、栈。

进程创建:Shell调用fork()创建子进程,子进程通过execve()加载hello.out,成为独立的进程实体,进入运行态。

1.1.2  Hello的020

020指From Zero to Zero。开始,内存中无hello文件相关内容,就是“zero”的状态,而后,hello程序启动,不再是zero,程序运行结束,shell回收进程,重又回到zero。

1.2      环境与工具

泰山服务器以及Visual Studio Community 2022

图 1 泰山服务器配置

图 2 Visual Studio Community 2022配置信息

1.3      中间结果

hello.c:原始hello程序的C语言代码

hello.i:预处理过后的hello代码

hello.s:由预处理代码生成的汇编代码

hello.o:二进制目标代码

hello:进行链接后的可执行程序

1.4     本章小结

本章主要概述了Hello程序的P2P(从程序到进程)和020(从零到零)生命周期,明确了其在计算机系统中的完整执行流程。通过介绍预处理、编译、汇编、链接、加载及进程管理的核心步骤,结合中间结果文件(如.i、.s、.o等),初步展现了程序从源代码到可执行文件的全过程,为后续章节的深入分析奠定了基础。

(第10.5分)

第2章         预处理

2.1     预处理的概念与作用

预处理是代码的准备工作,是C程序编译的第一步,将代码整合为一份完整可编译的代码文件,由预处理器处理源代码中的预处理指令(以#开头的命令),生成修改后的源代码也即.i文件,供后续编译使用。

2.2     在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

图 3 预处理命令

2.3     Hello的预处理结果解析

对比源程序和预处理后的程序,观察发现,代码剧增至三千多行,这是因为预处理命令将头文件的程序、宏变量、特殊符号等插入到代码中。程序的最后一部分与hello.c中的main函数完全相同。

图 4 hello的预处理结果

2.4     本章小结

本章详细探讨了预处理的作用与实现。通过gcc -E命令生成预处理文件hello.i,解析了头文件展开、宏替换等操作,展示了预处理后代码的扩展特性。结果表明,预处理整合了程序依赖的外部资源,为编译阶段提供了完整的代码输入。

(第20.5分)

第3章         编译

3.1     编译的概念与作用

编译是将高级语言的源代码转换为汇编代码的过程,由编译器(如gcc)完成。

它的作用是语法检查、代码优化、最后生成汇编代码:输出与机器架构相关的汇编指令文件(.s),为后续汇编阶段提供输入。  

3.2     在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

图 5 编译的命令

3.3     Hello的编译结果解析

图 6 hello编译结果

1. 数据

(1) 常量

字符串常量:

.LC0UTF-8编码的提示字符串

.LC1:格式化字符串"Hello %s %s %s\n",用于printf输出。

.LC0:

    .string "\347\224\250\346\263\225: Hello ..."  // 常量字符串

.LC1:

    .string "Hello %s %s %s\n"                   // 常量格式化字符串

(2) 变量

局部变量:

main函数中通过栈分配局部变量,例如:

str     w0, [sp, 28]       // 存储argc到栈偏移28字节处

str     x1, [sp, 16]       // 存储argv指针到栈偏移16字节处

str     wzr, [sp, 44]      // 初始化局部变量为0wzr是零寄存器)

2. 表达式与类型

(1) 类型转换

显式转换:

atoi函数将字符串参数转换为整数(char* → int):

ldr     x0, [sp, 16]        // 加载argv指针

add     x0, x0, 32          // argv[4](第五个参数)

ldr     x0, [x0]            // 获取argv[4]的字符串地址

bl      atoi                // 显式转换为整数

(2) 类型与sizeof

指针类型:

通过偏移量访问argv数组元素(argv为char**类型):

add     x0, x0, 8           // argv[1](偏移8字节,因为指针大小为8字节)

ldr     x1, [x0]            // 加载argv[1]的值(char*类型)

3. 赋值与操作符

(1) 赋值操作

直接赋值:

mov     x29, sp             // sp的值赋给x29(栈帧指针)

str     wzr, [sp, 44]      // 0赋给栈偏移44字节处的变量

4. 算术与逻辑操作

(1) 算术操作

加法:

add     x0, x0, 8           // x0 = x0 + 8(计算argv[1]地址)

比较:

ldr     w0, [sp, 28]        // 加载argc

cmp     w0, 5               // 比较argc是否等于5

beq     .L2                 // 若等于,跳转到.L2

(2) 逻辑操作

条件判断:

cmp     w0, 5               // 检查argc是否为5

beq     .L2                 // 等于则跳转(逻辑等于判断)

5. 数组、指针与结构操作

(1) 数组访问

argv数组操作:

ldr     x0, [sp, 16]        // 加载argv指针(char** argv

add     x0, x0, 8           // argv[1](每个元素占8字节)

ldr     x1, [x0]            // 获取argv[1]的值(char*

(2) 指针解引用

指针偏移与加载:

ldr     x0, [sp, 16]        // 加载argv指针(基地址)

add     x0, x0, 16          // 偏移16字节(argv[2]

ldr     x2, [x0]            // 解引用获取argv[2]的值

6. 控制转移

条件分支(if/else)

cmp     w0, 5               // 检查argc是否为5

beq     .L2                 // 等于则跳转到.L2if分支)

// 否则执行以下代码(else分支)

adrp    x0, .LC0

bl      puts                // 输出错误提示

bl      exit                // 退出程序

7. 函数操作

(1) 参数传递

值传递:

printf的参数通过寄存器x0-x3传递(ARM64调用约定):

mov     x3, x0              // 第四个参数(argv[3]

adrp    x0, .LC1            // 第一个参数(格式化字符串地址)

bl      printf              // 调用printf

地址传递:

exit的参数通过寄存器w0传递(值传递):

mov     w0, 1               // 参数为退出状态码1

bl      exit                // 调用exit

(2) 函数调用与返回

调用:

bl      puts                // 调用puts函数

bl      printf              // 调用printf函数

返回:

函数结束时通过ldp恢复栈帧并返回:

ldp     x29, x30, [sp], 48  // 恢复寄存器并调整栈指针

ret                         // 返回到调用者

3.4     本章小结

本章分析了编译阶段将高级语言转换为汇编代码的过程。通过gcc -S命令生成hello.s文件,解析了汇编代码中的常量、变量、控制流及函数调用逻辑,揭示了编译器如何优化代码并生成机器相关指令。

(第32分)

第4章         汇编

4.1     汇编的概念与作用

汇编是将包含汇编语言的.s文件翻译为机器语言指令,并把这些指令打包成为一个可重定位目标文件,生成目标文件.o文件的过程,由汇编器完成。

它的作用是指令转换——将汇编指令转换为二进制形式机器码,符号解析:记录代码中的符号(如函数名、全局变量)及其地址,生成目标文件(.o)以及段划分——组织代码段(.text)、数据段(.data、.bss)等,供链接器使用。

4.2     在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

图 7 汇编命令

4.3     可重定位目标elf格式

4.3.1    文件头

首先查看文件头(ELF Header)。其包含了描述ELF文件整体结构和属性的信息,包括ELF标识、目标体系结构、节表偏移、程序头表偏移等。

图 8 文件头

4.3.2    section

图 9 section头

如上图,hello.o中一共有13个节,8个重定位条目,7个全局符号。在这些重定位条目中,有两个对应rodata节中的数据地址,它们是printf使用的两个字符串地址。另外6个重定位条目都是被call指令调用过的函数地址。

夹在ELF头和节头部表之间的都为节,包含了文件中出现的各个节的语义,包括节类型、位置和大小等信息。

4.3.3    符号表

图 10 符号表

这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。Value列是表示函数相对于.text section起始位置的偏移量(16进制),size列表示所占字节数,type列为数据类型, bind列中global为全局符号,local为局部符号,vis列在c语言中并未使用,可以忽略, ndx(index)列为索引值。    

4.3.4    重定位节

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

图 11 重定位节

4.4     Hello.o的结果解析

图 12 hello.o的结果

4.4.1    hello.o的反汇编与hello.s的对照分析

有如下几个不同:

(1)    反汇编代码跳转指令的操作数由段名称变成了确定的地址

(2)    hello.s里的数是十进制表示,hello.asm里的数是十六进制表示

4.4.2    机器语言的构成

(1)    基本组成:

①  操作码(Opcode):指示CPU执行的具体操作。

②  操作数(Operand):指定操作涉及的数据或地址,可以是立即数、寄存器编号或内存地址。

(2)    指令格式:

不同架构的指令格式差异较大:

①  x86(CISC架构):变长指令,操作码和操作数长度灵活。

②  ARM(RISC架构):定长指令(32位或16位),操作码和操作数字段固定。

(3)    操作数类型:

①  立即数:直接编码在指令中的数值。

②  寄存器:指定寄存器编号。

③  内存地址:通过寻址模式计算实际地址。

4.4.3    汇编语言与机器语言的映射

(1)    一一对应关系:

每条汇编指令对应一条机器指令,例如:

①  汇编指令:ADD EAX,EBX

②  机器码:01 D8。

(2)    符号化与编码转换:

①  标签与地址:汇编中的符号会被汇编器转换为机器码中的相对偏移或绝对地址。

②  寻址模式:汇编中的复杂寻址会被编码为机器码中的基址、变址和位移字段。

4.4.4    操作数的不一致性:分支转移与函数调用

(1)    分支转移指令

①  汇编语言:使用符号标签(如JMP LOOP)。

②  机器语言:

                  i.           相对跳转:操作数为当前指令与目标地址的偏移量(如E9 0F 00表示向前跳转15字节)。

                 ii.           绝对跳转:操作数为目标地址的绝对值(如FF 25 00 00 00 00表示跳转到内存地址中的值)。

(2)    函数调用指令

①  汇编语言:使用函数名(如CALL printf)。

②  机器语言:

                  i.           直接调用:操作数为函数入口的绝对地址(如E8 00 00 00 00,链接后填充实际地址)。

                 ii.           间接调用:操作数为寄存器的值(如FF D0表示调用EAX指向的地址)。

4.5     本章小结

本章通过gcc -c命令生成可重定位目标文件hello.o,并利用objdump工具解析其ELF格式。重点分析了.text、.data等段的结构,符号表与重定位条目的作用,阐明了汇编器如何将汇编指令转换为二进制机器码,并为链接阶段提供符号引用信息。

(第41分)

第5章         链接

5.1     链接的概念与作用

链接是指链接是将目标文件(.o)和库文件合并为单一可执行文件的过程,由链接器完成。

它的作用是:符号重定位——解析不同目标文件中的符号引用(如调用外部函数),确保地址正确;合并代码与数据:将分散的代码段、数据段整合为完整的可执行文件以及库绑定——链接静态库(直接嵌入代码)或动态库(运行时加载),例如C标准库(libc)。

5.2     在Ubuntu下链接的命令

ld -o hello \

              /usr/lib/aarch64-linux-gnu/crt1.o \

              /usr/lib/aarch64-linux-gnu/crti.o \

              /usr/lib/aarch64-linux-gnu/crtn.o \

              hello.o \

              -lc \

              -dynamic-linker /lib/ld-linux-aarch64.so.1 \

              -m aarch64linux

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

5.3.1    ELF

Type处显示的是EXEC,表示时可执行目标文件,这与hello.o不同。hello中的节的数量为30个。

图 13 ELF头

5.3.2    section

Section表对hello中所有信息进行了声明,包括了大小、偏移量、起始地址以及数据对齐方式等信息。根据始地址和大小,我们就可以计算节头部表中的每个节所在的区域。

图 14 section头

5.3.3    程序头

图 15 程序头

5.4     hello的虚拟地址空间

5.4.1      虚拟地址空间信息

图 16 虚拟地址空间

代码段:0x400000-0x401000,权限 r-xp(读、执行)。

数据段:0x411000-0x412000,权限 rw-p(读、写)。

动态库映射:

libc-2.31.so: 代码段:0xfffff7e46000-0xfffff7fa1000,权限 r-xp。数据段:0xfffff7fb0000-0xfffff7fb4000,权限 rw-p。

ld-2.31.so:动态链接器的代码和数据段。代码段:0xfffff7fcc000-0xfffff7ffe000,权限 r-xp。数据段:0xfffff7ffd000-0xfffff7ffe000,权限 rw-p。

5.4.2    对照分析

(1) 代码段

地址范围:

ELF文件:0x4005e0-0x400838(.text 等);

虚拟地址空间:0x400000-0x401000(页对齐扩展)

权限:

ELF文件:R E(读、执行);

虚拟地址空间:r-xp(读、执行,不可写)

内容:

ELF文件:代码、只读数据;

虚拟地址空间:代码、只读数据(可能合并优化)

(2) 数据段

地址范围:

ELF文件:0x410e38-0x41104c(.data 等);

虚拟地址空间:0x411000-0x412000(页对齐扩展)

权限:

ELF文件:RW(读、写);

虚拟地址空间:rw-p(读、写,不可执行)

内容:

ELF文件:全局变量、GOT、动态链接信息;

虚拟地址空间:全局变量、动态库符号表

5.5     链接的重定位过程分析

5.5.1    Hello的反汇编结果

图 17 hello的反汇编结果1

图 18 hello的反汇编结果2

5.5.2    对比分析

1.虚拟地址不同,hello.o的反汇编代码虚拟地址从0开始,而hello的反汇编代码虚拟地址从0x400000开始。这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。

图 19 hello.o的反汇编代码虚拟地址

图 20 hello的反汇编代码虚拟地址

2.外部符号引用:hello.o未解析(如 puts、printf),而hello通过 PLT/GOT 动态链接

3. 动态链接信息:hello.o无,hello包含.dynamic、.got.plt、.interp

4.重定位条目:hello.o包含大量重定位信息(.rela.text)而hello无未解析的重定位条目

5.段合并与优化:hello.o各段独立(如 .text、.data)而hello段按权限合并(如代码段 r-xp、数据段 rw-p)

5.5.3    链接过程

1)符号解析

解析 hello.o 中未定义的符号(如 puts、printf),绑定到动态库(如 libc.so)中的实际地址。

2)段合并与地址分配

合并所有目标文件的代码段(.text)、数据段(.data)等,并分配虚拟地址。

3)重定位修正

根据重定位条目(如 R_AARCH64_CALL26),修正指令中的地址偏移。

4)生成动态链接信息

创建 .plt(过程链接表)、.got.plt(全局偏移表)和 .dynamic 节,支持运行时动态链接。

5.5.4    重定位

1. hello.o 的重定位条目

      比如:

图 21 hello.o的重定位条目节选

重定位类型:R_AARCH64_CALL26(26 位相对调用)。

需修正的指令地址:0x24(对应 bl 指令的操作数字段)。

符号:puts(未解析的外部函数)。

2. 链接器的重定位处理

链接器根据重定位条目完成以下操作:

(1) 符号解析

将 puts 绑定到动态库 libc.so 中的实际地址。

由于动态库的地址在链接时未知,链接器生成 PLT(过程链接表) 和 GOT(全局偏移表) 结构,支持运行时延迟绑定。

(2) 生成 PLT 条目

在hello中,为puts生成PLT条目:

图 22 为puts生成的PLT条目

PLT作用:首次调用puts@plt时,通过动态链接器解析puts的实际地址并写入GOT。

GOT条目:0x411000 + 0x30(即0x411030)存储puts的实际地址。

(3) 修正调用指令

将hello.o中的未解析调用bl 0修正为跳转到puts@plt:

图 23 修正调用指令

操作数计算:目标地址0x4005b0与当前指令地址0x400654的偏移为-0xa4,编码为26位相对地址。

5.6     hello的执行流程

1、_start (0x4005e0)

初始化栈帧指针x29=0

初始化链接寄存器x30=0

准备参数后调用__libc_start_main@plt (0x400570)

若失败调用abort@plt (0x4005a0)

2、C运行时初始化

__libc_start_main (0xfffff7e66d28)

设置线程局部存储(TLS)

调用初始化函数__libc_csu_init (0x4006d8)

注册终止函数__libc_csu_fini (0x400758)

最终跳转到main函数

3、主程序执行

main (0x400640)

参数检查:argc == 5?

参数错误分支:调用puts@plt (0x4005b0) 打印用法信息

调用exit@plt (0x400550) 退出

循环中调用:

printf@plt (0x4005d0) 打印信息

atoi@plt (0x400560) 转换参数

sleep@plt (0x400580) 睡眠

最终调用getchar@plt (0x4005c0) 等待输入

4、程序终止

exit (0xfffff7e7c600)

调用__run_exit_handlers (0xfffff7e7c390)

执行所有注册的退出处理函数

最终通过_exit系统调用终止进程

5.7     Hello的动态链接分析

此处用printf举例

1.       readelf 查找符号的重定位信息

图 24 重定位信息

可以看出printf的GOT地址为0x411040

2.       链接前

图 25 链接前地址

3.       链接后

图 26 链接后地址

5.8     本章小结

本章深入探讨了链接的核心机制,包括符号解析、地址重定位与动态库绑定。通过分析hello可执行文件的ELF格式、虚拟地址空间及PLT/GOT动态链接过程,揭示了链接器如何整合目标文件与共享库,生成具备完整逻辑地址空间的可执行程序。

(第51分)

第6章         hello进程管理

6.1     进程的概念与作用

进程是程序的一次动态执行实例,拥有独立的虚拟内存空间、系统资源(如文件句柄)和运行状态。

它的作用就是提供给程序独立的逻辑控制流——程序独占使用处理器的假象以及私有的地址空间,即程序独占使用内存系统的假象。

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

6.2.1    作用

Shell是用户与操作系统内核之间的交互接口,主要作用包括:

1)命令解释与执行

解析用户输入的命令(如ls、grep),调用内核接口执行程序。

区分内置命令(如cd、echo)和外部程序(如/bin/ls)。

2)脚本自动化

支持编写脚本文件(.sh),通过逻辑控制(条件判断、循环)实现任务自动化。

3)环境管理

定义和管理环境变量(如PATH、HOME),影响程序行为和命令搜索路径。

4)输入输出控制

通过重定向符(>、<、|)管理数据流,例如:

5)进程与作业管理

控制前台/后台任务(&、jobs、fg、bg)。

处理信号(如Ctrl+C 终止进程)。

6.2.2    处理流程

1)读取输入

从标准输入或脚本文件读取命令字符串。

2)解析与分词

分词:按空格、引号、运算符(如|、&&)拆分命令为单词(Token)。

3)扩展操作

变量扩展:替换$VAR为变量值(如echo $HOME)。

通配符扩展:解析*、? 匹配文件名(如ls *.txt)。

命令替换:执行$(command) 并替换结果(如echo $(date))。

算术扩展:计算$((表达式))(如echo $((2+3)))。

4)重定向处理

解析 >(覆盖写入)、>>(追加写入)、2>(错误输出)等符号,设置输入输出流。

5)执行命令

命令类型判断:

内置命令:由Bash直接执行(如cd)。

外部程序:通过fork()创建子进程,exec()加载程序。

路径搜索:根据PATH变量查找可执行文件(如/bin/ls)。

6)结果返回

等待子进程结束,获取退出状态码($?)。

状态码0表示成功,非0表示错误。

6.3     Hello的fork进程创建过程

在终端输入./hello后,Shell解析命令,识别出需要执行的可执行文件hello并检查hello是否存在、是否具有可执行权限,并确定其路径。当Hello程序执行到fork() 系统调用时,操作系统内核会创建一个与父进程几乎完全相同的子进程。下面是具体步骤:

1)内核创建子进程

复制父进程资源:内核复制父进程的代码段、数据段、堆栈段、文件描述符表、环境变量等资源。实际采用写时复制技术:物理内存页在初始时共享,仅当任一进程尝试修改时才会复制该页。

分配进程控制块(PCB):内核为新进程分配唯一的进程ID(PID)和进程控制块(包含进程状态、寄存器值、资源指针等)。

2)区分父进程与子进程

依据fork() 返回值:

父进程:返回子进程的PID(正整数)。

子进程:返回0。

失败时:返回-1(如系统资源不足)。

3)父进程与子进程独立运行

父进程和子进程从fork()返回后开始独立运行,执行顺序由操作系统调度决定。修改子进程的变量(如全局变量、堆内存)不会影响父进程。文件描述符共享或独立取决于fork()前的打开模式。

4)进程终止与回收

终止方式:

父进程:可通过wait()或waitpid()等待子进程结束,并获取其退出状态。

子进程:执行完毕后通过exit()终止,资源由内核回收。

若父进程未及时回收子进程,子进程终止后成为僵尸进程(保留退出状态直到父进程调用wait())。

6.4     Hello的execve过程

execve用于加载并执行一个新的程序,替换当前进程的代码段、数据段、堆栈段,但保留进程ID(PID)和其他资源(如文件描述符)。下面是execve 执行的具体过程:

1)调用 execve 的触发场景

假设用户通过Shell执行./hello,Shell 会通过以下步骤启动Hello程序:

fork子进程:Shell调用fork()创建一个子进程。

execve替换映像:子进程调用 execve("./hello", argv, envp),加载Hello程序并替换自身的内存映像。

2)execve的执行流程

      i. 参数传递

调用 execve 时需提供以下参数:

程序路径:./hello(需为绝对路径或可被 PATH 解析的相对路径)。

参数列表(argv):包含命令行参数。

环境变量(envp):继承自父进程或自定义的环境变量。

    ii. 内核验证文件

文件存在性检查:内核检查./hello是否存在。

权限验证:检查当前用户是否有执行权限(x权限位)。

文件格式识别:通过文件头确认可执行文件类型。

   iii. 加载可执行文件

解析ELF文件:读取ELF头,获取程序入口点、Section Header和Program Header等。根据程序头表加载代码段(.text)、数据段(.data、.bss)到内存。

内存映射:为代码段、数据段分配虚拟内存页,并设置权限(如代码段为 R-X,数据段为RW-)。动态链接器(ld.so)加载共享库(如libc.so)并解析符号。

   iv. 设置堆栈与参数

构建初始堆栈、寄存器初始化。

     v. 跳转到程序入口

执行_start、运行main函数。

3)关键错误处理

文件不存在:返回错误码ENOENT,进程继续执行原代码。

权限不足:返回EACCES。

非可执行文件:返回ENOEXEC。

内存不足:返回ENOMEM。

6.5     Hello的进程执行

1)进程创建与初始化

Shell调用fork()

Shell创建子进程,复制自身代码、数据、文件描述符等资源(写时复制)。子进程获得独立 PID,但暂时与父进程共享执行环境。

子进程通过execve("./hello", argv, envp) 加载Hello程序,替换自身内存映像。execve是系统调用,触发软中断(如svc指令),切换到内核态执行。

2)进程调度与时间片分配

Hello进程初始化完成后,被操作系统加入就绪队列,等待CPU调度。

时间片分配与抢占:操作系统为每个进程分配固定时间片,Linux默认使用完全公平调度器(CFS),动态调整进程优先级。

调度触发条件:时间片耗尽(时钟中断触发);进程主动让出CPU(如等待 I/O 或调用sleep());更高优先级进程就绪(抢占式调度)。

上下文切换:保存上下文——将当前进程的寄存器、程序计数器(PC)、栈指针(SP)等状态保存到其进程控制块(PCB)中。加载上下文——从目标进程的 PCB 恢复寄存器、PC、SP 等状态。在这里上下文切换由内核完成,需切换到核心态。

3) Hello 进程的执行过程

      i. 用户态执行

执行main函数。在这里,用户态指令有:执行算术运算、逻辑判断、函数调用等普通指令以及访问用户空间内存(如全局变量、堆栈数据)。

    ii. 系统调用与核心态切换

触发系统调用:当printf调用write()输出字符串时,触发系统调用 SYS_write。通过软中断(如 svc)切换到内核态,执行内核代码。核心态操作有:内核验证参数合法性(如缓冲区地址、长度)以及调用设备驱动将数据写入终端(如 /dev/tty)。系统调用返回后,恢复用户态执行。

   iii. 时间片耗尽与抢占

时钟中断(Timer Interrupt):硬件定时器每隔固定时间(如1ms)发送中断信号。中断触发后,CPU切换到内核态处理中断。

检查时间片:内核发现Hello进程的时间片耗尽,标记其为就绪状态,重新加入调度队列。选择下一个进程(如Shell)执行,触发上下文切换。

4)进程终止与资源回收

正常终止:Hello执行return 0后,调用exit()系统通知内核终止进程。exit()触发系统调用,用户态转为核心态。

资源回收:内核释放进程占用的内存、文件描述符等资源。父进程(Shell)通过 wait()获取子进程退出状态,避免僵尸进程。

6.6     hello的异常与信号处理

6.6.1    正常运行

图 27 正常运行

6.6.2    运行时按下Ctrl-C

发送SIGINT信号,向子进程发送SIGKILL信号使进程终止并回收

图 28 按下Ctrl-C的结果

6.6.3    运行时按下回车

没有影响,正常运行

图 29 按下回车的结果

6.6.4    运行时按下Ctrl-Z

发送SIGTSTP信号,子进程被挂起

图 30 按下Ctrl-Z的结果

之后再进行输入:

Ps: 会显示所有的进程及其状态

图 31 再输入ps的结果

Jobs:显示被停止的进程

图 32 再输入jobs的结果

Pstree:显示进程树,包括所有进程

图 33 再输入pstree的结果

Fg:使第一个后台作业变成前台作业,这里hello是第一个后台作业,所以变为前台执行

图 34 再输入fg的结果

Kill :依据ps给的输出来对kill输入,“杀死”hello进程

图 35 依据ps进行下面的输入

图 36 再输入kill的结果

6.7     本章小结

本章结合Shell的执行流程,解析了fork创建子进程、execve加载程序、进程调度与信号处理等关键机制。通过实验展示了Ctrl-C、Ctrl-Z等信号对进程的影响,并结合ps、jobs等命令验证了进程状态切换与资源回收的底层逻辑。

(第62分)

第7章         hello的存储管理

7.1     hello的存储器地址空间

1)逻辑地址(Logical Address)

定义:逻辑地址是程序在代码中直接使用的地址,通常由段选择子(Segment Selector) +偏移量(Offset)组成。在分段机制下,逻辑地址通过段表转换为线性地址。

特点:编译器和链接器生成的地址,基于程序视角的地址空间。

分段机制在多数现代操作系统中已被简化或禁用(如Linux使用平坦内存模型),逻辑地址通常直接映射为线性地址。

在main函数中,比如main函数编译后的地址为0x400630

2)线性地址(Linear Address,即虚拟地址)

定义:逻辑地址经过分段机制转换后的地址,称为线性地址。在分页机制启用时,线性地址需进一步转换为物理地址。

现代操作系统通常禁用分段,逻辑地址直接等于线性地址。

3)虚拟地址(Virtual Address)

定义:在分页机制下,虚拟地址是进程视角的地址空间,通过页表映射到物理地址。

虚拟地址空间:每个进程拥有独立的虚拟地址空间,实现内存隔离。

页表映射:由操作系统和硬件(MMU)管理,将虚拟地址按页(如4KB)映射到物理内存或磁盘交换区。

在hello中,比如说代码段的地址:

00000000004005e0 <_start>:

0000000000400630 <main>: 

4)物理地址(Physical Address)

定义:实际内存硬件上的地址,由虚拟地址通过分页机制转换得到。

转换过程:

MMU(内存管理单元)根据页表将虚拟地址拆分为页号+页内偏移,查找页表项(PTE)获取物理页帧号,最终生成物理地址。

公式:物理地址=物理页帧号*页大小+页内偏移 

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

1)地址转换流程

选择段描述符表:

根据段选择符的TI位选择GD或LDT。

GDT的基址由GDTR寄存器指定,LD的基址由LDTR寄存器指定。

2)查找段描述符:

通过索引值*8(每个描述符占8字节)定位到段描述符表中的条目。

3)提取段基址和限长:

从段描述符中读取段基址和段限长。

4)权限检查:

比较当前特权级(CPL)与段描述符的DPL,若权限不足触发通用保护异常(GPF)。

5)生成线性地址:

线性地址=段基址+偏移量。

检查偏移量是否超过段限长,若越界触发段错误异常(Segment Fault)。

其中,段选择符(16位):

高13位:索引(Index),用于在段描述符表中查找段描述符。

第2位:TI(Table Indicator),0表示使用全局描述符表(GDT),1表示使用局部描述符表(LDT)。

低2位:RPL(Requested Privilege Level),请求特权级(0 为内核态,3 为用户态)。

偏移量(32/64位):目标地址在段内的偏移。

段描述符表(GDT/LDT):GDT为全局描述符表,LDT为局部描述符表

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

流程如下:

(1) 获取顶级页表基址(CR3 寄存器)

CR3 寄存器 存储当前进程的 PML4 表(Page Map Level 4) 的物理基地址。

操作系统在进程切换时更新 CR3,确保每个进程拥有独立的页表。

(2) 逐级查询页表

PML4 表(第4级)→页目录指针表(第3级)→页目录表(第2级)→页表(第1级)。

(3) 合成物理地址:物理地址 = (PFN × 页大小) + 页内偏移

页大小为 4KB(12 位偏移),因此物理地址为 (PFN << 12) | (线性地址低12位)。

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

7.4.1    TLB的命中流程与缓存机制

1)TLB命中流程

步骤:

CPU生成虚拟地址(VA)。

检查TLB中是否存在该V的缓存条目。

若命中,直接获取物理页框号(PFN)和权限信息。

合成物理地址:PA = (PFN << 12) |页内偏移。

访问物理内存。

若TLB未命中,需遍历四级页表,并将结果缓存到TLB。

2)TLB缓存机制

缓存内容:VA到PFN的映射、权限位、ASID(进程标识)。

组织方式:全关联、组关联或直接映射(由硬件实现)。

替换策略:LRU、随机替换等。

多核同步:通过TLB刷新指令(如invlpg)维护一致性。

7.4.2    四级页表遍历流程

(1) 获取顶级页表基址

CR3寄存器:存储PML4表(Page Map Level 4)的物理地址。

操作系统在进程切换时更新 CR3,确保页表独立性。

(2) 逐级查询页表

PML4 表(第4级)→页目录指针表(PDPT,第3级)→页目录表(PD,第2级)→页表(PT,第1级)

(3) 合成物理地址

物理地址:PA = (PFN << 12) | 页内偏移。

(4) 更新 TLB

将VA到PFN的映射插入TLB,加速后续访问。

7.4.3    VA到PA的转换

1)划分字段

2)页表遍历

3)物理地址转换

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

7.5.1    三层Cache的层级结构

1)L1 Cache:集成在CPU核心内部,最靠近计算单元。容量32–64 KB(通常分为指令缓存和数据缓存,各32 KB)。延迟:1–3个CPU时钟周期,访问速度最快。8–16路组相联,平衡命中率与硬件复杂度。每个CPU核心独享,多核间不共享。

2)L2 Cache:位于CPU核心内部或紧邻核心的外部。容量256 KB–2 MB,比 L1 更大但速度稍慢。延迟:5–10 个 CPU 时钟周期。8–16 路组相联,优化局部性访问。通常为每个CPU核心独享(部分设计支持多核共享)。

3)L3 Cache:位于CPU芯片内部,作为最后一级缓存(LLC)。容量8–64 MB,显著大于L1/L2,覆盖更大内存范围。延迟:20–40 个CPU时钟周期,速度慢于 L1/L2。16–32路组相联,减少冲突未命中。所有CPU核心共享,用于多核间数据协同。

7.5.2    物理内存访问流程

1) L1 Cache查询

地址拆分:

物理地址拆分为Tag(标签)、Index(索引)、Offset(偏移)。

查找Cache Line:根据Index定位Cache组(Set),比较组内所有Cache Line 的Tag。

命中处理:若命中且状态有效(如MESI协议的Modified/Exclusive/Shared),直接返回数据。

未命中处理:触发L1 Miss,向L2 Cache发起请求。

2) L2 Cache查询

接收L1 Miss请求:

L2使用相同机制(Tag/Index/Offset)查找Cache Line。

命中处理:返回数据到L1 Cache,更新L1的Cache Line。

未命中处理:触发L2 Miss,向L3 Cache发起请求。

3) L3 Cache 查询

接收L2 Miss请求:

L3作为最后一级缓存(LLC),采用更大的容量和更高的关联性。

命中处理:返回数据到L2/L1,更新各级Cache。

未命中处理:触发L3 Miss,向主存(DRAM)发起请求。

4)主存访问

内存控制器处理:根据物理地址访问DRAM,读取数据块。

填充Cache:将数据逐级填充到L3→L2→L1 Cache,并更新Tag状态位。

7.6     hello进程fork时的内存映射

当调用fork()创建子进程时,操作系统采用写时复制(Copy-On-Write, COW)技术优化内存管理。以下是内存映射流程:

1)虚拟地址空间的复制

初始状态时,父进程和子进程的虚拟地址空间完全相同,包括代码段、数据段、堆、栈和内存映射区域。

页表复制后子进程继承父进程的页表结构,所有页表项(PTE)指向相同的物理页框(PFN)。

2)写时复制(COW)机制

物理页共享:父进程和子进程的页表项初始指向相同的物理页,但这些页被标记为只读。

触发复制:当任一进程尝试写入共享页时,触发页错误(Page Fault)。

操作系统处理:

分配新的物理页,复制原页内容到新页。

更新触发写入进程的页表项,指向新物理页,并恢复可写权限。

另一进程的页表项仍指向原物理页(保持只读)。

3)内存区域的具体处理

内存区域fork()后,代码段(Text)始终共享物理页(不可写,无需COW);数据段(Data)初始共享物理页(标记为COW),修改时触发复制;堆(Heap)动态分配的内存区域遵循COW规则,修改时复制;栈(Stack)      每个进程的栈独立,但初始内容共享,修改时触发COW;内存映射文件根据映射标志(MAP_SHARED/MAP_PRIVATE)决定是否共享或 COW。

4)页表项的状态变化

父进程与子进程的页表项初始状态:权限位:原可写(Write)页被标记为只读(Read-Only)。物理页框:指向相同的PFN。

写入时的状态更新:触发写入的进程获得新物理页,页表项更新为可写。另一进程的页表项保持原PFN和只读权限。

7.7     hello进程execve时的内存映射

当进程调用execve加载新程序(如hello)时,操作系统会完全替换当前进程的内存映像,重新构建虚拟地址空间的映射。以下是execve执行过程中内存映射的流程。

1)释放原进程的内存映射

清除当前进程的所有内存区域(代码段、数据段、堆、栈、共享库等)。

保留部分资源(如文件描述符默认保留,除非标记 O_CLOEXEC)。

但也有例外:共享内存段(如shmget创建的共享内存)可能保留,需显式释放。

2)映射私有区域

execve根据ELF文件头信息,将程序的各个段映射到虚拟地址空间。

设置堆和栈:堆初始化为匿名映射(MAP_ANONYMOUS),可动态扩展(通过brk或mmap)。栈固定大小的匿名映射。

3)加载动态链接器(如ld.so)

动态链接的必要性:若hello依赖共享库(如libc.so),需先加载动态链接器。

映射过程:动态链接器(ld-linux-x86-64.so)的代码段和数据段被映射到虚拟地址空间。

4)映射共享库(如libc.so)

动态链接器解析hello的依赖库,并映射到进程地址空间:

代码段从共享库文件读取,多进程共享物理页。

数据段使用写时复制(COW)实现进程隔离。

5)设置参数与环境变量

参数传递:命令行参数(argv)和环境变量(envp)被复制到新进程的栈顶。

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

缺页故障是CPU在访问虚拟地址时,因该地址对应的物理页未加载到内存或访问权限不足而触发的异常。这是虚拟内存管理中的核心机制,允许操作系统按需加载数据,优化内存使用。

7.8.1    缺页故障触发条件及解决方式

1)主要缺页:

触发条件:页未加载到内存(页表项标记为“不存在”),需从磁盘(如交换区或文件)加载数据。

解决方式:分配物理页,从磁盘读取数据。

2)次要缺页:

触发条件:页已在内存但未映射到进程页表(如共享库被其他进程加载)。

解决方式:直接映射到进程页表,无需磁盘I/O。

3)写保护缺页:

触发条件:尝试写入只读页(如代码段或COW页)。

解决方式:COW机制:复制新页并更新映射。

4)非法访问缺页:

触发条件:访问保留地址或未分配区域(如空指针)。  

解决方式:终止进程(触发 SIGSEGV)

7.8.2    缺页中断解决方式

1)保存上下文:

保存当前进程的寄存器状态和中断现场。

2)分析缺页原因:

通过 CR2 寄存器获取触发缺页的虚拟地址。

检查页表项的状态(Present 位、权限位)。

3)合法性检查:

合法访问:页未加载(主要缺页)或权限不足(写保护)。

非法访问:地址超出进程空间范围(如空指针解引用),终止进程。

4)处理合法缺页:

主要缺页:从磁盘(交换区或文件)读取目标页到内存。若内存不足,触发页面置换算法(如LRU)选择牺牲页。

更新页表项,标记为Present并设置权限。

次要缺页:直接映射到共享物理页(如动态库已由其他进程加载)。

写保护缺页(COW):分配新物理页,复制原页内容。更新当前进程页表项,标记为可写。

5)恢复执行:

恢复进程上下文,重新执行触发缺页的指令。

7.9     动态存储分配管理

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

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

7.10 本章小结

本章系统阐述了内存地址空间的分段与分页机制,详细分析了逻辑地址到物理地址的转换流程,包括TLB缓存、四级页表遍历及三级Cache的协同工作。结合fork的写时复制(COW)与execve的内存映射机制,揭示了操作系统如何高效管理进程的虚拟内存空间。

(第7 2分)

第8章         hello的IO管理

8.1     Linux的IO设备管理方法

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

设备的模型化:文件

设备管理:unix io接口

8.2     简述Unix IO接口及其函数

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

8.3     printf的实现分析

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

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4     getchar的实现分析

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

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5     本章小结

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

(第8 选做 0分)

结论

在大作业完成过程中,可以看到程序生命周期的系统性实现:Hello程序经历了预处理、编译、汇编、链接的完整编译流程,最终通过操作系统的进程管理与存储管理机制动态执行。从代码文本(Program)到进程实体(Process)的转换(P2P),体现了编译器、链接器与操作系统的无缝协作;而进程从加载到终止的“零到零”(020)过程,则展现了计算机资源的高效分配与回收机制。

在整个过程中,我感受到了系统组件的深度协同:通过分析ELF文件格式、虚拟地址空间映射、动态链接(PLT/GOT)及写时复制(COW)等机制,揭示了硬件(MMU/TLB/Cache)与操作系统(进程调度、缺页处理)的紧密配合。例如,四级页表与TLB的协同优化了地址转换效率,而三级Cache层级结构显著降低了内存访问延迟。

总的说来,Hello程序虽小,却完整映射了计算机系统的核心设计哲学。每一行代码的执行背后,是编译工具链、操作系统与硬件架构的精密协作。看似微小的程序,背后是紧密相连、环环相扣的精密处理过程,通过此次实验,我对程序的生命周期与实现过程有了更加深刻的感受。

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

附件

hello.c:原始hello程序的C语言代码

hello.i:预处理过后的hello代码

hello.s:由预处理代码生成的汇编代码

hello.o:二进制目标代码

hello:进行链接后的可执行程序

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

参考文献

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

[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.

[7]  Randal E. Bryant,David R. O’Hallaron. 深入理解计算机系统(原书第3版)[M]. 龚奕利,贺莲. 北京:机械工业出版社,2016:113-201.

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

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

原文链接:https://blog.csdn.net/2503_91018821/article/details/148004749

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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