23、缓冲区溢出:gets函数的禁用与fgets的替代

缓冲区溢出,这词儿听着就让人头疼。

我做了十几年嵌入式开发,见过太多因为缓冲区溢出导致的诡异bug。系统跑着跑着就崩了,或者莫名其妙地执行了错误逻辑。最可怕的是,这种bug往往很难复现,排查起来简直要命。

而这一切的罪魁祸首,很多时候就是那个看似无害的gets()函数。

为什么gets()是定时炸弹?

gets()函数的设计,说白了就是「不负责任」。它从标准输入读取一行字符串,但完全不检查目标缓冲区的大小

你想想看,如果你只准备了64字节的缓冲区,用户却输入了200个字符,会发生什么?

多余的字符会直接覆盖掉缓冲区后面的内存区域。那里可能存放着其他变量、函数返回地址,甚至是程序的控制流数据。

⚠️ 核心问题: gets() 没有边界检查,这是C语言标准库中公认的设计缺陷。C11标准已经正式将其移除。

我记得有一次,一个同事调试了整整三天,发现某个全局变量总是被莫名其妙地修改。最后定位到,就是gets()读入的数据悄悄越界,把那个变量给覆盖了。嗯,从那以后,我们团队就立下了规矩:代码中绝对不允许出现gets()

fgets():安全的替代方案

那用什么?fgets()。它才是正经干活儿的函数。

fgets()的原型是这样的:

char *fgets(char *s, int size, FILE *stream);

关键就在第二个参数size。它告诉函数:最多只能读 size-1 个字符,剩下的一个位置留给字符串结束符\0。这样,无论用户输入多长,都不会溢出缓冲区。

我个人习惯,在嵌入式开发中,fgets()的size参数直接填缓冲区的大小,比如:

char buf[64];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
    // 处理输入
}

这里有个细节要注意。fgets()会把换行符\n也读进来。如果你不需要它,得手动去掉:

// 去掉末尾的换行符
size_t len = strlen(buf);
if (len > 0 && buf[len-1] == '\n') {
    buf[len-1] = '\0';
}

gets() vs fgets() 对比

特性 gets() fgets()
缓冲区大小检查 有(通过size参数)
读取换行符 丢弃 保留(包含在字符串中)
安全性 极低(高危) 高(推荐)
C11标准状态 已移除 保留
适用场景 永远不要用 所有需要读取字符串的场景

避坑指南:fgets()的常见陷阱

我曾经在项目中遇到过一个问题:用fgets()读取用户输入,结果程序总是多出一个空行。排查了半天,才发现是fgets()把换行符也读进来了,而后续处理逻辑没考虑到这一点。

所以,使用fgets()时,有几点要特别注意:

  • 换行符处理:记得检查并移除末尾的\n,除非你确实需要它。
  • 缓冲区大小:size参数一定要填对。我见过有人填sizeof(ptr),结果ptr是个指针,只读了4个字节。正确做法是填sizeof(buf)或者明确的数字。
  • 返回值检查fgets()返回NULL表示读取失败(比如遇到EOF或错误)。一定要检查返回值,不要假设每次都能读到数据。
  • 输入截断:如果用户输入超过了size-1个字符,fgets()会截断输入。剩下的字符会留在输入流中,下次读取时会读到。如果你不希望这样,需要手动清空输入缓冲区。
💡 小技巧: 清空输入缓冲区可以用这个循环:while (getchar() != '\n'); 但注意,这只能在确定还有未读字符时使用,否则会阻塞等待新输入。

核心逻辑:缓冲区溢出防护

下面这张图,是我自己总结的缓冲区溢出防护的核心逻辑。说白了,就是「输入前检查,输入后清理」。

缓冲区溢出防护核心逻辑 用户输入 / 网络数据 选择读取函数 gets() 还是 fgets()? gets() 无边界检查 fgets() 有边界检查 缓冲区溢出 内存被破坏 程序崩溃 安全读取 数据在缓冲区 范围内 检查返回值 处理换行符 ✅ 安全可靠的输入处理

实际项目中的经验

我在做嵌入式项目时,遇到过一种情况:某个模块需要从串口接收不定长的命令。一开始有人用gets(),结果只要发送一个超长命令,系统就死机。

后来改成fgets(),配合一个循环读取的逻辑,问题就解决了。具体做法是:

  1. fgets()每次读取固定长度的数据块。
  2. 检查是否读到换行符,如果没读到,说明数据还没完,继续读。
  3. 拼接所有数据块,直到收到完整的命令。

这种做法虽然代码量多了几行,但系统稳定性提升了一个档次。你想想看,一个稳定的系统,比什么都重要。

📌 核心原则: 永远不要假设输入的长度。任何来自外部(用户、网络、文件)的数据,都必须经过长度检查。这是嵌入式软件工程师的基本素养。

嗯,关于gets()fgets(),就聊这么多。记住一句话:fgets(),别用gets()。这不仅是编程规范,更是对自己代码负责的态度。

专注资料整理