28 January 2019

Надежное программирование в разрезе языков — нубообзор. Часть 1

Abnormal programmingProgrammingSystem ProgrammingIndustrial ProgrammingDevelopment for IOT
В очередной раз провозившись два дня на написание и отладку всего четырехсот строк кода системной библиотеки, возникла мысль — “как бы хорошо, если бы программы писались менее болезненным способом”.

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

Конечно же, надо изобрести новый, самый лучший ЯП!
Нет, сначала попробуем выразить свои пожелания и посмотреть на то, что уже наизобретали.

Итак, что бы хотелось получить:

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

Область желательного применения — машинерия, транспорт, промышленные системы управления, IoT, эмбеддед включая телефоны.

Вряд ли это нужно для Веб, он построен (пока) на принципе “бросил и перезапустил” (fire and forget).

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

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

Итак, требования.

Устойчивость к ошибкам человека


Старательно полистав талмуды от PVS-Studio, я выяснил, что самые распространенные ошибки — это опечатки и недоправленная копипаста.

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

Чуть позже обдумав, пришел к выводу, что линтеры, примененные постфактум страдают от “ошибки выжившего”, поскольку в старых проектах серьезные ошибки уже исправлены.

а) Избавляемся от похожих имен

— должна проводится жесткая проверка видимости переменных и функций. Опечатавшись, можно использовать идентификатор из более общей области видимости, вместо нужного
— использоваться регистронезависимые имена. (VS) «Давайте функцию назовем как переменную, только в Кэмелкейз» и потом с чем нибудь сравним — в С так сделать можно (получим адрес ф-ции, который вполне себе число)
— имена с отличием на 1 букву должны вызывать предупреждение (спорно, можно выделять в IDE) но очень распространенная ошибка копипасты .x, .y, .w, .h.
— не допускаем одинаково именовать разные сущности — если есть константа с таким названием, то не должно быть одноименной переменной или имени типа
— крайне желательно, именование проверять по всем модулям проекта — легко перепутать, особенно если разные модули пишут разные люди

б) Раз упомянул — должна присутствовать модульность и желательно иерархическая — VS проект из 12000 файлов в одном каталоге — это ад поиска.
Еще модульность обязательна для описаний структур обмена данными между разными частями (модулями, программами) одного проекта. VS Встречал ошибку из-за разного выравнивания чисел в обменной структуре в приемнике и передатчике.

— Исключить возможность дублей линковки (компоновки).

в) Неоднозначности
— Должен быть определенный порядок вызовов функций. При записи X = funcA() + fB() или Fn(funcA(), fB(), callC()) — надо понимать, что человек рассчитывает получить вычисления в записанном порядке, (VS) а не как себе надумал оптимизатор.
— Исключить похожие операторы. А не как в С: + ++, < <<, | ||, & &&, = ==
— Желательно иметь немного понятных операторов и с очевидным приоритетом. Привет от тернарного оператора.
— Переопределение операторов скорее вредно. Вы пишете i := 2, но (VS) на самом деле это вызывает неявное создание объекта, для которого не хватает памяти, а диск дает сбой при сваппинге и ваш спутник падает на Марс :-(

На самом деле из личного опыта наблюдал вылет на строке ConnectionString = “DSN”, это оказалось сеттером, который открывал БД (а сервер не был виден в сети).

— Нужна инициализация всех переменных дефолтными значениями.
— Также ООП подход спасает от забывчивости переназначения всех полей в объекте в какой-нибудь новой сотой функции.
— Система типов должна быть безопасной — нужен контроль за размерностями присваиваемых объектов — защита от затирания памяти, арифметическим переполнением типа 65535+1, потерями точности и значимости при приведении типов, исключение сравнения несравнимого — ведь целое 2 не равно 2.0 в общем случае.

И даже типовое деление на 0 может давать вполне определенный +INF, вместо ошибки — нужно точное определение результата.

Устойчивость к входным данным


— Программа должна работать на любых входных данных и желательно, примерно одинаковое время. (VS) Привет Андроиду с реакцией на кнопку трубки от 0.2с до 5с; хорошо, что не Андроид управляет автомобильной ABS.

Например, программа должна корректно обрабатывать и 1Кб данных и 1Тб, не исчерпав ресурсы системы.

— Очень желательно иметь в ЯП RAII и надежную и однозначную обработка ошибок, не приводящую к побочным эффектам (утечкам ресурсов, например). (VS) Очень веселая вещь — утечка хендлов, проявиться может через многие месяцы.
— Было бы неплохо защититься от переполнения стека — рекурсию запретить.
— Проблема превышения доступного объема требуемой памятью, неконтролируемый рост потребления из-за фрагментации при динамическом выделении/освобождении. Если же язык имеет рантайм, зависимый от кучи, дело скорее всего плохо — привет STL и Phobos. (VS) Была история со старым C-рантаймом от Микрософт, который неадекватно возвращал память системе, из-за чего msbackup падал на больших объемах (для того времени).
— Нужна хорошая и безопасная работа со строками — не упирающаяся в ресурсы. Это сильно зависит от реализации (иммутабельные, COW, R/W массивы)
— Превышение времени реакции системы, не зависящее от программиста. Это типичная проблема сборщиков мусора. Хотя они и спасают от одних ошибок программирования — привносят другие — плохо диагностируемые.
— В определенном классе задач, оказывается, можно обойтись совсем без динамической памяти, либо однократно выделив ее при старте.
— Контролировать выход за границы массива, причем вполне допустимо написать предупреждение рантайма и игнорировать. Очень часто это некритичные ошибки.
— Иметь защиту от обращений к неинициализованному программой участку памяти, в т.ч к null-области, и в чужое адресное пространство.
— Интерпретаторы, JIT — лишние прослойки снижают надежность, есть проблемы с сборкой мусора (очень сложная подсистема — привнесет свои ошибки), и с гарантированным временем реакции. Исключаем, но есть в принципе Java Micro Edition (где от Явы отрезано так много, что остается только Я, была интересная статья dernasherbrezon (жаль, пристрелили) и .NET Micro Framework с C#.

Впрочем, по рассмотрению, эти варианты отпали:

  • .NET Micro оказался обычным интерпретатором (вычеркиваем по скорости);
  • Java Micro — пригодна только для внедряемых приложений, поскольку слишком сильно кастрирована по API, и придется для разработки переходить хотя бы на SE Embedded, которую уже закрыли или обычную Java’у, слишком монструозную и непредсказуемую по реакции.
    Впрочем, есть еще варианты, и хотя это и не выглядит заготовкой для работоспособного фундамента, но можно сравнить с другими языками, даже устаревшими или обладающими определенными недостатками.


— Устойчивость к многопоточной работе — защита приватных данных потока, и механизмы гарантированного обмена между потоками. Программа в 200 потоков может работать совсем не так, как в два.
— Контрактное программирование плюс встроенные юниттесты тоже весьма помогают спать спокойно.

Устойчивость к повреждению программы или данных — сбой носителя, взлом


— Программа должна целиком загружаться в память — без подгрузки модулей, особенно удаленно.
— Очищаем память при освобождении (а не только выделении)
— Контроль переполнения стека, областей переменных, особенно строки.
— Перезапуск после сбоя.

Кстати, подход, когда рантайм имеет свой логгинг, а не только выдает, что северный песец и стектрейс, мне очень импонирует.

Языки — и таблица соответствия


На первый взгляд, для анализа возьмем специально разработанные безопасные ЯП:

  1. Active Oberon
  2. Ada
  3. BetterC (dlang subset)
  4. IEC 61131-3 ST
  5. Safe-C

И пройдемся по ним с точки зрения вышеприведенных критериев.

Но это уже объем для статьи-продолжения, если карма позволит.

С выделением в таблицу вышеупомянутых факторов, ну и возможно — еще что то толковое почерпнется из комментариев.

Что же касается прочих интересных языков — C++, Crystal, Go, Delphi, Nim, Red, Rust, Zig (добавьте по вкусу) то заполнять таблицу соответствия по ним оставлю желающим.

Дисклеймеры:

  • В принципе, если программа, скажем на Питоне, потребляет 30Мб, а требования к реакции- секунды, а микрокомпьютер имеет 600 Мб свободной памяти и 600 МГц проц — то почему нет? Только надежной такая программа будет с некоторой вероятностью (пусть и 96%), не более.
  • Кроме того, язык должен стараться быть удобным для программиста — иначе никто его использовать не будет. Такие статьи «я придумал идеальный язык программирования, чтобы мне и только мне было удобно писать» — не редкость и на Хабре тоже, но это совсем о другом.
Tags:надежностьбезопасностьязыки программирования
Hubs: Abnormal programming Programming System Programming Industrial Programming Development for IOT
+15
9.4k 57
Comments 42