Вопрос

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

Это было полезно?

Решение

Типичная ситуация, с которой вы сталкиваетесь со строгими проблемами псевдонимов, — это наложение структуры (например, сообщения устройства/сети) на буфер размером слова вашей системы (например, указатель на 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, и он может решить, загружать или не загружать buff снова.Если SendMessage является частью другого API, который скомпилирован отдельно, вероятно, у него есть инструкции для загрузки содержимого buff.Опять же, возможно, вы работаете на 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:

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

Другие советы

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

Из статьи:

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

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

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

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

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

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

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

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

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

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

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


Так же С формулировка (С99;ИСО/МЭК 9899:1999 6.5/7;точно такая же формулировка используется в ISO/IEC 9899:2011 §6.5 ¶7):

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

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

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

Примечание

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

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

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

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

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

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

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

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

У нас есть интервал* указывая на память, занятую интервал и это действительный псевдоним.Оптимизатор должен предположить, что назначения через IP может обновить значение, занимаемое Икс.

В следующем примере показано использование псевдонимов, приводящее к неопределенному поведению (живой пример):

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?
}

В функции фу мы берем интервал* и плавать*, в этом примере мы вызываем фу и установите оба параметра так, чтобы они указывали на одну и ту же ячейку памяти, которая в этом примере содержит интервал.Обратите внимание reinterpret_cast сообщает компилятору, что выражение должно обрабатываться так, как если бы оно имело тип, указанный в параметре шаблона.В этом случае мы говорим ему обработать выражение &Икс как будто у него был тип плавать*.Мы можем наивно ожидать результата второго расчет быть 0 но с включенной оптимизацией с использованием -O2 и gcc, и clang дают следующий результат:

0
1

Этого можно не ожидать, но это вполне допустимо, поскольку мы вызвали неопределенное поведение.А плавать не может корректно использовать псевдоним интервал объект.Поэтому оптимизатор может предположить, что константа 1 сохраняется при разыменовании я будет возвращаемым значением, поскольку магазин через ж не могло существенно повлиять на интервал объект.Включение кода в 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 знает, что плавать не может использовать псевдонимы и интервал и оптимизирует нагрузку я.

Теперь к книге правил

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

Что говорит стандарт C11?

А С11 стандарт говорит следующее в разделе 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 имеет расширение и также что позволяет назначить беззнаковое целое* к интервал* даже если они не являются совместимыми типами.

— тип, который является знаковым или беззнаковым типом, соответствующим уточненной версии действующего типа объекта,

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
}

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

Что такое каламбур

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

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

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

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, но неопределенное поведение на С++ (посмотреть живой пример):

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

Как правильно писать каламбур?

Стандартный метод для каламбур как в C, так и в C++ есть память.Это может показаться немного жестковатым, но оптимизатор должен учитывать использование память для каламбур оптимизируйте его и сгенерируйте регистр для перемещения регистра.Например, если мы знаем int64_t такого же размера, как двойной:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

мы можем использовать память:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

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

С++20 и bit_cast

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

Ниже приведен пример использования бит_каст набрать каламбур беззнаковое целое число к плавать, (увидеть это вживую):

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

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

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 ;
}

К сожалению, нам нужен этот промежуточный тип, но это текущее ограничение бит_каст.

Обнаружение строгих нарушений псевдонимов

У нас не так уж много хороших инструментов для обнаружения строгого псевдонимов в C++, но те инструменты, которые у нас есть, способны обнаружить некоторые случаи нарушений строгого псевдонимов и некоторые случаи несогласованной загрузки и сохранения.

gcc, используя флаг -fstrict-алиасинг и -Wstrict-алиасинг может выявить некоторые случаи, хотя и не без ложных срабатываний/негативов.Например, в следующих случаях будет генерироваться предупреждение в 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=адрес

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

Для C у нас уже есть все инструменты, а также tis-interpreter, статический анализатор, который исчерпывающе анализирует программу на наличие большого подмножества языка C.Учитывая версии C предыдущего примера, где использование -fstrict-алиасинг пропустил один случай(увидеть это вживую)

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.

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

Строгое псевдонимирование относится не только к указателям, оно также влияет на ссылки. Я написал об этом статью для вики-сайта разработчиков boost, и она была так хорошо принята, что я превратил ее в страницу на своем консультационном веб-сайте.Там полностью объясняется, что это такое, почему это так смущает людей и что с этим делать. Технический документ о строгом псевдониме.В частности, он объясняет, почему объединения являются рискованным поведением для C++ и почему использование memcpy — единственное исправление, переносимое как на C, так и на C++.Надеюсь, это полезно.

В качестве дополнения к тому, что Дуг Т.Уже написано, вот простой тестовый пример, который, вероятно, запускает его с помощью GCC:

чек.с

#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 полностью исчезло из ассемблерного кода.

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

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

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

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

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

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

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

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

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

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

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

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

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

А Строгое правило псевдонимов позволяет нам избежать избыточного машинного кода. в тех случаях, когда это должно быть можно с уверенностью предположить, что два указателя не указывают на один и тот же блок памяти (см. также 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. Об этом можно сообщить компилятору двумя способами: используя разные типы для указания.то есть:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Используя restrict ключевое слово.то есть:

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

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

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

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

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

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

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

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

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

Технически в C++ строгое правило псевдонимов, вероятно, никогда не применимо.

Обратите внимание на определение косвенности (* оператор):

Унарный оператор * выполняет косвенное обращение:выражение, к которому оно применяется, должно быть указателем на тип объекта или указатель на тип функции и результатом является lvalue, ссылающееся на объект или функция на что указывает выражение.

Также из определение glvalue

GLVALUE - это выражение, оценка которой определяет идентичность объекта (... SNIP)

Таким образом, в любой четко определенной трассировке программы значение glvalue ссылается на объект. Таким образом, так называемое строгое правило псевдонимов никогда не применяется. Возможно, это не то, чего хотели дизайнеры.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top