Анализ макроса
Написанный макрос предназначен для использования в качестве функции.
#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])
Вероятное намерение - преобразовать два последовательных байтовых значения в 16-разрядное целое число при условии, что байты представлены в порядке с прямым порядком байтов, поэтому ptr[0]
является более значимым байтом, а ptr[1]
является менее значимым байтом.
Хотя документация не показана, вы должны ее пропустить указатель на массив из (как минимум) двух целых чисел, который затем будет подвергаться битовой обработке для получения результата. Поскольку это макрос, нет явного ограничения типа. Следовательно, он может быть вызван с любым из:
signed char ptr0[2] = { 0x23, 0x37 };
signed short ptr1[2] = { 0x23, 0x37 };
signed int ptr2[2] = { 0x23, 0x37 };
signed long ptr3[2] = { 0x23, 0x37 };
signed long long ptr4[2] = { 0x23, 0x37 };
unsigned char ptr5[2] = { 0x23, 0x37 };
unsigned short ptr6[2] = { 0x23, 0x37 };
unsigned int ptr7[2] = { 0x23, 0x37 };
unsigned long ptr8[2] = { 0x23, 0x37 };
unsigned long long ptr9[2] = { 0x23, 0x37 };
Учитывая указанные значения данных, он даже даст одинаковый результат от всех этих.
Проблемы с макросом
Однако, если какое-либо из значений со знаком было отрицательным, или если (преобразованное) отрицательное значение было присвоено второму элементу (показан как 0x37
выше) любого из типов без знака, кроме unsigned char
(таким образом ptr6
.. ptr9
), тогда вы не получите ожидаемого.
Нет никаких сомнений в том, что подразумевается, что ptr
должен быть указателем на два смежных значения unsigned char
. Затем макрос создает значение, в котором значение в ptr[0]
представляет собой старшие 8 битов значения uint16_t
, а значение в ptr[1]
представляет собой младшие 8 битов значения uint16_t
. Результат будет 0x2337.
Если типы больше char
или тип signed char
(или простой тип char
со знаком), и если значение в ptr[1]
отрицательно, вы получаете результаты, отличные от запланированных.
Демонстрация недостатков макроса
Вот тестовая программа (с довольно болезненным повторением в ней - но избавление от повторения также болезненно, и не стоит двух показанных тестовых случаев):
#include <stdio.h>
#include <stdint.h>
#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])
static void test1(void)
{
signed char ptr0[2] = { 0x23, 0x37 };
signed short ptr1[2] = { 0x23, 0x37 };
signed int ptr2[2] = { 0x23, 0x37 };
signed long ptr3[2] = { 0x23, 0x37 };
signed long long ptr4[2] = { 0x23, 0x37 };
unsigned char ptr5[2] = { 0x23, 0x37 };
unsigned short ptr6[2] = { 0x23, 0x37 };
unsigned int ptr7[2] = { 0x23, 0x37 };
unsigned long ptr8[2] = { 0x23, 0x37 };
unsigned long long ptr9[2] = { 0x23, 0x37 };
unsigned long long result;
printf("Two positive elements:\n");
result = GET_BE2(ptr0);
printf("ptr0[0] = 0x%.4hhX ptr0[1] = 0x%.16hhX ", ptr0[0], ptr0[1]);
printf("signed char = 0x%.16llX\n", result);
result = GET_BE2(ptr1);
printf("ptr1[0] = 0x%.4hX ptr1[1] = 0x%.16hX ", ptr1[0], ptr1[1]);
printf("signed short = 0x%.16llX\n", result);
result = GET_BE2(ptr2);
printf("ptr2[0] = 0x%.4X ptr2[1] = 0x%.16X ", ptr2[0], ptr2[1]);
printf("signed int = 0x%.16llX\n", result);
result = GET_BE2(ptr3);
printf("ptr3[0] = 0x%.4lX ptr3[1] = 0x%.16lX ", ptr3[0], ptr3[1]);
printf("signed long = 0x%.16llX\n", result);
result = GET_BE2(ptr4);
printf("ptr4[0] = 0x%.4llX ptr4[1] = 0x%.16llX ", ptr4[0], ptr4[1]);
printf("signed long long = 0x%.16llX\n", result);
result = GET_BE2(ptr5);
printf("ptr5[0] = 0x%.4hhX ptr5[1] = 0x%.16hhX ", ptr5[0], ptr5[1]);
printf("unsigned char = 0x%.16llX\n", result);
result = GET_BE2(ptr6);
printf("ptr6[0] = 0x%.4hX ptr6[1] = 0x%.16hX ", ptr6[0], ptr6[1]);
printf("unsigned short = 0x%.16llX\n", result);
result = GET_BE2(ptr7);
printf("ptr7[0] = 0x%.4X ptr7[1] = 0x%.16X ", ptr7[0], ptr7[1]);
printf("unsigned int = 0x%.16llX\n", result);
result = GET_BE2(ptr8);
printf("ptr8[0] = 0x%.4lX ptr8[1] = 0x%.16lX ", ptr8[0], ptr8[1]);
printf("unsigned long = 0x%.16llX\n", result);
result = GET_BE2(ptr9);
printf("ptr9[0] = 0x%.4llX ptr9[1] = 0x%.16llX ", ptr9[0], ptr9[1]);
printf("unsigned long long = 0x%.16llX\n", result);
}
static void test2(void)
{
signed char ptr0[2] = { 0x23, -0x00000037 };
signed short ptr1[2] = { 0x23, -0x00003A37 };
signed int ptr2[2] = { 0x23, -0x004B3A37 };
signed long ptr3[2] = { 0x23, -0x5C4B3A37 };
signed long long ptr4[2] = { 0x23, -0x5C4B3A37 };
unsigned char ptr5[2] = { 0x23, -0x00000037 };
unsigned short ptr6[2] = { 0x23, -0x00003A37 };
unsigned int ptr7[2] = { 0x23, -0x4B4B3A37 };
unsigned long ptr8[2] = { 0x23, -0x5C4B3A37 };
unsigned long long ptr9[2] = { 0x23, -0x5C4B3A37 };
unsigned long long result;
printf("One positive element, one negative element:\n");
result = GET_BE2(ptr0);
printf("ptr0[0] = 0x%.4hhX ptr0[1] = 0x%.16hhX ", ptr0[0], ptr0[1]);
printf("signed char = 0x%.16llX\n", result);
result = GET_BE2(ptr1);
printf("ptr1[0] = 0x%.4hX ptr1[1] = 0x%.16hX ", ptr1[0], ptr1[1]);
printf("signed short = 0x%.16llX\n", result);
result = GET_BE2(ptr2);
printf("ptr2[0] = 0x%.4X ptr2[1] = 0x%.16X ", ptr2[0], ptr2[1]);
printf("signed int = 0x%.16llX\n", result);
result = GET_BE2(ptr3);
printf("ptr3[0] = 0x%.4lX ptr3[1] = 0x%.16lX ", ptr3[0], ptr3[1]);
printf("signed long = 0x%.16llX\n", result);
result = GET_BE2(ptr4);
printf("ptr4[0] = 0x%.4llX ptr4[1] = 0x%.16llX ", ptr4[0], ptr4[1]);
printf("signed long long = 0x%.16llX\n", result);
result = GET_BE2(ptr5);
printf("ptr5[0] = 0x%.4hhX ptr5[1] = 0x%.16hhX ", ptr5[0], ptr5[1]);
printf("unsigned char = 0x%.16llX\n", result);
result = GET_BE2(ptr6);
printf("ptr6[0] = 0x%.4hX ptr6[1] = 0x%.16hX ", ptr6[0], ptr6[1]);
printf("unsigned short = 0x%.16llX\n", result);
result = GET_BE2(ptr7);
printf("ptr7[0] = 0x%.4X ptr7[1] = 0x%.16X ", ptr7[0], ptr7[1]);
printf("unsigned int = 0x%.16llX\n", result);
result = GET_BE2(ptr8);
printf("ptr8[0] = 0x%.4lX ptr8[1] = 0x%.16lX ", ptr8[0], ptr8[1]);
printf("unsigned long = 0x%.16llX\n", result);
result = GET_BE2(ptr9);
printf("ptr9[0] = 0x%.4llX ptr9[1] = 0x%.16llX ", ptr9[0], ptr9[1]);
printf("unsigned long long = 0x%.16llX\n", result);
}
int main(void)
{
test1();
test2();
return 0;
}
На MacBook Pro с MacOS Mojave 10.14.6 с G CC 9.2.0 и XCode 11.3.1, вывод из этого:
Two positive elements:
ptr0[0] = 0x0023 ptr0[1] = 0x0000000000000037 signed char = 0x0000000000002337
ptr1[0] = 0x0023 ptr1[1] = 0x0000000000000037 signed short = 0x0000000000002337
ptr2[0] = 0x0023 ptr2[1] = 0x0000000000000037 signed int = 0x0000000000002337
ptr3[0] = 0x0023 ptr3[1] = 0x0000000000000037 signed long = 0x0000000000002337
ptr4[0] = 0x0023 ptr4[1] = 0x0000000000000037 signed long long = 0x0000000000002337
ptr5[0] = 0x0023 ptr5[1] = 0x0000000000000037 unsigned char = 0x0000000000002337
ptr6[0] = 0x0023 ptr6[1] = 0x0000000000000037 unsigned short = 0x0000000000002337
ptr7[0] = 0x0023 ptr7[1] = 0x0000000000000037 unsigned int = 0x0000000000002337
ptr8[0] = 0x0023 ptr8[1] = 0x0000000000000037 unsigned long = 0x0000000000002337
ptr9[0] = 0x0023 ptr9[1] = 0x0000000000000037 unsigned long long = 0x0000000000002337
One positive element, one negative element:
ptr0[0] = 0x0023 ptr0[1] = 0x00000000000000C9 signed char = 0xFFFFFFFFFFFFFFC9
ptr1[0] = 0x0023 ptr1[1] = 0x000000000000C5C9 signed short = 0xFFFFFFFFFFFFE7C9
ptr2[0] = 0x0023 ptr2[1] = 0x00000000FFB4C5C9 signed int = 0xFFFFFFFFFFB4E7C9
ptr3[0] = 0x0023 ptr3[1] = 0xFFFFFFFFA3B4C5C9 signed long = 0xFFFFFFFFA3B4E7C9
ptr4[0] = 0x0023 ptr4[1] = 0xFFFFFFFFA3B4C5C9 signed long long = 0xFFFFFFFFA3B4E7C9
ptr5[0] = 0x0023 ptr5[1] = 0x00000000000000C9 unsigned char = 0x00000000000023C9
ptr6[0] = 0x0023 ptr6[1] = 0x000000000000C5C9 unsigned short = 0x000000000000E7C9
ptr7[0] = 0x0023 ptr7[1] = 0x00000000B4B4C5C9 unsigned int = 0x00000000B4B4E7C9
ptr8[0] = 0x0023 ptr8[1] = 0xFFFFFFFFA3B4C5C9 unsigned long = 0xFFFFFFFFA3B4E7C9
ptr9[0] = 0x0023 ptr9[1] = 0xFFFFFFFFA3B4C5C9 unsigned long long = 0xFFFFFFFFA3B4E7C9
Для простых случаев, когда значения в элементах массива достаточно малы, чтобы поместиться в диапазоне 0..SCHAR_MAX (127), выходные данные соответствуют ожидаемым, поскольку при повышении значений нет никаких знаковых битов, чтобы усложнить проблемы.
Почему макрос терпит неудачу
Если значения не так малы, выражение имеет неожиданные результаты.
#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])
Давайте добавить еще парен тезисы к этому:
#define GET_BE2(ptr) ((((uint16_t)(ptr)[0]) << 8) | (ptr)[1])
Оператору сдвига дается повышенное значение для операнда LHS. Это означает, что ptr[0]
сначала преобразуется в значение uint16_t
, а затем преобразуется в int
(I am в предположении, что «нормальная» машина, где sizeof(int) != sizeof(uint16_t)
. Результат смещен влево 8 биты RHS оператора |
также повышается до int
, два значения int
объединяются и дают результат. Обратите внимание, что преобразование знака signed char
в int
увеличивает значение. (Я принимаю при условии представления дополнения 2; если вас беспокоит дополнение 1 или величина знака, адаптируйте тестовый код et c в соответствии с вашей средой.)
Эти факторы приводит к тому, что в операндах для операторов shift и or устанавливаются всевозможные посторонние биты, что приводит к «неожиданным» результатам.
Исправление макроса
Для обеспечения безопасности макроса код макроса должен быть написан более аккуратно. Он может либо маскироваться с 0xFF
, либо приводиться к uint8_t
(или unsigned char
).
#define GET_BE2(ptr) (uint16_t)((((ptr)[0] & 0xFF) << 8) | ((ptr)[1] & 0xFF))
#define GET_BE2(ptr) ((((uint8_t)(ptr)[0]) << 8) | (uint8_t)(ptr)[1])
Используя любой из них, вывод одинаков и сам соответствует:
Two positive elements:
ptr0[0] = 0x0023 ptr0[1] = 0x0000000000000037 signed char = 0x0000000000002337
ptr1[0] = 0x0023 ptr1[1] = 0x0000000000000037 signed short = 0x0000000000002337
ptr2[0] = 0x0023 ptr2[1] = 0x0000000000000037 signed int = 0x0000000000002337
ptr3[0] = 0x0023 ptr3[1] = 0x0000000000000037 signed long = 0x0000000000002337
ptr4[0] = 0x0023 ptr4[1] = 0x0000000000000037 signed long long = 0x0000000000002337
ptr5[0] = 0x0023 ptr5[1] = 0x0000000000000037 unsigned char = 0x0000000000002337
ptr6[0] = 0x0023 ptr6[1] = 0x0000000000000037 unsigned short = 0x0000000000002337
ptr7[0] = 0x0023 ptr7[1] = 0x0000000000000037 unsigned int = 0x0000000000002337
ptr8[0] = 0x0023 ptr8[1] = 0x0000000000000037 unsigned long = 0x0000000000002337
ptr9[0] = 0x0023 ptr9[1] = 0x0000000000000037 unsigned long long = 0x0000000000002337
One positive element, one negative element:
ptr0[0] = 0x0023 ptr0[1] = 0x00000000000000C9 signed char = 0x00000000000023C9
ptr1[0] = 0x0023 ptr1[1] = 0x000000000000C5C9 signed short = 0x00000000000023C9
ptr2[0] = 0x0023 ptr2[1] = 0x00000000FFB4C5C9 signed int = 0x00000000000023C9
ptr3[0] = 0x0023 ptr3[1] = 0xFFFFFFFFA3B4C5C9 signed long = 0x00000000000023C9
ptr4[0] = 0x0023 ptr4[1] = 0xFFFFFFFFA3B4C5C9 signed long long = 0x00000000000023C9
ptr5[0] = 0x0023 ptr5[1] = 0x00000000000000C9 unsigned char = 0x00000000000023C9
ptr6[0] = 0x0023 ptr6[1] = 0x000000000000C5C9 unsigned short = 0x00000000000023C9
ptr7[0] = 0x0023 ptr7[1] = 0x00000000B4B4C5C9 unsigned int = 0x00000000000023C9
ptr8[0] = 0x0023 ptr8[1] = 0xFFFFFFFFA3B4C5C9 unsigned long = 0x00000000000023C9
ptr9[0] = 0x0023 ptr9[1] = 0xFFFFFFFFA3B4C5C9 unsigned long long = 0x00000000000023C9
Использование встроенная функция
Маловероятно, что те, кто написал макрос, намеревались использовать его с чем-либо, кроме char *
, unsigned char *
или, возможно, signed char *
(хотя вероятно, что signed char *
не было даже считается). Поэтому было бы лучше использовать функцию - предпочтительно функцию inline
- для выполнения этой работы. Это заставляет вас использовать правильный тип (или использовать неверный тип):
static inline uint16_t get_be2(const unsigned char *ptr)
{
return (ptr[0] << 8) | ptr[1];
}
Если по какой-то причине ваш компилятор настолько устарел, что не примет inline
(даже если это было частью стандарта C на протяжении всего нынешнего тысячелетия, такие компиляторы существуют), тогда просто пропустите inline
. Компилятор может даже сделать функцию встроенной самостоятельно; он может видеть, где он используется, потому что он ограничен текущим файлом, и может решить, что имеет смысл избежать накладных расходов при реальном вызове функции. Вот значительно сокращенный тестовый пример - хотя его можно легко перепроектировать, чтобы удалить очень много повторений. Обратите внимание на явное приведение к вызовам с использованием signed char
.
#include <stdio.h>
#include <stdint.h>
static inline uint16_t get_be2(const unsigned char *ptr)
{
return (ptr[0] << 8) | ptr[1];
}
#define GET_BE2(ptr) get_be2(ptr)
static void test1(void)
{
signed char ptr0[2] = { 0x23, 0x37 };
unsigned char ptr5[2] = { 0x23, 0x37 };
unsigned long long result;
printf("Two positive elements:\n");
result = GET_BE2((unsigned char *)ptr0);
printf("ptr0[0] = 0x%.4hhX ptr0[1] = 0x%.16hhX ", ptr0[0], ptr0[1]);
printf("signed char = 0x%.16llX\n", result);
result = GET_BE2(ptr5);
printf("ptr5[0] = 0x%.4hhX ptr5[1] = 0x%.16hhX ", ptr5[0], ptr5[1]);
printf("unsigned char = 0x%.16llX\n", result);
}
static void test2(void)
{
signed char ptr0[2] = { 0x23, -0x00000037 };
unsigned char ptr5[2] = { 0x23, -0x00000037 };
unsigned long long result;
printf("One positive element, one negative element:\n");
result = GET_BE2((unsigned char *)ptr0);
printf("ptr0[0] = 0x%.4hhX ptr0[1] = 0x%.16hhX ", ptr0[0], ptr0[1]);
printf("signed char = 0x%.16llX\n", result);
result = GET_BE2(ptr5);
printf("ptr5[0] = 0x%.4hhX ptr5[1] = 0x%.16hhX ", ptr5[0], ptr5[1]);
printf("unsigned char = 0x%.16llX\n", result);
}
int main(void)
{
test1();
test2();
return 0;
}
Вывод:
Two positive elements:
ptr0[0] = 0x0023 ptr0[1] = 0x0000000000000037 signed char = 0x0000000000002337
ptr5[0] = 0x0023 ptr5[1] = 0x0000000000000037 unsigned char = 0x0000000000002337
One positive element, one negative element:
ptr0[0] = 0x0023 ptr0[1] = 0x00000000000000C9 signed char = 0x00000000000023C9
ptr5[0] = 0x0023 ptr5[1] = 0x00000000000000C9 unsigned char = 0x00000000000023C9
Обычный char
против unsigned char
и signed char
Существует три различных (однобайтовых) типа символов: (обычный) char
, signed char
и unsigned char
. Простой тип char
может быть подписанным или без знака; это решение о реализации, которое должно быть задокументировано. Я не удосужился показать char
в объяснении, потому что он ведет себя так же, как один из signed char
(так он ведет себя на Ма c) или unsigned char
. Однако на практике код часто пишется с использованием простого char
. Если вы измените функцию, чтобы получить простой указатель char
, вы должны убедиться, что он работает правильно, независимо от того, является ли простой тип char
подписанным или неподписанным. В этом случае вы либо преобразуете входящие const char *ptr
в const unsigned char *uptr = (unsigned char *)ptr;
и ссылаетесь на uptr[0]
и uptr[1]
, либо добавляете приведения или маски, как в фиксированных вариантах макроса.
Предпочтительное решение
Используйте функцию inline
. Это обеспечивает правильность типа. Это позволяет полностью избежать проблем с макросом. И поскольку эта функция достаточно мала, чтобы компилятор почти наверняка смог встроить код, она бесплатна по сравнению с версией макроса.