Lo que estoy preguntando es el conocido truco "el ú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 de lo necesario y tratar el último miembro como si fuera más grande que el 1 char
especificado.
Entonces la pregunta es: ¿Es esta técnica un comportamiento técnicamente indefinido? . Esperaría que lo sea, pero tenía curiosidad por saber qué dice el estándar sobre esto.
PD: Soy consciente del enfoque C99 para esto, me gustaría que las respuestas se ajustaran específicamente a la versión del truco que se enumera anteriormente.
c
undefined-behavior
c89
Evan Terán
fuente
fuente
Respuestas:
Como dice C FAQ :
y:
La razón 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:
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:
fuente
p->s
nunca se usa como una matriz. Se pasa astrcpy
, en cuyo caso decae a un planochar *
, que pasa a apuntar a un objeto que puede ser interpretado legalmente comochar [100];
dentro del objeto asignado.malloc
, cuando simplemente ha convertido el archivo devueltovoid *
a un puntero a [una estructura que contiene] una matriz. Sigue siendo válido acceder a cualquier parte del objeto asignado utilizando un puntero achar
(o preferiblementeunsigned char
).malloc
. Busque "objeto" en el estándar antes de lanzar bs.Creo que técnicamente es un comportamiento indefinido. El estándar (posiblemente) no lo aborda directamente, por lo que se incluye "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.
El "posiblemente" anterior depende de la definición del operador de subíndice de matriz. Específicamente, dice: "Una expresión de sufijo seguida de una expresión entre corchetes [] es una designación subindicada de un objeto de matriz". (C89, §6.3.2.1 / 2).
Puede argumentar que el "de un objeto de matriz" está siendo violado aquí (ya que está subíndice fuera del rango definido del objeto de matriz), en cuyo caso el comportamiento es (un poquito más) explícitamente indefinido, en lugar de simplemente indefinido cortesía de nada que lo defina.
En teoría, puedo imaginar un compilador que verifica los límites de la matriz y (por ejemplo) abortaría el programa cuando / si intentara usar un subíndice fuera de rango. De hecho, no sé que exista algo así, y dada la popularidad de este estilo de código, incluso si un compilador intentara hacer cumplir los subíndices en algunas circunstancias, es difícil imaginar que alguien lo toleraría en esta situación.
fuente
arr[x] = y;
podría reescribirse comoarr[0] = y;
; para una matriz de tamaño 2,arr[i] = 4;
podría reescribirse comoi ? arr[1] = 4 : arr[0] = 4;
Si bien nunca he visto a un compilador realizar tales optimizaciones, en algunos sistemas embebidos podrían ser muy productivos. En un PIC18x, utilizando tipos de datos de 8 bits, el código para la primera declaración sería de dieciséis bytes, la segunda, dos o cuatro, y la tercera, ocho o doce. No es una mala optimización si es legal.a[2] == a + 2
), no lo hace. Si estoy en lo cierto, todos los estándares de C definen el acceso a la matriz como aritmática de puntero.Sí, es un comportamiento indefinido.
C Language Defect Report # 051 da una respuesta definitiva a esta pregunta:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
En el documento C99 Justificación, el Comité C agrega:
fuente
malloc
) es válido en la suma, entonces, ¿cómo puede el puntero idéntico, obtenido a través de otra ruta, ¿será inválido en la adición? Incluso si quieren reclamar que es UB, eso no tiene mucho sentido, porque computacionalmente no hay forma de que una implementación distinga entre el uso bien definido y el uso supuestamente indefinido.*foo
contiene un matriz de un solo elementoboz
, la expresión sefoo->boz[biz()*391]=9;
podría simplificar comobiz(),foo->boz[0]=9;
). Desafortunadamente, el rechazo de las matrices de elementos cero de los compiladores significa que una gran cantidad de código usa matrices de un solo elemento en su lugar, y esa optimización lo rompería.Esa forma particular de hacerlo no está definida explícitamente en ningún estándar de C, pero C99 incluye el "truco de estructura" 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 dechar
).fuente
No es un comportamiento indefinido , independientemente de lo que alguien, oficial o no , diga, porque está definido por el estándar.
p->s
, excepto cuando se usa como un lvalue, se evalúa como un puntero idéntico a(char *)p + offsetof(struct T, s)
. En particular, este es unchar
puntero válido dentro del objeto malloc'd, y hay 100 (o más, dependiendo de las consideraciones de alineación) direcciones sucesivas inmediatamente después de él que también son válidas comochar
objetos dentro del objeto asignado. El hecho de que el puntero se haya derivado usando en->
lugar de agregar explícitamente el desplazamiento al puntero devuelto pormalloc
, cast tochar *
, es irrelevante.Técnicamente, si
p->s[0]
el elemento único de lachar
matriz está dentro de la estructura, los siguientes elementos (por ejemplo, ap->s[1]
travésp->s[3]
) probablemente sean bytes de relleno dentro de la estructura, que podrían corromperse si realiza una asignación a la estructura como un todo, pero no si simplemente accede a un individuo miembros, y el resto de los elementos son espacio adicional en el objeto asignado que puede usar libremente como quiera, siempre que obedezca los requisitos de alineación (ychar
no tenga requisitos de alineación).Si le preocupa que la posibilidad de superposición con el acolchado de bytes de la estructura puede ser que de alguna forma de invocación demonios nasales, se podría evitar esta reemplazando la
1
de[1]
con un valor que asegura que no hay relleno al final de la estructura. Una forma simple pero inútil de hacer esto sería crear una estructura con miembros idénticos, excepto sin una matriz al final, y usarlas[sizeof struct that_other_struct];
para la matriz. Entonces,p->s[i]
se define claramente como un elemento de la matriz en la estructura parai<sizeof struct that_other_struct
y como un objeto char en una dirección que sigue al final de la estructura parai>=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 en sí comience con una alineación máxima en lugar de en el medio del relleno de algún otro elemento . Una vez más, no creo que nada de esto sea necesario, pero se lo ofrezco al más paranoico de los abogados del lenguaje.
Edición 2: La superposición con 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
struct T
se declarara una estructura idéntica a, pero con una matriz final más grande, el elementos[0]
tendría que coincidir con el elementos[0]
enstruct T
, y la presencia de estos elementos adicionales no podría afectar o verse afectada al acceder a elementos comunes de la estructura más grande. usando un puntero astruct T
.fuente
malloc
cual se accede como una matriz o si es una estructura más grande a la que se accede a través de un puntero a una estructura más pequeña cuyos elementos son un subconjunto inicial de los elementos de la estructura más grande, entre otros casos.malloc
no asigna un rango de memoria al que se puede acceder con aritmética de punteros, ¿de qué uso sería? Y sip->s[1]
está definido por la norma como el azúcar sintáctica para la aritmética de punteros, entonces esta respuesta simplemente reafirma quemalloc
es útil. ¿Qué queda por discutir? :)1
. Es tan simple como eso.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
suponer queif
se ingresa la rama. Esto es UB (y no se garantiza que se inicialicen
) según 6.5.6 p8 (última oración), como lo leí. Relacionado: 6.5.9 p6 con nota al pie 109. (Las referencias son a C11 n1570.) [...]Sí, es un comportamiento técnicamente indefinido.
Tenga en cuenta que hay al menos tres formas de implementar el "truco de estructura":
(1) Declarar la matriz final con tamaño 0 (la forma más "popular" en el código heredado). Obviamente, esto es UB, ya que las declaraciones de matriz de tamaño cero siempre son ilegales en C. Incluso si se compila, el lenguaje no ofrece garantías sobre el comportamiento de cualquier código que viole las restricciones.
(2) Declarar la matriz con un tamaño legal mínimo: 1 (su caso). En este caso, cualquier intento de tomar puntero ay
p->s[0]
usarlo para aritmética de puntero que vaya más alláp->s[1]
es un comportamiento indefinido. Por ejemplo, se permite una implementación de depuración para producir un puntero especial con información de rango incrustada, que atrapará cada vez que intente crear un puntero más alláp->s[1]
.(3) Declarar la matriz con un tamaño "muy grande" como 10000, por ejemplo. La idea es que se supone que el tamaño declarado es más grande que cualquier cosa que pueda necesitar en la práctica. Este método está libre de UB con respecto al rango de acceso a la matriz. Sin embargo, en la práctica, por supuesto, siempre asignaremos una menor cantidad de memoria (solo la realmente necesaria). 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 (asumiendo que nunca accedemos a los miembros "no asignados").
fuente
s[1]
no hay comportamiento indefinido. Es lo mismo que*(s+1)
, que es lo mismo que*((char *)p + offsetof(struct T, s) + 1)
, que es un puntero válido a achar
en el objeto asignado.foo[]
azúcar sintáctico*foo
), entonces cualquier acceso más allá del menor de su tamaño declarado y su tamaño asignado es UB, independientemente de cómo se haya realizado la aritmética del puntero.foo[]
en una estructura no es azúcar sintáctico para*foo
; es un miembro de matriz flexible C99. Para el resto, vea mi respuesta y comentarios sobre otras respuestas.unsigned char [sizeof object]
matriz superpuesta imaginaria . Mantengo mi afirmación de que el miembro de matriz flexible "hack" para pre-C99 tiene un comportamiento bien definido.El estándar es bastante claro que no puede acceder a cosas al lado del final de una matriz. (y pasar por punteros no ayuda, ya que no se le permite incluso incrementar los punteros más allá de uno después del final de la matriz).
Y por "trabajar en la práctica". He visto el optimizador gcc / g ++ usando esta parte del estándar generando así un código incorrecto al cumplir con esta C.
fuente
Si un compilador acepta algo como
Creo que está bastante claro que debe estar listo para aceptar un subíndice en 'dat' más allá de su longitud. Por otro lado, si alguien codifica algo como:
y luego accede a alguna estructura-> dat [x]; No creo que el compilador tenga la obligación de utilizar un código de cálculo de direcciones que funcione con valores grandes de x. Creo que si uno quisiera estar realmente seguro, el paradigma adecuado sería más como:
y luego hacer un malloc de (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + deseada_array_length) bytes (teniendo en cuenta que si deseada_array_length es mayor que LARGEST_DAT_SIZE, los resultados pueden no estar definidos).
Por cierto, creo que la decisión de prohibir las matrices de longitud cero fue desafortunada (algunos dialectos más antiguos como Turbo C lo admiten) ya que una matriz de longitud cero podría considerarse como una señal de que el compilador debe generar código que funcione con índices más grandes. .
fuente