为什么gets函数如此危险以至于不应该使用它?

当我尝试用GCC编译使用gets()函数的C代码时,我得到这个警告:

(.text+0x34):警告:' gets'函数是危险的,不应该使用。

我记得这与堆栈保护和安全性有关,但我不确定确切的原因。

如何删除此警告,为什么使用gets()会有这样的警告?

如果gets()是如此危险,那么为什么我们不能删除它?

218956 次浏览

为了安全地使用gets,你必须确切地知道你将读取多少字符,这样你就可以使你的缓冲区足够大。只有当您确切地知道要读取哪些数据时,您才会知道这一点。

与其使用gets,不如使用具有签名的fgets

char* fgets(char *string, int length, FILE * stream);

(fgets,如果它读取整行,将在字符串中保留'\n';你得自己处理。)

直到1999年ISO C标准,gets仍然是该语言的官方部分,但它在2011标准中被正式删除。大多数C实现仍然支持它,但至少gcc会对任何使用它的代码发出警告。

如果不破坏API,就不能删除API函数。如果您这样做,许多应用程序将不再编译或运行。

这就是一个引用给出的原因:

读取溢出的行 由s指向的数组结果为 未定义的行为。fgets()的使用 建议。< / p >

因为gets在从stdin获取字节并将它们放在某个地方时不做任何检查。举个简单的例子:

char array1[] = "12345";
char array2[] = "67890";


gets(array1);

现在,首先你可以输入你想要多少个字符,gets不会关心它。其次,超出数组大小的字节(在本例中为array1)将覆盖它们在内存中找到的任何内容,因为gets将写入它们。在前面的例子中,这意味着如果你输入"abcdefghijklmnopqrts",它也会不可预知地覆盖array2或其他东西。

该函数是不安全的,因为它假定输入一致。永远不要用它!

我最近在一篇USENET post到comp.lang.c中读到,gets()从标准中被删除了。哦吼

你会很高兴知道 委员会刚刚投票(一致通过) 将gets()从

函数可以

从stdin读取:

char string[512];


fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */

为什么gets()是危险的

第一个互联网蠕虫(莫里斯网络蠕虫)大约在30年前(1988-11-02)逃脱,它使用gets()和缓冲区溢出作为从一个系统传播到另一个系统的方法之一。基本的问题是,函数不知道缓冲区有多大,所以它继续读取,直到找到换行符或遇到EOF,并可能溢出给定的缓冲区边界。

你应该忘记你曾经听说过gets()的存在。

C11标准ISO/IEC 9899:2011取消了gets()作为标准函数,这是一个好东西™(它在ISO/IEC 9899:1999/Cor中被正式标记为“过时”和“已弃用”。3:2007 - C99的技术勘误3,然后在C11中删除)。遗憾的是,由于向后兼容的原因,它将在库中保留很多年(意思是“几十年”)。如果由我来决定,gets()的实现将变成:

char *gets(char *buffer)
{
assert(buffer != 0);
abort();
return 0;
}

考虑到您的代码迟早会崩溃,最好是尽早解决问题。我会准备添加一个错误消息:

fputs("obsolete and dangerous function gets() called\n", stderr);

如果你链接gets(),现代版本的Linux编译系统会生成警告,其他一些函数也有安全问题(mktemp(),…)。

gets()的替代方案

fgets ()

正如其他人所说,gets()的规范替代是fgets(),指定stdin作为文件流。

char buffer[BUFSIZ];


while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
...process line of data...
}

没有人提到的是gets()不包括换行符,但fgets()包含。因此,你可能需要使用fgets()的包装器来删除换行符:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
return buffer;
}
return 0;
}

或者,更好:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
buffer[strcspn(buffer, "\n")] = '\0';
return buffer;
}
return 0;
}

同样,正如caf指出的那样,在评论中和paxdiablo在他们的回答中显示的那样,使用fgets()可能会在一行上留下数据。我的包装器代码将这些数据留待下次读取;如果你喜欢,你可以很容易地修改它以吞噬其余的数据行:

        if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
else
{
int ch;
while ((ch = getc(fp)) != EOF && ch != '\n')
;
}

剩余的问题是如何报告三种不同的结果状态——EOF或错误,行读取而未截断,部分行读取但数据被截断。

这个问题不会出现在gets()中,因为它不知道你的缓冲区在哪里结束,并愉快地践踏结束,破坏你美丽的内存布局,如果缓冲区分配在堆栈上,通常会弄乱返回堆栈(堆栈溢出),如果缓冲区是动态分配的,则践踏控制信息,如果缓冲区是静态分配的,则将数据复制到其他重要的全局(或模块)变量上。这些都不是好主意——它们是“未定义行为”的缩影。


还有TR 24731 - 1(来自C标准委员会的技术报告),它为各种功能提供了更安全的替代方案,包括gets():

§6.5.4.1 gets_s函数

# # #剧情简介

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Runtime-constraints

s不能是空指针。n既不等于0也不大于RSIZE_MAX。从stdin.25)读取n-1字符时,将出现换行符、文件结束符或读取错误

3如果存在违反运行时约束的情况,s[0]将被设置为空字符,并且从stdin中读取并丢弃字符,直到读取换行字符,或文件结束或发生读取错误。

描述

gets_s函数从stdin指向的流读入s指向的数组,最多比n指定的字符数少一个。在换行符(被丢弃)之后或文件结束之后不读取其他字符。丢弃的换行字符不计入读取的字符数。空字符在最后一个字符读入数组后立即写入。

5如果遇到文件结束符并且没有字符被读入数组,或者在操作过程中发生了读取错误,则s[0]将被设置为空字符,并且s的其他元素取未指定的值。

推荐的做法

fgets函数允许正确编写的程序安全地处理太长而不能存储在结果数组中的输入行。一般来说,这要求fgets的调用者注意结果数组中是否存在换行字符。考虑使用fgets(以及任何基于换行符的必要处理)而不是gets_s

25) gets_s函数与gets函数不同,它使输入行溢出缓冲区以存储它成为违反运行时约束的行为。与fgets不同,gets_s在输入行和成功调用gets_s之间保持一对一的关系。使用gets的程序期望这样的关系。

Microsoft Visual Studio编译器实现了近似于TR 24731-1标准的签名,但是Microsoft实现的签名与TR中实现的签名之间存在差异。

C11标准,ISO/IEC 9899-2011,在附录K中包含TR24731作为库的可选部分。不幸的是,它很少在类unix系统上实现。


__abc0 - posix

POSIX 2008还提供了一个安全的gets()替代方案,称为getline()。它动态地为该行分配空间,因此您最终需要释放它。因此,它消除了对行长度的限制。它还返回被读取的数据的长度,或-1(而不是EOF!),这意味着可以可靠地处理输入中的空字节。还有一种名为getdelim()的“选择自己的单字符分隔符”变体;例如,如果你正在处理find -print0的输出,其中文件名的结尾用ASCII NUL '\0'字符标记,这可能很有用。

你不应该使用gets,因为它没有办法停止缓冲区溢出。如果用户输入的数据超过了缓冲区的容量,您很可能会出现损坏或更糟的情况。

事实上,ISO实际上已经从C标准中采取了删除 gets的步骤(截至C11,尽管它在C99中被弃用),考虑到他们对向后兼容性的评价有多高,应该表明该函数有多糟糕。

正确的做法是使用fgets函数和stdin文件句柄,因为你可以限制从用户读取的字符。

但这也有它的问题,如:

  • 用户输入的额外字符将在下次读取。
  • 没有用户输入过多数据的快速通知。

为此,几乎每个C编码器在他们职业生涯的某个时候都会编写一个更有用的fgets包装器。这是我的:

#include <stdio.h>
#include <string.h>


#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
int ch, extra;


// Get line with buffer overrun protection.
if (prmpt != NULL) {
printf ("%s", prmpt);
fflush (stdout);
}
if (fgets (buff, sz, stdin) == NULL)
return NO_INPUT;


// If it was too long, there'll be no newline. In that case, we flush
// to end of line so that excess doesn't affect the next call.
if (buff[strlen(buff)-1] != '\n') {
extra = 0;
while (((ch = getchar()) != '\n') && (ch != EOF))
extra = 1;
return (extra == 1) ? TOO_LONG : OK;
}


// Otherwise remove newline and give string back to caller.
buff[strlen(buff)-1] = '\0';
return OK;
}

用一些测试代码:

// Test program for getLine().


int main (void) {
int rc;
char buff[10];


rc = getLine ("Enter string> ", buff, sizeof(buff));
if (rc == NO_INPUT) {
printf ("No input\n");
return 1;
}


if (rc == TOO_LONG) {
printf ("Input too long\n");
return 1;
}


printf ("OK [%s]\n", buff);


return 0;
}

它提供了与fgets相同的保护,因为它可以防止缓冲区溢出,但它也会通知调用者发生了什么,并清除多余的字符,以便它们不会影响您的下一个输入操作。

随心所欲地使用它,我在此发布它的“做你该死的想做的事情”许可证:-)

在C11(ISO/IEC 9899:201x)中,gets()已被删除。(在ISO/IEC 9899:1999/ co .3:2007(E)中已弃用)

除了fgets()之外,C11引入了一个新的安全替代gets_s():

c .3.5.4.1 gets_s函数

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

然而,在推荐的做法部分中,fgets()仍然是首选。

fgets函数也允许正确编写的程序安全地处理输入行 在结果数组中存储。一般来说,这需要fgets的调用者支付 注意结果数组中是否存在换行字符。考虑 使用fgets(以及任何基于换行符的必要处理)而不是 gets_s . < / p >

我想向所有仍然在库中包含gets的C库维护者发出诚挚的邀请,“以防有人仍然依赖它”:请将您的实现替换为

char *gets(char *str)
{
strcpy(str, "Never use gets!");
return str;
}

这将有助于确保没有人仍然依赖它。谢谢你!

C语言的gets函数是危险的,是一个代价非常高昂的错误。Tony Hoare在他的演讲“Null References: The Billion Dollar Mistake”中特别提到了这一点:

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

整个小时都值得一看,但他从30分钟开始的评论观点在39分钟左右受到了批评。

希望这能激发你对整个演讲的兴趣,让你注意到我们如何需要语言中更正式的正确性证明,以及语言设计者如何应该为他们语言中的错误而受到指责,而不是程序员。这似乎是糟糕语言的设计者打着“程序员自由”的幌子把责任推给程序员的全部可疑原因。

gets()是危险的,因为用户可能会因为在提示符中输入太多而使程序崩溃。它不能检测可用内存的结束,因此如果您分配的内存量太小,它可能会导致segg错误和崩溃。有时用户在提示符中输入1000个字母似乎不太可能,但作为程序员,我们需要让我们的程序防弹。(如果用户发送太多数据可能导致系统程序崩溃,这也可能是一个安全风险)。

fgets()允许你指定从标准输入缓冲区中取出多少字符,这样它们就不会溢出变量。

简单地说,gets()(可能)是危险的,因为用户可能输入比变量有足够空间存储的内容更大的内容。第一个答案是关于fgets()和为什么它更安全。

额外的信息:

从Linux Ubuntu上的man 3 gets中,你将看到(强调添加):

DESCRIPTION
Never use this function.

并且,从cppreference.com wiki here (https://en.cppreference.com/w/c/io/gets)你会看到:Notes Never use gets().:

笔记

gets()函数不执行边界检查,因此这个函数非常容易受到缓冲区溢出攻击。它不能安全使用(除非程序运行在限制stdin上可以出现内容的环境中)。因此,该函数在C99标准的第三个勘误表中已被弃用,并在C11标准中被完全删除。fgets()gets_s()是推荐的替换。

永远不要使用gets()

如您所见,该函数在C11或更高版本中已完全弃用并被删除。

使用fgets()gets_s()代替。

下面是我使用fgets()的演示,带有完整的错误检查:

< >强read_stdin_fgets_basic_input_from_user.c < / >强:

#include <errno.h>   // `errno`
#include <stdio.h>   // `printf()`, `fgets()`
#include <stdlib.h>  // `exit()`
#include <string.h>  // `strerror()`


// int main(int argc, char *argv[])  // alternative prototype
int main()
{
char buf[10];


// NEVER USE `gets()`! USE `fgets()` BELOW INSTEAD!


// USE THIS!: `fgets()`: "file get string", which reads until either EOF is
// reached, OR a newline (`\n`) is found, keeping the newline char in
// `buf`.
// For `feof()` and `ferror()`, see:
// 1. https://en.cppreference.com/w/c/io/feof
// 1. https://en.cppreference.com/w/c/io/ferror
printf("Enter up to %zu chars: ", sizeof(buf) - 1); // - 1 to save room
// for null terminator
char* retval = fgets(buf, sizeof(buf), stdin);
if (feof(stdin))
{
// Check for `EOF`, which means "End of File was reached".
// - This doesn't really make sense on `stdin` I think, but it is a good
//   check to have when reading from a regular file with `fgets
//   ()`. Keep it here regardless, just in case.
printf("EOF (End of File) reached.\n");
}
if (ferror(stdin))
{
printf("Error indicator set. IO error when reading from file "
"`stdin`.\n");
}
if (retval == NULL)
{
printf("ERROR in %s(): fgets() failed; errno = %i: %s\n",
__func__, errno, strerror(errno));


exit(EXIT_FAILURE);
}


size_t num_chars_written = strlen(buf) + 1; // + 1 for null terminator
if (num_chars_written >= sizeof(buf))
{
printf("Warning: user input may have been truncated! All %zu chars "
"were written into buffer.\n", num_chars_written);
}
printf("You entered \"%s\".\n", buf);




return 0;
}

示例运行和输出:

eRCaGuy_hello_world/c$ gcc -Wall -Wextra -Werror -O3 -std=c17 read_stdin_fgets_basic_input_from_user.c -o bin/a && bin/a
Enter up to 9 chars: hello world!
Warning: user input may have been truncated! All 10 chars were written into buffer.
You entered "hello wor".


eRCaGuy_hello_world/c$ gcc -Wall -Wextra -Werror -O3 -std=c17 read_stdin_fgets_basic_input_from_user.c -o bin/a && bin/a
Enter up to 9 chars: hey
You entered "hey
".