证明“int *p = malloc(1); p[0]”是未定义行为

15

我正在尝试说服同事(引用C99标准的具体部分),以下内容是未定义行为:

int *p = malloc(1);
p[0] = 0;
但我无法在标准中找到明确确保这是未定义的部分。我特别寻找标准中从这些行到结论 "未定义行为" 的逻辑步骤。第一行是从 void * 转换成 int * 吗?第二行是赋值吗?
关于 malloc 的唯一相关部分是它返回一个适当对齐的指针 (7.20.3):
"如果分配成功,返回的指针将适当地对齐,以便可以将其分配给任何类型的对象的指针,然后用于访问所分配空间中的该对象或该对象数组"
我尝试在规范中搜索空间,但由于空格和其他词汇问题,有太多的噪音。

9
你正在分配 1 字节的内存,然后写入一个 int(4 字节?)。这足以使其成为未定义行为。 - Manos Nikolaidis
3
我修改了问题,以澄清我特别寻找标准中导致这个结论的部分,因为虽然我知道它是未定义的行为,但我找不到标准中适当的理据。 - anol
2
@Magisch "p[0] 等同于 p" - 不是这样的。"指针在 C 语言中可以安全自动正确地使用" - 不是这样的,事实与此相反。 - The Paramagnetic Croissant
2
@Magisch "从功能上来说,它们是" - 不,它们不是。如果p是一个指针,那么p[0]就相当于*p。你不可能断言指针总是与其所指向的对象相同吧?另外,我没有说过你需要强制转换void *,因为你不需要。只是因为“在C中指针可以安全自动正确地使用”并不意味着指针是安全的,因为在C中指针是不安全的。C不是一种托管语言 - “安全指针”与这种隐式类型转换毫无关系 - The Paramagnetic Croissant
2
@Magisch p 是一个地址。p[0] 是该地址处的数据。这两个东西可能非常不同,特别是因为 int 和地址甚至不一定具有相同的大小。 - 8bittree
显示剩余6条评论
6个回答

18

7.20.3.3 The malloc function中添加到您的引述:

malloc函数为一个对象分配内存空间,该对象的大小由参数size指定,其值是不确定的。
malloc函数返回一个空指针或者指向分配空间的指针。

所以有两种可能导致未定义行为的情况,一种是覆盖(保证int类型的大小为16位或更多,但您只分配了1个字节,这在几乎所有系统上都是8位),另一种是可能解引用空指针。

6.5.2.1 Array subscripting中可以知道,p[0] = 0等价于*p = 0。而*p的类型是int,因此它会将sizeof(*p) * CHAR_BIT位填充为0,这些位可能并不完全属于已分配的缓冲区,从而导致未定义行为。

第一行代码(赋值)中没有未定义行为,如果存在任何未定义行为,则会出现在第二行代码(解引用)中。

但是,在CHAR_BIT非常大且sizeof(int)1的机器上,只要malloc未返回空指针,这将是定义良好的行为。


6
int 的大小保证至少为 16 位,对吗?CHAR_BIT 可能为 16,且 sizeof(int) == sizeof(char) 吗?在这种奇怪的 C 实现中,该代码将是合法的。关于@Oliver的观点:标准规定在对象外写入是未定义行为。我自己没有标准来引用其中的片段。 - Peter Cordes
6
@PeterCordes 您是正确的。有些机器(比如 Cray)中,CHAR_BIT 的值为32,这意味着 sizeof(int) = sizeof(char) = 1。因此,malloc(1)malloc(sizeof(int)) 等价。但在 ILP32 和 LP64 系统中,这显然是未定义行为。 - Mohit Jain
2
@Peter Cordes 实际上并没有像“int必须在标准中有16位”这样的东西。除了char之外,所有类型的大小要求仅以char的大小的倍数给出。标准保证sizeof(int)> = 2。 - Vincent
2
根据C语言规范,一个int必须支持至少[-32767, +32767]的范围,这对应于最小的16位要求。而sizeof(int)NUMBER_OF_BITS / CHAR_BIT - Mohit Jain
10
@Vincent:请发布标准要求 sizeof(int) >= 2 的段落。唯一的要求是 sizeof(char) == 1。PeterCordes 没有说过“一个 'int' 必须有16位”。他只是说明它至少有16位,这符合 int 所需的最小范围(+/-32767)。sizeof(int) 的结果就是由此和 CHAR_BIT 确定的。(尽管他混淆了 size 和 width) - too honest for this site
显示剩余15条评论

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

由于您只分配了1个字节,并且在上面的赋值中尝试写入四个字节(假设int为四个字节),因此这是未定义的行为。只要sizeof(int) > 1,这就是正确的。


或者更好的说法是,在您的系统上尝试写入超过1个字节(等于sizeof(int)-1)的内容。 - Am_I_Helpful
你假设 int 不只是一个字节。但实际上,在一些现代系统中,它确实可以只有一个字节。 - Deduplicator
@Deduplicator:是的,但这很少见,而且其他人已经涵盖了这一点。 - Giorgi Moniava

5

6.5.3.2地址和间接运算符

...

语义

一元 & 运算符返回其操作数的地址。 如果操作数具有类型 "type",则结果的类型为 "指向 type 的指针"。 如果操作数是一元 * 运算符的结果,则不评估该运算符或 & 运算符,并且结果就像两者都省略了一样,但运算符的限制仍然适用,并且结果不是 lvalue。 同样,如果操作数是 [] 运算符的结果,则不评估 & 运算符或由 [] 暗示的一元 *,并且结果就像 & 运算符被移除并且 [] 运算符被改为 + 运算符一样。否则,结果是指定其操作数的对象或函数的指针。

一元 * 运算符表示间接。 如果操作数指向函数,则结果是函数指示器; 如果它指向对象,则结果是指示对象的 lvalue。 如果操作数具有类型 "指向 type 的指针",则结果的类型为 "type"。 如果向指针分配了无效值,则一元 * 运算符的行为未定义。

[] 运算符是指针上暗示的 * 运算符。 只要 sizeof(int)>1,分配给指针的值对于 int 来说就是无效的。

这种行为是未定义的。

而 NULL 是一个无效指针,因此也包括 malloc() 返回 NULL 的情况。


“invalid value” 似乎在标准中没有定义过,我所看到的只有一些不详尽的例子列表(大多数是在非规范情况下)。例如,这个线程间接地询问了它的含义。此外,“____的无效值”似乎没有被使用过;一个值要么有效,要么无效,当然,如果 malloc(1) 不返回 null,则会返回一个有效值。 - M.M
你引用的第一段不适用于此代码;它描述了&运算符的使用,而该代码中没有出现。在你加粗的文本中,“操作数”指的是“&”的操作数。 - M.M
@M.M - 我包括了第一段,因为它提到[]操作符是一个隐含的一元*操作符,而第二段用来指定UB。 (续) - Andrew Henle
标准规定:在使用一元运算符对指针进行解引用时,无效值包括空指针、指向不适当对齐的对象类型的地址以及指向生命周期结束后的对象的地址。* 在7.1.4中:如果函数的参数具有无效值(例如函数域之外的值或程序地址空间之外的指针... 将一个指向一个字节的指针作为更大的东西进行解引用可能会有效地“超出程序地址空间的指针”,因此是无效的。 - Andrew Henle
*[] 之间的关系在6.5.2.1(数组下标)中有明确描述,因此该部分可以被引用作为直接证据。 - M.M
但是6.5.2.1并没有明确将[]运算符标记为隐含的一元*,就像6.5.3.2的第一段那样,而正是6.5.3.2的第二段使得在“无效”的指针上使用*运算符成为未定义行为。 - Andrew Henle

5

标准中的引用:

J.2,未定义行为:在以下情况下行为是未定义的:……数组下标超出范围,即使对象使用给定下标似乎是可访问的。

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

只要 sizeof(int) > 1,你的 malloc(1) 就没有分配一个非空对象集,所以分配的数组大小为零,并且使用 p[0] 访问了一个超出范围的下标。QED。


附录J是非规范性的,并且在此代码中没有涉及任何数组类型。 - M.M

2
代码*p至少被6.3.2.1/1(也可能由其他部分覆盖)涵盖:
一个lvalue是一个表达式(具有非void对象类型),其可能指定一个对象;如果在评估lvalue时它不指定对象,则其行为未定义。
“对象”的定义是:
在执行环境中的数据存储区域,其内容可以表示值。 lvalue *p 指定sizeof(int) 个字节的空间,但只有1个字节的存储空间可表示值(换句话说,未分配的空间不能形成对象)。因此,如果 sizeof(int) > 1 ,则*p 不指定对象。
对于问题中的实际代码p[0]:这相当于*(p+0)。从6.5.6/8中不清楚p + 0是否会引起UB。但这是无关紧要的,因为即使它不引起UB,根据上述所示,解引用结果也会引起UB;因此,p[0] 任一种情况都会引起未定义行为。

找不到任何参考资料,但是根据加法和减法的定义,编译器是否可以安全地删除带有常量零的加法或减法。 (阅读了6.5.6.8很多次后,我对自己的论点不确定) - Mohit Jain
@MohitJain 嗯,它说“如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向原始元素偏移的元素[...]”但是,在这种情况下,p并没有指向数组对象的元素。嗯,至少不是int数组的元素!我认为措辞不是很精确。按照这个措辞,int x [5]; int * p = x + 5-5;将是未定义的,我认为这不是预期的。另一方面,似乎普遍认为将0添加到空指针是未定义的。 - M.M
同意。只有一个微不足道的更正,int *p = x + 5 - 5; 是良好定义的,而 int *p = x + 6 - 6; 则不是。 - Mohit Jain
@MohitJain 不,我的意思是说 x + 5 - 5x + 5 不指向数组对象的任何元素,因此如果我们按照上面的引用字面意思来理解,那么除了后面明确提到的 -1 之外,它不能被减去任何东西。 - M.M
x + k的有效性难道不意味着x + k - k也是有效的吗?在引用的部分中,这里的k1。否则,x + 5也应该是未定义行为。 - Mohit Jain
不行,因为x指向一个数组对象的元素,但x + 5不是。 - M.M

0
malloc(1)

返回一个指向1字节大缓冲区的地址。

一般来说,int比1字节大。

因此,将int值分配给1字节大的缓冲区是未定义行为。

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


一般来说,除非您还考虑数字信号处理器等设备。不过这些设备比台式机更加普遍。 - Deduplicator
1
我认为从技术上讲,malloc(1)返回一个指向足以容纳至少1个字符的缓冲区的指针。我见过的所有malloc()实现都将大小强制转换为本机字大小的整数倍。这并不改变行为未定义的事实,但可以解释为什么程序不会立即崩溃。 - TMN
@TMN 这可能是正确的,但在 C 标准中并没有定义为必需的。因此,最好将其实现定义。 - Magisch

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