C语言中的gets()函数

30
我认为使用gets()函数很酷,因为它类似于scanf(),可以获取包含空格的输入。但我在一个线程中读到(学生信息文件处理),他们说不建议使用它,因为被称为创建缓冲区溢出的恶魔工具(我不理解)。
如果我使用gets()函数,我可以做到这一点。 输入您的姓名:Keanu Reeves
如果我使用scanf(),我只能这样做。 输入您的姓名:Keanu 所以我听从他们的建议,用fgets()替换了所有的gets()代码。问题是现在我的一些代码不再工作...除了gets()fgets()外,是否有其他函数可以读取整行并忽略空格?
6个回答

41

它是一个用于创建缓冲区溢出的恶魔工具

因为gets不接受长度参数,所以它不知道你的输入缓冲区有多大。如果你传入一个10个字符的缓冲区而用户输入了100个字符——嗯,你懂的。

fgets是一个更安全的替代gets的方法,因为它将缓冲区长度作为参数,所以你可以这样调用它:

fgets(str, 10, stdin);

它最多会读取9个字符。

问题是现在我的一些代码不再工作了

这可能是因为 fgets 也会将结尾的换行符 (\n) 存储在缓冲区中 -- 如果你的代码没有考虑到这一点,你应该手动删除它:

int len = strlen(str);
if (len > 0 && str[len-1] == '\n')
  str[len-1] = '\0';

2
@casablanka:在引用 len - 1 之前,你需要检查 len 是否大于 0。 - unwind
2
注意,您应该始终捕获 fgets() 的结果:if (fgets(str, 10, stdin) != 0) { ...OK... } else { ...EOF 或错误... }。不建议使用 ferror()feof();我们刚刚修复了 AIX 上的性能 bug,该代码忽略了主要 I/O 函数(如 fputs())的错误,并改用了 ferror(),这会导致明显的减速。 - Jonathan Leffler
gets()函数会自动在字符串末尾添加空字符(Null-Terminator),但不会添加换行符(\n),对吗? - Minh Tran
@MinhTran:是的,如果你查看文档,它说gets会丢弃换行符,但fgets会保留它。这两个函数都会在字符串结尾附加一个空终止符。 - casablanca

14

正如其他回答中所指出的,gets()不检查缓冲区空间。除了意外溢出问题之外,这个弱点可以被恶意用户用来制造各种混乱。

1988年发布的第一个广泛传播的蠕虫程序就利用了gets()在互联网上进行自我传播。以下是Peter Van Der Linden在《专家C编程》一书中讨论它是如何工作的有趣摘录:

早期漏洞获取互联网蠕虫

C语言中的问题不仅限于语言本身。标准库中的一些例程具有不安全的语义。这在1988年11月通过在Internet网络上数千台计算机上爬行的蠕虫程序而得到了明显证明。当烟雾散去,调查完成时,确定蠕虫传播的其中一种方式是通过finger服务中的一个漏洞,该服务接受有关当前登录用户的网络查询。finger服务(in.fingerd)使用了标准I/O例程gets()

gets()的名义任务是从数据流中读取一个字符串。调用者告诉它将输入字符放到哪里。但是gets()不检查缓冲区空间;事实上,它无法检查缓冲区空间。如果调用者提供指向堆栈的指针,并且输入超过了缓冲区空间,gets()将乐意覆盖堆栈。finger服务包含以下代码:

main(argc, argv)
char *argv[];
{
char line[512];
...
gets(line);

在这里,line是一个自动分配在堆栈上的512字节数组。当用户提供给finger守护进程的输入超过这个大小时,gets()例程会一直将其放在堆栈上。大多数体系结构都容易被覆盖堆栈中现有条目的攻击,因为它们也覆盖了相邻的条目。在软件中检查每个堆栈访问的大小和权限的成本是禁止性的。一位有经验的恶意行为者可以通过将正确的二进制模式隐藏在参数字符串中来修改堆栈上的过程激活记录中的返回地址。这将使执行流不会返回到原来的位置,而是转到一个特殊指令序列(也小心翼翼地放置在堆栈上),该序列调用execv()来替换运行中的图像,以获取与远程计算机上的shell通信的能力。Voilà,现在您正在与远程计算机上的shell通信,而不是与finger守护进程通信,您可以发出命令将病毒复制到另一台计算机上。

具有讽刺意味的是,gets()例程是一个过时的函数,它提供了与可移植I/O库的第一个版本兼容性,并在十多年前被标准I/O所取代。man页甚至强烈建议始终使用fgets()fgets()例程设置了读取字符数的限制,因此它不会超出缓冲区的大小。finger守护进程通过两行修复变得更加安全,其替换如下:

gets(line);

按行:

if (fgets(line, sizeof(line), stdin) == NULL)
exit(1);

这个函数只能接受有限数量的输入,因此不能被程序运行者利用来覆盖重要的内存地址。然而,ANSI C标准并没有从语言中移除gets()函数。因此,虽然这个特定程序变得更加安全了,但是C标准库中潜在的缺陷并未被修复。


2
C1X标准将删除gets();实现这个标准需要很长时间。希望GNU能够用char *gets(char *buffer) { abort(); }替换当前的gets()函数。 - Jonathan Leffler

4
您可以查看这个问题: Safe alternative to gets()。那里有许多有用的答案。
您应该更加准确地说明为什么您的代码不能使用fgets()。正如其他问题中的答案所解释的那样,您必须处理gets()省略的换行符。

2

要使用scanf读取所有单词,可以按照以下方式进行:

示例:

printf("Enter name: ");

scanf("%[^\n]s",name);       //[^\n] is the trick

1

你可以使用scanf()读取多个字段,例如:

scanf("%s %s\n", first_name,  last_name);

然而,我认为最好读取一个字符串,然后自己拆分它,因为他们可能不仅输入了名字,还有中间名和姓氏。

你在使用 fgets() 时遇到了什么问题?

gets() 的问题在于它返回用户输入的所有字符 - 作为调用者,你无法控制这一点。所以你可能分配了80个字符,用户可能会输入100个字符,最后20个字符将被写入你已经分配的内存的末尾,覆盖了谁知道什么的内容。


作为一个设计问题,如果您不想处理用户输入名,名+姓或名+中间名+姓,我建议分别提示他们输入姓和名。 - The Archetypal Paul
我也想把名字分成名、中间名和姓... 但是我们老师给了我们把它放在数组名[31]中的规定。 - newbie
没问题。分别提示它们,然后将它们连接起来放入数组名称中。 - The Archetypal Paul

1

你可以使用scanf来模仿gets。但这并不是很好看。

#include <stdio.h>

#define S_HELPER(X) # X
#define STRINGIZE(X) S_HELPER(X)
#define MAX_NAME_LEN 20

int flushinput(void) {
  int ch;
  while (((ch = getchar()) != EOF) && (ch != '\n')) /* void */;
  return ch;
}

int main(void) {
  char name[MAX_NAME_LEN + 1] = {0};

  while (name[0] != '*') {
    printf("Enter a name (* to quit): ");
    fflush(stdout);
    scanf("%" STRINGIZE(MAX_NAME_LEN) "[^\n]", name); /* safe gets */
    if (flushinput() == EOF) break;
    printf("Name: [%s]\n", name);
    puts("");
  }

  return 0;
}

最好使用fgets进行读取,如果需要解析,则使用sscanf


编辑:解释scanf调用及其周围代码。

scanf的"%["转换规范接受一个最大字段宽度,不包括空终止符。因此,用于保存输入的数组必须比使用scanf读取的字符多1个。

为了只使用一个常量来实现这一点,我使用了STRINGIZE宏。使用此宏,我可以将#define的常量同时用作数组大小(用于变量定义)和字符串(用于说明符)。

还有一个值得一提的方面:flushinput。如果使用gets,所有数据都会被写入内存(即使缓冲区溢出),直到但不包括换行符。为了模仿这一点,scanf读取有限数量的字符,直到但不包括换行符,并且与gets不同,保留换行符在输入缓冲区中。因此,需要删除该换行符,这就是flushinput的作用。

其余的代码主要是为了设置测试环境。


对我来说,你的代码相当难懂。但我会努力学习。谢谢。 - newbie
我的意思是“虽然不太好看” :-) 已编辑,我会在代码中添加一些注释。 - pmg
这并不完全等同于 gets(); 它不读取换行符。此外,在格式字符串的末尾添加 \n 不是解决方案(这将是一个惊人的糟糕主意)。你可能需要使用 getchar() 读取下一个字符,并使用 unputc() 如果它不是换行符(或 EOF)。 - Jonathan Leffler
2
@JonathanLeffler:在格式字符串的末尾添加“%*1[\n]”以消耗换行符(如果存在)。 - Chris Dodd
@ChrisDodd:太棒了! - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接