Каковы последствия этого определения директивы препроцессора? - PullRequest
0 голосов
/ 30 января 2020

Я не вижу смысла, но похоже, что это функция:

#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

Мне нужно понять, что означает эта строка, но я не могу. Насколько я понимаю, вы могли бы поставить что-то вроде:

int *my_ptr = 1234;
int var;
var = GET_BE2(my_ptr); 

Это, вероятно, супер неправильно, но я просто хотел четко заявить, что я не понимаю. Я также не могу сказать, что делает

   ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

. ptr не упоминает, что мы работаем с массивом, так как же мы можем использовать []? Тогда я могу сказать, что мы сдвигаем 8 бит влево и или , вероятно, каковы 8 следующих битов.

1 Ответ

4 голосов
/ 31 января 2020

Анализ макроса

Написанный макрос предназначен для использования в качестве функции.

#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. Это обеспечивает правильность типа. Это позволяет полностью избежать проблем с макросом. И поскольку эта функция достаточно мала, чтобы компилятор почти наверняка смог встроить код, она бесплатна по сравнению с версией макроса.

...