Почему embedded-разработчикам следует использовать статический анализ кода

    Поверь в статический анализ кода!

    Решил кратко сформулировать 3 причины, по которым embedded-разработчикам полезны инструменты статического анализа кода программ.

    Первая причина: не надо тратить время на мучительный поиск некоторых ошибок


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

    Особенно полезен статический анализ может быть при отладке embedded-систем. В таких проектах разработчики сталкиваются не только с ошибками в программах, но и с ошибками в самом устройстве или с некачественным изготовлением макета (плохой контакт и т.п.). В результате, процесс поиска ошибки может сильно затянуться, так как часто непонятно, где её искать. Если программист посчитает, что код написан правильно, то это может повлечь долгие изыскания с привлечением схемотехников и других коллег, отвечающих за hardware-часть. Тем неприятней будет позже вернуться к коду программы и обнаружить, наконец, дурацкую опечатку. Колоссально неэффективный расход сил и времени коллектива. Великолепно, если такую ошибку найдёт статический анализатор.

    Вот как один знакомый описывал мне подобную ситуацию:

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

    Мне ставится очередная типовая задача, с которой я справляюсь буквально за пару часов и отдаю её двум коллегам для прошивки в созданное ими устройство. Коллеги удивляются, как быстро всё сделал, хвалят, на что я гордо заявляю „ну я уже профессионал подобные вещи писать и вообще там всё просто“. Коллеги удаляются с флешкой, куда я им записал бинарный файл для прошивки микроконтроллера.

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

    А они пришли. Но только через неделю. Говорят, ничего не понимаем. Всю голову сломали. Не работает наш стенд. Вернее, работает, но не совсем как надо. Мы уже перепаяли его заново и исполнительные электромеханические детали заменили. Не работает… Может, посмотришь? Может, всё-таки, в программе что-то не так…

    Открываю код и сразу вижу ошибку в духе:

    uchar A[3];
    ....
    for (uchar i = 0; i != 4; i++)
      average += A[i];
    average /= 3;

    За основу был взят другой мой проект, и код во многом написан методом Copy-Paste. В одном месте я забыл заменить 4 на 3. Мне так стыдно было, что я двух людей заставил проработать впустую неделю.»

    Вторая причина: обновить программу дорого, невозможно или поздно


    Ошибки во встраиваемых устройствах крайне неприятны тем, что их невозможно или почти невозможно исправить, если началось серийное производство. Если уже выпущены сотни тысяч стиральных машин и они разъехались по магазинам, то что делать, если в определённом режиме машина работает неадекватно? В общем-то вопрос риторический и реальных вариантов два:

    1. Смириться и получать негативные отзывы клиентов на различных сайтах, портя свою репутацию. Можно, конечно, выпустить и разослать к инструкции приложение «не делайте вот так», но тоже слабый вариант.
    2. Отозвать серию и заняться обновлением прошивок. Дорогое развлечение.

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

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

    Статический анализ кода не гарантирует отсутствие ошибок в коде. Однако просто необходимо использовать любую возможность дополнительно проверить корректность кода. Статические анализаторы могут указать на множество разнообразнейших ошибок, которые умудряются остаться в коде даже после нескольких Code-Review.

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

    Третья причина: программист может не знать, что делает что-то неправильно


    Ошибки в программах образно можно разделить на два типа. Про ошибки первого типа программист знает, и появляются они в коде случайно, по невнимательности. Ошибки второго рода возникают в ситуации, когда программист просто не знает о том, что так писать код нельзя. Другими словами, он может сколько угодно читать такой код, но всё равно не найдёт ошибку.

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

    Другим примером может служить неопределённое поведение программы, которое возникает из-за неправильного использования операторов сдвига <</>>. Эти операторы очень широко используются в коде микроконтроллеров. К сожалению, программисты часто используют эти операторы крайне безалаберно, делая программы ненадёжными и зависимыми от версии и настроек компилятора. При этом программа вполне себе может работать, но вовсе не потому, что написана правильно, а потому, что везёт.

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

    Заключение


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

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

    1. Сократить время на поиск и устранение ошибок (пример);
    2. Уменьшить вероятность критических ошибок;
    3. Уменьшить вероятность необходимости обновления прошивок;
    4. Контролировать общее качество кода (рекомендуем дополнительно посмотреть в сторону SonarQube);
    5. Контролировать качество работы новых членов команды;
    6. Контролировать качество кода сторонних модулей/библиотек.

    Нет причин не использовать статический анализ кода, кроме лени и иллюзии превосходства.

    Нам некогда использовать статический анализ кода


    Используйте статические анализаторы кода! Их много.

    Мы, естественно, предлагаем обратить внимание на наш анализатор кода PVS-Studio, который недавно начал поддерживать ряд ARM-компиляторов.


    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why embedded developers should use static code analysis.
    PVS-Studio 237,35
    Ищем ошибки в C, C++ и C# на Windows, Linux, macOS
    Поделиться публикацией
    Похожие публикации
    Комментарии 73
    • 0
      Вот, наконец-то SonarQube упомянули! Когда можно будет сравнить ваш java анализатор с его бесплатным и встроенным?
      • 0
        PVS-Studio не анализирует Java код
        • 0
          Да, вы правы. Но была публикация «PVS-Studio 2018: CWE, Java, RPG, macOS, Keil, IAR, MISRA» где анонсировали возможное появление анализатора для java. Мы дискутировали в комментариях, что эта ниша уже насыщена достаточно качественными бесплатными анализаторами.
          • 0
            Так ведь и ниша C++ насыщена бесплатными анализаторами (Cppcheck, Clang, ...). :)
            • 0
              Надо пообщаться с коллегами, почему они остановили свой выбор именно на pvs-studio для C++. Возможно причина не только в хорошем маркетинге. Статьи тут очень часто отличные!
        • +3
          К сожалению, на Java направление у нас пока выделено очень мало ресурсов. Так что ничего пока сказать не могу. Сейчас мы сосредоточены над C++: поддержкой macOS, классификацией предупреждений согласно CERT, внедрением в ядро символьных вычислений, embedded.
          • 0
            Уверен, что с embedded вашей компании лучше удастся продавать анализатор! Рынок важен с точки зрения надежности и высокой стоимости доставки обновлений. В области статического анализа Java кода достаточно высокая конкуренция…
            • +1
              Как будто в области статичекого анализа для C++ конкуренции нет… :)
              List of tools for static code analysis: C, C++; Multi-language.
            • 0
              Это же отлично, что упоминаете удобный инструмент статического анализа кода и continuous code quality! По прочтению этой публикации почему-то вспомнил ваш комментарий:
              sonar — использовали в одном из проектов. Не впечатлил.
          • +2
            Может быть вы объясните это острое нежелание показывать цены у отечественных производителей? Почему надо обязательно писать менеджеру, чтобы просто узнать цену?
            • +2
              Отечественность производителя здесь ни при чём, тем более, что мы ориентированы на иностранных клиентов. Есть классы продуктов, где невозможно просто написать цену, так как она формируется из количества лицензий, скидок при покупке на несколько лет, наценки реселлера и так далее. По этой же причине, вы не увидите цены для таких статических анализаторов как Coverity, Klocwork, Parasoft.

              Ориентировочную же цену вполне можно прикинуть, изучив страницу "Купить".
            • +1

              Сможет ли PVS-Studio выловить баг при копипасте, если код будет следующим?


              uchar A[3];
              ....
              for (uchar i = 0; i != 3; i++)
                average += A[i];
              average /= 4;

              То есть изначально код был на 4 элемента, его скопипастили и в одном месте забыли заменить 4 на 3.

              • +2
                Нет. Непонятно, к чему придраться. Можно конечно сделать жуткое эмпирическое правило, основанное на названии переменной в которой есть «average», но на практике, это всё равно не будет работать. Динамический анализ здесь тоже не поможет. Подобные ошибки следует выявлять юнит-тестами. Именно из-за разнообразия ошибок, все эти методологии поиска ошибок не конкурируют, а дополняют друг друга.

                Профилактикой может быть правило в стандарте кодирования компании или просто любовь к написанию качественного кода, что не позволяет использовать такие явные константы как 3, 4. Использовалась бы именованная константа, и беды не было.

                P.S. Есть ещё доказательство корректности программ и соответствующие инструменты, но это слишком медленно и дорого для подавляющего большинства проектов. Формализация (описание) как работает функция занимает больше, чем сама функция. Плюс там есть масса сложностей, с такими языками как C++.
                • 0

                  А какова вероятность, что разработчики, у которых не принята культура юнит-тестирования (а если бы это было не так, то и оригинальный баг из статьи скорее всего отловили бы), приобретут и будут систематически пользоваться (не говоря уже о CI) статическим анализатором?

                  • +2
                    Нерационально обсуждать тему, что всё плохо и что всё тлен. Давайте лучше по возможности доносить до программистов мысль, что качество кода — это очень важно, особенно когда речь идёт о управлении чем-то внешним. Я, например, в статьях рассказываю об одной из методологий — статическом анализе кода. Предлагаю и другим нести свет в царство тьмы. Особенно бывает темно как раз в мире embedded-систем. Кстати, у меня уже была заметка на эту тему: "Заземлённые указатели".
                    • +1
                      Кто бы рассказал, как это самое unit-тестирование сделать для встраиваемых проектов (для обычного PC, как я понимаю, это может делать некий фреймворк или IDE (я нифига не нашёл в инете unit-тестирование не с помощью фреймворков и не для абстрактной простой программы. Так оно для меня и осталось загадкой...)? Иначе протестировать сотню функций, делая по программе-тесту на каждую (100 штук!) я даже не представляю как! )? Вот есть у меня программа для микроконтроллера, которая, вообще говоря, нифиговая в принципе и в логике работы и в размерах (потому что это прошивка одного из 4 взаимодействующих между собой через ДОЗУ контроллеров платы прибора). Я бы с радостью тестировал бы всё это, но как? Там даже не Си++, а просто Си.
                      • +1
                        Разделять программу на обособленные блоки (как вариант, в ООП-языках — классы, на Си, в принципе, тоже можно писать в объектном стиле с некоторыми ухищрениями), разделять логику работы и слой взаимодействия непосредственно с железом (HAL)…
                        … и писать портируемый код, чтобы обособленные блоки, реализующие логику, можно было компилировать и запускать на ПК по тестовым сценариям используя любой тестовый фреймворк.
                        Ну или гонять тесты на самой борде, но это может оказаться уже сложнее.
                        В принципе, если для вас «архитектура» и «стандарты написания кода» — не пустой звук, то скорее всего пункты из первого абзаца у вас уже выполняются (надеюсь, у вас не проект с божественными объектами и сущности на сотни строк).

                        А самый простой тестовый фреймворк — набор макросов для запуска выбранной функции с определенными комбинациямиаргументамов и проверки, что получилось на выходе или что поменялось в окружении после работы.
                        • +3
                          Я не понимаю вот что: если я каждую функцию/класс обложу тестами с окружением всего, что ей нужно, то тест окажется больше самой программы в несколько раз/десятки рах! А привязку к HAL придётся делать указателями на функции HAL (иначе их не подменить).
                          Вот сейчас для проекта у меня ПО микроконтроллера как раз и пишется на обычном PC (под QNX) на Си с обвязкой на Си++ с имитацией драйверами всего окружения аппаратуры и имитацией HAL. Всё вроде бы хорошо, но один из процессоров необычный (отечественный аналог TMS середины 80-х) — у него байт 32 бита (да, и sizeof(char)=1 и char=32 бита), компилятор с Си 89 (а может и раньше) и глючной работой кое-чего в Си ( скажем, int a[2]={1,2}; компилируется, но инициализация не выполняется :) ). Как это всё поведёт себя на реальном процессоре — вопрос.

                          А что касается тестов вообще, я не понимаю, как всё это обложить тестами правильно. Писать ли для каждой функции/класса отдельную программу? Тогда программ будет масса — ведь каждому классу нужно имитировать окружение. Или тест надо встраивать в саму программу — это приведёт к тому, что программа резко распухнет и будет сложно разделить тестовую часть от штатной. А проверка логики работы будет заключаться во взаимодействии классов и потребует имитации этой же логики в тесте (иначе как проверить, что логика отработала верно). Вот всего этого я и не понимаю.
                          • +2
                            Я не понимаю вот что: если я каждую функцию/класс обложу тестами с окружением всего, что ей нужно, то тест окажется больше самой программы в несколько раз/десятки рах!

                            Да, это нормально, так и должно быть.
                            Посмотрите, например, на проект SQLite: у них 125 тысяч строк кода непосредственно библиотеки, и 91 миллион строк кода тестов — то есть тесты занимают примерно в 750 раз больше по объему, чем сама либа :)
                            А привязку к HAL придётся делать указателями на функции HAL (иначе их не подменить).

                            Да, как один из вариантов. Или более тупой и менее красивый, просто include'ить другой файл с теми же сигнатурами методов в зависимости от define'а при сборке.
                            А что касается тестов вообще, я не понимаю, как всё это обложить тестами правильно. Писать ли для каждой функции/класса отдельную программу?

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

                            А тут работают буквы S и D в принципе SOLID: single responsibility и dependency injection. У одной единицы — одна ответственность, следовательно, ее можно протестировать не затрагивая и не задействуя другие сущности. Ну и всегда есть mock- и stub-объекты, которые можно использовать вместо реальных, чтобы не воспроизводить окружение целиком.
                            • 0
                              Да, это нормально, так и должно быть.


                              Я ж застрелюсь. :)

                              Не совсем понял, что вы понимаете под «отдельной программой»,


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

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


                              Спасибо за информацию. :) Статьи я читал, но всё равно масса непоняток для реального проекта остаётся.

                              У одной единицы — одна ответственность,


                              Часто при таком подходе трудно запомнить связи между сущностями. Скажем, у меня есть менеджер управления электропитанием, менеджер времени, менеджер имитации ошибок, менеджер обмена с аппаратурой и так далее. Так вот, удобно их связывать воедино с помощью одного класса-системы. Иначе связь между этими менеджерами трудно отследить (а через класс системы — просто — все обращаются к нему и он переадресует).
                              • +1
                                Я ж застрелюсь. :)

                                Не надо :) Никто не говорит, что нужно бросаться и сразу же писать тесты вообще для всего. Как вариант, начать или с критически важных функций, или с тех мест, в которые часто вносятся изменения (чтобы тестами следить, не появились ли регрессии после правок).
                                Я имею в виду, что нужно ли делать отдельный тестовый стенд (программу) для каждого класса. Тесты же нужно откуда-то вызывать.

                                Это уже как вам удобнее. Можно делать отдельную «программу» на группу классов, объединенных по какому-либо признаку.
                                . Так вот, удобно их связывать воедино с помощью одного класса-системы. Иначе связь между этими менеджерами трудно отследить (а через класс системы — просто — все обращаются к нему и он переадресует).

                                Как я понял, вы описываете что-то вроде шаблона "Фасад".
                                Тут вариантов несколько, или при написании тестов реально держать связи в голове, или же дергать также за «класс-систему», но отдельные части системы подменять на фейковые, или не подменять вообще. Это уже не юнит-тесты, а интеграционные получаются.
                                • 0
                                  Спасибо, стало понятнее. :)
                            • 0
                              Если выполняете юнит тестирования то почитайте про fake, mock, stub, dummy. К тому же код должен быть организован с использованием dependency injection что упростит тестирование.
                              • 0
                                К сожалению, всё это всего лишь общие фразы. Без внятного объяснения реализации на настоящем примере ( без каких-либо вспомогательных фреймворков и программ ) это бесполезно. Потому я и не понимаю, как это реально делать.
                                • +1
                                  Знаю что есть книжка:
                                  «Test-Driven Development for Embedded C by James W. Grenning»
                                  Возможно вам поможет разобраться.
                      • +1

                        Ну кстати шанс как раз есть — юнит тесты мало того что писать надо, но ещё и код делать тестируемым, а с анализатором нужно просто запустить его.

                      • 0
                        > Использовалась бы именованная константа, и беды не было.

                        Даже с именованной константой никто не застрахует от возможности написать что-то вроде:
                        for (int i = 0; i <= NUM_ELEMENTS; ++i) {… }
                        • +1
                          Да. Однако, думаю вы согласитесь, что вероятность сделать ошибку уменьшается. Собственно, любые рекомендации написания кода не гарантируют отсутствие ошибок, но снижают вероятность их появления.
                      • 0
                        Статические анализаторы здесь могли бы помочь, если бы они выдавали предупреждения на код, написанный в таком «классическом» стиле. Чтобы разработчик задумался и переписал бы код, например, так:
                        array<uchar, 3> A;
                        ...
                        const auto average = accumulate(begin(A), end(A), 0) / A.size();
                        • –1
                          Чтобы разработчик задумался и переписал бы код, например, так:


                          Да-да, это очень правильно. Только мы сейчас об эмбеде, а не о модном и гламурном мире C++ '11 и прочих F#.

                          Если вы пишете для встроенной системы, то хороший тон — соблюдать правила если не C89, то максимум C99. Это гарантирует наибольшую переносимость между разными компиляторами и архитектурами. Так что забудьте про auto и прочее — просто надо внимательно подходить к написанию кода (не исключая использования статических анализаторов).
                          • +1
                            Во-первых, мир embedded очень разный. Где-то 8KiB памяти и жалкое подобие С89. Где-то 64-х битовые процессоры и гигабайты RAM.

                            Во-вторых, сможете показать, чем этот код на современном C++ уступает по эффективности/ресурсоемкости коду на C89/C99?
                            • 0
                              Насчет того, что разный, совершенно согласен. Но гигабайты RAM — это уже не совсем эмбеддед… Хотя, естесственно, и такой компьютер может быть встроен в какой-нибудь прибор. Правда, там все ограничения, типично ассоциируемые со встроенными решениями, уже неактуальны.

                              сможете показать, чем этот код на современном C++ уступает по эффективности/ресурсоемкости коду на C89/C99?


                              Начнем с того, что этот код не скомпилируется под, скажем, SDCC… Насчет Cosmic — тоже не уверен. Пробовать лень. Если попробуете — расскажите.
                              • 0
                                типично ассоциируемые
                                Здесь же код обсуждается, а не заблуждения на счет того, что считается embedded, а что нет.
                                Начнем с того, что этот код не скомпилируется под, скажем, SDCC…
                                Ну возьмите компилятор, который поддерживает C++11. В чем код на C++ будет уступать коду на C?

                                Кстати говоря, подобный код и на C++98 можно писать, только там не будет auto и array/begin/end нужно будет брать не из stdlib. Принцип остается тем же.

                                Если же ваш поинт был в том, что где-то в embedded кроме допотопных компиляторов C89 ничего нет. Ну чтож, бывает. Сейчас так. Пройдет немного времени и будет не так.
                                • 0
                                  Здесь же код обсуждается, а не заблуждения на счет того, что считается embedded, а что нет.


                                  Самый главный признак «embedded» в том, что там уже нельзя обсуждать сферический код в вакууме. Сразу возникают вопросы — для какой архитектуры, для какого конкретно компилятора, что там с поддержкой прерываний и пр.

                                  Ну возьмите компилятор, который поддерживает C++11.


                                  Покажите мне компилятор для, например, STM8, который поддерживает C++11, и будем сравнивать. Пока я не знаю, где такой взять. :)
                                  • 0
                                    Самый главный признак «embedded» в том, что там уже нельзя обсуждать сферический код в вакууме.

                                    Т.е. обсуждение этого кода в принципе лишено смысла, т.к. нет ответов на вопросы:
                                    для какой архитектуры, для какого конкретно компилятора, что там с поддержкой прерываний и пр.
                                    Я правильно понимаю вашу логику?
                                    Покажите мне компилятор для, например, STM8, который поддерживает C++11, и будем сравнивать.
                                    Прочтите, пожалуйста, то, что вам пишут. В частности:
                                    Если же ваш поинт был в том, что где-то в embedded кроме допотопных компиляторов C89 ничего нет. Ну чтож, бывает. Сейчас так. Пройдет немного времени и будет не так.
                                    • +1
                                      Ну, это мы говорим об элементарных примерах. Но да, можно вообразить себе архитектуру, компилятор для которой не поддерживает даже #define. И вот там придется использовать числа в коде.

                                      Да я прочел. Правда, меня берут сомнения в том, что можно сделать компилятор C++11 для устройства с тридцатью двумя байтами RAM и килобайтом FLASH.
                                      • 0
                                        Правда, меня берут сомнения в том, что можно сделать компилятор C++11 для устройства с тридцатью двумя байтами RAM и килобайтом FLASH.
                                        Какие сложности? На этом же устройстве будет работать не компилятор, а собранная им программа. Как, собственно, и в случае с C-компилятором.
                                        • 0
                                          Какие сложности?


                                          Это уже к m08pvv, например. :) Должен признать, что я не специалист в теории внутренней работы компиляторов.
                                          • +1
                                            я не специалист в теории внутренней работы компиляторов.

                                            Я тоже не специалист по компиляторам, но за любые приятности языка приходится платить размером кода и занимаемой памяти.
                                            компилятор C++11 для устройства с тридцатью двумя байтами RAM и килобайтом FLASH

                                            Если ограничения настолько суровые, то проблема не в том чтобы написать компилятор, а в том, чтобы написать программу, которая запустится. Компилятор будет стабильно выдавать программу с потреблением RAM > 32 байт и/или общим размером > килобайта.
                                            Можно, конечно, заставить компилятор оптимизировать данный код по размеру, но вряд ли экономически оправдано писать C++ компилятор для устройств с 32 байтами RAM и килобайтом FLASH, ибо под такие устройства скорее уж на их ассемблере писать надо.
                                            • 0
                                              Я тоже не специалист по компиляторам, но за любые приятности языка приходится платить размером кода и занимаемой памяти.
                                              Может хоть вы покажете, чем придется платить в приведенном C++ном коде?

                                              Если под «приятностями» понимаются вещи, которые требуют поддержки в run-time, вроде исключений, RTTI и динамической памяти, то такие вещи для сильно ограниченных по ресурсам устройств просто отключаются.
                                              • +1
                                                Может хоть вы покажете, чем придется платить в приведенном C++ном коде?


                                                Ну так, для этого надо раздобыть компилятор с поддержкой C++11 хотя бы для AVR или на худой конец Cortex, написать тестовую программу, дизассемблировать ее и посмотреть.

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

                                                динамической памяти


                                                Динамическое выделение памяти прямо запрещается MISRA.
                                                • 0
                                                  Да вы бы давно уже на godbolt.org сходили бы.
                                                • +1
                                                  Мы обсуждаем конкретный код вычисления среднего значения массива или написание компилятора с целевой архитектурой, у которой 32 байта RAM и килобайт FLASH?

                                                  Если про конкретный код, то он не представляет никаких проблем и любой компилятор спокойно выдаст оптимальный по размеру/скорости (в зависимости от флагов) код. И вообще любой код, который не далеко ушёл от чистого C, тоже не будет представлять проблем.

                                                  Если же про 32 байта RAM, то проблемы будут начиная с виртуальных методов (32 байта RAM очень быстро будут съедены таблицами виртуальных методов и лишними указателями) и прочего. Теоретически, вполне можно и это всё тоже оптимизировать (на таком микроконтроллере никто же не будет ничего подгружать динамически, поэтому вполне можно на этапе компиляции избавиться вообще от всего), но опять всё упирается в вопрос о целесообразности написания C++ компилятора для такой целевой архитектуры. В большинстве случаев, на таких микроконтроллерах решаются задачи, для которых чистого C хватает более чем.
                                                  • +1
                                                    Мы обсуждаем конкретный код вычисления среднего значения массива или написание компилятора с целевой архитектурой
                                                    Нет, скорее обсуждаем разумность применения C++ в embedded. С одной простой демонстрацией.
                                                    И вообще любой код, который не далеко ушёл от чистого C, тоже не будет представлять проблем.
                                                    По-вашему, показанный код с использованием accumulate не далеко ушел от чистого C? И шаблонный array из C++ — это все тот же чистый C?
                                                    Если же про 32 байта RAM, то проблемы будут начиная с виртуальных методов (32 байта RAM очень быстро будут съедены таблицами виртуальных методов и лишними указателями) и прочего.
                                                    Ну и какого прочего? Для совсем уж ограниченных условий RTTI, исключения и поддержка динамической памяти из run-time просто изымаются. Использовать ООП с наследованием и полиморфизмом никто не заставляет. А если виртуальные методы не используются, то платить за них не нужно. Что еще?
                                                    • +1
                                                      Нет, скорее обсуждаем разумность применения C++ в embedded. С одной простой демонстрацией.

                                                      С++ — это просто инструмент, для многих платформ и задач он вполне подходит и я этого не отрицаю.

                                                      По-вашему, показанный код с использованием accumulate не далеко ушел от чистого C? И шаблонный array из C++ — это все тот же чистый C?

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

                                                      Использовать ООП с наследованием и полиморфизмом никто не заставляет.

                                                      А что использовать от C++? auto — сахар, который никак не влияет. Шаблоны — легко сожрать весь доступный килобайт FLASH. Вообще, под такую платформу даже на чистом C будут проблемы, т.к. подключив какую-нибудь библиотеку легко выйти за допустимые ограничения по памяти (килобайт FLASH — это катастрофически мало для многих библиотек).

                                                      Если же никаких шаблонов, никаких исключений, никаких RTTI, осторожная работа с памятью, то для многих контроллеров можно писать и на C++
                                                      • +1
                                                        для многих платформ и задач он вполне подходит и я этого не отрицаю.
                                                        Тут скорее вопрос в том, что в большинстве случаев (возможно, подавляющем) C++ оставляет меньший простор для ошибок, в сравнении с чистым C.
                                                        А что использовать от C++?
                                                        А вот те же шаблоны. То, что они раздувают код — это предания из 90-х. Если не верите, то спросите себя: насколько раздувают код те же std::array или тот же std::accumulate. Тот же auto вполне себе не сахар, а средство борьбы со сложностью и с забывчивостью разработчиков (классический пример: какой тип возвращает strlen()?). Ссылки.

                                                        И, если не брать случай с 32 байтами RAM, в которых и на нормальном С не попрограммируешь, то единственное преимущество чистого C — это пока еще отсутствие C++ компиляторов для каких-то отдельных платформ. Ну и традиции, типа «диды писали...» :)
                                                        • +1
                                                          C++ оставляет меньший простор для ошибок, в сравнении с чистым C.

                                                          Согласен.

                                                          А вот те же шаблоны. То, что они раздувают код — это предания из 90-х. Если не верите, то спросите себя: насколько раздувают код те же std::array или тот же std::accumulate.

                                                          Зависит от использования. При неразумном использовании будет съедать место на FLASH. Да и сама stl будет съедать место. Придётся компилятору постараться и выкинуть всё лишнее, что не используется, а то и вовсе в несколько проходов оптимизировать, чтобы получить код, который укладывается в заданные ограничения.
                                                          единственное преимущество чистого C — это пока еще отсутствие C++ компиляторов для каких-то отдельных платформ

                                                          Под ARM более менее хорошо с компиляторами C++, а вот остальной зоопарк — вендоры не спешат делать компиляторы C++, т.к. под их микроконтроллеры большинство пишет на ассемблере или на C, а пишут они потому, что если есть возможность писать на C++, то результат требует доработки напильником или даже серьёзного разговора с компилятором. В итоге имеем порочный круг.

                                                          Ну и традиции, типа «диды писали...» :)

                                                          Компилятор C++ в среднем есть более сложная программа, чем компилятор C, и в сообществе разработчиков встроенных систем укоренились мифы (часть из которых до сих пор правда) о том, что иногда надо плясать с бубном, чтобы компилятор выдал то, что надо (особенно если весьма жёсткие ограничения по памяти или ещё какие). Поэтому, скорее всего, выбор и идёт в сторону проверенного и безотказного инструмента. Но вполне возможно, что в недалёком будущем это изменится, так как принципиальных проблем нет.
                              • 0

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

                                • +1
                                  Я не знаю, кто определяет эти правила хорошего тона


                                  Те, например, для кого SDCC — не «а что это такое?», а суровая реальность.
                                  • 0

                                    Сочувствую, конечно. Но мне кажется, что это ошибка — жаловаться, что C++ код не компилируется компилятором языка C. Если для вашей платформы нет компилятора C++ (я так понимаю, что вы для STM8 разрабатываете?) это не значит, что весь embedded такой. Вы сами выше написали, что нельзя обсуждать сферический код в вакууме без привязки к конкретике — и теперь сами же обобщаете правила кода для STM8 на всю область. А если найдётся контроллер, для которого только ассемблер есть — будем считать, что писать на C в embedded правило плохого тона? :)


                                    P.S.: Только что посмотрел — вроде IAR для STM8 вполне себе может C++.

                                    • 0
                                      Понятное дело, нельзя жаловаться, что один инструмент не приспособлен для другого. :)

                                      и теперь сами же обобщаете правила кода для STM8 на всю область


                                      Я лишь о том, что решать задачи надо минимально необходимыми средствами, чтобы код (хотя бы не привязанный к железу напрямую) работал на максимальном количестве платформ. Иначе может быть очень обидно, когда нужная библиотека существует, и вроде бы в плане ресурсоемкости самого алгоритма должна бы работать на целевом чипе, но написана на языке/диалекте, который не поддерживается используемым компилятором, и потому требует правки, сравнимой с переписыванием.

                                      я так понимаю, что вы для STM8 разрабатываете?


                                      Я разрабатываю, в зависимости от необходимости, для STM8, MSP430, AVR и STM32, иногда с привлечением CPLD Altera. :)

                                      Справедливости ради, прямо сейчас у меня на столе лежит плата, относящаяся к текущему проекту, и выполнена она на основе STM32F051R8T6.

                                      вроде IAR для STM8 вполне себе может C++.


                                      Только вы знаете, сколько стоит IAR? :) А вот Cosmic и SDCC бесплатны.

                                      Понятное дело, что для хобби разницы в, э-э-э, некотором смысле, нет, но когда это продукт, то лучше использовать официально бесплатные инструменты.
                                      • 0
                                        когда это продукт, то лучше использовать официально бесплатные инструменты.

                                        Скорее не бесплатные, а лицензионные. Если у фирмы есть лицензия на IAR, то почему бы им не воспользоваться? А если нет, то может стоит поставить вопрос о приобретении лицензии? Можно, конечно, и без колёс толкать телегу, говоря, что так бесплатно, но с колёсами удобнее.
                          • 0
                            В общем, «потому же, что и не-embedded разработчикам, но с поправкой на embedded специфику».

                            По делу: ваш блог стоит читать уже хотя бы ради КДПВ!) Ну и образцы косяков культуру кода поднимают.
                            • +2
                              Отличная иллюстрация зла магических чисел получилась.
                              • 0

                                Да надо было


                                const size_t ASize = 3;
                                uchar A[ASize];
                                ...

                                Или enum, что лучше? Можно и sizeof но с ним легко выстрелить себе в голову.

                              • 0
                                Примером может служить использование 32-битного типа time_t

                                Просто ради полноты картины, компиляторы поновее уже могут определять time_t как 64-битный тип (gcc 7.x для arm-none-eabi, например, определяет как __int_least64_t).

                                • +1
                                  Тут все должно начинаться даже не со статического анализа, а вообще с уделения внимания надежности и качеству кода.
                                  На хабре как-то была очень жизненная цитата на этот счет:
                                  "… многие «железячники» считают, что производство устройства — это искусство, подвластное избранным, а вот написать к нему код он сможет сам, так, на коленке. Это ж вообще мелочь. Получается работающий тихий ужас. Они очень обижаются, когда им на пальцах объясняют, почему их код дурно пахнет, потому что… ну… они ж железку сделали, че тут, программа какая-то."
                                  • 0
                                    for (uchar i = 0; i != SOME_ARRAY_SIZE; i++)…

                                    Такой цикл у меня одного вызывает сомнение?
                                    • 0

                                      Выглядит странно. А если ещё вдруг захочиться пропустить несколько элементов в теле цикла сделав там i += x то всё.

                                      • 0
                                        Отчасти, это дело вкуса. Например, я обычно пишу как раз !=. Причина: единообразие циклов, использующих итераторы или простые целочисленные типы. Тем не менее, я понимаю, что это плохо и наверное откажусь от такой практике. Например, CERT однозначно не рекомендует использовать != (MSC21-C).
                                      • 0
                                        Спрашивал как-то одного товарища из авионики про анализаторы. Ответ был простой: не используем поскольку все использованные инструменты также должны быть сертифицированны.
                                        • 0
                                          Странный ответ. А я думал, что это царство DO-178C, Misra и подобных стандартов. И они просто обязаны использовать инструментарий для статического анализа кода.
                                          • +2
                                            Есть опыт разработки в авиации, да, все верно, стандарты там весьма серьезные.
                                            Товарищ, видимо, имел в виду, что при разработке проекта все используемые библиотеки и фреймворки тоже должны быть сертифицированы и верифицированы, и это действительно часто так,
                                            но причем здесь упомянут статический анализатор кода, который никакого непосредственного влияния на результат не оказывает и в кодогенерации не участвует, для меня тоже осталось непонятным. Мы cppcheck, clang-analyzer и valgrind (да, я знаю, что он динамический, но тоже анализатор и тоже не сертифицированный) без проблем использовали.
                                        • 0
                                          Наконец то свершилось, PVS для IAR.
                                          Но
                                          Сразу же всплыла ложка дегтя
                                          В моем проекте нашлась ошибка:
                                          V512. A call of the 'Foo' function will lead to a buffer overflow or underflow.

                                          Перехожу в описательную часть с примерами таких ошибок и вижу пример от PVS-Studio как делать не надо:
                                          Sample N2.
                                          #define CONT_MAP_MAX 50
                                          int _iContMap[CONT_MAP_MAX];
                                          memset(_iContMap, -1, CONT_MAP_MAX);
                                          In this sample, the size of the buffer to be filled is also defined incorrectly.

                                          Вместо этого предлагается такой код (как «надо делать» по версии PVS):
                                          This is the correct version:
                                          #define CONT_MAP_MAX 50
                                          int _iContMap[CONT_MAP_MAX];
                                          memset(_iContMap, -1, CONT_MAP_MAX * sizeof(int));

                                          Вот тут то и закралось в голове: а что, если завтра в таком проекте мне необходимо будет исправить
                                          int _iContMap[CONT_MAP_MAX];
                                          на
                                          char _iContMap[CONT_MAP_MAX];
                                          ???

                                          Выводы для сотрудников PVS-Studio:
                                          Проверяйте примеры, которые выдаете за эталон «Как надо»

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

                                            Если кодстайл принуждает к sizeof(тип), то приводят примеры как у вас.
                                            int a;
                                            memset(&a, 0, sizeof(int));

                                            Меняем тип в объявлении переменной
                                            char a;
                                            memset(&a, 0, sizeof(int));

                                            и теперь memset пишет вне переменной.

                                            А если кодстайл принуждает к sizeof(переменная), то пример такой:
                                            Было:
                                            int a[100];
                                            memset(a, 0, sizeof(a));

                                            Делаем выделение памяти динамическим:
                                            int* a = (int*)malloc(100);
                                            memset(a, 0, sizeof(a));

                                            Теперь память недозаполняется.

                                            Получается как не пиши код, можно придраться.
                                            • +1

                                              Есть, кстати, трюк, который позволяет хоть немного, но защититься от проблемы со взятием sizeof() от указателя вместо массива. У гугла в коде такое можно встретить:


                                              template <typename T, size_t N>
                                              char (&ArraySizeHelper(T (&array)[N]))[N];
                                              
                                              #define arraysize(array) (sizeof(ArraySizeHelper(array)))

                                              И дальше можно использовать arraysize() — он не скомпилируется, если ему дать указатель.

                                          • 0
                                            (del)
                                            • 0
                                              Как уже отметили выше, не так просто предложить красивый вариант исправления ошибки. И на самом деле, такой задачи не ставится. Для написания красивого и надёжного кода, есть такие книги как «Совершенный код». Задача документации — просто показать какой-то вариант исправления. :)
                                            • 0
                                              Embedded разработчики очень хотят pvs-studio, но разработчки pvs-studio не хотят собирать софт под x86, а на x86_64 не работает сборка. Замкнутый круг.
                                              • 0
                                                А почему не работает сборка? Как я представляю, вы используете некий кросс-компилятор для сборки кода для встраиваемого устройства. В чем состоит затруднение запускать этот кросс-компилятор под управлением 64-битной операционной системе? В общем, прощу развить мысль.

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

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