gets() 函数的危险性 - 以及合理的替代

起因是在 G++17 中图个省事,输入字符串的时候直接写了 gets(),但怎么也没法编译,提示没有这个函数。我之前了解过 gets() 函数有危险而且不建议使用,但好像也没说不能用吧……? (不建议用就别用)

于是我去查了以下 gets() 函数,发现事情绝对没有我想象的那么简单……

什么是 gets() 函数

C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定

我们来看看它的原理:

gets() 函数的形参只有一个指针。它会从标准输入流中读字符到一块连续的内存地址空间中。这块地址空间的开始位置就是指针 str 指向的位置。当在输入流中遇到文件结束符 (EOF) 或者换行符 (\n) 时,读取操作结束。当读入换行符 (\n) 时,该字符不会被放入那块连续的地址空间中。在读取结束时, gets() 会自动在内存空间的末尾追加一个 NULL 字符。经过上述这些操作,对于程序员来说,这个函数得到的就是从标准输入进来的,以 NULL 字符结尾的 C 字符串。如果读入的字符流是一整行的话,行尾的换行符将会被舍去。

这看起来挺好:我不用提前考虑输入字符串的长度,只需要用空格和换行区分就可以了。

但是真的是这样吗?

问题

gets 的问题其实就在这个连续输入上。当使用 gets() 时,它会读取用户输入的字符,直到遇到换行符为止,并将这些字符存储到一个字符数组(缓冲区)中,然后返回缓冲区里的字符数据。

然而十行代码九行补漏洞,也没法阻止奇葩的用户需求。 你不知道用户会输入多长的字符串,但是缓冲区的大小是有限的。如果用户输入的字符数量超过了缓冲区的大小gets() 函数就会继续将字符写入缓冲区,导致缓冲区溢出

缓冲区溢出发生时,超出缓冲区边界的字符会覆盖其他内存区域,这可能导致程序崩溃或者被攻击者利用。攻击者可以利用缓冲区溢出漏洞来修改程序的控制流,执行恶意代码或者获取敏感信息。

关于缓冲区溢出,可以看下这篇文章,我觉得讲得还可以的。

正因为如此,按理来说使用 gets() 函数时必须特别小心,确保输入字符数量不会超出缓冲区的大小。但是,你怎么知道用户要输入多长呢?

所以,与其不如想办法限制输入长度以防止缓冲区溢出,不如不用 gets() 函数

也正因为如此,gets() 函数在 C 标准中被弃用,并于 C11 标准中正式移除。在 C11 标准中,gets() 函数被标记为 “obsolescent”(过时),并建议使用更安全的替代方案,如 fgets() 函数或者更推荐的 fgets() 的 C++ 等价函数 std::cin.getline()。由于 gets() 存在严重的安全漏洞,因此在更早的标准和编译器版本中,就已经开始警告开发者避免使用这个函数。例如,一些编译器在 C99 标准中就开始发出警告,而在 C11 标准中正式将其移除。

那为什么最初的 C 标准还要引入 gets() 呢?

以下是我的个人理解。

当初设计 gets() 函数并将其纳入 C 标准的主要原因是为了简化输入字符串的操作,使得程序员能够更方便地从标准输入中获取字符串数据。C 语言早期的时候,开发者还没考虑到那么多的安全性问题,而更注重的是编程语言的简洁性和易用性,而且安全性也并不是当时主要考虑的地方。因此,gets() 函数被设计为一种快速而方便的方法来读取用户输入的字符串。

然而,随着时间的推移和计算机科学的发展,人们开始意识到缓冲区溢出等安全漏洞的严重性。人们逐渐意识到,这个看起来很方便的函数,所引发的问题可能造成非常严重的安全后果。而这个缓冲区也不好再修修补补,况且经过这么多年发展,gets() 也不再是唯一一种读取字符串的方法,与其修这些问题,不如砍掉一了了之。

替代 / 解决方案

其实要想实现 gets() 函数同样的效果也并不难,有很多方法都可以替代 gets()。这其中有的是通过限制输入长度实现的,也有的可以直接读取

  1. fgets() 函数

    fgets() 可以从标准输入中读取一行字符串,并将其存储到指定大小的缓冲区中。与 gets() 不同,fgets() 允许指定要读取的字符数目,从而避免了缓冲区溢出的问题。

    fgets() 也是普遍认可的一个 gets() 的替代。

    1
    2
    char a[1000]
    fgets(a, sizeof(a), stdin);

    fgets() 的写入流改为 stdin 即可向对应的字符串写入。

  2. std::cin.getline() (仅限 C++)

    在 C++ 中,可以使用 std::cin.getline() 函数来替代 gets()。它可以从标准输入中读取一行字符串,并将其存储到指定大小的缓冲区中。与 gets() 类似,std::cin.getline() 允许指定要读取的字符数目,从而避免了缓冲区溢出的问题。

    1
    2
    3
    4
    #include<iostream>
    using namespace std;
    char buffer[100];
    cin.getline(buffer, sizeof(buffer));
  3. std::getline()

    在 C++ 中,也可以使用 std::getline() 函数来读取一行字符串,它更加灵活,可以读取任意长度的字符串,并将其存储到 std::string 对象中。

    1
    2
    3
    4
    5
    #include<iostream>
    #include<string>
    using namespace std;
    string line;
    getline(cin, line);
  4. scanf() + "%s"

    可以在 scanf() 函数中使用格式化字符串来指定要读取的字符数量,并且建议使用 %s 格式,并指定字段宽度。这样可以确保 scanf() 函数不会读取超过指定长度的字符,从而避免了缓冲区溢出的风险。

    下面是一个使用 scanf() 函数读取字符串的示例,其中指定了字段宽度为 99:

    1
    2
    char a[100];
    scanf("%99s", a);
  5. scanf() + "%[^\n]%*c"

    这种方法是我在做一道题的时候用的,我觉得这个好处是可以在 C 中非常轻松地实现。

    %[^\n] 格式表示读取除换行符以外的所有字符,直到遇到换行符为止。这样可以确保 scanf() 函数在读取完整行字符串时停止,而不会受到换行符的影响。

    然后,%*c 表示读取一个字符并丢弃它,也就是丢弃最后输入的换行符,这样可以清除输入缓冲区中的换行符,以防止它对后续输入产生干扰。

    1
    2
    char a[10005];
    scanf("%[^\n]%*c", a);

总结

一句话就是:gets() 别用。

实际编写代码的时候,对于编译器的警告,一定不能忽略,最好仔细看一下能不能有更好的替代。


gets() 函数的危险性 - 以及合理的替代
https://gt610.codeberg.page/2024/03/05/gets-function-replacement-in-c/
作者
GT610
发布于
2024年3月5日
许可协议