Я спрашиваю о хорошо известном трюке «последний член структуры имеет переменную длину». Это выглядит примерно так:
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 к этому, я хотел бы, чтобы ответы касались именно версии трюка, как указано выше.
c
undefined-behavior
c89
Эван Теран
источник
источник
Ответы:
Как сказано в C FAQ :
и:
Обоснование бита «строгое соответствие» содержится в спецификации, раздел J.2 «Неопределенное поведение» , которое включено в список неопределенного поведения:
В параграфе 8 Раздела 6.5.6 Аддитивные операторы еще раз упоминается, что доступ за пределы определенных границ массива не определен:
источник
p->s
никогда не используется как массив. Он передаетсяstrcpy
, и в этом случае он распадается на простойchar *
, который указывает на объект, который можно юридически интерпретировать какchar [100];
внутри выделенного объекта.malloc
, когда вы просто преобразовали возвращенныеvoid *
на указатель на [структуру, содержащую] массив. По-прежнему можно получить доступ к любой части выделенного объекта, используя указатель наchar
(или предпочтительноunsigned char
).malloc
. Поищите "объект" в стандарте перед тем, как выбросить bs.Я считаю, что технически это неопределенное поведение. Стандарт (возможно) не обращается к нему напрямую, поэтому он подпадает под действие «или из-за отсутствия какого-либо явного определения поведения». пункт (§4 / 2 C99, §3.16 / 2 C89), в котором говорится, что это неопределенное поведение.
Вышеупомянутое «возможно» зависит от определения оператора индексации массива. В частности, в нем говорится: «Постфиксное выражение, за которым следует выражение в квадратных скобках [], является обозначением объекта массива с нижним индексом». (C89, §6.3.2.1 / 2).
Вы можете утверждать, что здесь нарушается «объект массива» (поскольку вы указываете индекс за пределами определенного диапазона объекта массива), и в этом случае поведение (немного больше) явно undefined, а не просто undefined любезно ничего не определяющего.
Теоретически я могу представить компилятор, который выполняет проверку границ массива и (например) прерывает программу, когда / если вы пытаетесь использовать индекс вне диапазона. На самом деле, я не знаю, что такое существует, и, учитывая популярность этого стиля кода, даже если компилятор попытался принудительно использовать индексы при некоторых обстоятельствах, трудно представить, что кто-то будет мириться с тем, что он делает это в эта ситуация.
источник
arr[x] = y;
можно было бы переписать какarr[0] = y;
; для массива размером 2 егоarr[i] = 4;
можно переписать как:i ? arr[1] = 4 : arr[0] = 4;
Хотя я никогда не видел, чтобы компилятор выполнял такую оптимизацию, в некоторых встроенных системах они могут быть очень продуктивными. На PIC18x, использующем 8-битные типы данных, код для первой инструкции будет шестнадцать байтов, второй - два или четыре, а третий - восемь или двенадцать. Неплохая оптимизация, если она легальна.a[2] == a + 2
), это не так. Если я прав, все стандарты C определяют доступ к массиву как арифметический указатель.Да, это неопределенное поведение.
Отчет о дефектах языка C # 051 дает окончательный ответ на этот вопрос:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
В документе C99 Rationale Комитет C добавляет:
источник
malloc
) действителен при добавлении, поэтому как может идентичный указатель, полученный по другому маршруту, недопустимо в дополнении? Даже если они хотят заявить, что это UB, это довольно бессмысленно, потому что с вычислительной точки зрения реализация не может отличить четко определенное использование от предположительно неопределенного использования.*foo
содержит одноэлементный массивboz
, выражениеfoo->boz[biz()*391]=9;
можно упростить какbiz(),foo->boz[0]=9;
). К сожалению, отказ компиляторов от массивов с нулевым элементом означает, что во многих кодах вместо этого используются одноэлементные массивы, и эта оптимизация может нарушить работу.Этот конкретный способ сделать это явно не определен ни в одном стандарте C, но C99 действительно включает «взлом структуры» как часть языка. В C99 последним членом структуры может быть «гибкий элемент массива», объявленный как
char foo[]
(с любым типом, который вам нужен вместоchar
).источник
Это не неопределенное поведение , независимо от того , что говорят официальные лица или кто- либо другой , потому что оно определено стандартом.
p->s
, за исключением случаев использования в качестве lvalue, вычисляет указатель, идентичный(char *)p + offsetof(struct T, s)
. В частности, это действительныйchar
указатель внутри объекта malloc'd, и есть 100 (или более, в зависимости от соображений выравнивания) последовательных адресов, следующих сразу за ним, которые также действительны какchar
объекты внутри выделенного объекта. Тот факт, что указатель был получен путем использования->
вместо явного добавления смещения к указателю, возвращаемому функциейmalloc
cast tochar *
, не имеет значения.Технически,
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
.источник
malloc
которому осуществляется доступ как массив, или если это более крупная структура, доступ к которой осуществляется через указатель на меньшую структуру, элементы которой, среди прочего, являются начальным подмножеством элементов большей структуры. случаи.malloc
не выделить диапазон памяти, к которой можно получить доступ с помощью арифметики указателей, какой в этом смысл? И еслиp->s[1]
это определено стандартом , как синтаксический сахар для арифметики с указателями, то этот ответ : всего вновь утверждает , чтоmalloc
является полезным. Что осталось обсудить? :)1
. Это так просто.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
вif
ветку. Это UB (и не гарантируется инициализацияn
) согласно 6.5.6 p8 (последнее предложение), как я его читал. Связано: 6.5.9 p6 со сноской 109. (Ссылки на C11 n1570.) [...]Да, это технически неопределенное поведение.
Обратите внимание, что есть как минимум три способа реализовать «взлом структуры»:
(1) Объявление конечного массива размером 0 (самый «популярный» способ в устаревшем коде). Очевидно, это UB, поскольку объявления массива нулевого размера всегда недопустимы в C. Даже если он компилируется, язык не дает никаких гарантий относительно поведения любого кода, нарушающего ограничения.
(2) Объявление массива с минимальным допустимым размером - 1 (ваш случай). В этом случае любые попытки получить указатель
p->s[0]
и использовать его для арифметики указателя, выходящей за рамки,p->s[1]
являются неопределенным поведением. Например, отладочная реализация может создавать специальный указатель со встроенной информацией о диапазоне, которая будет перехватывать каждый раз, когда вы пытаетесь создать указатель за пределамиp->s[1]
.(3) Объявление массива с «очень большим» размером, например, 10000. Идея состоит в том, что заявленный размер должен быть больше всего, что вам может понадобиться на практике. Этот метод свободен от UB в отношении диапазона доступа к массиву. Однако на практике, конечно, мы всегда будем выделять меньший объем памяти (ровно столько, сколько действительно необходимо). Я не уверен в законности этого, т.е. мне интересно, насколько законно выделять для объекта меньше памяти, чем заявленный размер объекта (при условии, что мы никогда не обращаемся к «невыделенным» членам).
источник
s[1]
нет неопределенного поведения. Это то же самое*(s+1)
, что то же самое*((char *)p + offsetof(struct T, s) + 1)
, что является действительным указателем на achar
в выделенном объекте.foo[]
синтаксический сахар для*foo
), то любой доступ, превышающий меньший из его объявленного размера и его выделенного размера, будет UB, независимо от того, как выполнялась арифметика указателя.foo[]
в структуре не является синтаксическим сахаром для*foo
; это гибкий член массива C99. В остальном смотрите мой ответ и комментарии к другим ответам.unsigned char [sizeof object]
массив. . Я поддерживаю свое утверждение, что гибкий элемент массива "hack" для pre-C99 имеет четко определенное поведение.Стандарт совершенно ясно, что вы не можете получить доступ к вещам за пределами конца массива. (и переход через указатели не помогает, так как вам не разрешено даже увеличивать указатели после единицы после конца массива).
И за «отработку на практике». Я видел, как оптимизатор gcc / g ++ использовал эту часть стандарта, создавая неправильный код при встрече с этим недопустимым C.
источник
Если компилятор принимает что-то вроде
Я думаю, совершенно очевидно, что он должен быть готов принять нижний индекс на «dat», превышающий его длину. С другой стороны, если кто-то кодирует что-то вроде:
а затем обращается к somestruct-> dat [x]; Я бы не подумал, что компилятор обязан использовать код вычисления адреса, который будет работать с большими значениями x. Я думаю, что если бы кто-то хотел быть в безопасности, правильная парадигма была бы больше похожа на:
а затем выполните malloc размером (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + required_array_length) байтов (имея в виду, что если желаемый_array_length больше LARGEST_DAT_SIZE, результаты могут быть неопределенными).
Кстати, я думаю, что решение запретить массивы нулевой длины было неудачным (некоторые старые диалекты, такие как Turbo C, поддерживают его), поскольку массив нулевой длины можно рассматривать как знак того, что компилятор должен генерировать код, который будет работать с большими индексами .
источник