【问题标题】:Does sscanf require a null terminated string as input?sscanf 是否需要以空字符结尾的字符串作为输入?
【发布时间】:2021-03-01 21:40:15
【问题描述】:

最近发现的对 GTA 加载时间过长的解释(1) 表明 sscanf() 的许多实现在其输入字符串上调用 strlen() 来为与共享的内部例程设置上下文对象其他扫描功能(scanf()fscanf()...)。当输入字符串很长时,这可能会成为性能瓶颈。解析加载为字符串的 10MB JSON 文件并重复调用带有偏移量的 sscanf()%n 转换被证明是加载时间的主要原因。

我的问题是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

【问题讨论】:

  • FWIW,在正常解析期间首先调用strlen 而不是“只是”等待\0 的原因是,如果你在其余的正常机器上搭载sscanf *scanf 系列,明显的实现使用了一个基于扫描字符串的伪 FILE 对象。 FILE 对象通常包括计数和返回 EOF 的方法,这是其余代码所期望的。 (换句话说,寻找\0 而不是EOF,或者懒洋洋地将\0 变成EOF 将是一个更复杂的返工。)
  • @SteveSummit:我很清楚这一事实,在通用代码中对特殊情况'\0' 没有涉及返工。我在自己的实现中就是这样做的。 '\0' 不能是数字的一部分,因此无需更改数字解析器。其他解析器很容易适应。对任意长字符串调用 strlen() 是不可接受的。
  • @chqrlie 对浪费您的时间深表歉意;我明确地在我的评论前加上“FWIW”,以为所有人都不会感兴趣。
  • "sscanf 是否需要以空字符结尾的字符串作为输入?"和“sscanf() 是否应该读取超出转换完成所需字节的输入字符串?”是可能有不同答案的问题。 IMO sscanf 需要一个以 null 结尾的字符串是完全可以的,但是在消耗任何字节之前寻找任意长字符串的末尾仍然是一个 QoI 问题。

标签: c scanf language-lawyer undefined-behavior


【解决方案1】:

以下是 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字符串,但以更多的方式实现保守的方式。这也将避免超出范围字符串表示的数字转换潜在的未定义行为。

【讨论】:

    【解决方案2】:

    在编写标准时,几乎所有现有实现都以相同的方式处理许多库函数,但某些实现可能有充分的理由以不同方式处理少数情况。如果有理由与普通行为不同的实现的数量很大,那么委员会要么要求所有实现以共同的方式运行(例如在计算 UINT_MAX+1u 时发生),要么明确声明它们不是需要这样做(例如在计算INT_MAX+1 时)。但是,如果存在明显的共同行为,但可能并非对所有实现都适用,委员会通常只是简单地避免发表任何意见,假设大多数编译器没有理由偏离共同行为,并且那些有理由偏离的作者将比委员会更好地判断遵循共同行为与偏离它的利弊。

    有问题的sscanf 行为符合后一种模式。委员会不想强制要求必须更改如果数据源没有尾随零字节时会出现问题的实现以处理此类数据源,但他们也不想要求程序员从源中复制数据在使用 sscanf 之前的地方没有尾随零字节,即使它们的实现不关心超出将被有意义检查的源部分的任何内容。由于需要尾随零的实现的制造商可能会阻止对标准的任何更改,这些更改会要求他们容忍它的缺失,而没有施加此类不必要要求的实现的程序员将阻止对标准的任何更改,这些更改会要求他们添加额外的数据复制步骤到他们的代码中,除非人们同意将强制尾随字节要求的实现分类为“符合但有缺陷”,并要求他们通过预定义的宏或其他此类方式指示这种缺陷,否则情况将保持僵局。

    【讨论】:

      猜你喜欢
      • 2011-06-06
      • 1970-01-01
      • 2013-05-03
      • 1970-01-01
      • 2020-03-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多