为什么gets函数最初被包括在C标准中?

6

每个C程序员都知道,除非标准输入连接到可信源,否则没有办法安全地使用gets函数。但是,在将其作为C标准的官方组成部分之前,为什么C语言的开发人员没有注意到这样一个明显的错误呢?而且,为什么要等到C11才将其从标准中删除,并用一个执行边界检查的函数来替换它呢?我知道通常会使用fgets代替gets,但后者有一个麻烦的习惯,即保留末尾的\n符号。


那时候,性能可能比代码安全更加重要。 - Mike Christensen
因为那是1973年。目标是创建一种易于使用的语言,可以在小型PDP-7上快速编写代码。 - Lee Daniel Crocker
1
你可以用同样的说法来描述strcpy()或许多其他命令。 - Jiminion
1
如果你知道fgets在行末添加了一个'\n'的目的,那么这并不会那么让人烦恼。 - autistic
5个回答

5
答案很简单,C语言非常古老,可以追溯到20世纪70年代初。当该语言首次开发时,并没有我们今天认为理所当然的安全威胁。长时间以来,C语言是AT&T公司内部流行的语言。直到20世纪70年代后期,商业编译器才开始普及。但是,当UNIX操作系统被用C重写后,编译器变得更加容易获取,特别是在Kernighan和Ritchie于1978年出版了标准参考书《C程序设计语言》之后,该语言开始流行起来。
尽管这种语言广泛而且越来越受欢迎,但它本身直到1989年才被标准化。那时,C语言已经近20年了,并且有大量的C代码。标准委员会相对保守;它的工作基于一种假设,即标准将使现有的惯例合法化,而不是要求采用新的做法。与声明大量已安装的代码不合规相比,get() 的缓冲区溢出漏洞似乎微不足道。
1988年的Morris互联网蠕虫确实表明需要更安全的编码实践,但即使如此,在20世纪80年代末,互联网仍然极其初步。 (如果我没记错的话,David Pogue于1990年代初出版的一本Macintosh书回答了如何将Mac连接到互联网的问题,答案是“别费力了,互联网不值得”。)我们几乎不能因标准委员会错误地评估互联网的指数级增长及相关安全威胁而责怪他们。
当标准在1999年修订时,情况当然已经发生了变化。但是,委员会再次选择谨慎起见,以弃用而非完全删除get()。这是否是正确的决定有待商榷,但它并不明显是错误的。
在C11标准中保留get() 显然是错误的决定,当前标准正确地将其删除。但是,你的问题建立在这样一个前提之上,即这一做法一直都是正确的,从历史的角度来看,这个假设似乎是值得质疑的。

他们可能本应该用类似于fgets的东西来替换gets,或者将“您是否已经完成了K&R的《C程序设计语言》中的练习?”放入缓冲区。然后人们可能会注意到使用stdin的大多数解决方案都误用了控制台,如果从一开始就进行一致的设计,就不必如此复杂。无论如何,这个答案似乎是回答实际问题的最佳尝试。 - autistic
我读到当Morris蠕虫传播时,X3J11几乎完成了C标准。顺便说一句,令人沮丧的是,即使我认为RMS在1987年向X3J11提出了建议,snprintf也没有被纳入C89中。 - Yuhong Bao

3

最初,C语言出现在计算机互联网络普及之前。在当时的背景下,如果你使用C编写了一个程序并使用gets()函数,并且抱怨因为输入太大导致程序崩溃,那么回应只会是“那就别这么做!”。“不可信输入”的整个概念几乎是无意义的——输入由操作员显式提供。

C89标准没有删除它,因为标准委员会的主要任务是将现有惯例 codify,此时gets()肯定是现有惯例的一部分。

它在C99中被弃用,作为其删除的第一步,随后在C11中进行了删除,正如您所指出的那样。


是的,记住当时大多数终端甚至无法输入非ASCII字符。虽然有重定向功能,但stdin通常不会被重定向到一个不可信的来源。 - Yuhong Bao

2
最初的ANSI标准的任务是将现有的实践进行编码,而不是发明一种新语言。这在理论文档中已经明确说明:
“最初的X3J11章程明确规定了对常见现有实践进行编码,C89委员会坚持先例,只要那是清晰和明确的。 C89定义的大部分语言与Brian Kernighan和Dennis Ritchie的《C程序设计语言》第一版附录A中定义的完全相同,并且几乎所有C翻译器都实现了该语言。(本文以下简称K&R)。”
因此,由于gets是语言的一部分,它被纳入了标准。还有其他不安全的东西仍然存在,从业者应该知道如何明智地使用他们的工具。
如果您担心多余的换行符,很容易修复:
{
    size_t len = strlen (buffer);
    if ((len > 0) && (buffer[len-1] == '\n'))
        buffer[len-1] = '\0';
}

或者更简单的方式:
buffer[strcspn (buffer, "\n")] = '\n';

你甚至可以编写自己的 fgets 前端来为你完成此操作,例如 这个,显然是由 SO 中更聪明、外表更好看的成员之一编写的 :-)

我突然觉得 Stack Overflow 中最聪明、最帅的成员之一也是最自负的之一... 如果你还不知道,你可以更干净地消除你不关心的 '\n'size_t len = strcspn(buffer, "\n"); buffer[len] = '\0'; - autistic
@undefinedbehaviour:触动了我的心弦 :-) 很棒的代码。我会将修改后的版本添加到答案中。 - paxdiablo

2

无论是否将gets加入标准都存在争议,但委员会决定当程序员对输入有足够的控制时,gets是有用的。

以下是委员会的官方解释。

国际标准——C语言编程语言的理由 §7.19.7.7 gets函数:

由于gets不检查缓冲区溢出,在其输入不在程序员控制下时使用通常是不安全的。这引起了一些人对其是否应该出现在标准中的质疑。委员会决定,在那些程序员对输入有足够控制,并且作为长期存在的实践时,gets是有用和方便的,并需要一个标准规范。然而,在一般情况下,首选函数是fgets(参见§7.19.7.2)。


"[...], 但委员会认为当程序员对输入没有足够的控制时,gets是有用的。" - flarn2006
1
@flarn2006 哦,那是个打错字了,我一会儿就修正。下面引用的部分是正确的。 - Yu Hao

0
早期计算技术的空间和时间限制不允许采用今天普遍使用的更实用的安全措施。为了保持代码兼容性,现有的有缺陷的例程得以维护。

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