Loading... # 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. 函数原型 ```mermaid graph LR A[源字符串 src] -->|strlcpy| B[目标缓冲区 dst] B -->|返回| C[源字符串长度] D[目标字符串 dst] -->|strlcat| E[目标缓冲区 dst] F[源字符串 src] -->|strlcat| E E -->|返回| G[总长度] ```  ```c 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) ```c strcpy(path, homedir); strcat(path, "/"); strcat(path, ".foorc"); len = strlen(path); ``` 这段代码存在缓冲区溢出的风险,因为 `homedir` 环境变量由用户控制,长度可以是任意的。 ## 2. 使用 strncpy/strncat 的安全版本(示例 1b) ```c 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) ```c strlcpy(path, homedir, sizeof(path)); strlcat(path, "/", sizeof(path)); strlcat(path, ".foorc", sizeof(path)); len = strlen(path); ``` 这个版本和原始代码一样简单易读,但不会发生缓冲区溢出。 ## 4. 带截断检测的完整版本(示例 1d) ```c 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.5 - DEC AXPPCI166:166MHz Alpha CPU,运行 OpenBSD 2.5 ## 3. 性能数据 ```mermaid 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秒] end ```  | CPU 架构 | 函数 | 时间(秒) | |---------|------|----------| | m68k | strcpy | 0.137 | | m68k | strncpy | 0.464 | | m68k | strlcpy | 0.140 | | alpha | strcpy | 0.018 | | alpha | strncpy | 0.100 | | alpha | strlcpy | 0.020 | ## 4. 性能分析 从测试结果可以看出: - `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. OpenBSD `strlcpy()` 和 `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()` 的返回值来检测截断,并在需要时分配更大的缓冲区重新操作。 *** ## 参考资料 1. [strlcpy and strlcat – consistent, safe, string copy and concatenation](https://www.millert.dev/papers/strlcpy/) 最后修改:2026 年 02 月 03 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏