Picture 1

Во время анализа кода, PVS-Studio выполняет анализ потока данных и оперирует значениями переменных. Значения берутся из констант или выводятся из условных выражений. Мы называем их виртуальными значениями. Недавно мы улучшали их для работы с multi-character-константами и это стало поводом для создания нового диагностического правила.

Введение


Multi-character-литерал является implementation-defined, поэтому различные компиляторы могут кодировать эти литералы по-разному. К примеру, GCC и Clang задают значение, основываясь на порядке символов в литерале, тогда как MSVC перемещает их в зависимости от типа символа (обычный или escape).

Например, литерал 'T\x65s\x74' будет закодирован разными способами, в зависимости от компилятора. Похожую логику пришлось добавить и в анализатор. В итоге мы сделали новое диагностическое правило V1039 для выявления таких литералов в коде. Такие литералы представляют опасность в кросс-платформенных проектах, использующих несколько компиляторов для сборки.

Диагностика V1039


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

#include <stdio.h>

void foo(int c)
{
  if (c == 'T\x65s\x74')                       // <= V1039
  {
    printf("Compiled with GCC or Clang.\n");
  }
  else
  {
    printf("It's another compiler (for example, MSVC).\n");
  }
}

int main(int argc, char** argv)
{
  foo('Test');
  return 0;
}

Программа, скомпилированная разными компиляторами, напечатает разные сообщения на экран.

Для проекта, использующего определенный компилятор, это не будет заметно, однако при портировании могут возникнуть проблемы, поэтому следует заменить такие литералы простыми числовыми константами, к примеру, 'Test' поменять на 0x54657374.

Чтобы продемонстрировать разницу между компиляторами, напишем небольшую утилиту, где взяты последовательности из 3-х и 4-х символов, например, 'GHIJ' и 'GHI', и выводятся на экран их представление в памяти после компиляции.

Код утилиты:

#include <stdio.h>

typedef int char_t;

void PrintBytes(const char* format, char_t lit)
{
  printf("%20s : ", format);

  const unsigned char *ptr = (const unsigned char*)&lit;
  for (int i = sizeof(lit); i--;)
  {
    printf("%c", *ptr++);
  }
  putchar('\n');
}

int main(int argc, char** argv)
{
  printf("Hex codes are: G(%02X) H(%02X) I(%02X) J(%02X)\n",'G','H','I','J');
  PrintBytes("'GHIJ'", 'GHIJ');
  PrintBytes("'\\x47\\x48\\x49\\x4A'", '\x47\x48\x49\x4A');
  PrintBytes("'G\\x48\\x49\\x4A'", 'G\x48\x49\x4A');
  PrintBytes("'GH\\x49\\x4A'", 'GH\x49\x4A');
  PrintBytes("'G\\x48I\\x4A'", 'G\x48I\x4A');
  PrintBytes("'GHI\\x4A'", 'GHI\x4A');
  PrintBytes("'GHI'", 'GHI');
  PrintBytes("'\\x47\\x48\\x49'", '\x47\x48\x49');
  PrintBytes("'GH\\x49'", 'GH\x49');
  PrintBytes("'\\x47H\\x49'", '\x47H\x49');
  PrintBytes("'\\x47HI'", '\x47HI');
  return 0;
}

Вывод утилиты, скомпилированной Visual C++:

Hex codes are: G(47) H(48) I(49) J(4A)
              'GHIJ' : JIHG
  '\x47\x48\x49\x4A' : GHIJ
     'G\x48\x49\x4A' : HGIJ
        'GH\x49\x4A' : JIHG
        'G\x48I\x4A' : JIHG
           'GHI\x4A' : JIHG
               'GHI' : IHG
      '\x47\x48\x49' : GHI
            'GH\x49' : IHG
         '\x47H\x49' : HGI
            '\x47HI' : IHG

Вывод утилиты, скомпилированной GCC или Clang:

Hex codes are: G(47) H(48) I(49) J(4A)
              'GHIJ' : JIHG
  '\x47\x48\x49\x4A' : JIHG
     'G\x48\x49\x4A' : JIHG
        'GH\x49\x4A' : JIHG
        'G\x48I\x4A' : JIHG
           'GHI\x4A' : JIHG
               'GHI' : IHG
      '\x47\x48\x49' : IHG
            'GH\x49' : IHG
         '\x47H\x49' : IHG
            '\x47HI' : IHG

Заключение


Диагностика V1039 добавлена в анализатор PVS-Studio версии 7.03, релиз которой недавно состоялся. Скачать последнюю версию анализатора можно на странице загрузки.



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. The dangers of using multi-character constants

Комментарии (12)


  1. Amomum
    26.06.2019 12:39

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


    1. iliazeus
      27.06.2019 06:52

      cppreference говорит, что они остались как наследие языка B.


      SvyatoslavMC, встречали ли вы такие константы в проектах, которые проверяли с PVS-Studio?


      1. SvyatoslavMC Автор
        27.06.2019 07:12

        Сейчас работаю с кодом проекта Haiku OS. Там нашлось около сотни таких мест.


  1. staticmain
    26.06.2019 13:36

    следует заменить такие литералы простыми числовыми константами, к примеру, 'Test' поменять на 0x54657374.


    А зачем вы даете такой вредный совет?
    В свой продукт вы добавили поддержку проверки по MISRA C, которая ориентирована на встраиваемые системы (там даже stdio запрещено). Встраиваемые системы — это почти всегда ARM. ARM — это отличный от Intel порядок байт. Поэтому ваша константа 0x54657374 либо станет 0x74736554 либо такой станет проверяемая переменная.


    1. dr_begemot
      27.06.2019 10:26
      +2

      Причем тут это? Вы путаете запись числа и представление в памяти в зависимости от big или little endian систем. Число 0xA1A2 на всех типах систем всегда должно быть равным 41378, вне зависимости от порядка байт в памяти, все остальное это ошибки программиста.


  1. ivan386
    26.06.2019 13:38

    А зачем они их переворачивают? Получается только у VC++ в одном случае(если всё за ecape'ить) строка останется в памяти в том же порядке.


    1. eisaev
      26.06.2019 14:49

      Наоборот же:

      for (int i = sizeof(lit); i--;)

      Оно задумано «в обратном порядке» и только GCC/Clang делают то, что попросили.


      Не. Основная логика то здесь:
      printf("%c", *ptr++);

      И тогда действительно становится жутко…


      1. ivan386
        28.06.2019 01:52

        Выше подсказали ответ:


        Multicharacter constants were inherited by C from the B programming language. Although not specified by the C standard, compilers implement multicharacter constants as specified in B: the values of each char in the constant initialize successive bytes of the resulting integer, in big-endian zero-padded right-adjusted order, e.g. the value of '\1' is 0x00000001 and the value of '\1\2\3\4' is 0x01020304.

        Соответственно строка '\1\2\3\4' переводится в число 0x01020304 а порядок байт чисел в памяти уже зависит от процессора. В данном случае ('GHIJ' : JIHG) это Little Endian. Но случай с VC++ это конечно не объясняет.


  1. loginsin
    27.06.2019 16:22

    Помнится в одной из статей PVS (и, кажется, даже, не одной) проскакивал лозунг: не сокращайте код, компилятор умнее и сам сделает как надо. И тут в статье вижу:

      for (int i = sizeof(lit); i--;)

    Объединить условие и декремент — это здорово, красиво и свежо, однако противоречит вашим же базовым принципам, а кроме того еще и опасно: если переменная при инициализации примет отрицательное значение, то будет бяка (я не про sizeof в конкретном примере, а про выстрелы себе в ногу людей, которые примут на вооружение подобный bad-practice-код).
    Еще пример:
    printf("%c", *ptr++);

    Я смотрю на код и пытаюсь понять: мы инкрементируем поинтер или значение, на которое он ссылается? Дай-ка я возьму сейчас учебник и найду: приоритетнее разыменование или инкремент?

    Я точно читаю статью от PVS?


    1. SvyatoslavMC Автор
      27.06.2019 16:31

      а про выстрелы себе в ногу людей, которые примут на вооружение подобный bad-practice-код
      Ну так мы в основном и специализируемся на публикации bad-practice-кода :D


      1. loginsin
        27.06.2019 16:42

        … а также объясняете его проблемы и приводите правильный вариант.
        Здесь же раскрывается суть одной проблемы и неявно сверху закладывается еще парочка.


  1. SvyatoslavMC Автор
    27.06.2019 16:30

    (del)