【问题标题】:Proving that "int *p = malloc(1); p[0]" is undefined behavior证明“int *p = malloc(1); p[0]”是未定义的行为
【发布时间】:2015-11-18 09:25:38
【问题描述】:

我试图说服(引用 C99 标准的特定部分)一位同事以下是未定义的行为:

int *p = malloc(1);
p[0] = 0;

但我在标准中找不到明确确保这是未定义的特定部分。 我正在专门寻找标准中从这些行得出结论的逻辑步骤:未定义的行为。是第一行从void *int * 的转换吗?第二行的赋值?

我能找到的关于 malloc 的唯一相关部分是它返回一个适当对齐的指针 (7.20.3):

如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给指向任何类型对象的指针,然后用于访问分配的空间中的此类对象或此类对象的数组(...)

我尝试在规范中查找 space,但由于 white space 和其他词汇问题,噪音太大。

【问题讨论】:

  • 您正在分配 1 字节的内存,然后写入 int(4 字节?)。这应该足以使它成为未定义的行为。
  • 我编辑了问题以澄清我正在专门寻找标准中导致此结论的部分,因为虽然我知道它是 UB,但我无法在标准中找到适当的理由。
  • @Magisch "p[0] 等同于 p"——不,不是。 “并且指针在 c 中可以安全地自动正确使用”——不,它们不是,事实并非如此。
  • @Magisch "从功能上讲,它们是" - 不,它们不是。 如果p 是一个指针,那么p[0]*p 相同.您不可能断言指针始终与它指向的对象相同吗?另外,我没有说你需要投void *,因为你不需要。只是“指针在 c 中安全地自动正确使用”并不意味着, 因为指针在 C 中是不安全的。C 不是托管语言——而且“安全指针”与这种隐式类型转换无关
  • @Magisch p[i] 表示*(p + i)。如果你想要p + i,则拼写为&p[i]

标签: c malloc language-lawyer


【解决方案1】:

7.20.3.3 的 malloc 函数添加到您的报价中:

malloc 函数为 size 为 由大小指定,其值不确定。
malloc 函数返回一个 空指针 或一个指向分配空间的指针。

所以有 2 个可能的未定义行为来源,一个是覆盖(int 的大小保证为 16 位或更多,但您只分配 1 个字节,在几乎所有系统上都是 8 位)缓冲区,第二个可以取消引用空指针。

6.5.2.1 数组下标p[0] = 0 等价于*p = 0*p 的类型是 int,因此它会用 0 填充 sizeof(*p) * CHAR_BIT 位,这可能并不都属于导致 UB 的分配缓冲区。

在第一行代码(赋值)中没有未定义的行为,UB(如果有的话)将在第二行(取消引用)。

但在CHAR_BIT 很大且sizeof(int)1 的机器上,当malloc 没有return 为空指针时,这将是明确定义的行为。

【讨论】:

  • int 大小的唯一保证是它至少是 16 位,对吧? CHAR_BIT 是否有可能是 16 和 sizeof(int) == sizeof(char)?该代码在那个奇怪的 C 实现上是合法的。回复:@Oliver 的观点:在对象之外写入的标准是 UB。不过,我自己没有快速拨号的标准来引用它的 sn-ps。
  • @PeterCordes 是的,你是对的。有些机器(Crays)的 CHAR_BIT 为 32。这意味着 sizeof(int) = sizeof(char) = 1。这意味着 malloc(1) 相当于 malloc(sizeof(int)) 但在 ILP32 和 LP64 系统上,这显然是 UB。
  • @Peter Cordes 实际上没有像“标准中的 int 必须有 16 位”这样的东西。除 char 之外的所有类型的大小要求仅以 char 大小的倍数给出。该标准保证 sizeof(int) >= 2。
  • @Vincent 根据C 规范,int 必须至少支持映射到最低 16 位要求的[−32767, +32767] 范围。而sizeof(int)NUMBER_OF_BITS / CHAR_BIT
  • @Vincent:请在标准要求sizeof(int) >= 2的地方贴出段落。唯一的要求是sizeof(char) == 1。并且 PeterCordes 没有声明“int 必须有 16 位”。他刚刚声明它有至少 16 位,这遵循int (+/-32767) 的最小要求范围。 sizeof(int) 的结果来自 this 和 CHAR_BIT。 (不过,他混淆了大小和宽度)
【解决方案2】:
int *p = malloc(1);
p[0] = 0;

这是未定义的行为,因为您已分配 1 个字节,并且在上述分配中您尝试写入四个字节(假设 int 是四个字节)。只要sizeof(int) > 1 就成立。

【讨论】:

  • 或者更确切地说,尝试在系统上写入超过 1 个字节(等于 sizeof(int) - 1)。
  • 您假设int 不仅仅是一个字节。它可以是,即使在一些现代系统上。
  • @Deduplicator:是的,但这种情况很少见,而且其他人已经涵盖了这一点。
【解决方案3】:

引用标准:

J.2,未定义的行为:在以下情况下行为未定义:...数组下标超出范围,即使对象显然可以使用给定的下标访问 p>

6.2.5,类型,20:数组类型描述了一组连续分配的非空对象。

只要sizeof(int) > 1,您的malloc(1) 没有分配一组非空对象,因此分配的数组大小为零,而p[0] 您使用超出范围的下标访问。 QED。

【讨论】:

  • 附件J是非规范性的,这段代码中没有涉及到数组类型
【解决方案4】:

6.5.3.2 Address and indirection operators

...

语义

一元 & 运算符产生其操作数的地址。如果操作数 类型为“type”,结果类型为“pointer to type”。如果 操作数是一元 * 运算符的结果,既不是该运算符也不是 & 运算符被评估,结果就像两者都被省略了, 除了对运营商的限制仍然适用并且 结果不是左值。同样,如果操作数是 [] 运算符,既不是 & 运算符,也不是 一元 * [] 隐含的被求值,结果就像 & 运算符被删除, [] 运算符被更改为 + 操作员。否则,结果是指向对象或函数的指针 由其操作数指定。

一元 * 运算符表示间接。如果操作数指向一个 函数,结果是一个函数指示符;如果它指向一个 对象,结果是一个指定对象的左值。如果操作数 类型为“类型指针”,结果类型为“类型”。 如果 无效的值已分配给指针,的行为 一元 * 运算符未定义。

[] 运算符是指针上隐含的* 运算符。对于int,分配给指针的值与sizeof( int ) > 1 一样无效

行为未定义。

NULL 是一个无效指针,所以这也涵盖了malloc() 返回NULL

【讨论】:

  • “无效值”似乎没有在任何地方由标准定义,我所看到的只是非详尽的示例列表(主要是在非规范情况下)。例如this thread 间接询问其含义。此外,“____ 的无效值”似乎没有在任何地方使用。一个值要么无效,要么无效,当然malloc(1)(如果不返回 null)返回一个有效值。
  • 您引用的第一段不适用于此代码;它描述了& 运算符的使用,这在此代码中没有出现。在您的粗体文本中,“操作数”的意思是“& 的操作数”。
  • @M.M - 我在第一段中包含了对 [] 运算符的引用,它是一个隐含的一元 * 运算符,因为这是第二段用来指定 UB 的。 (续)
  • 标准规定:一元 * 操作符取消引用指针的无效值包括空指针、与指向的对象类型不恰当对齐的地址,以及对象在其生命周期结束之后。 并且,在 7.1.4 中:如果函数的参数具有无效值(例如函数域之外的值, 或程序地址空间外的指针... 将指向一个字节的指针解引用为更大的东西很可能实际上是“程序地址空间外的指针”因此无效。
  • *[] 之间的关系在 6.5.2.1(数组下标)中明确描述,因此可以引用该部分作为直接证据
【解决方案5】:

代码*p 包含在(至少-其他部分也可能包含)6.3.2.1/1:

左值是一个表达式(对象类型不是 void),它可能 指定一个对象;如果左值在计算时未指定对象,则行为未定义。

“对象”的定义是:

执行环境中的数据存储区域,其内容可以表示值

左值*p 指定sizeof(int) 字节的空间,但是只有1 字节的存储可以表示值(换句话说,未分配的空间不能构成对象的一部分)。所以,如果sizeof(int) > 1,那么*p 不指定一个对象。


对于问题p[0] 中的实际代码:这相当于*(p+0)。从 6.5.6/8 开始,我不清楚 p + 0 是否会导致 UB。但这没有实际意义,因为即使它不会导致 UB,对结果的延迟也会如上所示;所以p[0] 会导致 UB。

【讨论】:

  • 找不到任何引用,但是查看加减法的定义,编译器不能非常安全地删除常量零的加法或减法。 (多次阅读 6.5.6.8 我不确定我的论点)
  • @MohitJain 很好,它说“如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素 [... ]" 但是,p 在这种情况下并不指向数组对象的元素。好吧,无论如何都不是整数数组的元素!我认为措辞不是很准确。通过这种措辞,int x[5]; int *p = x + 5 - 5; 将是未定义的,我认为这不是有意的。另一方面,似乎普遍认为将0 添加到空指针是未定义的。
  • 同意。只是一个微不足道的更正,int *p = x + 5 - 5; 定义明确,int *p = x + 6 - 6; 不是。
  • @MohitJain 不,我是说x + 5 - 5x + 5 不指向数组对象的元素,所以如果我们按字面意思理解上面的引用,那么除了 - 1 之外,它不能减去任何东西
  • x + k 的有效性不意味着x + k - k 有效吗?在提到的部分中,这个k1。否则x + 5也应该是UB。
【解决方案6】:
malloc(1)

将地址返回到 1 字节的大缓冲区。

一般来说,int 大于 1 个字节。

因此,将 int 值分配给 1 字节的大缓冲区是 UB。

malloc 返回的指针不需要在 c 中强制转换,因为它们在使用时会安全且自动地提升为正确的指针类型。

【讨论】:

  • 一般来说,除非你也考虑DSP等。无论如何,这比台式机更常见。
  • 我认为从技术上讲malloc(1) 返回一个指向缓冲区的指针,该缓冲区大到足以容纳 至少 1 个字符。我见过的所有 malloc() 实现都将大小强制为原生字长的整数。这并没有改变行为未定义的事实,但确实解释了为什么程序不会立即崩溃。
  • @TMN 这可能是真的,但在 C 标准中没有定义为必需的。所以充其量是它的实现定义的。
猜你喜欢
  • 2017-12-16
  • 1970-01-01
  • 1970-01-01
  • 2019-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-22
  • 2019-11-21
相关资源
最近更新 更多