p8:并发bug和应对(死锁/数据竞争/原子性违反/防御性违反和动态分析)

防御性编程:对输入、状态和返回值进行检查,提前处理异常情况,提高程序的健壮性和可靠性。

死锁(Deadlock):

定义:多个线程(或进程)互相等待对方释放资源,导致所有线程都无法继续执行。

产生死锁的四个必要条件:

① 互斥 ② 占有并等待 ③ 不可剥夺 ④ 循环等待

Lock Ordering(锁顺序)---一种避免死锁的方法:

核心思想:所有线程必须按照统一的顺序获取锁

并发bug:数据竞争Data Race(不上锁会带来的一种问题)

定义:多个线程同时访问同一个共享变量,且至少有一个线程进行写操作,没有使用锁或其他同步机制进行保护。

Lockdep

定义:是 Linux 内核的运行时死锁检测工具。它记录程序执行过程中观察到的锁获取顺序,并构建锁依赖有向图。若发现锁依赖图中存在环,例如 x → yy → x,则说明不同线程可能以相反顺序获取锁,从而产生循环等待,Lockdep 会报告潜在死锁。

ThreadSanitizer

定义:是运行时数据竞争检测工具。它根据同一线程中的程序顺序以及线程间的 release-acquire 同步关系,建立 happens-before 关系图。对于不同线程中访问同一内存位置且至少有一次写操作的两个事件 xy,如果无法证明 x happens-before yy happens-before x,则认为两次访问并发发生,并报告数据竞争。

Sanitizers(运行时动态分析工具)

工具 主要检测内容
AddressSanitizer(ASan) 越界、释放后使用、重复释放等非法内存访问
ThreadSanitizer(TSan) 多线程数据竞争
MemorySanitizer(MSan) 读取未初始化的数据
UBSanitizer(UBSan) 整数溢出、错误对齐、除零等未定义行为

注:上述工具通常是在 GCC 或 Clang 编译时手动加入对应选项,编译器不会默认开启。

Canary检测栈空间越界代码如下:

核心思想:在栈空间的上下边界放置一段固定的特殊值,程序运行一段时间后,再检查这些位置:

  • 如果值没有变化,说明没有越界写;
  • 如果值被修改,说明程序写出了合法栈空间。

p9:操作系统的状态机模型 (操作系统的加载; thread-os 代码讲解)

  在裸机系统中,CPU 复位后会进入硬件规定的初始状态,程序计数器 PC 指向固定的 ROM 地址。ROM 中保存厂商提供的固件,固件负责初始化基本硬件,并从 Flash、SD 卡等存储介质中将二级加载器、操作系统或用户程序加载到 RAM,最后修改 PC 并跳转执行。整个启动过程依赖硬件与软件之间预先规定的地址、文件格式和调用约定。

Firmware:BIOS和UEFI

定义:都属于计算机主板中的Firmware(固件),复杂在操作系统启动前初始化硬件并加载启动程序。

1.BIOS(基本输入输出系统)是传统PC使用的启动固件。

传统 BIOS 通常读取磁盘第一个扇区(512字节),也就是 MBR,然后把其中的启动代码加载到物理内存(7c00)并执行。

电脑上电后,BIOS主要完成: 


CPU 复位
  ↓
执行 BIOS
  ↓
检测并初始化内存、键盘、磁盘等硬件
  ↓
按照启动顺序寻找启动设备
  ↓
读取磁盘启动扇区
  ↓
跳转执行 Bootloader
  ↓
Bootloader 加载操作系统


BIOS特点:

  • 启动方式较老;
  • 通常配合 MBR 分区表;
  • 启动代码空间有限;
  • 主要工作在早期 x86 实模式环境;
  • 界面通常是键盘操作的文字界面;
  • 对大容量磁盘和现代安全机制支持较弱

2.UEFI(统一可扩展固件接口)

  它不再只是从磁盘第一个扇区读取一小段代码,而是可以直接识别磁盘中的 EFI System Partition,ESP 分区,然后从其中加载 .efi 启动程序。

  它在 CPU 复位后完成硬件初始化,并通过统一的 EFI API 和设备驱动框架向启动程序提供磁盘、文件系统、图形和网络等服务。


现代UEFI PC的典型流程是:

CPU 上电或复位
    ↓
执行 UEFI Firmware
    ↓
初始化 CPU、内存和基本设备
    ↓
加载 UEFI 驱动
    ↓
UEFI Boot Manager 查看启动项
    ↓
读取 GPT 磁盘中的 ESP 分区
    ↓
加载 .efi 启动程序
    ↓
EFI 启动程序加载操作系统内核
    ↓
操作系统接管硬件


操作系统启动的一般流程:

上电/复位 Reset
    ↓
执行固件 Firmware
    ├─ 传统机器:BIOS
    └─ 现代机器:UEFI
    ↓
固件完成硬件初始化并选择启动设备
    ↓
加载 Bootloader
    ↓
Bootloader 加载操作系统内核
    ↓
内核初始化并启动用户空间程序


Makefile的一些基本概念:

1、定义:用来描述“项目如何编译”的配置文件,是make工具所读取的配置文件。

最基本的格式为:

目标文件: 依赖文件
        执行命令

2、Makefile中常用的自动变量:

自动变量 含义
$@ 当前目标文件
$< 第一个依赖文件
$^ 所有依赖文件

示例代码:

main: main.o utils.o
    $(CC) -o $@ $^

P10:状态机模型的应用

Profiler(性能分析器)是一种用来查找程序性能瓶颈的工具

Profiler通常分析以下指标:

  • CPU时间:各函数占用了多少处理器时间。
  • 调用次数:函数执行了多少次。
  • 调用关系:哪个函数调用了哪个函数。
  • 内存使用:在哪里申请内存,是否存在泄漏或频繁分配。
  • 缓存未命中:CPU是否经常需要访问较慢的内存。
  • 线程状态:线程是否长时间阻塞或等待锁。

常见工具包括:

  • Linux perf
  • gprof
  • Valgrind的Callgrind
  • Visual Studio Profiler
  • Java VisualVM
  • Python cProfile

P11:操作系统上的进程 (最小 Linux; fork, execve 和 exit) 

最小Linux:

整个操作系统世界都是由系统调用(进程、内存、文件、网络...)创建出来的。

这个最小Linux实际上就是“Linux内核 + 内存中的BusyBox文件系统 + 第一个用户程序 /init”,不需要真实硬盘,也能在QEMU中启动并进入Shell。

注:强调“静态链接”是因为静态程序不依赖相应动态链接器和共享库。


initramfs:启动时使用的内存文件系统,它只包含两个文件如下:

.
├── bin
│   └── busybox
└── init

其中busybox 是一个集成了许多常用Linux命令的小型程序,例如:

ls
cd
cat
echo
mkdir
sh


fork()

所在进程 返回值
父进程 子进程的PID(大于0)
子进程 0
创建失败 -1,仅父进程继续运行

根据返回值的不同,在两个进程中分别执行不同的代码。

execve()

重置一个状态机,重置成某一个程序的初始状态。(也就是替换当前进程中的程序)

  先通过 fork() 创建一个新进程,再让子进程通过 execve() 执行另一个程序;execve() 成功后 PID 不变,但原程序的代码和内存被新程序完全替换。

exit()

exit()------也就是清理后退出
高层、负责清理:
执行 atexit,刷新 stdio,结束整个进程。

_exit()------也就是整个进程立即退出
低层、快速退出:
不执行 atexit,不刷新 stdio,通常结束整个进程。

syscall(SYS_exit)---------也就是当前线程立即退出
原始 Linux 系统调用:
不清理用户态资源,只结束当前线程。

P12:进程的地址空间 (pmap; vdso; mmap; 游戏修改器/外挂)

进程的地址空间由若干连续的"段“组成。

地址空间: 一个进程能够使用的全部虚拟地址范围。

vdso:非陷入系统调用

作用: Linux 内核将一小段可执行代码映射到用户进程地址空间, 让部分系统服务在用户态直接完成,避免陷入内核。

优点: 减少用户态/内核态切换; 避免 syscall 指令开销; 适合高频时间查询。

进程地址空间:mmap

mmap 就是在进程的虚拟地址空间中划出一段区域,并规定这段虚拟地址应该对应文件、匿名内存或共享内存。

它主要有三种用途:

  • 将文件映射到内存,之后可以像操作数组一样读写文件;
  • 申请匿名内存;
  • 创建进程间共享内存。

突破进程隔离:

  1. 进程隔离
    Linux 中每个进程都有独立的虚拟地址空间,正常情况下一个进程不能直接访问另一个进程的内存,这是操作系统保护进程安全的机制。

  2. 获取目标进程 PID
    可以使用 pstop 等命令找到目标进程的 PID,后续通过 PID 定位目标进程。

  3. 查看进程内存布局
    /proc/[PID]/mapspmap PID 可以查看目标进程的内存区域,例如代码段、数据段、堆(heap)和栈(stack),从而确定可能存放目标数据的区域。

  4. 访问目标进程内存
    /proc/[PID]/mem 可以映射目标进程的虚拟内存;也可以使用 ptrace 机制,在具备相应权限的情况下读取或修改目标进程的内存。gdb 等调试工具底层也依赖 ptrace

  5. 内存扫描定位数据
    如果不知道变量地址,可以先搜索已知数值,例如先搜索 2000,改变程序中的数值为 1700 后再次搜索,通过不断缩小候选范围找到目标地址。

  6. 核心流程
    获取 PID → 查看内存布局 → 获得访问权限 → 扫描/定位目标数据 → 读取或修改内存 → 验证结果。

P13:C标准库的实现(系统调用的封装,内存空间管理)

  系统调用虽然是操作系统提供的底层接口,但直接使用它比较麻烦,所以通常会提供更高层的 API 对系统调用进行封装。

高级 API
   ↓
库函数封装
   ↓
系统调用
   ↓
操作系统内核
   ↓
硬件


P15:fork的应用(文件描述符的复制、写时复制、创建平行宇宙的魔法)

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐