Является ли он корректным, чтобы держать смещенный указатель, до тех пор, пока вы его не разыскиваете?
У меня есть код 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:
- Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен 68) для ссылочного типа поведение не определено. [...]
Он ничего не говорит о разыменовании указателя - даже преобразование имеет неопределенное поведение.
Действительно, модифицированный код, который вы считаете безопасным для ARM, может быть даже не безопасным для Intel - компиляции, как известно, генерируют код для Intel, который может сбой при неизмененном доступе. Хотя не в связанном случае, возможно, просто умный компилятор может принять преобразование в качестве доказательства того, что адрес действительно выровнен и использует специализированный код для memcpy
.
Выравнивание в сторону, ваш первый отрывок также страдает от строгого нарушения псевдонимов. C11 6.5p7:
- Объект должен иметь сохраненное значение, доступ к которому можно получить только с помощью выражения 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));