我再次需要帮助!我认为使用gets()
函数非常酷,因为它就像scanf()
,其中我可以获得带有空格的输入。但是我在其中一个线程(student info file handling)中读到它并不好用,因为根据它们,它是一个用于创建缓冲区溢出的魔鬼工具(我不明白)
如果我使用gets()
功能,我可以这样做。输入您的姓名:Keanu Reeves
。
如果我使用scanf()
,我只能这样做。输入您的姓名:Keanu
所以我听取了他们的建议,并用gets()
替换了所有fgets()
代码。问题是现在我的一些代码不再起作用了...除gets()
和fgets()
以外的任何函数是否可以读取整行并忽略空格。
答案 0 :(得分:39)
它是用于创建缓冲区溢出的魔鬼工具
因为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';
答案 1 :(得分:13)
正如其他回复所指出的那样,gets()
不会检查缓冲区空间。除了意外溢出问题外,恶意用户还可以利用这种弱点来制造各种各样的破坏。
1988年发布的第一批广泛传播的蠕虫之一,使用gets()
在整个互联网上传播自己。以下是Peter Van Der Linden的 Expert C Programming 的有趣摘录,其中讨论了它是如何工作的:
早期的Bug获取()Internet蠕虫
C中的问题并不仅限于语言。标准库中的一些例程具有不安全的语义。 1988年11月,蠕虫程序在互联网网络上蠕动了数千台机器,这大大证明了这一点。当烟雾消失并且调查完成后,确定蠕虫传播的一种方式是通过finger
守护程序中的弱点,该守护程序通过网络接受有关当前登录的人的查询。{{ 1}}守护程序finger
使用标准I / O例程in.fingerd
。
gets()
的名义任务是从流中读取字符串。调用者告诉它将传入的字符放在哪里。但gets()
不检查缓冲区空间;实际上,它无法检查缓冲区空间。如果调用者提供指向堆栈的指针,并且输入的数量多于缓冲区空间,gets()
将很快覆盖堆栈。 gets()
守护程序包含代码:
finger
这里,main(argc, argv)
char *argv[];
{
char line[512];
...
gets(line);
是在堆栈上自动分配的512字节数组。当用户提供的输入多于line
守护程序的输入时,finger
例程将继续将其置于堆栈中。大多数体系结构都容易被覆盖堆栈中间的现有条目覆盖更大的东西,这也会覆盖相邻的条目。在软件中检查每个堆栈访问的大小和权限的成本是高昂的。知识渊博的犯罪分子可以通过在参数字符串中存储正确的二进制模式来修改堆栈中过程激活记录中的返回地址。这会将执行流程转移回其来源,而是转移到一个特殊的指令序列(也小心地存放在堆栈中),调用gets()
以用shell替换正在运行的图像。 Voilà,您现在正在与远程计算机上的shell而不是execv()
守护程序进行通信,您可以发出命令将病毒副本拖到另一台计算机上。
具有讽刺意味的是,finger
例程是一个过时的函数,它提供了与第一版便携式I / O库的兼容性,并在十多年前被标准I / O取代。该联机帮助页甚至强烈建议始终使用gets()
。 fgets()
例程设置了读取字符数的限制,因此它不会超过缓冲区的大小。 fgets()
守护程序通过替换的两行修复程序变得安全:
finger
行:
gets(line);
这会吞下有限数量的输入,因此无法操作由运行该程序的人覆盖重要位置。但是,ANSI C标准未从语言中删除if (fgets(line, sizeof(line), stdin) == NULL)
exit(1);
。因此,虽然这个特定程序是安全的,但C标准库中的潜在缺陷并未被删除。
答案 2 :(得分:3)
你可以看看这个问题:Safe alternative to gets()
。有很多有用的答案。
您应该更准确地说明为什么您的代码无法与fgets()
一起使用。正如另一个问题中的答案所解释的那样,您必须处理gets()
省略的换行符。
答案 3 :(得分:2)
要使用scanf阅读所有单词,您可以这样做
示例:
printf("Enter name: ");
scanf("%[^\n]s",name); //[^\n] is the trick
答案 4 :(得分:1)
您可以使用scanf()
阅读多个字段,以便执行以下操作:
scanf("%s %s\n", first_name, last_name);
但是,我认为最好先阅读一个字符串,然后自己拆分它,因为它们可能没有输入名字,或者第一个/中间/最后一个。
fgets()
有什么问题?
gets()
的问题在于它会返回与用户输入的字符一样多的字符 - 因为调用者无法控制此字符。所以你可以分配80个字符,用户可以输入100个字符,最后20个字符将写在你分配的内存的末尾,踩到谁知道什么。
答案 5 :(得分: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读取的字符多1个字符。
为了只使用一个常量,我使用了STRINGIZE宏。使用这个宏,我可以使用#define'd常量作为数组大小(对于变量定义)作为字符串(对于说明符)。
还有一个方面值得一提:flushinput
。如果使用gets
,则所有数据都写入内存(即使缓冲区溢出),但不包括换行符。为了模仿它,scanf
读取有限数量的字符,但不包括换行符,与gets
不同,将换行保留在输入缓冲区。因此需要删除换行符,这就是flushinput
所做的。
其余代码主要是设置测试环境。