第1章:历史遗留问题——不安全函数的替代方案
说实话,每次我看到老项目里还有 gets()、strcpy() 这些函数,心里就咯噔一下。这不是矫情,是真的被坑过。
我入行那会儿,C 标准库里的这些函数还是「标配」。大家写代码都这么写,也没觉得有啥问题。直到有一次,我负责的一个嵌入式设备在客户现场跑着跑着就挂了——后来定位到是 sprintf() 缓冲区溢出,直接把栈给踩了。嗯,从那以后,我对这些函数的态度就变成了:能不用,坚决不用。
这一章,我就带你把这些「历史遗留问题」一个一个捋清楚。说白了,就是告诉你哪些函数有坑,用什么来替代。
1. gets() → fgets()
gets() 这个函数,C 标准委员会自己都看不下去了,在 C11 标准里直接把它删了。为什么?因为它不检查输入长度。
// 危险写法
char buf[16];
gets(buf); // 用户输入超过15个字符,直接溢出
你想想看,用户输入多少字符,完全不可控。攻击者可以利用这个漏洞执行任意代码。这不是危言耸听,历史上很多安全漏洞就是这么来的。
替代方案很简单:用 fgets()。
// 安全写法
char buf[16];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
// 处理输入
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
}
我个人习惯是,每次用 fgets() 之后,都会手动去掉末尾的换行符。因为 fgets() 会保留换行,而很多场景下我们不需要它。
2. strcpy() / strcat() → strncpy() / strncat() 或 snprintf()
strcpy() 和 strcat() 的问题跟 gets() 类似——不检查目标缓冲区大小。
// 危险写法
char dest[32];
char *src = "这是一段非常非常长的字符串,远远超过32个字节...";
strcpy(dest, src); // 溢出!
strcat(dest, "更多内容"); // 继续溢出!
我在项目中遇到过这样的 bug:两个字符串拼接后,直接把相邻的变量给覆盖了。调试了整整一天才发现是 strcat() 的问题。
替代方案有两个:
- strncpy() / strncat():指定最大复制长度
- snprintf():更灵活,推荐使用
// 安全写法
char dest[32];
char *src = "这是一段很长的字符串...";
// 方法1:strncpy
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保以'\0'结尾
// 方法2:snprintf(我更喜欢这个)
snprintf(dest, sizeof(dest), "%s", src);
3. sprintf() → snprintf()
sprintf() 跟 strcpy() 是「难兄难弟」。它把格式化后的字符串写入缓冲区,但不检查缓冲区大小。
// 危险写法
char buf[32];
int val = 123456;
sprintf(buf, "value = %d", val); // 如果格式化结果超过32字节,溢出!
我曾经见过一个产品,因为 sprintf() 溢出导致系统随机重启。那个 bug 时有时无,跟用户输入的数据有关,排查起来特别痛苦。
替代方案:用 snprintf(),它多了一个参数指定缓冲区大小。
// 安全写法
char buf[32];
int val = 123456;
int len = snprintf(buf, sizeof(buf), "value = %d", val);
if (len >= sizeof(buf)) {
// 输出被截断了,需要处理
fprintf(stderr, "警告:输出被截断\n");
}
注意 snprintf() 的返回值:它返回的是「如果缓冲区足够大,应该写入的字符数」。如果返回值大于等于缓冲区大小,说明输出被截断了。这个信息可以用来做错误处理。
4. atoi() / atol() / atof() → strtol() / strtoul() / strtof()
atoi() 系列函数的问题在于:它没有错误处理机制。如果输入不是有效的数字,它返回 0。但 0 本身也是一个合法值,你分不清是转换成功还是失败。
// 危险写法
char *input = "abc";
int val = atoi(input); // 返回0,但无法判断是否出错
替代方案:用 strtol()、strtoul()、strtof() 等函数。它们提供了错误检测能力。
// 安全写法
char *input = "123";
char *endptr;
long val = strtol(input, &endptr, 10);
if (endptr == input) {
// 没有读取到任何数字
fprintf(stderr, "错误:无效的数字格式\n");
} else if (*endptr != '\0') {
// 读取到了数字,但后面还有多余字符
fprintf(stderr, "警告:输入包含多余字符\n");
} else {
// 转换成功
printf("value = %ld\n", val);
}
我个人习惯是,只要涉及用户输入的数字转换,一律用 strtol() 家族。虽然代码多几行,但心里踏实。
知识体系总览
下面这张图总结了本章的核心内容:不安全函数及其替代方案。
总结
这一章我们聊了四个典型的不安全函数及其替代方案。说白了,核心原则就一条:永远不要假设输入的长度是可控的。
我曾经在代码审查时看到有人写 strcpy(dest, "hello"),觉得「反正源字符串很短,不会溢出」。这种想法很危险——代码是会演化的,今天写死的字符串,明天可能就改成从配置文件读取了。到时候忘了改 strcpy,bug 就埋下了。
所以我的建议是:从一开始就用安全版本。多打几个字符,换来的是长期的安心。
避坑指南:
- 我曾经因为 gets() 导致栈溢出,系统在客户现场崩溃——从那以后我再也没用过 gets()。
- 我曾经因为 strcat() 覆盖了相邻变量,调试了整整一天——现在我只用 snprintf() 做字符串拼接。
- 我曾经因为 atoi() 无法区分「转换失败」和「输入为0」,导致业务逻辑出错——现在一律用 strtol()。
下一章,我们会继续聊另一个经典陷阱:数组下标越界。嗯,那也是个大坑。