LLVM — open-source проект с огромной кодовой базой. Лучший из лучших, если говорить о качестве кода, учитывая его размеры и открытость. Ведь кому, как не разработчикам инструментов для компиляторов, лучше знать о возможностях языка и правильном их использовании. Их код всегда на высоте, а найти ошибки в нём всегда вызов для нашего анализатора, который мы принимаем.

Пару месяцев назад у LLVM вышла в релиз 18-я версия, поэтому настало время в очередной раз убедиться в качестве их кода. Для тех, кому интересно, наши предыдущие статьи про проверку можно прочитать тут и тут.

Для нас это всегда особые проверки, ведь, по сути, статические анализаторы выполняют схожую с компиляторами работу для проведения анализа. А компиляторы также проводят статический анализ для выдачи предупреждений. Практически двоюродные братья. Но каждый из них хорош в своём деле. Доказательством этого как раз служит эта статья. Компилятор скомпилировал наш анализатор, причём так, чтобы тот работал (кстати, именно Clang, входящий в состав LLVM, и у нас даже есть статья о переходе на него с MSVC). Ну а наш анализатор нашёл ошибки в компиляторе. Чем не доказательство синергии?

Версия LLVM, на которой производилась проверка — LLVM 18.1.0.

Фрагмент N1

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

if (Tok->is(tok::hash)) {
  // Start of a macro expansion.
  First = Tok;
  Tok = Next;
  if (Tok)
    Tok = Tok->getNextNonComment();
} else if (Tok->is(tok::hashhash)) {
  // Concatenation. Skip.
  Tok = Next;
  if (Tok)
    Tok = Tok->getNextNonComment();
} else if (Keywords.isVerilogQualifier(*Tok) ||
           Keywords.isVerilogIdentifier(*Tok)) {
  First = Tok;
  Tok = Next;
  // The name may have dots like `interface_foo.modport_foo`.
  while (Tok && Tok->isOneOf(tok::period, tok::coloncolon) &&
         (Tok = Tok->getNextNonComment())) {
    if (Keywords.isVerilogIdentifier(*Tok))
      Tok = Tok->getNextNonComment();
  }
} else if (!Next) {
  Tok = nullptr;
} else if (Tok->is(tok::l_paren)) {
  // Make sure the parenthesized list is a drive strength. Otherwise the
  // statement may be a module instantiation in which case we have already
  // found the instance name.
  if (Next->isOneOf(
          Keywords.kw_highz0, Keywords.kw_highz1, Keywords.kw_large,
          Keywords.kw_medium, Keywords.kw_pull0, Keywords.kw_pull1,
          Keywords.kw_small, Keywords.kw_strong0, Keywords.kw_strong1,
          Keywords.kw_supply0, Keywords.kw_supply1, Keywords.kw_weak0,
          Keywords.kw_weak1)) {
    Tok->setType(TT_VerilogStrength);
    Tok = Tok->MatchingParen;
    if (Tok) {
      Tok->setType(TT_VerilogStrength);
      Tok = Tok->getNextNonComment();
    }
  } else {
    break;
  }
} else if (Tok->is(tok::hash)) {
  if (Next->is(tok::l_paren))
    Next = Next->MatchingParen;
  if (Next)
    Tok = Next->getNextNonComment();
}

Предупреждение анализатора:

V517 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 3016, 3058. TokenAnnotator.cpp

Давайте посмотрим внимательнее. В первом условии стоит проверка Tok->is(tok::hash). В последнем условии в конструкции else if стоит аналогичная проверка. При этом токен, с которым работают, не меняется. Таким образом, код в последнем else if никогда не выполнится. В этом случае это критично, так как код под этими условиями разный.

if (Tok->is(tok::hash)) {
  // Start of a macro expansion.
  First = Tok;
  Tok = Next;
  if (Tok)
    Tok = Tok->getNextNonComment();
} else
....
else if (Tok->is(tok::hash)) {
  if (Next->is(tok::l_paren))
    Next = Next->MatchingParen;
  if (Next)
    Tok = Next->getNextNonComment();
}

Возможно, такую конструкцию стоило бы заменить на switch, и тогда проще было бы заметить ошибку, но это уже дело вкуса.

Фрагмент N2

Интересный фрагмент кода, которым я бы хотел с вами поделиться:

assert(bArgs.size() == reduc.size() + needsUniv ? 1 : 0);

Предупреждения анализатора:

  • V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '+' operator. LoopEmitter.cpp 983

  • V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '+' operator. LoopEmitter.cpp 1039

Предупреждения два, потому что этот фрагмент встречается в двух разных местах.

Анализатор говорит, что здесь возможно неправильное использование тернарного оператора. Давайте разбираться.

Для начала вспомним приоритет операторов. Приоритет в порядке убывания следующий: оператор +, оператор ==, тернарный оператор. Также необходимо отметить, что переменная needsUniv имеет тип bool.

Получается, что сначала произойдёт сложение результата работы reduc.size() и needsUniv. При этом needsUniv неявно кастанётся к типу size_t. Далее результат сложения сравнится с результатом работы bArgs.size(). Затем выполнится тернарный оператор, который вернёт либо 1, либо 0.

Согласитесь, выглядит немного странновато. Вероятнее всего, хотели написать такой код:

assert(bArgs.size() == reduc.size() + (needsUniv ? 1 : 0));

В таком случае сначала выполнится тернарный оператор, который вернёт либо 1, либо 0. Затем это значение сложится с результатом работы *reduc.size(), *а затем произойдёт сравнение с результатом работы bArgs.size().

Занимательно, что результат работы и в первом, и во втором случае один и тот же.

Ещё занимательнее, что можно было написать так:

assert(bArgs.size() == reduc.size() + needsUniv);

Результат и в этом случае был бы тот же, но уже без лишнего выполнения тернарного оператора.

В общем, интересный случай, когда код написан неправильно, но работает правильно.

Фрагмент N3

Довольно любопытное использование постфиксного инкремента. Внимание на второй аргумент кастомной функции Printf, вызываемой у объекта strm:

static void DumpTargetInfo(uint32_t target_idx, Target *target,
                           const char *prefix_cstr,
                           bool show_stopped_process_status, Stream &strm) 
{
  ....
  uint32_t properties = 0;
  if (target_arch.IsValid()) 
  {
    strm.Printf("%sarch=", properties++ > 0 ? ", " : " ( ");
    target_arch.DumpTriple(strm.AsRawOstream());
    properties++;
  }
}

Предупреждение анализатора:

V547 Expression 'properties ++ > 0' is always false. CommandObjectTarget.cpp:100

На мой взгляд, здесь сначала хотели провести сравнение properties c нулём, и, чтобы дальше не писать лишний код с инкрементацией переменной properties, решили тут же воспользоваться постфиксным инкрементом.

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

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

Фрагменты N4-8

Рассмотрим следующий код и срабатывание PVS-Studio:

bool areStatementsIdentical(const Stmt *FirstStmt, const Stmt *SecondStmt,
                            const ASTContext &Context, bool Canonical) 
{
  ....
  if (FirstStmt->getStmtClass() != FirstStmt->getStmtClass())
    return false;
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions 'FirstStmt->getStmtClass()' to the left and to the right of the '!=' operator. ASTUtils.cpp:99

Казалось бы, мелочь. Ну перепутали FirstStmt и SecondStmt, чего бухтеть-то?

А потом встречаем такой код:

static bool sameFunctionParameterTypeLists(Sema &S,
                                           const OverloadCandidate &Cand1,
                                           const OverloadCandidate &Cand2) {
  if (!Cand1.Function || !Cand2.Function)
    return false;

  FunctionDecl *Fn1 = Cand1.Function;
  FunctionDecl *Fn2 = Cand2.Function;

  if (Fn1->isVariadic() != Fn1->isVariadic())
    return false;
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions to the left and to the right of the '!=' operator: Fn1->isVariadic() != Fn1->isVariadic(). SemaOverload.cpp:10190

Думаешь: "Ну ошиблись второй раз, с кем не бывает".

А потом видишь такой код:

if (G1->Rank < G1->Rank)
  G1->Group = G2;
else {
  G2->Group = G1;
}

Предупреждение анализатора:

V501 There are identical sub-expressions to the left and to the right of the '<' operator: G1->Rank < G1->Rank. SCCIterator.h:285

Начинают закрадываться сомнения.

А потом появляется такой фрагмент:

ValueBoundsConstraintSet::areOverlappingSlices(MLIRContext *ctx,
                                               HyperrectangularSlice slice1,
                                               HyperrectangularSlice slice2) {
  assert(slice1.getMixedOffsets().size() == slice1.getMixedOffsets().size()
      && "expected slices of same rank");
  assert(slice1.getMixedSizes().size()   == slice1.getMixedSizes().size() 
      && "expected slices of same rank");
  assert(slice1.getMixedStrides().size() == slice1.getMixedStrides().size() 
      && "expected slices of same rank");
  ....
}

Предупреждения анализатора:

  • V501 There are identical sub-expressions 'slice1.getMixedOffsets().size()' to the left and to the right of the '==' operator. ValueBoundsOpInterface.cpp:581

  • V501 There are identical sub-expressions 'slice1.getMixedSizes().size()' to the left and to the right of the '==' operator. ValueBoundsOpInterface.cpp:583

  • V501 There are identical sub-expressions 'slice1.getMixedStrides().size()' to the left and to the right of the '==' operator. ValueBoundsOpInterface.cpp:585

И ещё один почти такой же:

ValueBoundsConstraintSet::areEquivalentSlices(MLIRContext *ctx,
                                              HyperrectangularSlice slice1,
                                              HyperrectangularSlice slice2) {
  assert(slice1.getMixedOffsets().size() == slice1.getMixedOffsets().size() 
      && "expected slices of same rank");
  assert(slice1.getMixedSizes().size()   == slice1.getMixedSizes().size() 
      && "expected slices of same rank");
  assert(slice1.getMixedStrides().size() == slice1.getMixedStrides().size() 
      && "expected slices of same rank");
  ....
}

Предупреждения анализатора:

  • V501 There are identical sub-expressions 'slice1.getMixedOffsets().size()' to the left and to the right of the '==' operator. ValueBoundsOpInterface.cpp:646

  • V501 There are identical sub-expressions 'slice1.getMixedSizes().size()' to the left and to the right of the '==' operator. ValueBoundsOpInterface.cpp:648

  • V501 There are identical sub-expressions 'slice1.getMixedStrides().size()' to the left and to the right of the '==' operator. ValueBoundsOpInterface.cpp:650

И удивляешься: неужели так много ошибок может тихо жить в коде? И ведь из-за каждого из таких мест что-то может отвалиться или работать не так, как задумано.

Кстати, видя все эти Fn1, G1, slice1, вспоминается статья коллеги "Ноль, один, два, Фредди заберёт тебя".

Ну и на закуску аналогичные предупреждения:

  • V501 There are identical sub-expressions 'EltRange.getEnd() >= Range.getEnd()' to the left and to the right of the '||' operator. HTMLLogger.cpp:421

  • V501 There are identical sub-expressions 'SrcExpr.get()->containsErrors()' to the left and to the right of the '||' operator. SemaCast.cpp:2938

  • V501 There are identical sub-expressions 'ND->getDeclContext()' to the left and to the right of the '!=' operator. SemaDeclCXX.cpp:4391

  • V501 There are identical sub-expressions 'DepType != OMPC_DOACROSS_source' to the left and to the right of the '&&' operator. SemaOpenMP.cpp:24348

  • V501 There are identical sub-expressions '!OldMethod->isStatic()' to the left and to the right of the '&&' operator. SemaOverload.cpp:1425

  • V501 There are identical sub-expressions 'lldb::eTypeClassUnion' to the left and to the right of the '|' operator. JSONUtils.cpp:139

  • V501 There are identical sub-expressions to the left and to the right of the '&&' operator: !BFI &&!BFI. JumpThreading.cpp:2531

  • V501 There are identical sub-expressions 'BI->isConditional()' to the left and to the right of the '&&' operator. VPlanHCFGBuilder.cpp:401

  • V501 There are identical sub-expressions to the left and to the right of the '==' operator: getNumRows() == getNumRows(). Simplex.cpp:108

Фрагмент N9

А вот ещё интересный фрагмент кода. Попробуйте самостоятельно разобраться, где тут ошибка:

const Expr *CGOpenMPRuntime::getNumTeamsExprForTargetDirective(
    CodeGenFunction &CGF, const OMPExecutableDirective &D, int32_t &MinTeamsVal,
    int32_t &MaxTeamsVal) 
{
  ....
  if (isOpenMPParallelDirective(NestedDir->getDirectiveKind()) ||
          isOpenMPSimdDirective(NestedDir->getDirectiveKind())) {
    MinTeamsVal = MaxTeamsVal = 1;
    return nullptr;
  }
  MinTeamsVal = MaxTeamsVal = 1;
  return nullptr;
  ....
}

Давайте посмотрим, что говорит анализатор.

Предупреждение анализатора:

V523 The 'then' statement is equivalent to the subsequent code fragment. CGOpenMPRuntime.cpp:6040, 6036

Он говорит, что код после if точно такой же, как и в then-ветке. Соответственно, здесь лишняя либо проверка, либо же часть кода после неё.

Фрагмент N10

Следующий фрагмент выглядит весьма странно:

explicit MapLattice(Container C) { C = std::move(C); }

Предупреждение анализатора:

V570 The 'C' variable is assigned to itself. MapLattice.h:52

И неспроста. Внутри тела конструктора класса MapLattice произошло сокрытие нестатического поля с тем же именем, что и у параметра. И в этом фрагменте забыли явно указать this слева от оператора присваивания.

Небольшая правка, и код заработает так, как и ожидалось:

explicit MapLattice(Container C) { this->C = std::move(C); }

Хотя, на мой взгляд, куда изящнее было бы воспользоваться списком инициализации конструктора:

explicit MapLattice(Container C) : C { std::move(C) } {};

В таком случае сокрытия не происходит из-за правил поиска имён (тык, тык).

Ну а вообще, в имена приватных полей можно добавлять префикс или постфикс, чтобы было легче отличать их в коде от параметров.

Фрагмент N11

У вас когда-нибудь возникало чувство, будто вы что-то забыли? Например, вы не помните, взяли ли вы с собой ключи. Начинаете ощупывать карманы и не можете найти их.

Так вот, в следующем фрагменте как будто забыли использовать результат работы функции:

ScalarEvolution::getRangeRefIter(const SCEV *S,
                                 ScalarEvolution::RangeSignHint SignHint) 
{
  ....
  for (const SCEV *P : reverse(drop_begin(WorkList))) {
    getRangeRef(P, SignHint);
    ....
  }
  ....
}

Предупреждение анализатора:

V530 The return value of function 'getRangeRef' is required to be utilized. ScalarEvolution.cpp:6587

Можно заметить, что результат работы getRangeRef никак не используется.

А вот и сигнатура этой функции, которая подтверждает, что результат работы есть:

const ConstantRange &getRangeRef(const SCEV *S, RangeSignHint Hint,
                                 unsigned Depth = 0);

Да и далее по коду видно, что в других местах её результат используется.

Но не всё так просто. Иногда наши чувства нас подводят и оказывается, что ключи всё это время были прямо у нас в руке.

Над самим фрагментом есть такой комментарий:

// Use getRangeRef to compute ranges for items in the worklist in reverse
// order. This will force ranges for earlier operands to be computed before
// their users in most cases.

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

Таким образом, формально анализатор оказался прав, но фактически ошибки здесь нет. Для таких случаев у нас есть способы подавления отдельных срабатываний. Например, с помощью комментария в коде:

for (const SCEV *P : reverse(drop_begin(WorkList))) {
  getRangeRef(P, SignHint); //-V530
  .... }  

Фрагмент N12

Для самых внимательных. Найдите три отличия кода в if и else ветках. Ладно, хотя бы одно:

case OptionParser::eOptionalArgument:
  if (OptionParser::GetOptionArgument() != nullptr) {
    option_element_vector.push_back(OptionArgElement(
        opt_defs_index,
        FindOriginalIndex(dummy_vec[OptionParser::GetOptionIndex() - 2],
                          args),
        FindOriginalIndex(dummy_vec[OptionParser::GetOptionIndex() - 1],
                          args)));
  } else {
    option_element_vector.push_back(OptionArgElement(
        opt_defs_index,
        FindOriginalIndex(dummy_vec[OptionParser::GetOptionIndex() - 2],
                          args),
        FindOriginalIndex(dummy_vec[OptionParser::GetOptionIndex() - 1],
                          args)));
  }

И вы абсолютно правы. Их нет.

Предупреждение анализатора:

V523 The 'then' statement is equivalent to the 'else' statement. Options.cpp 1212

Заключение

Всё хорошее имеет свойство заканчиваться. Но к статье это не относится. Большому проекту — большая статья, а ещё лучше две. В этот раз я решил придерживаться второго варианта. В следующей части будут рассмотрены ошибки гораздо серьёзнее, чем в этой (спойлер: связанные с UB).

Даже лучшие из нас совершают ошибки (и не только в коде). Что уж говорить про рядовых пользователей. Но ошибки это не повод расстраиваться. Ошибки — это повод становиться лучше и расти (можете говорить это вашему тимлиду каждый раз, когда что-то отваливается на проде).

Ну а чтобы избегать ошибок в коде, нужно пробовать разные способы их поиска. Например, помимо покрытия кода тестами и code review можно использовать инструменты динамического и статического анализа кода.

Интересно узнать, какие ошибки есть в вашем коде? Тогда вы можете проверить это бесплатно.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Smolskas. What errors are lurking in LLVM code?.

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


  1. datacompboy
    29.05.2024 09:48
    +1

    Показывает на качество покрытия тестами. То есть часть эвристик есть, но не покрыто полноценно регрессами... Скорее всего, это практически невозможно, потому и.


  1. Mingun
    29.05.2024 09:48
    +2

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

    Скорее всего результат неудачного разрешения конфликта при мердже. Возможно, даже автоматического. В итого оказалось, что взяли обе половины конфликта.


  1. slonopotamus
    29.05.2024 09:48

    В общем, интересный случай, когда код написан неправильно, но работает правильно.

    Это называется ложное срабатывание.


    1. AVaTar123
      29.05.2024 09:48
      +2

      Да, но только для разработчика, но не анализатора.


      1. slonopotamus
        29.05.2024 09:48
        +3

        Вкусовщина. Я с тем же успехом могу сказать что ошибка разработчика в том что он вообще на C++ пишет. Писал бы на языке где bool не приводится втихаря к int, эта конструкция вообще не скомпилировалась.


    1. Mingun
      29.05.2024 09:48
      +2

      Это называется "мина замедленного действия"


  1. Okunev_PY
    29.05.2024 09:48
    +2

    Не первый раз вижу статьи с анализом чужих продуктов, из этого закономерный вопрос, а можно результаты проверок самой Pvs-Studio увидеть?


    1. Alphrixus Автор
      29.05.2024 09:48

      Да, например, есть такая статья. А так мы регулярно проверяем сами себя во время ночных сборок на сервере, и разработчикам приходят уведомления на почту, если есть срабатывания.


  1. sabudilovskiy
    29.05.2024 09:48
    +1

    А вы открыли issues в LLVM?


    1. Alphrixus Автор
      29.05.2024 09:48

      Да, ссылка. Там их уже, кстати, немало от нас :D


      1. ShaltaiBoltai
        29.05.2024 09:48

        У вас очень полезный продукт и дело вы делаете очень полезное.

        Я так подозреваю, что для всех публичных продуктов с открытым исходным кодом, что вы проверяете, вы также создаете issues в github. Было бы интересно, если бы вы в статьях также рассказывали, какая есть реакция на созданные вами issues - правятся ли те ошибки, что вы находите, или же ваши предложения игнорируют.


        1. Andrey2008
          29.05.2024 09:48

          По разному бывает :) Про игнор вспомнилось - 1000 глаз, которые не хотят проверять код открытых проектов :)


          1. ShaltaiBoltai
            29.05.2024 09:48

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

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

            Или где properties увеличили 2 раза.

            И что еще интересно: вы говорите, что код LLVM один из самых удобочитаемых и аккуратных, а параллельно есть мнение, что этот код "один из самых убогих и ненормальных".


  1. Shermike
    29.05.2024 09:48

    И неспроста. Внутри тела конструктора класса MapLattice произошло сокрытие нестатического поля с тем же именем, что и у параметра. И в этом фрагменте забыли явно указать this слева от оператора присваивания.

    В который раз убеждаюсь: LLVM code style один из самых убогих и ненормальных. Как можно додуматься писать имена классов и всех переменных в одном стиле!