Недавно мы перешли на более позднюю версию 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 **
действительно быть здесь проблемой и привести к таким тяжелым последствиям?
Вы компилируете с помощью -Wall
и/или -Wstrict-aliasing
и/или -Wstrict-aliasing=3
? Рискованно компилировать с включенной оптимизацией строгого псевдонимов, но с отключенными соответствующими предупреждениями.
Решает ли проблему компиляция с -fno-strict-aliasing
? Уровни -Os
, -O2
и -O3
включают -fstrict-aliasing
, поэтому явное включение его в сочетании с одним из них не дает вам никакой новой информации, но его отключение может помочь.
Оптимизация с помощью -O2
и -O3
работает нормально, даже с -fstrict-aliasing
. «Я не заметил никаких сбоев. Сегодня. В моем ограниченном тестировании. Прямо сейчас. Во всяком случае, пока». это не то же самое, что «работает нормально».
@JohnBollinger Конечно, добавление -fno-strict-aliasing
решает проблему. Но я ожидал, что эта «оптимизация» также повлияет на -O2
и -O3
. Вот почему вопрос здесь в первую очередь, я не был уверен в происхождении ошибки.
То, что -fno-strict-aliasing
действительно решает проблему, было бы важной информацией для включения в вопрос. Именно поэтому мой комментарий здесь. Я не вижу никакого «конечно» в этом отношении, по крайней мере, без предварительного анализа кода, чтобы найти нарушение строгого псевдонимов, которое действительно существует. И обратите внимание, что проблема не в преобразовании указателя, о котором вы упоминаете, а в способе использования преобразованного результата в сочетании с другими факторами.
*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.-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
. В примере нет такого доступа, но разумно предположить, что он был вдохновлен кодом, который содержит.
memcpy
по сути предназначен для*initialize_this = 1234
с проверкой выравнивания.