man sscanf:%d 在 C 或 glibc 中已弃用?

Jan*_*der 39 c gnu glibc scanf

我刚刚阅读 glibcsscanf 手册页(来自 Linux 手册页包),发现了以下内容:

\n
\n

可以使用以下转换说明符:
\n(...)

\n

d \xc2\xa0\xc2\xa0 已弃用。匹配一个可选的有符号十进制整数;\nnext 指针必须是指向 的指针int

\n

i \xc2\xa0\xc2\xa0 已弃用。匹配一个可选的有符号整数;next\n指针必须是指向 的指针int。如果整数以 或 开头,则以 16 为基数读取0x0X如果以 开头,则以 8 为基数读取 0;否则以 10 为基数读取。仅使用与\n基数相对应的字符。

\n

o \xc2\xa0\xc2\xa0 已弃用。匹配无符号八进制整数;下一个指针\n必须是指向 的指针unsigned int

\n

(...)

\n
\n
    \n
  • 怎么就%d被废弃了呢?似乎所有int说明符都已弃用。
  • \n
  • 它是什么意思以及有什么可以替代它们?
  • \n
\n

Joh*_*ger 45

\n

怎么就%d被废弃了呢?似乎所有int说明符都已弃用。

\n
\n

它们并没有像软件文档中通常使用的术语那样被弃用。没有计划将它们从语言中删除,也没有直接的替代品。负责维护语言标准的 ISO 委员会并未表示任何应该避免使用它们的意见,尽管确实有解决方法可以避免使用它们。

\n

您所询问的某些 Linux 手册页上的弃用通知构成了该版本文档的维护者的不当自由行为。同一页面的BUGS部分对此进行了解释:

\n
\n

数字转换说明符

\n使用数字转换说明符会产生未定义\n无效输入的行为。请参阅 C11 7.21.6.2/10\n\xe2\x9f\xa8https://port70.net/%7Ensz/c/c11/n1570.html#7.21.6.2p10\xe2\x9f\xa9。这是 ISO C 标准中的错误,而不是 API 的固有设计问题。但是,当前的实现并不能避免该错误,因此不建议使用它们。相反,程序应该使用 strtol(3) 等函数来解析数字输入。本手册页不推荐使用数字转换说明符,除非它们被 ISO C 修复。

\n

\n

不幸的是,手册页维护者既固执己见,又非典型的咄咄逼人。这是一个有点争议的观点,认为它构成了标准中的一个错误,因为受影响的函数对无效输入具有未定义的行为。这是一个有效的观点,这是避免数字转换说明符的一个很好的理由,但作者无权以手册页读者通常理解的方式弃用这些函数。解决这种情况的传统方法是在手册文本的适当位置添加对 BUGS 部分的引用,甚至可能带有简短的解释性注释。弃用标签并非如此,无论文档中其他地方如何解释它们。

\n

话虽如此,scanf-family 函数总体上很难正确使用。这里的一些人倾向于建议完全避免它们,这当然应该考虑。如果你确实避免了它们,那么问题就没有意义了。

\n

  • 也许`不要使用不受信任的输入;请参阅错误。`。抄送:@zwol (8认同)
  • 至少每次出现“*Deprecated.*”时都应提及原因并建议替代方案:“*Deprecated,更喜欢`strtol`.*”或“*Deprecated,see BUGS.*”! (6认同)
  • 我和你一样对手册页维护者的弃用态度感到沮丧,但我认为对于 7.21.6.2p10 的规则“如果转换的结果不能在对象,行为未定义”是标准中的设计缺陷。我没有提交 DR 的唯一原因是我认为“*scanf”不适合目的_无论如何_,原因更难修复。 (4认同)
  • 我理解维护者的态度。由于输入通常不受程序员的控制,因此 UB 是利用恶意输入或至少触发拒绝服务的潜在机会。但是许多程序处理来自已知来源的输入,那么这不是问题。我也对用自己的代码替换像 scanf 这样经过充分测试的多功能工具的建议持怀疑态度。正确使用“strtol”也不是完全微不足道的(令牌被完全读取的条件是什么?)并且仍然存在令牌化等问题。 (4认同)
  • @Pod,“已弃用”是对作者在解释中传达的内容的不准确描述,因此这些标签至少具有误导性。但是,由于读者倾向于(有充分的理由)解释 C 标准库函数的手册页,以便为他们提供语言规范的准确表示,**是的**,传达不同的印象是一种不适当的自由。这个答案已经描述了手册页应该采取的传统且适当的方法来表达所讨论的问题。 (3认同)
  • @zwol 在看到我无意中造成了多少挫败感后,我对此感到抱歉。我会努力弥补之前的错误。你能原谅我吗?:) (2认同)
  • @JohnBollinger这应该这样做: <https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/commit/?id=30cdb698f6f1af19f13b26c9a1b64bb67b45768a> (2认同)
  • @alx-recommendscodidact 感谢您的聆听和反应!我认为它现在会引起更少的混乱,就像触发这篇文章的那样(并且经验丰富的用户会更少摇头;-))。 (2认同)

Bar*_*mar 24

手册页的BUGS部分对此进行了解释:

\n
\n

数字转换说明符
\n使用数字转换说明符会产生未定义\n无效输入的行为。请参阅 C11 7.21.6.2/10\n\xe2\x9f\xa8https://port70.net/%7Ensz/c/c11/n1570.html#7.21.6.2p10\xe2\x9f\xa9。这是 ISO C 标准中的错误,而不是 API 的固有设计问题。但是,当前的实现并不能避免该错误,因此不建议使用它们。相反,程序应该使用 strtol(3) 等函数来解析数字输入。本手册页不推荐使用数字转换说明符,除非它们被 ISO C 修复。

\n
\n

因此,C 语言规范并未弃用它,手册页的作者使用此表示法来表明它们使用不安全。

\n

然而,如果正在读取的输入可能不包含有效格式的数据,这在实践中只是一个问题。如果您正在读取格式可靠的文件,则可以安全地使用这些说明符。

\n

这实际上似乎是语言规范中的不一致,因为它还表示该函数返回有效转换的数量(或者EOF如果在第一次转换之前发生输入失败)。说转换失败是未定义的行为以及在这种情况下返回什么是没有意义的并且大多数实现都正确返回值。

\n

在我看来,手册页作者建议不要使用这些说明符过于迂腐。

\n

  • 他们抱怨“使用数字转换说明符会对无效输入产生未定义的行为”。这是一个 glibc 开发者可以自由修复的“bug”——没有人阻止他们定义“他们的”实现的行为。例如,GCC 有 `-fwrapv` 定义了有符号整数溢出的行为 - 否则,如果遵循本手册页的“逻辑”,则会“弃用”C 中的所有整数运算。“它们可能会溢出并导致未定义的行为!!!” 如果编译器“弃用”整型参数之间每次使用“+”,那么它是否理智? (11认同)
  • 将其放入手册页有什么双重坏处?手册页的作者是他们所抱怨的“实现”的作者。C 标准并不阻止 glibc 作者定义他们自己的实现的行为。 (6认同)
  • @AndrewHenle 他们没有抱怨实施。他们说这是 ISO C 规范中的一个错误,因为说它是未定义的然后说它返回有效转换的数量是不一致的。 (5认同)
  • @AndrewHenle:如果手册页的目的是警告这些不安全*可移植*,即使 glibc 确实检查了溢出,我也不会感到惊讶。(实际上,我确信在最坏的情况下,glibc scanf 中整数溢出的行为是包装的,因为它们编译为必须适用于非溢出情况的 asm,并且转换循环很简单“total = Total*base + digital”除非像 glibc 的其他部分一样检查溢出,例如处理 printf 的 `%12d` 转换。解析 `12` 确实会检查溢出,不幸的是,对于常见的小情况来说,它有点慢。) (5认同)
  • 那太愚蠢了。返回有效转换的数量和[“此对象没有适当的类型,或者如果转换的结果无法在对象中表示,则行为未定义”](https://port70.net /~nsz/c/c11/n1570.html#7.21.6.2p10)。Glibc 开发人员可以实现他们想要的东西。有符号整数溢出中的矛盾会导致未定义的行为和 [**6.5.6 加法运算符**,第 5 段](https://port70.net/~nsz/c/c11/n1570.html#6.5.6p5) “二元+运算符的结果是操作数之和”。 (4认同)
  • 我不认为你错了 - 只是手册页中给出的*借口*是无意义的并且很难解释。整数溢出只是我的例子,其中 UB 与“+”运算符的定义并不矛盾。手册页中的解释是抱怨无法转换的输入,因为它无法表示 - 就像一个数字字符串在“%d”转换中溢出“int”。在 IMO 看来,这并不比 UB 的“+”操作溢出更“bug”。 (2认同)
  • 我懂了。你是说这个警告就像弃用“+”一样,因为在某些情况下它是不安全的。OTOH,理论上也可以首先检查“+”的操作数,但在扫描输入时不能这样做(除非您自己进行解析,从而消除了对“scanf()”的需要)。 (2认同)
  • @AndrewHenle:我检查过;glibc `scanf("%d", &tmp)` *确实* 检查溢出,将 `1111111111111111111111111111111` 转换为 `-1`。(这不仅仅是完整结果的截断。)我单步执行它,最终到达“__strtol_l”(https://codebrowser.dev/glibc/glibc/stdlib/strtol_l.c.html#215) (一次将一个字符复制到 tmp 缓冲区后,每次检查基数以查看是否应该检查十六进制或基数 10 数字...) https://codebrowser.dev/glibc/glibc/stdlib/ strtol_l.c.html#466 是 mul/add 之前的溢出检查 ULONG_MAX/10 及其尾随数字 (2认同)

Pet*_*des 10

手册页中的这一声明是为了那些尝试编写可移植程序的人的利益。
由于有人猜测 glibc 本身在这种情况下会做什么,所以我决定检查一下。

glibc源代码实际上避免了有符号溢出UB,至少在转换函数scanf("%d")使用中是这样。在最坏的情况下,您可以说 glibc 的转换结果是未定义的,但整个程序的行为却不是。 int在 GNU 系统上没有陷阱值(它是 2 的补码),因此这不会使您的程序崩溃或行为异常,除了可能没有与您从其他解析字符串的方式获得的数值相匹配的数值之外。例如,如果您的代码查看最后一个十进制数字并使用sscanf转换,-1即使最后一个十进制数字是偶数,您也可以这样做。

errno == ERANGEscanf在溢出的glibc 整数转换之后longunsigned long,对于long或更窄的转换。
%lld在 32 位系统上只会检查 的溢出long long。)


我检查了这个测试程序:

#include <stdio.h>

int main(){
        int tmp = 0xcccccccc;
        int conv_result = scanf("%d", &tmp);
        printf("successful conversions = %d,  result = %d = %#x\n",
                                       conv_result, tmp, (unsigned)tmp);
}
Run Code Online (Sandbox Code Playgroud)

通过适合 a long(x86-64 GNU/Linux 上的 64 位)的输入,我们将该值截断为int.
对于较大的输入,glibc 检测到溢出并生成-1(实际上LONG_MINLONG_MAX根据符号,在本例中为 LONG_MAX,-1当缩小到 时,它会被截断int)。

例如,它转换1111111111111111111111111111111-1,但1111111111111111111转换为734294471= 0x2bc471c7在Godbolt上看到它,有 2 个执行程序,它们向 stdin 提供这些输入。无论哪种方式,它都将其视为成功的转换, scanf 返回1,例如

successful conversions = 1,  result = -1 = 0xffffffff
Run Code Online (Sandbox Code Playgroud)

我在 Arch GNU/Linux 系统上使用 GDB 通过 glibc 2.38-7 单步进入 scanf(让 debuginfod 获取库源代码,非常有帮助)。它最终达到了__strtol_lhttps://codebrowser.dev/glibc/glibc/stdlib/strtol_l.c.html#215经过一堆 stdio 开销并将字符一次复制到 tmp 缓冲区中,每次检查基数后, )看看是否应该检查十六进制或以 10 为基数的数字。哎呀,效率不高。

https://codebrowser.dev/glibc/glibc/stdlib/strtol_l.c.html#466是该函数的实际部分,它在转换之前检查类似的内容total >= ULONG_MAX/10以及尾随十进制数字ULONG_MAX与正在转换的新数字之间的溢出情况做total = total*base + digit.

// glibc/stdlib/strtol_l.c
INT
INTERNAL (__strtol_l) (const STRING_TYPE *nptr, STRING_TYPE **endptr,
               int base, int group, locale_t loc)
{
...
    if (c >= L_('0') && c <= L_('9'))
      c -= L_('0');
...  // check for grouping characters like ' if enabled
    else if (ISALPHA (c))
      c = TOUPPER (c) - L_('A') + 10;
    else
      break;

// my comments added:
// c is a the new digit converted to integer in the [0,base) range
// i is the total to be returned
    if ((int) c >= base)
      break;
    /* Check for overflow.  */
    if (i > cutoff || (i == cutoff && c > cutlim))   // cutoff and cutlim were set from a lookup table according to base
      overflow = 1;
    else
      {
      use_long:             // goto label from a loop using narrower types, if LONG isn't the same size as long
        i *= (unsigned LONG int) base;
        i += c;
      }
    }

...
  if (__glibc_unlikely (overflow))
    {
      __set_errno (ERANGE);
#if UNSIGNED
      return STRTOL_ULONG_MAX;
#else
      return negative ? STRTOL_LONG_MIN : STRTOL_LONG_MAX;
#endif
    }
...
Run Code Online (Sandbox Code Playgroud)

(是的,循环可以跳过溢出的数字并仍然处理稍后的较小数字,但如果设置了,则后面的代码i根本不使用。)overflow


Pod*_*Pod 9

我们都可以看到 man7 确实将其列为已弃用,但这里没有人回答“为什么”提出的相关问题。

为什么 %d 被弃用了?似乎所有 int 说明符都已弃用。

手册页描述了当前 POSIX 发行版的状态。因此,每个系统可能都有其一组手册页,并且一个系统的文档可能与另一个系统不同。理想情况下,您可以查阅当地的手册页man sscanf。不过,在线管理(例如 man7)很方便。但请注意,他们描述的是一个不属于您的系统,甚至可能是一个不存在的理想化系统。

您应该始终谨慎阅读您不为其编程的系统的手册页,因为它们可能记录了同一接口的旧版本或新版本。

在本例中,man7 托管 Linux 内核团队和 GNU lib c 团队使用的手册页。这一将 sscanf 整数说明符标记为已弃用的特殊更改是在a15d34326c581eab10一年前完成的,并包含在已发布的man-pages-6.02. 添加 BUGS 注释的最新更改已完成1f9949d11f499e5758f7e21并包含在man-pages 6.03. 该更改是否最终出现在您的发行版的手册页中是另一回事。

围绕这个问题的讨论实际上是关于ERANGE的,你可以在几个地方关注它,例如

甚至有人问了和OP同样的问题。可以在 处查看响应From: Alejandro Colomar @ 2023-01-20 13:12 UTC。一些片段:

它真的应该被弃用吗?

虽然 sscanf(3) 数字转换的接口没有设计错误并且可以修复,但它没有正确实现,甚至没有标准化。

我认为弃用是正确的,除非有明确的努力来修复它。

这里的未定义行为是现实世界中任何地方的问题,还是这只是基于 C 标准解释的理论问题?

sscanf(3) 的所有实现都会产生未定义行为 (UB),AFAIK。对于每个程序员来说,你认为 UB 是一个现实世界问题的程度不同,但我倾向于认为所有 UB 都像鼻恶魔一样糟糕。我并不是说 UB 不应该存在,只是说你不应该调用它。用于扫描用户输入的函数是您真正希望避免调用 UB 的地方之一。

手册页文档的一个共同点是它们区分了 POSIX 兼容接口和系统使用的接口。两者均可在 man7.org 上找到:

您会注意到 3p 版本并未列为%d已弃用。因此%d仅在 man7.org 记录的系统上被弃用。

如果您想停止使用 scanf(以及 sscanf、fscanf),那么这里有一个方便的指南

  • 区分 Posix 手册页和系统手册页的好点。我什至不知道 Posix 的存在。 (2认同)
  • *因此 %d 仅在记录的系统上被弃用* - 恰恰相反;我认为他们更关心编写必须在非 GNU 系统上运行的可移植代码的人。Glibc 在内部“确实”避免了有符号溢出 UB,甚至为不适合“long”或“unsigned long”的数字设置“errno = ERANGE”。但对于像“%d”而不是“%ld”这样的窄转换,它会在溢出时将“LONG_MAX”或“LONG_MIN”截断为“int”,如我的答案所示。(但这只是当前的行为,没有记录继续这样做;更有用的可能是缩小类型限制。) (2认同)

Jan*_*der 8

正如评论中指出的(感谢@JeffHolt、@Eugene-sh、@DanielWalker、@Barmar、@DanielWalker),答案确实在 Bug 部分:

\n
BUGS\n   Numeric conversion specifiers\n       Use of the numeric conversion specifiers produces Undefined\n       Behavior for invalid input.  See C11 7.21.6.2/10 \n       \xe2\x9f\xa8https://port70.net/%7Ensz/c/c11/n1570.html#7.21.6.2p10\xe2\x9f\xa9.  This is\n       a bug in the ISO C standard, and not an inherent design issue\n       with the API.  However, current implementations are not safe from\n       that bug, so it is not recommended to use them.  Instead,\n       programs should use functions such as strtol(3) to parse numeric\n       input.  This manual page deprecates use of the numeric conversion\n       specifiers until they are fixed by ISO C.\n
Run Code Online (Sandbox Code Playgroud)\n

我确实同意“弃用”在这里意味着“明确反对”(来自@Barmar的评论)。

\n


Joh*_*ode 6

与其他人一样,“已弃用”的这种使用很奇怪。它们的真正意思是“不推荐”,而不是“不再支持”。

这是手册页作者抱怨的问题:

假设代码

int x;
printf( "Gimme a number: " );
if ( scanf( "%d", &x ) == 1 )
  do_something_with( x );
else
  // handle input error
Run Code Online (Sandbox Code Playgroud)

和输入

12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

这是一个语法上有效的十进制整数常量:

6.4.4.1 整数常量
...
整数常数十进制常数整数后缀选择
    八进制常数整数后缀选择
    十六进制常数整数后缀选择

小数常量非零位小数
    常量数字

非零数字:其中之一
    1 2 3 4 5 6 7 8 9

数字:其中之一
    0 1 2 3 4 5 6 7 8 9

scanf函数将匹配满足转换的最长字符序列%d

7.21.6.2 fscanf 函数
...
9 从流中读取输入项,除非规范包含说明符n输入项被定义为最长的输入字符序列,该序列不超过任何指定的字段宽度,并且是匹配输入序列或者是匹配输入序列的前缀。285) 输入项之后的第一个字符(如果有)保持未读状态。如果输入项的长度为零,则指令执行失败;除非文件结尾、编码错误或读取错误阻止了流的输入,否则此条件是匹配失败,在这种情况下,它是输入失败。

未指定字段宽度,因此整个输入被转换并分配给并返回xscanf 指示1成功;问题是输入溢出并导致未定义的行为。

在没有显式字段宽度的情况下使用%dor%i%o(或%s几乎任何转换说明符)会让您接受可能导致数字溢出或更糟的输入。

这是 C 没有刀片防护罩的区域之一,如果您不小心,就会割伤您。可选的边界检查版本 ( scanf_s) 仅确保没有任何参数是NULL; 它不检查数字溢出。

*scanf仅当您知道您的输入行为良好时才真正合适。如果您不能保证您的输入行为良好,那么您*scanf 根本不应该使用; 相反,用于fgets将输入读取为文本,并在尝试进行任何转换之前对长度和内容执行一些基本的完整性检查。