calloc()能否总共分配超过SIZE_MAX的内存?

35

最近的代码审查中声称:

在某些系统上,calloc()可以分配超过SIZE_MAX总字节,而malloc()受到限制。

我认为这是错误的,因为calloc()创建了一个对象数组的空间 - 这个数组本身也是一个对象。因此,没有任何对象的大小能超过SIZE_MAX

那么我们中谁是正确的呢?在一个(可能是假想的)地址空间大于size_t范围的系统上,当调用参数乘积大于SIZE_MAX时,calloc()允许成功吗?

更具体地说:以下程序是否会以非零状态退出?

#include <stdint.h>
#include <stdlib.h>

int main()
{
     return calloc(SIZE_MAX, 2) != NULL;
}

2
更多引用:“一个好的calloc(n, size)将检测到n * size大于SIZE_MAX的情况”。这实际上看起来像是一种观点。标准没有提到“好的calloc”,也没有关于检测“n * size大于SIZE_MAX”的情况的说明。 - user7860670
1
@你好,没错。我不相信这是一个有效的调用,因为这样的数组违反了size_t可以表示任何对象的规则。 - Toby Speight
3
DR266似乎与此有关。只发现了这个链接(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1061.htm):`DR-266RM位置是sizeof从不溢出。DG - 忽略calloc问题。PJ - 根据定义,size_t必须可表示并且不能溢出。试图溢出应该是一种约束违规/未定义行为。` - KamilCuk
2
这里是DR-266的链接。 - P.P
@Stargateur 我不确定我错过了什么 - 这正是我在评论中指出的。DR-266没有直接涵盖这一点,但提供了相关信息。 - P.P
显示剩余14条评论
7个回答

20

calloc()能否分配超过SIZE_MAX的总字节数?

由于断言“在某些系统上,calloc()可以分配超过SIZE_MAX的总字节数,而malloc()受到限制。”来自我发布的评论,我将解释我的理由。


size_t

size_t是至少16位的一些无符号类型。

size_tsizeof运算符的结果的无符号整数类型;C11dr §7.19 2

“其实现定义值应等于或大于以下给定值的相应值”...size_t的极限值SIZE_MAX... 65535 §7.20.3 2

sizeof

sizeof运算符返回其操作数的大小(以字节为单位),操作数可以是表达式或类型的括号名称。§6.5.3.4 2

calloc

void *calloc(size_t nmemb, size_t size);

calloc函数为一个大小为nmemb*size的数组分配空间,其中每个对象的大小为size。§7.22.3.2 2


考虑nmemb * size远超过SIZE_MAX的情况。

size_t alot = SIZE_MAX/2;
double *p = calloc(alot, sizeof *p); // assume `double` is 8 bytes.
如果calloc()真正分配了nmemb * size个字节,并且如果p!= NULL为真,则这违反了什么规范?

每个元素(每个对象)的大小都是可表示的。

// Nicely reports the size of a pointer and an element.
printf("sizeof p:%zu, sizeof *p:%zu\n", sizeof p, sizeof *p); 

每个元素都可以被访问。

// Nicely reports the value of an `element` and the address of the element
for (size_t i = 0; i<alot; i++) {
  printf("value a[%zu]:%g, address:%p\n", i, p[i], (void*) &p[i]); 
}

calloc()细节

"分配一个由nmemb对象组成的数组的空间":这无疑是一个有争议的关键点。 "为数组分配空间"是否要求<= SIZE_MAX?我在C规范中找不到要求此限制的内容,因此得出结论:

calloc()可能总共分配超过SIZE_MAX的内存。


calloc()使用较大的参数返回非NULL通常是不常见的 - 不管是否符合规范。通常这种分配会超出可用的内存,所以这个问题就没了。 我遇到的唯一情况是在Huge memory model中,其中size_t为16位,对象指针为32位。


@chux 你有一个libc实现的例子可以工作吗?这将需要在比size_t更大的类型中存储实际大小,我非常怀疑任何calloc的实现都是如此。我刚刚检查了几个libc实现,它们都将乘积放在size_t中;其中一个检查溢出并返回NULL,另一个只返回一个截断溢出大小的数组,如果您尝试迭代它,则会访问越界(当然会引发未定义行为),因此这肯定是不安全的。 - Kevin
1
@Kevin,正如答案中所述,这样的calloc()在旧时代具有巨大的内存模型。 calloc()仅通过将nmemb,size相乘以形成所需大小,而不考虑OF,是calloc()的弱实现。 libc的这种弱点,特别是在您在其他方面指出的已准备好的修复程序中,不禁止C规范可以允许的内容。 OP的标题问题不是:可以使用大操作数返回非NULL吗?当然可以-使用弱库代码。 问题是“calloc()是否可以分配超过SIZE_MAX的内存...?”-暗示:calloc()是否可以正确执行此操作? - chux - Reinstate Monica

19

SIZE_MAX并不一定指定对象的最大大小,而是size_t的最大值,这两个概念并非完全相同。详见为什么数组的最大大小“太大”?

但是很明显,向期望size_t参数的函数传递比SIZE_MAX更大的值是没有定义的。因此,在理论上,SIZE_MAX是限制,理论上将允许分配SIZE_MAX * SIZE_MAX字节。

malloc/calloc分配的对象没有类型。有类型的对象有各种限制,例如永远不会超过某个限制,例如SIZE_MAX。但是,由这些函数返回的结果所指向的数据没有类型,它还不是一个数组。

形式上,该数据没有声明类型,但当您在分配的数据中存储某些内容时,它就具有了用于存储的数据的有效类型(C17 6.5 §6)。

这反过来意味着可以分配比C语言中任何类型能够容纳的更多的内存,因为分配的内容还没有类型。

因此,就C标准而言,calloc(SIZE_MAX, 2)返回一个与NULL不同的值是完全可以接受的。如何以明智的方式使用分配的内存,或者哪些系统甚至支持在堆上分配这么大块的内存,是另外一回事。


这确实暗示了SIZE_MAXptrdiff_t之间的一种特殊关系,因为在一个calloc可能表现如描述的系统上,ptrdiff_t必须足够大以应对。 - Steve Summit
1
@SteveSummit 是的,正如链接帖子中被接受的答案所解释的那样,SIZE_MAXPTRDIFF_MAX必须始终遵循,后者是有符号类型。然而,如果给定一个SIZE_MAX 2^n,标准并不限制编译器必须具有一个2^(n+1)的PTRDIFF_MAX。实际上,对于编译器来说,拥有这样一个繁琐的类型系统非常不方便,因此在实践中并没有实现。总的来说,C标准处理这两种类型的问题并不好,但将思考留给了“实现”。 - Lundin
如果标准库没有将calloc()定义为可能失败的方式,DOS COMPACT内存模型实际上可以做到这一点。 - Joshua
@DietrichEpp 这是一个奇怪的部分。但是,包括编译器在内的任何人都不需要考虑 UB 的情况,因此假设符合规范的实现,这几乎不能证明 PTRDIFF_MAX 可以小于 SIZE_MAX。 - Lundin
有趣的是,在32位机器上,您可以malloc 3GB,并且char指针之间相隔30亿个对象,因此ptr1-ptr2是未定义的行为。但是,如果您有两个int和int> = 2字节,则int*指针的ptr1-ptr2应始终正确。棘手的问题。 - gnasher729
显示剩余3条评论

2

来自

7.22.3.2 The calloc function

Synopsis
1

 #include <stdlib.h>
 void *calloc(size_t nmemb, size_t size);`

Description
2 The calloc function allocates space for an array of nmemb objects, each of whose size is size. The space is initialized to all bits zero.

Returns
3 The calloc function returns either a null pointer or a pointer to the allocated space.

我不明白为什么分配的空间应该限制在SIZE_MAX字节以内。


6
我的推理是calloc()函数为对象数组分配空间。数组是一个对象,因此必须使用size_t来测量它。 - Toby Speight
1
@TobySpeight在这个答案中所说的“但是这些函数返回的数据指向的内容没有类型。它还不是一个数组。”与“数组是一个对象”的问题有关。 - chux - Reinstate Monica
1
@chux:calloc()的行为是在一个时代确立的,当时存储器是否持有任何特定类型的“对象”,或者是可能适合的每种“对象”的联合体,或者根本没有对象都无所谓。然而,由于语言中没有测量任何创建的对象或对象的手段,因此不需要具备容纳这种测量的类型。 - supercat

2
如果程序超出了实现限制,其行为将是未定义的。这源于实现限制的定义为“由实现对程序施加的限制”(C11中的3.13)。标准还说,严格符合要求的程序必须遵守实现限制(C11中的4p5)。但这也适用于一般程序,因为标准没有说明大多数实现限制超出时会发生什么(因此这是另一种未定义行为,其中标准没有指定会发生什么)。
标准还没有定义可能存在哪些实现限制,因此这有点像“白板”,但我认为最大对象大小实际上与对象分配相关。(顺便说一下,最大对象大小通常比SIZE_MAX小,因为对象内指向char的指针之间的差异必须在ptrdiff_t中可表示。)
这引导我们得出以下观察结果:调用c​​alloc(SIZE_MAX,2)超出了最大对象大小限制,因此实现可以返回任意值而仍符合标准。
某些实现实际上会为类似于c​​alloc(SIZE_MAX / 2 + 2,2)的调用返回非空指针,因为实现没有检查乘法结果是否适合size_t值。在这种情况下,实现限制如此容易检查,而且有一种完全正确的报告错误的方法,因此这是否是一个好主意是另一回事。就个人而言,我认为c​​alloc中缺少溢出检查是实现错误,并且在看到它们时向实现者报告了错误,但从技术上讲,它只是一个实现质量问题。
对于栈上的可变长度数组,超出实现限制的规则导致未定义行为更加明显:
size_t length = SIZE_MAX / 2 + 2;
short object[length];

在这种情况下,实现无法做任何事情,因此必须未定义。


你为什么要引入实现限制呢?在C标准的J.3.12中,我没有看到calloc的任何实现定义限制,除了“当请求的大小为零时,calloc、malloc和realloc函数返回空指针或分配对象的指针(7.22.3)”。 - Werner Henze
正如在DR-266中所述,翻译限制不适用于运行时/已分配的对象。因此不确定翻译限制是否适用于calloc - P.P
1
SIZE_MAX 不一定超过最大对象大小。实现中可以有一个 PTRDIFF_MAX 的值为 2^33(带符号数),同时还有一个 SIZE_MAX 的值为 2^32 (无符号数)。这种类型系统对编译器来说非常不便,但标准并不关心此问题 [甚至没有考虑过这个问题]。 - Lundin
C语言中的calloc()函数用于为对象数组分配空间。calloc(SIZE_MAX, 2)将分配SIZE_MAX个对象,每个对象的大小都小于等于SIZE_MAX。如果规范说明它分配了一个由size个元素组成、每个元素大小为nmemb的数组对象,那么就会引起“超过最大对象大小限制”的担忧。 - chux - Reinstate Monica
1
@Lundin:有趣的是,C11规定ptrdiff_t必须至少为17位,即使在总存储空间不到32K的独立实现中也是如此(托管实现所支持的对象大小已增加到65,535字节,但我不确定这有什么意义 - 无论标准说什么,实际上能够支持那么大对象的实现会这样做,而不能的则不会)。无论如何,在总存储空间不到32K的实现中,我真的不确定17位的ptrdiff_t有什么用处。 - supercat
显示剩余2条评论

2
根据标准文本,也许是因为该标准在这方面有意保持模糊不清。根据6.5.3.4 ¶2:
sizeof运算符返回其操作数的大小(以字节为单位),根据7.19 ¶2:
size_t是sizeof运算符结果的无符号整数类型;如果实现允许任何类型(包括数组类型)的大小不能表示为size_t,则前者通常无法满足。请注意,无论您是否解释有关calloc返回指向“数组”的指针的文本,任何对象都涉及一个数组:其表示形式为unsigned char [sizeof object]的重叠数组。
最好,允许创建大于SIZE_MAX(或其他原因的PTRDIFF_MAX)的任何对象的实现具有严重的QoI(实现质量)问题。除非您特别想确保与特定的损坏的C实现兼容(有时与嵌入式等相关),否则在代码审查中声称您应考虑此类糟糕的实现是错误的。

1

仅作补充:通过一点数学运算,您可以证明 SIZE_MAX * SIZE_MAX = 1(按照 C 规则计算)。

然而,calloc(SIZE_MAX, SIZE_MAX) 只允许执行以下两个操作之一:返回一个指向 SIZE_MAX 个元素,每个元素大小为 SIZE_MAX 字节的数组的指针;或者返回 NULL。它不允许通过简单地将参数相乘来计算总大小,得到结果为 1,并分配一个清零的字节。


0

标准并未说明是否可能创建指针,使得ptr+number1+number2成为有效指针,但number1+number2将超过SIZE_MAX。它当然允许number1+number2超过PTRDIFF_MAX的可能性(尽管由于某种原因,C11决定要求即使是16位地址空间的实现也必须使用32位的ptrdiff_t)。

标准并不强制要求实现提供任何创建指向如此大对象的指针的手段。然而,它定义了一个函数calloc(),其描述表明可以要求它尝试创建这样的对象,并建议如果无法创建该对象,则calloc()应返回空指针。

能够有用地分配任何类型的对象,是实现质量问题。标准从不要求任何特定的分配请求成功,也不会禁止实现返回一个可能无法使用的指针(在某些Linux环境中,malloc()可能会产生指向超额提交的地址空间区域的指针;当可用物理存储不足时尝试使用该指针可能会导致致命陷阱)。对于非反复无常的calloc(x,y)实现来说,如果x和y的数值乘积超过SIZE_MAX,则返回null肯定比返回无法用于访问那么多字节的指针更好。然而,标准没有明确规定返回一个可以用于访问x个y字节对象的指针是否应该被认为比返回null更好或更差。每种行为在某些情况下都有优势,在其他情况下则有劣势。

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