Является ли" struct hack " технически неопределенным поведением?


то, о чем я спрашиваю, - это хорошо известный трюк "последний член структуры имеет переменную длину". Это звучит примерно так:

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

из-за того, что структура выложена в памяти, мы можем наложить структуру на больший, чем необходимо блок и обрабатывать последний элемент, как если бы он был больше, чем 1 char указано.

Итак, вопрос: является ли эта техника технически неопределенным поведением?. Я бы ожидал, что это, но было любопытно, что стандарт говорит об этом.

PS: Я знаю о подходе C99 к этому, я хотел бы, чтобы ответы придерживались конкретно версии трюка, как указано выше.

8 108

8 ответов:

Как C FAQ говорит:

неясно, является ли он законным или портативным, но он довольно популярен.

и:

... официальная интерпретация сочла, что она не полностью соответствует стандарту C, хотя, похоже, она работает во всех известных реализациях. (Компиляторы, которые тщательно проверяют границы массива, могут выдавать предупреждения.)

обоснование строго соответствующий бит в спецификации, раздел J. 2 неопределенное поведение, который включает в себя в списке неопределенного поведения:

  • индекс массива находится вне диапазона, даже если объект, по-видимому, доступен с данным индексом (как в выражении lvalue a[1][7] данной декларации int a[4][5]) (6.5.6).

пункт 8 раздела 6.5.6 аддитивные операторы есть еще одно упоминание, что доступ за пределами определенные границы массива не определены:

если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение не определено.

Я считаю, что технически это неопределенное поведение. Стандарт (возможно) не обращается к нему напрямую, поэтому он подпадает под "или пропуск любого явного определения поведения."пункт (§4/2 из C99, §3.16 / 2 из C89), который говорит, что это неопределенное поведение.

" возможно " выше зависит от определения оператора подписки на массив. В частности, в нем говорится: "постфиксное выражение, за которым следует выражение в квадратных скобках [] , является подписанным обозначением объект array."(C89, §6.3.2.1 / 2).

вы можете утверждать, что здесь нарушается" объект массива " (так как ВЫ подписываетесь за пределами определенного диапазона объекта массива), и в этом случае поведение (немного больше) явно не определено, а не просто неопределенная любезность ничего не определяющего его.

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

этот конкретный способ сделать это явно не определен ни в одном стандарте C, но C99 включает в себя "struct hack" как часть языка. В C99 последним членом структуры может быть "гибкий элемент массива", объявленный как char foo[] (с любым типом вы желаете вместо char).

Да, это неопределенное поведение.

отчет о дефекте языка C #051 дает окончательный ответ на этот вопрос:

идиома, в то время как общий, не строго соответствует

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

в пояснительном документе C99 комитет C добавляет:

достоверность этой конструкции всегда была под вопросом. В ответ на один Дефект Доклада, комитет решил, что это неопределенное поведение, потому что массив P->элементы содержит только один элемент, независимо от того, существует ли пространство.

это не неопределенное поведение, независимо от того, что кто-нибудь, официальные или иначе, говорит, потому, что это определено стандартом. p->s, за исключением случаев, когда используется в качестве lvalue, вычисляет указатель, идентичный (char *)p + offsetof(struct T, s). В частности, это действительно char указатель внутри объекта malloc'D, и есть 100 (или более, в зависимости от соображений выравнивания) последовательных адресов, непосредственно следующих за ним, которые также действительны как char объекты внутри выделенные объекты. Дело в том, что указатель был получен с помощью -> вместо явного добавления смещения к указателю, возвращаемому malloc приведение к char *, это не имеет никакого отношения.

технически p->s[0] является единственным элементом char массив внутри структуры, следующие несколько элементов (например p->s[1] через p->s[3]), вероятно, заполнение байтов внутри структуры, которые могут быть повреждены, если вы выполняете назначение структуре в целом, но не если вы просто получаете доступ отдельные элементы и остальные элементы являются дополнительным пространством в выделенном объекте, которое вы можете использовать по своему усмотрению, если вы подчиняетесь требованиям выравнивания (и char не имеет требований к выравниванию).

если вы беспокоитесь, что возможность перекрытия с байтами заполнения в структуре может каким-то образом вызвать носовые демоны, вы можете избежать этого, заменив 1 на [1] со значением, которое гарантирует отсутствие заполнения в конце структура. Простой, но расточительный способ сделать это - сделать структуру с идентичными членами, за исключением массива в конце, и использовать s[sizeof struct that_other_struct]; для массива. Тогда,p->s[i] четко определяется как элемент массива в структуре для i<sizeof struct that_other_struct и как объект char по адресу, следующему за концом структуры для i>=sizeof struct that_other_struct.

Edit: на самом деле, в приведенном выше трюке для получения нужного размера вам также может потребоваться поместить объединение, содержащее каждый простой тип перед массивом, чтобы убедиться, что сам массив начинается с максимального выравнивания, а не в середине заполнения какого-либо другого элемента. Опять же, я не считаю, что все это необходимо, но я предлагаю это для самых параноидальных языковых юристов.

Edit 2: перекрытие с байтами заполнения определенно не является проблемой из-за другой части стандарта. C требует, чтобы если две структуры согласуются в начальной подпоследовательности их элементов, то общие исходные элементы могут быть доступны через указатель на любой тип. Как следствие, если структура идентична struct T но с большим конечным массивом были объявлены, элемент s[0] должен был бы совпадать с элементом s[0] in struct T, и наличие этих дополнительных элементов не может повлиять или быть затронуты в связи c общими элементами более крупной структуры с помощью указателя на struct T.

Да, это технически неопределенное поведение.

обратите внимание, что существует по крайней мере три способа реализации "struct hack":

(1) объявление конечного массива размером 0 (самый "популярный" способ в устаревшем коде). Это, очевидно, UB, поскольку объявления массива нулевого размера всегда являются незаконными в C. Даже если он компилируется, язык не дает никаких гарантий относительно поведения любого кода, нарушающего ограничения.

(2) объявления массив с минимальным юридическим размером-1 (ваш случай). В этом случае любые попытки взять указатель на p->s[0] и использовать его для указателя арифметики, которая выходит за рамки p->s[1] - это неопределенное поведение. Например, реализация отладки позволяет создать специальный указатель со встроенной информацией о диапазоне, который будет перехватывать каждый раз, когда вы пытаетесь создать указатель за пределами p->s[1].

(3) объявление массива с "очень большим" размером как 10000, например. Идея это то, что объявленный размер должен быть больше, чем все, что вам может понадобиться на практике. Этот метод свободен от UB относительно диапазона доступа к массиву. Однако, на практике, конечно, мы всегда выделить меньший объем памяти (только столько, сколько действительно необходимо). Я не уверен в законности этого, т. е. мне интересно, насколько законно выделять меньше памяти для объекта, чем объявленный размер объекта (предполагая, что мы никогда не обращаемся к "не выделенным" членам).

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

и для "работы на практике". Я видел оптимизатор gcc / g++, использующий эту часть стандарта, таким образом генерируя неправильный код при встрече с этим недопустимым C.

если компилятор принимает что-то вроде

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

Я думаю, что это довольно ясно, что он должен быть готов принять индекс на 'dat' за его длину. С другой стороны, если кто-то кодирует что-то вроде:

typedef struct {
  int whatever;
  char dat[1];
} MY_STRUCT;

а затем обращается к somestruct - >dat[x]; я бы не подумал, что компилятор обязан использовать код вычисления адреса, который будет работать с большими значениями x. я думаю, что если бы кто-то хотел быть действительно безопасным, правильная парадигма была бы более например:

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

а затем сделать malloc(sizeof (MYSTRUCT)-LARGEST_DAT_SIZE + desired_array_length) байт (имея в виду, что если desired_array_length больше, чем LARGEST_DAT_SIZE, результаты могут быть неопределенными).

кстати, я думаю, что решение запретить массивы нулевой длины было неудачным (некоторые старые диалекты, такие как Turbo C, поддерживают его), поскольку массив нулевой длины можно рассматривать как признак того, что компилятор должен генерировать код, который будет работать с более крупные показатели.