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

У меня есть код C, который анализирует упакованные/неадаптированные двоичные данные, поступающие из сети.

Этот код был/работает отлично под Intel/x86, но когда я скомпилировал его под ARM, он часто падал.

Преступник, как вы могли догадаться, был негласным указателем - в частности, код синтаксического анализа привел бы к таким сомнительным вещам:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]);  // misaligned access -- can crash under ARM!

... что, очевидно, не собирается летать на ARM-land, поэтому я изменил его, чтобы выглядеть более как это:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord));  // slower but ARM-safe

Мой вопрос (с точки зрения языка-юриста) заключается в следующем: мой подход "ARM-fixed" хорошо определен в соответствии с правилами языка C?

Мое беспокойство заключается в том, что, возможно, даже просто иметь неправильный-int32_t-указатель может быть достаточно, чтобы вызывать неопределенное поведение, даже если я никогда не разглажу его напрямую. (Если мое беспокойство действительно, я думаю, что я мог бы исправить эту проблему, изменив тип pNextWord от (const int32_t *) до (const char *), но я бы предпочел не делать этого, если это действительно необходимо сделать, поскольку это означающее выполнение некоторой арифметики указателя-шага вручную)

Ответ 1

Нет, новый код по-прежнему имеет неопределенное поведение. C11 6.3.2.3p7:

  1. Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен 68) для ссылочного типа поведение не определено. [...]

Он ничего не говорит о разыменовании указателя - даже преобразование имеет неопределенное поведение.


Действительно, модифицированный код, который вы считаете безопасным для ARM, может быть даже не безопасным для Intel - компиляции, как известно, генерируют код для Intel, который может сбой при неизмененном доступе. Хотя не в связанном случае, возможно, просто умный компилятор может принять преобразование в качестве доказательства того, что адрес действительно выровнен и использует специализированный код для memcpy.


Выравнивание в сторону, ваш первый отрывок также страдает от строгого нарушения псевдонимов. C11 6.5p7:

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

Поскольку массив buf[2048] статически типизирован, каждый элемент является char, и поэтому эффективными типами элементов являются char; вы можете получить доступ к содержимому массива только как символы, а не как int32_t s.

Т.е. даже

int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);

имеет неопределенное поведение.

Ответ 2

Чтобы безопасно разобрать многобайтовое целое по всем компиляторам/платформам, вы можете извлечь каждый байт и собрать их в целое число в соответствии с именем endian. Например, чтобы прочитать 4-байтовое целое число из буфера big-endian:

uint8_t* buf = any address;

uint32_t val = 0;
uint32_t  b0 = buf[0];
uint32_t  b1 = buf[1];
uint32_t  b2 = buf[2];
uint32_t  b3 = buf[3];

val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;

Ответ 3

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

void copy_uint32(uint32_t *dest, uint32_t *src)
{
  memcpy(dest, src, sizeof (uint32_t));
}

Если и dest и src поддерживают 32-разрядные выровненные адреса, вышеуказанная функция может быть оптимизирована для одной загрузки и одного хранилища даже на платформах, которые не поддерживают несвязанные обращения. Однако, если функция была объявлена для принятия аргументов типа void*, такая оптимизация не была бы разрешена на платформах, где неглавные 32-битные обращения будут вести себя иначе, чем последовательность байтовых обращений, сдвигов и бит-действий.

Ответ 4

Как упоминалось в ответе Антти Хаапалы, просто преобразование указателя в другой тип, когда результирующий указатель неправильно выровнен, вызывает неопределенное поведение в соответствии с разделом 6.3.2.3p7 стандарта C.

Ваш модифицированный код использует pNextWord для перехода к memcpy, где он преобразуется в void *, поэтому вам даже не нужна переменная типа uint32_t *. Просто передайте адрес первого байта в буфере, который вы хотите прочитать, с memcpy. Тогда вам не нужно беспокоиться о выравнивании вообще.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));