Es el comportamiento “struct hack” técnicamente indefinido?

Lo que estoy preguntando es sobre el bien conocido truco de “último miembro de una estructura tiene longitud variable”. Es algo parecido a esto:

struct T { int len; char s[1]; }; struct T *p = malloc(sizeof(struct T) + 100); p->len = 100; strcpy(p->s, "hello world"); 

Debido a la forma en que la estructura se presenta en la memoria, podemos superponer la estructura sobre un bloque más grande que el necesario y tratar al último miembro como si fuera más grande que el 1 char especificado.

Entonces la pregunta es: ¿esta técnica es un comportamiento técnicamente indefinido? . Esperaría que lo fuera, pero era curioso lo que dice el estándar sobre esto.

PD: Estoy al tanto del enfoque C99 para esto, me gustaría que las respuestas se ajusten específicamente a la versión del truco que se menciona arriba.

Como dice la C Preguntas Frecuentes :

No está claro si es legal o portátil, pero es bastante popular.

y:

… una interpretación oficial ha considerado que no se ajusta estrictamente al Estándar C, aunque parece funcionar en todas las implementaciones conocidas. (Los comstackdores que verifican cuidadosamente los límites de la matriz pueden emitir advertencias).

El razonamiento detrás del bit “estrictamente conforme” está en la especificación, sección J.2 Comportamiento indefinido , que incluye en la lista de comportamiento indefinido:

  • Un subíndice de matriz está fuera de rango, incluso si un objeto es aparentemente accesible con el subíndice dado (como en la expresión lvalue a[1][7] dada la statement int a[4][5] ) (6.5.6).

El párrafo 8 de la Sección 6.5.6. Operadores aditivos tiene otra mención de que el acceso más allá de los límites definidos de la matriz no está definido:

Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto del arreglo, o uno más allá del último elemento del objeto del arreglo, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido.

Creo que técnicamente es un comportamiento indefinido. El estándar (discutiblemente) no lo aborda directamente, por lo que cae bajo “o por la omisión de cualquier definición explícita de comportamiento”. cláusula (§4 / 2 de C99, §3.16 / 2 de C89) que dice que es un comportamiento indefinido.

Lo “discutible” anterior depende de la definición del operador de suscripción de matriz. Específicamente, dice: “Una expresión de postfijo seguida de una expresión entre corchetes [] es una designación con subíndices de un objeto de matriz”. (C89, §6.3.2.1 / 2).

Puede argumentar que el “de un objeto de matriz” se está violando aquí (ya que está suscribiendo fuera del rango definido del objeto de matriz), en cuyo caso el comportamiento es (un poco más) explícitamente indefinido, en lugar de solo indefinido cortesía de nada que lo defina.

En teoría, puedo imaginarme que un comstackdor que hace la comprobación de límites de matriz y (por ejemplo) abortará el progtwig cuando / si intenta utilizar un subíndice de fuera de rango. De hecho, no sé si existe tal cosa, y dada la popularidad de este estilo de código, incluso si un comstackdor intentó forzar subíndices en algunas circunstancias, es difícil imaginar que alguien aguantaría haciéndolo en esta situación.

Esa forma particular de hacerlo no está explícitamente definida en ningún estándar C, pero C99 sí incluye el “struct hack” como parte del lenguaje. En C99, el último miembro de una estructura puede ser un “miembro de matriz flexible”, declarado como char foo[] (con el tipo que desee en lugar de char ).

Sí, es un comportamiento indefinido.

El Informe de defectos del lenguaje C # 051 da una respuesta definitiva a esta pregunta:

La expresión idiomática, aunque es común, no se ajusta estrictamente

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

En el documento de C99 Rationale, el Comité C agrega:

La validez de esta construcción siempre ha sido cuestionable. En la respuesta a un Informe de Defectos, el Comité decidió que era un comportamiento indefinido porque el conjunto de elementos p-> contiene solo un elemento, independientemente de si el espacio existe.

No es un comportamiento indefinido , independientemente de lo que digan los demás , oficiales o no , porque está definido por el estándar. p->s , excepto cuando se usa como un valor l, se evalúa como un puntero idéntico a (char *)p + offsetof(struct T, s) . En particular, este es un puntero de char válido dentro del objeto malloc’d, y hay 100 (o más, dependiente de las consideraciones de alineación) direcciones sucesivas inmediatamente después que también son válidas como objetos char dentro del objeto asignado. El hecho de que el puntero se obtuvo usando -> lugar de agregar explícitamente el desplazamiento al puntero devuelto por malloc , convertir a char * , es irrelevante.

Técnicamente, p->s[0] es el único elemento de la matriz de caracteres dentro de la estructura, los próximos elementos (por ejemplo, p->s[1] a p->s[3] ) probablemente rellenen bytes dentro de la estructura , que podría corromperse si realiza una asignación a la estructura como un todo, pero no si solo accede a miembros individuales, y el rest de los elementos son espacio adicional en el objeto asignado que puede usar libremente como quiera, siempre que lo desee. obedeces los requisitos de alineación (y char no tiene requisitos de alineación).

Si te preocupa que la posibilidad de superposición con bytes de relleno en la estructura pueda invocar demonios nasales de alguna manera, puedes evitar esto reemplazando el 1 en [1] con un valor que asegure que no haya relleno al final de la estructura. Una manera simple pero inútil de hacer esto sería hacer una estructura con miembros idénticos, excepto que no hay una matriz al final, y usar s[sizeof struct that_other_struct]; para la matriz. Entonces, p->s[i] se define claramente como un elemento de la matriz en la estructura para i y como un objeto char en una dirección que sigue al final de la estructura para i>=sizeof struct that_other_struct .

Editar: en realidad, en el truco anterior para obtener el tamaño correcto, es posible que también deba colocar una unión que contenga cada tipo simple antes de la matriz, para asegurarse de que la matriz comience con la alineación máxima en lugar de en medio del relleno de algún otro elemento . De nuevo, no creo que nada de esto sea necesario, pero lo estoy ofreciendo para el más paranoico de los abogados de idiomas que hay.

Edición 2: La superposición con los bytes de relleno definitivamente no es un problema, debido a otra parte del estándar. C requiere que si dos estructuras coinciden en una subsecuencia inicial de sus elementos, se puede acceder a los elementos iniciales comunes mediante un puntero a cualquier tipo. Como consecuencia, si se declarase una estructura idéntica a la struct T pero con una matriz final más grande, el elemento s[0] debería coincidir con el elemento s[0] en la struct T , y la presencia de estos elementos adicionales no podría afecta o se ve afectado al acceder a elementos comunes de la estructura más grande usando un puntero a struct T

Sí, es un comportamiento técnicamente indefinido.

Tenga en cuenta que hay al menos tres formas de implementar el “struct hack”:

(1) Declarar la matriz final con el tamaño 0 (la forma más “popular” en el código heredado). Esto es obviamente UB, ya que las declaraciones de matriz de tamaño cero siempre son ilegales en C. Incluso si se comstack, el lenguaje no garantiza el comportamiento de ningún código que viole la restricción.

(2) Declarar la matriz con tamaño legal mínimo – 1 (su caso). En este caso, cualquier bash de tomar puntero a p->s[0] y usarlo para la aritmética del puntero que va más allá de p->s[1] es un comportamiento indefinido. Por ejemplo, se permite que una implementación de depuración produzca un puntero especial con información de rango incrustado, que se atrapará cada vez que intente crear un puntero más allá de p->s[1] .

(3) Declarar la matriz con un tamaño “muy grande” como 10000, por ejemplo. La idea es que el tamaño declarado se supone que es más grande que cualquier cosa que puedas necesitar en la práctica real. Este método está libre de UB con respecto al rango de acceso de la matriz. Sin embargo, en la práctica, por supuesto, siempre asignaremos una menor cantidad de memoria (solo la cantidad que realmente se necesita). No estoy seguro de la legalidad de esto, es decir, me pregunto qué tan legal es asignar menos memoria para el objeto que el tamaño declarado del objeto (suponiendo que nunca accedamos a los miembros “no asignados”).

El estándar es bastante claro que no puede acceder a las cosas al final de una matriz. (y ir a través de punteros no ayuda, ya que no está permitido incluso incrementar punteros más allá de uno después del final de la matriz).

Y para “trabajar en la práctica”. He visto el optimizador de gcc / g ++ usando esta parte del estándar generando código incorrecto al cumplir con esta C inválida.

Si un comstackdor acepta algo así como

 typedef struct {
   int len;
   char dat [];
 }; 

Creo que está bastante claro que debe estar listo para aceptar un subíndice sobre ‘dat’ más allá de su longitud. Por otro lado, si alguien codifica algo como:

 typedef struct {
   int lo que sea;
   char dat [1];
 } MY_STRUCT; 

y luego accede a somestruct-> dat [x]; No creo que el comstackdor tenga ninguna obligación de usar código de cálculo de direcciones que funcione con valores grandes de x. Creo que si uno quisiera estar realmente seguro, el paradigma correcto sería más como:

 #define LARGEST_DAT_SIZE 0xF000
 typedef struct {
   int lo que sea;
   char dat [LARGEST_DAT_SIZE];
 } MY_STRUCT; 

y luego haga un malloc de (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + desired_array_length) bytes (teniendo en cuenta que si desired_array_length es más grande que LARGEST_DAT_SIZE, los resultados pueden estar indefinidos).

Por cierto, creo que la decisión de prohibir matrices de longitud cero fue desafortunada (algunos dialectos antiguos como Turbo C lo respaldan) ya que una matriz de longitud cero podría considerarse como una señal de que el comstackdor debe generar código que funcione con índices más grandes. .