Неопределенный старший порядок uint64_t при сдвиге и маскировании 32-битных значений

0

У меня есть неопределенное поведение в кажущейся безобидной функции, которая анализирует double значение из буфера. Я читаю double в две половинки, потому что я достаточно уверен, что языковой стандарт говорит, что сдвиг значений char действителен только в 32-битном контексте.

inline double ReadLittleEndianDouble( const unsigned char *buf )
{
    uint64_t lo = (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0];
    uint64_t hi = (buf[7] << 24) | (buf[6] << 16) | (buf[5] << 8) | buf[4];
    uint64_t val = (hi << 32) | lo;
    return *(double*)&val;
}

Поскольку я храню 32-битные значения в 64-битных переменных lo и hi, я разумно ожидаю, что 32-разрядные значения этих переменных будут всегда 0x00000000. Но иногда они содержат 0xffffffff или другой 0xffffffff мусор.

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

uint64_t val = ((hi & 0xffffffffULL) << 32) | (lo & 0xffffffffULL);

Кроме того, он работает, если я маскирую во время назначения вместо этого:

uint64_t lo = ((buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0]) & 0xffffffff;
uint64_t hi = ((buf[7] << 24) | (buf[6] << 16) | (buf[5] << 8) | buf[4]) & 0xffffffff;

Я хотел бы знать, почему это необходимо. Все, что я могу придумать, чтобы объяснить это, заключается в том, что мой компилятор делает все смещение и объединение для lo и hi непосредственно на 64-битных регистрах, и я мог бы ожидать неопределенного поведения в 32-битных разрядах высокого порядка, если это так.

Может кто-то, пожалуйста, подтвердите мои подозрения или иным образом объясните, что здесь происходит, и прокомментируйте, какой (если есть) из моих двух решений предпочтительнее?

  • 3
    Комментарий Кита Томпсона об удаленном ответе объяснил проблему: оператор << не применяется к char операнду. Целочисленные преобразования применяются к операндам, поэтому char повышается до int (или, возможно, до unsigned int в действительно странной реализации). Правый операнд (не более 24) не «больше или равен числу битов в расширенном левом операнде»; проблема в том, что foo << 24 может переполниться. Приведения к unsigned должно быть достаточно, если int и unsigned int имеют ширину не менее 32 бит. uint64_t к uint64_t , вероятно, чище.
  • 0
    Я думаю, что @TonyD правильно. Кроме того, в зависимости от компилятора и процессора, некоторые версии могут компилироваться в bswap а другие - нет. Я не мог размышлять о том, что испускает компилятор все же.
Показать ещё 5 комментариев
Теги:

1 ответ

3
Лучший ответ

Если вы попытаетесь перенести char или unsigned char вы оставите себя во власти стандартных целых рекламных акций. Вам лучше отказаться от ценностей самостоятельно, прежде чем пытаться их переместить. Если вы это сделаете, вам не нужно отделять нижнюю и верхнюю половинки.

inline double ReadLittleEndianDouble( const unsigned char *buf )
{
    uint64_t val = ((uint64_t)buf[7] << 56) | ((uint64_t)buf[6] << 48) | ((uint64_t)buf[5] << 40) | ((uint64_t)buf[4] << 32) |
                   ((uint64_t)buf[3] << 24) | ((uint64_t)buf[2] << 16) | ((uint64_t)buf[1] << 8) | (uint64_t)buf[0];
    return *(double*)&val;
}

Все это необходимо только в том случае, если процессор имеет большой энтитизм или если буфер не может быть правильно выровнен для архитектуры ЦП, в противном случае вы можете значительно упростить это:

    return *(double*)buf;
  • 2
    *(double *)buf нарушает строгое правило алиасинга

Ещё вопросы

Сообщество Overcoder
Наверх
Меню