计算机系统
大作业
题 目 ++++程序人生-Hello++++ ++++'++++ ++++s P2P++++
专 业 ++++计算机与电子通信++++
学 号 ++++2023111735++++
班 级 ++++23L0509++++
学 生 ++++杨祥锐++++
指 导 教 师 ++++史先俊++++
计算机科学与技术学院
202 5 年 5 月
摘 要
本文从hello.c开始,使用计算机系统课上及教材上的知识详细记录了其在预处理、编译、汇编、链接过程时的一点点转变(P2P),并介绍了hello的进程管理和存储管理。讲述了一个简单的hello程序从被创建到被回收的历程,它在Linux操作系统的带领下完成了平凡但不平庸的生命历程。
关键词: 计算机系统;P2P;Hello;进程;存储
(摘要0分,缺失-1分, 根据内容精彩称都酌情加分0-1分 )
目 录
[++++第1章 概述++++](#第1章 概述)
[++++1.1 Hello简介++++](#1.1 Hello简介)
[++++1.2 环境与工具++++](#1.2 环境与工具)
[++++1.3 中间结果++++](#1.3 中间结果)
[++++1.4 本章小结++++](#1.4 本章小结)
[++++第2章 预处理++++](#第2章 预处理)
[++++2.1 预处理的概念与作用++++](#2.1 预处理的概念与作用)
[++++2.3 Hello的预处理结果解析++++](#2.3 Hello的预处理结果解析)
[++++2.4 本章小结++++](#2.4 本章小结)
[++++第3章 编译++++](#第3章 编译)
[++++3.1 编译的概念与作用++++](#3.1 编译的概念与作用)
[++++3.2 在Ubuntu下编译的命令++++](#3.2 在Ubuntu下编译的命令)
[++++3.3 Hello的编译结果解析++++](#3.3 Hello的编译结果解析)
[++++3.4 本章小结++++](#3.4 本章小结)
[++++第4章 汇编++++](#第4章 汇编)
[++++4.1 汇编的概念与作用++++](#4.1 汇编的概念与作用)
[++++4.2 在Ubuntu下汇编的命令++++](#4.2 在Ubuntu下汇编的命令)
[++++4.3 可重定位目标elf格式++++](#4.3 可重定位目标elf格式)
[++++4.4 Hello.o的结果解析++++](#4.4 Hello.o的结果解析)
[++++4.5 本章小结++++](#4.5 本章小结)
[++++第5章 链接++++](#第5章 链接)
[++++5.1 链接的概念与作用++++](#5.1 链接的概念与作用)
[++++5.2 在Ubuntu下链接的命令++++](#5.2 在Ubuntu下链接的命令)
[++++5.3 可执行目标文件hello的格式++++](#5.3 可执行目标文件hello的格式)
[++++5.4 hello的虚拟地址空间++++](#5.4 hello的虚拟地址空间)
[++++5.5 链接的重定位过程分析++++](#5.5 链接的重定位过程分析)
[++++5.6 hello的执行流程++++](#5.6 hello的执行流程)
[++++5.7 Hello的动态链接分析++++](#5.7 Hello的动态链接分析)
[++++5.8 本章小结++++](#5.8 本章小结)
[++++第6章 hello进程管理++++](#第6章 hello进程管理)
[++++6.1 进程的概念与作用++++](#6.1 进程的概念与作用)
[++++6.2 简述壳Shell-bash的作用与处理流程++++](#6.2 简述壳Shell-bash的作用与处理流程)
[++++6.3 Hello的fork进程创建过程++++](#6.3 Hello的fork进程创建过程)
[++++6.4 Hello的execve过程++++](#6.4 Hello的execve过程)
[++++6.5 Hello的进程执行++++](#6.5 Hello的进程执行)
[++++6.6 hello的异常与信号处理++++](#6.6 hello的异常与信号处理)
[++++第7章 hello的存储管理++++](#第7章 hello的存储管理)
[++++7.1 hello的存储器地址空间++++](#7.1 hello的存储器地址空间)
[++++7.2 Intel逻辑地址到线性地址的变换-段式管理++++](#7.2 Intel逻辑地址到线性地址的变换-段式管理)
[++++7.3 Hello的线性地址到物理地址的变换-页式管理++++](#7.3 Hello的线性地址到物理地址的变换-页式管理)
[++++7.4 TLB与四级页表支持下的VA到PA的变换++++](#7.4 TLB与四级页表支持下的VA到PA的变换)
[++++7.5 三级Cache支持下的物理内存访问++++](#7.5 三级Cache支持下的物理内存访问)
[++++7.6 hello进程fork时的内存映射++++](#7.6 hello进程fork时的内存映射)
[++++7.7 hello进程execve时的内存映射++++](#7.7 hello进程execve时的内存映射)
[++++7.8 缺页故障与缺页中断处理++++](#7.8 缺页故障与缺页中断处理)
[++++第8章 hello的IO管理++++](#第8章 hello的IO管理)
[++++8.1 Linux的IO设备管理方法++++](#8.1 Linux的IO设备管理方法)
[++++8.2 简述Unix IO接口及其函数++++](#8.2 简述Unix IO接口及其函数)
[++++8.3 printf的实现分析++++](#8.3 printf的实现分析)
[++++8.4 getchar的实现分析++++](#8.4 getchar的实现分析)
第1章 概述
1.1 Hello简介
1.1.1 P2P
P2P,即From Program to Process,指的是从程序到进程的过程。源程序hello.c经过预处理器得到文本文件hello.i;接着编译器(ccl)会将hello.i翻译成文本文本hello.s,它包含了一个汇编语言程序;汇编器(as)将hello.s翻译成机器语言指令,并打包成可冲定位目标程序存在目标文件hello.o中;;链接器(ld)将hello.o和运行时库合并,生成可执行文件hello。在shell中输入./hello后,操作系统通过fork函数为hello创建一个进程,再通过execve执行hello。

图1.从源文件到目标文件的转化
1.1.2 020
020,即zero to zero,是一种"从无到有再到无"的闭环。第一个0是指开始时磁盘上的可执行文件尚未被加载,CPU、内存、I/O设备等处于空闲状态;在0和0之间,进程被创建后会通过 execve() 加载到虚拟内存后,CPU 开始执行其指令直至 调用exit() 或信号终止;第二个0是指进程退出时,父进程会回收该子进程,CPU、内存、I/O设备等再次处于空闲状态。
1.2 环境与工具
硬件环境:
CPU:13th Gen Intel(R) Core(TM) i9-13900HX,RAM:16GB
软件环境:
Windows11 64位
Linux version 6.8.0-59-generic
开发与调试工具:
Visual Studio 2022、Ubuntu 12.3.0-1ubuntu1~22.04、gdb、edb、gcc、vim
1.3 中间结果
|-----------------|-------------------------|
| hello.c | hello 的C源文件 |
| hello.i | hello.c经过预处理后的预编译文本文件 |
| hello.s | hello.i经过编译得到的汇编语言文本文件 |
| hello.o | hello.s经过汇编得到的机器语言二进制文件 |
| hello_elf.txt | hello.o的ELF格式文本文件 |
| hello_dis.txt | hello.o反汇编的文本文件 |
| hello_elf_1.txt | hello的ELF格式文本文件 |
| hello_dis1.txt | hello的反汇编文本文件 |
| hello | hello.o经过链接得到的可执行目标文件 |
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章首先以Hello程序为例,介绍了P2P、O2O的概念,随后提供了大作业中使用的环境及工具,最后以表格形式介绍了为编写本论文生成的所有中间文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理是指预处理器(ccl)根据以字符#开头的命令,修改原始的C程序(主要完成宏展开、头文件包含和条件编译等操作),得到修改后的源程序并提供给编译器。
预处理的作用:在预处理阶段预处理的内容包括:扫描以#开头的指令,将宏定义替换为对应文本;展开 #include 指定的头文件内容并"拼接"到源文件中;根据 #if/#ifdef 等条件选择性地保留或剔除代码;移除注释和执行行控制。完成这些可以令后续程序的修改、调试、移植等更加便利。
2.2在Ubuntu下预处理的命令
在Ubantu中可以通过gcc -E hello.c -o hello.i命令来进行预处理,如图所示:
图2-2-1 预处理命令
图2-2-2预处理结果
2.3 Hello的预处理结果解析
经过预处理后,得到的hello.i文件仍是文本文件,但已经从原本的24行变成了3092行,在hello.i文件的3079到3092行我们可以看见c语言源代码(如图2-3-1所示),它们并没有被修改只是删去了注释的部分。

图2-3-1 hello.i中的源代码部分
文件中的其他部分主要是把stdio.h,unistd.h,stdlib.h这三个我们引用的头文件进行展开。首先cpp会到Linux系统的环境变量下寻找头文件,我们根据hello.i提供的地址找到了引用的三个头文件(如图2-3-2、图2-3-3所示)。
图2-3-2 hello.i中给出的头文件地址
图2-3-3 hello程序引用的头文件
仔细阅读头文件展开的部分,我们可以发现头文件中的函数以及对指针、类型定义的声明等(如图2-3-4、图2-3-5所示)。
图2-3-4 函数声明示例
图2-3-5 类型定义信息
2.4 本章小结
本章主要围绕预处理展开,使我预处理的概念、作用、命令、解析进行了深入的理解。预处理是进行编译器编译前的一步,通过对hello.i的分析可以知道在这个过程中主要包括宏展开、文件包含、条件编译、注释移除,以便后续的编译等流程。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译是指编译器将预处理后的源程序(hello.i)翻译成汇编语言程序(hello.s)的过程。
编译的作用:编译的主要步骤包括:编译器对预处理输出的纯 C 源文本进行语法和语义检查,即确认所有的指令是否都符合语法规则;应用各类优化,即通过调整使代码保持原功能但执行效率更高,常见优化有常量折叠、死代码消除、寄存器分配等;生成与目标处理器架构紧密对应的汇编指令。通过编译,可以让源代码更加接近机器语言。
3.2 在Ubuntu下编译的命令
Ubantu下使用gcc -S hello.i -o hello.s命令进行编译。
图3-2 编译命令
3.3 Hello的编译结果解析
3.3.1数据
3.3.1.1常量
(1)字符串常量
所有的字符串字面量都被收进只读数据段(.rodata)并贴上标签(.LC0、.LC1)。运行时用指令如 leaq .LC1(%rip), %rax 即可拿到它们的地址。
图3-3-1 汇编代码中的字符串常量表示
图3-3-2 源代码中的字符串常量
可见在汇编代码中汉字以utf-8的格式进行编码,每个汉字占3个字节。
(2)整形常量
源代码的整形常量是数字,在汇编代码中可直接体现。
图3-3-3整型常量
3.3.1.2 变量
(1)函数形参
图3-3-4 汇编代码中形参
函数形参 int argc、char *argv[] 放在寄存器%edi、%rsi中,函数调用开始时储存在栈中,当函数返回所占空间被释放。
(2)局部变量
局部变量i被分配在栈上
图3-3-5 局部变量
3.3.2 赋值
i=0,局部变量初始化赋值
图3-3-6 赋值
3.3.3 算术操作
局部变量i在循环时的自增,每次循环都通过addl指令实现+1。
图3-3-7 算术操作
3.3.4关系操作
源代码中有两处关系操作
第一处:cmpl指令代表比较,根据下面可得知如果相等则跳转,不相等则继续执行下一行。
图3-3-8 argc与5的比较
第二处:局部变量i与9相比,i<=9则跳转到L2(循环),i>9则跳出循环

图3-3-9 i与9的比较
3.3.5 数组/指针/结构操作
源代码中char *argv[]是字符串指针数组,对于指针char*大小为8字节,因此每个参数字符串地址相差8个字节,以-32(%rbp)为基址,采用基址-变址寻址法访问指针数组。
图3-3-10 字符串指针数组
3.3.6控制转移
编译器使用jump指令进行跳转,实际该指令有多种形式,本文件中有je、jmp、jle三种,它们会根据前面的表达式跳转到相应的地址。
图3-3-11 控制转移
3.3.7 函数操作
(1)main函数:
参数传递:源代码中main函数有参数传递:int argc,char *argv[],在汇编代码中体现为 movl %edi, -20(%rbp) ,movq %rsi, -32(%rbp),可知第一个参数和第二个参数分别通过寄存器EDI和寄存器RSI传递,两个参数被写入栈。
函数调用:由系统函数调用
函数返回:通过EAX寄存器返回
图3-3-12 main函数的函数返回
(2)printf函数
参数传递:需要输出的字符串及格式
函数调用:通过call+函数地址调用
参数返回:返回打印的字符数
图3-3-13 第一次printf
通过汇编代码得知,编译器在第一次实际上调用的是puts函数,这是因为此次调用printf函数不需要传递额外的参数,因此编译器做了一点等价的替换。
图3-3-14第二次printf
第二次调用则是调用了四个参数,所以不能用puts函数直接代替,查看汇编代码可得知rcx、rdx、rsi三个寄存器传递了字符串的首地址,而rax传递了标签LC1对应的字符串的首地址。
(3)exit函数
参数传递:退出码,即存在寄存器edi的数字"1"
函数调用:call exit@PLT

图3-3-15 exit函数
(4)sleep函数
参数传递:将atoi函数的返回值存入寄存器edi中作为sleep函数的参数,代表睡眠秒数
函数调用:sleep @PLT
函数返回:返回值被忽略(返回的是实际休眠时间)

图3-3-16 sleep函数
(5)atoi函数
参数传递:字符串地址,即存入寄存器rdi的argv[4]
函数调用:call atoi@PLT
函数返回:转换的结果,存入寄存器eax中

图3-3-17 atoi函数
(6)getchar函数
函数调用:call getchar@PLT

图3-3-18 getchar函数
此函数不需要参数,因此不需要寄存器来传参
3.4 本章小结
通过本章的实践与思考,我对于编译的概念与作用有了认知,在一次次查看分析hello.s该汇编文件的过程中,理解了C语言的各种数据与操作是如何在汇编代码中体现的,尤其感受了函数传参、调用以及在程序中的跳转语句带来的思维碰撞,使我对编译的理解更加深刻。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编是指汇编器将汇编语言翻译成机器语言的过程。在这个过程中会将机器语言指令打包成可重定位目标程序的格式并保存在目标文件hello.o中,hello.o文件是一个二进制文件。
汇编的作用:将汇编代码翻译为计算机可以直接执行的二进制指令,组织程序的代码段、数据段等,为链接阶段提供标准的目标文件格式。
4.2 在Ubuntu下汇编的命令
使用gcc -c hello.s -o hello.o命令进行汇编

图4-1汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.3.1 readelf
通过readelf -a hello.o > hello_elf.txt命令将hello.o的ELF格式转化为txt文档便于分析

图4-2 readelf
4.3.2 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小(64字节),目标文件的类型(可重定位目标文件),机器类型(AMD X86-64),节头部表的文件偏移,以及节区头数量(14个)和节区头大小(64字节)。

图4-3 ELF头
4.3.3节头
该部分描述了各节的基本信息,包括节的名称、类型、大小等

图4-4 节头
4.3.4 重定位节
在编译生成的目标文件阶段,代码和数据的地址通常是相对的,因此需要在链接时或运行时,将这些相对地址转化为实际的物理或虚拟地址,以便程序能够正确运行,这就是重定位。重定位节用于存放重定位表,记录目标代码中哪些位置存在相对地址,需要在链接或运行时修正。

图4-5 重定位节
4.3.5符号表
可以只包含一个自定义函数 main,其余均为外部符号(UND),需链接 libc 时解析,如exit、sleep函数等。

图4-6符号表
4.4 Hello.o的结果解析

图4-7hello.o的反汇编文件
在目标文件中,机器指令由一系列字节序列构成:前缀、操作码字节、ModR/M 字节(要操作的寄存器/内存位置)、位移(disp8/disp32)或立即数(imm8/imm32)字段(用于具体的常量或偏移量)。CPU 可以直接识别这种机器指令并执行相应的操作。
hello.s与hello.o的反汇编代码之间有着一一对应的关系,也就是说汇编语言中的每一条伪指令在机器码里都有对应的字节编码。比如 mov %rsp,%rbp 对应的就是前缀 48、操作码 89 和 ModR/M E5;而 subq $0x20,%rsp 则是前缀 48、操作码 83、ModR/M EC 和立即数 20。
当然比较分析之下二者也有一些差异:
操作数:hello.s中的操作数均为十进制,而反汇编文件机器码中的操作数被转换成十六进制
分支跳转:分支跳转在 hello.s 中写作 je .L2、jmp .L3、jle .L4 等,以标签 .L2、.L3、.L4 为目标,完全是符号化、可读的。汇编器会根据标签相对当前位置自动计算偏移,帮你选择短跳还是长跳。而在反汇编文件中看到的是真实的机器码,没有标签,只有操作码字节和紧跟的相对偏移字节。
函数调用:函数调用方面,在 hello.s 中使用"call 函数名@PLT"的方式,清晰标明"调用外部函数,通过 PLT"来进行函数调用。并且省略了立即数参数和PLT 地址,知识由汇编器留一个占位并在重定位节里记录要做一个 PLT32 重定位的位置。在反汇编文件中的机器码都先表现为零位移的占位形式,每条 E8(相当于CALL)指令后面的 4 字节立即数都为 00 00 00 00,这是汇编时留的"待填充值"。真正要调用哪个函数、跳到 PLT 表哪里,在 .rela.text 里有重定位条目,链接时才把这些零填成指向 PLT 条目的实际偏移。
4.5 本章小结
在本章中介绍了汇编的概念与作用,对可重定位目标文件hello.o的ELF格式进行了分析,将hello.o的反汇编代码与hello.s之间比较分析,揭示了二者之间的联系与差异。在汇编阶段会把汇编语言翻译成机器语言,机器语言是计算机可以直接执行的二进制指令,也是链接阶段需要的目标文件形式。在hello.o的ELF格式中包括ELF头、节头、重定位节、符号表,可以从中获取各节的基本信息、重定位表等。通过本章学习,我对汇编、重定位、机器码有了更深刻的理解。
(第4章1分)
第 5 章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为了一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接包括静态链接和加载时共享库的动态链接。
连接的作用:在链接过程中链接器会解析目标文件中的符号表,将所有未定义的外部符号进行匹配,并依照重定位表修正用于函数调用而引用的地址占位,使机器指令中的相对偏移和绝对地址指向正确的目标地址。同时,链接器还负责按照目标平台的内存布局规范,合并并重新定位test、data等各节区,为可执行文件生成程序头表,以便操作系统能正确地将程序映射到内存中。
5.2 在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 /usr/lib/x86_64-linux-gnu/crtn.o /usr/lib/x86_64-linux-gnu/libc.so hello.o

图5-1 链接命令
5.3 可执行文件hello的格式
使用命令:readelf -a hello > hello_elf_1.txt
将hello的ELF格式转化为txt文档便于分析

图5-2 hello的ELF
5.3.1节头
节头中体现了各段的起始地址,大小等信息,例如data段的大小是0x04,地址是0x404048。

图5-3 节头
5.3.2 程序头

图5-4 程序头
程序头定义了加载器如何将文件映射到内存段,包括哪些部分可执行、哪些部分可写、哪些部分是只读。可执行文件被分为十二个段,我们可以通过程序头获得相关信息如偏移、虚拟地址等。以PHDR段为例,有读权限,开始于内存地址0x000040处,总共的内存大小为0x2a0字节。
5.4 hello的虚拟地址空间
使用gdb加载hello。

图5-5 gdb
输入info files指令界面如图:

图5-5 gdb加载界面
如图,可以看出不同节的首尾地址,我们以.data为例,始尾地址为0x404048和0x40404c,二者之差为0x04,与hello的ELF格式中一致。

图5-6 对比举例
输入i proc map 指令

图5-7 i proc map
如图可以看到各段信息,通过于程序头的信息对比也有不同,比如第一个LOAD段文件偏移 0x0 大小 0x5f0,但仅映射 0x1000 对齐页,这是因为当动态链接器用 mmap 把第一个 LOAD 段映射到进程地址空间时,会遵循页对齐规则,而不是按节区或文件中的精确大小映射, 段在文件中的实际大小是 0x5f0(1520 B),但文件映射必须覆盖完整的页。因此,内核会把从偏移 0x0 开始、长度至少到下一个页面边界(0x1000)的整个区域映射进来 。
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello > hello_dis1.txt

图5-8 反汇编命令

图5-9 反汇编结果
链接的过程:链接过程就是把目标文件( hello.o)与运行时的支持库(CRT、libc 等)以及重定位信息合并,按段对齐打包,同时解析和修正所有对外部符号和常量的地址占位,生成一个包含入口点、运行时启动/终结逻辑、PLT/GOT 支持、动态链接表、程序头表的完整可执行文件。这个过程在反汇编里表现为------占位变为真实偏移;PLT/GOT 代码段与寄存器间接跳转逻辑出现;以及新增 _start、.init、.fini 等多段启动和析构代码等。
(1)占位变为真实偏移

图5-10 表现1
同一条指令已经被重定位为真实的相对偏移,这也是进行重定位的过程的一部分。
(2)代码段与寄存器间接跳转逻辑,这也是进行重定位的过程的一部分。

图5-11 表现2
链接后可执行文件新增了完整的 PLT 段和 GOT 间接跳转,例如在 .plt 段,这些间接跳转在 hello.o 中并不存在。
(3)新增启动和析构代码

图5-12 表现3
5.6 hello的执行流程
使用edb,查看symbol

图5-13 edb结果
|-----------------------|----------|
| 子程序名 | 地址(16进制) |
| hello! init | 0x401000 |
| hello!puts@plt | 0x401030 |
| hello! printf chk@plt | 0x401050 |
| hello!exit@plt | 0x401060 |
| hello!sleep@plt | 0x401070 |
| hello!getc@plt | 0x4010f0 |
| hello! start | 0x4010a0 |
| hello! fini | 0x401260 |
5.7 Hello的动态链接分析
动态链接通过全局偏移量表(GOT)和过程链接表(PLT)的协同机制实现对外部库函数的地址解析。对于程序内部的变量,可通过代码段与数据段的相对位置固定原则直接计算地址;而对外部库函数的调用,则需依赖 PLT 和 GOT 的协作机制。PLT 中初始存放的是一组跳转代码,其目标指向 GOT 中对应的条目。首次调用库函数时,GOT 条目默认指向 PLT 中的第二条指令,此时程序会将函数标识符和重定位表地址压入栈中,并跳转至 PLT[0] 执行公共桩代码,进而调用动态链接器。链接器通过栈中的信息解析函数实际地址,回填至 GOT 相应条目,再将控制权转移给目标函数。此后再次调用同一函数时,PLT 的跳转指令会直接通过 GOT 中已更新的地址实现高效间接跳转,无需重复解析。
在ELF文件中找到got.plt的位置:

结合edb查看动态链接前:

动态链接之后:

可以看出调用了dl_init之后字节改变了。
动态链接通过 PLT(过程链接表)和 GOT(全局偏移量表)协同实现延迟绑定:程序初次调用共享库函数时,从 PLT 跳到 GOT 条目(初值指向 PLT 本身),再经 PLT[0] 进入动态链接器。链接器解析出函数实际地址后写入 GOT,随后跳回执行目标函数。此后再调用时,PLT 直接从 GOT 读取真实地址跳转,无需再次进入链接器。全局变量则直接使用数据段内的固定偏移。这样既保证首调用时正确解析共享库,又使后续调用仅需一次间接跳转。
5.8 本章小结
本章首先阐述了链接的基本概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察了hello文件的虚拟地址空间使用情况,最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。
(第5章1分)
第 6 章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
进程的作用:进程是资源管理的最小单位,具有独立的虚拟地址空间、代码、数据、堆、栈、文件描述符等。通过进程,操作系统可以实现对多个程序的并发运行,同时保障进程之间的隔离与安全。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Shell是操作系统提供的一种命令行解释器,是用户与操作系统内核交互的桥梁。用户通过 Shell 输入命令,Shell 负责解析命令,查找并启动程序,将用户的需求转化为操作系统可以执行的系统调用。Shell 同时提供了脚本编程能力、环境变量管理、管道、重定向等功能,支持用户自动化管理系统,进行批量任务处理,控制进程的执行和管理。
处理流程:
(1)读取命令
Shell 等待并读取用户从终端输入的命令行。
(2)命令解析与预处理
内容包括:使用空格、Tab、换行等分隔符将输入拆分为 tokens;处 理别名替换、~ 用户目录替换、环境变量($VAR)替换;处理命令替 换与算术表达式;再次分割命令,进行通配符展开;删除注释,完成 最终命令与参数的确定。
(3)命令查找与分类
优先检查是否为 Shell 内置命令,如果是内置命令,直接执行。如果 是外部程序,Shell 依据环境变量查找对应的可执行文件路径。
(4)创建进程并执行
首先Shell 调用 fork() 创建一个新的子进程,然后子进程调用execve() 执行目标程序(如 hello),成功后进程映像被替换。父进程会判断命 令是前台还是后台执行,若是前台Shell 使用 wait() 等待子进程结束, 若是后台Shell 不阻塞,继续等待下一条用户输入,同时接收SIGCHLD 处理子进程退出。
(5)输入输出重定向与信号处理
在执行前,Shell会设置好输入输出重定向,并且保持响应用户键盘信 号(如 Ctrl+C, Ctrl+Z),进行相应处理,如终止、挂起子进程。
(6)循环等待下一条命令
当当前命令执行结束,Shell 会清理资源并返回提示符,准备接收下 一条输入。
6.3 Hello的fork进程创建过程
当用户在 Shell 输入命令 ./hello 后,Shell 会识别该命令不是内置指令,因此会调用 fork()创建一个新的子进程。在该过程中,操作系统会为子进程分配一个新的进程号(PID),并复制父进程的用户级虚拟地址空间,包括代码段、数据段等。父子进程还会共享打开的文件描述符,因此子进程可以继续读写父进程打开的文件。
尽管虚拟地址空间相同,但父子进程的物理页面是相互独立的。fork() 在父进程中返回子进程的 PID,而在子进程中返回 0,因此父子进程可以基于返回值区分各自执行的逻辑分支。完成 fork() 后,子进程会进一步调用 execve() 替换自身映像为 hello 程序,而父进程会判断命令的前后台类型,若为前台则调用 wait() 等待子进程结束。
6.4 Hello的execve过程
在由 Shell 创建的子进程中,子进程会调用 execve 函数加载并运行 hello 可执行文件,同时传递参数列表 argv 和环境变量列表 envp。execve 不会创建新的进程,而是用 hello 程序覆盖当前子进程的地址空间,保持 PID 不变,并继承所有已打开的文件描述符。
execve 的执行流程包括:
-
删除当前进程已有的用户空间,包括代码段、数据段、堆、栈等;
-
映射 hello 程序的私有区域,如代码段、数据段、.bss、堆和新的栈区域,这些区域采用写时复制;
-
映射共享区域,如共享库(例如 libc.so);
-
初始化用户栈,将参数 argc、argv、envp 等传递到新程序的 main 函数;
-
最后设置程序计数器(PC)指向新程序的入口 _start,从而转交控制权执行 hello 程序;execve 只有在加载失败时才会返回到调用的 Shell 子进程,而正常情况下 execve 加载成功后,控制权不再返回,而是直接进入新程序的执行流程。
6.5 Hello的进程执行
在 Linux 系统中,Hello的进程执行离不开进程调度、上下文切换、用户态与内核态的切换等机制的协调配合。
6.5.1逻辑控制流:
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC 值的序列叫做逻辑控制流,或者简称逻辑流。

图6-1 逻辑控制流
6.5.2时间片与时间分片:
一个逻辑流的执行在时间上与另一个流重叠,称为并发流。多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
6.5.3上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等构成。上下文转换包括1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程。

图6-2 上下文切换
6.3.4调度:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决策称为调度,是由内核中的调度器代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
例如Hello 进程在执行过程中,由于调用了如 sleep、read 等系统调用,进程会因等待 IO 等事件而进入阻塞状态,此时内核调度器会执行调度决策,将 Hello 进程挂起,保存其完整的上下文(包括寄存器、PC、用户栈、内核栈、内核数据结构等),并从就绪队列中选择下一个进程恢复其上下文,交由 CPU 继续执行。
当等待事件完成或休眠结束,调度器再次将 Hello 进程唤醒,恢复其上下文继续执行。
6.3.5 用户态与核心态转换
为了保证系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户态与核心态的划分,处于核心态的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。Linux 通过处理器的模式位区分用户态与内核态,进程默认运行在用户态,只有在发生系统调用、中断或故障时,才会由硬件将当前进程切换到内核态,内核先在内核态执行相关服务或异常处理,之后再将进程切换回用户态继续执行 Hello 程序,这种机制确保用户进程无法直接访问内核的核心代码和数据,防止系统被破坏,提升了系统的稳定性与安全性。
6.6 hello的异常与信号处理
6.6.1异常
异常就是控制流中的突变,用来相应处理器状态中的某些变化。一共有四种异常:
(1)中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。中断处理流程如图:

图6-3 中断处理
(2)陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。陷阱处理如图:

图6-4 陷阱处理
(3)故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时, 处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort 例程,例程会终止引起故障的应用程序。
故障处理如图:

图6-5 故障处理
(4)终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理如图:

图6-6 终止处理
6.6.2信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件。
Linux信号的种类如图(30种):

图6-7 Linux信号
6.6.3异常与信号处理
(1)不停乱按,包括回车
在程序运行时我们输入的东西不会阻止程序的正常运行,只是显示在屏幕上,shell会正常输出本应输出的,但输出十次也就是运行完毕后shell会把我们随意输的乱码当做指令去执行。
结果如图所示:

图6-8 不停乱按结果
(2)Ctrl-Z
按下Ctrl+Z之后,进程会收到一个SIGSTP 信号,使得当前的hello进程被挂起。用ps指令查看其进程PID,可以发现hello的PID是6767;再用jobs查看此时hello的后台 job号是1,调用指令fg %1将其调回前台继续完成按Ctrl+Z时还没完成的任务。
结果如图:

图6-9 ctrl z结果

图6-10 ps、jobs、fg
通过kill可将挂起的进程终止,如图:

图6-11 kill
(3)Ctrl-C
按下Ctrl+Z之后,进程会收到一个SIGINT信号,使得当前的hello进程被终止,输入jobs也看不到进程。
结果如图:

图6-12 Ctrl-C
6.7本章小结
本章介绍了进程的概念与作用,并以hello进程为例阐述了shell对hello的处理全过程,包括创建、运行、加载、异常和信号的处理管理。在hello运行的过程中,内核对其进行进程管理,决定何时进行进程调度,在接收到不同的异常、信号时,还要及时地进行对应的处理。
(第6章 2 分)
第 7 章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是CPU 在程序执行时生成的"段:偏移"格式的地址,代表了程序在编译或链接时需要引用的代码和数据的地址。如在 hello.o 的反汇编里的 mov %edi, -0x14(%rbp) 等指令所用的偏移,都是基于逻辑地址计算。
线性地址:线性地址是段式管理后的地址,是逻辑地址经过段基址相加得到的结果。线性地址空间是连续非负整数地址的有序集合。
虚拟地址:开启分页后线性地址即为进程的虚拟地址空间中的地址,
物理地址:物理地址是MMU 将线性/虚拟地址映射到的实际 DRAM 地址,也就是内存单元的绝对地址,用于实际的内存读写操作。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 x86 架构中,每个内存引用首先以逻辑地址的形式出现在程序指令或数据访问中。逻辑地址由一个段选择符和一个段内偏移组成,段选择符指示该地址属于哪个段(如代码段、数据段、堆栈段等),段内偏移指定在该段内的具体位置 。 当处理器处于保护模式下,逻辑地址首先由分段单元处理,硬件从段寄存器(CS/DS/SS/ES/FS/GS)中的段选择符取出相应的段描述符,该描述符保存在全局描述符表(GDT)或本地描述符表(LDT)中,其内容包含段的基址(Base)、段限长(Limit)和访问权限等信息。处理器会验证偏移是否在段限长内且访问类型(读/写/执行)是否合法,若检查失败则抛出保护异常,否则将段基址与偏移相加产生线性地址,也称为"虚拟地址"。
在实模式下,段寄存器直接存放段值,CPU 读取段寄存器后左移 4 位与偏移相加,得到 20 位的线性地址,该地址同时也是物理地址;在现代 64 位模式中,为简化管理通常采用 Flat 段模型,将所有段基址设为 0,从而逻辑地址可以直接作为线性地址使用,无段界限检查 。

图7-1 段式管理示意图
7.3 Hello的线性地址到物理地址的变换-页式管理
操作系统将整个线性地址空间切分成大小相同的"虚拟页",而物理内存也被划分为相同大小的"物理页帧"。这样,内存管理就可以以固定大小的页为单位进行分配和回收,可有效利用内存,并为后续的映射与替换提供了基本单位。
每个进程维护一个多级页表,其中最底层的页表条目(PTE)包含一个"有效位"和一个"物理页号(PPN)"字段。当有效位为 1 时,PPN 指示该虚拟页当前映射到哪个物理页帧;若为 0,则表明该页要么未分配,要么被换出到磁盘。
CPU 在访问内存时,将 n 位的线性地址分为两部分:高(n − p)位作为"虚拟页号(VPN)",用于在页表中查找映射;低 p 位作为"页内偏移(VPO)",用于定位物理页帧内的具体字节。这样,每次地址转换都只需支付一次页表或快表查询的开销,而页内偏移可直接重用,效率更高。
为了加速 VPN→PPN 的转换,CPU 内部提供了翻译后备缓冲器(TLB),它缓存了最近使用的页表条目。每次访问先查询 TLB,若命中即可立刻获得物理页号,无需访问页表;若未命中,则进入多级页表遍历阶段,同时将查到的结果回填到 TLB,供下一次访问使用。
当 TLB 未命中时,MMU 会依次访问页目录、页上级目录直至最底层页表:它先使用 VPN 的最高位索引 PML4(或页目录),取得下一层页表的物理地址,再用中间位索引下一层,直到在最后一级页表中找到包含目标物理页号的页表条目。从最底层页表条目中取得 PPN 后,MMU 将物理页号左移 p 位并与原 VPO 按位或运算,生成最终的物理地址。同时,MMU 会将这一(VPN→PPN)对写入 TLB,以便下次遇到同一虚拟页时可以直接命中,减少时延。
如果在任何一级页表查找中发现对应条目的有效位为 0,MMU 会触发缺页异常。此时,操作系统会检查该虚拟页是否属于合法映射区域:若合法则将对应内容从磁盘读入内存或分配新页,更新页表并重试指令;若非法则向进程发送 SIGSEGV,从而实现安全保护。
对于 Hello 程序来说,其 .text、.data、在执行时都会映射到各自的虚拟页。每当 Hello 访问任一地址(如读取格式字符串或写入局部变量),CPU 便按上述流程将线性地址逐级转换为物理地址,并通过 L1/L2/L3 缓存最终访问 DRAM,从而确保 Hello 在独立、受保护的虚拟空间中正确高效地运行。

图7-2 页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
将虚拟地址(Virtual Address,VA)转换为物理地址(Physical Address,PA)依赖于两大关键机制:TLB和四级页表。
每当 CPU 要访问内存时,它首先生成一个 48 位或更少位宽的线性地址。为了尽量减少对内存的访问次数,MMU 在硬件中包含了一个小型缓存------TLB,用来保存最近使用的"虚拟页号→物理页号"映射。当 CPU 发送线性地址时,MMU 先在 TLB 中查找相应的页表项;若命中,便可在 1--2 个周期内立即获得物理页号,几乎无需额外延迟 。
如果 TLB 未命中,MMU 必须执行一个多级页表遍历。首先,MMU 将线性地址划分为五个字段:四个虚拟页号(VPN₄、VPN₃、VPN₂、VPN₁,各 9 位)和一个页内偏移(Offset,12 位)。每个 VPN 索引对应级别的页表,以此查找下一层的页表基址,直到获得最终的物理页号(PPN) 。
四级页表由 PML4、PDPT、PD 和 PT 四层组成,每层都有 512 条 8 字节的页表项。MMU 首先使用 VPN₄ 索引 PML4 表,以获得 PDPT 的物理基址;随后用 VPN₃ 索引 PDPT 得到 PD 基址;再用 VPN₂ 索引 PD,最后用 VPN₁ 索引 PT,从而取得包含 PPN 的页表项。如果任何一级表项的"Present"位为 0,则表明该页不在内存中,需要触发缺页异常。一旦从最底层页表项中读取到物理页号 PPN,MMU 便将其左移 12 位,并与线性地址的 12 位页内偏移合并,生成完整的物理地址。随后,该物理地址被送入 L1→L2→L3 缓存层级,最后访问 DRAM 。
在完成页表遍历并获得 PPN 后,MMU 通常将此 VPN→PPN 映射写入 TLB,使未来对同一虚拟页的访问能够直接命中 TLB、跳过昂贵的多级遍历,从而显著提升访问速度。
若任何级别的页表项不在内存(Present 位为 0),MMU 会触发缺页异常(Page Fault)。操作系统的缺页处理程序随后检查该虚拟页是否属于合法的映射范围,若合法则将该页从磁盘或交换区载入内存、更新页表并重试指令;否则向进程发送 SIGSEGV,以实现内存访问保护。
7.5 三级Cache支持下的物理内存访问
当 MMU 最终生成了一个PA后,CPU 会将该地址根据缓存结构参数拆分为三个字段------块内偏移、组索引和标记位。其中,块内偏移用于在缓存块内部确定具体的字节位置;组索引用来在多组数的缓存中选定一个组;标记位则与该组内各路所存储的标记进行逐一比较,以判定数据是否命中该缓存行。
CPU 首先在最快速的 L1 高速缓存 中进行数据查找。硬件同时读出该组所有路的标记位和有效位,然后将它们与物理地址的标记位字段并行比较;若找到某一路既有效又与 Tag 匹配,即为 L1 命中,CPU 随即根据块内偏移读取出所需字节,极大减少了访问延迟,通常仅需几周期即可完成。
若 L1 未命中,CPU 会自动在容量更大但速度稍慢的 L2 缓存 中执行相同的组---标记---偏移查找流程。L2 缓存一般为 4--16 路集合关联结构,块大小和组数根据具体实现而异。若在 L2 中命中,数据不仅被返回给 CPU,同时还会回写(填充,Fill)至 L1,以提高下一次访问的命中率。
当 L2 仍未命中时,CPU 继续退到更大但更慢的 L3 缓存(往往是多核共享)进行同样的查找。L3 的容量通常数倍于 L2,可存储更多数据,减少对主存的直接访问。若 L3 命中,同样会依次回填到 L2 和 L1,实现多级缓存的协同效应。
仅当三级缓存均未命中时,CPU 才发起对 DRAM 主存 的访问。数据返回后,先填充到 L3,再逐级填充到 L2 和 L1。在每一级缓存中,若目标组中存在有效位为 0 的空闲行,则直接写入;否则,缓存控制器会根据替换策略。选择一条"最久未访问"或"最不频繁使用"的行进行驱逐,然后将新数据写入,以确保热点数据能迅速留驻于高速缓存中。
7.6 hello进程fork时的内存映射
当调用fork创建子进程时,Linux 内核利用虚拟内存和写时复制技术,为父子进程提供私有且独立的地址空间。
当父进程执行fork时,内核不会立刻复制所有物理页,而是为子进程创建一个新的 mm_struct(进程地址空间描述符)以及与父进程相同的 vm_area_struct(虚拟内存区域列表)副本,继承完整的虚拟地址布局。随后,父子进程共享同一套页表页,所有共享页面在两者的页表中均被标记为"只读",引用计数增加,并开启写时复制模式。在此阶段,读操作完全共享,不发生任何物理页复制。
只有当父或子进程尝试向某个共享页面写入时,MMU 会因为缺少写权限而触发一次页故障,内核的 COW 处理程序才会为该写操作分配新的物理页面,将原内容复制过去,并在故障进程的页表中启用写权限,其它进程依然继续共享原页面。
7.7 hello进程execve时的内存映射
在 Linux 中,execve会在当前进程上下文中卸载旧的可执行映像并映射新的可执行文件及其依赖库,从而重新构建进程的虚拟地址空间。步骤如下:
(1)内核删除原有的用户内存区域(包括代码段、数据段、堆和栈)
(2)依据 ELF 文件的程序头将新的 .text 和 .data 段私有映射到各自的内存区域,同时为 .bss、堆和栈创建映射
(3)内核通过动态链接器将所有共享库(如 libc.so)映射到共享区域
(4)设置程序计数器(PC)指向入口点,启动新的程序。
整个过程中,所有新创建的区域默认采取写时复制策略,只有在首次写入时才真正分配物理页。
7.8 缺页故障与缺页中断处理
当进程访问某个虚拟地址 A 对应的页表项(PTE)"缺失"或权限不足时,MMU 会产生缺页异常(Page Fault),将控制权切换至内核的 page_fault_handler。内核先保存用户态寄存器和程序计数器等现场信息,为后续恢复执行做准备。
内核检查地址 A 是否落在任何已定义的虚拟内存区域范围内,若不在则发送 SIGSEGV 终止进程;若在但访问类型(读/写/执行)与区域权限不符,则发送 Protection Fault 并终止进程;否则认为这是一次合法的缺页,可进一步处理。
对于有效缺页,若该页在磁盘或交换区存在,内核先根据替换算法选出一个物理页帧(若该页帧已"脏"则写回磁盘),再从后备存储读取目标页;若页已在内存中仅需更新 PTE,则直接标记为 Present。完成后,内核恢复现场并重新执行导致缺页的指令,此次访问即可正确命中。

图7.3 缺页处理
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)
7.10本章小结
本章介绍了虚拟内存相关的知识,从逻辑地址、虚拟地址、物理地址出发,讲述了段式管理、页式管理、TLB与四级页表支持下的VA到PA的变换。并且阐述了三级cache对物理内存访问的支持、hello进程在fork和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的经历:hello的一生从预处理阶段开始,随着头文件内容的插入,从hello.c变成了hello.i;接下来是编译阶段(hello.s)和汇编阶段(hello.o),hello被进一步转为了汇编代码和机器码,此时的hello已经是计算机能读懂的二进制代码了;紧接着是链接阶段,链接器将hello.o和标准库中的目标代码链接,生成了可执行文件hello。在这之后我们在终端输入./hello,shell调用fork创建子进程,在子进程中调用execve运行hello,这其中操作系统会将程序抽象为进程并用异常控制流控制进程的运行、用虚拟内存实现数据到物理内存的映射,最后随着进程被回收,hello的一生结束了。
(结论 0 分,缺失- 1 分)
附件
|-----------------|-------------------------|
| hello.c | hello 的C源文件 |
| hello.i | hello.c经过预处理后的预编译文本文件 |
| hello.s | hello.i经过编译得到的汇编语言文本文件 |
| hello.o | hello.s经过汇编得到的机器语言二进制文件 |
| hello_elf.txt | hello.o的ELF格式文本文件 |
| hello_dis.txt | hello.o反汇编的文本文件 |
| hello_elf_1.txt | hello的ELF格式文本文件 |
| hello_dis1.txt | hello的反汇编文本文件 |
| hello | hello.o经过链接得到的可执行目标文件 |
(附件0分,缺失 -1分)
参考文献
- https://blog.csdn.net/General_zy/article/details/126445351?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522680f8af78782f7705996b311301fd924%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D\&request_id=680f8af78782f7705996b311301fd924\&biz_id=0\&utm_medium=distribute.pc_search_result.none-task-blog-2\~all\~top_positive\~default-1-126445351-null-null.142\^v102\^control\&utm_term=虚拟内存\&spm=1018.2226.3001.418
- 深入理解计算机系统(原书第三版).机械工业出版社, 2016.
- https://blog.csdn.net/qq_42570601/article/details/116668310?ops_request_misc=%257B%2522request%255Fid%2522%253A%25227bec2b41300700d4f90ace4f5dfc2d0d%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D\&request_id=7bec2b41300700d4f90ace4f5dfc2d0d\&biz_id=0\&utm_medium=distribute.pc_search_result.none-task-blog-2\~all\~top_positive\~default-1-116668310-null-null.142\^v102\^control\&utm_term=异常控制流\&spm=1018.2226.3001.4187
- https://blog.csdn.net/qq_42570601/article/details/123721693?ops_request_misc=\&request_id=\&biz_id=102\&utm_term=csapp存储\&utm_medium=distribute.pc_search_result.none-task-blog-2\~all\~sobaiduweb\~default-0-123721693.142\^v102\^control\&spm=1018.2226.3001.4187
(参考文献0分,缺失 -1分)