Является ли "структура взломать" технически неопределенным поведением? - PullRequest
109 голосов
/ 14 сентября 2010

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

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 ]

51 голосов
/ 14 сентября 2010

Как сказано в C FAQ :

Не ясно, легально ли это или портативно, но довольно популярно.

и:

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

Логическое обоснование бита 'строго согласованного' находится в разделе спецификации J.2 Неопределенное поведение , которое включаетв списке неопределенного поведения:

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

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

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

34 голосов
/ 14 сентября 2010

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

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

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

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

12 голосов
/ 13 сентября 2012

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

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

Эта идиома, хотя и распространенная, не совсем соответствует

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

В обосновании C99 Комитет C добавляет:

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

11 голосов
/ 15 сентября 2010

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

7 голосов
/ 15 сентября 2010

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

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

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

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

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

7 голосов
/ 15 сентября 2010

Это не неопределенное поведение , независимо от того, что кто-либо, официальный или иным образом , говорит, потому что оно определено стандартом.p->s, за исключением случаев использования в качестве lvalue, оценивает указатель, идентичный (char *)p + offsetof(struct T, s).В частности, это действительный указатель char внутри объекта malloc, и сразу после него есть 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.

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

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

3 голосов
/ 08 мая 2012

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

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

1 голос
/ 17 сентября 2010

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

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 + required_array_length) (имея в виду, что еслиrequired_array_length больше, чем LARGEST_DAT_SIZE, результаты могут быть неопределенными).

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

...