高并发内存池 - 申请内存过程联调到此我们已经将内存池中 “申请内存” 功能的逻辑实现的七七八八了接下来我们将设计一个测试函数看看目前的功能是否能跑通。UintTest.cpp#includeComm.h#includeConcurrentAlloc.hvoidAlloc1(){void*ptr1ConcurrentAlloc(1);void*ptr2ConcurrentAlloc(2);void*ptr3ConcurrentAlloc(3);void*ptr4ConcurrentAlloc(4);void*ptr5ConcurrentAlloc(5);void*ptr6ConcurrentAlloc(6);void*ptr7ConcurrentAlloc(7);void*ptr8ConcurrentAlloc(8);}voidAlloc2(){for(inti0;i5;i)void*ptrConcurrentAlloc(7);}intmain(){Alloc1();Alloc2();std::cout第一阶段测试成功std::endl;return0;}下面讲讲我在此次测试中其中出现的 BUG以及错误原因是什么如何调试的1. 野指针问题在 Page Cache 的实现中出现了野指针问题Span*spanN_spanLists[i].PopFront();Span*spanK;//踩的第一个坑没有申请空间野指针直接使用spanK-_pageIdspanN-_pageId;spanK-_nk;spanN-_pageIdk;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 (main24) | 旧帧指针 (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(sizeMAX_BYTES);size_t alignsizeSizeClass::RoundUp(size);size_t indexSizeClass::Index(size);if(!_freeList[index].Empty()){return_freeList[index].Pop();}else{//从中心缓存获取数据returnFetchFromCentralCache(index,size);//踩坑一开始传入的不是alignsize导致读出了垃圾值}}所以出现了下面的报错情况:这次产生的是段错误这里有了上面的经验我开始使用 lldb 调试。这里出现了调试信息代表可能并非野指针问题但是这里的地址值很有趣 0x807060504030201是一个递减的数列我的直觉告诉我程序崩溃的秘密就藏在这串地址中于是我去询问了 AI 这串地址的具体含义这种地址叫做内存垃圾值。下面我们讲解一下什么叫做内存垃圾值内存中的垃圾值不是一团乱码它们有明确的来源和可识别的模式来源典型值64位含义识别特征未初始化栈变量0xcdcdcdcdcdcdcdcdMSVC/clang Debug 填充连续CD已释放堆内存0xddddddddddddddddMSVC “Dead Memory”连续DD已释放栈内存0xccccccccccccccccMSVC 栈释放标记连续CCmmap 新分配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 用 size2 构建自由链表startsize;// start 0x100000002void*tailnewSpan-_freelist;// tail 0x100000000while(startend){ObjNext(tail)start;// 向 tail 写入 8 字节指针tailObjNext(tail);// 从 tail 读取 8 字节指针startsize;// start 2}第一次循环tail 0x100000000ObjNext(tail) 0x100000002向地址 0x100000000 写入 8 字节小端序下内存变成02 00 00 00 01 00 00 00tail ObjNext(tail)读取出来是 0x100000002第二次循环tail 0x100000002ObjNext(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 来读取时endstart;// 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不过都是些很简单的错误没有什么有趣的现象和值得深度挖掘的东西就不在这里讲述了我们下篇文章再见。