strlcpy 与 strlcat：安全的 C 语言字符串操作一、概述1. 背景C 语言中的字符串操作一直是一个安全隐患的来源。传统的 strcpy() 和 strcat() 函数不进行边界检查，容易导致缓冲区溢出攻击。为了解决这个问题，C 标准库提供了 strncpy() 和 strncat() 函数，但这些函数的设计存在许多问题，使得正确使用它们变得困难。2. 问题陈述在 1996 年，OpenBSD 项目组在审计源代码时发现，程序员在尝试使用 strncpy() 和 strncat() 进行安全字符串操作时，经常误解其 API 语义，导致错误的使用。这些错误虽然不一定都能被利用，但表明 strncpy() 和 strncat() 的安全操作规则被广泛误解。3. 解决方案Todd C. Miller 和 Theo de Raadt 设计了两个新的函数 strlcpy() 和 strlcat()，提供了更直观、一致的 API，专门设计用于安全的字符串复制操作。二、传统函数的问题1. strncpy 的问题A. 不保证 NULL 终止strncpy() 只有在源字符串长度小于 size 参数时才会对目标字符串进行 NULL 终止。如果源字符串长度大于或等于 size，目标字符串将不会被 NULL 终止，这可能导致后续的字符串操作出现问题。B. 性能问题strncpy() 会用 NULL 字符填充目标字符串的剩余空间，当目标缓冲区远大于源字符串时，这会导致显著的性能损失。C. 零填充影响缓存大量的 NULL 填充操作会有效地刷新 CPU 的数据缓存，进一步降低性能。2. strncat 的问题A. 容易混淆的 size 参数strncat() 的 size 参数表示目标字符串中剩余的可用空间（不包括 NULL 终止符），而不是目标字符串的总大小。这个区别经常被程序员误解。B. 需要计算可用空间由于 size 参数是可用空间而非总大小，程序员必须手动计算 sizeof(dst) - strlen(dst) - 1，这个计算经常出错。三、新函数的设计1. 函数原型graph LR A[源字符串 src] -->|strlcpy| B[目标缓冲区 dst] B -->|返回| C[源字符串长度] D[目标字符串 dst] -->|strlcat| E[目标缓冲区 dst] F[源字符串 src] -->|strlcat| E E -->|返回| G[总长度]size_t strlcpy(char *dst, const char *src, size_t size); size_t strlcat(char *dst, const char *src, size_t size);2. 设计原则A. 保证 NULL 终止只要 size 参数非零，strlcpy() 和 strlcat() 保证目标字符串总是被 NULL 终止。B. 使用总大小作为参数两个函数都接受目标字符串的总大小作为 size 参数，通常可以直接使用 sizeof(dst)，无需复杂计算。C. 不进行零填充除了必需的 NULL 终止符外，函数不会填充目标字符串的剩余空间。D. 返回尝试创建的字符串总长度返回值表示如果没有截断会发生什么，这使得检测截断变得简单。四、使用示例对比1. 不安全的原始代码（示例 1a）strcpy(path, homedir); strcat(path, "/"); strcat(path, ".foorc"); len = strlen(path);这段代码存在缓冲区溢出的风险，因为 homedir 环境变量由用户控制，长度可以是任意的。2. 使用 strncpy/strncat 的安全版本（示例 1b）strncpy(path, homedir, sizeof(path) - 1); path[sizeof(path) - 1] = '\0'; strncat(path, "/", sizeof(path) - strlen(path) - 1); strncat(path, ".foorc", sizeof(path) - strlen(path) - 1); len = strlen(path);这个版本虽然安全，但代码复杂，容易出错：需要手动终止字符串每次调用 strncat 都要计算剩余空间仍然需要最终的 strlen 调用3. 使用 strlcpy/strlcat 的简单版本（示例 1c）strlcpy(path, homedir, sizeof(path)); strlcat(path, "/", sizeof(path)); strlcat(path, ".foorc", sizeof(path)); len = strlen(path);这个版本和原始代码一样简单易读，但不会发生缓冲区溢出。4. 带截断检测的完整版本（示例 1d）len = strlcpy(path, homedir, sizeof(path)); if (len >= sizeof(path)) return (ENAMETOOLONG); len = strlcat(path, "/", sizeof(path)); if (len >= sizeof(path)) return (ENAMETOOLONG); len = strlcat(path, ".foorc", sizeof(path)); if (len >= sizeof(path)) return (ENAMETOOLONG);这个版本不仅安全，而且能够检测截断并优雅地处理错误。由于返回值已经是最终长度，无需额外的 strlen 调用。五、性能对比1. 测试场景测试程序将字符串 "this is just a test" 复制 1000 次到一个 1024 字节的缓冲区中。2. 测试环境HP9000/425t：25MHz 68040 CPU，运行 OpenBSD 2.5DEC AXPPCI166：166MHz Alpha CPU，运行 OpenBSD 2.53. 性能数据graph TB subgraph m68k_架构性能 A1[strcpy: 0.137秒] A2[strncpy: 0.464秒] A3[strlcpy: 0.140秒] end subgraph alpha_架构性能 B1[strcpy: 0.018秒] B2[strncpy: 0.100秒] B3[strlcpy: 0.020秒] endCPU 架构函数时间（秒）m68kstrcpy0.137m68kstrncpy0.464m68kstrlcpy0.140alphastrcpy0.018alphastrncpy0.100alphastrlcpy0.0204. 性能分析从测试结果可以看出：strncpy() 的性能远差于 strcpy() 和 strlcpy()strlcpy() 的性能与 strcpy() 相当在 m68k 架构上，strncpy() 比 strlcpy() 慢约 3.3 倍在 alpha 架构上，strncpy() 比 strlcpy() 慢约 5 倍六、实际应用案例1. Apache 项目Apache 项目组将 strncpy() 调用替换为内部函数后，注意到了性能提升。2. ncurses 项目ncurses 软件包移除一处 strncpy() 使用后，tic 工具的速度提升了 4 倍。3. rsync 项目rsync 软件包现在使用 strlcpy() 和 strlcat()，如果操作系统不支持则提供自己的实现。七、适用场景与限制1. 适用场景strlcpy() 和 strlcat() 非常适合处理固定大小的缓冲区，这是 C 语言中最常见的字符串操作场景。2. 不适用场景有一些场景仍然需要使用 strncpy() 和 strncat()：处理不是真正 C 字符串的缓冲区（例如 struct utmp 中的字符串）需要非 NULL 终止的字符序列3. 设计理念strlcpy() 和 strlcat() 不是试图修复 C 语言中的字符串处理，而是设计在 C 字符串的正常框架内工作。如果需要支持动态分配、任意大小缓冲区的字符串函数，可以考虑其他方案，如 mib software 的 astring 包。八、采用情况1. OpenBSDstrlcpy() 和 strlcat() 首次出现在 OpenBSD 2.4 中，是新代码和更新旧代码时的首选字符串操作函数。2. Solaris这两个函数已被批准在未来版本的 Solaris 中包含。3. 第三方软件越来越多的第三方软件开始采用这个 API，通常在操作系统不支持时提供自己的实现。4. 标准化前景作者希望这个 API 能够获得更广泛的接受，并在某个时候成为标准。九、获取方式strlcpy() 和 strlcat() 的源代码以 BSD 风格许可证免费提供，作为 OpenBSD 操作系统的一部分。可以通过匿名 FTP 从以下地址获取：服务器：ftp.openbsd.org目录：/pub/OpenBSD/src/lib/libc/string源文件：strlcpy.c 和 strlcat.c文档：strlcpy.3十、最佳实践建议1. 编写新代码时优先使用 strlcpy() 和 strlcat()，它们更安全、更高效、更易于正确使用。2. 审计现有代码时检查 strncpy() 和 strncat() 的使用是否正确，特别关注：是否正确处理了 NULL 终止size 参数的计算是否正确是否有截断检测3. 性能敏感场景如果目标缓冲区远大于预期的输入，strncpy() 的零填充操作会造成显著的性能损失，应使用 strlcpy()。4. 截断处理利用 strlcpy() 和 strlcat() 的返回值来检测截断，并在需要时分配更大的缓冲区重新操作。参考资料strlcpy and strlcat – consistent, safe, string copy and concatenation