overview
1. 计算机系统概述
1.1 CPU核心架构
现代CPU是一个精密的执行引擎,其核心组件包括:
内存子系统
- 内存可以类比为快递箱系统:支持数据的存储和读取
- 存储的最小单元是字节(byte)
- 内存中存储的都是二进制数据(0和1),数据的意义由程序定义
- 指针:存储其他内存单元地址的特殊变量
内存访问优化
1
2
3
4
5
6
7
8
// 内存访问的局部性原理
int array[100][100];
// 按行访问(高效) vs 按列访问(低效)
for(int i = 0; i < 100; i++) {
for(int j = 0; j < 100; j++) {
array[i][j] = i + j; // 空间局部性好
}
}
寄存器文件
- 数量有限的快速存储单元
- 可以直接通过汇编指令访问
- 比内存访问速度快几个数量级
算术逻辑单元(ALU)
- 纯组合逻辑电路,无状态
- 执行算术和逻辑运算
- 类似纯函数:输入确定,输出确定
程序计数器(PC)
- 特殊的寄存器,指向当前执行的指令地址
- 控制程序的执行流程
控制单元
- 指挥数据通路协调工作
- 解释指令并生成控制信号
需要重点理解的内容
内存:取快递的快递箱,可以往里面放也可以往外拿;存储的最小单元;里面是一大堆01,所有的意义都是人赋予它的
指针:存储着另一个存储单元的地址
可以把memory想象成一个巨大的一维数组
为什么把四个放在一行?—否则效率太低,一次拿一批;地址总线和数据总线
寄存器的数量是有限的,于是我们可以指名道姓地指定用哪个寄存器—汇编语言
有了寄存器的内容,就要计算—ALU(算数和逻辑单元)
ALU是一种纯组合电路,可以理解成纯函数(没有任何状态的函数,任何时候我们输入a和b,都会输出c),属于组合逻辑电路
而有状态的就叫时序逻辑单元,是有状态的
PC也是一个寄存器,指向了当前运行的指令的位置,负责告诉我们程序运行到了哪里
程序的指令也认为是一种数据
CPU只能操作Datapath,谁在指挥呢?—Control
2. 程序执行全过程
2.1 从源代码到可执行文件
编译流程:
C源代码 → 预处理器 → 编译器 → 汇编器 → 链接器 → 可执行文件
C语言代码怎么变成01?—纯软件过程,比如gcc
1
2
3
4
5
# 分步编译过程
gcc -E hello.c -o hello.i # 预处理
gcc -S hello.i -o hello.s # 编译
gcc -c hello.s -o hello.o # 汇编
gcc hello.o -o hello # 链接
2.2 进程内存布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
高地址
┌─────────────┐
│ 内核空间 │
├─────────────┤
│ 栈(stack) │ ← 向下生长
│ ↓ │
│ ↑ │
│ 堆(heap) │ ← 向上生长
├─────────────┤
│ BSS段 │ ← 未初始化全局变量
├─────────────┤
│ 数据段 │ ← 已初始化全局变量
├─────────────┤
│ 代码段 │ ← 只读的程序指令
└─────────────┘
低地址
各段详细说明:
代码段(.text)
- 存储机器指令
- 只读属性,防止程序意外修改指令
- 在内存中通常有执行权限
数据段(.data)
- 已初始化的全局变量和静态变量
- 程序加载时即具有初始值
BSS段(.bss)
- 未初始化的全局变量和静态变量
- 程序加载时自动初始化为0
- “Better Save Space” - 节省磁盘空间
堆(heap)
- 动态内存分配区域
- 手动管理(malloc/free, new/delete)
- 向上生长
栈(stack)
- 自动管理局部变量和函数调用信息
- 向下生长
- 每个函数调用创建栈帧
3. 堆与栈的深入理解
3.1 栈(Stack)机制
如果没有栈也没有堆,内存就是一块平坦的线性空间,但是内存应该是一个高度动态的东西
如果想要实现函数调用,我们就不得不承认栈是最适合函数调用的机制!
只有当前的栈是active的!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
栈帧结构
高地址
┌─────────────┐
│ 参数n │
├─────────────┤
│ ... │
├─────────────┤
│ 参数1 │
├─────────────┤
│ 返回地址 │
├─────────────┤
│ 保存的基址 │ ← EBP/RBP
├─────────────┤
│ 局部变量1 │
├─────────────┤
│ ... │
├─────────────┤
│ 局部变量n │ ← ESP/RSP
└─────────────┘
低地址
栈的核心特性:
- 后进先出(LIFO)的数据结构
- 自动管理:编译器负责分配和释放
- 仅当前栈帧处于活跃状态
- 快速分配和释放
堆
函数调用时会在堆内开辟一块内存,这块内存不会随着函数调用的结束而摧毁
但是堆并不知道什么时候会不需要自己,因此需要手动管理,这也就是堆容易造成内存泄漏的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdlib.h>
void heapExample() {
// 在堆上分配内存
int* array = (int*)malloc(100 * sizeof(int));
if (array != NULL) {
// 使用堆内存
for (int i = 0; i < 100; i++) {
array[i] = i;
}
// 必须手动释放
free(array);
array = NULL; // 避免悬空指针
}
}
堆的核心特性:
- 动态内存分配
- 手动管理,生命周期由程序员控制
- 分配相对较慢,需要寻找合适的内存块
- 容易产生内存泄漏和碎片
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 管理方式 | 自动 | 手动 |
| 分配速度 | 快(O(1)) | 慢(需要搜索) |
| 生命周期 | 函数作用域 | 直到显式释放 |
| 大小限制 | 较小(通常MB级) | 较大(受系统内存限制) |
| 碎片问题 | 无 | 有 |
| 安全性 | 相对安全 | 容易内存泄漏 |
4. 进程隔离与虚拟内存
每个进程只能玩自己的游戏,而操作系统是上帝视角,是游戏规则的制定者
4.1 虚拟内存的意义
解决的问题:
- 进程间内存隔离
- 物理内存有限性
- 内存访问保护
虚拟内存的优势:
1
2
3
// 每个进程都有独立的地址空间
// 进程A看到的地址0x1000 ≠ 进程B看到的地址0x1000
// 操作系统通过页表实现地址转换
虚拟内存布局:
text
1
2
3
4
5
6
7
8
9
10
11
每个进程的视角:
0x00000000 ┌─────────────┐
│ 代码段 │
├─────────────┤
│ 数据段 │
├─────────────┤
│ 堆 │
│ ↑ │
│ ↓ │
│ 栈 │
0x7fffffff └─────────────┘
4.2 地址转换机制
页表的作用:
- 虚拟地址 → 物理地址的映射
- 内存访问权限控制
- 实现内存共享和写时复制
5. RISC-V架构简介
https://eseo-tech.github.io/emulsiV/
5.1 RISC-V设计哲学
- 精简指令集(RISC)架构
- 模块化设计:基础指令集 + 标准扩展
- 开源架构,无授权费用
5.2 主要特性
# RISC-V汇编示例
# 寄存器命名:x0-x31,其中:
# x0: 零寄存器(恒为0)
# x1: 返回地址(ra)
# x2: 栈指针(sp)
# x8: 帧指针(fp)
add x5, x6, x7 # x5 = x6 + x7
ld x10, 0(x11) # 从内存加载
sd x12, 8(x13) # 存储到内存
6.ELF:Executable and Linkable Format
6.1 链接与加载过程
符号(Symbol)概念:
- 函数名、全局变量等对外接口
- 链接器通过符号解析解决依赖关系
- 局部变量不是符号(作用域限制)
如何编译?
链接:把多个文件组合起来—合并什么?
合并依赖!
A需要B,B需要C
SYMBOL:对外的接口、全局变量、数据都属于symbol,symbol就是让外界能用的你提供的东西
在C语言中,局部变量不是symbol,而函数和全局变量是,因为他们是“稳定可靠”的
symbol是一种中间状态!人—编译器—CPU
ELF(可执行、可链接的格式)文件主要段:
.text段:可执行代码 .data段:已初始化数据 .bss段:未初始化数据(节省磁盘空间)
better save space —未初始化的全局变量:我只需要存储有哪些全局变量,不需要给它赋值,节省了很多空间
.rodata段:只读数据 .strtab段:字符串表,存放字符串字面值 .symtab段:符号表
6.2 段(Sections)与节(Segments)
关键区别:
- 段(Sections):链接视图,供链接器使用,only relevant at runtime
- 节(Segments):执行视图,供加载器使用,only relevant at link time
把sections理解成库,把segments理解成执行视图,会真的被加载到内存中
6.3 加载过程详解
三步加载过程:
- 解析头部
- 读取ELF头验证文件格式
- 检查架构兼容性
- 内存映射
- 根据程序头表创建内存映射
- 设置适当的权限(读/写/执行)
- 执行启动
- 设置入口点
- 传递环境变量和参数
- 开始执行程序
示例加载代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 简化的加载器逻辑
void load_elf(Elf32_Ehdr *ehdr) {
// 1. 验证ELF魔数
if (ehdr->e_ident[EI_MAG0] != ELFMAG0 ||
ehdr->e_ident[EI_MAG1] != ELFMAG1 ||
ehdr->e_ident[EI_MAG2] != ELFMAG2 ||
ehdr->e_ident[EI_MAG3] != ELFMAG3) {
return; // 无效的ELF文件
}
// 2. 遍历程序头表并建立映射
Elf32_Phdr *phdr = (Elf32_Phdr *)((char *)ehdr + ehdr->e_phoff);
for (int i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD) {
// 映射到内存
map_segment(&phdr[i]);
}
}
// 3. 跳转到入口点
jump_to_entry(ehdr->e_entry);
}
7. 实际应用与调试技巧
7.1 内存问题调试
常见内存错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 栈溢出
void stack_overflow() {
int huge_array[1000000]; // 可能栈溢出
}
// 2. 内存泄漏
void memory_leak() {
char *buffer = malloc(1024);
// 忘记调用 free(buffer);
}
// 3. 悬空指针
void dangling_pointer() {
char *ptr = malloc(100);
free(ptr);
// ptr现在成为悬空指针
// strcpy(ptr, "dangerous"); // 未定义行为
}
调试工具:
gdb:调试器valgrind:内存检查工具strace:系统调用跟踪
7.2 性能优化建议
- 利用局部性原理
- 减少动态内存分配
- 合理使用栈和堆
- 注意缓存友好性






