Понимание порядка байтов и его практического применения в Golang

Little Endian и Big Endian — это два широко используемых порядка следования байтов в компьютерных системах, которые определяют, как данные хранятся в памяти и как к ним осуществляется доступ. Понимание этих концепций имеет решающее значение для разработчиков, особенно при работе с низкоуровневым программированием, форматами двоичных файлов или протоколами связи. В этой статье мы обсудим различия между Little Endian и Big Endian, варианты их использования и приведем примеры с использованием языка программирования Go (Golang).

Little Endian против Big Endian

Endian относится к порядку, в котором байты хранятся для многобайтовых типов данных, таких как целые числа или числа с плавающей запятой. Существует два основных способа представления этих значений в памяти:

Little Endian: младший значащий байт (LSB) хранится по самому младшему адресу памяти, а остальные байты хранятся в порядке возрастания значимости. Например, 32-битное целое число 0x12345678 будет сохранено как 0x78, 0x56, 0x34, 0x12.
Рассмотрим пример 32-битного целого числа 0x12345678:

В шестнадцатеричной системе счисления каждая цифра представляет 4 бита. Следовательно, 32-битное целое число состоит из 8 шестнадцатеричных цифр. В данном случае это 8 шестнадцатеричных цифр: 12, 34, 56 и 78.

Когда целое число 0x12345678 сохраняется в памяти с использованием порядка байтов Little Endian, младший байт (0x78) сохраняется первым по самому младшему адресу памяти. Остальные байты хранятся в порядке возрастания значимости, например:

Memory Address: | ... |  n  | n+1 | n+2 | n+3 | ... |
                -------------------------------------
Byte Value:     | ... | 0x78 | 0x56 | 0x34 | 0x12 | ... |

В этой схеме адрес памяти увеличивается слева направо. Самый младший байт (0x78) хранится по младшему адресу памяти n, за ним следуют другие байты в порядке возрастания значимости: 0x56 по адресу n+1, 0x34 по адресу n+2 и, наконец, старший байт (0x12) по адресу n+3.

Big Endian: старший значащий байт (MSB) хранится по наименьшему адресу памяти, а остальные байты хранятся в порядке убывания значимости. В этом случае целое число 0x12345678 будет сохранено как 0x12, 0x34, 0x56, 0x78.

Рассмотрим пример 32-битного целого числа 0x12345678:

В шестнадцатеричной системе счисления каждая цифра представляет 4 бита. Следовательно, 32-битное целое число состоит из 8 шестнадцатеричных цифр. В данном случае это 8 шестнадцатеричных цифр: 12, 34, 56 и 78.

Когда целое число 0x12345678 сохраняется в памяти с использованием порядка байтов Big Endian, старший байт (0x12) сохраняется первым по самому младшему адресу памяти. Остальные байты хранятся в порядке убывания значимости, например:

Memory Address: | ... |  n  | n+1 | n+2 | n+3 | ... |
                -------------------------------------
Byte Value:     | ... | 0x12 | 0x34 | 0x56 | 0x78 | ... |

В этой схеме адрес памяти увеличивается слева направо. Самый значащий байт (0x12) хранится по младшему адресу памяти n, за ним следуют другие байты в порядке убывания значимости: 0x34 по адресу n+1, 0x56 по адресу n+2 и, наконец, младший значащий байт (0x78) по адресу n+3.

Какой из них более удобочитаем для человека

Нотация с обратным порядком байтов обычно считается более удобочитаемой для человека, чем нотация с прямым порядком байтов. Причина этого в том, что порядок байтов Big Endian хранит старший значащий байт (MSB) по наименьшему адресу памяти, а остальные байты — в порядке убывания значимости. Это представление согласуется с тем, как мы читаем и записываем числа в позиционных системах счисления, таких как десятичная система, слева направо, начиная со старшего разряда.

Например, рассмотрим 32-битное шестнадцатеричное целое число 0x12345678. В порядке байтов Big Endian целое число хранится как 0x12, 0x34, 0x56, 0x78. Это тот же порядок, в котором мы естественным образом читаем и пишем число, поэтому людям его легче интерпретировать и понимать.

Напротив, нотация с прямым порядком байтов хранит младший значащий байт (LSB) по самому младшему адресу памяти, а остальные байты — в порядке возрастания значимости. Используя тот же пример, целое число 0x12345678 будет храниться как 0x78, 0x56, 0x34, 0x12 в порядке байтов Little Endian. Этот обратный порядок может быть более сложным для чтения и понимания людьми, поскольку он не соответствует тому, как мы обычно читаем и пишем числа.

Однако важно отметить, что выбор между соглашениями Little Endian и Big Endian является в первую очередь технической проблемой для компьютерных систем, поскольку они обрабатывают хранение и обработку двоичных данных. Для людей разница в удобочитаемости между этими двумя обозначениями менее значительна, поскольку мы обычно работаем с высокоуровневыми представлениями чисел, а не с их форматами хранения на уровне байтов.

Случаи использования

Каждый порядок байтов имеет свои преимущества и недостатки, и выбор зависит от конкретных требований системы или приложения. Некоторые соображения включают:

  1. Little Endian: этот порядок байтов обычно используется в процессорах x86 и ARM, а также в операционных системах Windows и Linux. Little Endian обычно предпочтительнее для математических операций, поскольку он согласуется с тем, как люди выполняют арифметические действия.
  2. Big Endian: Big Endian часто используется в сетевых протоколах, таких как TCP/IP, где данные передаются в согласованном и понятном формате. Он также используется в некоторых процессорных архитектурах, таких как Motorola 68000 и серии IBM POWER.

Примеры Голанга

Пакет «encoding/binary» в Go предоставляет функции для чтения и записи двоичных данных с использованием порядка байтов как Little Endian, так и Big Endian. Вот несколько примеров для демонстрации этих функций:

Запись целого числа в форматах Little Endian и Big Endian:

package main

import (
 "bytes"
 "encoding/binary"
 "fmt"
)

func main() {
 buf := new(bytes.Buffer)
 var num int32 = 0x12345678

 // Write as Little Endian
 binary.Write(buf, binary.LittleEndian, num)
 fmt.Printf("Little Endian: %X\n", buf.Bytes())

 buf.Reset()

 // Write as Big Endian
 binary.Write(buf, binary.BigEndian, num)
 fmt.Printf("Big Endian: %X\n", buf.Bytes())
}

Выход:

Little Endian: 78563412
Big Endian: 12345678

Чтение целого числа из данных с прямым и обратным порядком байтов:

package main

import (
 "bytes"
 "encoding/binary"
 "fmt"
)

func main() {
 littleEndianData := []byte{0x78, 0x56, 0x34, 0x12}
 bigEndianData := []byte{0x12, 0x34, 0x56, 0x78}

 var littleEndianNum, bigEndianNum int32

 // Read as Little Endian
 binary.Read(bytes.NewReader(littleEndianData), binary.LittleEndian, &littleEndianNum)
 fmt.Printf("Little Endian: %X\n", littleEndianNum)

 // Read as Big Endian
 binary.Read(bytes.NewReader(bigEndianData), binary.BigEndian, &bigEndianNum)
 fmt.Printf("Big Endian: %X\n", bigEndianNum)
}

Выход:

Little Endian: 12345678
Big Endian: 12345678

Данные как Little Endian, так и Big Endian считываются и преобразуются в одно и то же целочисленное значение 0x12345678.

Мой опыт работы с Litte и Big Endian

Как бэкэнд-разработчик, работающий с Go, я столкнулся с проблемой при извлечении данных из таблицы SQL Server в приложении сбора измененных данных (CDC). В SQL Server тип UNIQUEIDENTIFIER хранится как LittleEndian. Однако для правильного анализа этого столбца мы ожидали, что необработанные данные будут храниться как BigEndian. Чтобы устранить это несоответствие, мне нужно было переупорядочить необработанные байты, извлеченные из sqlserver нашим приложением cdc, чтобы гарантировать, что значения UUID в исходной и целевой системах были одинаковыми. Вот фрагмент кода, демонстрирующий процесс переупорядочения байтов:

package main

import (
 "encoding/binary"
 "fmt"

 "github.com/google/uuid"
)

func fromLittleEndianGUID(data []byte) (uuid.UUID, error) {
 if len(data) != 16 {
  return uuid.Nil, fmt.Errorf("input byte array should have a length of 16")
 }

 bigEndianData := make([]byte, 16)
 binary.BigEndian.PutUint32(bigEndianData[:4], binary.LittleEndian.Uint32(data[:4]))
 binary.BigEndian.PutUint16(bigEndianData[4:6], binary.LittleEndian.Uint16(data[4:6]))
 binary.BigEndian.PutUint16(bigEndianData[6:8], binary.LittleEndian.Uint16(data[6:8]))
 copy(bigEndianData[8:], data[8:])

 return uuid.FromBytes(bigEndianData)
}

func main() {
 littleEndianGUIDData := []byte{0x56, 0x69, 0xB9, 0x0C, 0xCF, 0xA2, 0xE9, 0x46, 0x9C, 0x69, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}

 u, err := fromLittleEndianGUID(littleEndianGUIDData)
 if err != nil {
  fmt.Printf("Error parsing GUID: %v\n", err)
  return
 }

 fmt.Printf("Parsed LittleEndian GUID: %s\n", u.String())
}
Parsed LittleEndian GUID: 0cb96956-a2cf-46e9-9c69-123456789abc

И пример переупорядочения байтов из библиотеки go, предназначенной для работы с sqlserver:

https://github.com/denisenkom/go-mssqldb/blob/master/uniqueidentifier.go

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу