Обидно за мнения про статические анализаторы кода

    Инструменты статического анализа кода ушли далеко вперёд. Это вовсе не те "линтеры", которые активно применялись 20 лет тому назад. Однако многие по-прежнему относятся к ним как к очень простым инструментам. Обидно. Обидно как за методологию анализа кода в целом, так и за инструмент PVS-Studio.



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

    if (A[0] == 0)
    {
      X = Y;
      if (A[0] == 0)
        ....
    }

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

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

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

    Именно так и поступает статический анализатор PVS-Studio. Взглянем на следующий синтетический пример:

    char get();
    int foo(char *p, bool arg)
    {
        if (p[1] == 1)
        {
            if (arg)
                p[0] = get();
            if (p[1] == 1)          // Warning
                return 1;
        }
        if (p[2] == 2)
        {
            if (arg)
                p[2] = get();
            if (p[2] == 2)          // Ok
                return 2;
        }
        return 3;
    }

    Код содержит два похожих блока. В одном проверяемая переменная не меняется, а во втором меняется. Поэтому анализатор выдаёт предупреждение только для первого блока: V547 Expression 'p[1] == 1' is always true.

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


    Дополнительные ссылки



    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Upsetting Opinions about Static Analyzers.
    PVS-Studio
    Статический анализ кода для C, C++, C# и Java

    Комментарии 90

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

      Писал много раз, и еще напишу: изучайте ваши инструменты, анализаторы, санитайзеры, фаззеры, SAT-солверы, профилировщики, отладчики, средства символьного исполнения и т.п., потому что все они и сильно повышают качество вашего ПО, и сильно ускоряют его разработку, и снижают накал рутины, потому что действия по монотонной проверке инвариантов теперь исполняет машина, которая не скучает, не отвлекается, и не устает.
        0
        Спасибо :)
          +11

          А по мере набора ещё некоторого опыта приходит понимание, что линтеры не гарантируют практически ничего, и единственный способ что-то гарантировать — брать языки, где эти гарантии можно так или иначе выражать. От liquid types (чтобы с SAT SMT-солверами и без необходимости писать пруфтермы руками) до полноценного хардкора с dependent types.


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

            +7
            Спасибо вам за ваш труд, серьезно. Проблема только в том, что гарантировать и не нужно, нужно улучшить статут кво. Бизнесу не интересно переписывать все их миллионы строк кода на языках, которые непонятно когда появятся, потому что продукт нужно выпускать уже в этом году, а код сам по себе — это не продукт, это технические детали реализации. Нельзя отмахиваться от инструментов улучшения существующей кодовой базы только потому, что инструменты эти не универсальные, или пропускают ошибки, или не находят ошибки, потому что нет вообще ничего совершенного и даже идеальные программы все равно на реальном железе исполняются.
              +2

              Да, бизнесу не интересно переписывать миллионы строк кода, но и расставлять аннотации для SMT-солверов, чтобы они там решали, удовлетворяет функция контрактам или нет, тоже никто не будет. И фаззингом даже заниматься никто не будет, и с санитайзерами собирается поразительно мало людей, так что имеем то, что имеем.


              Спасибо вам за ваш труд, серьезно.

              Эх, да когда он ещё до результата дойдёт. Пока у меня всё как-то ожидаемо скатилось в работу над формальным доказательством, что мой модельный язык с refinement types корректно компилируется в мой же модельный язык с dependent types.

                +2
                Курочка по зернышку, скажем так, и фаззингом уже давно занимаемся, и санитайзерами собираемся сами и других заставляем собираться, и указатели толстые принесли в прошивку, чтобы совсем голышом не сидеть. Это все намного лучше, чем ничего, в любом случае.
                  +2
                  Мы, например, стат анализ при пушах гоняем, а перед релизом санитайзеры. Фазеры на критических маршрутах перед передачей на сертификацию. И пришли к выводу, что это дешевле, чем потом из-за тупой баги все устройства у Заказчика перешивать.
                  Так что труд просветителей не проходит даром :) Спасибо
          +6

          Что является источником изменения переменной?


          Я вот с ходу могу назвать три:
          1) Изменения внутри линейного кода программы (вы про это написали, мол, отслеживаем).
          2) Изменения внутри кода программы в соседнем треде (хохо, какого джина я только что выпустил...), или, даже, в обработчике сигнала (что, по сути, тот же "соседний тред").
          3) Изменения, вызванные аппаратным обеспечением. DMA операция, например.


          Но… линтер всегда прав?

            +5

            2) и 3) это особые случаи и обычно вставляется комментарий который выключит проверку анализатора в этом месте. Добро пожаловать в 21 век.

              +10

              2). и отчасти 3). — это практика современного низкоуровневого программирования, добро пожаловать в 2021 год.
              Расставляя комментарии с отключением анализатора смысл этой затеи теряется, т.к. подобный комментарий имеет ровно ту же проблему, что и обычный — очень малое время актуальности, после которого у разработчика возникает дополнительная проблема — поддержка не только кода, но и комментариев к нему.

                +6

                Обычно, в низкоуровневом коде этого избегают, а если нет, такие переменные в низкоуровневом коде должны быть помечены как volatile. А современный стат анализатор должен это выловить. В любом случае, если вы обновляете данные по DMA, то вы и компилятору и анализатору должны дать это понять, чтобы там ничего не оптимизировалось само.
                Иначе, никто кроме вас про это не узнает и могут быть грабельки.


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

                +16

                Да, спасибо. Очень важно для каждого линтера и редактора вставлять комментарии (во вполне рабочий код), чтобы они были счастливы.


                // vim: set fileencoding=utf-8
                // -*- coding: utf-8 -*-
                // foostyle: no fs32, fs55, fs66
                // style2-no-unknown-comments
                // mylineter4:
                //   disable:
                //      - myl4
                //      - myl5
                
                void main() { // nazilinter: skip
                   // linter_from_company_bar: skip-doc-comment
                   if ( 42 == 42) {  // noqa // fascistlinter:skip another_analyzer: thank-you-no
                      return 0; // foostyle: skip
                 } // mylineter4: allow

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

                  +23
                  Более того, компилятору тоже придется указать на то, что данные в этом A[] могут меняться «внезапно» вне текущего контекста исполнения, и анализатор эти указания скорее всего тоже примет к сведению. И в #3 весь буфер будет volatile, в #2 — тоже, плюс там еще синхронизацией обмазываться в любом случае.
                  Если кусок памяти не помечен специальным образом, то оптимизирующий компилятор по стандарту вправе считать, что данные в нем «внезапно» не изменяются, и потому удалить вот эту повторную проверку. Анализатор, кстати, сообщает именно об этом, т.е. «ребята, data flow-анализ есть не только у меня, но и у вашего компилятора, вы действительно имели в виду то, что написали?»
                    +8

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


                    Имхо, большинство негативных отзывов приходят от людей, которыми анализаторами никогда не пользовались.

                      +13
                      Даже ложные срабатывания это полезно, и время на их начальное разгребание (т.к. когда у вас уже 10к+ строк кода, а анализатор до этого не использовался никогда) окупится сторицей и найденными багами (не видел я вот ни разу еще, чтобы в большом проекте на С или С++, в котором до этого анализаторы не использовались, ничего бы не нашлось), и повышением вашего профессионализма как разработчика на этих языках (потому что расследования ответов на вопросы вроде «да какого хрена ему тут то не нравится?» сильно расширяют кругозор).

                      Уже приводил этот пример в осуждениях PVS-Studio на opennet.ru, процитирую сам себя оттуда:
                      боролся я однажды с интересным багом при инициализации дополнительных ядер процессора AMD Merlin Falcon в прошивке. Нулевое ядро там назвают BSP (BootStrap Processor), а остальные — AP (Application Processor), так вот, инициализация очередного AP иногда зависала на ровном месте в ~0.1% случаев (а т.к. процесс работы прошивки до этого времени детерминирован, то в таких зависаниях чаще всего виновато оборудование или код, который его неправильно использует).
                      Зависающая прошивка — это show stopper, т.е. пока это не починим, дальше разрабатывать нет смысла. Я просидел две недели в попытках понять, что не так, и пошел пробовать статические анализаторы, которые давали попробовать (AMI использует CppCheck для своего кода, но он совсем слабый по сравнению с любыми коммерческими анализаторами).
                      В итоге PVS-Studio нашла переменную в структуре, у которой был пропущен аттрибут volatile, а в коде было что-то вроде такого:
                      // Prepare CPU context structure

                      apStructPtr->InSync = 0;

                      // Send context to AP

                      // Wait for AP sync
                      while(apStructPtr->InSync) {
                      // Still waiting
                      }

                      Т.к. ни apStructPtr, ни InSync не были помечены как volatile, то компилятор просто выбрасывал цикл, т.к. он не влияет на наблюдаемое поведение и аналогичен while(0), в результате получалась гонка между внутренними процессами в CPU и продолжением исполнения, и выигрывали её чаще всего внутренние процессы, т.е. все работало почти всегда. Но потом стало больше ядер, вышли новые процессоры и более долгой синхронизацией, и все сломалось.
                      А PVS-Studio сразу сообщила, что вот тут место подозрительное, проверьте. Проверил, добавил volatile, баг исчез. Вот так и помогает, вполне реально.
                        0

                        Негативные отзывы обычно когда людей заставляют пользоваться, в CI например внедрили, а для себя они пользы не видят.

                    +18
                    Изменения внутри кода программы в соседнем треде (хохо, какого джина я только что выпустил...)
                    Покуда элемент массива не является volatile, джин из бутылки вылезти не должен.
                      0

                      А вот интересный вопрос. Если у меня обработчик сигнала это меняет, это должно быть volatile или нет?

                        +8
                        Должно.
                        According to the C99 Rationale [C99 Rationale 2003], other than calling a limited, prescribed set of library functions, «the C89 Committee concluded that about the only thing a strictly conforming program can do in a signal handler is to assign a value to a volatile static variable which can be written uninterruptedly and promptly return.»
                        However, this issue was discussed at the April 2008 meeting of ISO/IEC WG14, and it was agreed that there are no known implementations in which it would be an error to read a value from a volatile sig_atomic_t variable, and the original intent of the committee was that both reading and writing variables of volatile sig_atomic_t would be strictly conforming.
                          +3
                          Допускаются любые оптимизации, не меняющие семантику выполнения при однопоточном выполнении.
                          Если у вас кто-то извне ломится — нужно использовать спецсредства.
                          volatile — это пометка «необычной» памяти. Есть и более специализированные инструменты типа атомиков и прочих.
                        +13
                        2) и 3) это UB в C/C++, если не используются volatile или примитивы синхронизации, а их анализатор может увидеть
                            0

                            Если это positive в опциональном инструменте, то их ценность — очень низкая.


                            Высокая ценность возникает, когда это гарантированная преграда, которую не обойти. И нет, я не говорю о "спецправилах в CI". Я говорю о языке программирования, который в силу разумных настроек, не пропустит smelly code ещё на этапе компиляции. Си, это, конечно, не касается, но в более благородных языках (aka Rust) компилятор может быть слишком подозрительным, но зато он одинаково подозрительный ко всем, без спецкомментов для "new shiny toy" коллеги по работе, не имеющих смысла для другого коллеги.

                              +4
                              Принуждение можно организовать по-разному, в Rust вот реализовали прямо в компиляторе (и это отлично), а на С никто не мешает внутри отдела RnD потребовать использования анализатора, либо поставить его на pre-commit hook и выдавать предупреждения сразу же там, плюс еще раз снова на ревью. Понятно, что это решение технических проблем административными мерами, но это тем не менее решение, и оно вполне подходит тем, кто свой код на Rust или SPARK не может просто так переписать — его там до этого писали 15 лет, и там его 500к строк, например.
                                –3

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


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

                                  +1

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

                                  +2
                                  Я говорю о языке программирования, который в силу разумных настроек, не пропустит smelly code ещё на этапе компиляции.

                                  Статический анализ — это вещь недешевая, на каждую локальную пересборку выполнять flow analysis дороговато, поэтому компилятор вряд ли когда-нибудь в обозримом будущем заменит статический анализатор, и от конкретного языка тут мало что зависит (тот же компилятор раста и не пытается ловить логические ошибки/опечатки типа «if (p[1] == 1) {… if (p[1] == 1)… }» или «if foo == 1 || foo != 1», да и не его это дело). Да и бессмысленно компилятору этим заниматься — если он, скажем, будет «гарантированно преграждать» использование условных выражений, которые always true, то это будет большим неудобством, например, при отладке. В общем, это скорее из разряда вредных советов.
                                    0
                                    Ну там в Rust пришлось внести очень серьезные ограничения на инварианты для ссылочных типов, чтобы анализ этот стал дешевле, но по сравнению с C и C++ компилятор медленный все равно. Совет не то, чтобы вредный, но вот это «прокрустово ложе до горизонта» нужно принять (кому-то это дается легче, кому-то — тяжелее). Есть огромное множество языков без него на выбор, и появление новых языков с ним я приветствую, потому что оно расширяет этот самый выбор. «Пусть расцветают сто цветов, и процветают сто школ».
                                      –1
                                      В расте эти ограничения ввели не для того, чтобы анализ был побыстрее, а для того, чтобы он в принципе был возможен и работал одинаково для всех. Даже в примере из статьи формально ворнинг не совсем верный, так как (и это в комментах уже писали) значение вполне может измениться в другом потоке. Даже можно обложить мьютексами с cond var, чтобы не было UB. Да, это странный код, но возможный, а значит его кто-нибудь да напишет, а кто-то другой начнет на этот код завязываться. (но тем не менее для адекватных людей проверка более чем полезная, да).
                                  +4
                                  Мы используем checkmarx и проблема в том что когда 99% ложно-позитивных срабатываний и насыпает он их от души, плюс каждая новая версия включает новые анализаторы и на старый код ещё пару сотню выдаст.

                                  Тогда получается что инструмент воспринимается как шумогенератор и реальная ценность падает.

                                  Это не критика вашего анализатора, а опыт использования при слишком большом количестве ложных сообщений.
                                    0
                                    Мы используем Coverity и серезно снизили накал ложных срабатываний моделированием.
                                      +1
                                      У нас контроль над настройками в другой команде и там позиция что лучше перебдеть, чем недобдеть. Т.е. если что, чтобы можно было показать логи и сказать, наш инструмент предупредил, всё ок. А то что это скрыто в тысячах бесполезных сообщений, это не имеет значения. Психология не учитывается.
                                        0
                                        Как я понимаю, это некий аналог [[noreturn]] функций? Анализатор PVS-Studio ориентируется на такие вещи. Аккуратно написанный код снижает количество ложных срабатываний. Например, диагностика V779 на это смотрит. И другие: пример.
                                        void error_1();
                                        [[noreturn]] void error_2();
                                        
                                        void warning()
                                        {
                                            int A[10];
                                            for (int i = 0; i < 20; i++)
                                            {
                                                if (i > 9)
                                                    error_1();
                                                A[i] = 1; // есть предупреждение
                                            }
                                        }
                                        
                                        void ok()
                                        {
                                            int A[10];
                                            for (int i = 0; i < 20; i++)
                                            {
                                                if (i > 9)
                                                    error_2();
                                                A[i] = 1; // нет предупреждения
                                            }
                                        }
                                        
                                          0
                                          Вот еще примеры. По сути, это такой способ пояснить анализатору, что вот эта твоя функция — это memcpy на самом деле, или malloc, а то, что она выглядит странно и вызывается через указатель на указатель — не повод пропускать ее на taint-анализе, и не повод ругаться на нее за то, что она повторяет якобы системную библиотеку, которой у нас все равно нет.
                                  0
                                  DMA операция, например

                                  Я тоже почему-то сразу подумал про DMA, но, видимо, в прикладном не-эмбед софте колдовство с DMA считается очень маловероятным событием, скорее свойственным разным вирусоподобным штукам типа сбивания отладчика с толку

                                  Еще можно вспомнить архитектуры с устройствами ВВ, замапленными на пространство ОП. Там последовательные чтения одного и того же, и даже просто обращения по адресам «ни для чего» очень даже осмысленны. Но ЭВМ общего применения с такой архитектурой уже архаичны (PDP и VAX ЕМНИП), а актуальные сейчас — разве что микроконтроллеры и SoC ;)

                                  Тем не менее, для массового программиста и прикладного софта линтер — скорее хорошая, чем плохая вещь. Это как ПДД для массового автолюбителя. Полезно и безопасно на дорогах общего пользования. Бессмысленно для автоспорта.
                                    +2
                                    Тут уже volatile все вспомнили, да и линтер хорош даже для системного программиста. Более того, для системного он особенно хорош, потому что цена ошибки там намного выше, и потому любые инструменты по их раннему обнаружению — это безусловное благо.

                                    Все вот эти хитрые трюки, которые необходимы на системном уровне — их тоже можно и нужно пояснить и компилятору, и анализатору. «Я знаю, что делаю, не мешай», как с unsafe в Rust. И это тоже замечательно, посколько позволяет потом обычным поиском по тексту найти все места с практической магией, и обратить на них внимание в первую очередь при отладке действительно сложных проблем.
                                      +2

                                      Вы сильно переоцениваете то, что можно делать в unsafe. Даже в unsafe там такой уровень type checking, что среднему языку и не снилось.


                                      Я вот, недавно, столкнулся с тем, что у меня массив не давал сделать #[derive (Copy)] (поддержку копирования).


                                      Полез — внутри структуры был массив, состоящий из option внутри которых SyncSender. SyncSender по своей реализации не поддерживает копирование (чтобы не было путаницы), и по вложенности, Option от него не поддерживает копирование, и массив не поддерживает копирование, и структура с массивом не поддерживает копирование. И всё это автоматом защищает от выстрела в ногу. Даже в unsafe-коде.

                                        0
                                        Я согласный, я тут больше про то, что по unsafe очень удобно искать в случае серьезных непонятных проблем, чтобы проверить эти блоки первыми. К сожалению, нарушения инвариантов в unsafe нелокально, т.е. способно приводить к поломкам safe-кода хрен знает в какой дали от самого нарушения, но даже с этим всем оно сильно лучше, чем у остальных, особенно у C и C++.
                                      +1

                                      На самом деле DMA — это всё-таки экзотика. А вот io_uring — запросто (https://kernel.dk/io_uring.pdf).


                                      Я не спорю, что линтеры полезны. Я, скорее, сомневаюсь, что "отключите ложную проверку комментарием" — это полезно. Скорее, нет. Захламляет код и исправляет чужие (авторов линтера) косяки.

                                      0
                                      Если память изменяется снаружи, об этом нужно рассказать компилятору. Например ключевым словом volatile. Если его нету, то это ошибка.
                                        +3
                                        Это код на C.
                                        Без явного указания «переменная может изменяться извне» компилятор волен считать, что извне она изменяться не может, поэтому все дополнительные проверки могут быть (и будут, при включенной оптимизации) выброшены, о чем, в принципе, и сообщает анализатор.
                                        +1

                                        А если дважды сравнивается volatile переменная? И учитывает ли PVS возможные асинхронные изменения?


                                        Не камень в огород, правда интересно.

                                          +1
                                          В большинстве случаев volatile переменные рассматриваются особенно и сделаны специальные исключения в диагностиках. Кое-что про асинхронность PVS-Studio тоже знает.
                                          –1
                                          if (A[0] == 0)
                                          {
                                            X = Y;
                                            if (A[0] == 0)
                                              ....
                                          }


                                          Да элементарно, хочется проверить что содержимое буффера не изменилось с тех пор (например счеткик в таймере). В некоторых системах такое может быть критично.

                                          С тем, что стоит выдавать предупреждение — полностью согласен, но называть это опечаткой — пусть код-ревьювер решает.
                                            +6
                                            Примечание общего плана. Мне не очень понятна страсть к рассмотрению в комментариях каких-то особенных случаев (асинхронность, DMA и так далее). На одно ложное срабатывание из-за такой причины, приходится 100 обыкновенных ошибок-опечаток. Может ли анализатор ошибаться? Да, может. Однако, на один хитрый особенный низкоуровневый if приходиться 5000 обыкновенных. Давайте получать пользу от их проверки :).
                                              0
                                              Асинхронность в общем виде это не особенный случай. И еще — хотя в циферки типа 1 к 100 я и готов поверить, но при этом ошибку в асинхронном коде поймать зачастую на порядки сложнее, чем 5000 обыкновенных. Так что польза от правильной обработки была бы большая.
                                                +2
                                                Меж строк читается предположение, что статические анализаторы не знают про асинхронность :). А они кое-что вполне знают и учитывают :). И даже имеют некоторые специализированные диагностики (пример, пример).
                                                  +9
                                                  Ну, нет. Я все-таки не об этом. Вот у меня есть сонар, и он делает эти вот ваши 5000 проверок каждую сборку (я понимаю, что он не ваш, и вообще сравнивать их вот так некорректно, поэтому это просто пример). За примерно три года он нашел примерно 1 баг. Да и то, это было не прямое указание, а скорее намек. В тоже время свои 1000 ложных срабатываний он трудолюбиво выдает каждый раз, и время на проверку тоже тратит постоянно. Уж сколько рабочих часов он моих сожрал, прямо скажем…

                                                  Опять же — это не сравнение, потому что больше всего в диагностике сонара достают скажем прямо, результаты вкусовщины — т.е. советы типа: «А вот эту лямбду замените ссылкой на метод (java)». Потому что кто-то решил, что ссылки на метод проще поддерживать. Ну то есть, это вообще не баги, и даже не претендуют ими быть. Это кто-то обобщил свой опыт веб программирования (например), и распространил его на всю компанию, включая проекты других типов.

                                                  И да, если вы думаете, что багов не было — как-бы не так бы. Были конечно, штук по 10 в месяц регистрируем. Как правило там, где статический анализ бессилен по определению — т.е. например на стыке с БД, где может внезапно поменяться схема данных.

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

                                                  А польза от сложных могла бы быть очень высокой — но обычно упирается в технические ограничения статического анализа как такового.
                                                +1

                                                Не соглашусь, в разработке под микроконтроллер, это совсем не особенный случай, почти в каждой программе такое встречается и не раз, как минимум обработка прерываний — это основа. ОСРВ и DMA также.
                                                Почти все критические ошибки с ними и связаны. Другое дело, что подходить к разработке таких модулей нужно с умом, но это не отменяет того факта, что для ембед программиста — это типичный случай.

                                                +1

                                                Какой-то совсем примитивный пример. Что будет при таком варианте?


                                                 if (p[2] == 2) {
                                                        bar(p);
                                                        if (p[2] == 2)          // ??
                                                            return 2;
                                                }
                                                
                                                void bar(const char*);

                                                А если оператор == переопределен, проверяется ли, что функция сравнения чистая?


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


                                                if(a == 5) {
                                                        ENTER_CRITICAL_SECTION();
                                                        if(a == 5) {
                                                                ...
                                                        }
                                                        EXIT_CRITICAL_SECTION();
                                                }
                                                  +3
                                                  Разное будет… Зависит от volatile. Зависит от, что такое такое bar() и что такое p. Если p это член класса, а bar() константная функция класса, то значит p меняться не может и это одна ситуация. А если это не константная функция, то эта другая ситуация… Смотря что такое ENTER_CRITICAL_SECTION… Если хочется получить ответы, как и что будет в конкретном случае, предлагаю поиграться с PVS-Studio online.

                                                  Если же вопрос: Можно запутать PVS-Studio? Ответ: Да, можно. :)
                                                    0

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

                                                      0
                                                      (и на самом деле вполне раскрывается через тред-аннотации, <...>)

                                                      Вы меня заинтриговали. Что за аннотации такие?

                                                        0

                                                        Тема довольно мощная, но к сожалению очень бедная на примеры и документацию (на хабре одна статья всего, да и та перевод кусочка документации LLVM; авторских статей вообще не встречал на русском) https://habr.com/ru/company/infopulse/blog/304176/


                                                        Общая идея проста: можно пометить аннотациями мьютексы и треды. Можно пометить аннотациями сущности, которым нужны эти мьютексы или треды. Можно пометить аннотациями функции, которые используют аннотированные сущности и мьютексы/треды. И по этим аннотациям компилятор будет выдавать варнинги, где нужно.
                                                        Например,


                                                        CSphVector<CSphIndex*>      m_dDiskChunks GUARDED_BY ( m_tChunkLock );

                                                        • работа с m_dDiskChunks должна быть под локом m_tChunkLock.
                                                          Позже прямо при написании кода я получаю подсвеченный фрагмент с тултипом:

                                                        А при компиляции — такой же варнинг:


                                                        src/sphinxrt.cpp:4360:49: warning: passing variable 'm_dDiskChunks' by reference requires holding mutex 'm_tChunkLock' [-Wthread-safety-reference]
                                                                        CSphFixedVector<int> dNames = GetIndexNames ( m_dDiskChunks, false );
                                                                                                                      ^

                                                        Фича поддерживается по большей части на clang (на самом деле, именно ради неё я держу его в тулчейне), и отчасти на gcc.
                                                        Отсутствие подробной документации вынуждает экспериментировать, но зато результаты потом радуют.


                                                        Примеры можно найти на гитхабе, например, в нашем же проекте, или просто поиском по ключевым словам (вроде REQUIRES_SHARED или NO_THREAD_SAFETY_ANALYSIS). Для подсветки я добавил -Wthread-safety во флаги clangd на CLion.

                                                          0

                                                          А можно просто использовать Rust ¯\_(ツ)_/¯

                                                            +3

                                                            Можно. Если начать с нуля. Но...


                                                            Проект родился тогда, когда Rust не было. Совсем не было. Потом, спустя пару-тройку лет, в течение которых появились первые десятки тысяч строк кода проекта он появился. Официально. Работал ли он в то время на какой-нибудь реальной ОС, причём в продакшне, а не в виде "идеи" (Solaris? Windows XP?) — неизвестно.


                                                            А нынче он уже давно жив. И поэтому — нужно сперва сделать, чтобы он беспроблемно собирался и быстро (есть бенчмарки!) работал на актуальных системах (rhel6/trusty/wheezy — rhel8/focal/buster, а также win(7..10), macos (10.14..10.15) и фряха). Да, и ещё сторонние либы! Всевозможные коннекторы под sql/expat/odbc/openssl/icu/snowball должны прямо, просто и легко линковаться или встраиваться (причём достаточно частое требование — чтобы либа была не из какого-то репозитория Васи с гитхаба, написавшего её на коленке (где вся мощь/безопасность/возможности языка разбивается нафиг о текстовый disclaimer в репозитории, не имеющий никакого отношения к либе, но отправляющий всю "безопасность" коту под хвост), а официальная, от авторской компании или официального мейнтейнера, который гарантирует безопасность/функциональность, не прикрываясь "поверх компилятора" такими дисклеймерами).


                                                            А потом — переписать ~200Kloc легаси-кода на плюсах. Часть на С++98, часть на следующих, часть на С++11, кусочки на С++14. Отчасти аннотированный, отчасти полный легаси. Отчасти даже со вставками на ассемблере.


                                                            И желательно с той же командой (для некоторых из которых даже RAII вызывает эмоцию "ааа! Дэвид Блэйн!")


                                                            Иначе такая критика — откровенно НЕконструктивная (не несёт реальной пользы). Мы живём в реальной жизни на реальном проекте его же поддерживаем и развиваем с учётом наследия, а не строим воображаемые замки на некоем "идеальном языке в вакууме".

                                                              0
                                                              У Rust есть поддержка, скажем, такой штуки: www.xilinx.com/products/design-tools/microblaze.html?
                                                      +2
                                                      И зря обижаетесь. Анализатор не человек, и ложных срабатываний у него хватает. Вот например давеча на код C# примерно такого содержания:
                                                      if (defaultStringObjects.Count == 0) {
                                                          LogNullError(nameof(defaultStringObjects));
                                                          return true;
                                                      }
                                                      
                                                      ...
                                                      
                                                      float translationCompleteness = currentStringObjects.Count / (float) defaultStringObjects.Count;
                                                      
                                                      


                                                      Анализатор выдал ошибку
                                                      V3152 Potential division by zero. The variable was compared with a range of values which includes zero before it was used as a divisor. Check lines: blablabla


                                                      Человеку — очевидно, что случая деления на ноль быть не может, потому что будет return. А анализатору — не очевидно. Анализатор не заменит вдумчивое чтение кода, просто позволит вдумчиво читать не весь код. Он (имхо) для этого и нужен, чтобы сократить объём работы.
                                                        +3

                                                        А что такое в этом коде defaultStringObjects если не секрет? Если это не дай бог List, то никаких гарантий, что между проверкой .Count на 0 и его использования в качестве делителя список не очистится. Ну то есть, как и в примере из статьи, фрагмент кода вызвает много вопросов не только у анализатора, но и у обычного программиста :) А вот если это массив или какой-нибудь IReadonlyList, то да — нужно писать ребятам в PVS. Я о таких вещах обычно сообщаю и рано или поздно они фиксятся.

                                                          +1
                                                          Опять-таки, IReadOnlyList — это коллекция, которую пользовательский код не может изменить, но это не значит, что обёрнутая коллекция не изменилась.
                                                          +1
                                                          Здравствуйте. У нас есть механизм, который отслеживает подобные ‘return’. По данному фрагменту трудно понять, почему анализатор выдал предупреждение. Например, если написать вот так, то сообщение выдано не будет:

                                                          public static void Main()
                                                          {
                                                              List<string> defaultStringObjects = GetListRandomLenght();
                                                          
                                                              if (defaultStringObjects.Count == 0)
                                                              {
                                                                  return;
                                                              }
                                                             
                                                              float check = 10 / (float)defaultStringObjects.Count;
                                                          }

                                                          Поэтому пожалуйста, напишите нам об этой проблеме в поддержку, приложив более полный фрагмент кода. Спасибо.
                                                            0
                                                            В Вашем примере переменная локальная, и ссылка никуда не утекает.
                                                            В примере же выше природа переменной/поля defaultStringObjects неясна.
                                                          +4
                                                          Программисты могут не переживать. Современные инструменты продвинуты и выдают предупреждения только на действительно подозрительный код.
                                                          А может это подсознательная боязнь того, что какой-то другой разработчик оказался умнее него? :)
                                                            +2
                                                            Я таки не понял, PVS учитывает атрибуты переменных и функций?
                                                            Или картинка актуальна?
                                                              0
                                                              Вопрос расплывчатый. Если в целом, то ответ: да, учитываются.
                                                              +1
                                                              А если так:
                                                              int A[ 2 ] = { 1, 0 };
                                                              
                                                              int& X = A[ 0 ];
                                                              int& Y = A[ 1 ];
                                                              
                                                              void foo( );
                                                              
                                                              int main( )
                                                              {
                                                              	foo( );
                                                              	return 1;
                                                              }
                                                              
                                                              void foo( )
                                                              {
                                                              	if( 0 ==  A[ 1 ] )
                                                              	{
                                                              		Y = X;
                                                              
                                                              		if( 0 == A[ 1 ] )
                                                              			return;
                                                              	}
                                                              	return;
                                                              }

                                                              «поймет» ли анализатор что Y = X изменит A[ 1 ]?
                                                              если через memcpy память менять причем хитро… что то типа memcpy( A-4, B, 16 );
                                                              ну допустим А знаю что А часть структуры и по адресу А-4 есть поля структуры которые надо изменить — понимаю что так несколько ректально но это же гипотетически ))
                                                                +6
                                                                Поздравляю, Вы запутали анализатор. А заодно и всех, кто будет сопровождать этот код. :)
                                                                  0
                                                                  могём )
                                                                    0
                                                                    Хотел бы я посмотреть на code review такого кода :)
                                                                      +1
                                                                      В прод такой код только отморозок пустит. А вот выстрелить себе в ногу сто одним способом ( ну чисто теоретически ) можно ради развлечения. :)
                                                                        0

                                                                        Видел такой код в проде в некоторых достаточно требовательных к производительности местах. Без UB иначе там сделать было нельзя.

                                                                          0
                                                                          Может стоило написать на asm?
                                                                          Прямо даже интересно стало, что там за код такой, что ни новые стандарты, ни GNU extensions, ни флаги вроде -flto, не помогли добиться желаемого. // Не то, чтобы я совсем отрицал такую ситуацию, но с годами всё меньше и хотелось, и требовалось придумывать свои «гениальные» велосипеды
                                                                            0
                                                                            Может стоило написать на asm?

                                                                            Ну, почти. Было в планах написать кодогенератор в LLVM IR или что-то подобное, но состояние рынков начиная с февраля этого года нарушило эти и многие другие планы.


                                                                            Прямо даже интересно стало, что там за код такой, что ни новые стандарты, ни GNU extensions, ни флаги вроде -flto, не помогли добиться желаемого.

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

                                                                +2
                                                                А никто не помнит анекдот про японскую пилу и лесорубов с ломом? Только у нас тут программисты и статический анализатор.
                                                                  0

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

                                                                    –2
                                                                    Спасибо за этот анекдот. Он послужил основой дня новой статьи :).
                                                                    +1
                                                                    Если не секрет. а какие артефакты анализатор оставляет после себя при проверке кода? Мы можем не пересобирать проект целиком, а что позволяет не перепроверять целиком?
                                                                    0
                                                                    Естественно, любой современный статический анализатор отслеживает изменение значения переменных. Если переменная не меняется, будет сообщение. Если меняется, сообщения не будет. Для этого используется технология анализа потока данных.


                                                                    А как при этом учитывается возможность алиасов указателей (насколько я помню это неразрешимая, в общем случае, проблема для языков с возможностями C \C++)?
                                                                      –2
                                                                      Слабенько учитывается. Но с практической точки зрения, эта не такая уж проблема.
                                                                        0
                                                                        Но с практической точки зрения, эта не такая уж проблема.

                                                                        … На фоне всех остальных. Потому что алиас указателей компиляторы C/C++ корректно обрабатывать не умеют.

                                                                          0
                                                                          … На фоне всех остальных. Потому что алиас указателей компиляторы C/C++ корректно обрабатывать не умеют.

                                                                          Умеют, просто надо пользоваться проверенными решениями (gcc) а не этой новомодной ерундой, с которой вечно проблемы. Хотя прямо таких дефектов компиляции я ещё не видел, но:
                                                                          1) писать компилятор на прикладном с++ вместо системного с
                                                                          2) компилятор компилируется (сам себя) около суток на обычном компе
                                                                          3) всякие мелкие педантичные пакости типа запрета unsigned int argc в main (который формально не по стандарту, но по факту не существует систем где оно несовместимо, да и стандарт кажется запрещает int и unsigned int иметь разные представления для положительных чисел) — причём это не варнинг а ошибка, да ещё и неотключаемая
                                                                          4) включёные по дефолту идиотские варнинги о рекомендации писать (A&&B)||C вместо A&&B||C и ещё какие-то (не помню) — создают впечатление что оно сделано для умственно отсталых
                                                                          Хотя из этого всего напрямую не следует, что он ещё и компилирует дефективно, но следует что его авторы, мягко говоря, странные люди, и лучше их продукты избегать.

                                                                            +1

                                                                            Давайте обсудим.


                                                                            писать компилятор на прикладном с++ вместо системного с

                                                                            Я бы ни на том, ни на другом не писал компилятор, но чем C++ хуже для этих задач, чем C?


                                                                            А, ну и с разморозкой: в gcc тоже разрешили C++.


                                                                            компилятор компилируется (сам себя) около суток на обычном компе

                                                                            Машина семилетней давности:


                                                                            % qlop -a clang llvm
                                                                            sys-devel/clang: 11′36″ average for 36 merges
                                                                            sys-devel/llvm: 13′37″ average for 35 merges

                                                                            Хмм, 25 минут на clang + llvm (при этом второй ещё используется в mesa, как один из бекендов в ghc, и так далее, чего gcc не умеет делать).


                                                                            % qlop -a gcc       
                                                                            sys-devel/gcc: 30′19″ average for 50 merges

                                                                            Что-то как-то не сильно быстрее. Даже вообще не быстрее.


                                                                            всякие мелкие педантичные пакости типа запрета

                                                                            Приучайтесь писать код по стандарту.


                                                                            включёные по дефолту идиотские варнинги о рекомендации писать (A&&B)||C вместо A&&B||C и ещё какие-то (не помню) — создают впечатление что оно сделано для умственно отсталых

                                                                            А я не хочу вспоминать, && или || приоритетнее, и требовать это от читателей моего кода. Это хороший, годный ворнинг, повышающий читабельность.


                                                                            Хотя из этого всего напрямую не следует, что он ещё и компилирует дефективно, но следует что его авторы, мягко говоря, странные люди, и лучше их продукты избегать.

                                                                            На практике clang последние пару лет генерирует в среднем более эффективный код, чем gcc.

                                                                              0

                                                                              Ну я помню что при пересборке FreeBSD сначала целый день компилируется clang, а потом за пару часов всё остальное. Но возможно это какие-то особенности системы, сейчас лень выяснять.


                                                                              Я бы ни на том, ни на другом не писал компилятор, но чем C++ хуже для этих задач, чем C?

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


                                                                              Приучайтесь писать код по стандарту.

                                                                              Стараюсь писать по стандарту, но не следует веровать в его идеальность и безальтернативность. Повторюсь, от unsigned int argc ничего нигде не сломается, и по факту gcc просто поддерживает эту вариацию, не прилагая для этого никаких специальных усилий, а в clang вставили пакостную неотключаемую ошибку, без какой либо вменяемой причины, просто чтобы заставить программистов писать так, как нравится авторам clang'а. А может кто-то использует нестандартное libc и там вообще прототип main другой? Зачем этому мешать, причём так агрессивно? Да, пусть компилятор предупреждает включённым по дефолту варнингом "кажется у этого main нестандартный прототип" (хотя у gcc по дефолту отключено, -Wmain), это и правда с большой вероятностью окажется просто опечатка, но принудительно запрещать это — бред, программист лучше знает что ему нужно. По такой же логике компилятор перед началом работы должен проверять наличие в файловой системе файлов stdio.h и подобных, и аварийно завершаться, если их нет (даже если в компилируемом исходнике нет к ним отсылок) — ведь у этой ОС не соответствующие стандарту инклюды, а значит надо запретить компиляцию под неё.
                                                                              На всякий случай пояснение: main — это обычная функция, вызываемая из startup-кода в libc, и компилятору в общем случае вообще нет нужды как-то по особому её интерпретировать (кроме синтаксического сахара с необязательным return'ом).


                                                                              А я не хочу вспоминать, && или || приоритетнее, и требовать это от читателей моего кода. Это хороший, годный ворнинг, повышающий читабельность.

                                                                              Вам никто не мешает расставлять скобки вокруг &&. Более того, никто не мешает включить это предупреждение там, где по дефолту его нет, если без него вы скобки ставить забываете (хоть и хотите). Но по-моему очень сложно не запомнить порядок основных операций, использующихся в условиях (арифметика -> сравнения -> && -> ||; кстати, насчёт арифметики, в формуле a+b*c тоже надо скобки ставить a+(b*c)?), пользуясь языком чаще чем раз в год, а лишние скобки зрительно засоряют код и тратят площадь экрана, так что вполне вероятно программист не захочет их писать. Ориентировать же дефолтные настройки предупреждений на пользующихся языком раз в год — странно.
                                                                              Кстати я вот иногда дорабатываю свой же код 10+ летней давности — там везде эти скобки расставлены, убираю их по мере столкновений. Но мне тогда совершенно не требовались варнинги чтобы их ставить.

                                                                              0
                                                                              Умеют, просто надо пользоваться проверенными решениями (gcc) а не этой новомодной ерундой, с которой вечно проблемы.

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

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

                                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                        Самое читаемое