c language-lawyer

¿Puede calloc() asignar más de SIZE_MAX en total?



language-lawyer (7)

En una reciente revisión del código , se afirmó que

En los sistemas seleccionados, calloc() puede asignar más de SIZE_MAX bytes totales, mientras que malloc() está limitado.

Mi afirmación es que está equivocado, porque calloc() crea espacio para una matriz de objetos, que, al ser una matriz, es en sí misma un objeto. Y ningún objeto puede ser más grande en tamaño que SIZE_MAX .

Entonces, ¿cuál de nosotros es correcto? En un sistema (posiblemente hipotético) con un espacio de direcciones mayor que el rango de size_t , ¿se permite que calloc() tenga éxito cuando se le llama con argumentos cuyo producto es mayor que SIZE_MAX ?

Para hacerlo más concreto: ¿saldrá el siguiente programa con un estado distinto de cero?

#include <stdint.h> #include <stdlib.h> int main() { return calloc(SIZE_MAX, 2) != NULL; }


¿Puede calloc () asignar más de SIZE_MAX en total?

Como la afirmación "En los sistemas seleccionados, calloc() puede asignar más de SIZE_MAX bytes totales, mientras que malloc() está limitado". A partir de un comment que publiqué, explicaré mi razonamiento.

tamaño_t

size_t es un tipo sin signo de al menos 16 bits.

size_t que es el tipo entero sin signo del resultado del operador sizeof ; C11dr §7.19 2

"Su valor definido por la implementación será igual o mayor en magnitud ... que el valor correspondiente dado a continuación" ... límite de SIZE_MAX ... 65535 §7.20.3 2

tamaño de

El operador sizeof produce el tamaño (en bytes) de su operando, que puede ser una expresión o el nombre entre paréntesis de un tipo. §6.5.3.4 2

calloc

void *calloc(size_t nmemb, size_t size);

La función calloc asigna espacio para una matriz de objetos nmemb , cada uno de cuyo size es el tamaño. §7.22.3.2 2

Considere una situación en la que el nmemb * size SIZE_MAX nmemb * size excede de SIZE_MAX .

size_t alot = SIZE_MAX/2; double *p = calloc(alot, sizeof *p); // assume `double` is 8 bytes.

Si calloc() realmente asigna bytes de nmemb * size y si p != NULL es verdadero, ¿qué especificación viola?

El tamaño de cada elemento, (cada objeto) es representable.

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

Se puede acceder a cada elemento.

// 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]); }

detalles de calloc()

"espacio para una matriz de objetos nmemb ": este es ciertamente un punto clave de contención. ¿El "asigna espacio para la matriz" requiere <= SIZE_MAX ? No encontré nada en la especificación de C que requiera este límite y, por lo tanto, concluyo:

calloc() puede asignar más de SIZE_MAX en total.

Es ciertamente poco común que calloc() con argumentos grandes devuelva no compatible con NULL , o no. Por lo general, tales asignaciones exceden la memoria disponible, por lo que el problema es discutible. El único caso que encontré fue con el modelo de memoria enorme, donde size_t era de 16 bits y el puntero del objeto era de 32 bits.


Desde

7.22.3.2 La función calloc

Sinopsis
1

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

Descripción
2 La función calloc asigna espacio para una matriz de objetos nmemb, cada uno de cuyo tamaño es el tamaño. El espacio se inicializa a todos los bits cero.

Devoluciones
3 La función calloc devuelve un puntero nulo o un puntero al espacio asignado.

No veo por qué el espacio asignado debería limitarse a SIZE_MAX bytes.


El Estándar no dice nada acerca de si es posible crear un puntero de alguna manera, de manera que ptr+number1+number2 podría ser un puntero válido, pero number1+number2 excedería SIZE_MAX . Sin duda, permite la posibilidad de que el número number1+number2 supere a PTRDIFF_MAX (aunque por alguna razón, C11 ha decidido requerir que incluso las implementaciones con un espacio de direcciones de 16 bits deban usar un ptrdiff_t 32 bits).

El Estándar no exige que las implementaciones proporcionen ningún medio para crear punteros a objetos tan grandes. Sin embargo, sí define una función, calloc() , cuya descripción sugiere que podría pedirse que intente crear un objeto de este tipo, y sugeriría que calloc() debería devolver un puntero nulo si no puede crear el objeto.

Sin embargo, la capacidad de asignar cualquier tipo de objeto de manera útil es un problema de calidad de implementación. El Estándar nunca exigiría que una solicitud de asignación en particular tenga éxito, ni prohibiría que una implementación devuelva un puntero que pueda resultar inutilizable (en algunos entornos Linux, un malloc () podría generar un puntero a una región comprometida en exceso). espacio de direcciones; un intento de usar el puntero cuando no hay suficiente almacenamiento físico disponible podría causar una trampa fatal). Sin duda, sería mejor que una implementación no caprichosa de calloc(x,y) devuelva nulo si el producto numérico de y excede SIZE_MAX que para que produzca un puntero que no pueda usarse para acceder a ese número de bytes. . Sin embargo, el Estándar es silencioso, ya sea que la devolución de un puntero que se puede usar para acceder a los objetos y de x bytes debe considerarse mejor o peor que la devolución de valores null . Cada comportamiento sería ventajoso en algunas situaciones, y desventajoso en otras.


Según el texto de la norma, tal vez, porque la norma es (algunos dirían intencionalmente) vaga acerca de este tipo de cosas.

Por 6.5.3.4 ¶2:

El operador sizeof produce el tamaño (en bytes) de su operando

y por 7.19 ¶2:

tamaño_t

que es el tipo entero sin signo del resultado del operador sizeof ;

Lo primero no se puede satisfacer en general si la implementación admite algún tipo (incluidos los tipos de arreglos) cuyo tamaño no se pueda representar en size_t . Tenga en cuenta que, independientemente de si interpreta el texto sobre el puntero devuelto por el calloc apunta a "una matriz", siempre hay una matriz involucrada con cualquier objeto: la matriz superpuesta de tipo unsigned char[sizeof object] que es su representación .

En el mejor de los casos, una implementación que permita la creación de cualquier objeto más grande que SIZE_MAX (o PTRDIFF_MAX , por otras razones) tiene problemas fatalmente malos de QoI (calidad de implementación). El reclamo en la revisión de código que debe tener en cuenta para tales implementaciones incorrectas es falso, a menos que esté tratando específicamente de asegurar la compatibilidad con una implementación C en particular (a veces relevante para incrustado, etc.).


Si un programa excede los límites de implementación, el comportamiento no está definido. Esto se desprende de la definición de un límite de implementación como una restricción impuesta a los programas por la implementación (3.13 en C11). El estándar también dice que los programas estrictamente conformes deben cumplir con los límites de implementación (4p5 en C11). Pero esto también implica a los programas en general porque el estándar no dice qué sucede cuando se exceden la mayoría de los límites de implementación (por lo que es el otro tipo de comportamiento indefinido, donde el estándar no especifica qué sucede).

El estándar tampoco define qué límites de implementación pueden existir, por lo que esto es un poco de carta blanca , pero creo que es razonable que el tamaño máximo del objeto sea realmente relevante para las asignaciones de objetos. (El tamaño máximo del objeto suele ser más pequeño que SIZE_MAX , por cierto, porque la diferencia de punteros a caracteres dentro del objeto debe ser representable en ptrdiff_t ).

Esto nos lleva a la siguiente observación: una llamada a calloc (SIZE_MAX, 2) excede el límite de tamaño máximo de objeto, por lo que una implementación podría devolver un valor arbitrario mientras sigue cumpliendo con el estándar.

Algunas implementaciones devolverán un puntero que no es nulo para una llamada como calloc (SIZE_MAX / 2 + 2, 2) porque la implementación no comprueba que el resultado de la multiplicación no se ajuste a un valor de size_t . Si esta es una buena idea, es un asunto diferente, dado que el límite de implementación puede verificarse tan fácilmente en este caso, y hay una manera perfectamente correcta de informar errores. Personalmente, considero que la falta de control de desbordamiento en calloc un error de implementación, y he reportado errores a los implementadores cuando los vi, pero técnicamente, es simplemente un problema de calidad de implementación.

Para las matrices de longitud variable en la pila, la regla sobre exceder los límites de implementación que resultan en un comportamiento indefinido es más obvia:

size_t length = SIZE_MAX / 2 + 2; short object[length];

Realmente no hay nada que una implementación pueda hacer aquí, por lo que tiene que estar indefinida.


Solo una adición: con un poco de matemáticas puede mostrar que SIZE_MAX * SIZE_MAX = 1 (cuando se evalúa de acuerdo con las reglas de C).

Sin embargo, a calloc (SIZE_MAX, SIZE_MAX) solo se le permite hacer una de dos cosas: devolver un puntero a una matriz de elementos SIZE_MAX de bytes SIZE_MAX, O devolver NULL. NO se permite calcular el tamaño total simplemente multiplicando los argumentos, obteniendo un resultado de 1 y asignando un byte, borrado a 0.


SIZE_MAX no especifica necesariamente el tamaño máximo de un objeto, sino el valor máximo de size_t , que no es necesariamente lo mismo. Consulte ¿Por qué es el tamaño máximo de una matriz "demasiado grande"? ,

Pero, obviamente, no está bien definido pasar un valor mayor que SIZE_MAX a una función que espera un parámetro size_t . Entonces, en teoría, SIZE_MAX es el límite, y en teoría calloc permitiría SIZE_MAX * SIZE_MAX bytes asignados.

La cosa con malloc / calloc es que asignan objetos sin un tipo. Los objetos con un tipo tienen restricciones, como nunca ser más grande que un cierto límite como SIZE_MAX . Pero los datos apuntados por el resultado de estas funciones no tienen un tipo. No es (todavía) una matriz.

Formalmente, los datos no tienen un tipo declarado , pero a medida que almacena algo dentro de los datos asignados, obtiene el tipo efectivo de acceso a los datos utilizados para el almacenamiento (C17 6.5 §6).

Esto, a su vez, significa que sería posible que calloc asigne más memoria de la que puede contener cualquier tipo en C, porque lo que está asignado no tiene (todavía) un tipo.

Por lo tanto, en lo que respecta al estándar C, es perfectamente calloc(SIZE_MAX, 2) devuelva un valor diferente de NULL. Otra forma es cómo usar esa memoria asignada de una manera sensata, o qué sistemas que incluso soportan grandes trozos de memoria en el montón.