高并发内存池 - 申请内存过程联调
到此我们已经将内存池中 “申请内存” 功能的逻辑实现的七七八八了,接下来我们将设计一个测试函数,看看目前的功能是否能跑通。
UintTest.cpp
#include"Comm.h"#include"ConcurrentAlloc.h"voidAlloc1(){void*ptr1=ConcurrentAlloc(1);void*ptr2=ConcurrentAlloc(2);void*ptr3=ConcurrentAlloc(3);void*ptr4=ConcurrentAlloc(4);void*ptr5=ConcurrentAlloc(5);void*ptr6=ConcurrentAlloc(6);void*ptr7=ConcurrentAlloc(7);void*ptr8=ConcurrentAlloc(8);}voidAlloc2(){for(inti=0;i<5;i++)void*ptr=ConcurrentAlloc(7);}intmain(){Alloc1();Alloc2();std::cout<<"第一阶段测试成功"<<std::endl;return0;}下面讲讲我在此次测试中,其中出现的 BUG,以及错误原因是什么,如何调试的?
1. 野指针问题
在 Page Cache 的实现中,出现了野指针问题:
Span*spanN=_spanLists[i].PopFront();Span*spanK;//踩的第一个坑,没有申请空间,野指针直接使用spanK->_pageId=spanN->_pageId;spanK->_n=k;spanN->_pageId+=k;spanN->_n-=k;_spanLists[spanN->_n].PushFront(spanN);//踩的第三个坑,把 spanK 挂上去了returnspanK;可以看到我在Span* spanK后并没有 new 创建一个对象,这导致了野指针问题,程序直接发生了崩溃。
对于 bus error 的错误我是第一次遇见,在此之前的 BUG 基本靠着调试都可以找到崩溃那一步,从而去向上推演,但这次不一样,一步一步调试无法在给我任何信息。
并没有跳到崩溃的那一步去,所以一步一步调试已经无法解决这个问题,在询问过 AI 后,我开始使用 lldb 去解决这个问题。
此时使用 run 指令,观察指针就会发现,指针地址为 0x0000000000000001,很明显出现了野指针问题,0x0000000000000001 指针不会是正常开辟出来的,因为实在是太小了,直接指向核心区低地址,所以一定是指针没有初始化,造成了野指针问题,我开始一步一步向上排查。
在我尝试 bt 后,发现并没有本应呈现的调试信息并没有展现,甚至是在我编译已经加了 -g 的情况下,此时说明栈已经被彻底破坏了,到此基本就可以判定一定是野指针问题,指针未被初始化就直接使用。
但是本次调试最难的点就在于栈已经被破坏。这意味着无法再输出更多的调试信息,
栈帧的结构为这样:
高地址 +------------------+ | 返回地址 (RA) | ← 函数执行完后,CPU 要跳回哪里继续执行 | 旧帧指针 (FP) | ← 上一个函数的栈帧起点,用于回溯调用链 | 局部变量 | ← 你定义的数组、结构体、临时变量 | 参数区 | ← 传给下一个函数的参数 +------------------+ 低地址 ← sp (栈指针)当代码中出现缓冲区溢出、野指针写栈、链表越界写时,写入的数据可能冲破了局部变量的边界,直接覆盖了返回地址或帧指针:
+------------------+ | 返回地址 (RA) | ← 原本存的是 0x100005678 (main+24) | 旧帧指针 (FP) | ← 原本存的是 0x16fdfd000 +------------------+ | 局部变量 buf[8] | ← 你的代码写了 buf[10] = 0x1 +------------------+buf[10] 写到了返回地址的位置,把 0x100005678 改成了 0x1。
当函数执行到 return 时,CPU 从栈上取出返回地址,准备跳回去。但它取到的是被覆盖后的 0x1,于是:
- CPU 试图在地址 0x1 执行指令
- 0x1 不是有效的代码地址,触发 EXC_BAD_ACCESS
- 调试器问栈:“你是谁调用的?” 栈说:“我不知道,我的帧指针和返回地址都被人改了。”
- 于是 bt 只能显示当前这一帧 0x1,上面一片空白
这就是 “栈被彻底破坏”:不是数据错了,而是"现场记录"本身被销毁了。
所以这也是这个 BUG 最难排查的点,不过我尝试从 Page Cache 层向上排查定义指针变量的位置,然后逐一寻找,最后还是锁定了出错位置,解决了这个问题。
当然这其中有很多的调试工具可以使用例如 ASan、watchpoint 等,但我对这些了解也不是深,所以没有办法讲解如何依靠这些工具进行调试,后续的学习中我会将这方面能力补上。
2. 内存垃圾值问题
在 Thread Cache 中,我手滑将没有调整的空间字节大小传入,造成了第二个很麻烦的问题。
void*ThreadCache::Allocate(size_t size){assert(size<MAX_BYTES);size_t alignsize=SizeClass::RoundUp(size);size_t index=SizeClass::Index(size);if(!_freeList[index].Empty()){return_freeList[index].Pop();}else{//从中心缓存获取数据returnFetchFromCentralCache(index,size);//踩坑,一开始传入的不是alignsize,导致读出了垃圾值}}所以出现了下面的报错情况:
这次产生的是段错误,这里有了上面的经验,我开始使用 lldb 调试。
这里出现了调试信息,代表可能并非野指针问题,但是这里的地址值很有趣 0x807060504030201,是一个递减的数列,我的直觉告诉我,程序崩溃的秘密就藏在这串地址中,于是我去询问了 AI 这串地址的具体含义,这种地址叫做内存垃圾值。
下面我们讲解一下什么叫做内存垃圾值:
内存中的垃圾值不是"一团乱码",它们有明确的来源和可识别的模式:
| 来源 | 典型值(64位) | 含义 | 识别特征 |
|---|---|---|---|
| 未初始化栈变量 | 0xcdcdcdcdcdcdcdcd | MSVC/clang Debug 填充 | 连续CD |
| 已释放堆内存 | 0xdddddddddddddddd | MSVC “Dead Memory” | 连续DD |
| 已释放栈内存 | 0xcccccccccccccccc | MSVC 栈释放标记 | 连续CC |
| mmap 新分配 | 0x0000000000000000 | 操作系统清零 | 全零 |
| 越界读取拼接 | 0x807060504030201 | 你遇到的! | 有规律但不统一 |
| 安全金丝雀 | 0xfd7a3e9c2b1d405f | 栈保护随机值 | 完全随机 |
关键洞察:0x807060504030201 属于"越界读取拼接值"——它不像 CDCDCDCD 那样来自编译器填充,而是代码自己把相邻内存的数据块拼成了一个 64 位整数。
假设 mmap 分配了一块 8KB 的内存,起始地址是 0x100000000(简化假设)。系统出于安全考虑,会把匿名映射内存清零,所以初始状态是:
地址 内存内容 0x100000000: 00 00 00 00 00 00 00 00 0x100000008: 00 00 00 00 00 00 00 00 0x100000010: 00 00 00 00 00 00 00 00 ...现在 GetOneSpan 用 size=2 构建自由链表:
start+=size;// start = 0x100000002void*tail=newSpan->_freelist;// tail = 0x100000000while(start<end){ObjNext(tail)=start;// 向 tail 写入 8 字节指针!tail=ObjNext(tail);// 从 tail 读取 8 字节指针!start+=size;// start += 2}第一次循环:
- tail = 0x100000000
- ObjNext(tail) = 0x100000002:向地址 0x100000000 写入 8 字节
- 小端序下,内存变成:02 00 00 00 01 00 00 00
- tail = ObjNext(tail):读取出来是 0x100000002
第二次循环:
- tail = 0x100000002
- ObjNext(tail) = 0x100000004:向地址 0x100000002 写入 8 字节
- 这会覆盖 0x100000002 到 0x100000009 的内存!
灾难发生:
第一次写入在 0x100000000 的数据是:
地址: 0 1 2 3 4 5 6 7 内存: [02 00 00 00 01 00 00 00]第二次写入在 0x100000002 覆盖后:
地址: 0 1 2 3 4 5 6 7 内存: [02 00 04 00 00 00 01 00] // ↑ 从这里开始被覆盖!链表已经彻底混乱了。 每个"节点"只有 2 字节,但写入是 8 字节,导致每个写入都在覆盖前面节点的数据。
当 FetchRangeObj 来读取时:
end=start;// end = 0x100000000ObjNext(end);// 从 0x100000000 读取 8 字节此时 0x100000000 处的内存已经被多次覆盖,可能变成了:
内存字节: 01 20 30 40 50 60 70 80在小端序(ARM64/macOS)下,CPU 读取这 8 个字节为一个 64 位指针时:
- 字节 0 (01) 是最低有效字节
- 字节 7 (80) 是最高有效字节
组合结果:0x807060504030201
所以就造成了地址的递减情况,幸运的是 0x807060504030201 并不指向合法地址处,所以产生了崩溃,使我们可以观察到问题,如果恰好指向合法地址处,那么恐怕会隐藏的更深。
继续调试:
现在通过 bt,我们可以观察到调用的堆栈结构,接着逐一向上排查
网上排查了一层,观察一下变量的值,逐一去查看他们是否正确,其实到这一步就已经初见端倪了,size 的值应该是 8,但是却为 2,所以排查出了是 size 的问题,最终也成功解决这个 BUG。
其实中途还解决了一些 BUG,不过都是些很简单的错误,没有什么有趣的现象和值得深度挖掘的东西,就不在这里讲述了,我们下篇文章再见👋。