当我使用GCC编译使用gets()
函数的C代码时,会收到以下警告:
(.text+0x34): 警告:`gets'函数是危险的,不应该使用。
我记得这与堆栈保护和安全有关,但我不确定具体原因。
如何消除此警告?为什么使用gets()
会产生这样的警告?
如果gets()
如此危险,为什么我们不能将其删除?
当我使用GCC编译使用gets()
函数的C代码时,会收到以下警告:
(.text+0x34): 警告:`gets'函数是危险的,不应该使用。
我记得这与堆栈保护和安全有关,但我不确定具体原因。
如何消除此警告?为什么使用gets()
会产生这样的警告?
如果gets()
如此危险,为什么我们不能将其删除?
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);
现代Linux编译系统会在链接gets()
以及其他存在安全问题的函数(mktemp()
等)时生成警告。
gets()
的替代方法fgets()
正如其他人所说,可以使用fgets()
并将stdin
作为文件流来替代gets()
。
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')
;
}
gets()
时不会出现,因为它不知道你的缓冲区在哪里结束,随意践踏超过尾部,在美好地布置内存布局上制造破坏,通常会混乱返回堆栈(Stack Overflow)如果缓冲区在堆栈上分配,或者践踏控制信息如果缓冲区是动态分配的,或者复制数据到其他珍贵的全局(或模块)变量如果缓冲区是静态分配的。这些都不是一个好主意——它们是“未定义行为”的典型。
还有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);
s
不得为 null 指针。n
既不等于零也不大于 RSIZE_MAX。从 stdin
读取 n-1
个字符时,应出现新行字符、文件结尾或读取错误。25)
如果存在运行时约束违规情况,则将 s[0]
设置为 null 字符,并从 stdin
中读取并丢弃字符,直到读取到一个新行字符或文件结尾或发生读取错误为止。
gets_s
函数最多从指向 stdin
的流中读取 n
指定的字符数减一的数量,读入指向 s
的数组。在新行字符(被丢弃)或文件结尾之后,不会读取其他字符。被丢弃的新行字符不计入读取的字符数。null 字符将立即写入到数组中最后一个读取的字符之后。
如果在操作期间遇到文件结尾并且没有将任何字符读入数组中,或者发生读取错误,则将 s[0]
设置为 null 字符,并使 s
的其他元素采用未指定的值。
6
fgets
函数允许适当编写的程序安全地处理输入行,这些行太长而无法存储在结果数组中。一般来说,调用fgets
的调用者需要注意结果数组中是否存在换行符。考虑使用fgets
(以及基于换行符的任何必要处理)来代替gets_s
。
25)与
gets
不同,gets_s
函数将使输入行溢出缓冲区而无法存储成为运行时约束违规。与fgets
不同,gets_s
保持输入行与成功调用gets_s
之间的一对一关系。使用gets
的程序期望这种关系。
Microsoft Visual Studio编译器实现了TR 24731-1标准的近似版本,但Microsoft实现的签名与TR中的签名存在差异。
C11标准ISO/IEC 9899-2011将TR24731作为库的可选部分包含在附录K中。不幸的是,在类Unix系统上很少实现它。
getline()
— POSIXPOSIX 2008提供了一个安全的替代gets()
的方法,称为getline()
。它动态分配行空间,因此您最终需要释放它。它消除了对行长度的限制。它还返回读取的数据长度,或者-1
(而不是EOF
!),这意味着输入中的空字节可以可靠地处理。还有一种“选择自己的单字符定界符”的变体,称为getdelim()
;如果您正在处理来自find -print0
的输出,其中文件名的末尾用ASCII NUL '\0'
字符标记,那么这可能很有用。
fgets()
和您的 fgets_wrapper()
版本都会将超长行的尾部留在输入缓冲区中,以供下一个输入函数读取。在许多情况下,您需要读取和丢弃这些字符。 - cafgetline()
及其相关的getdelim()
,它们返回由命令读取的'行'的长度,并分配所需的空间以便能够存储整个行。即使如此,在你有一个单行JSON文件大小为几个GB时也会导致问题;你能承担所有这些内存吗?(顺便问一下,我们能否有返回指向末尾的空字节的strcpy()
和strcat()
变体等等?) - Jonathan Lefflerfgets()
的另一个问题是,如果文件包含空字节(null byte),你无法确定在空字节后到行末(或EOF)还有多少数据。strlen()
只能报告数据中空字节之前的长度;在空字节之后,就只能靠猜测了,因此几乎肯定会出错。 - Jonathan Lefflergets()
存在的事情。当我这样做时,我又会遇到它并回到这里。你是在黑客StackOverflow以获取赞吗? - candied_orangegets()
函数在glibc中包含一个pragma或attribute,导致编译器在使用时发出警告。 - fuz因为 gets
在从 stdin 获取字节并将它们放在某个地方时不会执行任何检查。一个简单的例子:
char array1[] = "12345";
char array2[] = "67890";
gets(array1);
首先,你可以输入任意数量的字符,gets
不会限制。其次,如果你输入的字节数超过了数组(在本例中是 array1
)的大小,它们就会覆盖内存中的任何东西,因为 gets
将写入它们。在前面的例子中,这意味着如果你输入 "abcdefghijklmnopqrts"
,可能会不可预测地覆盖 array2
或其他内容。
这个函数是不安全的,因为它假设输入始终是一致的。永远不要使用它!
gets
完全无法使用的原因是它没有接受数组长度/计数参数;如果有这个参数,它就只是另一个普通的 C 标准函数。 - legends2kgets
的预期用途是什么,为什么没有制作标准的fgets变体以方便那些不希望换行符成为输入的一部分的用例? - supercatgets
函数的名称暗示它是用来从stdin
中获取字符串的,然而对于没有包含_size_参数的理由可能是出于_C语言的精神_: 信任程序员。这个函数在_C11_中被移除,并且提供了一个替代方案gets_s
,它需要输入缓冲区的大小。至于fgets
部分我不清楚。 - legends2kgets
。在这种情况下,如果硬件无法提交超过127字节长的行,则将其gets
到128字节缓冲区中可能是合理的,尽管我认为当期望较小的输入时能够指定较短的缓冲区的优点足以弥补成本。 - supercatrealloc
,使得一个只接受字符串参数的gets
可以接受本地堆栈上的小数组的直接引用,或者是一个“自动调整大小的堆字符串”描述符的引用(允许根据需要分配堆存储)。 - supercat你不应该使用 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
相同的保护措施,以防止缓冲区溢出,但它还会通知调用者发生了什么,并清除多余的字符,以使它们不影响您的下一个输入操作。
请随意使用它,我在此释放它使用“你他妈想干啥就干啥”的许可证:-)
gets()
的7.19.7.7节或<stdio.h>
子节中的7.26.9未明确弃用它。甚至没有关于它危险性的脚注。(话虽如此,我在Yu Hao的答案中看到了"它在ISO/IEC 9899:1999/Cor.3:2007(E)中被弃用")。但是,C11从标准中删除了它——这是时候了! - Jonathan Lefflerint getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)
隐藏了 size_t
到 int
的转换。 sz > INT_MAX || sz < 2
可以捕捉到 sz
的异常值。 - chux - Reinstate Monicafgets函数。
从标准输入读取:
char string[512];
fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
我最近在一篇发给comp.lang.c
的USENET帖子中读到,gets()
将从标准中删除。太好了!
你会很高兴地知道,委员会刚刚投票(结果是全票通过),决定将gets()从草案中删除。
gcc -std=c2012 -pedantic ...
进行编译时,gets() 将无法通过。(我只是举例说明了 -std
参数) - pmg在C11(ISO/IEC 9899:201x)中,gets()
已被删除。(它在ISO/IEC 9899:1999/Cor.3:2007(E)中被弃用)
除了fgets()
,C11还引入了一个新的安全替代品gets_s()
:
C11 K.3.5.4.1 The
gets_s
function
#define __STDC_WANT_LIB_EXT1__ 1 #include <stdio.h> char *gets_s(char *s, rsize_t n);
然而,在推荐实践部分,仍然更喜欢使用fgets()
。
fgets
函数允许编写正确的程序安全地处理输入行,这些行太长而无法存储在结果数组中。一般来说,这要求调用者注意结果数组中是否存在换行符。考虑使用fgets
(以及基于换行符的任何所需处理)代替gets_s
。
scanf("%s", arr)
选项和gets()
一样有问题。 - undefinedgets()
是危险的,因为用户可能通过在提示中键入过多内容来使程序崩溃。它无法检测可用内存的结束,因此如果您为特定目的分配了太小的内存量,则可能会导致段错误并崩溃。有时,似乎很不可能让用户在为人名而设计的提示中键入1000个字母,但作为程序员,我们需要使我们的程序防弹。(如果用户可以通过发送过多数据来使系统程序崩溃,这也可能是安全风险)。
fgets()
允许您指定从标准输入缓冲区中取出多少个字符,以便它们不会超出变量范围。
gets()
缓冲区溢出攻击gets()
函数是一种不安全的输入方法,因为它无法检测用户输入的大小。攻击者可以利用这个漏洞来向程序中的缓冲区写入超出其分配大小的数据,导致缓冲区溢出攻击。攻击者可以利用这种漏洞执行恶意代码或者修改程序的逻辑。为了避免这种攻击,建议使用更加安全的输入方法,如fgets()
。 - EsmaeelEgets()
- EsmaeelEscanf("%s", b)
存在与gets
相同的问题。 - William Pursellgets()
! - Andrew