c language-lawyer undefined-behavior c11 strict-aliasing

¿Qué precauciones debo tomar para crear un conjunto de memoria que no invoque un comportamiento indefinido?



language-lawyer undefined-behavior (5)

Al conocer el tamaño de la union de los 3 tipos, puede darse una asignación más eficiente.

union common { struct foo f; void * ptr; char ch; }; void *allocate3(struct foo **f, size_t m, void **ptr, size_t n, char **ch, size_t o) { size_t u_sz = sizeof (union common); size_t f_sz = sizeof *f * m; size_t f_cnt = (f_sz + u_sz - 1)/u_sz; size_t p_sz = sizeof *ptr * n; size_t p_cnt = (p_sz + u_sz - 1)/u_sz; size_t c_sz = sizeof *ch * o; size_t c_cnt = (c_sz + u_sz - 1)/u_sz; size_t sum = f_cnt + p_cnt + c_cnt; union common *u = malloc(sum * u_sz); if (u) { *f = &u[0].f; *ptr = &u[f_cnt].ptr; *ch = &u[f_cnt + c_cnt].ch; } return u; }

De esta manera, cada uno de los 3 arreglos comienza en un límite de union , por lo que se cumplen los problemas de alineación. Al ajustar el espacio de cada matriz para que sea un múltiplo del tamaño de la union , menos espacio desperdiciado que la primera respuesta cumple con los objetivos publicados de OP.

Un poco de desperdicio es struct foo es grande, pero o es pequeño. Podría utilizar el siguiente como una mejora adicional. No hay necesidad de rellenar después de la última matriz.

malloc((f_cnt + p_cnt) * u_sz + c_cz);

Pensamiento adicional en exprimir la asignación. Cada "elemento de cuenta de unión" posterior puede usar una unión diferente que omita los tipos anteriores y así sucesivamente. Al llegar al final, esa es la esencia de la idea anterior, la última matriz solo necesita depender del último tipo. Esto hace que el código sea más complicado (propenso a errores), pero aumenta la eficiencia de espacio sin problemas de alimentación, etc. A continuación se presentan algunas ideas de codificación

union common_last2 { // struct foo f; void * ptr; char ch; }; size_t u2_sz = sizeof (union common_last2); size_t p_cnt = (p_sz + u2_sz - 1)/u2_sz; ... malloc(f_cnt*usz + p_cnt*u2_sz + c_cz); *ch = tbd;

Mi problema inicial es que tengo, en un proyecto, varios objetos que comparten toda una vida (es decir, una vez que libero uno de ellos, los libero a todos), luego quise asignar un solo bloque de memoria. Tengo matrices de tres tipos de objetos diferentes, struct foo , void * y char . Al principio quise malloc() un bloque como este:

// +---------------+---------+-----------+---------+---------+ // | struct foo[n] | padding | void *[m] | padding | char[o] | // +---------------+---------+-----------+---------+---------+

Pero entonces ... ¿cómo podría lograr esto sin invocar un comportamiento indefinido? Es decir, respetando las reglas de aliasing de tipo, aligment ... ¿Cómo calcular correctamente el tamaño del bloque de memoria, declarar el bloque de memoria (con su tipo efectivo) y cómo obtener los punteros a las tres secciones dentro de forma portátil?

(Entiendo que podría malloc() 3 bloques, lo que daría como resultado tres free() , pero me gustaría saber cómo hacerlo con un solo bloque mientras todavía se comporte bien.)

Me gustaría ampliar mi problema a una pregunta más general: ¿qué precauciones se deben tomar para implementar un conjunto de memoria para objetos con tamaños y alineación arbitrarios mientras se mantiene el programa en buen estado? ( Suponiendo que es posible implementarlo sin invocar un comportamiento indefinido ).


Como se dijo en otra respuesta, no puedes reimplementar malloc dentro de C en sí. La razón es que no puede generar objetos que no tengan un tipo efectivo sin malloc .

Pero para su aplicación no necesita esto, puede usar malloc o similar, vea a continuación, para tener un gran bloque de memoria sin problemas.

Si tiene un bloque tan grande, debe saber cómo colocar los objetos dentro de este bloque. El principal problema aquí es la alineación, debe colocar todos sus objetos en los límites que correspondan a sus requisitos mínimos de alineación.

Desde C11, la alineación de tipos se puede obtener con el operador _Alignof , y la memoria desalineada se puede solicitar con aligned_alloc .

Poner todo esto junto lo que dice:

  • calcula el mcm de todas las alineaciones de tus tipos
  • con aligned_alloc solicite suficiente memoria que esté alineada en ese valor
  • Coloca todos tus objetos en múltiplos de esa alineación.

El alias entonces no es un problema si está comenzando con un objeto sin tipo que recibe a través de un puntero void* . Cada parte de ese objeto grande tiene el tipo efectivo con el que has escrito, mira mi entrada de blog reciente.

La parte relevante del estándar C es 6.5 p6:

El tipo efectivo de un objeto para un acceso a su valor almacenado es el tipo declarado del objeto, en su caso.87) Si un valor se almacena en un objeto que no tiene un tipo declarado a través de un lvalue que tiene un tipo que no es un tipo de carácter , entonces el tipo del valor l se convierte en el tipo efectivo del objeto para ese acceso y para los accesos posteriores que no modifican el valor almacenado. Si un valor se copia en un objeto que no tiene un tipo declarado usando memcpy o memmove, o se copia como una matriz de tipo de carácter, entonces el tipo efectivo del objeto modificado para ese acceso y para los accesos posteriores que no modifiquen el valor es el tipo efectivo del objeto desde el cual se copia el valor, si tiene uno. Para todos los demás accesos a un objeto que no tiene un tipo declarado, el tipo efectivo del objeto es simplemente el tipo del valor de l utilizado para el acceso.

Aquí el "objeto sin tipo declarado" es un objeto (o subobjeto) asignado por malloc o similar. Dice claramente que dichos objetos se pueden escribir con cualquier tipo en cualquier momento y que esto cambia el tipo efectivo al deseado.


En primer lugar, asegúrese de usar -fno-strict-aliasing o lo que sea el equivalente en su compilador. De lo contrario, incluso si todas las alineaciones están satisfechas, un compilador puede usar reglas de alias para superponer diferentes usos del mismo bloque de memoria.

Dudo que esto sea coherente con la intención de los autores del Estándar, pero dado que los optimizadores pueden ser tan agresivos que la única forma de implementar grupos de memoria de tipo seguro es deshabilitar el análisis de aliasing basado en el tipo. Los autores de la Norma querían evitar la marca como no compatible con algunos compiladores que utilizaban alias basados ​​en tipos. Además, pensaron que podían aplazar el juicio de los escritores de compiladores sobre cómo reconocer y manejar los casos en los que era probable el aliasing. Identificaron los casos en los que los escritores de compiladores podrían pensar que no era necesario reconocer el alias (por ejemplo, entre los tipos firmados y no firmados), pero que los escritores de compiladores esperaban ejercer un juicio razonable. No veo evidencia de que pretendan que su lista de casos permitidos se considere exhaustiva incluso en plataformas en las que serían útiles otras formas de creación de alias.

Además, no importa con qué cuidado se cumpla el Estándar, no hay garantía de que los compiladores apliquen "optimizaciones" de última hora de todos modos. Al menos a partir de gcc 6.2 hay errores de creación de alias que romperán el código que usa el almacenamiento como tipo X, lo escribe como Y, lo lee como Y, escribe el mismo valor que X y lee el almacenamiento como X - comportamiento que es 100 % definido bajo el Estándar.

Si se cuida el aliasing (por ejemplo, utilizando el indicador indicado), y conoce el requisito de alineación en el peor de los casos para su sistema, definir el almacenamiento para el grupo es fácil:

union { char [POOL_BLOCK_SIZE] dat; TYPE_WITH_WORST_ALIGNMENT align; } memory_pool[POOL_BLOCK_COUNT];

Desafortunadamente, el estándar no proporciona ninguna manera de evitar problemas de alias basados ​​en tipos, incluso si se tienen en cuenta todos los problemas de alineación que dependen de la plataforma.


Para responder a una de las preguntas de OP

¿Cómo podría lograr esto (quería malloc () un bloque como este) sin invocar un comportamiento indefinido?

Un enfoque ineficiente del espacio. Asignar una union de los tipos. Razonable si el tamaño necesario de los tipos más pequeños no es demasiado grande.

union common { struct foo f; void * ptr; char ch; }; void *allocate3(struct foo **f, size_t m, void **ptr, size_t n, char **ch, size_t o) { size_t sum = m + n + o; union common *u = malloc(sizeof *u * sum); if (u) { *f = &u[0].f; *ptr = &u[m].ptr; *ch = &u[m + n].ch; } return u; } void sample() { struct foo *f; void *ptr; char *ch; size_t m, n, o; void *base = allocate3(&f, m, &ptr, n, &ch, o); if (base) { // use data } free(base); }


Por mucho que lo intentes, no es posible implementar malloc en C pura.

Siempre terminas violando el aliasing estricto en algún momento. Para evitar dudas, el uso de un búfer de caracteres que no tenga una duración de almacenamiento dinámico también violará las reglas estrictas de aliasing. También debería asegurarse de que cualquier puntero devuelto tenga una alineación adecuada.

Si está feliz de atarse a una plataforma en particular, entonces también puede recurrir a esa implementación particular de malloc busca de inspiración.

Pero, ¿por qué no considerar escribir una función de código auxiliar que llame a malloc y también construya una tabla de otros objetos asignados? Incluso se podría implementar algún tipo de marco de observador / notificación. Otro punto de partida podría ser los recolectores de basura conocidos que se han escrito en C.