【问题标题】:Does the C standard guarantee buffers are not touched past their null terminator?C 标准保证缓冲区不会超过它们的空终止符吗?
【发布时间】:2015-04-27 01:33:23
【问题描述】:

在为标准库的许多字符串函数提供缓冲区的各种情况下,是否保证缓冲区不会被修改超出空终止符?例如:

char buffer[17] = "abcdefghijklmnop";
sscanf("123", "%16s", buffer);

现在是否要求buffer 等于"123\0efghijklmnop"

另一个例子:

char buffer[10];
fgets(buffer, 10, fp);

如果读取的行只有 3 个字符长,可以确定第 6 个字符与调用 fgets 之前相同吗?

【问题讨论】:

  • 虽然这是事实,但对于许多其他边缘情况,该标准并未提供保证。
  • 无法想象某事并不是假设某些编译器编写者或库实现者无法想象的好理由。如果所有 C 程序员都有同样的想象力,那就太无聊了。
  • @Ángel 我相信你没有抓住问题的重点。
  • 更好的问题是“为什么重要?”。您不应该依赖幕后和未指明的行为。如果需要该行为,则应创建/使用明确打算以这种方式运行的显式函数。
  • 了解暗角的位置有助于避免它们。

标签: c standards c-standard-library


【解决方案1】:

C99 draft 标准没有明确说明在这些情况下应该发生什么,但通过考虑多种变体,您可以表明它必须以某种方式工作,以便在所有情况下都符合规范。

标准说:

%s - 匹配一系列非空白字符。252)

如果不存在 l 长度修饰符,则相应的参数应为 指向足够大的字符数组的初始元素的指针,以接受 序列和一个终止的空字符,它将自动添加。

这里有两个例子表明它必须按照您提议的方式工作才能达到标准。

示例 A:

char buffer[4] = "abcd";
char buffer2[10];  // Note the this could be placed at what would be buffer+4
sscanf("123 4", "%s %s", buffer, buffer2);
// Result is buffer =  "123\0"
//           buffer2 = "4\0"

示例 B:

char buffer[17] = "abcdefghijklmnop";
char* buffer2 = &buffer[4];
sscanf("123 4", "%s %s", buffer, buffer2);
// Result is buffer = "123\04\0"

请注意,sscanf 的接口没有提供足够的信息来真正知道它们是不同的。所以,如果 Example B 要正常工作,它不能与 Example A 中的空字符后面的字节混淆。这是因为它必须根据这个规范在两种情况下都工作。

所以隐式由于规范,它必须按照你所说的那样工作。

可以为其他函数放置类似的参数,但我想你可以从这个例子中看到这个想法。

注意: 以格式提供大小限制,例如“%16s”,可以改变行为。根据规范,在将数据写入缓冲区之前,sscanf 将缓冲区归零到其限制在功能上是可以接受的。在实践中,大多数实现都选择性能,这意味着它们不考虑其余部分。

当规范的意图是进行这种归零时,通常会明确指定。 strncpy 就是一个例子。如果字符串的长度小于指定的最大缓冲区长度,它将用空字符填充剩余的空间。这个相同的“字符串”函数也可以返回一个未终止的字符串,这一事实使得它成为人们推出自己版本的最常见的函数之一。

就 fgets 而言,可能会出现类似的情况。唯一的问题是规范明确指出,如果没有读入,则缓冲区保持不变。一个可接受的功能实现可以通过在清零缓冲区之前检查是否至少有一个字节要读取来回避这一点。

【讨论】:

  • 聪明。我喜欢你的想法=)
  • 我想得越多,如果您为缓冲区提供大小,您的论点不会崩溃,这就是问题的全部内容...在您的示例中,如果我们提供大小对于格式的缓冲区,它们必须按照标准不重叠,不是吗?然后我们就不能再隐含地假设任何事情......
  • @Segmented: fgets 明确记录为在读取错误后未指定缓冲区的全部内容(即,从提供的起始地址开始提供的字节数)。如果没有读取错误,那么我相信插入0字节之后的缓冲区部分不会被触及。
  • @rici 你能提供一个链接吗?特别是第二部分。
  • @Segmented:请参阅我对那部分的回答。
【解决方案2】:

缓冲区中的每个单独字节都是一个对象。除非sscanffgets 的函数描述的某些部分提到修改这些字节,或者甚至暗示它们的值可能会改变,例如通过声明它们的值变得未指定,然后一般规则适用:(强调我的)

6.2.4 对象的存储时长

2 [...] 对象存在,具有恒定地址,并在其整个生命周期内保留其最后存储的值。 [...]

同样的原则保证了这一点

#include <stdio.h>
int a = 1;
int main() {
  printf ("%d\n", a);
  printf ("%d\n", a);
}

尝试打印 1 两次。虽然a 是全局的,但printf 可以访问全局变量,并且printf 的描述没有提到修改a

fgetssscanf 的描述都没有提到修改缓冲区超出实际应该写入的字节(除了读取错误的情况),因此这些字节不会被修改。

【讨论】:

  • 没有什么能阻止 fgets 在写入之前清除缓冲区的实现。从规范的角度来看,这是一个有效的实现。 6.2.4 当然没有说 fgets 不能做到这一点。它只是说,如果 fgets 改变它,它不会再自行改变。
  • @caveman 事实上,没有任何东西授予fgets 更改除明确指定要修改的字节之外的任何字节的权限,这意味着fgets 的这种实现将不符合要求。在抽象机器中,fgets 不会更改那些字节,并且这些字节的值不会变得不确定、未指定或未定义。因此,具体实现必须保持将这些字节保留为其最后存储值的行为。如果您不同意,那么您如何看待我回答中的示例?是否允许打印"1\n2\n"?如果没有,为什么不呢?
  • 这是一个很好的论点,我同意。我认为你的评论比你的答案本身更清楚。我在您的回答中遗漏的要点是您将每个字节视为一个对象。因此,它们不会变得不确定、未指定或已定义的声明真正阐明了您的意思。
  • 就标准而言,这可能还不是全部。定义最后存储的值可能是一个长故事,否则您将无法拥有可由硬件更改的内存映射寄存器(这是许多嵌入式平台上的常用技术)。
  • @RespawnedFluff 该标准在“对于易失性对象的情况下,程序中的最后一个存储不需要显式”的语句中附加了一个脚注。该脚注得到了描述volatile (6.7.3p6) 的规范性文本的支持。但在 OP 的情况下,没有 volatile 对象,所以这不是问题。
【解决方案3】:

标准对此有些模棱两可,但我认为对它的合理解读是:是的,不允许向缓冲​​区写入比读取+空更多的字节。另一方面,对文本进行更严格的阅读/解释可能会得出结论,答案是否定的,不能保证。这是publicly avaialble draftfgets 的评价。

char *fgets(char * restrict s, int n, FILE * restrict stream);

fgets 函数最多从stream 指向的流中读取比n 指定的字符数少一个到s 指向的数组。在换行符(保留)之后或文件结尾之后不会读取其他字符。在读入数组的最后一个字符之后立即写入一个空字符。

如果成功,fgets 函数将返回 s。如果遇到文件结尾并且没有字符被读入数组,则数组的内容保持不变并返回一个空指针。如果在操作过程中发生读取错误,则数组内容不确定,返回空指针。

保证应该从输入中读取多少,即在换行符或 EOF 处停止读取,并且读取的内容不超过 n-1 字节。尽管没有明确说明允许写入 到缓冲区的程度,但众所周知的是fgetsn 参数用于防止缓冲区溢出。有点奇怪,标准使用了模棱两可的术语read,这不一定意味着gets不能write到缓冲区超过n字节,如果你想挑剔它使用的术语。但请注意,这两个问题使用相同的“读取”术语:n-limit 和 EOF/换行符限制。因此,如果您将n 相关的“读取”解释为缓冲区写入限制,那么[为了一致性]您可以/应该以相同的方式解释另一个“读取”,即写入的内容不超过字符串为时读取的内容比缓冲区短。

另一方面,如果您区分短语动词“read into”(=“write”)和“read”的用法,那么您就不能以同样的方式阅读委员会的文本。您可以保证它不会“读入”(=“写入”)数组超过n 字节,但是如果输入字符串更快地被换行符或 EOF 终止,您只能保证其余的( input) 不会被“读取”,但在这种更严格的读取下,这是否意味着缓冲区不会被“读取”(=“写入”)是不清楚的。关键问题是关键字是“into”,它被省略了,所以问题是我在下面修改的引号中括号中给出的完成是否是预期的解释:

在换行符(保留)之后或文件结尾之后,不会将其他字符读入 [到数组中]。

坦率地说,一个postcondition 被表述为一个公式(在这种情况下会很短)会比我引用的措辞更有帮助......

我懒得去分析他们关于*scanf 家族的文章,因为我怀疑考虑到这些函数中发生的所有其他事情,它会变得更加复杂;他们为fscanf 撰写的文章大约有五页长……但我怀疑类似的逻辑也适用。

【讨论】:

  • 不难想象,对于某些类型的操作系统,允许覆盖超出第一行末尾的信息 [但在指定的空间量内] 可以提高性能。如果缓冲区为 128 字节,读取 128 字节的时间小于读取 1 字节的时间的两倍,并且相对 fseek 的时间类似,那么除了短于四个字符的行,读取 128 字节,扫描一个换行,然后在需要时返回可能比单独读取字节更快。
  • @supercat:嗯,是的,你可以想象得到,但是[在任何规范中] 的常识是函数不会有(用户可见的)副作用标准中规定。在 C 标准的情况下,hvd's answer 已经指出了保证这一普遍的常识性原则的措辞。
  • 我同意,由于标准没有授权实现写到末尾,fgets 不应该期望客户端代码容忍这种行为。我的观点是,如果那些编写规范的人选择这样做,那么将缓冲区内容描述为未指定的空再见之后会有潜在的性能优势。
【解决方案4】:

是否保证缓冲区不会被修改超出null 终结者?

不,不能保证。

现在需要缓冲区等于“123\0efghijklmnop”吗?

是的。但这仅仅是因为您在与字符串相关的函数中使用了正确的参数。如果您弄乱了缓冲区长度,输入修饰符到sscanf 等,那么您的程序将编译。但它很可能会在运行时失败。

如果读取的行只有 3 个字符长,可以确定第 6 个字符与调用 fgets 之前相同吗?

是的。一旦fgets() 发现您有一个 3 字符的输入字符串,它将输入存储在提供的缓冲区中,并且根本不关心提供的空间的重置。

【讨论】:

  • 你自相矛盾,如果不能保证,那么你提供的两个答案应该是“否”。
  • @Segmented :再次阅读我的答案(有编辑)。伊戈尔 S.K.只是解释了现实(有时与标准不同)
  • @Segmented 好吧,“许多字符串函数”的“在各种情况下”“否”是一个很常见的答案。想象一下strcpy(dst,src)src 将完全保持不变,但写入dst 可能会导致缓冲区溢出。或者它可能只是部分修改,保持dst[strlen(src)] 之后的所有内容不变。而且我也给了你具体的例子我的答案。
【解决方案5】:

现在需要缓冲区等于“123\0efghijklmnop”吗?

这里的buffer 只包含123 保证在NUL 处终止的字符串。

是的,分配给数组buffer 的内存不会被解除分配,但是你要确保/限制你的字符串buffer 最多只能有16 char 元素,你可以在任何时候读入它时间。现在取决于您是只写一个字符还是buffer 可以接收的最大值。

例如:

char buffer[4096] = "abc";` 

实际上在下面做了一些事情,

memcpy(buffer, "abc", sizeof("abc"));
memset(&buffer[sizeof("abc")], 0, sizeof(buffer)-sizeof("abc"));

标准坚持认为,如果 char 数组的任何部分被初始化,那么它在任何时候都包含它的全部内容,直到遵守其内存边界。

【讨论】:

  • 终结符之外的内存不会消失。正是在那里,我很好奇标准规定了什么,而不是在它之前。
  • 是的,您可以随时阅读它们,这就是为什么我很好奇标准对终结符之外的额外空间有何规定。
【解决方案6】:

标准没有任何保证,这就是为什么建议使用函数sscanffgets,如您在问题中显示的那样(以及使用@987654323与gets 相比,@ 被认为更可取)。

但是,一些标准函数在其工作中使用空终止符,例如strlen(但我想你问的是字符串修改)

编辑:

在你的例子中

fgets(buffer, 10, fp);

保证 10-th 之后的字符不受影响(buffer 的内容和长度不会被fgets 考虑)

EDIT2:

此外,当使用fgets 时,请记住'\n' 将存储在缓冲区中。例如

 "123\n\0fghijklmnop"

而不是预期的

 "123\0efghijklmnop"

【讨论】:

  • 我不是反对者,但您似乎没有掌握最初的问题,而且您没有引用标准。
  • 标准没有提及将字符字符保存在允许 fgets 的第二个参数使用的内存中......所以我的回答“不保证”
【解决方案7】:

取决于使用的功能(以及在较小程度上的实现)。 sscanf 将在遇到第一个非空白字符时开始写入,并继续写入直到它的第一个空白字符,在那里它将添加一个结束 0 并返回。但是像strncpy(著名)这样的函数会将缓冲区的其余部分清零。

然而,C 标准中没有规定这些函数的行为方式。

【讨论】:

    猜你喜欢
    • 2016-12-16
    • 1970-01-01
    • 2015-02-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多