5、字符串安全:strcpy/strcat的危险性、strncpy/strncat的正确使用、snprintf替代sprintf、安全的字符串处理函数。
字符串处理,是C语言里最容易翻车的地方。我见过太多线上事故,最后定位到根因,就是一行简单的strcpy或者sprintf。说白了,C语言把内存的缰绳交到你手里,但没告诉你马会尥蹶子。
这一章,我们就来聊聊字符串的那些“坑”,以及怎么填平它们。
5.1 strcpy/strcat:看似简单,实则危险
strcpy和strcat,是C标准库最早期的字符串函数。它们的设计思路很单纯:你给我源字符串,我给你复制/拼接过去。但问题在于——它们不做边界检查。
我在项目中遇到过这样一个案例:一个嵌入式设备,运行了几个月后突然重启。查了三天,最后发现是某处日志打印用了strcat,拼接了一个异常长的设备名称,直接把栈上的返回地址给覆盖了。嗯,从那以后,我对strcat就彻底失去了信任。
看一个典型的问题代码:
char buf[16];
char *input = "This is a very long string that will overflow!";
strcpy(buf, input); // 危险!buf只有16字节,input远超16字节
这段代码编译时不会有任何警告,运行时也不会立即崩溃。但buf后面的内存已经被污染了。这种bug,你想想看,最难查的就是它——症状和病因往往不在同一个地方。
strcat同理:
char buf[32] = "Hello";
char *suffix = ", this is a very long suffix that will overflow the buffer!";
strcat(buf, suffix); // 危险!buf剩余空间可能不够
为什么会这样?因为strcat不知道buf还剩多少空间,它只管往buf的'\0'位置开始复制,直到遇到源字符串的'\0'。
5.2 strncpy/strncat:有限度的安全,但别掉以轻心
为了解决上述问题,C标准引入了strncpy和strncat。它们多了一个参数——最大复制长度。听起来靠谱多了,对吧?但实际用起来,还是有不少细节需要注意。
5.2.1 strncpy 的正确用法
strncpy(dst, src, n) 最多复制 n 个字符到 dst。但有个坑:如果 src 长度大于等于 n,dst 不会以 '\0' 结尾。
strncpy 后,手动在 dst[n-1] 位置写入 '\0'。
我个人习惯这样写:
char dst[32];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0'; // 确保以'\0'结尾
注意这里用的是 sizeof(dst) - 1,而不是 sizeof(dst)。留一个字节给终止符。
5.2.2 strncat 的正确用法
strncat 的行为和 strncpy 略有不同。它的第三个参数表示最多追加的字符数,不包括终止符。而且 strncat 会自动在结果末尾加 '\0'。
char dst[32] = "Hello";
char *src = " World!";
strncat(dst, src, sizeof(dst) - strlen(dst) - 1);
这里计算剩余空间时,减去了 strlen(dst) 和 1(留给 '\0')。
#define SAFE_STRNCPY(dst, src) do { \
strncpy(dst, src, sizeof(dst) - 1); \
dst[sizeof(dst) - 1] = '\0'; \
} while(0)
5.3 snprintf:sprintf 的替代者
sprintf 和 strcpy 是同一类问题——不检查输出长度。格式化输出时,如果目标缓冲区太小,一样会溢出。
我曾经在调试一个通信协议栈时,发现日志里偶尔出现乱码。查了半天,原来是 sprintf 拼接的字符串超过了缓冲区大小,把相邻的协议状态变量给覆盖了。从那以后,我代码里所有的 sprintf 都被替换成了 snprintf。
snprintf 的原型:
int snprintf(char *str, size_t size, const char *format, ...);
它保证最多写入 size - 1 个字符,并在末尾加 '\0'。返回值是“如果缓冲区足够大,应该写入的字符数”(不包括终止符)。
标准用法:
char buf[64];
int n = snprintf(buf, sizeof(buf), "Value: %d, Name: %s", val, name);
if (n >= sizeof(buf)) {
// 输出被截断了,可以处理或记录日志
}
注意检查返回值。如果返回值大于等于缓冲区大小,说明输出被截断了。你可以选择报错、重新分配更大的缓冲区,或者直接忽略(如果截断可接受)。
snprintf 替代 sprintf。没有例外。
5.4 安全的字符串处理函数
除了上面提到的函数,C11标准还引入了一批“安全版”字符串函数,定义在 string.h 中(需要定义 __STDC_WANT_LIB_EXT1__ 宏)。比如:
strcpy_sstrcat_sstrncpy_ssprintf_ssnprintf_s
这些函数的特点是:明确接收缓冲区大小参数,并在溢出时返回错误码或调用约束处理函数。
举个例子:
errno_t err = strcpy_s(dst, sizeof(dst), src);
if (err != 0) {
// 处理错误
}
不过要注意,这些安全函数并非所有平台都支持。嵌入式开发中,很多编译器(尤其是老旧的)并不实现 Annex K。所以我的建议是:
- 如果平台支持:优先使用
_s系列函数 - 如果不支持:用
snprintf+ 手动'\0'终止的方式,封装自己的安全函数
下面是我在项目中常用的一套封装:
// 安全字符串复制
static inline void safe_strcpy(char *dst, size_t dst_size, const char *src) {
if (dst_size == 0) return;
size_t src_len = strlen(src);
size_t copy_len = (src_len < dst_size - 1) ? src_len : dst_size - 1;
memcpy(dst, src, copy_len);
dst[copy_len] = '\0';
}
// 安全字符串拼接
static inline void safe_strcat(char *dst, size_t dst_size, const char *src) {
size_t dst_len = strlen(dst);
size_t remaining = dst_size - dst_len;
if (remaining <= 1) return;
safe_strcpy(dst + dst_len, remaining, src);
}
你想想看,多写这几行代码,能省去多少排查内存问题的痛苦?
5.5 本章知识体系
下面这张图,帮你梳理字符串安全的核心逻辑:
说白了,字符串安全的核心就一句话:永远假设输入比你预期的长。在这个假设下,所有不检查长度的函数都是定时炸弹。
strcpy、strcat、sprintf。代码审查时看到这三个函数,直接打回。你想想看,这能省多少事。
记住:防御式编程不是让你写更复杂的代码,而是让你写更不容易出错的代码。字符串安全,就是防御式编程的第一道防线。
公众号:蓝海资料掘金营,微信deep3321