【问题标题】:Allocating less memory than the specified size of a pointer-to-array分配的内存少于指向数组的指针的指定大小
【发布时间】:2021-12-15 15:33:53
【问题描述】:

在 C 语言中,如果我们只访问位于分配内存中的元素,那么为指向数组的指针分配内存不足是否“合法”?还是这会引发未定义的行为?

int (*foo)[ 10 ];                  //Pointer to array of 10 ints
foo = malloc( sizeof( int ) * 5 ); //Under-allocation! 
                                   //Only enough memory for 5 ints
//Now we only ever access (*foo)[ 0 - 4 ]

如果这本身不是未定义的行为,那么访问另一个不相关的对象,其内存地址恰好落在数组未分配部分的地址空间内,这可能会导致严格-混叠违规?

【问题讨论】:

  • 嗯。有趣的是,clang-cl 的代码分析器会警告您的 malloc 行:warning GED7FF984:“malloc”的结果被转换为“int [10]”类型的指针,这与 sizeof 操作数类型“不兼容” int' [clang-analyzer-unix.MallocSizeof]。和foo = malloc(sizeof(int[5])); 的类似警告(但不是在其中使用10 时)。
  • 本机 MSVC 代码分析器不发出警告。但请注意,诊断的存在/不存在(甚至通过 clang)并不能(反)证明 UB。
  • 我认为从技术上讲它是未定义的。考虑int (*foo)[16] 并将前12 个元素设置为某个值。通过对*foo 进行操作,您将告诉编译器那里有一个int [16] 对象,这意味着为它保留了64 个字节。因此编译器可能会决定使用一个不错的快速 AVX-512 指令来存储前 12 个元素。该指令还存储到最后 4 个元素,但编译器认为这没关系,因为它们的值是不确定的,所以它可以使它们成为任何它想要的东西。然后编译器正在覆盖未分配的内存。它甚至可能未映射。
  • @EricPostpischil 在 80 年代有很多大学的学生使用 C 的解释器学习 C。现在通常说 C 编译器,好像语言和编译器会是一样的:)。
  • *foo上没有操作

标签: c malloc language-lawyer pointer-to-array


【解决方案1】:

这是undefined behavior

foo 应该指向int[10] 类型的对象(或对象数组中的第一个)。这被认为是 数组类型 的对象,在C standard

的第 6.2.5p20 节中定义

数组类型描述了一个连续分配的非空 一组具有特定成员对象类型的对象,称为 元素类型。元素类型应是完整的,只要 指定数组类型。数组类型的特点是 元素类型和数组中元素的数量。数组类型 据说是从它的元素类型派生的,如果它的元素类型 是 T ,数组类型有时被称为“T 的数组”。这 从元素类型构造数组类型称为“数组” 类型推导''

我以粗体突出显示的部分是重要部分。因此,int[10] 是一组连续分配的 10 个 int 类型的对象。

您没有分配足够的空间,因此具有 int[10] 类型的表达式 *foo 访问该类型的对象,但这样做会读取已分配内存段的末尾。

【讨论】:

  • 这是一个 [language-lawyer] 问题,最好引用支持此立场的规范部分。
  • @JohnBollinger:我添加了 language-lawyer 标签,因为这似乎是问题的本质,但 OP 应该确认或否认他们的兴趣。
  • 啊。感谢您的澄清,@EricPostpischil。
  • @EricPostpischil 我认为语言律师标签适合这里 :) 我对这个特定点以及标准对此的看法很感兴趣,但没有找到解决相关问题的方法。
  • 我反对 具有 int[10] 类型的表达式 *foo 访问该类型的对象 不超过 int bar[10] 表达式 bar 访问对象,即根本没有,因为*foobar 在任何访问发生之前都会隐式转换为指向第一个元素的指针。
【解决方案2】:

正如@dbush 在his answer 中描述的那样,数组被定义为元素类型(C17 6.2.5/20)的连续分配的非空对象集。显然,malloc( sizeof( int ) * 5 ) 没有为int[10] 分配足够的空间。

但我发现很难正式支持该答案的最后一部分,声称尺寸差异使(例如)(*foo)[4] 具有未定义的行为。这个结论似乎是合理的,但标准实际上是在哪里说的呢?

这里的一个主要问题是(动态)分配的对象没有声明的类型,只有在某些情况下,一个有效的类型取决于它们是如何被访问的。 (C17 6.5/6 和脚注 88)。我们确实知道,在成功时,malloc(n) 返回一个指向大小为n 的对象的指针(C17 7.22.3.4/2),但是我们如何将未定义的行为专门归因于与描述对象的有效类型的对象的关联大小大于n

我最终决定将这些点连接起来的最佳方法如下。假设o 是大小为n 的已分配对象,T 是具有sizeof(T) > n 的完整类型,并且o 是通过T 类型的左值读取或写入的。然后第 6.5/6 段将有效类型 T 归于对象 o,但由于 o 的大小不足,我们必须得出结论,它的表示构成了类型为 T陷阱表示( C17 3.19.4)。然后第 6.2.6.1/5 段重申了“陷阱表示”的定义,并将我们带到了我们想去的地方:

某些对象表示不需要表示 对象类型。 如果一个对象的存储值有这样一个 表示并由不具有的左值表达式读取 字符类型,行为未定义。 如果这样的表示是 由修改对象的全部或任何部分的副作用产生 通过没有字符类型的左值表达式, 行为未定义。这种表示称为陷阱 表示。

(已添加重点。)

【讨论】:

  • 这是正确答案。未定义的行为来自 trap 表示
  • 我不同意这有资格作为陷阱表示。仅仅因为读取或写入超出分配的内存并不意味着读取/写入的表示是陷阱表示。假设int 是没有填充位的二进制补码,则无论访问是否进入未分配空间,都没有陷阱表示。
  • @dbush,它不是任何广义意义上的陷阱表示。事实上,不存在独立于数据类型的陷阱表示。但是,如果分配对象的表示被解释为T 类型的值,那么它必然是该类型的陷阱表示(我声称),因为它太小而不能是其他类型。否则,我们需要发明第三类,既不是有效表示也不是陷阱表示,而且我们还需要在规范中为具有 UB 的访问提供不同的基础。
  • 这与读写无关;它大约是被访问对象的大小。
  • @alinsoar,正如这个答案已经说过的,C 定义“陷阱表示”在 C17 第 3.19.4 节中给出。即:“不需要表示对象类型值的对象表示”。而已。您将认识到在上文引用的第 6.2.6.1/5 段的开头重申了这一点。如果您通过“具体链接”询问与硬件架构或行为的关联,则没有具体链接。规范中的许多其他内容也没有具体的链接。创建这样的链接是实现的作用。
猜你喜欢
  • 2011-12-04
  • 2015-12-10
  • 2019-05-13
  • 1970-01-01
  • 1970-01-01
  • 2015-06-19
  • 1970-01-01
  • 2014-11-10
  • 1970-01-01
相关资源
最近更新 更多