>
>
>
Находим ошибки в коде компилятора GCC с…

Андрей Карпов
Статей: 672

Находим ошибки в коде компилятора GCC с помощью анализатора PVS-Studio

Я регулярно проверяю различные открытые проекты, чтобы продемонстрировать возможности статического анализатора кода PVS-Studio (C, C++, C#). Настало время компилятора GCC. Бесспорно, GCC — это очень качественный и оттестированный проект, поэтому найти в нём хотя бы несколько ошибок уже большое достижение для любого инструмента. К моей радости, PVS-Studio справился с этой задачей. Никто не застрахован от опечаток и невнимательности. Именно поэтому PVS-Studio может стать вашей дополнительной линией обороны на фронте бесконечной войны с багами.

GCC

GNU Compiler Collection (обычно используется сокращение GCC) — набор компиляторов для различных языков программирования, разработанный в рамках проекта GNU. GCC является свободным программным обеспечением, распространяется фондом свободного программного обеспечения на условиях GNU GPL и GNU LGPL и является ключевым компонентом GNU toolchain. Проект написан на языке C и C++.

Компилятор GCC имеет хорошие встроенные диагностики, помогающие выявлять многие ошибки на этапе компиляции. Естественно, GCC собирается с помощью GCC и, соответственно, может выявлять ошибки в собственном коде. Дополнительно исходный код GCC проверяется с помощью анализатора Coverity. Да и вообще, думаю GCC проверялся энтузиастами с помощью многих анализаторов и других инструментов. Это делает поиск ошибок в GCC большим испытанием для анализатора кода PVS-Studio.

Для анализа была взята trunk версия из git-репозитория:

git://gcc.gnu.org/git/gcc.git

(git) commit:

00a7fcca6a4657b6cf203824beda1e89f751354b svn+ssh://gcc.gnu.org/svn/gcc/trunk@238976

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

Предвидя дискуссию

Как я сказал во введении, я считаю GCC проектом с высоким качеством кода. Уверен, многие захотят поспорить. В качестве примера приведу цитату из Wikipedia на русском языке:

Некоторые разработчики OpenBSD, например Тео де Раадт и Отто Мурбек (Otto Moerbeek), критикуют GCC, называя его "громоздким, глючным, медленным и генерирующим плохой код".

Я считаю такие заявления необоснованными. Да, возможно, код GCC содержит много макросов, которые затрудняют его чтение. Но я никак не могу согласиться с заявлением о его глючности. Если бы GCC глючил, вообще бы нигде ничего не работало. Вы только вспомните, как много программ им компилируется и успешно работает. Создатели GCC делают огромную, сложную работу с большим профессионализмом. Спасибо им. Я рад, что могу протестировать работу PVS-Studio на таком высококачественном проекте.

Для тех, кто скажет, что код компилятора Clang всё равно круче, напомню: в нём PVS-Studio также находил ошибки: 1, 2.

PVS-Studio

Я проверил код GCC с помощью Alpha-версии анализатора PVS-Studio for Linux. Мы планируем начать выдавать заинтересовавшимся программистам Beta-версию анализатора в середине сентября 2016 года. Инструкцию о том, как стать одним из первых, кто сможет попробовать Beta-версию PVS-Studio for Linux на своём проекте, вы найдете в статье "PVS-Studio признаётся в любви к Linux".

Если вы читаете эту статью гораздо позже, чем сентябрь 2016, и хотите попробовать PVS-Studio for Linux, то приглашаю вас на страницу продукта: http://www.viva64.com/ru/pvs-studio/

Результаты проверки

Мы добрались до самого интересного раздела, который, я думаю, с нетерпением ждут наши постоянные читатели. Рассмотрим участки кода, где анализатор нашел ошибки или крайне подозрительные моменты.

К сожалению, я не могу выдать разработчикам компилятора полный отчёт. В нем пока слишком много мусора (ложных срабатываний), связанных с тем, что анализатор не полностью готов к встрече с миром Linux. Нужно проделать работу по уменьшению количества ложных предупреждений на типовые используемые конструкции. Попробую пояснить на одном простом примере. Многие диагностики не должны ругаться на выражения, относящиеся к макросам assert. Эти макросы бывают устроены весьма творчески и надо научить анализатор не обращать на них внимание. Но дело в том, что определяется макрос assert очень по-разному, и надо обучить анализатор всем типовым вариантам.

Поэтому разработчиков GCC прошу подождать выхода по крайней мере Beta-версии анализатора. Я не хочу испортить впечатление отчетом, сгенерированным недоделанной версией.

Классика (Copy-Paste)

Начнем мы с самой классической и распространённой ошибки, которая выявляется с помощью диагностики V501. Как правило, такие ошибки появляются из-за невнимательности при Copy-Paste или просто являются опечатками, допускаемыми при наборе нового кода.

static bool
dw_val_equal_p (dw_val_node *a, dw_val_node *b)
{
  ....
  case dw_val_class_vms_delta:
    return (!strcmp (a->v.val_vms_delta.lbl1,
                     b->v.val_vms_delta.lbl1)
            && !strcmp (a->v.val_vms_delta.lbl1,
                        b->v.val_vms_delta.lbl1));
  ....
}

Предупреждение анализатора PVS-Studio: V501 There are identical sub-expressions '!strcmp(a->v.val_vms_delta.lbl1, b->v.val_vms_delta.lbl1)' to the left and to the right of the '&&' operator. dwarf2out.c 1428

Быстро увидеть ошибки проблематично и следует внимательно присмотреться. Именно поэтому ошибка и не была выявлена при обзорах кода и рефакторинге.

Функция strcmp дважды сравнивает одни и те же строки. Мне кажется, второй раз следовало сравнивать не члены класса lbl1, а lbl2. Тогда корректный код должен выглядеть так:

return (!strcmp (a->v.val_vms_delta.lbl1,
                 b->v.val_vms_delta.lbl1)
        && !strcmp (a->v.val_vms_delta.lbl2,
                    b->v.val_vms_delta.lbl2));

Хочу отметить, что код, приведённый в статье, немного отформатирован, чтобы он занимал мало места по оси X. На самом деле, код выглядит так:

Ошибки, возможно, удалось бы избежать, если использовать "табличное" выравнивание кода. Например, ошибку было бы легче заметить, если отформатировать код так:

Подробнее я рассматривал такой подход в электронной книге "Главный вопрос программирования, рефакторинга и всего такого" (см. главу N13: Выравнивайте однотипный код "таблицей"). Рекомендую всем, кто заботится о качестве своего кода, познакомиться с приведённой здесь ссылкой.

Давайте рассмотрим ещё одну ошибку, которая, я уверен, появилась из-за Copy-Paste:

const char *host_detect_local_cpu (int argc, const char **argv)
{
  unsigned int has_avx512vl = 0;
  unsigned int has_avx512ifma = 0;
  ....
  has_avx512dq = ebx & bit_AVX512DQ;
  has_avx512bw = ebx & bit_AVX512BW;
  has_avx512vl = ebx & bit_AVX512VL;       // <=
  has_avx512vl = ebx & bit_AVX512IFMA;     // <=
  ....
}

Предупреждение анализатора PVS-Studio: V519 The 'has_avx512vl' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 500, 501. driver-i386.c 501

В переменную has_avx512vl дважды подряд записываются различные значения. Это не имеет смысла. Я изучил код и обнаружил переменную has_avx512ifma. Скорее всего, именно она и должна инициализироваться выражением ebx & bit_AVX512IFMA. Тогда корректный код должен быть таким:

has_avx512vl   = ebx & bit_AVX512VL;    
has_avx512ifma = ebx & bit_AVX512IFMA;

Опечатка

Продолжу испытание вашей внимательности. Посмотрите на код и, не подсматривая ниже, попробуйте найти ошибку.

static bool
ubsan_use_new_style_p (location_t loc)
{
  if (loc == UNKNOWN_LOCATION)
    return false;

  expanded_location xloc = expand_location (loc);
  if (xloc.file == NULL || strncmp (xloc.file, "\1", 2) == 0
      || xloc.file == '\0' || xloc.file[0] == '\xff'
      || xloc.file[1] == '\xff')
    return false;

  return true;
}

Предупреждение анализатора PVS-Studio: V528 It is odd that pointer to 'char' type is compared with the '\0' value. Probably meant: *xloc.file == '\0'. ubsan.c 1472

Здесь программист случайно забыл разыменовать указатель в выражении xloc.file == '\0'. В результате указатель просто сравнивается с 0, т.е. с NULL. Никакого эффекта это не имеет, так как ранее такая проверка уже выполнялась: xloc.file == NULL.

Хорошо, что терминальный ноль программист записал как '\0'. Это помогает быстрее понять, что код ошибочен и как его надо исправить. Про это я также писал в книге (см. главу N9: Используйте для обозначения терминального нуля литерал '\0').

Правильный вариант кода:

if (xloc.file == NULL || strncmp (xloc.file, "\1", 2) == 0
    || xloc.file[0] == '\0' || xloc.file[0] == '\xff'
    || xloc.file[1] == '\xff')
  return false;

Хотя, давайте ещё немного улучшим код. Я рекомендую отформатировать выражение так:

if (   xloc.file == NULL
    || strncmp (xloc.file, "\1", 2) == 0
    || xloc.file[0] == '\0'
    || xloc.file[0] == '\xff'
    || xloc.file[1] == '\xff')
  return false;

Обратите внимание: теперь, если допустить ту же ошибку, шанс её заметить будет чуть-чуть выше:

if (   xloc.file == NULL
    || strncmp (xloc.file, "\1", 2) == 0
    || xloc.file == '\0'
    || xloc.file[0] == '\xff'
    || xloc.file[1] == '\xff')
  return false;

Потенциальное разыменование нулевого указателя

Ещё этот раздел можно было бы назвать "стотысячный пример, почему макросы — это плохо". Я очень не люблю макросы и всегда призываю поменьше их использовать. Макросы затрудняют чтение кода, провоцируют появление ошибок, усложняют работу статическим анализаторам. Как мне показалось из недолгого общения с кодом GCC, его авторы очень любят макросы. Я замучался изучать, во что раскрывается тот или иной макрос и возможно поэтому пропустил немало интересных ошибок. Признаюсь, я иногда бываю ленив. Но пару ошибок, связанных с макросами, я всё-таки продемонстрирую.

odr_type
get_odr_type (tree type, bool insert)
{
  ....
  odr_types[val->id] = 0;
  gcc_assert (val->derived_types.length() == 0);
  if (odr_types_ptr)
    val->id = odr_types.length ();
  ....
}

Предупреждение анализатора PVS-Studio: V595 The 'odr_types_ptr' pointer was utilized before it was verified against nullptr. Check lines: 2135, 2139. ipa-devirt.c 2135

Видите здесь ошибку? Думаю, нет, и сообщение анализатора ясности не вносит. Всё дело в том, что odr_types — это не имя переменной, а макрос, объявленным следующим образом:

#define odr_types (*odr_types_ptr)

Если раскрыть макрос и убрать всё не относящееся к делу, мы получим следующий код:

(*odr_types_ptr)[val->id] = 0;
if (odr_types_ptr)

В начале указатель разыменовывается, а потом проверяется. Приведёт это к беде на практике или нет, сказать сложно. Все зависит от того, может ли возникнуть ситуация, когда указатель действительно будет равен nullptr. Если такая ситуация невозможна, то следует удалить лишнюю проверку, которая будет вводить в заблуждение людей, поддерживающих код и анализатор кода. Если указатель может быть нулевым, то это серьёзная ошибка, которая требует ещё большего внимания и исправления.

Рассмотрим ещё один аналогичный случай:

static inline bool
sd_iterator_cond (sd_iterator_def *it_ptr, dep_t *dep_ptr)
{
  ....
  it_ptr->linkp = &DEPS_LIST_FIRST (list);
  if (list)
    continue;
  ....
}

Предупреждение анализатора PVS-Studio: V595 The 'list' pointer was utilized before it was verified against nullptr. Check lines: 1627, 1629. sched-int.h 1627

Чтобы увидеть ошибку, нам опять потребуется показать устройство макроса:

#define DEPS_LIST_FIRST(L) ((L)->first)

Раскрываем макрос и получаем:

it_ptr->linkp = &((list)->first);
if (list)
  continue;

И сейчас многие воскликнут: "Стоп, стоп! Здесь нет ошибки. Мы ведь просто получаем указатель на член класса. Никакого разыменования нулевого указателя здесь нет. Да, возможно код не аккуратен, но ошибки здесь нет!".

Всё не так просто. Здесь возникает неопределённое поведение. И то, что такой код может работать на практике, это просто везение. На самом деле, так писать нельзя. Например, оптимизирующий компилятор, увидев list->first, может удалить проверку if (list). Раз мы выполняли оператор ->, значит предполагается, что указатель не равен nullptr. Если это так, то проверять указатель не нужно.

Я написал целую статью на эту тему: "Разыменовывание нулевого указателя приводит к неопределённому поведению". Там как раз рассматривается аналогичный случай. Прежде чем спорить, прошу внимательно познакомиться с этой статьёй.

Впрочем, рассмотренная ситуация действительно сложна и неочевидна. Я допускаю, что могу быть всё-таки неправ и ошибки здесь нет. Однако, до сих пор мне никто не смог это доказать. Будет интересно услышать комментарии разработчиков GCC, если они обратят внимание на эту статью. Уж они-то точно должны знать, как работает компилятор и следует ли интерпретировать такой код как ошибочный, или нет.

Использование разрушенного массива

static void
dump_hsa_symbol (FILE *f, hsa_symbol *symbol)
{
  const char *name;
  if (symbol->m_name)
    name = symbol->m_name;
  else
  {
    char buf[64];
    sprintf (buf, "__%s_%i", hsa_seg_name (symbol->m_segment),
       symbol->m_name_number);
     name = buf;
  }
  fprintf (f, "align(%u) %s_%s %s",
           hsa_byte_alignment (symbol->m_align),
           hsa_seg_name(symbol->m_segment),
           hsa_type_name(symbol->m_type & ~BRIG_TYPE_ARRAY_MASK),
           name);
  ....
}

Предупреждение анализатора PVS-Studio: V507 Pointer to local array 'buf' is stored outside the scope of this array. Such a pointer will become invalid. hsa-dump.c 704

Строка формируется во временном буфере buf. Адрес этого временного буфера сохраняется в переменной name и используется далее в теле функции. Ошибка в том, что после записи буфера в переменную name, сам этот буфер будет уничтожен.

Использовать указатель на разрушенный буфер нельзя. Формально мы имеем дело с неопределённым поведением. На практике этот код может вполне успешно работать. Корректная работа программы - это один из вариантов проявления неопределенного поведения.

В любом случае, этот код содержит ошибку и её необходимо исправить. Работать код может по той причине, что компилятор может посчитать ненужным использование временного буфера для последующего хранения других переменных или массивов. И тогда, хотя массив, созданный на стеке, считается разрушенным, на самом деле его может никто не трогать, и функция правильно выполнит свою работу. Вот только такое везение в любой момент может кончиться и код, который работал 10 лет, вдруг, при переходе на новую версию компилятора, начинает вести себя удивительнейшим образом.

Чтобы исправить ошибку, достаточно объявить массив buf в той же области видимости, что и указатель name:

static void
dump_hsa_symbol (FILE *f, hsa_symbol *symbol)
{
  const char *name;
  char buf[64];
  ....
}

Выполнение одинаковых действий, независимо от условия

Анализатор выявил участок кода, который однозначно я не могу идентифицировать как ошибочный. Однако, крайне подозрительно выполнить проверку, а потом, независимо от её результата, выполнять одни и те же действия. Конечно, возможно, это задел на будущее и пока всё корректно, но проверить этот участок кода явно стоит.

bool
thread_through_all_blocks (bool may_peel_loop_headers)
{
  ....
  /* Case 1, threading from outside to inside the loop
     after we'd already threaded through the header.  */
  if ((*path)[0]->e->dest->loop_father
      != path->last ()->e->src->loop_father)
  {
    delete_jump_thread_path (path);
    e->aux = NULL;
    ei_next (&ei);
  }
  else
  {
    delete_jump_thread_path (path);
    e->aux = NULL;
    ei_next (&ei);
  }
  ....
}

Предупреждение анализатора PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. tree-ssa-threadupdate.c 2596

Если этот код ошибочный, я, к сожалению, не догадываюсь как его следует исправить. Это тот случай, когда надо быть знакомым с проектом, чтобы внести правку.

Избыточное выражение вида (A == 1 || A != 2)

static const char *
alter_output_for_subst_insn (rtx insn, int alt)
{
  const char *insn_out, *sp ;
  char *old_out, *new_out, *cp;
  int i, j, new_len;

  insn_out = XTMPL (insn, 3);

  if (alt < 2 || *insn_out == '*' || *insn_out != '@')
    return insn_out;
  ....
}

Предупреждение анализатора PVS-Studio: V590 Consider inspecting this expression. The expression is excessive or contains a misprint. gensupport.c 1640

Нас интересует условие: (alt < 2 || *insn_out == '*' || *insn_out != '@')

Его можно сократить до: (alt < 2 || *insn_out != '@')

Рискну предположить, что оператор != следует заменить на ==. Тогда код примет более осмысленный вид:

if (alt < 2 || *insn_out == '*' || *insn_out == '@')

Обнуление не того указателя

Рассмотрим функцию, занимающуюся освобождением ресурсов:

void
free_original_copy_tables (void)
{
  gcc_assert (original_copy_bb_pool);
  delete bb_copy;
  bb_copy = NULL;
  delete bb_original;
  bb_copy = NULL;
  delete loop_copy;
  loop_copy = NULL;
  delete original_copy_bb_pool;
  original_copy_bb_pool = NULL;
}

Предупреждение анализатора PVS-Studio: V519 The 'bb_copy' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1076, 1078. cfg.c 1078

Обратите внимание на эти 4 строчки кода:

delete bb_copy;
bb_copy = NULL;
delete bb_original;
bb_copy = NULL;

Случайно дважды обнуляется указатель bb_copy. Правильный вариант:

delete bb_copy;
bb_copy = NULL;
delete bb_original;
bb_original = NULL;

Assert, который ничего не проверят

Неправильное условие, являющееся аргументом макроса gcc_assert не повлияет на корректность работы программы, но усложнит поиск ошибки, если таковая возникнет. Рассмотрим код:

static void
output_loc_operands (dw_loc_descr_ref loc, int for_eh_or_skip)
{
  unsigned long die_offset
    = get_ref_die_offset (val1->v.val_die_ref.die);
  ....
  gcc_assert (die_offset > 0
        && die_offset <= (loc->dw_loc_opc == DW_OP_call2)
             ? 0xffff
             : 0xffffffff);
  ....
}

Предупреждение анализатора PVS-Studio: V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '<=' operator. dwarf2out.c 2053

Приоритет тернарного оператора ?: ниже, чем у операторов <= и &&. Это значит, что мы имеем дело с условием вида:

(die_offset > 0 &&
  die_offset <= (loc->dw_loc_opc == DW_OP_call2)) ?
    0xffff : 0xffffffff;

Оба значения 0xffff и 0xffffffff обозначают истину, поэтому выражение можно упростить до:

(true)

Это явно не то, что задумывал программист. Чтобы исправить ситуацию, следует добавить пару круглых скобок:

gcc_assert (die_offset > 0
      && die_offset <= ((loc->dw_loc_opc == DW_OP_call2)
           ? 0xffff
           : 0xffffffff));

Оператор ?: очень коварен и его лучше не использовать в сложных выражениях. Уж очень легко допустить ошибку. У нас собрано большое количество примеров таких ошибок, найденных анализатором PVS-Studio в различных открытых проектах. Подробнее об операторе ?: я писал в уже упомянутой ранее книге (см. главу N4: Бойтесь оператора ?: и заключайте его в круглые скобки).

Кажется, забыли про "cost"

Структура alg_hash_entry объявлена следующим образом:

struct alg_hash_entry {
  unsigned HOST_WIDE_INT t;
  machine_mode mode;
  enum alg_code alg;
  struct mult_cost cost;
  bool speed;
};

В функции synth_mult программист решил проверить, тот ли это объект, который ему нужен. Для этого ему требуется сравнить поля структуры. Однако, кажется в этом месте допущена ошибка:

static void synth_mult (....)
{
  ....
  struct alg_hash_entry *entry_ptr;
  ....
  if (entry_ptr->t == t
      && entry_ptr->mode == mode
      && entry_ptr->mode == mode
      && entry_ptr->speed == speed
      && entry_ptr->alg != alg_unknown)
  {
  ....
}

Предупреждение анализатора PVS-Studio: V501 There are identical sub-expressions 'entry_ptr->mode == mode' to the left and to the right of the '&&' operator. expmed.c 2573

Два раза подряд проверяется mode, но зато нет проверки cost. Возможно, одно из сравнений нужно просто удалить, а возможно, нужно сравнивать cost. Мне сложно судить, но код явно стоит поправить.

Дубликаты присваиваний

Следующие участки кода, на мой взгляд, не представляют опасности и, кажется, дублирующееся присваивание можно просто удалить.

Случай N1

type_p
find_structure (const char *name, enum typekind kind)
{
  ....
  structures = s;                   // <=
  s->kind = kind;
  s->u.s.tag = name;
  structures = s;                   // <=
  return s;
}

Предупреждение анализатора PVS-Studio: V519 The 'structures' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 842, 845. gengtype.c 845

Случай N2

static rtx
ix86_expand_sse_pcmpistr (....)
{
  unsigned int i, nargs;
  ....
    case V8DI_FTYPE_V8DI_V8DI_V8DI_INT_UQI:
    case V16SI_FTYPE_V16SI_V16SI_V16SI_INT_UHI:
    case V2DF_FTYPE_V2DF_V2DF_V2DI_INT_UQI:
    case V4SF_FTYPE_V4SF_V4SF_V4SI_INT_UQI:
    case V8SF_FTYPE_V8SF_V8SF_V8SI_INT_UQI:
    case V8SI_FTYPE_V8SI_V8SI_V8SI_INT_UQI:
    case V4DF_FTYPE_V4DF_V4DF_V4DI_INT_UQI:
    case V4DI_FTYPE_V4DI_V4DI_V4DI_INT_UQI:
    case V4SI_FTYPE_V4SI_V4SI_V4SI_INT_UQI:
    case V2DI_FTYPE_V2DI_V2DI_V2DI_INT_UQI:
      nargs = 5;         // <=
      nargs = 5;         // <=
      mask_pos = 1;
      nargs_constant = 1;
      break;
  ....
}

Предупреждение анализатора PVS-Studio: V519 The 'nargs' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 39951, 39952. i386.c 39952

Случай N3

Последний случай более странный, чем остальные. Возможно, тут есть какая-то ошибка. Переменной steptype значение присваивается 2 или 3 раза. Это подозрительно.

static void
cand_value_at (....)
{
  aff_tree step, delta, nit;
  struct iv *iv = cand->iv;
  tree type = TREE_TYPE (iv->base);
  tree steptype = type;                 // <=
  if (POINTER_TYPE_P (type))
    steptype = sizetype;                // <=
  steptype = unsigned_type_for (type);  // <=
  ....
}

Предупреждение анализатора PVS-Studio: V519 The 'steptype' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 5173, 5174. tree-ssa-loop-ivopts.c 5174

Заключение

Я рад, что написал эту статью. Теперь мне есть что отвечать на комментарии вида "PVS-Studio не нужен, так как все те же предупреждения выдаёт и GCC". Как видите, PVS-Studio очень мощный инструмент и превосходит по диагностическим возможностям GCC. Я не отрицаю, что в GCC реализованы отличные диагностики. Этот компилятор, при должной настройке, действительно выявляет много проблем в коде. Но PVS-Studio - это специализированный и быстро развивающийся инструмент, а это значит, он всегда будет лучше выявлять ошибки в коде, чем это делают компиляторы.

Приглашаю познакомиться с проверками других известных открытых проектов, посетив этот раздел нашего сайта. А также, тем, кто использует Twitter, последовать за мной @Code_Analysis. Я регулярно публикую ссылки на интересные статьи по программированию на языке C и C++, а также рассказываю о новых достижениях нашего анализатора.