2010-09-14 2 views
106

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

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 к этому вопросу, я бы хотел, чтобы ответы на них были специально включены в версию трюка, как указано выше.

+0

Как и в приведенном выше коде, который не будет работать во время компиляции? Посмотрите на свою линию 'malloc'. В частности, тип decl. – jer

+33

Это кажется вполне понятным, разумным и, прежде всего, * ответственным * вопросом. Не видя причины закрытого голосования. – cHao

+0

@jer: Я спрашиваю, действительно ли трюк UB, а не если он компилируется. жаль, что я забыл ключевое слово 'struct', исправлено. –

ответ

51

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

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

и:

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

Обоснованием «строго в соответствии» бит в спецификации, раздел J.2 Неопределенное поведение, которая включает в себя в списке неопределенного поведения:

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

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

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

+1

В коде OP 'p-> s' никогда не используется в качестве массива. Он передается 'strcpy', и в этом случае он распадается на простой символ' char * ', который, как представляется, указывает на объект, который может быть юридически интерпретирован как' char [100]; 'внутри выделенного объекта. –

+3

Возможно, еще один способ взглянуть на это заключается в том, что язык может существенно ограничивать доступ к фактическим ** переменным массива **, как описано в J.2, но нет способа сделать такие ограничения для объекта, выделенного 'malloc' , когда вы просто преобразовали возвращенный 'void *' в указатель на [структуру, содержащую] массив. Он по-прежнему действителен для доступа к любой части выделенного объекта, используя указатель на 'char' (или предпочтительно' unsigned char'). –

+0

@R. - Я вижу, как J2 может не охватывать это, но разве это не распространяется на 6.5.6? – detly

34

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

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

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

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

+0

+1, это хороший ответ –

+1

Я также могу представить себе компилятор, который мог бы решить, что если массив имел размер 1, тогда 'arr [x] = y;' может быть переписан как 'arr [0] = у; '; для массива размером 2, 'arr [i] = 4;' может быть переписано как 'i? arr [1] = 4: arr [0] = 4; «Хотя я никогда не видел, чтобы компилятор выполнял такую ​​оптимизацию, на некоторых встроенных системах они могли быть очень продуктивными. На PIC18x, используя 8-битные типы данных, код для первого оператора будет шестнадцатью байтами, вторым, двумя или четырьмя, а третий, восемь или двенадцать. Неплохая оптимизация, если это законно. – supercat

+0

Если стандарт определяет доступ к массиву за пределами границ массива как неопределенное поведение, тогда и структура-хак. Если, однако, стандарт определяет доступ к массиву в качестве синтаксического сахара для арифметики указателя ('a [2] == a + 2'), это не так. Если я прав, все стандарты C определяют доступ к массиву как арифметический указатель. – YoYoYonnY

8

Это не определено поведение, независимо от того, что кто-нибудь, чиновник или иначе, говорит, потому что это определено стандартом. 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];. Затем четко определяется как элемент массива в структуре для i<sizeof struct that_other_struct и как объект char по адресу после конца структуры для i>=sizeof struct that_other_struct.

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

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

+0

Можете ли вы создавать массивы «взломанных структур»? Как в 'struct hack * p = malloc (42 * (sizeof * p + 100));'? – pmg

+0

Вы можете, но вам придется делать подписку самостоятельно, включая обработку выравнивания. 'p [1]' не будет указывать на вторую структуру, а скорее будет перекрываться с 'p-> s [4]' или так ... –

+3

Вы правы, что характер арифметики указателя не имеет значения, но вы 're * wrong * о доступе за пределы объявленного размера массива. См. [N1494] (http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1494.pdf) (последний публичный проект C1x), раздел 6.5.6, пункт 8 - вам даже не разрешено сделать * добавление *, которое принимает указатель более одного элемента за объявленный размер массива, и вы не можете разыгрывать его, даже если это всего лишь один элемент прошлого. – zwol

7

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

Обратите внимание, что есть по крайней мере три способа реализации «STRUCT хака»:

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

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

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

+1

В (2) 's [1]' не является неопределенным поведением. Это то же самое, что и '* (s + 1)', что совпадает с символом '* ((char *) p + offsetof (struct T, s) + 1)', который является действительным указателем на 'char' в выделенный объект. –

+0

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

+0

R: Если массив был объявлен как имеющий размер (это не просто синтаксический сахар 'foo []' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ''' ' независимо от того, как была выполнена арифметика указателя. – zwol

11

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

+0

Чтобы быть педантичным, это не структурный хак. Структурный хак использует массив с фиксированным размером, а не с гибким членом массива. Структурный взлом - это то, о чем спросили и является UB. Гибкие члены массива просто кажутся попыткой успокоить людей, замеченных в этой теме, жалующихся на этот факт. –

1

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

 
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;

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

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

3

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

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

+0

Можете ли вы привести пример? – Tal

11

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

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

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

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

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

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

+2

+1 для поиска этого, но я все еще утверждаю, что это противоречиво. Два указателя на один и тот же объект (в данном случае заданный байт) равны, и один указатель на него (указатель на массив представления всего объекта, полученный «malloc'), действителен в дополнении, так как может идентичный указатель, полученный по другому маршруту, недействителен при добавлении? Даже если они хотят утверждать, что это UB, это довольно бессмысленно, потому что нет возможности для реализации для различения четко определенного использования и предположительно неопределенного использования. –

+0

Слишком плохо, что компиляторы C начали запрещать объявление массивов нулевой длины; если бы не этот запрет, многим компиляторам не пришлось бы делать какую-либо специальную обработку, чтобы заставить их работать так, как они должны ", но все равно мог бы иметь специальный код для одноэлементных массивов (например, если' * foo' содержит одноэлементный массив 'boz', выражение' foo-> boz [biz() * 391] = 9; 'может быть упрощено как' biz(), foo-> boz [0] = 9; '). К сожалению, массивы с нулевыми элементами отказов компиляторов означают, что много кода использует одноэлементные массивы и будет нарушено этой оптимизацией. – supercat

Смежные вопросы