我在 Stack Overflow 上读到一些 C 函数“已过时”或“应该避免”。你能给我一些这种功能的例子和原因吗?
这些功能有哪些替代品?
我们可以安全地使用它们 - 有什么好的做法吗?
我在 Stack Overflow 上读到一些 C 函数“已过时”或“应该避免”。你能给我一些这种功能的例子和原因吗?
这些功能有哪些替代品?
我们可以安全地使用它们 - 有什么好的做法吗?
不推荐使用的
不安全
函数 此类函数的完美示例是gets(),因为无法告诉它目标缓冲区有多大。因此,任何使用gets() 读取输入的程序都存在缓冲区溢出漏洞。出于类似的原因,应该使用strncpy()代替strcpy()和strncat()代替strcat()。
还有一些示例包括tmpfile()和mktemp()函数,因为覆盖临时文件的潜在安全问题被更安全的mkstemp()函数取代。
不可重入
其他示例包括gethostbyaddr()和gethostbyname(),它们是不可重入的(因此不能保证是线程安全的)并且已被可重入的getaddrinfo()和freeaddrinfo()取代。
您可能会注意到这里的一种模式......缺乏安全性(可能是由于未能在签名中包含足够的信息以可能安全地实现它)或不可重入是弃用的常见来源。
过时的、不可移植的
一些其他功能只是被弃用了,因为它们重复了功能并且不像其他变体那样可移植。例如,不推荐使用bzero()以支持memset()。
线程安全和重入
您在帖子中询问了线程安全和重入。有一点不同。如果一个函数不使用任何共享的、可变的状态,它就是可重入的。因此,例如,如果它需要的所有信息都传递给函数,并且所需的任何缓冲区也传递给函数(而不是由对函数的所有调用共享),那么它是可重入的。这意味着不同的线程,通过使用独立的参数,不会冒着意外共享状态的风险。可重入性是比线程安全性更强的保证。如果一个函数可以被多个线程同时使用,那么它就是线程安全的。如果满足以下条件,则函数是线程安全的:
一般来说,在单一 UNIX 规范和IEEE 1003.1(即“POSIX”)中,任何不能保证可重入的函数都不能保证是线程安全的。因此,换句话说,只有保证可重入的函数才能在多线程应用程序中可移植地使用(没有外部锁定)。然而,这并不意味着这些标准的实现不能选择使不可重入函数成为线程安全的。例如,Linux 经常向不可重入函数添加同步,以增加线程安全的保证(超出单一 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()等结合使用并手动解析,因为我不是可变参数或格式字符串。也就是说,据我所知,[f]scanf(),[f]printf()没有问题等,只要您自己制作格式字符串,就永远不会传递任意格式字符串或允许将用户输入用作格式字符串,并且在适当的情况下使用<inttypes.h>中定义的格式宏。(注意,应该使用snprintf()代替sprintf(),但这与未能指定目标缓冲区的大小而不是使用格式字符串有关)。我还应该指出,在 C++ 中,boost::format提供了类似 printf 的格式,但没有可变参数。
人们再一次像咒语一样重复荒谬的断言,即 str 函数的“n”版本是安全版本。
如果那是它们的目的,那么它们将始终为空终止字符串。
函数的“n”个版本是为使用固定长度字段(例如早期文件系统中的目录条目)而编写的,其中仅当字符串未填充字段时才需要 nul 终止符。这也是为什么这些函数具有奇怪的副作用,如果只是用作替换,这些副作用会毫无意义地低效——以 strncpy() 为例:
如果 s2 指向的数组是一个短于 n 字节的字符串,则将空字节附加到 s1 指向的数组的副本中,直到总共写入 n 个字节。
由于分配用于处理文件名的缓冲区通常为 4kbytes,这可能会导致性能大幅下降。
如果你想要“假定”安全的版本,那么获取 - 或编写你自己的 - strl 例程(strlcpy、strlcat 等),它们总是 nul 终止字符串并且没有副作用。请注意,尽管这些并不是真正安全的,因为它们可以静默地截断字符串——这在任何实际程序中很少是最好的做法。在某些情况下这是可以的,但在许多情况下它可能会导致灾难性的结果(例如打印出医疗处方)。
这里有几个答案建议使用strncat()
over strcat()
; 我建议strncat()
(和strncpy()
)也应该避免。它存在难以正确使用并导致错误的问题:
strncat()
与(但不完全准确 - 见第三点)可以复制到目标的最大字符数有关,而不是与目标缓冲区的大小有关。这使得strncat()
使用起来更加困难,特别是如果多个项目将连接到目的地。s1
是strlen(s1)+n+1
”对于看起来像的调用strncat( s1, s2, n)
strncpy()
还有一个问题可能会导致您尝试以直观的方式使用它的错误 - 它不能保证目的地为空终止。为了确保您必须通过自己将 a 放到缓冲区的最后一个位置来确保您专门处理该极端情况'\0'
(至少在某些情况下)。
我建议使用 OpenBSD 之类的strlcat()
和strlcpy()
(尽管我知道有些人不喜欢这些功能;我相信它们比strncat()
/更容易安全使用strncpy()
)。
strncat()
以下是 Todd Miller 和 Theo de Raadt 关于and问题的一些看法strncpy()
:
strncpy()
当和strncat()
用作 和 的安全版本strcpy()
时会遇到几个问题strcat()
。这两个函数都以不同且不直观的方式处理 NUL 终止和长度参数,即使是有经验的程序员也会感到困惑。它们还没有提供简单的方法来检测何时发生截断。...在所有这些问题中,由长度参数引起的混乱和NUL终止的相关问题是最重要的。当我们审计 OpenBSD 源代码树的潜在安全漏洞时,我们发现滥用strncpy()
和strncat()
. 虽然并非所有这些都导致了可利用的安全漏洞,但他们清楚地表明,使用strncpy()
和strncat()
安全字符串操作的规则被广泛误解。
OpenBSD 的安全审计发现这些功能的漏洞“猖獗”。与 不同的是gets()
,这些功能可以安全使用,但在实践中存在很多问题,因为界面混乱、不直观且难以正确使用。我知道微软也进行了分析(尽管我不知道他们可能发布了多少数据),因此禁止(或至少非常不鼓励 - “禁止”可能不是绝对的)使用strncat()
和strncpy()
(以及其他功能)。
一些包含更多信息的链接:
设置jmp.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。字符串.h
strncat()
. 有一个经常被滥用的尴尬界面。它主要是一个多余的功能。另见备注strncpy()
。strncpy()
. 此功能的目的绝不是成为更安全的strcpy()
. 它的唯一目的始终是在 Unix 系统上处理一种古老的字符串格式,而它被包含在标准库中是一个已知的错误。这个函数很危险,因为它可能使字符串没有空终止,并且众所周知,程序员经常错误地使用它。参考:为什么 strlcpy 和 strlcat 被认为是不安全的?,这里有更详细的解释:Strcpy 危险吗?应该改用什么?.断言.h
assert()
. 带有开销,通常不应在生产代码中使用。最好使用特定于应用程序的错误处理程序,它显示错误但不一定关闭整个程序。信号.h
signal()
. 参考:MISRA-C:2012 规则 21.5,CERT C SIG32-C。标准参数.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。
字符串.h
strcat()
. 见备注strcpy()
。strcpy()
. 使用起来非常好,除非要复制的数据大小未知或大于目标缓冲区。如果未检查传入数据大小,则可能存在缓冲区溢出。这不是strcpy()
它本身的错,而是调用应用程序的错——strcpy()
不安全主要是微软创造的一个神话。strtok()
. 更改调用者字符串并使用内部状态变量,这可能使其在多线程环境中不安全。有些人会声称应该避免,而赞成strcpy
and 。在我看来,这有点主观。strcat
strncpy
strncat
在处理用户输入时绝对应该避免使用它们 - 毫无疑问。
在远离用户的代码中,当您只知道缓冲区足够长时,strcpy
可能strcat
会更有效率,因为计算n
传递给他们的表兄弟可能是多余的。
避免
strtok
对于多线程程序,因为它不是线程安全的。gets
因为它可能导致缓冲区溢出可能值得再次补充的是,它不是其名称可能暗示strncpy()
的通用替代品。strcpy()
它是为不需要 nul 终止符的固定长度字段而设计的(它最初是为与 UNIX 目录条目一起使用而设计的,但对于加密密钥字段之类的东西很有用)。
但是,它很容易strncat()
用作 的替代品strcpy()
:
if (dest_size > 0)
{
dest[0] = '\0';
strncat(dest, source, dest_size - 1);
}
(if
测试显然可以在常见情况下被放弃,你知道这dest_size
绝对是非零的)。
另请查看 Microsoft 的禁用 API列表。这些是 Microsoft 代码中禁止使用的 API(包括许多已在此处列出的 API),因为它们经常被滥用并导致安全问题。
您可能不同意所有这些,但它们都值得考虑。当滥用导致许多安全漏洞时,他们会将 API 添加到列表中。
很难scanf
安全使用。使用好scanf
可以避免缓冲区溢出,但在读取不适合请求类型的数字时,您仍然容易受到未定义行为的影响。在大多数情况下,fgets
后跟自解析(使用sscanf
、strchr
等)是更好的选择。
但我不会说“scanf
一直避免”。 scanf
有它的用途。例如,假设您要读取char
10 个字节长的数组中的用户输入。您要删除尾随换行符(如果有)。如果用户在换行符前输入超过 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;
}
}
几乎所有处理 NUL 终止字符串的函数都可能是不安全的。如果您从外部世界接收数据并通过 str*() 函数对其进行操作,那么您将面临灾难
不要忘记 sprintf - 它是许多问题的原因。这是真的,因为替代方案 snprintf 有时具有不同的实现,这会使您的代码无法移植。
linux: http: //linux.die.net/man/3/snprintf
窗口:http: //msdn.microsoft.com/en-us/library/2ts7cx93%28VS.71%29.aspx
在情况 1 (linux) 中,返回值是存储整个缓冲区所需的数据量(如果它小于给定缓冲区的大小,则输出被截断)
在情况 2(Windows)中,如果输出被截断,则返回值为负数。
通常,您应该避免以下功能:
缓冲区溢出安全(这里已经提到了很多功能)
线程安全/不可重入(例如 strtok)
在每个函数的手册中,您应该搜索以下关键字:safe、sync、async、thread、buffer、bugs
在所有字符串复制/移动场景中 - strcat()、strncat()、strcpy()、strncpy() 等 -如果强制执行一些简单的启发式方法,事情会变得更好(更安全
):
1. 总是 NUL 填充添加数据之前的缓冲区。
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");
比较安全。memset() 应该出现在 strncpy() 之前,即使我们在编译时初始化了 Buffer,因为我们不知道在调用我们的函数之前其他代码放入了哪些垃圾。strncpy() 会将复制的数据截断为“1234567890”,并且不会以NUL 终止它。但是,由于我们已经用 NUL 填充了整个缓冲区 - sizeof(Buffer),而不是 BUFSIZE - 只要我们使用 BUFSIZE 限制写入,就可以保证最终的“超出范围”终止 NUL常量,而不是 sizeof(Buffer)。
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)
因为 target 可能是一个指针(因此 sizeof() 不会返回我们需要的信息)。对于代码中的 strcat() 实例,我没有一个好的“通用”解决方案。
我经常遇到“strFunc()-aware”程序员的一个问题是试图通过使用 strlen() 来防止缓冲区溢出。如果保证内容是 NUL 终止的,这很好。否则, strlen() 本身可能会导致缓冲区溢出错误(通常导致分段违规或其他核心转储情况),然后才能到达您试图保护的“有问题的”代码。
atoi 不是线程安全的。根据手册页的建议,我使用 strtol 代替。