#c #c11 #strict-aliasing
Вопрос:
Учитывая эту программу, строгие правила сглаживания и оптимизация анализа псевдонимов на основе типов:
#include<stdio.h>
#include<stddef.h>
#include<stdalign.h>
#include<assert.h>
struct thing {
int x;
int y;
};
static_assert(sizeof("hello") < sizeof(struct thing),"buffer can't hold text");
int main() {
alignas(struct thing) char buffer[sizeof(struct thing)] = "hello";
void *ptr = buffer;
struct thing* s = ptr;
s->x = 20;
s->y = 30;
printf("%s",buffer);
return 0;
}
может ли соответствующий компилятор решить, что буфер никогда не изменяется, поэтому его можно поместить в сегмент, доступный только для чтения (или аналогичный), и, таким образом, это выведет hello? (работает, как и ожидалось на практике)
Возможно, я добавлю мотивирующий пример того, почему кто-то хотел бы это сделать. Допустим, кто-то создал распределитель арены, который отлично работает, когда мэллок передает начальный блок памяти. Нет проблем, каждый раз, когда мы записываем в память, полученную из этого распределителя, мы меняем эффективные типы при первой записи в (void*), которую мы получаем. Однако, если наш распределитель арены инициализируется символом*, полученным из стековой памяти (скажем, выровненным соответствующим образом для любого типа, который мы будем в него записывать), это большое нет, так как мы будем сглаживать буфер символов.
Это наводит меня на мысль, что это дыра в языке. Почему мы не можем стеком выделить соответствующим образом выровненную нетипизированную (но размерную) память?
Комментарии:
1. Поправьте меня, если я ошибаюсь, но похоже, что программа имеет UB и поэтому может печатать все, что захочет.
2. @n.1.8e9-где мой sharem.: Стандарт не предназначен для полного описания всего необходимого, чтобы сделать реализацию подходящей для каких-либо конкретных задач. Вместо этого он ожидает, что реализации, предназначенные для различных целей, расширят семантику языка, указав поведение в большем количестве случаев, чем предписано. Реализации, подходящие для низкоуровневого программирования, расширяют язык таким образом, а реализации, которые не поддерживают абсолютно ничего, кроме того, что требуется Стандартом, непригодны для низкоуровневого программирования.
3. @supercat Ты мне объясняешь, что значит «неопределенное поведение»? Я знаю.
4. @n.1.8e9-где моя доля.: При педантичном чтении правил большинство не придуманных программ нарушают ограничения N1570 6.5p7. Вопрос о том, какие программы следует осмысленно обрабатывать, остается вопросом качества реализации. Все не придуманные компиляторы расширяют язык, чтобы с пользой поддерживать конструкции, например
struct foo someStruct = {1,someFunction()};
, в автоматическом режиме, илиsomeStruct.intArray[n] = 23;
даже если оба получают доступ к объекту типаstruct foo
в течение его срока службы, но не через значение lvalue совместимого типа. С другой стороны, ни лязга, ни gcc…5. …поддерживает все угловые случаи, предусмотренные Стандартом, за исключением случаев, когда вызывается с
-fno-strict-aliasing
помощью . Если рассматривать эту сноску в подразумевая, что качество реализации должны лишь толковать нормы, как говорят, когда компиляторы должны предположить, что вещи могут псевдоним , даже если нет других конкретных доказательств того, что они сделали бы так , что эта конструкция должна иметь никаких проблем, но потом снова тоже самое для конструкции разработчики Clang и gcc отказаться./
Ответ №1:
Выравнивание-не единственная проблема здесь. Эффективным типом buffer
является массив символов, но вы предоставляете доступ к значению как int
. Это довольно явное нарушение строгого сглаживания и неопределенное поведение. Может случиться все, что угодно.
«Что угодно» включает в s->x = 20;
себя запись, или оптимизацию, или сбой программы, потому что строка была выделена в разделе «только для чтения», или сбой программы, но строка осталась нетронутой, потому что она была выделена в режиме реального времени, например, во флэш-памяти.
Комментарии:
1. Что касается «соответствующего компилятора», единственное требование состоит в том, что он должен принимать строго соответствующую программу. И это не так, поскольку оно содержит неопределенное поведение.
2. Разве не разрешено записывать любой тип в
char
буфер? Будет ли это иметь значение?3. @TedLyngmo Нет, все наоборот. Вам разрешено проверять любой тип байт за байтом с помощью символьного указателя. И в правилах псевдонимов есть исключение из доступа к значению lvalue, когда это делается с помощью символьного типа. Ваш пример-другое исключение, это «тип агрегата или объединения», который также содержит исключение в списке (C17 6.5).
4. Это тот ответ, которого я ожидал. Похоже, большая проблема в том, что по существу нельзя использовать «хранилище, выделенное стеком» (без обмана объединения), потому что оно имеет объявленный тип, который может вмешиваться с помощью строгих правил псевдонима. Я хотел бы, чтобы это можно было изменить путем выделения стека без объявленного типа. Т. е. выравнивание(обязательно) нетипизированного буфера[размер]. Это будет вести себя так, как если бы это было результатом malloc, но с установленным требованием выравнивания. Разумный вариант использования для этого, например, скажем, у вас есть распределитель арены, который отлично работает с хранилищем из malloc, почему он не работает с правильно выровненным стеком
5. @anonymouscoward Нестандартные функции, такие как alloca, могут работать подобным образом. По сути, они просто увеличивают указатель стека на несколько байтов. Но лучше всего использовать
-fno-strict-aliasing
gcc или просто держаться подальше от него. Большинство проблем со злоупотреблением псевдонимами в gcc были исправлены задолго до выпуска C11, но людям иногда удается создавать сценарии, в которых он все еще ломается. Все дело сводится к недостаткам в языковом стандарте.
Ответ №2:
Стандарт приглашает реализации, чьи клиенты сочтут полезными конструкции, подобные вашей, указать, что они будут обрабатывать такой код «документированным способом, характерным для среды», даже в тех случаях, когда сам Стандарт не предъявляет никаких требований. Поскольку у авторов не было оснований ожидать, что реализации, чьи клиенты сочтут такую поддержку полезной, откажутся от ее предоставления, они не видели необходимости в ее санкционировании. Вместо этого такая поддержка остается проблемой качества реализации. Реализации, разработанные и настроенные таким образом, чтобы они подходили для низкоуровневого программирования, обеспечат это, но те, которые не разработаны и не настроены таким образом, могут этого не сделать.
Что касается того, что может произойти, если реализация не обеспечит такую поддержку, некоторые компиляторы, которые разработаны и настроены так, чтобы подходить только для программ, которые будут обрабатывать исключительно надежные входные данные (например, clang и gcc с включенной полной оптимизацией), будут намеренно считать невозможными любые входные данные, которые стандарт не запрещает им обрабатывать бессмысленным образом, и опускать код, который будет уместен только в том случае, если такой ввод получен. Следовательно, такие реализации могут обрабатывать действия, которые Стандарт характеризует как Неопределенное поведение бессмысленными способами, которые не ограничены обычными законами времени или причинности.