¿Cuál es la necesidad de una matriz con elementos cero?

122

En el código del kernel de Linux encontré lo siguiente que no puedo entender.

 struct bts_action {
         u16 type;
         u16 size;
         u8 data[0];
 } __attribute__ ((packed));

El código está aquí: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

¿Cuál es la necesidad y el propósito de una matriz de datos con cero elementos?

Jeegar Patel
fuente
No estoy seguro de si debería haber una etiqueta de longitud cero o una etiqueta struct-hack ...
hippietrail
@hippietrail, porque a menudo cuando alguien pregunta qué es esta estructura, no saben que se conoce como "miembro de matriz flexible". Si lo hicieran, podrían haber encontrado fácilmente su respuesta. Como no lo hacen, no pueden etiquetar la pregunta como tal. Por eso no tenemos esa etiqueta.
Shahbaz
10
Vota para reabrir. Estoy de acuerdo en que esto no fue un duplicado, porque ninguna de las otras publicaciones aborda la combinación de un "truco de estructura" no estándar con longitud cero y el miembro de matriz flexible de características C99 bien definido. También creo que siempre es beneficioso para la comunidad de programación de C arrojar algo de luz sobre cualquier código oscuro del kernel de Linux. Principalmente porque mucha gente tiene la impresión de que el kernel de Linux es una especie de código C de última generación, por razones desconocidas. Si bien en realidad es un desastre terrible inundado de exploits no estándar que nunca deben considerarse como un canon de C.
Lundin
5
No es un duplicado, no es la primera vez que veo a alguien cerrar una pregunta innecesariamente. También creo que esta pregunta se suma a la base de conocimientos de SO.
Aniket Inge

Respuestas:

139

Esta es una forma de tener tamaños variables de datos, sin tener que llamar malloc( kmallocen este caso) dos veces. Lo usarías así:

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

Esto solía no ser estándar y se consideraba un truco (como dijo Aniket), pero estaba estandarizado en C99 . El formato estándar para él ahora es:

struct bts_action {
     u16 type;
     u16 size;
     u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */

Tenga en cuenta que no menciona ningún tamaño para el datacampo. Tenga en cuenta también que esta variable especial solo puede aparecer al final de la estructura.


En C99, este asunto se explica en 6.7.2.1.16 (énfasis mío):

Como caso especial, el último elemento de una estructura con más de un miembro nombrado puede tener un tipo de matriz incompleta; esto se llama un miembro de matriz flexible. En la mayoría de situaciones, se ignora el miembro de matriz flexible. En particular, el tamaño de la estructura es como si se omitiera el miembro de matriz flexible, excepto que puede tener más relleno final del que implicaría la omisión. Sin embargo, cuando a. (o ->) el operador tiene un operando izquierdo que es (un puntero a) una estructura con un miembro de matriz flexible y el operando derecho nombra ese miembro, se comporta como si ese miembro fuera reemplazado por la matriz más larga (con el mismo tipo de elemento ) que no agrandaría la estructura que el objeto al que se accede; el desplazamiento de la matriz seguirá siendo el del miembro de la matriz flexible, incluso si esto difiera de la de la matriz de reemplazo. Si esta matriz no tuviera elementos,

O en otras palabras, si tiene:

struct something
{
    /* other variables */
    char data[];
}

struct something *var = malloc(sizeof(*var) + extra);

Puedes acceder var->datacon índices en formato [0, extra). Tenga en cuenta que sizeof(struct something)solo dará el tamaño teniendo en cuenta las otras variables, es decir, da dataun tamaño de 0.


También puede ser interesante observar cómo el estándar realmente da ejemplos de malloctal construcción (6.7.2.1.17):

struct s { int n; double d[]; };

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

Otra nota interesante por el estándar en la misma ubicación es (énfasis mío):

asumiendo que la llamada a malloc tiene éxito, el objeto apuntado por p se comporta, para la mayoría de los propósitos, como si p se hubiera declarado como:

struct { int n; double d[m]; } *p;

(hay circunstancias en las que se rompe esta equivalencia; en particular, las compensaciones del miembro d pueden no ser las mismas ).

Shahbaz
fuente
Para ser claros, el código original en la pregunta aún no es estándar en C99 (ni C11), y aún se consideraría un truco. La estandarización C99 debe omitir el límite de la matriz.
MM
¿Qué es [0, extra)?
SS Anne
36

Este es un truco en realidad, de hecho para GCC ( C90 ).

También se llama truco de estructura .

Entonces, la próxima vez, diría:

struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);

Sería equivalente a decir:

struct bts_action{
    u16 type;
    u16 size;
    u8 data[100];
};

Y puedo crear cualquier número de tales objetos de estructura.

Aniket Inge
fuente
7

La idea es permitir una matriz de tamaño variable al final de la estructura. Presumiblemente, bts_actiones un paquete de datos con un encabezado de tamaño fijo (los campos typey size) y un datamiembro de tamaño variable . Al declararlo como una matriz de longitud 0, se puede indexar como cualquier otra matriz. Luego, asignaría una bts_actionestructura, por ejemplo, de 1024 bytes de datatamaño, así:

size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);

Véase también: http://c2.com/cgi/wiki?StructHack

ella u
fuente
2
@Aniket: No estoy del todo seguro de dónde viene esa idea.
sheu
en C ++ sí, en C, no es necesario.
amc
2
@sheu, proviene del hecho de que tu estilo de escritura mallocte hace repetir varias veces y si alguna vez el tipo de actioncambios, tienes que arreglarlo varias veces. Compare los dos siguientes por sí mismo y lo sabrá: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));El segundo es más corto, más limpio y claramente más fácil de cambiar.
Shahbaz
5

El código no es válido C ( ver esto ). El kernel de Linux, por razones obvias, no se preocupa en lo más mínimo por la portabilidad, por lo que utiliza mucho código no estándar.

Lo que están haciendo es una extensión no estándar de GCC con un tamaño de matriz 0. Un programa compatible con el estándar se habría escrito u8 data[];y habría significado exactamente lo mismo. A los autores del kernel de Linux aparentemente les encanta hacer las cosas innecesariamente complicadas y no estándar, si se revela una opción para hacerlo.

En los estándares C más antiguos, terminar una estructura con una matriz vacía se conocía como "el truco de estructura". Otros ya han explicado su propósito en otras respuestas. El hack de struct, en el estándar C90, era un comportamiento indefinido y podría causar bloqueos, principalmente porque un compilador de C es libre de agregar cualquier número de bytes de relleno al final de la estructura. Estos bytes de relleno pueden colisionar con los datos que intentó "piratear" al final de la estructura.

GCC al principio hizo una extensión no estándar para cambiar este comportamiento de indefinido a bien definido. Luego, el estándar C99 adaptó este concepto y, por lo tanto, cualquier programa C moderno puede utilizar esta función sin riesgo. Se conoce como miembro de matriz flexible en C99 / C11.

Lundin
fuente
3
Dudo que "el kernel de Linux no se preocupe por la portabilidad". ¿Quizás se refería a la portabilidad a otros compiladores? Es cierto que está bastante entrelazado con las características de gcc.
Shahbaz
3
Sin embargo, creo que esta pieza de código en particular no es un código convencional y probablemente se haya omitido porque su autor no le prestó mucha atención. La licencia dice que se trata de algunos controladores de instrumentos de Texas, por lo que es poco probable que los programadores centrales del kernel le presten atención. Estoy bastante seguro de que los desarrolladores del kernel actualizan constantemente el código antiguo de acuerdo con nuevos estándares o nuevas optimizaciones. ¡Es demasiado grande para asegurarse de que todo esté actualizado!
Shahbaz
1
@Shahbaz Con la parte "obvia", me refería a la portabilidad a otros sistemas operativos, lo que naturalmente no tendría ningún sentido. Pero tampoco parece que les importe un comino la portabilidad a otros compiladores, han usado tantas extensiones GCC que probablemente Linux nunca será portado a otro compilador.
Lundin
3
@Shahbaz En cuanto al caso de cualquier cosa etiquetada como Texas Instruments, los propios TI son conocidos por producir el código C más inútil, cutre e ingenuo jamás visto, en sus notas de aplicación para varios chips de TI. Si el código se origina en TI, entonces todas las apuestas con respecto a la posibilidad de interpretar algo útil a partir de él están canceladas.
Lundin
4
Es cierto que linux y gcc son inseparables. El kernel de Linux también es bastante difícil de entender (principalmente porque un sistema operativo es complicado de todos modos). Sin embargo, mi punto fue que no es agradable decir "A los autores del kernel de Linux aparentemente les encanta hacer las cosas innecesariamente complicadas y no estándar, si se revela una opción para hacerlo" debido a una mala práctica de codificación de terceros. .
Shahbaz
1

Otro uso de la matriz de longitud cero es como una etiqueta con nombre dentro de una estructura para ayudar a compilar la verificación de compensación de la estructura en el tiempo.

Suponga que tiene algunas definiciones de estructuras grandes (abarcan varias líneas de caché) que desea asegurarse de que estén alineadas con el límite de la línea de caché tanto al principio como en el medio donde cruza el límite.

struct example_large_s
{
    u32 first; // align to CL
    u32 data;
    ....
    u64 *second;  // align to second CL after the first one
    ....
};

En el código, puede declararlos usando extensiones GCC como:

__attribute__((aligned(CACHE_LINE_BYTES)))

Pero aún desea asegurarse de que esto se aplique en tiempo de ejecución.

ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);

Esto funcionaría para una sola estructura, pero sería difícil cubrir muchas estructuras, cada una tiene un nombre de miembro diferente para alinear. Lo más probable es que obtenga un código como el siguiente, donde tiene que encontrar los nombres del primer miembro de cada estructura:

assert (offsetof (one_struct,     <name_of_first_member>) == 0);
assert (offsetof (one_struct,     <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);

En lugar de ir de esta manera, puede declarar una matriz de longitud cero en la estructura que actúa como una etiqueta con nombre con un nombre coherente pero no consume ningún espacio.

#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
    CACHE_LINE_ALIGN_MARK (cacheline0);
    u32 first; // align to CL
    u32 data;
    ....
    CACHE_LINE_ALIGN_MARK (cacheline1);
    u64 *second;  // align to second CL after the first one
    ....
};

Entonces el código de aserción en tiempo de ejecución sería mucho más fácil de mantener:

assert (offsetof (one_struct,     cacheline0) == 0);
assert (offsetof (one_struct,     cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);
Wei Shen
fuente
Idea interesante. Solo una nota que el estándar no permite matrices de longitud 0, por lo que esto es algo específico del compilador. Además, podría ser una buena idea citar la definición de gcc del comportamiento de las matrices de longitud 0 en una definición de estructura, como mínimo para mostrar si podría introducir relleno antes o después de la declaración.
Shahbaz