Зачем нужен массив с нулевыми элементами?

122

В коде ядра Linux я обнаружил следующее, чего не могу понять.

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

Код здесь: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Для чего нужен массив данных с нулевыми элементами?

Джигар Патель
источник
Я не уверен, должны ли быть массивы нулевой длины или тег struct-hack ...
hippietrail
@hippietrail, потому что часто, когда кто-то спрашивает, что это за структура, они не знают, что она называется «гибкий элемент массива». Если бы они это сделали, они бы легко нашли свой ответ. Поскольку они этого не делают, они не могут пометить вопрос как таковой. Вот почему у нас нет такой метки.
Shahbaz
10
Проголосуйте за повторное открытие. Я согласен, что это не было дубликатом, потому что ни один из других постов не рассматривает комбинацию нестандартного «взлома структуры» с нулевой длиной и четко определенного элемента гибкого массива функции C99. Я также считаю, что сообществу программистов C всегда полезно пролить свет на любой непонятный код ядра Linux. В основном потому, что по неизвестным причинам у многих людей сложилось впечатление, что ядро ​​Linux - это своего рода современный код на языке C. Хотя на самом деле это ужасный беспорядок, наводненный нестандартными эксплойтами, которые никогда не следует рассматривать как какой-то канон C.
Lundin
5
Не дубликат - я не первый раз вижу, как кто-то закрывает вопрос без надобности. Также я думаю, что этот вопрос пополняет базу знаний SO.
Аникет Инге

Ответы:

139

Это способ иметь переменные размеры данных без необходимости дважды вызывать malloc( kmallocв данном случае). Вы бы использовали это так:

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

Раньше это было нестандартно и считалось взломом (как сказал Аникет), но оно было стандартизировано в C99 . Стандартный формат для этого сейчас:

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

Обратите внимание, что вы не указываете размер dataполя. Также обратите внимание, что эта специальная переменная может находиться только в конце структуры.


В C99 этот вопрос объясняется в 6.7.2.1.16 (выделено мной):

В качестве особого случая последний элемент структуры с более чем одним поименованным членом может иметь неполный тип массива; это называется гибким элементом массива, В большинстве случаев гибкий элемент массива игнорируется. В частности, размер структуры такой, как если бы элемент гибкого массива был опущен, за исключением того, что у него может быть больше завершающих отступов, чем это могло бы подразумеваться. Однако когда файл. (или ->) оператор имеет левый операнд, который является (указателем на) структуру с гибким элементом массива, и правый операнд называет этот член, он ведет себя так, как если бы этот член был заменен самым длинным массивом (с тем же типом элемента ), что не сделало бы структуру больше, чем объект, к которому осуществляется доступ; смещение массива должно оставаться смещением гибкого элемента массива, даже если оно будет отличаться от смещения массива замены. Если бы в этом массиве не было элементов,

Или другими словами, если у вас есть:

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

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

Вы можете получить доступ var->dataс помощью индексов в [0, extra). Обратите внимание, что sizeof(struct something)это дает только размер, учитывающий другие переменные, т.е. дает dataразмер 0.


Также может быть интересно отметить, как стандарт фактически дает примеры создания mallocтакой конструкции (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]));

Еще одно интересное примечание стандарта в том же месте (выделено мной):

предполагая, что вызов malloc завершился успешно, объект, на который указывает p, в большинстве случаев ведет себя так, как если бы p был объявлен как:

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

(есть обстоятельства, при которых эта эквивалентность нарушается; в частности, смещения элемента d могут быть разными ).

Shahbaz
источник
Чтобы было ясно, исходный код в вопросе все еще не является стандартным для C99 (или C11) и все равно будет считаться взломом. Стандартизация C99 должна опускать границу массива.
MM
Что [0, extra)?
SS Anne
36

Фактически, это взлом для GCC ( C90 ).

Это также называется взломом структуры .

Поэтому в следующий раз я бы сказал:

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

Это будет эквивалентно высказыванию:

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

И я могу создать любое количество таких структурных объектов.

Аникет Инге
источник
7

Идея состоит в том, чтобы разрешить массив переменного размера в конце структуры. По- видимому, bts_actionнекоторые пакетные данные с заголовком фиксированного размера (The typeи sizeполей), и переменного размера dataэлемента. Объявив его как массив нулевой длины, он может быть проиндексирован так же, как любой другой массив. Затем вы выделяете bts_actionструктуру размером, скажем, 1024 байта data, например:

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

См. Также: http://c2.com/cgi/wiki?StructHack

Sheu
источник
2
@Aniket: Я не совсем уверен, откуда взялась эта идея.
Sheu 01
в C ++ да, в C не требуется.
amc
2
@sheu, это происходит из-за того, что ваш стиль письма mallocзаставляет вас повторяться несколько раз, и если когда-либо тип actionизменений, вы должны исправить это несколько раз. Сравните следующие два для себя, и вы поймете: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));Второй короче, чище и, очевидно, легче изменить.
Shahbaz
5

Код недействителен C ( см. Это ). Ядро Linux по очевидным причинам нисколько не заботится о переносимости, поэтому в нем используется много нестандартного кода.

То, что они делают, - это нестандартное расширение GCC с размером массива 0. Была бы написана соответствующая стандарту программа, u8 data[];и это означало бы то же самое. Авторы ядра Linux, по-видимому, любят делать вещи излишне сложными и нестандартными, если появится возможность сделать это.

В более старых стандартах C завершение структуры пустым массивом было известно как «взлом структуры». Другие уже объяснили его цель в других ответах. Взлом структуры в стандарте C90 имел неопределенное поведение и мог вызвать сбои, в основном потому, что компилятор C может добавлять любое количество байтов заполнения в конец структуры. Такие байты заполнения могут конфликтовать с данными, которые вы пытались «взломать» в конце структуры.

GCC рано сделал нестандартное расширение, чтобы изменить это поведение с неопределенного на четко определенное. Затем стандарт C99 адаптировал эту концепцию, и поэтому любая современная программа C может использовать эту функцию без риска. Он известен как C99 / C11 гибкий элемент массива .

Лундин
источник
3
Я сомневаюсь, что «ядро linux не заботится о переносимости». Может, вы имели в виду переносимость на другие компиляторы? Это правда, что он довольно тесно переплетен с функциями gcc.
Shahbaz
3
Тем не менее, я думаю, что этот конкретный фрагмент кода не является основным и, вероятно, упущен, потому что его автор не обратил на него особого внимания. В лицензии говорится о некоторых драйверах texas instruments, поэтому маловероятно, что основные программисты ядра обратили на это внимание. Я почти уверен, что разработчики ядра постоянно обновляют старый код в соответствии с новыми стандартами или новыми оптимизациями. Просто он слишком велик, чтобы быть уверенным, что все обновлено!
Shahbaz
1
@Shahbaz Под «очевидной» частью я имел в виду переносимость на другие операционные системы, что, естественно, не имело бы никакого смысла. Но им, похоже, наплевать на переносимость на другие компиляторы, они использовали так много расширений GCC, что Linux вряд ли когда-либо будет перенесен на другой компилятор.
Lundin
3
@Shahbaz Что касается того, что называется Texas Instruments, то сами TI печально известны тем, что в своих заметках к приложениям для различных чипов TI выпускают самый бесполезный, дрянной, наивный код C, который когда-либо видел. Если код исходит от TI, то все ставки на возможность интерпретации из него чего-то полезного отключены.
Lundin
4
Это правда, что linux и gcc неразделимы. Ядро Linux также довольно сложно понять (в основном потому, что ОС в любом случае сложна). Я же хотел сказать, что нехорошо говорить: «Авторы ядра Linux, по-видимому, любят делать вещи излишне сложными и нестандартными, если такая возможность обнаружится» из-за плохой практики кодирования сторонних разработчиков. ,
Shahbaz
1

Еще одно использование массива нулевой длины - это именованная метка внутри структуры для помощи при проверке смещения структуры во время компиляции.

Предположим, у вас есть несколько больших определений структур (охватывающих несколько строк кэша), которые вы хотите убедиться, что они выровнены по границе строки кеша как в начале, так и в середине, где она пересекает границу.

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

В коде вы можете объявить их, используя расширения GCC, например:

__attribute__((aligned(CACHE_LINE_BYTES)))

Но вы все же хотите убедиться, что это выполняется во время выполнения.

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

Это сработает для одной структуры, но будет сложно охватить несколько структур, каждая из которых имеет разные имена членов, которые нужно выровнять. Скорее всего, вы получите код, подобный приведенному ниже, где вам нужно найти имена первого члена каждой структуры:

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);

Вместо этого вы можете объявить в структуре массив нулевой длины, действующий как именованная метка с согласованным именем, но не занимающий места.

#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
    ....
};

Тогда будет намного проще поддерживать код утверждения времени выполнения:

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);
Вэй Шэнь
источник
Интересная идея. Просто обратите внимание, что массивы нулевой длины не разрешены стандартом, так что это специфическая вещь для компилятора. Кроме того, было бы неплохо процитировать определение gcc поведения массивов нулевой длины в определении структуры, по крайней мере, чтобы показать, может ли он вводить заполнение до или после объявления.
Shahbaz