必须(应该)避免使用标准库中的哪些函数?


90

我在Stack Overflow上已经读到一些C函数“过时”或“应避免”。您能给我一些这种功能的例子及其原因吗?

这些功能有哪些替代方案?

我们可以安全地使用它们吗?有任何好的做法吗?


3
我相信这是一件非常好的事情。确实应该避免使用C的某些功能,并且仅将其用于教育目的。
INS 2010年

@Felix,将来编辑一个(标记为正确的)答案会更容易。答案可能会随着时间的流逝而改变,这真的是悬而未决。谁知道呢,也许杰夫会在接下来的几年中向那些保持最新答案的人提供一个“管理员”徽章。
Tim Post

1
@Tim Post:好的,我将删除我的评论。
Felix Kling

3
我不会用strncpy()代替它strcpy(),也不会用strncat(),因为它具有可以想象到的最直观的界面-您知道length参数指定了什么吗?
乔纳森·莱夫勒

2
使用strncpy和strncat几乎总是一个错误。当然,不应该使用它们代替strcpy和strcat!如果您知道如何使用scanf和sprintf,它们也可以完美使用...
R .. GitHub停止帮助ICE 2010年

Answers:


58


不安全
不推荐使用的函数此函数的一个完美示例是gets(),因为没有办法告诉它目标缓冲区有多大。因此,任何使用gets()读取输入的程序都具有缓冲区溢出漏洞。出于类似的原因,应该使用strncpy()代替strcpy()strncat()代替strcat()

由于覆盖临时文件的潜在安全性问题,还有更多示例包括tmpfile()mktemp()函数,这些示例将由更安全的mkstemp()函数取代。

非可重入
其他示例包括非可重入的gethostbyaddr()gethostbyname()(因此,不能保证是线程安全的),并且已被可重入的getaddrinfo()freeaddrinfo()取代。

您可能在这里注意到了一种模式……缺乏安全性(可能是由于未在签名中包含足够的信息以可能无法安全地实现它)或不重新输入是不赞成使用的常见原因。

过时的,不可移植的
一些其他功能只是因为它们重复了功能并且不像其他变体那样具有可移植性而变得过时。例如,不建议使用bzero(),而推荐使用memset()

线程安全和重入
您在帖子中询问了线程安全和重入。有细微的差别。如果函数不使用任何共享的可变状态,则该函数是可重入的。因此,例如,如果将其需要的所有信息都传递到函数中,并且还将所需的任何缓冲区也传递给了该函数(而不是由对该函数的所有调用所共享),则它是可重入的。这意味着通过使用独立的参数,不同的线程不会冒意外共享状态的风险。重入是线程安全性的有力保证。如果一个函数可以同时被多个线程使用,则它是线程安全的。在以下情况下,函数是线程安全的:

  • 它是可重入的(即,两次调用之间不共享任何状态),或者:
  • 它是不可重入的,但是根据共享状态的需要使用同步/锁定。

通常,在Single UNIX规范IEEE 1003.1(即“ POSIX”)中,不能保证任何不能重入的函数都不能保证是线程安全的。因此,换句话说,只有保证可重入的函数才能在多线程应用程序中移植(无需外部锁定)。但是,这并不意味着这些标准的实现不能选择使非可重入函数成为线程安全的。例如,Linux经常向非可重入函数添加同步,以增加对线程安全性的保证(超出Single UNIX规范的保证)。

字符串(
通常是内存缓冲区),您还询问字符串/数组是否存在一些基本缺陷。有人可能会说是这种情况,但我会说不,这种语言没有根本缺陷。C和C ++要求您分别传递数组的长度/容量(与某些其他语言不同,它不是“ .length”属性)。本质上,这不是缺陷。任何C和C ++开发人员只要在需要的地方将长度作为参数传递,就可以编写正确的代码。问题在于,需要此信息的多个API无法将其指定为参数。或假设将使用某些MAX_BUFFER_SIZE常量。现在已弃用了此类API,并已替换为允许指定数组/缓冲区/字符串大小的API。

Scanf(回答您的最后一个问题)就
个人而言,我使用C ++ iostreams库(std :: cin,std :: cout,<<和>>运算符,std :: getline,std :: istringstream,std :: ostringstream等等),因此我通常不会处理。但是,如果我被迫使用纯C,则我个人只是将fgetc()getchar()strtol()strtoul()等结合使用,并手动解析内容,因为我不喜欢varargs或格式字符串。就是说,据我所知,[f] scanf()[f] printf()都没有问题等,只要您自己制作格式字符串,就不要传递任意格式字符串或允许用户输入用作格式字符串,并在适当的地方使用<inttypes.h>中定义的格式宏。(注意,应使用snprintf()代替sprintf(),但这与未能指定目标缓冲区的大小而不是格式字符串的使用有关)。我还应该指出,在C ++中,boost :: format提供了类似printf的格式而没有varargs。


4
“不赞成使用”是一个很强的词,在讨论C ++标准时具有特定的含义。从这个意义上说,不建议使用gets(),strcpy()等。

4
只要您区分“ C标准已弃用”,“ Michael Aaron Safyan已弃用”和“一个或多个未知的人已弃用,这些人希望知道他们在谈论什么[引用]”。问题关于首选的编码样式,而不是C标准,因此后两者是合适的。但是像尼尔一样,在意识到您的陈述无意隐含第一个含义之前,我需要采取双重行动。
史蒂夫·杰索普

11
strncpy通常也应该避免。它不执行大多数程序员认为的操作。它不能保证终止(导致缓冲区溢出),并且可以填充较短的字符串(在某些情况下可能会降低性能)。
Adrian McCarthy

2
@Adrian:我同意你的看法,strncpy()strncat()明智的选择是替代n-less变体。
乔纳森·莱夫勒

4
这个答案散布了关于“用strncpy替换strcpy的废话-我不知道为什么,但是微软告诉我。” strncpy从未打算成为strcpy的安全版本!面对现实,这更加不安全。请参阅为什么strlcpy和strlcat不安全?
伦丁

24

人们再三重复着类似咒语的荒谬断言,即str函数的“ n”版本是安全版本。

如果那是他们的意图,那么它们将始终为null终止字符串。

函数的“ n”个版本是为固定长度字段(例如早期文件系统中的目录条目)编写的,其中仅当字符串不填充该字段时才需要nul终止符。这也是为什么这些函数具有奇怪的副作用的原因,这些副作用如果仅用作替换,效率将很低-以strncpy()为例:

如果s2指向的数组是一个短于n个字节的字符串,则将空字节附加到s1指向的数组中的副本上,直到总共写入n个字节为止。

由于分配给处理文件名的缓冲区通常为4 KB,因此可能会导致性能大幅下降。

如果您想要“应该”的安全版本,请获取-或编写自己的-strl例程(strlcpy,strlcat等),该例程始终使字符串终止并没有副作用。请注意,尽管它们并不是真正安全的,因为它们可以无声地截断字符串-这几乎不是任何实际程序中的最佳做法。在某些情况下可以这样做,但在许多情况下可能会导致灾难性的后果(例如打印出医疗处方)。


1
你是对的strncpy(),但是是错误的strncat()strncat()并非设计用于固定长度的字段-实际上,它的设计是strcat()为了限制串联的字符数。这是很容易以此作为一个“安全strcat()通过保持做多串连时,甚至容易使用它作为一个”安全保留在缓存空间的轨道“ strcpy()”(由目标缓冲区的第一个字符设置为'\0'前调用它)。 strncat() 总是终止目标字符串,并且不写extra '\0'
caf 2010年

2
@caf-是的,但是strncat()完全没有用,因为它将要复制的最大长度(而不是目标缓冲区的长度)作为参数。为了防止缓冲区溢出,您需要知道当前目标字符串的长度,如果知道为什么要使用strncat()-必须再次计算出目​​标长度-而不是仅将strlcat()作为源字符串目标字符串的末尾。
油尺

这仍然不会改变以下事实:您暗示strncat()并不总是终止目标,并且它是设计用于固定长度字段的,这两个都是错误的。
caf 2010年

2
@chrisharris: strncat()无论源字符串的长度如何,它都能正常工作,而strcat()不会。strlcat()这里的问题是它不是标准的C函数。
David Thornley 2010年

@caf-您对strncat()是正确的。我从来没有实际使用过它,因为-正如我上面指出的-它完全没有用。因此,仍应避免这种情况。
油尺

19

这里有几个答案建议使用strncat()over strcat(); 我建议strncat()(和strncpy())也应避免。它有一些问题,使其难以正确使用并导致错误:

  • 的length参数strncat()与可以复制到目标的最大字符数(而非目标缓冲区的大小)相关(但不完全相同,请参见第3点)。这使strncat()使用起来比应有的难度更大,尤其是如果将多个项目串联到目的地时。
  • 可能很难确定结果是否被截断(可能重要,也可能不重要)
  • 容易出现一个错误的错误。正如C99标准所指出的,“因此,对于看起来像这样的调用,可以在指向的数组中结尾的最大字符数s1strlen(s1)+n+1strncat( s1, s2, n)

strncpy()还有一个问题,可能会导致您尝试以直观的方式使用它时会遇到错误-它不能保证目标为null终止。为了确保您必须确保自己专门处理该极端情况,方法是'\0'自己将a 放到缓冲区的最后一个位置(至少在某些情况下)。

我建议使用类似OpenBSD的strlcat()strlcpy()(虽然我知道有些人不喜欢这样的功能,我相信他们更容易安全地使用比strncat()/ strncpy())。

这是Todd Miller和Theo de Raadt关于strncat()and 问题的一些评论strncpy()

还有时遇到的几个问题strncpy()strncat()被用作安全版本strcpy()strcat()。这两个函数都以不同且不直观的方式处理NUL终止和length参数,这甚至使有经验的程序员都感到困惑。它们也没有提供简单的方法来检测何时发生截断。在所有这些问题中,最重要的是长度参数引起的混乱以及相关的NUL终止问题。当我们审核OpenBSD源代码树中的潜在安全漏洞时,我们发现普遍滥用strncpy()strncat()。尽管并非所有这些都会导致可利用的安全漏洞,但他们清楚地表明,使用字符串strncpy()strncat()安全字符串操作的规则被误解了。

OpenBSD的安全审核发现这些功能的错误是“猖r的”。不同于gets()可以安全地使用这些功能,但是实际上,由于界面混乱,不直观且难以正确使用,因此存在很多问题。我知道微软也进行了分析(尽管我不知道他们可能已经发布了多少数据),因此被禁止(或至少强烈劝阻-“禁止”可能不是绝对的)使用的strncat()strncpy()(除其他功能)。

一些带有更多信息的链接:


1
最好在字符串本身之外跟踪字符串的长度。这样,安全地连接两个字符串(具有长度)就变成了一个简单的计算(以获取所需的缓冲区大小),可能的重新分配和a的问题memmove()。(好吧,memcpy()在字符串独立的情况下,您可以在正常情况下使用。)
Donal Fellows 2010年

1
第二点是完全错误的- strncat() 总是终止目标字符串。
caf 2010年

1
您的第二点是错误的strncat()。但是,它对于是正确的strncpy(),它还有其他一些问题。 strncat()是的合理替代strcat(),但strncpy()不是合理的替代strcpy()
David Thornley

2
caf和David对我的说法strncat()并不总是null终止是100%正确的。我被混淆的行为strncat()strncpy()一个位(另一个原因是他们的功能,以避免-他们有暗示类似行为的名字,但实际上行为不同的重要途径......)。我已经修改了答案,以更正此问题并添加其他信息。
Michael Burr

1
请注意,char str[N] = ""; strncat(str, "long string", sizeof(str));如果N不够大,则是缓冲区溢出。该strncat()功能太容易滥用。它不应该被使用。如果可以strncat()安全使用,则可以使用memmove()memcpy()替代(这样会更有效)。
乔纳森·勒夫勒

9

应该使用的标准库函数:

setjmp.h

  • setjmp()。与longjmp()这些功能一起,被广泛认为使用起来极为危险:它们导致意大利面条式编程,它们具有多种形式的不确定行为,它们可能在程序环境中引起意想不到的副作用,例如影响存储在堆栈上的值。参考:MISRA-C:2012规则21.4,CERT C MSC22-C
  • longjmp()。请参阅setjmp()

标准版

  • gets()。该功能已从C语言中删除(按照C11),因为根据设计它不安全。该功能已在C99中标记为过时。使用fgets()代替。参考:ISO 9899:2011 K.3.5.4.1,另请参见注释404。

标准库

  • atoi()功能家族。它们没有错误处理,但是每当发生错误时都会调用未定义的行为。完全多余的功能,可以用strtol()功能族代替。参考:MISRA-C:2012规则21.7。

字符串

  • strncat()。具有笨拙的界面,经常被滥用。它主要是多余的功能。另请参阅的备注strncpy()
  • strncpy()。此功能的目的永远不是的更安全版本strcpy()。它的唯一目的是始终在Unix系统上处理古老的字符串格式,并且将其包含在标准库中是一个已知的错误。此函数很危险,因为它可能会使字符串没有空终止,并且众所周知程序员经常错误地使用它。参考:为什么strlcpy和strlcat不安全?

应谨慎使用的标准库函数:

断言

  • assert()。带有开销,通常不应在生产代码中使用。最好使用特定于应用程序的错误处理程序,该处理程序显示错误,但不一定关闭整个程序。

信号

stdarg.h

  • va_arg()功能家族。C程序中存在可变长度函数几乎总是表明程序设计不佳。除非您有非常特殊的要求,否则应避免使用。

stdio.h
通常,不建议将整个库用于生产代码,因为它附带了很多情况下行为不明确和类型安全性很差的情况。

  • fflush()。非常适合用于输出流。如果用于输入流,则调用未定义的行为。
  • gets_s()gets()C11边界检查界面中包含的安全版本。fgets()根据C标准建议,最好改为使用。参考:ISO 9899:2011 K.3.5.4.1。
  • printf()功能家族。资源密集型功能具有许多不确定的行为和较差的类型安全性。sprintf()也有漏洞。在生产代码中应避免使用这些功能。参考:MISRA-C:2012规则21.6。
  • scanf()功能家族。请参阅有关的评论printf()。此外,scanf()如果使用不正确,- 容易受到缓冲区溢出的影响。fgets()最好在可能的情况下使用。参考:CERT C INT05-C,MISRA-C:2012规则21.6。
  • tmpfile()功能家族。带有各种漏洞问题。参考:CERT C FIO21-C

标准库

  • malloc()功能家族。尽管要知道C90中的众所周知的问题,所以不要在结果中使用,这在托管系统中使用非常好。该malloc()系列函数不应该在独立的应用中使用。参考:MISRA-C:2012规则21.3。

    另请注意,realloc()如果您用的结果覆盖旧指针,这是很危险的realloc()。万一该功能失败,则会造成泄漏。

  • system()。带有大量开销,并且尽管可移植,但是通常最好使用系统特定的API函数。带有各种定义不明确的行为。参考:CERT C ENV33-C

字符串

  • strcat()。请参阅的备注strcpy()
  • strcpy()。除非要复制的数据大小未知或大于目标缓冲区,否则使用起来非常好。如果未检查传入数据大小,则可能存在缓冲区溢出。这strcpy()本身不是故障,而是调用应用程序的故障-这strcpy()是不安全的,主要是Microsoft创造的神话
  • strtok()。更改调用者字符串并使用内部状态变量,这可能使其在多线程环境中不安全。

我建议,建议static_assert()在地方assert(),如果条件可以在编译时得到解决。而且,sprintf()几乎可以随时更换snprintf(),这样更安全。
user694733 '17

1
使用strtok()中的功能的装置,其(a)该函数不能调用任何其他功能也使用strtok()而A正在使用它,以及(b)装置,其没有功能调用一个可以是使用strtok()时,它调用A.换句话说,使用strtok()使呼叫链中毒;它不能在库代码中安全使用,因为它必须记录它strtok()用来防止其他用户strtok()调用库代码的过程。
乔纳森·勒夫勒

strncpy是一个写零时要使用的适当的功能填充与来自任一零终止的字符串或零填充缓冲区,其大小至少一样大的目的地所采取的数据串缓冲区。零填充缓冲区并不是很常见,但是它们也不是完全模糊。
超级猫

7

有人主张strcpystrcat应避免使用strncpy和支持strncat。我认为这有些主观。

在处理用户输入时,绝对应该避免使用它们-毫无疑问,这里。

在离用户“远”的代码中,当您只知道缓冲区足够长时,strcpy并且strcat可能会更有效率,因为计算n传递给它们的表亲的缓冲区可能是多余的。


4
并且,如果有的话,最好使用strlcatstrlcpy,因为'n'版本不能保证目标字符串的NULL终止。
Dan Andreatta 2010年

听起来对我来说是过早的优化。但是IIRC strncpy()n根据需要使用nul字符精确地写字节。作为Dan,使用安全版本是IMO的最佳选择。
BastienLéonard'4

@Dan,不幸的是这些功能不是标准功能,因此不属于此讨论
Eli Bendersky 2010年

@Eli:但是关于strncat()正确和安全地使用(可能应该避免)的困难可能是一个事实。
Michael Burr

@Michael:我不会说正确和安全地使用它很困难。小心一点,就可以了
Eli Bendersky

6

避免

  • strtok 对于多线程程序,因为它不是线程安全的。
  • gets 因为这可能导致缓冲区溢出

2
它们的情况略有不同。如果您知道程序不是多线程的,或者以某种方式锁定对它的访问,则可以安全地使用strtok,但是您不能安全地使用gets,因此永远不要使用它。
jcoder

9
这个问题strtok()超出了线程安全性的范围-即使在单线程程序中也不安全,除非您确定使用该代码strtok()不会调用任何函数,否则也不要使用它(否则它们会弄乱strtok()状态)从你下面)。实际上,大多数针对多线程平台的编译器strtok()都通过使用线程本地存储strtok()的静态数据来解决线程可能遇到的潜在问题。但这仍然不能解决您在同一线程中使用其他函数的问题。
Michael Burr

的确,我现在要闭嘴,因为我不想鼓励任何人使用strtok,因为它确实存在许多问题。我只是想指出,这是不同于得到尽管因为它可以安全地使用它,而这是不可能的使用安全得到。
jcoder 2010年

@Jimmy:通常有非标准的库扩展,或者您可以编写自己的扩展。与最大的问题strtok()是,它使一个精确的缓冲区来工作,所以最擅长的替代品需要大约保持缓冲值和传球入。
大卫·索恩利

strcspn完成您所需的大部分内容-查找下一个令牌分隔符。您可以strtok使用它重新实现一个合理的变体。
bluss 2014年

5

可能值得再次添加它,strncpy()而不是strcpy()它的名字所暗示的通用替代品。它是为不需要nul终止符的定长字段设计的(它最初是为与UNIX目录条目一起使用而设计的,但是对于诸如加密密钥字段之类的东西可能很有用)。

但是,很容易strncat()代替strcpy()

if (dest_size > 0)
{
    dest[0] = '\0';
    strncat(dest, source, dest_size - 1);
}

if在常见情况下,显然可以将测试丢弃,因为您知道那dest_size肯定是非零的)。


5

还要查看Microsoft的禁止API列表。这些API(包括许多已经在此处列出的API)已被Microsoft代码禁止,因为它们经常被滥用并导致安全问题。

您可能不同意所有这些,但都值得考虑。当误用导致许多安全漏洞时,他们会将API添加到列表中。


2

处理NUL终止的字符串的几乎所有函数都可能不安全。如果您要从外界接收数据并通过str *()函数进行操作,那么您将为灾难做好准备


2

不要忘记sprintf-这是许多问题的原因。这是正确的,因为可选的snprintf有时具有不同的实现,这会使您的代码不可移植。

  1. linux:http//linux.die.net/man/3/snprintf

  2. Windows:http//msdn.microsoft.com/en-us/library/2ts7cx93%28VS.71%29.aspx

在情况1(Linux)中,返回值是存储整个缓冲区所需的数据量(如果小于给定缓冲区的大小,则输出将被截断)

在情况2(窗口)中,如果输出被截断,则返回值为负数。

通常,应避免使用以下功能:

  1. 缓冲区溢出安全(此处已经提到很多功能)

  2. 线程安全/不可重入(例如strtok)

在每个功能的手册中,您应该搜索以下关键字:安全,同步,异步,线程,缓冲区,错误


2
_sprintf()是在标准snprintf()到来之前由Microsoft创建的IIRC。不过,“ StringCbPrintf()”与之非常相似snprintf()
BastienLéonard'4

你可以用sprintf某种方式安全地在某些情况下:sprintf(buffer,"%10s",input);限制复制NB字节到10(如果bufferchar buffer[11]它是安全的,即使数据可能结业截断。
让·弗朗索瓦·法布尔

2

scanf安全使用非常困难。善用scanf可以避免缓冲区溢出,但是当读取不符合请求类型的数字时,您仍然容易受到未定义行为的影响。在大多数情况下,fgets其次是自我解析(使用sscanfstrchr等等)是一个更好的选择。

但是我不会说“一直回避scanf”。 scanf有其用途。举例来说,假设您要读取char10字节长的数组中的用户输入。您要删除尾随的换行符(如果有)。如果用户在换行符之前输入的字符超过9个,则您希望将前9个字符存储在缓冲区中,并丢弃所有内容,直到下一个换行符为止。你可以做:

char buf[10];
scanf("%9[^\n]%*[^\n]", buf));
getchar();

一旦习惯了这个惯用语,它就会比以下内容更短,并且在某种程度上更干净:

char buf[10];
if (fgets(buf, sizeof buf, stdin) != NULL) {
    char *nl;
    if ((nl = strrchr(buf, '\n')) == NULL) {
        int c;
        while ((c = getchar()) != EOF && c != '\n') {
            ;
        }
    } else {
        *nl = 0;
    }
}

0

在所有字符串复制/移动方案中-strcat(),strncat(),strcpy(),strncpy()等- 如果强制执行几个简单的试探法,情况会好得多(更安全):

   1.始终为NUL-fill您的缓冲区,然后再添加数据。
   2.使用宏常量将字符缓冲区声明为[SIZE + 1]。

例如,给定:

#define   BUFSIZE   10
char      Buffer[BUFSIZE+1] = { 0x00 };  /* The compiler NUL-fills the rest */

我们可以使用如下代码:

memset(Buffer,0x00,sizeof(Buffer));
strncpy(Buffer,BUFSIZE,"12345678901234567890");

比较安全。即使我们在编译时初始化了Buffer,memset()也应该出现在strncpy()之前,因为在调用函数之前,我们不知道其他代码会放入什么垃圾中。strncpy()会将截断的数据截断为“ 1234567890”,并且不会 NUL终止它。但是,由于我们已经用NUL填充了整个缓冲区-sizeof(Buffer),而不是BUFSIZE-只要我们使用BUFSIZE约束写入,就可以保证最终会有一个“范围外”的终端终止NUL。常量,而不是sizeof(Buffer)。

缓冲区和BUFSIZE同样适用于snprintf():

memset(Buffer,0x00,sizeof(Buffer));
if(snprintf(Buffer,BUFIZE,"Data: %s","Too much data") > BUFSIZE) {
    /* Do some error-handling */
}   /* If using MFC, you need if(... < 0), instead */

即使snprintf()专门只写BUFIZE-1个字符,后跟NUL,也可以安全地工作。因此,我们在Buffer的末尾“浪费”了一个无关的NUL字节...我们防止了缓冲区溢出和未终止的字符串情况,只用了很小的内存成本。

我对strcat()和strncat()的调用更加严格:不要使用它们。安全地使用strcat()很难,并且strncat()的API太违反直觉了,以至于正确使用它需要付出的努力会抵消任何好处。我建议进行以下插入:

#define strncat(target,source,bufsize) snprintf(target,source,"%s%s",target,source)

创建一个strcat()插件很诱人,但不是一个好主意:

#define strcat(target,source) snprintf(target,sizeof(target),"%s%s",target,source)

因为目标可能是一个指针(因此sizeof()不返回我们需要的信息)。对于您的代码中的strcat()实例,我没有很好的“通用”解决方案。

我经常从“知道strFunc()的”程序员那里遇到的一个问题是尝试使用strlen()来防止缓冲区溢出。如果内容保证是NUL终止的,这很好。否则,在到达要保护的“有问题的”代码之前,strlen()本身可能会导致缓冲区溢出错误(通常会导致分段冲突或其他核心转储情况)。


-2

atoi不是线程安全的。根据手册页中的建议,我改用strtol。


5
这听起来像适用于一种特定的实现。没有理由为什么strtol()不是线程安全的atoi()
David Thornley

2
推荐使用strtol与线程安全无关,但与错误处理无关。不知道您从哪个手册页中获得了该信息-我在下面找不到任何建议man atoi(尽管应该如此)。
伦丁
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.