Когда вы спрашиваете о общем поведении undefined в C, души более просвещены, чем я говорил о правиле строгого псевдонимов.
О чем они говорят?
Типичная ситуация, с которой вы сталкиваетесь при строгих проблемах с псевдонимом, - это наложение структуры (например, устройства/сети msg) на буфер размера слова вашей системы (например, указатель на uint32_t
или uint16_t
s). Когда вы накладываете структуру на такой буфер или буфера на такую структуру через кастинг, вы можете легко нарушать правила строгого сглаживания.
Поэтому в такой настройке, если я хочу отправить сообщение на что-то, мне придется иметь два несовместимых указателя, указывающих на один и тот же кусок памяти. Тогда я мог бы наивно закодировать что-то вроде этого:
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
Правило строгого псевдонима делает эту установку незаконной: разыменование указателя, который псевдонизирует объект, который не соответствует совместимому типу, или один из других типов, разрешенных C 2011 6.5 пунктом 7 1, является неопределенным поведением. К сожалению, вы все равно можете запрограммировать этот способ, может быть, получить некоторые предупреждения, скомпилировать его, только чтобы иметь странное неожиданное поведение при запуске кода.
(GCC выглядит несколько непоследовательным в своей способности давать предупреждения на псевдонимы, иногда давая нам дружеское предупреждение, а иногда и нет).
Чтобы понять, почему это поведение не определено, мы должны подумать о том, что правило строкового aliasing покупает компилятор. В принципе, с этим правилом, ему не нужно думать о вставке инструкций, чтобы обновлять содержимое buff
каждый цикл цикла. Вместо этого при оптимизации с некоторыми досадными неулучшаемыми предположениями об aliasing он может опустить эти инструкции, load buff[0]
и buff[1
] в регистры CPU один раз до того, как цикл будет запущен, и ускорить тело цикла. Прежде чем вводить строгий псевдоним, компилятор должен был жить в состоянии паранойи, чтобы содержимое buff
могло меняться в любое время из любого места кем угодно. Таким образом, чтобы получить дополнительную отдачу от производительности, и если большинство людей не набирают указатели на каламбуры, было введено строгое правило псевдонимов.
Имейте в виду, если вы считаете, что пример надуман, это может произойти, если вы передаете буфер другой функции, выполняющей отправку для вас, если вы это сделали.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
И переписал наш более ранний цикл, чтобы воспользоваться этой удобной функцией
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Компилятор может быть или не быть способен или достаточно умен, чтобы попытаться встроить SendMessage, и он может или не может снова загрузить или не загружать бафф. Если SendMessage
является частью другого API, который скомпилирован отдельно, он, вероятно, имеет инструкции для загрузки содержимого буфера. Опять же, возможно, вы находитесь в C++, и это некоторая шаблонная версия только для заголовка, которую компилятор считает встроенным. Или, может быть, это просто то, что вы написали в своем.c файле для вашего удобства. В любом случае может возникнуть неопределенное поведение. Даже когда мы знаем некоторые из того, что происходит под капотом, это по-прежнему является нарушением правила, поэтому не гарантируется четкое поведение. Так что просто обертывание в функцию, которая берет наш буфер с разделителями слов, не обязательно помогает.
Итак, как мне обойти это?
Используйте союз. Большинство компиляторов поддерживают это, не жалуясь на строгий псевдоним. Это разрешено на C99 и явно разрешено в C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Вы можете отключить строгое сглаживание в своем компиляторе (f [no-] строгое сглаживание в gcc))
Вы можете использовать char*
для сглаживания вместо системного слова. Правила допускают исключение для char*
(включая signed char
и unsigned char
). Он всегда предполагал, что char*
псевдонимы других типов. Однако это не сработает по-другому: нет предположения, что ваша структура псевдонизирует буфер символов.
Начинающий остерегайтесь
Это только одно потенциальное минное поле при наложении двух типов друг на друга. Вы также должны узнать об истинности, выравнивании слов и о том, как правильно решать проблемы выравнивания с помощью структурирования упаковки.
1 Типы C 2011 6.5 7 позволяют получить доступ к lvalue:
unsigned char*
использоваться вместо char*
? Я склонен использовать unsigned char
вместо char
в качестве базового типа для byte
потому что мои байты не подписаны, и я не хочу, чтобы странность подписанного поведения (особенно в отношении переполнения)
unsigned char *
нормально.
Лучшее объяснение, которое я нашел, - Майк Актон, Понимание строго алиасинга. Он немного сосредоточился на разработке PS3, но в основном это просто GCC.
Из статьи:
"Строгое сглаживание - это предположение, сделанное компилятором C (или С++), что указатели на разузнавание объектов разных типов никогда не будут ссылаться на одно и то же место памяти (то есть псевдонимы друг друга.)"
Итак, в основном, если у вас есть int*
, указывающий на некоторую память, содержащую int
, а затем вы указываете a float*
в эту память и используете ее как float
, вы нарушаете правило. Если ваш код не соответствует этому, оптимизатор компилятора, скорее всего, нарушит ваш код.
Исключением из правила является char*
, которому разрешено указывать любой тип.
Это правило строгого сглаживания, которое содержится в разделе 3.10 стандарта С++ 03 (другие ответы дают хорошее объяснение, но никто не предоставил это правило):
Если программа пытается получить доступ к сохраненному значению объекта через значение l, отличного от одного из следующих типов, поведение undefined:
- динамический тип объекта,
- cv-квалифицированная версия динамического типа объекта,
- тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
- тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
- совокупность или тип объединения, который включает один из вышеупомянутых типов среди своих членов (включая рекурсивно, член подгруппы или объединенный союз),
- тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
- a
char
илиunsigned char
.
С++ 11 и C++ 14 (подчеркнуты изменения):
Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:
- динамический тип объекта,
- cv-квалифицированная версия динамического типа объекта,
- тип, аналогичный (как определено в 4.4) для динамического типа объекта,
- тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
- тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
- тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
- тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
- a
char
илиunsigned char
.
Два изменения были небольшими: glvalue вместо lvalue и уточнение случая агрегата/объединения.
Третье изменение делает более сильную гарантию (ослабляет правило сильного aliasing): новая концепция подобных типов, которые теперь безопасны для псевдонимов.
Кроме того, формулировка C (C99; ISO/IEC 9899: 1999 6.5/7, точно такая же формулировка используется в ISO/IEC 9899: 2011 §6.5 ¶7):
Объект должен иметь сохраненное значение, доступное только с помощью значения lvalue выражение, имеющее один из следующих типов 73) или 88):
- тип, совместимый с эффективным типом объекта,
- квалифицированная версия типа, совместимого с эффективным типом объект,
- тип, который является подписанным или неподписанным типом, соответствующим эффективный тип объекта,
- тип, который является подписанным или неподписанным типом, соответствующим квалифицированная версия эффективного типа объекта,
- тип агрегата или объединения, который включает один из вышеупомянутых типы среди своих членов (в том числе, рекурсивно, член subaggregate или contains union) или
- тип символа.
73) или 88) Цель этого списка - указать те обстоятельства, при которых объект может или не может быть сглажен.
restrict
, но я не уверен).
Строгое сглаживание не относится только к указателям, оно также влияет на ссылки, я написал статью об этом для повышения вики-разработчика и так хорошо воспринял, что превратил его в страницу на моем консультационном веб-сайте. Он полностью объясняет, что это такое, почему он так много путает людей и что с этим делать. Строгий алиасинг. В частности, это объясняет, почему профсоюзы являются рискованным поведением для С++, и почему использование memcpy - единственное исправление, переносимое как на C, так и на С++. Надеюсь, что это будет полезно.
В качестве дополнения к тому, что писал Дуг Т., здесь это простой тестовый пример, который, вероятно, запускает его с помощью gcc:
check.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Скомпилируйте с помощью gcc -O2 -o check check.c
.
Обычно (с большинством версий gcc, которые я пробовал) это выводит "жесткую проблему псевдонимов", потому что компилятор предполагает, что "h" не может быть тем же адресом, что и "k" в функции "проверка". Из-за этого компилятор оптимизирует if (*h == 5)
и всегда вызывает printf.
Для тех, кто интересуется здесь, ассемблерный код x64, созданный gcc 4.6.3, работает на ubuntu 12.04.2 для x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Итак, условие if полностью исчезло из кода ассемблера.
long long*
и int64_t
*). Можно было бы ожидать, что здравомыслящий компилятор должен признать, что long long*
и int64_t*
могут обращаться к одному и тому же хранилищу, если они хранятся одинаково, но такое обращение уже не модно.
Это выдержка из моего "Что такое строгое правило сложения и почему мы заботимся"? записать.
В C и C++ aliasing имеет отношение к тем, к каким типам выражений нам разрешен доступ к хранимым значениям через. В C и C++ стандарт указывает, какие типы выражений разрешены для псевдонимов, какие типы. Компилятору и оптимизатору разрешено считать, что мы строго следуем правилам псевдонимов, следовательно, это правило строгого псевдонима. Если мы попытаемся получить доступ к значению с использованием недопустимого типа, он классифицируется как неопределенное поведение (UB). Как только у нас есть неопределенное поведение, все ставки отключены, результаты нашей программы уже не являются надежными.
К сожалению, при строгих нарушениях псевдонимов мы часто получаем ожидаемые результаты, оставляя возможность того, что будущая версия компилятора с новой оптимизацией нарушит код, который мы считали действительным. Это нежелательно, и стоит постараться понять правила строгого сглаживания и как избежать их нарушения.
Чтобы узнать больше о том, почему мы заботимся, мы обсудим проблемы, возникающие при нарушении правил строгого сглаживания, пишущих по типу, поскольку обычные методы, используемые в типе punning, часто нарушают правила строгого сглаживания и как правильно набирать слова.
Давайте посмотрим на некоторые примеры, затем мы можем говорить о том, что говорят стандарты (ы), изучать некоторые дополнительные примеры, а затем посмотреть, как избежать строгих псевдонимов и уловов, которые мы пропустили. Вот пример, который не должен удивлять (живой пример):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
У нас есть int *, указывающий на память, занимаемую int, и это действительное псевдонижение. Оптимизатор должен предположить, что присвоения через ip могут обновить значение, занимаемое x.
Следующий пример показывает сглаживание, которое приводит к неопределенному поведению (живой пример):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
В функции foo мы берем int * и float *, в этом примере мы вызываем foo и устанавливаем оба параметра, чтобы указывать на ту же ячейку памяти, которая в этом примере содержит int. Обратите внимание, что reinterpret_cast сообщает компилятору обработать выражение так, как если бы он имел тип, определенный его параметром шаблона. В этом случае мы говорим, что обрабатываем выражение & x, как если бы он имел тип float *. Мы можем наивно ожидать, что результатом второго cout будет 0, но с оптимизацией, включенной с использованием -O2, gcc и clang дают следующий результат:
0
1
Который может и не ожидаться, но совершенно корректен, поскольку мы вызывали неопределенное поведение. Поплавок не может быть действительным псевдонимом для объекта int. Поэтому оптимизатор может считать константу 1, хранящимися при разыменовании я буду возвращаемое значением, так как магазин через F не может законным образом повлиять на Int объекта. Подсоединение кода в Compiler Explorer показывает, что это именно то, что происходит (живой пример):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
Оптимизатор, использующий анализ псевдонимов на основе типа (TBAA), предполагает, что 1 будет возвращен и будет напрямую перемещать постоянное значение в регистр eax, который несет возвращаемое значение. TBAA использует правила языков о том, какие типы разрешены для псевдонима, чтобы оптимизировать нагрузки и магазины. В этом случае TBAA знает, что float не может использовать псевдоним и int и оптимизирует загрузку i.
Что именно означает, что стандарт разрешен и не разрешен? Стандартный язык не является простым, поэтому для каждого элемента я попытаюсь представить примеры кода, которые демонстрируют смысл.
В стандарте C11 говорится следующее в разделе 6.5. Выражения, пункт 7:
Объект должен иметь сохраненное значение, доступ к которому можно получить только с помощью выражения lvalue, которое имеет один из следующих типов: 88) - тип, совместимый с эффективным типом объекта,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- квалифицированная версия типа, совместимая с эффективным типом объекта,
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- тип, который является подписанным или неподписанным типом, соответствующим эффективному типу объекта,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc/clang имеет расширение, а также позволяет присваивать unsigned int * int *, даже если они не совместимы.
- тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии эффективного типа объекта,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- совокупный или тип объединения, который включает один из вышеупомянутых типов среди его членов (включая рекурсивно, член субагрегата или содержащегося объединения) или
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- тип символа.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
Проект C++ 17 в разделе [basic.lval] в пункте 11 гласит:
Если программа пытается получить доступ к сохраненному значению объекта с помощью glvalue другого, чем один из следующих типов, поведение не определено: 63 (11.1) - динамический тип объекта,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - cv-квалифицированная версия динамического типа объекта,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - тип, аналогичный (как определено в 7.5) для динамического типа объекта,
(11.4) - тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - тип, который является подписанным или неподписанным типом, соответствующим стандартной версии динамического типа объекта cv,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - совокупный или объединенный тип, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - символ char, unsigned char или std :: byte.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Стоит отметить, что подписанный символ не включен в список выше, это заметное отличие от C, в котором указан тип символа.
Мы дошли до этого момента, и нам может быть интересно, зачем мы хотим использовать псевдоним? Ответ, как правило, заключается в том, чтобы набирать pun, часто используемые методы нарушают строгие правила псевдонимов.
Иногда мы хотим обойти систему типов и интерпретировать объект как другой тип. Это называется типом punning, чтобы переинтерпретировать сегмент памяти как другой тип. Тип punning полезен для задач, которые хотят получить доступ к базовому представлению объекта для просмотра, транспортировки или манипулирования. Типичными областями, которые мы используем, является использование компиляторов, сериализация, сетевой код и т.д.
Традиционно это достигается путем принятия адреса объекта, приведения его к указателю типа, который мы хотим переосмыслить, а затем получить доступ к значению или, другими словами, с помощью псевдонимов. Например:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
Как мы видели ранее, это не является допустимым псевдонимом, поэтому мы вызываем неопределенное поведение. Но традиционно компиляторы не воспользовались строгими правилами псевдонимов, и этот тип кода, как правило, просто работал, разработчики, к сожалению, привыкли делать это таким образом. Общим альтернативным методом для punning типа является объединение, которое действительно в C, но неопределенное поведение в C++ (см. Живой пример):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n", u.n ); // UB in C++ n is not the active member
Это недействительно в C++, а некоторые считают, что профсоюзы должны быть исключительно для реализации вариантов вариантов и чувствовать, что использование союзов для типа punning является злоупотреблением.
Стандартный метод для пиннинга типа в C и C++ - memcpy. Это может показаться немного тяжелым, но оптимизатор должен распознать использование memcpy для тиража и оптимизировать его и создать регистр для регистрации перемещения. Например, если мы знаем, что int64_t имеет тот же размер, что и double:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
мы можем использовать memcpy:
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
На достаточном уровне оптимизации любой достойный современный компилятор генерирует идентичный код для ранее упомянутого метода reinterpret_cast или метода объединения для пин-кода типа. Изучая сгенерированный код, мы видим, что он использует только register mov (пример Live Compiler Explorer).
В C++ 20 мы можем получить bit_cast (реализация доступна в ссылке из предложения), которая дает простой и безопасный способ ввода слов, а также может использоваться в контексте constexpr.
Ниже приведен пример использования bit_cast для ввода pun для unsigned int для float (см. Его в прямом эфире):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
В случае, когда типы To и From не имеют одинакового размера, нам требуется использовать промежуточную структуру15. Мы будем использовать структуру, содержащую массив символов sizeof (unsigned int) (предполагается, что 4 байта без знака int) является типом From и unsigned int в качестве типа.
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
К сожалению, нам нужен этот промежуточный тип, но это текущее ограничение bit_cast.
У нас нет большого количества хороших инструментов для ловли строгого сглаживания в C++, инструменты, которые у нас есть, поймают некоторые случаи строгих нарушений псевдонимов и некоторые случаи неправильных нагрузок и магазинов.
gcc, используя флаг -fstrict-aliasing и -Wstrict-aliasing, может поймать некоторые случаи, хотя не без ложных срабатываний/негативов. Например, следующие случаи будут генерировать предупреждение в gcc (см. Его в прямом эфире):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
хотя он не поймает этот дополнительный случай (см. его в прямом эфире):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Хотя clang позволяет эти флаги, по-видимому, на самом деле не реализует предупреждения.
Другим инструментом, который мы имеем для нас, является ASan, который может поймать неверные нагрузки и магазины. Хотя они не являются строго строгими нарушениями псевдонимов, они являются общим результатом нарушений строгих нарушений. Например, следующие случаи будут генерировать ошибки времени выполнения при построении с помощью clang с использованием -fsanitize = address
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
Последний инструмент, который я порекомендую, является C++ конкретным, а не строго инструментом, а практикой кодирования, не допускайте приведения C-стиля. Как gcc, так и clang будут производить диагностику для C-стиля при использовании -Wold-style-cast. Это заставит любые неопределенные фразы типа использовать reinterpret_cast, в общем случае reinterpret_cast должен быть флагом для более тщательного обзора кода. Также проще выполнить поиск в базе кода для reinterpret_cast для проведения аудита.
Для C у нас есть все инструменты, которые уже были рассмотрены, и у нас также есть tis-interpreter, статический анализатор, который исчерпывающе анализирует программу для большого подмножества языка C. Учитывая C-примеры предыдущего примера, когда использование -fstrict-aliasing пропускает один случай (см. Его в прямом эфире)
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter способен уловить все три, следующий пример вызывает tis-kernal как tis-интерпретатор (вывод редактируется для краткости):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Наконец, TySan находится в разработке. Этот дезинфицирующее средство добавляет информацию проверки типов в сегмент теневой памяти и проверяет доступ, чтобы увидеть, нарушают ли они правила псевдонимов. Инструмент потенциально должен улавливать все нарушения псевдонимов, но может иметь большие накладные расходы.
Тип punning с помощью указателей (в отличие от использования объединения) является основным примером нарушения строгой псевдонимы.
В соответствии с обоснованием C89, авторы Стандарта не хотели требовать, чтобы компиляторы задавали такой код:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
необходимо перезагрузить значение x
между оператором присваивания и возврата, чтобы позволить возможность p
указывать на x
, а присвоение *p
может, следовательно, изменить значение of x
. Понятие о том, что компилятор должен иметь право предполагать, что в подобных ситуациях не будет сглаживания, не противоречиво.
К сожалению, авторы C89 написали свое правило таким образом, что, если бы его прочитали буквально, он сделал бы даже следующую функцию invoke Undefined Behavior:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
потому что он использует lvalue типа int
для доступа к объекту типа struct S
, а int
не относится к типам, которые можно использовать для доступа к struct S
. Поскольку было бы абсурдно относиться ко всем использованиям элементов и объединений несимвольных типов как Undefined Behavior, почти все признают, что существуют, по крайней мере, некоторые обстоятельства, когда lvalue одного типа может использоваться для доступа к объекту другой тип. К сожалению, Комитет по стандартам С не смог определить, каковы эти обстоятельства.
Большая часть проблемы является результатом отчета об ошибке № 028, в котором спрашивается о поведении программы, например:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
В отчете "Ошибка № 28" указано, что программа вызывает Undefined "Поведение", потому что действие записи члена объединения типа "double" и чтение одного из типа "int" вызывает поведение, определяемое реализацией. Такое рассуждение бессмысленно, но составляет основу для правил эффективного типа, которые бесполезно усложняют язык, не делая ничего для решения исходной проблемы.
Лучшим способом решения исходной проблемы, вероятно, будет рассмотрение сноска о цели правила, как будто это было нормативно и сделано правило не имеющее законной силы, за исключением случаев, которые фактически связаны с конфликтующими обращениями с использованием псевдонимов. Учитывая что-то вроде:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Не существует конфликта внутри inc_int
, потому что все обращения к хранилищу, доступ к которому осуществляется через *p
, выполняются с lvalue типа int
, и нет конфликта в test
, потому что p
явно отображается из struct S
, и к следующему моменту использования s
все обращения к этому хранилищу, которые когда-либо будут выполняться через p
, уже произойдут.
Если код немного изменился...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Здесь существует конфликт псевдонимов между p
и доступом к s.x
в отмеченной строке, поскольку в этой точке выполнения существует еще одна ссылка, которая будет использоваться для доступа к одному и тому же хранилищу.
Если отчет о дефектах 028 сказал, что исходный пример вызвал UB из-за перекрытия между созданием и использованием двух указателей, это сделало бы вещи более понятными, не добавляя "Эффективные типы" или другую такую сложность.
После прочтения многих ответов я чувствую необходимость добавить что-то:
Строгое сглаживание (которое я опишу немного) важно, потому что:
Доступ к памяти может быть дорогостоящим (с точки зрения производительности), поэтому данные обрабатываются в регистре CPU, прежде чем они будут возвращены в физическую память.
Если данные в двух разных регистрах ЦП будут записаны в одно и то же пространство памяти, , мы не можем предсказать, какие данные "выживут" , когда мы с кодом на C.
В сборке, где мы будем вручную загружать и выгружать регистры процессора, мы будем знать, какие данные остаются нетронутыми. Но C (к счастью) абстрагирует эту деталь.
Поскольку два указателя могут указывать на одно и то же место в памяти, это может привести к сложному коду, который обрабатывает возможные конфликты.
Этот дополнительный код медленный и повреждает производительность, поскольку он выполняет дополнительные операции чтения/записи в памяти, которые медленнее и (возможно) не нужны.
Правило Strict aliasing позволяет избежать избыточного машинного кода в случаях, когда должно быть безопасным предположить, что два указателя не указывают на один и тот же блок памяти (см. также restrict
ключевое слово).
Строгое сглаживание позволяет с уверенностью предположить, что указатели на разные типы указывают на разные местоположения в памяти.
Если компилятор замечает, что два указателя указывают на разные типы (например, int *
и a float *
), предполагается, что адрес памяти отличается и не будет защищен от столкновений с адресами памяти, что приводит к ускорению машинного кода.
Например:
Предположим, что следующая функция:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Чтобы обрабатывать случай, в котором a == b
(оба указателя указывают на одну и ту же память), нам нужно заказать и протестировать способ загрузки данных из памяти в регистры процессора, поэтому код может закончиться это:
загрузите a
и b
из памяти.
добавить a
в b
.
сохранить b
и перезагрузить a
.
(сохранение из регистров CPU в память и загрузка из памяти в регистр CPU).
добавить b
в a
.
сохранить a
(из регистра CPU) в память.
Шаг 3 очень медленный, потому что ему нужно получить доступ к физической памяти. Однако для защиты от экземпляров, где a
и b
указывают на один и тот же адрес памяти, требуется защита.
Строгое сглаживание позволит нам предотвратить это, сообщив компилятору, что эти адреса памяти отличаются друг от друга (что в этом случае позволит еще больше оптимизировать, что не может быть выполнено, если указатели разделяют адрес памяти).
Это можно передать компилятору двумя способами, используя разные типы, чтобы указать на. то есть:.
void merge_two_numbers(int *a, long *b) {...}
Использование ключевого слова restrict
. то есть:.
void merge_two_ints(int * restrict a, int * restrict b) {...}
Теперь, выполнив правило Strict Aliasing, можно избежать шага 3, и код будет работать значительно быстрее.
Фактически, добавив ключевое слово restrict
, вся функция может быть оптимизирована для:
загрузите a
и b
из памяти.
добавить a
в b
.
сохранить результат как до a
, так и b
.
Эта оптимизация не могла быть выполнена раньше, из-за возможного столкновения (где a
и b
были бы утроены, а не удвоены).
b
(не перезагружая его) и перезагружая a
. Надеюсь, теперь стало понятнее.
Строгое сглаживание не позволяет использовать разные типы указателей для одних и тех же данных.
Эта статья должна помочь вам разобраться в проблеме подробно.
int
и структура, которая содержит int
).
Технически в C++ правило строгого сглаживания, вероятно, никогда не применимо.
Обратите внимание на определение оператора косвенности (*):
Оператор унарного * выполняет косвенное обращение: выражение, к которому оно применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, относящееся к объекту или функции, к которой относится выражение.
Также из определения glvalue
Значение gl - выражение, оценка которого определяет идентичность объекта, (... snip)
Таким образом, в любой четко определенной трассировке программы glvalue ссылается на объект. Таким образом, так называемое строгое правило сглаживания не применяется. Возможно, это не то, что хотели дизайнеры.
c
иc++faq
.