Оптимизация GCC с помощью -Os неправильно предполагает, что указатель равен NULL

Недавно мы перешли на более позднюю версию GCC, и она оптимизировала целую функцию и заменила ее кодом ловушки «доступ к нулевому указателю» при оптимизации размера. Глядя на godbolt, проблема возникла в GCC 11.1 при оптимизации с помощью -Os. Оптимизация с помощью -O2 и -O3 работает нормально, даже с -fstrict-aliasing.

Упрощенно код выглядит так (ссылка godbolt):

#include <inttypes.h>
#include <stddef.h>
#include <string.h>

typedef struct bounds_s {
  uint8_t *start;
  uint8_t *end;
} bounds_s_t;

static void reserve_space(bounds_s_t *bounds, size_t len, uint8_t **element)
{
  if (bounds->start + len > bounds->end) {
    return;
  }
  
  *element = bounds->start;
  bounds->start += len;
}

void bug(uint8_t *buffer, size_t size)
{
  bounds_s_t bounds;
  uint32_t *initialize_this;
  
  initialize_this = NULL;
  bounds.start = buffer;
  bounds.end = buffer + size;

  reserve_space(&bounds, sizeof(*initialize_this), (uint8_t **)&initialize_this);

  uint32_t value = 1234;
  memcpy(initialize_this, &value, sizeof(*initialize_this));
}

И приводит к следующей сборке:

bug:
        xor     eax, eax
        mov     DWORD PTR ds:0, eax
        ud2

Какая оптимизация заставляет GCC думать, что переменная initialize_this имеет значение NULL? Единственное, что мне приходит в голову, — это нарушение строгих правил псевдонимов. Но может ли приведение типов двойных указателей от uint32_t ** к uint8_t ** действительно быть здесь проблемой и привести к таким тяжелым последствиям?

memcpy по сути предназначен для *initialize_this = 1234 с проверкой выравнивания.

Konstantin W 05.08.2024 16:17

Вы компилируете с помощью -Wall и/или -Wstrict-aliasing и/или -Wstrict-aliasing=3? Рискованно компилировать с включенной оптимизацией строгого псевдонимов, но с отключенными соответствующими предупреждениями.

John Bollinger 05.08.2024 16:17

Решает ли проблему компиляция с -fno-strict-aliasing? Уровни -Os, -O2 и -O3 включают -fstrict-aliasing, поэтому явное включение его в сочетании с одним из них не дает вам никакой новой информации, но его отключение может помочь.

John Bollinger 05.08.2024 16:23

Оптимизация с помощью -O2 и -O3 работает нормально, даже с -fstrict-aliasing. «Я не заметил никаких сбоев. Сегодня. В моем ограниченном тестировании. Прямо сейчас. Во всяком случае, пока». это не то же самое, что «работает нормально».

Andrew Henle 05.08.2024 17:24

@JohnBollinger Конечно, добавление -fno-strict-aliasing решает проблему. Но я ожидал, что эта «оптимизация» также повлияет на -O2 и -O3. Вот почему вопрос здесь в первую очередь, я не был уверен в происхождении ошибки.

Konstantin W 05.08.2024 17:42

То, что -fno-strict-aliasing действительно решает проблему, было бы важной информацией для включения в вопрос. Именно поэтому мой комментарий здесь. Я не вижу никакого «конечно» в этом отношении, по крайней мере, без предварительного анализа кода, чтобы найти нарушение строгого псевдонимов, которое действительно существует. И обратите внимание, что проблема не в преобразовании указателя, о котором вы упоминаете, а в способе использования преобразованного результата в сочетании с другими факторами.

John Bollinger 05.08.2024 17:47
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
6
93
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

*element = bounds->start; нарушает строгий псевдоним:

  • Поскольку element имеет тип uint8_t **, *element имеет тип uint8_t *, поэтому это сохраняется в *element с типом uint8_t *.
  • На данный момент element имеет адрес initialize_this, у которого объявлен тип и, следовательно, эффективный тип uint32_t *.
  • Доступ к uint32_t * с типом uint8_t * не соответствует ни одному из правил псевдонимов в C 2018 6.5 7.
  • В GCC -fstrict-aliasing включается -Os.
  • Таким образом, компилятору разрешено сделать вывод, поскольку он не видит записи в тип, который может иметь псевдоним uint32_t * после initialize_this = NULL;, что initialize_this не изменяется по сравнению с нулевым указателем.

Кроме того, в коде нет средств контроля, гарантирующих, что значение bounds->start является адресом, правильно выровненным по отношению к uint32_t.

Эти проблемы можно исправить:

  • Вызывающий reserve_space также должен выполнить требование выравнивания, которое можно вычислить при вызове с использованием _Alignof (uint32_t *) (и, с учетом ожидаемого будущего стандарта C, _Alignof (typeof (initialize_this))). reserve_space следует добавлять байты заполнения по мере необходимости, чтобы начальный адрес был кратен этому требованию.
  • Вместо параметра типа uint8_t **, reserve_space должен возвращать void *, указывающий на зарезервированное пространство. Вызывающая процедура может затем присвоить это значение initialize_this, и неявное преобразование присваивания автоматически преобразует его в правильный тип.

В коде также не указано происхождение buffer. Если это динамически выделяемое пространство, то после правильной настройки initialize_this, как описано выше, *initialize_this можно использовать как обычный uint32_t. В частности, нет необходимости использовать memcpy для копирования в него значения; его можно установить с помощью *initialize_this = 1234;. Однако если buffer создан каким-либо другим способом, например, объявленным массивом uint8_t, то проблемы с псевдонимами могут остаться.

Помимо непосредственных проблем с псевдонимами и выравниванием указателей, может возникнуть еще один набор таких проблем, если была предпринята попытка использовать *initialize_this для доступа к содержимому предоставленного буфера, в зависимости от того, на что на самом деле указывает buffer. В примере нет такого доступа, но разумно предположить, что он был вдохновлен кодом, который содержит.

John Bollinger 05.08.2024 17:12

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