%%
\
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
![[02-开发笔记/03-计算机基础/机器视角下的进程#^rovvrr]]
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
一个进程的地址空间包含**运行的程序的所有内存状态**。
- 栈:保存当前函数的调用信息,局部变量,函数参数,函数返回值,分配和释放由编译器管理。
- 堆:由用户管理、动态分配的内存。(`malloc`、`new` 等申请的动态内存)
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 进程的虚拟内存空间 ⭐
> 进程结构体 PCB 中存储有 **进程页表基地址**(虚拟内存地址) => 用于加载到页表基地址寄存器(寄存器里存的是物理地址)
每个进程的**虚拟内存空间**都是一段 **==从 0 开始==** 的 **连续地址空间**(`0x0` 起)。
- 地址空间**最顶部的==高地址区域==** 为 "**内核地址空间**"——保留给 **OS 内核中的代码和数据**;
- 地址空间**底部==低地址区域==** 为 "**用户地址空间**"——存放 **用户进程定义的代码和数据**。
Linux 系统中进程的**虚拟内存空间**布局如下[^1] [^2] [^3]:
![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-829AAF3EB10496D83DE54757D82B1112.png|385]]
在用户地址空间中,从**高到低地址**分别是 6 种不同的内存段[^4]:
- **栈区** `stack`:保存**函数调用上下文:局部变量、函数参数等**等。
- **映射段** `mms`:共享内存的映射区域,包括动态库、mmap等,从低地址开始向上增长;
- **堆区** `heap`:**动态分配的内存,从低地址开始向上增长**
- **数据区**
- **未初始化数据段** `.bss`:存放 "**==未显式初始化或初始化为 0=="** 的静态存储持续性变量(全局变量、静态变量)
- **初始化数据段** `.data`:存放 "**已显式初始化**" 的静态存储持续性变量(全局变量、静态变量)
- **只读数据段** `.rodata`:存放只读的 **常量数据**,包括 **`const` 全局常量**、**`const` 静态常量**、 "**字符串字面量**"、**虚函数表**、**switch 跳转表**;
- **文本区/代码区**
- **代码段/文本段** `.text`(只读):**存放程序的二进制可执行代码(机器指令)**
代码段下面还有一段内存空间的(灰色部分),这一块区域是 "**不可访问的「==内存保留区==」**"——通常在大多数的系统里,**认为较小数值的地址不是一个合法地址**。例如,**通常在 C 的代码里会将无效的指针赋值为 NULL**(编译器可能具体实现为 0 地址)。
> [!info] `.rodata` 段通常位于 `.data` 段与 `.text` 段之间,上图未标注出来
> [!info] x86-64 下,Linux 中进程虚拟地址区间范围
>
> - 内核地址空间: `[2^48, ...]`
> - 用户地址空间:`[0x400000, 2^48-1]`;
> - **代码段**总是从 `0x400000` 起始,后跟数据段;
> - **用户栈的栈底地址、堆区起始地址、映射段的起始地址**会受 "地址空间布局随机化"( ALSR)影响;
>
> [!INFO] 地址空间布局随机化(ASLR)
>
> ASLR(Address Space Layout Randomization)是用于安全防护的一种手段,其会**随机化进程用户空间 "栈、映射段、堆"的起始地址**。
>
> [!NOTE] 关于 `_end` 链接器符号
>
> `_end` 是一个链接器符号,表示程序**在内存中的==已分配段==的结束地址**——通常指向 **BSS 段的结束位置**,即**未初始化数据段的末尾(高地址末尾)**。
>
> 在许多嵌入式系统或操作系统开发中,**链接器脚本**(linker script)会定义这个符号,
> 以便开发者知道程序数据段结束的位置,从而可以**在这之后分配堆栈、堆等动态内存区域**。
>
<br>
### (1)内核地址空间
每个进程的虚拟地址空间 **顶端部分** 是 "内核地址空间",由 **==所有进程共享==**,映射到 **内核占用的物理内存页**。
(内核地址空间在各进程中的地址范围完全相同,不受 ASLR 地址空间布局随机化影响)
> [!caution] **用户程序无法访问内核地址区域,只有 OS 内核才能访问**(受页表中 SUP 位控制)
![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-21E018ECB246824E47D17198E9760EB2.png|563]]
在内核地址空间中,存储有:
- **进程相关的数据结构**:页表、task 结构体等等;
- **一块连续的虚拟页面**(大小等同于物理内存 DRAM 总量)会被映射到 "**整个物理内存空间**" => 便于内核 **访问物理内存中任意地址**。
- **内核自身占用的内存**——**内核代码段**、**内核数据段**、**内核栈**。
> [!NOTE] 将 "内核地址空间" 映射到 "所有进程的虚拟地址空间" 中的优点
>
> **当进程执行系统调用或发生中断时,CPU 可以无缝地切换到内核模式**。
>
> 1. 当进程发起 "**系统调用**" 而下陷到内核态时,**不需要切换虚拟地址空间**;
> 2. **OS 在处理系统调用** 时也**可方便地访问用户态的数据**;
>
> 缺点在于:应用程序与内核的隔离性不够强,内核可能不小心跳转到用户态的代码段,或者将关键数据写误写入用户态的数据段
<br>
### (2)**栈区**(Stack)
> 栈区 (Stack),也称**运行时栈(Runtime Stack)| 函数栈 | 用户栈 | 调用栈**,参见 [[#函数栈 运行时栈 ⭐]]
<br><br>
### (3)**映射段** (Memory Mapping Segment,MMap)
映射段用于「**==文件映射==**」和「**==共享内存==**」。
映射段的用途:
- **文件映射**:
- 将**文件直接映射到虚拟地址中来访问**,任何应用程序都可以请求该区域,通过将文件与一块连续的虚拟内存区域关联,可以之间通过**读写内存的方式**来访问文件(像访问内存数组一样访问文件)而不需要使用常规的文件 I/O 函数(如 `read()` 或 `write()`)。<br>这种方式通常更高效,因为操作系统可以利用现代硬件的能力,如页面缓存、延迟加载等。
- **延迟加载**:当文件被映射到内存时,实际的数据加载可能会被推迟到真正访问该数据时。
- **写时复制**(Copy-On-Write, COW)技术可以被使用来延迟数据复制,直到进程尝试修改数据。这特别有用于共享内存和 `fork()` 系统调用。
- **用作共享库的内存映射区域**:
- **动态库**(例如 `libc` 等)作为可供多个进程使用的共享库,通常都被**映射到该区域中,并标记为只读**
- **进程间内存共享**:
- 共享内存区域可以使用特殊的标志与 `mmap()` 函数结合来创建。
- 不同的进程通过访问共享内存区域来实现进程间的数据共享,是进程间通信的一种高效方式;
- **匿名映射**:
- 除了映射实际的文件,还可以创建匿名内存映射,它们**不与任何文件关联**,但提供了一种手段来分配可由多个进程共享的内存。
> [!NOTE] Linux 中通过 `mmap()` 系统调用实现 "内存映射"
<br>
### (4)**堆区** (Heap)
堆区 (Heap),也称**运行时堆**(Runtime Heap),是用于 **==动态内存分配==** 的区域,堆区通常从**低地址向高地址**增长。
堆内存的**动态分配**和**释放**由开发者手动维护。忘记释放已分配的内存会导致**内存泄漏**。
> [!NOTE] C/C++中使用 `malloc`,`calloc`,`realloc`, `new` 申请的动态内存空间都是位于堆区
<br>
### (5)**数据段**(初始化数据段 & 未初始化数据段)
数据段——用于存放程序的 **==静态存储持续性变量==(全局变量,`static` 静态变量)**,进一步分为:
- **初始化数据段**(Data Segment)
- `.data`段——存放 **==已显式初始化==的全局变量和静态变量**。
- `.data` 段的大小在编译时确定,**占 ELF 文件空间**。
- **未初始化数据段**(BSS Segment,Block Started by Symbol)
- `.bss` 段——存放程序中 **未被==显式初始化==或 ==显式初始化为 0== 的全局变量和静态变量**。
- `.bss` 段在 ELF 文件中**不占文件空间**(即不占磁盘空间),仅在 "**节头部表**" 中标识
> [!info] `readelf -l <program>` 查看节头部表,可见 `.bss` 不占空间
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-D66C17CCCC7F8319F66E1383CB2A1D43.png|694]]
> [!NOTE] `.bss` 与 `.data` 节的区别
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-81139ACBB4BF8A6B4C0FE2ACECFAABC2.png|597]]
>
> ```cpp
> // 已显式初始化的静态存储持续性变量———全局变量/静态变量, 位于.data段.
> // 编译时确定初始值,会写入到ELF文件中, 占ELF文件空间.
> int a = 42;
> static int b = 100;
>
> // 未显式初始化的静态存储持续性变量——全局变量/静态变量, 位于.bss段.
> // 默认初始化为0, 编译器省略为其存储初值0, 不占ELF文件空间, 由进程加载到内存中时才为其分配内存.
> int x;
> static int y;
> ```
> ^xt6kcg
<br>
### (6)只读数据段(rodata)
`.rodata` 段——存储**只读数据**[^4],包括:
- **静态存储持续性的常量**——**const 全局常量**、**const static 静态常量**
- **字符串字面量**(例如 `printf` 中的格式字符串)
- 编译器生成的数据:
- **==虚函数表==** vtable
- **类型信息表**(typeinfo),存储 `typeid`,用于实现 RTTI
- **`switch` 语句中的跳转表**
- ....
> [!note] `constexpr` 常量通常在**编译时取值**(编译器直接替换),故编译器不会为其分配内存空间
>
> 除非对 `constexpr` 常量取地址`&`,或是一个 `static constexpr` 常量,此时才会被存入到 `.rodata` 段,
```cpp
// 静态存储持续性的全局常量&静态常量, 位于.rodata段
const int x = 2;
static const int sx = 12;
static constexpr int sy = 13;
int main() {
// 字符串字面量"hello"位于.rodata段.
// s是指向一个只读字符数组的指针, 是局部变量, 位于栈上.
const char* s = "hello";
}
```
<br>
### (7)代码段 | 文本段 (Text Segment)
`.text` 段——保存**程序的二进制可执行代码**本身(**机器指令**),"**只读**",以防止程序意外修改自身指令。
> [!NOTE] 程序中所有函数的地址,包括虚函数,均是位于 `.text` 段部分
<br><br><br>
# 函数栈 | 运行时栈 ⭐
> 函数栈,也称**运行时栈** (Runtime Stack),用户栈
**栈区**是进程虚拟地址空间中,用于存放 **程序==局部数据==** 的内存区域,由 **编译器** 自动管理,进行**自动分配和释放**,特点如下:
- **栈从==高==地址向==低==地址方向增长**;
- **==栈指针==**(**`%rsp` 寄存器**)指示 **==栈顶==地址**(低地址)
- 大于等于 `%rsp` 的地址是**已使用的**;
- 小于 `%rsp` 的地址是**未使用**的。
CPU 通过执行 `push`/`pop` 指令实现在栈区上 **分配与释放内存**,这两个指令完成两个操作:
1. 减小/增大 **栈指针 `%rsp` 的值**;
2. 将数据**压入/弹出**栈中。
> [!NOTE] **局部非静态变量(即自动存储持续性的变量)都存储在栈区**,其**被删除的顺序**与**创建顺序相反**,符合栈的特性。
> [!NOTE] Linux 中程序线程的==**栈区大小默认为 `8M`**==,可通过 `ulimit -a` 查看。
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-F72B77CA35A00089C98E7268825624D7.png|386]]
>
> 该值是 OS 设置的 "**==每个线程==**" 的默认**栈区大小**(`8MB`)。
>
> OS 在创建进程时,会据此为其主线程 **分配一块栈区内存**(虚拟内存)[^5]。
> 栈指针指示了 "**栈区中==已使用区域的栈顶==**"。当进程实**际所需使用的栈内存超出栈区容量**时,触发 "**栈溢出**",终止进程。
>
> 编译器或链接器通常指定程序或线程的栈大小——例如,GCC 链接器 `ld` 的 `-stack` 选项。
<br>
## 函数调用机制
"**函数调用**" 机制是在 "**栈区**" 上实现的,提供了 **"后进先出 LIFO" 的内存管理方式**。
对计算机而言,进行函数调用(**过程 P 调用过程 Q,Q 执行后返回 P**)涉及**三方面动作**:
- **==传递控制==**:
- 进入过程 Q 之前,将程序计数器 PC 设置为 **Q 的代码的起始地址**;
- 将返回到 P 时,将程序计数器 PC 设置为 "**P 中过程调用指令的后一条指令**"
- **==传递数据==**(传递参数,返回值)
- **==分配和释放内存==**(创建 / 销毁局部变量)
> [!NOTE] 函数调用通过 `call` 与 `ret` 两个机器指令完成
>
> 过程调用指令 `call <label>` 执行的两个操作:
>
> 1. 将 "**返回地址**"(`%rip` 当前值) 压入栈中;
> 2. **设置 `%rip` 为被调用函数的入口地址**,**跳转至被调用函数**。
>
> 返回指令 `ret` 执行的操作:
>
> 1. **弹出被调用函数的栈帧**(释放),再进而弹出 "**返回地址**"。
> 2. **设置 `%rip` 为返回地址**。
>
<br><br>
## 栈帧
每次进行 "**函数调用**" 都会在栈上为 "**被调用函数**" 分配一块新的连续内存空间,称之为 **==栈帧==**(Stack Frame),用以存放 "**被调用函数**" 的相关数据:
| 栈帧上保存的内容 | 说明 |
| -------------- | ---------------------------------------------------------------------------------------------------- |
| (1)**保存的寄存器值** | 包括两类:**被调用者保存的寄存器 callee-saved**、**调用者保存的寄存器 caller-saved** |
| (2)**局部变量** | 被调用函数内的局部变量(按声明顺序,**最先声明的最先创建、最后销毁**) |
| (3)**参数** | 在 "**当前函数**" 内调用其他函数时,所需传递的调用参数。 <br>(注:x86-64 下前 6 项参数首先通过 **6 个特定寄存器**传递,多余参数才通过压入 "当前函数" 的栈帧内传递) |
| (4)**返回地址** | 在 "**当前函数**" 内调用其他函数时,**返回地址压入 "当前函数" 的栈帧内**。 |
编译器在生成汇编代码时,对于函数调用会插入**创建栈帧**和**释放栈帧**相关的指令(例如 push、pop 指令,其移动栈指针,入栈弹栈数据)。
> [!info] 栈帧示意[^6]
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-4198A5560E3FD4B14522BE9E256C43B3.png|660]]
>
> 栈帧的具体布局受目标平台和编译器影响,可能会有所不同。
>
> [!NOTE] x86-64 中,函数调用的 **前 6 个参数** 直接通过寄存器 `%rdi`,`%rsi`, `%rdx`, `%rcx`, `%r8`, `%r9` **按序**直接传递
>
> **剩余多出来的参数,或是参数无法通过寄存器传递时,就需要保存到 "==调用者的栈帧中==",供被调用者获取**。
> [!NOTE] 关于 "返回地址"
>
> 返回地址——发生函数调用时,紧跟 **过程调用指令`call`** 之后的**下一条机器指令**的虚拟内存地址,也即 **==程序计数器 PC 中的值==(`%rip`)**。
>
> 例如 A 调用 B,**返回地址**是函数 A 的机器指令中,调用完 B 函数之后的**下一条指令的地址**。
<br>
### 栈帧上保存的寄存器值
函数调用过程中,寄存器是被不同函数共享的硬件资源,故"**CPU 当前寄存器值**" 可能需要被暂存,以便后续使用,分为两类:
- (1)**被调用者**保存的寄存器(**==callee-saved==**):保存到 "**==被调用者==的栈帧**" 中
- (2)**调用者**保存的寄存器(**==caller-saved==**):保存在 "**==调用者==的栈帧**" 或者 "**其他==空闲寄存器==**" 中。
除 "**栈指针 `%rsp`**" 和 "**被调用者保存寄存器**" 以外的其余 9 个通用寄存器均为 "**调用者保存的寄存器**" [^7]。
> [!example] 函数调用过程中,寄存器值 "暂存" 示例 [^7]
>
> 如下图所示:
>
> - P 作为其 "**上层函数**" 的 "**被调用者**",**使用 `%rbp` 与 `%rbx` 两个 callee-saved 寄存器前,需要将旧值压栈,以便 P 返回时恢复**;
> - P 作为 "**被调用函数 Q**" 的 "**调用者**",需要先将 **`%rdi` 这一 caller-saved 寄存器旧值**(上层函数传给 P 的参数 x)暂存,再**通过 `%rdi` 传递调用 Q 的实参。**
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-E0E421B4E2373988855E01C6000AFB88.png|745]]
>
<br>
##### (1)被调用者保存的寄存器(callee-saved)
x86-64 下共 6 个:`%rbx`、`%rbp`、`%r12` ~ `%r15`。
callee-saved 寄存器中的值是 "**==外层函数所需使用的==**"。
因此,当 **==被调用函数==** 将要使用/修改这些寄存器时,必须先将 **寄存器旧值** 压入其栈帧,以便**被调用函数返回前恢复旧值**。
> [!example] callee-saved 寄存器示例
>
> ```asm
> ; 被调用函数内:
> push %rbx ; 保存
> ...
> ; 使用 %rbx 做某些事
> ...
> pop %rbx ; 恢复
> ```
>
<br>
##### (2)调用者保存的寄存器(caller-saved)
x86-64 下共 9 个:
- **函数返回值用 or 临时寄存器**:`%rax`;
- **参数传递用 or 临时寄存器**(6 个):`%rdi`,`%rsi`,`%rdx`,`%rcx`,`%r8`,`%r9`
- **临时寄存器**(常用于 syscall):`%r10`,`%r11`
caller-saved 寄存器中的值是 "**==当前函数所需使用的==**",例如上层函数传递给 "**当前函数**" 的实参值。
因此,在**当前函数 "调用其他函数" ==之前==**,若这些寄存器值在 "**调用其他函数之后" 仍会用到**,则需要将 "寄存器当前值" **压入其栈帧或保存到 "其他空闲寄存器" 中**。因为 "**被调用函数**" 可能会修改这些寄存器值。
> [!example] caller-saved 寄存器示例
>
> - A 调用 zoo 时通过寄存器 `%rsi` 传递了**调用 zoo 的实参**。
> - zoo 中调用 foo,**需要通过 `%rsi` 向 foo 传递参数**,故需要**暂存 `%rsi` 中原先 A 传递给 zoo 的实参**。
>
> ```asm
> ; 调用者函数内:
> push %rsi ; 保存,因为调用 foo 后还需要用到
> call foo ; foo 可以随意改 %rsi
> pop %rsi ; 恢复
> ```
>
<br><br>
## 栈指针 `%rsp`
**栈指针寄存器 `%rsp`** :保存着当前 "**==运行时栈==**" 的 **==栈顶==地址**(**低地址**)
> [!NOTE] 「**栈指针**」与「**帧指针**」
>
> - 栈指针:保存着 "**当前线程的栈区的==栈顶==地址**"(低地址)
> - 帧指针:保存着 "**当前函数栈帧上,存放 `%rbp` 旧值**" 的栈内存地址
>
> 两个指针值存放在 CPU 中的**两个通用寄存器**中:
>
> - x86-64 中,为栈指针寄存器 `%rsp` 、帧指针寄存器 `%rbp` ;
> - ARM 中,对应为 `SP` 和 `FP`
>
<br>
## 帧指针 `%rbp`
**帧指针**寄存器 `%rbp` :保存着**当前函数栈帧上,存放 "前一 `%rbp`旧值"** 的**栈内存**的虚拟地址。
在函数调用过程中,是先将 `%rbp` 旧值压入**当前函数的栈帧**(`%rsp` 减小),
再**将此时的 `%rsp` 值存入到 `%rbp` 寄存器** 中,即作为 "**==当前函数栈帧的基地址==**",故也称 "**==基指针==**"(base pointer),
该地址可用于 **==定位==栈帧内==局部变量和参数==的地址**:
- `%rbp + offset` 向上可访问**当前函数的返回地址 or 传入参数**;
- `%rbp - offset` 向下可访问**当前函数的局部变量**。
> [!NOTE] 帧指针示意 [^8]
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-62363106DB051162C41A00AA73F28746.png|401]]
<br>
### 帧指针的作用
> <font color="#c0504d">❓为什么需要帧指针?</font> ^rovvrr
帧指针的作用:
- (1)**涉及==变长==栈帧时,便于==定位==栈帧内==局部变量和参数==的地址**
- 说明: "`%rbp`" 始终指向 "**当前栈帧的基地址**",局部变量和参数的地址**相对于 `%rbp`的偏移量固定不变**,所以便于定位
- (2)实现 "**==栈追踪/栈回溯==**",追溯出 **==调用链==**
- 说明:"`%rbp`" 所指地址处存的是 "**调用者的`%rbp` 值**"—— 每个**调用者的 `%rbp` 值**都被保存在 "**被调用者的栈帧中**",所以可以追溯
> [!caution] 不是所有函数栈帧都需要保存帧指针 `%rbp`!
>
> 在 x86-64 架构的函数调用过程中,**栈帧是否保存帧指针 `%rbp`** 取决于**函数的实现方式**和**编译器的优化策略**。
> 只有在需要 **==变长栈帧==**(用以存储变长静态数组)、**==调试==**、**==复杂栈帧管理==** 时才需要保存帧指针。
> [!NOTE] 变长栈帧
>
> **涉及变长栈帧时**,**==栈帧大小在运行时确定==**(即 `%rsp` 位置在运行时才确定),**通过 `%rsp` 定位局部变量和参数较为复杂**。
>
> 变长栈帧的场景:函数局部变量有**变长静态数组**或使用了其他**需要==动态调整栈帧大小==** 的特性(例如使用 `alloca` 在栈上分配任意字节数量的存储)
>
> [!NOTE] 省略帧指针
>
> 在特定编译器优化下,可省略帧指针以节省寄存器和提高性能——栈指针(`%rsp`)将被用来定位所有局部变量和参数。
>
> 略帧指针的优化称为**帧指针省略(Frame Pointer Omission, FPO)** 或 **帧指针消除(Frame Pointer Elimination, FPE)**。
<br><br>
## 初始函数栈的结构示意
进入 `main` 时,**函数栈的初始结构**[^9]
![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-E4B4B3869DBC0FD62CAE096E43061EAB.png|566]]
其中:
- `argv` 与 `envp` 分别为**参数列表**、**环境变量列表**,对应 `int main(int argc, char** argv, char** envp)` 原型中的后两项。
- 两个数组中的各项 `argv[i]` 与 `envp[i]` 存储的 **字符串指针** 指向栈底的 "参数字符串" 与 "环境变量字符串" 中各项的起始地址。
- `libc_start_main` 是 **系统启动函数**。
<br>
## 函数调用过程说明——压栈和出栈
![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-157E4045829813F6FD5D1B0771D99F41.png|605]]
- 当**发生函数调用**时,在进程的运行时栈上会压入**调用参数**和**返回地址**,随后再 "**压入当前被调用函数的==栈帧==**"。(栈指针`%rsp`减小)
- 在 "**调用者**" 的栈帧中:
- 压入 "**调用者保存的寄存器**" 的值(caller-saved:例如 `%rdi`,`%rsi` 等,如果有需要)
- 压入 **调用参数**(如果超过 6 个或无法通过寄存器传递时);
- 压入 **返回地址**;
- 在 **"被调用函数" 的栈帧中**:
- 压入 "**被调用者保存的寄存器**" 的值(callee-saved:`%rbp`,`%rbx`,`%r12-%r15`)
- 压入 "**局部变量**";
- 当**被调用函数返回** 时,则会**弹出 "当前被调用函数的栈帧" 以及 "返回地址"**( 栈指针 `%rsp` 增加)
- 函数调用的返回值通过 **`%rax` 寄存器**暂存;
- 将栈帧上存储的寄存器旧值 "**恢复到寄存器中**";
- 将返回地址写入到 **`%rip` 指令指针寄存器**;
> [!summary]
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-185975CC37D9B084681E036E84D0C54C.png|396]]
>
>
>
### 示例说明
> 参见 [^10] [^11]
程序代码:
```cpp title:example.cpp
int add (int a, int b) {
int result = a + b;
return result;
}
int main (int argc, char** argv) {
int answer;
answer = add (40, 2);
}
```
#### 调用过程
说明:
- (1)**进入 `main` 函数**:
- 发生一个函数调用,将**返回地址**压入栈,ESP 减小; (返回地址:函数 `main` 被调用后的下一条指令)<br>(下图 1 所示)
- 将当前 EBP 寄存器的值压入到栈保存(**旧的 EBP 值**,即调用者的**帧地址**),ESP 减小。<br>(下图 2 所示)
- 将 EBP 寄存器的值设置为 **==当前 ESP 寄存器==** 的值,此时 EBP 寄存器值 = ESP 寄存器值,即为 **当前函数的栈帧的基地址**。而在这地址上存储的值,则是外层函数的 EBP 值。 <br>(下图 3 所示)
- ESP 减小,向低地址移动,为**当前函数**的 **栈帧** 分配栈空间。<br>(下图 4 所示,当前函数为 `main`);
- (2)**发生一个新的函数调用**(**`main` 调用 `add`**):
- 创建函数调用的参数 <br>(下图 5 所示);
- 将函数返回地址压入栈,ESP 减小;(函数 `add` 的返回地址,即 `main` 中调用完 `add` 后的下一条指令)<br>(下图 6 所示)
- 将旧的 EBP 值压入栈,ESP 后移; <br>(下图 7 所示)
- 设置 EBP 寄存器=ESP 寄存器值;<br>(下图 8 所示)
- ESP 减小,为当前函数的栈帧分配栈空间 <br>(下图 9 所示)
- 执行加法计算,并加计算结果写入局部变量 result<br>(下图 10~11 所示)
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-E9666F056DDABCA3C3181D06A94B84FC.png|517]]
<br>
#### 函数返回过程
- 将 EBP 寄存器中的值赋给 ESP 寄存器,即 ESP 移动到当前函数栈帧的基地址,释放掉当前函数的栈帧。<br>(下图 2 所示)
- 弹栈,ESP 后移,得到旧的 EBP 值(即调用者的 EBP 值)并**恢复到 EBP 寄存器中**。此时 EBP 寄存器中为 `main` 函数的基地址;<br>(下图 3 所示)
- 弹栈,ESP 后移,**返回地址**从栈上弹出并赋给 **EIP 寄存器(指令指针)**,控制转回到**调用函数**;<br>(下图 4 所示)
- 继续重复上述步骤,完成 `main` 的调用返回过程。<br>(下图 5~8)
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-7A4B6CEFC3A0B0FAC476629303D5C388.png|443]]
---
<br><br><br>
# 栈溢出 (Stack Overflow)
「**栈溢出**」:**程序实际使用的运行时栈超过了栈空间限制**(超过了 OS 为其分配的大小),会**导致程序==异常终止==**。
> [!info] Linux 下默认的栈空间限制为 8MB,可通过 `ulimit -a` 查看
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-BBFAAC939999F827C359A750E46EFEA6.png|388]]
>
#### 栈溢出的两点原因
1. **==函数调用层次过深==**:**较深层次的嵌套函数调用**而导致函数栈帧 **耗尽栈空间**。 常见于**错误编写的无限递归**;
2. **==局部变量过大==**:在函数内声明了**非常大的局部变量**,导致变量所需空间耗尽栈空间。
> [!tip]
> - 如果知道应用程序**需要大量的栈空间**,可以考虑**调整默认的栈大小**设置。
> - **避免在栈上分配过大的数据结构**,或**考虑使用==堆内存==代替栈内存**。
<br><br><br>
# 缓冲区溢出(Buffer Overflow)
**缓冲区溢出**——当程序**试图向缓冲区(例如字符数组)写入超过其容量的数据时,==超出部分的数据==会==覆盖到缓冲区相邻的内存区域==**,称之为 **缓冲区溢出**。
缓冲区溢出包括两种情况:
- **==栈缓冲区==溢出(Stack Buffer Overflow)**:发生在**栈上分配**的缓冲区内。
- **==堆缓冲区==溢出(Heap Buffer Overflow)**:发生在**堆上动态分配的内存区域** 上。
> [!faq] **导致缓冲区溢出的原因**
>
> **未对写入缓冲区的数据长度进行检查,==实际写入数据量超出了缓冲区大小==**,
> 从而**覆盖了缓冲区相邻的内存区域**(例如栈帧上的其他内容、或是堆上的其他内存空间)
#### 缓冲区溢出的后果
1. **程序崩溃**:会**覆盖内存**中的其他数据,导致程序崩溃或未定义行为;
2. **数据损坏**:可能导致**覆盖程序中的关键数据**,导致数据损坏。
3. **安全漏洞**:恶意攻击者可以利用缓冲区溢出执行**代码注入攻击、缓冲区溢出攻击**等,从而控制程序的执行流程,造成安全漏洞。
#### 缓冲区溢出的常见原因
- **缺乏边界检查**:程序在写入缓冲区数据时,未检查数据的长度是否超过缓冲区的容量
- **数组越界 "写入"**:数组**下标越界**并向越界位置写入数据;
- **指针越界 "写入"**:**指针所指位置越界**,并向越界位置写入数据;
- 常见于**使用不安全的库函数**: 一些标准库函数(如`gets`、`strcpy`、`sprintf`、`scanf`等)不执行边界检查,容易导致缓冲区溢出。
- **野指针访问**:使用未初始化的指针(野指针),指针指向未知的内存地址,可能**导致非法访问(段错误)或缓冲区溢出**。
<br>
## 栈缓冲区溢出 (Stack Buffer Overflow)
栈上存储有函数调用相关信息,攻击者可利用 **栈缓冲区溢出** 覆盖**栈上的返回地址**,控制程序执行,例如令程序跳转到恶意代码的位置。
> [!example] 栈缓冲区溢出示例
>
> ```cpp
> #include <stdio.h>
> #include <string.h>
>
> void MyCopy (char* input) {
> char buffer[5]; // 局部变量, 栈上分配5字节
> strcpy(buffer, input); // 未检查输入长度,可能超过5字节, 导致栈缓冲区溢出
> }
>
> int main() {
> char largeInput[20] = "This is a input.";
> MyCopy(largeInput);
> }
> ```
>
> [!example] 栈缓冲区溢出攻击
>
> 示例:
>
> ```cpp
> void vulnerable() {
> char buf[100];
> gets(buf); // 可能读入恶意代码, 存入栈上, 再通过修改返回地址跳转至栈中恶意代码处进行执行, 即实现 "栈缓冲区溢出攻击"
> }
> ```
>
> 现代 OS 和编译器默认都让栈 **不可执行(non-executable stack)** 来防止这类攻击。
>
> 例如 Linux 下 ELF 文件的 "**程序头部表**" 中有一个特殊段 `GNU_STACK` 用于**标记栈是否可执行**(默认不可执行,权限为 `RW`)。
> 在栈不可执行时,CPU 在执行跳转后会检测到 “**目标地址所在的页是不可执行的**”,然后 **抛出段错误**。
<br>
## 堆缓冲区溢出 (Heap Buffer Overflow)
> [!NOTE] 报错示例
>
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-399555F7FD5986D03CB07D185CA2BC53.png|598]]
>
> 常见于**对 "容器"(例如`vector`) 的越界写入**。
> [!example] 堆缓冲区溢出示例
>
> ```cpp
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
>
> void vulnerableFunction (const char *input) {
> char *buffer = (char *) malloc (5); // 在堆上分配 5 字节的内存
> if (buffer == NULL) {
> fprintf (stderr, "Memory allocation failed\n");
> exit (1);
> }
> strcpy (buffer, input); // 没有检查输入长度,可能导致堆溢出
> printf ("Buffer content: %s\n", buffer);
> free (buffer); // 释放内存
> }
>
> int main () {
> char largeInput[20] = "This is a input"; // 超过 5 字节的输入
> vulnerableFunction (largeInput);
> return 0;
> }
> ```
>
<br><br><br>
# 段错误 (Segmentation Fault)
- 「**段错误**」——触发 "段错误" 的 **直接原因** 是 **进程收到 ==`SIGSEGV(11)` 信号==**,本质原因通常是 "**非法内存访问**"。
- 「**非法内存访问**」——**程序试图访问/写入 "==非法内存地址=="**,触发 **缺页异常**,致使 OS 下陷到内核态后向进程发送 SIGSEGV 信号,进而触发段错误,终止进程。
> [!info] 进程对 SIGSEGV 信号的默认处理是 "终止进程" 并打印 "Segmentation Fault"
> ![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-9C57179328C05BD36BD7CE0C4B3A1581.png|473]]
所谓 "**非法内存地址**",是指该地址 "**无有效映射**"(该地址 **==尚未分配给进程==** or **==权限不符==**)[^12] [^13]。例如:
- "**==未分配==的内存地址**":例如**未被分配或已释放的堆内存**。
- "**==受保护==的内存地址**":
- **系统保护的地址**:例如内核地址空间,不允许用户进程访问。
- **只读数据段地址**:`.rodata` 段,不允许修改
- **栈的保护区**:栈底和栈顶附近的区域,专用于检测栈溢出错误。
每个进程 **"已分配的虚拟地址区域"** 由 **`vm_area_struct` 结构体构成的链表** 表示。
当触发缺页异常时,异常检测程序会**搜索该链表**(借助额外的**平衡树结构**进行搜索而非链表遍历)。
若**访问地址** 不在任何 `vm_area_struct` 所表示的地址区间中,则表明为 "**==未分配地址==**",进而触发段错误[^12],如下所示:
![[_attachment/02-开发笔记/03-计算机基础/机器视角下的进程.assets/IMG-机器视角下的进程-90BB65555A9A3044838AE007B8881582.png|470]]
#### 非法内存访问的常见原因
1. **数组下标越界**
2. **==指令越权访问内存==**,常见于:
- **试图访问==空指针==**:空指针`nullptr` 指向的内存地址为 `0`,该地址属于 "**内存保留区**"。例如 `*((int*)0) = 0;` 即为非法内存访问。
- **试图访问==野指针==**:未初始化的野指针**所指向的地址是未知的**,如果指向非法内存则导致段错误。
<br><br>
# 参考资料
- [【C++ 基础】进程内存布局及其相关知识 - 她爱喝水 - 博客园](https://www.cnblogs.com/PikapBai/p/17577466.html)
# Footnotes
[^1]: 《深入理解计算机系统》(P484)
[^2]: 《深入理解计算机系统》(P580)
[^3]: 《深入理解计算机系统》(P510)
[^4]: 《深入理解计算机系统》(P467)
[^5]: 《操作系统:原理与实现》(P109)
[^6]: 《深入理解计算机系统》(P164)
[^7]: 《深入理解计算机系统》(P173)
[^8]: 《深入理解计算机系统》(P203)
[^9]: 《深入理解计算机系统》(P522)
[^10]: [函数调用过程中栈到底是怎么压入和弹出的? - Rich的回答 - 知乎](https://www.zhihu.com/question/22444939/answer/705117359)
[^11]: [深入了解运行时栈(C语言)](https://blog.csdn.net/cainiaochufa2021/article/details/123693134)
[^12]: 《深入理解计算机系统》(P581)
[^13]: 《操作系统:原理与实现》(P107)