sscanf需要以空字符结尾的字符串作为输入吗?

17
最近发现了GTA长时间加载的原因(1),许多实现sscanf()的调用会在其输入字符串上调用strlen()以设置用于与其他扫描函数(scanf()fscanf()等)共享的内部例程的上下文对象。当输入字符串非常长时,这可能成为性能瓶颈。使用偏移量和%n转换重复调用sscanf()解析作为字符串加载的10MB JSON文件被证明是导致加载时间过长的主要原因。

我的问题是,sscanf()是否应该读取超出完成转换所需的字节的输入字符串?例如,以下代码是否会引发未定义行为:

int test(void) {
    char buf[1] = { '1' };
    int v;
    sscanf(buf, "%1d", &v);
    return v;
}

该函数应返回1,并且不需要从buf读取超过一个字节,但是sscanf()是否允许从buf读取超过第一个字节?

(1) JdeBP提供的参考资料:
https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/
https://news.ycombinator.com/item?id=26297612
https://github.com/biojppm/rapidyaml/issues/40


3
值得一提的是,之所以在常规解析过程中首先调用 strlen 而不是“仅仅”等待 \0,是因为如果你将 sscanf 嵌套在其余 *scanf 家族的正常机制上,显而易见的实现方法会使用一个基于已扫描字符串的伪 FILE 对象。这是因为一个 FILE 对象通常包括一个计数器和一个返回 EOF 的方式,而其他代码正在期望这些内容。(换句话说,查找\0而不是EOF或者将\0懒惰地转换为EOF需要更复杂的改动。) - Steve Summit
@SteveSummit:我非常清楚这一点,而在通用代码中特殊处理 '\0' 并不需要进行涉及重做的工作。我已经在自己的实现中这样做了。'\0' 不能是数字的一部分,因此在数字解析器中不需要进行任何更改。其他解析器也很容易适应。对任意长字符串调用 strlen() 是不可接受的。 - chqrlie
1
@chqrlie,非常抱歉浪费了您的时间;我在我的评论中明确注明了“FWIW”,预计并不是所有人都会感兴趣。 - Steve Summit
3
“sscanf函数是否需要以空字符结尾的字符串作为输入?”和“sscanf()是否应该读取超过所需字节以完成转换的输入字符串?”这些问题可能会有不同的答案。我个人认为,sscanf需要以空字符结尾的字符串作为输入是完全可以接受的,但在消耗任何字节之前就寻找任意长的字符串的末尾仍然是一个质量问题。 - trent
2个回答

12

以下是C标准中相关部分:

7.21.6.7 sscanf函数概述

概述

#include <stdio.h>
int sscanf(const char * restrict s, const char * restrict format, ...);

描述
sscanf函数与fscanf函数等效,不同之处在于输入源是字符串(由参数s指定),而不是流。到达字符串结尾相当于遇到了fscanf函数的文件结尾。如果对象重叠进行复制,则行为是未定义的。

返回值
如果在第一个转换(如果有的话)完成之前发生输入失败,则sscanf函数返回宏EOF的值。否则,sscanf函数返回分配的输入项数,这可以少于提供的输入项数,甚至在早期匹配失败的情况下为零。

输入明确指定为一个字符串,因此它应该以空字符结尾。

尽管除了与转换说明符匹配的初始前缀和潜在确定匹配序列结束的下一个字节之外,字符串中的任何字符都不用于转换,但这些字符必须后跟空字符,以便输入成为格式良好的字符串,并且调用strlen()来确定输入长度是符合规范的。

为避免长输入字符串的线性时间复杂度,sscanf()应使用strnlen()或等效函数限制查找字符串末尾的扫描大小,并传递适当的补充函数。传递巨大的长度并让内部例程特殊处理空字符是一种更好的方法。

同时,程序员应避免将长输入字符串传递给sscanf(),并使用更专业的函数来进行解析任务,例如strtol(),它也需要格式良好的C字符串,但实现方式更为稳健。这也会避免数字转换时可能出现的超出范围字符串表示的未定义行为。


4
当标准被编写时,许多库函数在几乎所有现有的实现中都被同样处理,但一些实现可能因为某些原因而对少数情况进行了不同处理。如果有大量实现具有与常规行为不同的理由,则委员会要么要求所有实现以常见方式行事(例如计算UINT_MAX+1u时发生的情况),要么明确说明它们没有义务这样做(例如计算INT_MAX+1时)。然而,在存在明显共同行为的情况下,但在所有实现上都可能不可行,委员会通常只是保持沉默,假定大多数编译器没有理由偏离常见行为,而那些有理由偏离的作者比委员会更能判断遵循常见行为与偏离常见行为之间的利弊。
问题的sscanf行为符合后一种模式。委员会不想强制实现必须更改以处理没有尾随零字节的数据源而导致困难的情况,但他们也不想要求程序员在使用sscanf之前将没有尾随零字节的数据从源处复制到有尾随零字节的位置,即使他们的实现不会关心超出有意义的部分之外的任何内容。由于需要尾随零的实现制造商很可能阻止要求它们容忍尾随零缺失的标准更改,并且强制实施不必要要求的实现的程序员将阻止任何要求他们在代码中添加额外数据复制步骤的标准更改,因此,除非人们可以同意将强制实施要求尾随字节的实现分类为“符合但不足”并要求他们通过预定义的宏或其他方式指示这种缺陷,否则情况将保持僵局。

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