Разбор машинного слова на байты

Иногда при работе с данными нужно разобрать 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