Jan*_*der 39 c gnu glibc scanf
我刚刚阅读 glibcsscanf 手册页(来自 Linux 手册页包),发现了以下内容:
\n\n可以使用以下转换说明符:
\n
\n(...)\n
d\xc2\xa0\xc2\xa0 已弃用。匹配一个可选的有符号十进制整数;\nnext 指针必须是指向 的指针int。\n
i\xc2\xa0\xc2\xa0 已弃用。匹配一个可选的有符号整数;next\n指针必须是指向 的指针int。如果整数以 或 开头,则以 16 为基数读取0x;0X如果以 开头,则以 8 为基数读取0;否则以 10 为基数读取。仅使用与\n基数相对应的字符。\n
o\xc2\xa0\xc2\xa0 已弃用。匹配无符号八进制整数;下一个指针\n必须是指向 的指针unsigned int。(...)
\n
%d被废弃了呢?似乎所有int说明符都已弃用。Joh*_*ger 45
\n\n怎么就
\n%d被废弃了呢?似乎所有int说明符都已弃用。
它们并没有像软件文档中通常使用的术语那样被弃用。没有计划将它们从语言中删除,也没有直接的替代品。负责维护语言标准的 ISO 委员会并未表示任何应该避免使用它们的意见,尽管确实有解决方法可以避免使用它们。
\n您所询问的某些 Linux 手册页上的弃用通知构成了该版本文档的维护者的不当自由行为。同一页面的BUGS部分对此进行了解释:
\n\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
不幸的是,手册页维护者既固执己见,又非典型的咄咄逼人。这是一个有点争议的观点,认为它构成了标准中的一个错误,因为受影响的函数对无效输入具有未定义的行为。这是一个有效的观点,这是避免数字转换说明符的一个很好的理由,但作者无权以手册页读者通常理解的方式弃用这些函数。解决这种情况的传统方法是在手册文本的适当位置添加对 BUGS 部分的引用,甚至可能带有简短的解释性注释。弃用标签并非如此,无论文档中其他地方如何解释它们。
\n话虽如此,scanf-family 函数总体上很难正确使用。这里的一些人倾向于建议完全避免它们,这当然应该考虑。如果你确实避免了它们,那么问题就没有意义了。
Bar*_*mar 24
手册页的BUGS部分对此进行了解释:
\n\n\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 修复。
因此,C 语言规范并未弃用它,手册页的作者使用此表示法来表明它们使用不安全。
\n然而,如果正在读取的输入可能不包含有效格式的数据,这在实践中只是一个问题。如果您正在读取格式可靠的文件,则可以安全地使用这些说明符。
\n这实际上似乎是语言规范中的不一致,因为它还表示该函数返回有效转换的数量(或者EOF如果在第一次转换之前发生输入失败)。说转换失败是未定义的行为以及在这种情况下返回什么是没有意义的,并且大多数实现都正确返回值。
在我看来,手册页作者建议不要使用这些说明符过于迂腐。
\nPet*_*des 10
手册页中的这一声明是为了那些尝试编写可移植程序的人的利益。
由于有人猜测 glibc 本身在这种情况下会做什么,所以我决定检查一下。
glibc源代码实际上避免了有符号溢出UB,至少在转换函数scanf("%d")使用中是这样。在最坏的情况下,您可以说 glibc 的转换结果是未定义的,但整个程序的行为却不是。 int在 GNU 系统上没有陷阱值(它是 2 的补码),因此这不会使您的程序崩溃或行为异常,除了可能没有与您从其他解析字符串的方式获得的数值相匹配的数值之外。例如,如果您的代码查看最后一个十进制数字并使用sscanf转换,-1即使最后一个十进制数字是偶数,您也可以这样做。
errno == ERANGEscanf在溢出的glibc 整数转换之后long或unsigned 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_MIN或LONG_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_l(https://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
我们都可以看到 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),那么这里有一个方便的指南
正如评论中指出的(感谢@JeffHolt、@Eugene-sh、@DanielWalker、@Barmar、@DanielWalker),答案确实在 Bug 部分:
\nBUGS\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.\nRun Code Online (Sandbox Code Playgroud)\n我确实同意“弃用”在这里意味着“明确反对”(来自@Barmar的评论)。
\n与其他人一样,“已弃用”的这种使用很奇怪。它们的真正意思是“不推荐”,而不是“不再支持”。
这是手册页作者抱怨的问题:
假设代码
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) 输入项之后的第一个字符(如果有)保持未读状态。如果输入项的长度为零,则指令执行失败;除非文件结尾、编码错误或读取错误阻止了流的输入,否则此条件是匹配失败,在这种情况下,它是输入失败。
未指定字段宽度,因此整个输入将被转换并分配给并返回x以scanf 指示1成功;问题是输入会溢出并导致未定义的行为。
在没有显式字段宽度的情况下使用%dor%i或%o(或%s几乎任何转换说明符)会让您接受可能导致数字溢出或更糟的输入。
这是 C 没有刀片防护罩的区域之一,如果您不小心,就会割伤您。可选的边界检查版本 ( scanf_s) 仅确保没有任何参数是NULL; 它不检查数字溢出。
*scanf仅当您知道您的输入行为良好时才真正合适。如果您不能保证您的输入行为良好,那么您*scanf 根本不应该使用; 相反,用于fgets将输入读取为文本,并在尝试进行任何转换之前对长度和内容执行一些基本的完整性检查。
| 归档时间: |
|
| 查看次数: |
4209 次 |
| 最近记录: |