Что такое строгое правило псевдонимов? - PullRequest
738 голосов
/ 19 сентября 2008

Когда спрашивают о распространенном неопределенном поведении в C , души более просветленные, чем я, ссылались на правило строгого наложения имен. О чем они говорят?

Ответы [ 11 ]

530 голосов
/ 19 сентября 2008

Типичная ситуация, с которой вы сталкиваетесь со строгими проблемами с наложением имен, - это наложение структуры (например, сообщения устройства / сети) на буфер размера слова вашей системы (например, указатель на uint32_t с или uint16_t с). Когда вы накладываете структуру на такой буфер или буфер на такую ​​структуру с помощью приведения указателя, вы можете легко нарушить строгие правила наложения имен.

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

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 выглядит несколько непоследовательным в своей способности давать псевдонимы предупреждениям, иногда давая нам дружеское предупреждение, а иногда нет).

Чтобы понять, почему это поведение не определено, нам нужно подумать о том, что правило строгого алиасинга покупает компилятор. По сути, с этим правилом не нужно думать о вставке инструкций для обновления содержимого buff при каждом запуске цикла. Вместо этого, при оптимизации с некоторыми досадно необоснованными предположениями о псевдонимах, он может опустить эти инструкции, загрузить buff[0] и buff[1] в регистры ЦП один раз перед запуском цикла и ускорить тело цикла. До введения строгого псевдонима компилятор должен был жить в состоянии паранойи, когда содержимое 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:

  • тип, совместимый с эффективным типом объекта,
  • квалифицированная версия типа, совместимого с эффективным типом объекта,
  • тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,
  • агрегатный или объединенный тип, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения), или
  • тип символа.
226 голосов
/ 19 сентября 2008

Лучшее объяснение, которое я нашел, - Майк Актон, Понимание строгого алиасинга . Он немного сфокусирован на разработке PS3, но в основном это только GCC.

Из статьи:

"Строгое псевдонимы - это предположение, сделанное компилятором C (или C ++), что разыменование указателей на объекты разных типов никогда не будет ссылаться на одну и ту же ячейку памяти (то есть псевдонимы друг друга)"

То есть, если у вас есть int*, указывающий на некоторую память, содержащую int, а затем вы указываете float* на эту память и используете ее как float, вы нарушаете правило. Если ваш код не соблюдает это, оптимизатор компилятора, скорее всего, сломает ваш код.

Исключением из правила является char*, которому разрешено указывать на любой тип.

129 голосов
/ 10 августа 2011

Это строгое правило псевдонимов, которое можно найти в разделе 3.10 стандарта C ++ 03 (другие ответы дают хорошее объяснение, но ни один не предоставил само правило):

Если программа пытается получить доступ к сохраненному значению объекта через значение lvalue, отличное от одного из следующих типов, поведение не определено:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим cv-квалифицированной версии динамического типа объекта,
  • агрегатный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или unsigned char type.

C ++ 11 и C ++ 14 формулировка (изменения выделены):

Если программа пытается получить доступ к сохраненному значению объекта через glvalue другого, чем один из следующих типов, поведение не определено:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) динамическому типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим cv-квалифицированной версии динамического типа объекта,
  • агрегатный или объединенный тип, который включает один из вышеупомянутых типов среди своих элементов или нестатических элементов данных (включая, рекурсивно, элемент или нестатический элемент данных из субагрегатное или автономное объединение),
  • тип, который является (возможно, квалифицированным cv) типом базового класса динамического типа объекта,
  • a char или unsigned char type.

Два изменения были небольшими: glvalue вместо lvalue и уточнение случая совокупности / объединения.

Третье изменение дает более сильную гарантию (ослабляет строгое правило алиасинга): Новая концепция похожих типов , которые теперь безопасны для псевдонима.


Также формулировка C (C99; ISO / IEC 9899: 1999 6.5 / 7; точно такая же формулировка используется в ISO / IEC 9899: 2011 §6.5 ¶7):

Объект должен иметь свое сохраненное значение, доступное только через lvalue выражение, которое имеет один из следующих типов 73) или 88) :

  • тип, совместимый с эффективным типом объекта,
  • квалифицированная версия типа, совместимого с эффективным типом объект,
  • тип, который является типом со знаком или без знака, соответствующим эффективный тип объекта,
  • тип, который является типом со знаком или без знака, соответствующим квалифицированная версия действующего типа объекта,
  • агрегированный или объединенный тип, который включает один из вышеупомянутых типы среди его членов (включая, рекурсивно, член субагрегат или объединенный союз), или
  • тип символа.

73) или 88) Цель этого списка - указать те обстоятельства, при которых объект может или не может иметь псевдоним.

51 голосов
/ 08 июля 2018

Примечание

Это выдержка из моего «Что такое строгое правило алиасинга и почему нас это волнует?» описание.

Что такое строгий псевдоним?

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

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

Чтобы лучше понять, почему нас это волнует, мы обсудим проблемы, возникающие при нарушении строгих правил псевдонимов, так как типизирование наказаний часто применяется, так как обычные методы, используемые в типах наказаний, часто нарушают строгие правила псевдонимов и как правильно вводить pun.

Предварительные примеры

Давайте посмотрим на некоторые примеры, затем мы сможем поговорить о том, что конкретно говорится в стандарте (ах), рассмотрим некоторые дополнительные примеры, а затем посмотрим, как избежать строгого наложения псевдонимов и выявить нарушения, которые мы пропустили. Вот пример, который не должен удивлять ( живой пример ):

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

Что может и не ожидаться, но совершенно верно, так как мы вызвали неопределенное поведение. float не может корректно использовать псевдоним int объекта. Поэтому оптимизатор может предположить, что константа 1 , сохраненная при разыменовании i , будет возвращаемым значением, так как сохранение через 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?

Стандарт 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

Проект стандарта 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 , в котором указано тип символа .

Что такое Type Punning

Мы дошли до этой точки, и нам может быть интересно, зачем нам нужен псевдоним? Обычно ответом является type pun , часто используемые методы нарушают строгие правила наложения имен.

Иногда мы хотим обойти систему типов и интерпретировать объект как другой тип. Это называется type 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 ) ;

Как мы видели ранееэто неверный псевдоним, поэтому мы вызываем неопределенное поведение. Но традиционно компиляторы не пользовались преимуществами строгих правил псевдонимов, и этот тип кода обычно просто работал, разработчики, к сожалению, привыкли делать такие вещи. Распространенный альтернативный метод для обозначения типов - через объединения, который допустим в 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 ++, и некоторые считают, что целью объединений является исключительно реализация реализаций типов, и считают, что использование объединений для наказания типов является злоупотреблением.

Как правильно печатать Pun?

Стандартный метод для type punning в C и C ++ - memcpy . Это может показаться немного сложным, но оптимизатор должен распознавать использование memcpy для типа punning , оптимизировать его и генерировать регистр для регистрации перемещения. Например, если мы знаем, что 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 или union для type punning . Изучая сгенерированный код, мы видим, что он использует только регистр mov ( live Compiler Explorer Пример ).

C ++ 20 и bit_cast

В C ++ 20 мы можем получить bit_cast ( реализация доступна в ссылке из предложения ), которая дает простой и безопасный способ ввода слов, а также может использоваться в контекст constexpr.

Ниже приведен пример использования bit_cast для ввода каламбура unsigned int до float , ( смотрите вживую ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

В случае, когда типы To и From не имеют одинакового размера, нам необходимо использовать промежуточную структуру struct15. Мы будем использовать структуру, содержащую массив символов sizeof (unsigned int) ( предполагает, что 4-байтовое целое число без знака int ) будет иметь тип From и unsigned int как To type.:

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 ( see it live ):

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-интерпретатор, статический анализатор, который исчерпывающе анализирует программу для большого подмножества языка 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 , который в настоящее время находится в разработке. Это дезинфицирующее средство добавляет информацию о проверке типов в сегменте теневой памяти и проверяет доступы, чтобы определить, не нарушают ли они правила псевдонимов. Инструмент потенциально должен быть в состоянии отследить все нарушения псевдонимов, но может иметь большие накладные расходы во время выполнения.

42 голосов
/ 20 июня 2011

Строгое псевдонимы относятся не только к указателям, но и к ссылкам, я написал статью об этом для вики-сайта Boost для разработчиков, и он был настолько хорошо принят, что я превратил его в страницу на своем консультационном веб-сайте. Это полностью объясняет, что это такое, почему это так сильно смущает людей и что с этим делать. Строгий Aliasing White Paper . В частности, это объясняет, почему объединения являются рискованным поведением для C ++, и почему использование memcpy является единственным переносимым исправлением как для C, так и для C ++. Надеюсь, что это полезно.

33 голосов
/ 14 мая 2013

Как дополнение к тому, что уже писал Дуг Т., здесь простой тестовый пример, который, вероятно, запускает его с помощью 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» в функции «check». Из-за этого компилятор оптимизирует 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 полностью ушло из кода ассемблера.

16 голосов
/ 19 сентября 2008

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

14 голосов
/ 27 апреля 2017

Согласно обоснованию C89, авторы стандарта не хотели требовать, чтобы компиляторы давали код, подобный:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

должно потребоваться для перезагрузки значения x между оператором присваивания и возврата, чтобы учесть вероятность того, что p может указывать на x, а присвоение *p может, следовательно, изменить значение x. Понятие о том, что компилятор должен иметь право предполагать, что псевдоним не будет в ситуациях, подобных вышеприведенным , не вызывает сомнений.

К сожалению, авторы C89 написали свое правило таким образом, что, если читать буквально, заставит даже следующую функцию вызывать неопределенное поведение:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

, поскольку он использует lvalue типа int для доступа к объекту типа struct S, а int не относится к типам, которые могут использоваться для доступа к struct S. Поскольку было бы абсурдно рассматривать любое использование элементов структур и объединений, не относящихся к символьному типу, как неопределенное поведение, почти каждый признает, что существуют, по крайней мере, некоторые обстоятельства, когда lvalue одного типа может использоваться для доступа к объекту другого типа. , К сожалению, Комитет по стандартам C не смог определить, каковы эти обстоятельства.

Большая часть проблемы является результатом отчета о дефектах # 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 гласит, что программа вызывает неопределенное поведение, потому что действие записи члена объединения типа "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 из-за совпадения между созданием и использованием двух указателей, что сделало бы вещи более ясными без добавления «эффективных типов» или других подобных сложностей.

10 голосов
/ 24 декабря 2017

После прочтения многих ответов я чувствую необходимость что-то добавить:

Строгий псевдоним (который я опишу чуть позже) важен, потому что :

  1. Доступ к памяти может быть дорогим (с точки зрения производительности), поэтому данные обрабатываются в регистрах ЦП перед записью обратно в физическую память.

  2. Если данные в двух разных регистрах ЦП будут записаны в одно и то же пространство памяти, мы не можем предсказать, какие данные «выживут» при кодировании в C.

    В сборке, где мы кодируем загрузку и выгрузку регистров ЦП вручную, мы узнаем, какие данные остаются нетронутыми. Но С (к счастью) абстрагируется от этой детали.

Поскольку два указателя могут указывать на одно и то же место в памяти, это может привести к сложному коду, который обрабатывает возможные коллизии .

Этот дополнительный код работает медленно и снижает производительность , так как он выполняет операции чтения / записи дополнительной памяти, которые являются одновременно более медленными и (возможно) ненужными.

Правило Strict aliasing позволяет нам избежать избыточного машинного кода в тех случаях, когда он должен быть безопасным, предполагая, что два указателя не указывают на один и тот же блок памяти (см. также ключевое слово restrict).

Строгий псевдоним утверждает, что можно с уверенностью предположить, что указатели на разные типы указывают на разные места в памяти.

Если компилятор заметит, что два указателя указывают на разные типы (например, int * и float *), он будет считать, что адрес памяти отличается, и не будет защищать от памяти конфликты адресов, приводящие к ускорению машинного кода.

Например :

Давайте предположим следующую функцию:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Чтобы обработать случай, когда a == b (оба указателя указывают на одну и ту же память), нам нужно упорядочить и протестировать способ загрузки данных из памяти в регистры ЦП, чтобы код мог выглядеть так: это:

  1. загрузка a и b из памяти.

  2. добавьте a к b.

  3. сохранить b и перезагрузить a.

    (сохранить из регистра ЦП в память и загрузить из памяти в регистр ЦП).

  4. добавить b к a.

  5. сохранить a (из регистра ЦП) в памяти.

Шаг 3 очень медленный, потому что ему нужен доступ к физической памяти. Однако требуется защита от случаев, когда a и b указывают на один и тот же адрес памяти.

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

  1. Это можно сказать компилятору двумя способами, используя разные типы для указания. i.e.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Использование ключевого слова restrict. i.e.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Теперь, выполнив правило строгого псевдонима, можно избежать шага 3, и код будет выполняться значительно быстрее.

Фактически, добавив ключевое слово restrict, можно оптимизировать всю функцию:

  1. загрузка a и b из памяти.

  2. добавить a к b.

  3. сохранить результат как в a, так и в b.

Эту оптимизацию нельзя было сделать раньше из-за возможного столкновения (где a и b будут утроены, а не удвоены).

6 голосов
/ 19 сентября 2008

Строгое псевдонимы не разрешают разные типы указателей на одни и те же данные.

Эта статья должна помочь вам понять проблему в деталях.

...