大家好,我是仲一。
先上题,别往下看,你心里先给个答案:
char buf[] = "hello world";
memcpy(buf + 4, buf, 6); // src 和 dst 重叠了,安全吗?
你可能会想:memcpy 不就是拷内存吗?重叠就重叠呗,大不了拷贝的结果不对,但它不至于"出错"吧。
答案比你想的严重得多。这行代码在 C 标准里是未定义行为——意思是,编译器想怎么处理就怎么处理,可能正确、可能错、可能直接崩,全看它的心情和实现。
标准怎么说:memcpy 要求不重叠
C 标准对 memcpy 的定义,原文大意是:从 src 拷贝 n 个字节到 dst,如果这两个内存区域重叠,行为是未定义的。
注意措辞,"未定义"不是"可能出错",是"标准根本不保证任何结果"。它可以正确拷贝,可以拷出脏数据,可以让程序崩溃,也可以看似正常跑一年然后在某个版本更新后突然炸掉。
那如果真需要处理重叠呢?标准给的是另一个函数——memmove。它明确保证:即使 src 和 dst 重叠,也能拷贝出正确结果。
一句话区别:memcpy 假设源和目的不重叠,所以实现可以更激进地优化;memmove 保证重叠安全,而现代 libc 在不重叠时性能跟 memcpy 几乎一样——很多实现直接复用 memcpy 的快速路径,只在检测到重叠时才切换方向。
所以这里没有"为性能选 memcpy"的权衡空间。当你不确定是否重叠时,直接用 memmove——那点理论上的性能差,不值得拿未定义行为去赌。
为什么重叠会让 memcpy 出错
要理解这个,得看 memcpy 的实现。大多数平台的 memcpy 是按"从前往后"的顺序逐块拷贝的——从低地址往高地址拷。
问题就出在这。假设你要把 buf 前 6 个字节拷到它自己偏后 4 个字节的位置:
buf: [h][e][l][l][o][ ][w]...
↑src ↑dst
从前往后拷,第一步把 buf[0] 的 'h' 拷到 buf[4],覆盖了原来的 'o'。可后面还要把 buf[4] 的 'o' 拷到 buf[8],但 buf[4] 早就被覆盖成 'h' 了——拷过去的是错的。
所以一旦 src 在 dst 前面、且两者重叠,正向拷贝必然把还没读的源数据提前覆盖掉。数据就坏了。
memmove 聪明在哪?它会先判断:如果 dst 在 src 后面(重叠时正向会出事),就改成从后往前拷——先拷末尾的字节,再往前,这样就不会覆盖还没读的源数据。方向一变,问题就没了。
为什么有时"看着没事"——一个真实的教训
这里有个反直觉的点,也是这题最容易答浅的地方:很多人重叠着用 memcpy,跑得好好的,从来没出过错,于是觉得"标准说的未定义行为是吓唬人的"。
这个想法很危险。它能跑对,往往只是巧合——拷贝方向恰好跟你的重叠方向匹配,或者当前的 libc 实现恰好没踩雷。可这个"恰好"不保证一直成立。
真实世界出过大事。glibc 在 2.13 版本做了一次性能优化,改了 x86-64 上 memcpy 的拷贝顺序。结果呢?大量程序崩了——因为它们一直在重叠区域上非法调用 memcpy,老实现恰好用某种拷贝顺序,把这些 bug 掩盖了。优化一把拷贝顺序,隐藏的雷全炸了。glibc 2.14 不得不加版本化符号,让链接旧版本的程序继续用老实现,才算稳住。
但要说清楚,这个版本化符号不是"修复",是"隔离"——老程序里那些重叠调用的未定义行为,只是继续被老实现的巧合行为盖着,并没有变安全。它只是让已经跑起来的程序别在新版本上立刻崩掉。
真正有价值的是这件事的长期遗产。glibc 那次翻车,推动了工具链开始帮你抓这类 bug:GCC 加了 -Wstringop-overlap 警告,ASan 能在运行时检测重叠拷贝。如果你还在靠"跑着没事"判断代码对不对,至少把这两个开关打开。
面试怎么答出深度
这题面试官一般怎么问、想听到什么?分几层。
第一层(及格):答"memcpy 要求源和目的不重叠,重叠要用 memmove"。能说出标准层面的话,算过关。
第二层(加分):能讲出 memmove 的实现原理——判断 dst 和 src 的位置关系,dst 在前就从前往后,dst 在后就改成从后往前,避免覆盖未读数据。这说明你真懂,不是背的。
第三层(出彩):能提到 memcpy 的重叠是未定义行为,**不只是"会出错",而是"不保证任何结果"**——并举出 glibc 2.13 改拷贝顺序引发大面积崩溃这种真实案例,说明"能跑不等于没 bug"。能讲到这一层,面试官基本知道你是个写过底层、踩过坑的人。
再补一个实用的判断:什么时候该担心?凡是原地操作缓冲区——数组元素前移/后移、滑动窗口、环形缓冲、在字符串中间插入——都可能重叠,这种场景直接用 memmove 就对了。memcpy 留给你确定不重叠的场景(比如两个独立结构体之间拷贝),别为那点理论性能去赌 UB。
做底层的人常说,C 语言的坑一半在指针,一半在"标准说未定义、实际看着没事"。memcpy 的重叠就是后者最典型的一个。它不会当场报错,只会让你在某次升级后,对着一个莫名其妙的 bug 怀疑人生。
你遇到过 memcpy/memmove 重叠的诡异 bug 吗?还是曾经"看着没事"然后某天炸了?留言说说。
仲一做嵌入式已经六七年了,现在在某头部芯片公司做驱动开发,之前带过团队、也面试招过人。简历看多了,面试也面多了,所以职业怎么走、简历哪里不对、面试卡在哪儿,这些事我算是有点发言权。
这些年陆陆续续有读者找过来,问方向、问简历,也拉我做过不少模拟面试。带过的师弟师妹有大二进小米海康的,也有四个月拿到大疆、oppo、华为 offer 的,简历前前后后改过两千多份。
就一个要求:建议你得真去做。不然别来找我,浪费彼此时间。
职业方向、简历、面试,哪块有疑惑都可以找我聊聊。想聊的,公众号后台回复「咨询」。
end
113