Преобразование порядка байтов без использования неопределенного поведения

Я использую C для чтения файла изображения .png, и если вы не знакомы с форматом кодирования PNG, полезные целочисленные значения кодируются в файлах .png в виде 4-байтовых целых чисел с прямым порядком байтов.

Мой компьютер - это машина с прямым порядком байтов, поэтому для преобразования из прямого порядка байтов uint32_t, который я прочитал из файла с fread(), в формат с прямым порядком байтов, который понимает мой компьютер, я использовал эту небольшую функцию, которую я написал:

#include <stdint.h>

uint32_t convertEndian(uint32_t val){
  union{
    uint32_t value;
    char bytes[sizeof(uint32_t)];
  }in,out;
  in.value=val;
  for(int i=0;i<sizeof(uint32_t);++i)
    out.bytes[i]=in.bytes[sizeof(uint32_t)-1-i];
  return out.value;
}

Это прекрасно работает в моей среде x86_64 UNIX, gcc компилируется без ошибок или предупреждений даже с флагом -Wall, но я уверен, что полагаюсь на поведение undefined и каламбур, которые могут не работать так же хорошо в других системах.

Есть ли стандартная функция, которую я могу вызвать, которая может надежно преобразовать целое число с прямым порядком байтов в то, что понимает собственная машина, или, если нет, есть ли альтернативный более безопасный способ сделать это преобразование?


person Willis Hershey    schedule 21.05.2020    source источник
comment
Вы можете использовать старые добрые сдвиги для беззнаковых типов. Не уверен насчет подписанных, но это определенно не может быть невозможным.   -  person Oppen    schedule 21.05.2020
comment
А как насчет htonl() и ntohl()?   -  person Fred Larson    schedule 21.05.2020
comment
Я думаю, вы имеете в виду ntohl()   -  person Barmar    schedule 21.05.2020
comment
@Barmar: Ага, буквы перепутались.   -  person Fred Larson    schedule 21.05.2020
comment
htonl () и ntohl () полагаются на файл arpa/inet.h, который недоступен в системах, отличных от UNIX.   -  person Willis Hershey    schedule 21.05.2020
comment
@FredLarson Я думаю, что это делает ненужное предположение о порядке байтов сети   -  person Eugene Sh.    schedule 21.05.2020
comment
Используйте uint8_t bytes вместо char bytes. На редких машинах, где char не 8 бит, код не компилируется, а не компилируется и выполняется неправильно.   -  person chux - Reinstate Monica    schedule 21.05.2020
comment
@EugeneSh. не совсем. Названия функций плохие, но в спецификации это ясно: network == big для них. И сама программа, OP, говорит, что сначала проверяет порядок байтов, предоставленный файлом PNG.   -  person Oppen    schedule 21.05.2020
comment
Обратите внимание, что convertEndian() выполняет обратный порядок байтов, а не преобразует целое число с прямым порядком байтов в единицу на собственной машине. Я ожидал, что big_to_host32() будет лучшим подходом.   -  person chux - Reinstate Monica    schedule 21.05.2020
comment
Из точки зрения именования ntohl() подразумевает от сети до длинной, но сеть подразумевает большой, даже если в каком-то подключенном сетевом протоколе используется прямой порядок байтов, а длинный подразумевает 32-битный, даже если long 64-битный. Мне нравятся такие, как be32toh() больше.   -  person chux - Reinstate Monica    schedule 22.05.2020
comment
Ваш код не полагается на UB, но он полагается на то, что ваш компьютер является прямым порядком байтов.   -  person M.M    schedule 22.05.2020
comment
к сожалению, в стандарте C нет определения порядка байтов во время компиляции, но GCC предоставляет предопределенные макросы   -  person M.M    schedule 22.05.2020


Ответы (4)


Я не вижу настоящего UB в коде OP.

Проблемы с переносимостью: да.

"каламбур, который может не работать так же хорошо в других системах" не является проблемой для кода C OP, но может вызвать проблемы с другими языками.


А как насчет большого (PNG) порядка байтов для размещения?

Извлеките байты по адресу (от младшего адреса, у которого есть MSByte до самого высокого адреса, у которого есть LSByte - «большой» порядок байтов) и сформируйте результат со смещенными байтами.

Что-то типа:

uint32_t Endian_BigToHost32(uint32_t val) {
  union {
    uint32_t u32;
    uint8_t u8[sizeof(uint32_t)]; // uint8_t insures a byte is 8 bits.
  } x = { .u32 = val };
  return 
      ((uint32_t)x.u8[0] << 24) |
      ((uint32_t)x.u8[1] << 16) |
      ((uint32_t)x.u8[2] <<  8) |
                 x.u8[3];
}

Совет: во многих библиотеках есть функция, специфичная для реализации. Пример be32toh.

person chux - Reinstate Monica    schedule 21.05.2020
comment
Эта функция неправильно меняет порядок байтов ввода, и даже если это так, она все равно обращается к неустановленному члену объединения, что является тем же потенциально опасным поведением, что и код в исходном вопросе. - person Willis Hershey; 24.05.2020
comment
@WillisHershey он по-прежнему обращается к неустановленному члену союза - ›неверно. Что это за неустановленный член? Вы забыли = { .u32 = val };? Это действительно меняет порядок байтов, если мой компьютер является машиной с прямым порядком байтов. - person chux - Reinstate Monica; 24.05.2020
comment
Я ошибся, ваша функция работает. Ситуация, которую я пытался избежать, заключалась в использовании объединений, чтобы притвориться, что 4-байтовое целое число является 4-байтовым целым числом. Единственными улучшениями здесь являются замена char на uint8_t и удаление цикла, что является шагом в правильном направлении, но не решает проблему полностью. - person Willis Hershey; 24.05.2020
comment
@WillisHershey Здесь нет притворства, только спецификация C и .png совместимый код и без UB . Файлы .png содержат 4-байтовые целые числа с прямым порядком байтов в определенном порядке байтов -big. Этот код точно преобразует это в локальный uint32_t. Какую часть проблемы вы считаете нерешенной полностью? - person chux - Reinstate Monica; 24.05.2020

ИМО, было бы лучше читать из байтов в желаемый формат, чем, по-видимому, memcpy'ing a uint32_t, а затем внутренне манипулировать uint32_t. Код может выглядеть так:

uint32_t read_be32(uint8_t *src)   // must be unsigned input
{
     return (src[0] * 0x1000000u) + (src[1] * 0x10000u) + (src[2] * 0x100u) + src[3];
}

Довольно легко ошибиться в подобном коде, поэтому убедитесь, что вы получили его от высокопоставленных пользователей SO ????. Вы можете часто видеть альтернативное предложение return (src[0] << 24) + (src[1] << 16) + (src[2] << 8) + src[3];, которое вызывает неопределенное поведение, если src[0] >= 128 из-за переполнения целого числа со знаком, из-за неудачного правила, согласно которому целочисленные рекламные акции переводят uint8_t в подписанное int. А также вызывает неопределенное поведение в системе с 16-битным int из-за больших сдвигов.

Современные компиляторы должны быть достаточно умными, чтобы оптимизировать это, например сборка, созданная clang little-endian:

read_be32:                              # @read_be32
    mov     eax, dword ptr [rdi]
    bswap   eax
    ret

Однако я вижу, что gcc 10.1 создает гораздо более сложный код, это кажется неожиданной ошибкой оптимизации, которая упускается из виду.

person M.M    schedule 21.05.2020

Это решение не полагается на доступ к неактивным членам объединения, но вместо этого полагается на операции сдвига битов целого числа без знака, которые могут переносимо и безопасно преобразовывать из прямого порядка байтов в обратный порядок байтов или наоборот

#include <stdint.h>

uint32_t convertEndian32(uint32_t in){
  return ((in&0xffu)<<24)|((in&0xff00u)<<8)|((in&0xff0000u)>>8)|((in&0xff000000u)>>24);
}
person Willis Hershey    schedule 24.05.2020

Этот код считывает uint32_t из указателя на uchar_t в хранилище с прямым порядком байтов, независимо от порядка байтов в вашей архитектуре. (Код действует так, как если бы он читал число с базой 256)

uint32_t read_bigend_int(uchar_t *p, int sz)
{
    uint32_t result = 0;
    while(sz--) {
        result <<= 8;   /* multiply by base */
        result |= *p++; /* and add the next digit */
    }
}

если вы позвоните, например:

int main()
{
    /* ... */
    uchar_t buff[1024];
    read(fd, buff, sizeof buff);

    uint32_t value = read_bigend_int(buff + offset, sizeof value);
    /* ... */
}
person Luis Colorado    schedule 24.05.2020