资讯详情

C语言内存操作函数详解:malloc、memcpy、realloc等核心机制与避坑指南

📅 2026/10/10 7:54:57 | 华诺云谱 👁 阅读
C语言内存操作函数详解:malloc、memcpy、realloc等核心机制与避坑指南
1. 内存操作函数C语言的命门也是翻车重灾区如果你写过一段时间的C代码肯定绕不开内存操作函数malloc、free、memcpy、memset、memmove、calloc、realloc。我接下来要聊的就是这份C内存操作函数详解。这篇内容不打算按手册挨个念而是结合调试过的崩溃现场把每个函数真实的行为、参数含义和隐藏风险一次讲透。刚学完指针准备写真实项目的学生或者被线上偶发故障折磨过一段时间的在职开发者都可以拿它当一份备查笔记。内存操作函数解决的问题很纯粹在程序运行期间向操作系统要一块动态内存、在内存块之间搬数据、把内存里的内容初始化为指定字节。它们跟普通业务函数最大的区别是直接操作地址和字节没有类型系统兜底也没有运行时越界检查。你唯一可靠的依据就是自己传入的size参数和内心对内存布局的理解。所以我在标题里特意用了“详解”。因为网上那种把每个函数签名抄一遍的文档没法帮你写出正确的代码。真正的难点在于为什么realloc不能直接给原指针赋值为什么memcpy和memmove结果可能不同为什么memset一个int数组为1会得到奇怪的值这些问题的答案藏在函数背后的实现语义和内存本身的特性里。下面我逐个拆开讲。1.1 这些函数到底管什么按功能分C内存操作函数其实只有三大类。一类是动态内存管理malloc、calloc、realloc、free负责从堆上申请和归还内存一类是字节级搬运与填充memcpy、memmove、memset、memcmp负责在已存在的内存区间之间复制、设置和比较数据还有一类是容易被人忽略的缓冲区安全函数比如snprintf、strncpy等严格说它们是字符串函数但核心参数同样是内存地址和字节数而且经常和内存操作混在一起用。第一类是基本功一个malloc对应一个freerealloc可能搬家容错要谨慎。第二类是细节重灾区参数单位、重叠区间、字节序、对齐每一项都能单独写一篇。第三类则是第一类和第二类混合使用时的边界保护很多越界崩溃都是因为它们的安全语义没搞明白。从使用频率上来说一个中型C项目中memcpy和memset几乎无处不在malloc/free也随处可见。正因为太常见反而容易被写代码的人当成“理所当然”。我见过太多因为少写一个字节、多传一个0、或者把sizeof(指针)当成sizeof(数组)造成的线上事故。理解这些函数管什么只是第一步真正重要的是理解它们的限制。1.2 为什么一个字节的错就能摧毁整个进程你可能好奇为什么要花这么大力气去抠一个函数看看Java、Go这些带自动内存管理的语言程序员根本不需要关心这些。但对于C语言来说内存操作函数的危险性恰恰在于“没有保护”。想象你有一块连续的内存被分成若干个大小不一的区块堆管理器通过簿记信息记录每个区块是否空闲、大小多少。这时你在某个区块里写多了一个字节这个字节恰好越过边界写进了相邻区块的簿记区域。下次free这个相邻区块时堆管理器按已经被改坏的簿记信息去回收轻则内存泄漏重则直接段错误。更可怕的是这种越界往往不会在写入时立刻崩溃而是延迟到几毫秒甚至几秒后的另一个完全无关的函数里。这也是为什么很多人觉得C的内存问题“诡异”。如果你不熟悉底层你会以为是业务逻辑出了错实际上只是某个memcpy的size计算多算了一格。所以我才会在这篇详解里反复强调单位、边界和状态检查。下面从最常用的动态内存函数开始。2. 动态内存三件套malloc、calloc、realloc的细节与误区这一章主要讲堆上的三个函数。先给个对比表格然后逐节展开。函数原型是否清零主要风险mallocvoid *malloc(size_t size)否size计算错误、未检查NULLcallocvoid *calloc(size_t nmemb, size_t size)是大块清零性能差reallocvoid *realloc(void *ptr, size_t size)否返回值忽略、旧指针失效2.1 malloc只负责分配不负责初始化malloc申请到的内存内容是“不确定”的。一些刚由操作系统映射进来的页面可能是全零但在堆里反复分配释放过的区域残留数据可能来自上一次的对象。所以不要依赖malloc出来的内存默认是0更不要直接读没初始化的字段。正确做法是malloc之后立即memset或者用calloc。malloc的size参数是字节数不是元素个数。最常见的错误是int *p malloc(sizeof(p));。p是一个指向int的指针sizeof(p)在64位系统上是8字节你只分配了8字节空间。如果你要100个int正确写法是malloc(sizeof(int) * 100)保险写法是malloc(sizeof(*p) * 100)这样以后即使p变成别的类型也不用改这个分配表达式。还需要记住malloc不是必然成功。在堆空间耗尽、虚拟内存受限或单次请求过大时返回NULL。最好在每次malloc之后检查返回值。许多嵌入式新手觉得“板上内存那么大怎么会失败”直到某个模块一次性申请了一个巨大的连续数组才发现内存碎片化已经让分配器无块可分。检查NULL的代价很低不检查的代价可能是一次难以定位的段错误。2.2 calloc清零是真的零但性能有代价calloc与malloc的核心差异是清零。内部大体等价于malloc后memset整块为0同时它要求传入两个参数元素个数和每个元素的大小。分配器会先检查两者相乘是否会溢出size_t溢出则返回NULL并置errno。这个溢出检查是calloc独有的安全特性所以在计算大数组时优先选calloc而不是malloc(n * size)后者可能在乘法时溢出。清零在某些场景非常重要比如你定义了一个结构体里面有几个字段需要在初始化时是0、NULL或false。calloc一次搞定省得手工逐字段赋值。代价是它需要把整块内存从头到尾写一遍。分配几十MB时这个清零耗时可能明显如果你只是想保留一个临时大缓存之后马上会用memcpy覆盖那calloc的额外开销就是浪费。另外别把“清零”理解成“初始化成某种神奇状态”。在主流平台上NULL指针的位模式通常是全0所以用calloc初始化结构体后指针类型字段等于NULL是成立的。但如果你写的是跨全平台的代码标准并不保证0位模式就是NULL指针不过这在现代通用平台上几乎不会遇到。2.3 realloc扩容的隐藏行为别当指针搬运工realloc的作用是在不丢失原有数据的前提下调整一块内存的大小。它的内部行为有两种可能原地扩容或者移动到新区域并释放旧块。作为调用者你无法预测是哪一种所以必须假设返回的指针可能变化。最常见的错误写法是p realloc(p, new_size);。如果realloc失败它会返回NULL但原来那块内存仍然有效不会被自动释放。你把NULL赋给p之后原来的地址就丢了既没法继续使用也没法free直接造成内存泄漏。正确做法是void *tmp realloc(p, new_size); if (tmp ! NULL) { p tmp; } else { // p 仍然指向旧内存可以做错误处理 }第二个坑是realloc之后继续用旧指针。即使realloc成功若它返回了新地址旧地址就已被释放。继续读旧地址是未定义行为可能在特殊情况下还能读到数据但那是运气。所以永远用realloc的返回值更新你的指针变量。第三个容易混淆的点realloc的第一参数可以是NULL。此时完全等价于malloc(size)。这给了你一个方便的技巧写一个分配/扩容函数时统一用realloc(NULL, size)实现首次分配。但同时也要小心realloc(p, 0)的行为是实现定义的某些平台等效于free某些平台只是分配0字节不要依赖这种写法。2.4 free之后悬空指针与重复释放free把内存还给堆管理器但指针变量中的地址值并不会被自动清空。这个残留的指针就是悬空指针。你在free后再访问它读到的可能是已经改写的堆数据也可能直接段错误。更隐蔽的是如果这个地址又被重新分配给了别的对象你通过旧指针修改数据会把另一个毫无关系的对象改坏。这种bug最难排查。解决习惯很简单free之后立刻将指针置为NULL。free(p); p NULL;。这能避免大部分悬空访问。但要注意如果同一个地址被保存在多个变量中你只清空了一个其他变量仍是悬空指针。所以在设计上要明确“谁分配谁释放”不要让多个模块争夺同一个内存的所有权。重复释放是指连续free两次同一地址或称double free。标准规定这是未定义行为常见的堆管理器会检测到并报告“double free or corruption”但也有可能直接破坏堆管理结构。与其依赖运行时的报错不如在编码时杜绝释放后置NULL并且在释放前检查指针是否非NULL。虽然free(NULL)本身合法但如果你不知道指针是否已经释放过置NULL这个习惯依然是最简单的护身符。3. 内存块搬运与填充memcpy、memmove、memset、memcmp动态内存管好之后接下来就是已有内存区间的操作了。这四个函数的共同特点是它们的操作对象是“字节流”不关心这些字节代表什么类型。这既是优势也是陷阱。3.1 memcpy 效率高但怕重叠memcpy(dest, src, n)从src地址复制n个字节到dest地址。它不做任何重叠检查要求两块区域不能有重叠。为什么因为很多实现的memcpy会优先使用CPU的字长比如一次复制8字节甚至用SIMD来提高吞吐同时在算法上可能从前往后逐块复制。如果dest的起始地址在src内部且靠前复制时会把还没读到的src内容覆盖掉结果就是数据错乱。实际开发中很多人以为“两个数组可能有重叠吗不会吧”。但当你操作的是同一块缓冲区中的不同区段时比如把一段日志数据从缓冲区的第100字节搬到第80字节memcpy就可能出问题。这种场景在协议解析、环形缓冲区、滑动窗口里非常常见。size参数同样是字节数。memcpy(dest, src, n * sizeof(int))是拷贝n个intmemcpy(dest, src, n)只拷贝n个字节。我见过有人以元素个数填进去导致拷贝数据量只有预期的一半甚至四分之一。建议在代码中明确写出sizeof比如memcpy(dest, src, count * sizeof(*src))让后续维护者一眼看懂。如果无法确定是否重叠不要心存侥幸直接用memmove。memcpy省下来的那点性能远不足以抵消一次数据错乱带来的调试成本。3.2 memmove 怎么处理重叠代价是什么memmove的语义是安全的即使dest和src重叠它也保证最终结果正确。C标准没有规定具体实现只是要求行为等效于先把src整体复制到临时缓冲再把临时缓冲复制到dest。但直接这么做会多一次内存分配和额外的两次复制性能太差。实际库通常会用更聪明的办法根据dest和src的相对位置选择复制方向。如果dest在src左边dest src意味着我们从src向低地址方向拷贝从前往后是安全的。如果dest在src右边dest src从前往后复制会覆盖src还没被读取的尾部这时应该从src的末尾开始从后往前复制。于是memmove只需要做一个地址比较再决定正向还是反向循环。因此它比memcpy通常慢一点点但在可接受范围内。一个容易忽略的点是dest和src完全相同时memmove也安全什么也不改变。有人会在拷贝前判断if (dest ! src)但对同一块内存的重叠来说这没必要。memmove的正确性不值得你用if去优化。在日常项目中面对不确定是否重叠的场景选memmove这几乎是我唯一的建议。只有在确定不重叠、并且已经通过profiler确认memcpy是热点时才值得降级回memcpy。3.3 memset 的字节级填充与奇怪的“0x01”memset(p, value, n)把p开始的n个字节都设置为value的低8位。无论value传什么最终落到每个字节上的值都是(unsigned char)value。这就是“奇怪的0x01”问题的根源。比如你想初始化一个int数组为1int arr[8]; memset(arr, 1, sizeof(arr));你以为arr每个元素都是1实际每个int的4个字节都是0x01于是在小端机器上int值是0x01010101即16843009。这往往不是你想要的结果。那如何初始化int数组为1只能写循环或者用编译器的指定初始化器扩展。memset只适合把每个字节设成相同模式的初始化例如全0、全0xFF。全0xFF也很有用在主流二进制补码平台上int的-1在所有位上是1所以memset(arr, 0xFF, sizeof(arr))可以把int数组初始化为-1。如果你只想初始化第一个元素别用memset直接arr[0] -1。memset的另一个常见错误是size参数写成sizeof(指针)。例如char buf[256]; memset(buf, 0, sizeof(buf)); // 正确256 char *pbuf buf; memset(pbuf, 0, sizeof(pbuf)); // 错误只有8字节这提醒我们当缓冲区退化成指针后数组大小信息就丢失了。要么保留原来的size变量要么使用宏定义的长度。3.4 memcmp 比较内存别误当字符串memcmp(p1, p2, n)逐字节比较两个内存区间的前n个字节。返回值小于0表示p1首字节小于p2等于0表示n个字节全部相等大于0相反。由于它是按字节比较而且必须指定长度它比较的是“字节序列”不是字符串语义。字符串比较应该用strcmp、strncmp。需要注意memcmp不会因为遇到\0就停止所以它常被用来比较包含二进制数据的结构体。比如比较两个网络协议报文是否相同或者比较一段加密后的哈希值。这里size必须严格等于你想要比较的数据长度多一个字节或少一个字节都会改变结果。还有一个容易踩的坑memcmp在比较多字节整数时结果和数值大小没有直接关系。以小端机器为例int 1在内存中是01 00 00 00int 256是00 01 00 00memcmp比较前4字节时0x01 0x00会得出1大于256。所以memcmp适合判断“字节完全相同”不适合做数值比较。想比大小就老老实实用运算符。4. 对齐与字节序内存操作函数绕不开的两个底层概念前面的内容都在讲函数本身但实践中有两个底层概念几乎躲不开内存对齐和字节序。不搞清楚这两个你写的memcpy、memset、memcmp可能在特定平台或网络场景下悄悄出错。4.1 内存对齐struct padding 与 memcpy 拷贝结构体结构体在内存里不一定是成员紧挨着排列的。为了访问性能编译器会在结构体成员之间插入padding使得每个成员的首地址满足其自然对齐要求。常见的规则是int成员需要4字节对齐double需要8字节对齐。一个struct { char c; int i; }sizeof很可能等于8而不是5中间有3个padding字节。这对内存函数意味着什么如果你用memcpy把结构体当整体复制memcpy(dst_struct, src_struct, sizeof(src_struct))是安全的让编译器把padding也一并拷贝。只要你没有对padding内容做语义假设就没问题。但如果你把结构体成员逐个赋值到某个紧凑缓冲区例如char buf[5]; memcpy(buf, src.c, 1); memcpy(buf 1, src.i, 4);你会把src.i的原始字节拷进去但双方结构体的padding不同这个紧凑buf并没有模拟出原结构体的内存布局。真正的问题往往出在反向把缓冲区里的紧凑数据用memcpy回结构体时如果不小心把buffer里的数据当作结构体直接强转再访问成员就会造成未对齐访问。跨平台协议解析中推荐显式定义协议结构并使用编译器指令取消对齐。打包后结构体大小等于成员大小之和memcpy进出的尺寸可以精确控制。代价是未对齐访问在某些处理器上会变慢甚至崩溃x86相对宽容部分ARM架构严格。所以我的建议是本地数据结构不需要打包保持自然对齐跨进程或网络传输的字节流才打包而且最好用显式序列化不要依赖强转结构体。4.2 大小端memcpy 拷贝 int 的陷阱字节序影响的是多字节整数在内存中的存放顺序。小端模式把低位字节放在低地址大端模式把高位字节放在低地址。x86和大多数ARM默认是小端有些嵌入式DSP或网络协议栈使用大端。当你用memcpy把一个int拷贝到char数组时得到的字节序取决于当前机器的硬件而不是数字的书写顺序。例如int x 0x12345678; char bytes[4]; memcpy(bytes, x, 4); printf(%02x %02x %02x %02x\n, bytes[0], bytes[1], bytes[2], bytes[3]);在小端机器上打印的是78 56 34 12大端机器是12 34 56 78。如果你在A机器上用小端顺序把数据写入文件又拿到B机器上按大端解析数据含义就会完全错乱。处理字节序的正确姿势是用htonl、ntohl、htons、ntohs这类专用于网络字节序转换的函数而不是靠memcpy手动倒序。如果项目需要自己定义格式可以写read_be32/write_be32等工具函数内部用移位和或运算完成而不是直接memcpy这样无论平台大小端行为都是确定的。4.3 通过内存函数做序列化/反序列化明白了对齐和字节序之后再来看内存操作函数在序列化中的用法。序列化的目标很清晰把内存中的结构体、整型、字符串转换成一个连续的字节流发送到网络或写入文件之后能在另一台机器或另一个时间点恢复出来。一个手工序列化的模板是定义缓冲区uint8_t buf[256]维护当前偏移off。写整数时先做字节序转换再memcpy进缓冲区写字符串时需要先写入长度通常2字节或4字节再写入字符串内容。例如uint16_t len (uint16_t)strlen(s); uint16_t be_len htons(len); memcpy(buf off, be_len, sizeof(be_len)); off sizeof(be_len); memcpy(buf off, s, len); off len;反序列化则是反向操作每次memcpy读取前要检查off 需要读取的字节数 缓冲区总大小否则就是越界读。很多协议漏洞都来自长度字段被伪造而解析代码没有校验就memcpy。这种检查不是可选项而是安全底线。如果你觉得手写序列化容易漏边界可以封装一个简单结构体比如后面第6章会给出的缓冲区结构用push_bytes和pop_bytes两个函数统一管理偏移和边界。这样所有memcpy都收在同一处出了问题只需要排查一个函数。5. 越界、泄漏、双重释放常见问题的排查实录无论函数语义背得多熟真正写代码时还是会有防不胜防的时候。这一章我记录几个自己或身边同事实际踩过的坑以及排查思路。为避免对号入座我用泛化描述。5.1 三个典型崩溃现场第一个是“多一字节和少一字节”。某个模块在解析数据包时把结构体字段长度计算错了一位。具体表现是memcpy(dst, src, item_len 1)而dst正好只能容纳item_len个字节。写入时没有立即报错直到几秒后调用free堆管理器发现簿记信息被破坏才抛出段错误。排查花了一整天。问题不在于memcpy本身而在于业务代码里没有统一使用容量常量。第二个是“用错sizeof”。一个函数接收char buffer[]作为参数在函数内部直接int len sizeof(buffer);得到的是指针大小8而不是调用方的数组长度。随后用这个len做memset或memcpy的边界大概率越界。更好的设计是同时传入缓冲区容量或者用宏在调用点计算sizeof(arr)。第三个是realloc失败丢内存。服务端在扩容连接缓冲区时写的是conn-buf realloc(conn-buf, new_size);某次分配失败后旧缓冲区指针被NULL覆盖不仅当前请求处理失败那块旧内存也永远无法释放。虽然失败概率不高但一旦发生就是内存泄漏叠加数据丢失。换用临时指针后问题消失。5.2 内存泄漏排查思路与工具手工排查泄漏核心是核对“每次分配都有对应的释放”。code review时画一张表列malloc/calloc/realloc调用点列对应的free调用点检查所有提前return路径。特别是处理错误的goto err或return NULL分支最容易漏掉在成功路径末尾才释放的临时缓冲区。如果函数持有多个资源建议统一走一个标签做清理不要在每个if里各自free。工具方面我最常用的是Valgrind和AddressSanitizer。Valgrind的memcheck在Linux下排查泄漏非常直观valgrind --leak-checkfull --show-leak-kindsall ./your_program它会报告每一处“definitely lost”和“indirectly lost”并给出分配时的调用栈。缺点是运行很慢适合在测试环境跑。ASan则是编译期插桩在运行时捕获越界、释放后使用、双重释放等问题性能开销小得多也可以集成到CI里。写C项目时Debug构建我基本总会开-fsanitizeaddress -fno-omit-frame-pointer -g在出问题的回归用例上一跑错误位置一目了然。嵌入式或没有操作系统的环境里这两类工具可能用不了。此时可以自己维护一个简单的内存记录模块在malloc时记录调用点和大小free时标记释放定期扫描未释放列表。虽然原始但配合压测场景往往也能定位到泄漏模块。5.3 新手避坑清单写代码时的三个习惯第一个习惯每次调用内存函数前先问自己“这个size到底是多少字节”。我现在的默认是用sizeof(src)或明确的容量常量杜绝魔法数字。如果单位为元素个数就写成count * sizeof(*src)让式子自己解释自己。第二个习惯释放后立刻置NULL。这大概是成本最低、收益最高的内存操作习惯。虽然它不能解决所有的悬空指针问题但能挡掉大概率事故。配合上一条每次free之后顺手写p NULL;几乎不花时间。第三个习惯接受“保守使用安全函数”。不确定是否重叠就用memmove不确定目标缓冲区是否够大就手动检查并用snprintf代替sprintf不确定分配结果就不直接p realloc(p, size)。这些保守写法平均性能损失很小但能把一堆未定义行为变成可控行为。等做过性能热点分析之后再决定哪些地方需要换成更激进的写法。6. 实战参考几段可以直接用的安全封装讲了一大堆原理和坑这章给几段能直接抄的代码。这部分内容偏实战建议你把它们放到自己的工具库里再根据项目风格微调。6.1 安全 realloc 包装先看最常用的realloc包装。核心思想是用临时变量接收返回值失败时保留原指针并可以返回错误码给上层int safe_realloc(void **ptr, size_t new_size) { void *tmp realloc(*ptr, new_size); if (tmp NULL) { return -1; } *ptr tmp; return 0; }调用示例int *data NULL; if (safe_realloc((void**)data, 100 * sizeof(int)) ! 0) { // 分配失败data保持不变如果是首次分配则仍为NULL }这个封装的用意很明显避免直接覆盖导致泄漏。如果你还需要在分配失败时区分“原指针是否有效”可以在错误分支检查new_size是否大于0或者直接让上层决定是否free原指针。还有一点realloc(NULL, size)等价于malloc(size)所以这个函数天然支持首次分配不用额外分支。6.2 用内存函数实现一个简单的字节缓冲区一个动态字节缓冲区在协议解析、日志拼接、消息格式化时很常用。我会写一个最小版本只保留增长的append和读取typedef struct { unsigned char *data; size_t len; size_t cap; } ByteBuf; int buf_reserve(ByteBuf *b, size_t extra) { if (b-len extra b-cap) { size_t new_cap b-cap ? b-cap * 2 : 64; while (new_cap b-len extra) { new_cap * 2; } if (safe_realloc((void**)b-data, new_cap) ! 0) { return -1; } b-cap new_cap; } return 0; } int buf_append(ByteBuf *b, const void *src, size_t n) { if (buf_reserve(b, n) ! 0) { return -1; } memcpy(b-data b-len, src, n); b-len n; return 0; }这里有两个关键点。一是扩容用倍增策略避免频繁realloc导致O(n^2)的拷贝开销二是在每次append之前都调用buf_reserve保证有足够容量后再memcpy不会越界。这个缓冲区用完以后记得调用free(b-data)释放。你也可以在这个基础上加入buf_clear、buf_read等方法但核心的边界控制就是reserve和len/cap两个字段。6.3 手动实现 memmove 加深理解前面讲了memmove的语义下面给一个手动实现用来帮助理解方向选择的逻辑void *mymemmove(void *dest, const void *src, size_t n) { unsigned char *d dest; const unsigned char *s src; if (d s) { for (size_t i 0; i n; i) { d[i] s[i]; } } else { for (size_t i n; i 0; i--) { d[i - 1] s[i - 1]; } } return dest; }当dest地址小于src时从前往后复制当dest位于src右侧或完全重叠时从后往前复制避免覆盖还没读的源字节。注意d s这种比较只应在同一个数组或同一个缓冲区内部使用如果两块内存完全无关比较它们本身就是未定义行为。所以这个实现只适合用来理解原理真正生产环境请用标准库的memmove。自己造轮子的意义是让你明白为什么库函数能保证重叠安全。7. 最后分享几点个人经验7.1 我一直在用的三个固定习惯写这么多年C代码我慢慢养成了几个外人看起来有点啰嗦、但实际救过我好几次的习惯。第一个是“所有分配和释放必须在同一个函数层次完成”。除非有明确的ownership注释否则不把内存所有权交给别人。如果一个函数返回了malloc出来的指针那函数命名里我一定会加上_alloc、_new这类字样让调用者知道他们需要负责free。第二个是“写内存操作之前先画一张小图”。不需要多正式就是在草稿纸上画一个矩形代表缓冲区标出src起始地址、dest起始地址、长度n然后看两个区间是否重叠、是否越界。很多时候不用画图光是在脑子里把地址和字节数过一遍就能发现问题。第三个是“默认使用安全版本”。能选memmove就不选memcpy能选snprintf就不选sprintf能检查realloc返回值就不直接赋值。有人会说这样性能低但实际上绝大多数代码路径的性能瓶颈根本不在这里。等真的通过profile发现某个memmove是热点再优化成不重叠场景下的memcpy也不迟。7.2 一次真实调试给我的启发有一次我调一个网络解析模块的偶发崩溃程序大概运行几小时后才在某个神秘的位置段错误。加了一堆打印都定位不到后来用ASan编译跑了一遍马上定位到一个越界写。原因是解析数据包时用memcpy把一个可变长度字段拷进固定大小的堆缓冲区长度字段没做上限校验某个异常数据包把它撑破了一个字节。就这么一个字节让堆调整了我完全想不到的地方最后在字符串格式化时崩溃。所以我现在总会跟团队说内存操作函数的size参数不是“你想拷多少”而是“最多能拷多少”。少拷一点顶多数据不完整多拷一点可能整个进程都要陪葬。这篇详解写到这里我并没有把各种函数的所有边角都列出来因为真正从实战中积累出来的关键是那三条字节、边界、状态。字节单位别搞错边界范围每次都要算清楚分配和释放的状态变化心里要有数。后面的路就在你每次调用这些函数时的习惯和检查里。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑