Является ли "структурный хак" технически undefined поведением?

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

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.

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

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

Ответ 1

В качестве C часто задаваемых вопросов говорится:

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

и

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Ответ 4

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

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

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

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

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

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

Ответ 5

Это не поведение undefined, независимо от того, что кто-либо, официальный или иначе, говорит, потому что он определен стандартом. 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 in [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.

Ответ 6

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

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

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

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

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

Ответ 7

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

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

Ответ 8

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

typedef struct {
  int len;
  char 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 + wish_array_length) байтов (имея в виду, что если length_array_length больше LARGEST_DAT_SIZE, результаты могут быть undefined).

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