Разбор машинного слова на байты
Иногда при работе с данными нужно разобрать 16-ти или 32-х битную переменную на байты. Или наоборот, собрать слово из отдельныых байтов. Для этого используют:
- сдвиг переменной и наложение маски
- доступ к байтам переменной по указателю
Разбор слова на байты при помощи сдвига и наложения маски
Использует свойство неявно сужающего преобразования (приведения) беззнакового целого длинного типа при присваивании в более короткий: всё что не влезло - отбрасывается. Например: uint32_t b = 0x12345678;. В результате неявного преобразования при присваивании: uint8_t a = b; получим результат: a = 0x78;. Старшие байты слова были отброшены. Сдвигая слово вправо на байт (8 бит), готовим второй байт для присваивания. И так далее...
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
int main()
{
uint8_t a[4] = {};
uint32_t b = 0x12345678;
a[0] = b;
a[1] = b>>8;
a[2] = b>>16 & 0xff;
a[3] = (uint8_t)(b>>24 & 0xff);
printf("a[0] = %x \n", a[0]);
printf("a[1] = %x \n", a[1]);
printf("a[2] = %x \n", a[2]);
printf("a[3] = %x \n", a[3]);
getch();
}
Для элемента a[2] используется наложение маски 0xFF (обнуляется всё кроме младшего байта). Компилятору это необязательно, но показывает другому программисту, что сужающее преобразование сделано сознательно. Для элемента a[3] используется явное приведение типа к uint8_t. При отсутствии явного указания компилятор неявно приведёт тип к нужному.
Сборка 32-х битной переменной из отдельных байтов
Для сборки переменной используются операции сдвига влево и побитового сложения.
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
int main()
{
uint8_t a[4] = {0x12, 0x34, 0x56, 0x78};
uint32_t b = 0;
b = (uint32_t)a[0]<<24 | (uint32_t)a[1]<<16 | (uint32_t)a[2]<<8 | a[3];
printf("b = %x \n", b);
getch();
}
Явное преобразование типа (uint32_t) может не использоваться для определённых компиляторов. Например: clang в C++ Builder/Win10-64 при любом сдвиге влево неявно расширяет однобайтную переменную до 4-х байтов (до int, но не более).
Доступ к байтам переменной по указателю
Суть этого метода: представить многобайтовую переменную в виде массива байтов и получить доступ к этим элементам массива при помощи оператора [ ] - индекс массива (доступ к элементам массива по индексу).
Пусть есть некоторая 32-х битная переменная типа беззнаковое целое: uint32_t a = 0x12345678.
1) Возьмем адрес переменной a: &a. Этот адрес указывает на первый байт 32-х битного беззнакового целого.
2) Приведём этот адрес к типу указатель на байт (8-ми битное беззнаковое целое) (uint8_t *)&a.
3) Теперь при помощи оператора доступа к элементам массива по индексу [ ] можно получить доступ к любому байту 4-х байтной (32-х битной) исходной переменной: ((uint8_t *)&a)[0]. В нашем случае индекс массива находится в диапазоне от 0 до 3. Дополнительные круглые скобки, объединяющие преобразование адреса, необходимы для соблюдения порядка операций, т.к. оператор индекса массива имеет приоритет над преобразованием типа переменной.
Пример программы на языке C для Windows в C++ Builder:
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
int main()
{
uint32_t b = 0x12345678;
printf("b[0] = %x \n", ((uint8_t *)&b)[0]);
printf("b[1] = %x \n", ((uint8_t *)&b)[1]);
printf("b[2] = %x \n", ((uint8_t *)&b)[2]);
printf("b[3] = %x \n", ((uint8_t *)&b)[3]);
getch();
}
Подключаемые библиотеки: <stdio.h> - для функции printf(), <conio.h> - для функции getch() (окно терминала с результатом выполнения программы не закрывается сразу, а ждет ввода любого символа с клавиатуры), <stdint.h> - содержит определение типов целых чисел фиксированной длины, не зависящей от аппаратной платформы, в частности: uint32_t.
Результат выполнения программы:
b[0] = 78
b[1] = 56
b[2] = 34
b[3] = 12
Видно, что в начале массива с нулевым индексом (с меньшим адресом в памяти) содержится младший байт слова. А старший байт слова имеет больший адрес в памяти. Такой порядок байтов (endianness) в данном случае (на х86 компьютере) называется little-endian. Факт разного порядка байтов в слове необходимо учитывать при разработке.
Пример программы на Си для определения порядка байтов:
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
int main()
{
uint16_t b = 0x0001;
printf("%s-endian\n", *((uint8_t *)&b) ? "little" : "big");
getch();
}
В этом примере оператор доступа к первому элементу массива с индексом 0: ((uint8_t *)&a)[0] заменён на оператор разыменовывания указателя на массив: *((uint8_t *)&a), что по определению является одним и тем же.
Сборка 32-х битной переменной из отдельных байтов при помощи указателей
При "сборке" 32-х битной переменной из отдельных байтов делаем "всё так же, только наоборот".
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
int main()
{
uint8_t a[4] = {0x12, 0x34, 0x56, 0x78};
uint32_t b;
((uint8_t *)&b)[0] = a[0];
((uint8_t *)&b)[1] = a[1];
((uint8_t *)&b)[2] = a[2];
((uint8_t *)&b)[3] = a[3];
printf("b = %x \n", b);
getch();
}
Каков ожидаемый результат выполнения этой программы? Для порядка следования байтов litte-endian младший байт слова b[0] имеет меньший адрес и располагается справа при выводе на экран. Ожидаемый результат: 0x78563412. Проверим:
b = 78563412