Русский
Русский
English
Статистика
Реклама

Pvs-studio

Обработка дат притягивает ошибки или 77 дефектов в Qt 6

16.02.2021 22:10:00 | Автор: admin

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


Это классическая статья о проверке открытого проекта, которая пополнит нашу "доказательную базу" полезности и эффективности использования PVS-Studio для контроля качества кода. Хотя мы уже писали про поверку проекта Qt (в 2011, в 2014 и в 2018), очень полезно сделать это вновь. Так, мы на практике демонстрируем простую, но очень важную мысль: статический анализ должен применяться регулярно!


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


Всё, теория заканчивается. Давайте посмотрим, что интересного нас ждёт в коде. А пока вы будете читать статью, предлагаю скачать PVS-Studio и запросить демонстрационный ключ, чтобы посмотреть, что интересного найдётся в ваших собственных проектах :).


Даты


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


Встретились ошибки обработки дат и в Qt. Давайте с них и начнём.


Фрагмент N1: неправильная интерпретация статуса ошибки


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


static const char qt_shortMonthNames[][4] = {    "Jan", "Feb", "Mar", "Apr", "May", "Jun",    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};static int fromShortMonthName(QStringView monthName){  for (unsigned int i = 0;       i < sizeof(qt_shortMonthNames) / sizeof(qt_shortMonthNames[0]); ++i)  {    if (monthName == QLatin1String(qt_shortMonthNames[i], 3))      return i + 1;  }  return -1;}

В случае успеха функция возвращает номер месяца (значение от 1 до 12). Если имя месяца некорректно, то функция возвращает отрицательное значение (-1). Обратите внимание, что функция не может вернуть значение 0.


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


QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format){  ....  month = fromShortMonthName(parts.at(1));  if (month)    day = parts.at(2).toInt(&ok);  // If failed, try day then month  if (!ok || !month || !day) {    month = fromShortMonthName(parts.at(2));    if (month) {      QStringView dayPart = parts.at(1);      if (dayPart.endsWith(u'.'))        day = dayPart.chopped(1).toInt(&ok);    }  }  ....}

Проверки номера месяца на равенство нулю никогда не сработают, и программа продолжит выполняться с некорректным отрицательным номером месяца. Анализатор PVS-Studio видит здесь целый букет несостыковок, о чем сообщает сразу четырьмя сообщениями:


  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4907
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4911
  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4913
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4921

Фрагмент N2: ошибка логики обработки даты


Для начала посмотрим на реализацию функции, возвращающей количество секунд.


enum {  ....  MSECS_PER_DAY = 86400000,  ....  SECS_PER_MIN = 60,};int QTime::second() const{    if (!isValid())        return -1;    return (ds() / 1000)%SECS_PER_MIN;}

Рассмотренная функция может вернуть значение в диапазоне [0..59] или статус ошибки -1.


В одном месте эта функция используется очень странным образом:


static qint64 qt_mktime(QDate *date, QTime *time, ....){  ....  } else if (yy == 1969 && mm == 12 && dd == 31             && time->second() == MSECS_PER_DAY - 1) {      // There was, of course, a last second in 1969, at time_t(-1); we won't      // rescue it if it's not in normalised form, and we don't know its DST      // status (unless we did already), but let's not wantonly declare it      // invalid.  } else {  ....}

Предупреждение PVS-Studio: V560 [CWE-570] A part of conditional expression is always false: time->second() == MSECS_PER_DAY 1. qdatetime.cpp 2488


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


Ошибочно вот это сравнение:


time->second() == MSECS_PER_DAY - 1

MSECS_PER_DAY 1 это 86399999. Функция second, как мы уже знаем, никак не может вернуть такое значение. Таким образом, здесь какая-то логическая ошибка и код заслуживает пристального внимания разработчиков.


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


Опечатки


Фрагмент N3: неожиданно, мы поговорим о HTML!


QString QPixelTool::aboutText() const{  const QList<QScreen *> screens = QGuiApplication::screens();  const QScreen *windowScreen = windowHandle()->screen();  QString result;  QTextStream str(&result);  str << "<html></head><body><h2>Qt Pixeltool</h2><p>Qt " << QT_VERSION_STR    << "</p><p>Copyright (C) 2017 The Qt Company Ltd.</p><h3>Screens</h3><ul>";  for (const QScreen *screen : screens)    str << "<li>" << (screen == windowScreen ? "* " : "  ")        << screen << "</li>";  str << "<ul></body></html>";  return result;}

Предупреждение PVS-Studio: V735 Possibly an incorrect HTML. The "</ body>" closing tag was encountered, while the "</ ul>" tag was expected. qpixeltool.cpp 707


В PVS-Studio есть диагностики, которые не только проверят сам код, но и выискивают аномалии в строковых константах. Здесь как раз сработала одна из таких диагностик. Это достаточно редкий случай, зато этим он и примечательный.


Дважды используется тег открытия списка. Это явная опечатка. Первый тег, должен открывать список, а второй закрывать. Правильный код:


str << "</ul></body></html>";

Фрагмент N4: повторная проверка в условии


class Node{  ....  bool isGroup() const { return m_nodeType == Group; }  ....};void DocBookGenerator::generateDocBookSynopsis(const Node *node){  ....  if (node->isGroup() || node->isGroup()      || node->isSharedCommentNode() || node->isModule()      || node->isJsModule() || node->isQmlModule() || node->isPageNode())    return;  ....}

Предупреждение PVS-Studio: V501 [CWE-570] There are identical sub-expressions to the left and to the right of the '||' operator: node->isGroup() || node->isGroup() docbookgenerator.cpp 2599


Простая опечатка, но её исправление зависит от того, чего на самом деле хотели достичь в этом коде. Если проверка просто дублируется, то её стоит просто удалить. Но возможен и другой сценарий: не проверено какое-то другое нужное условие.


Фрагмент N5: создание лишней локальной переменной


void MainWindow::addToPhraseBook(){  ....  QString selectedPhraseBook;  if (phraseBookList.size() == 1) {    selectedPhraseBook = phraseBookList.at(0);    if (QMessageBox::information(this, tr("Add to phrase book"),          tr("Adding entry to phrasebook %1").arg(selectedPhraseBook),           QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok)                          != QMessageBox::Ok)      return;  } else {    bool okPressed = false;    QString selectedPhraseBook =       QInputDialog::getItem(this, tr("Add to phrase book"),                            tr("Select phrase book to add to"),                            phraseBookList, 0, false, &okPressed);    if (!okPressed)      return;  }  MessageItem *currentMessage = m_dataModel->messageItem(m_currentIndex);  Phrase *phrase = new Phrase(currentMessage->text(),                              currentMessage->translation(),                              QString(), nullptr);  phraseBookHash.value(selectedPhraseBook)->append(phrase);}

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


Единорог из старой коллекции


Предупреждение PVS-Studio: V561 [CWE-563] It's probably better to assign value to 'selectedPhraseBook' variable than to declare it anew. Previous declaration: mainwindow.cpp, line 1303. mainwindow.cpp 1313


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


QString selectedPhraseBook =

В результате в else-блоке появилась ещё одна локальная строковая переменная, которая инициализируется, но не используется. Тем временем переменная с таким же именем во внешнем блоке останется пустой.


Фрагмент N6: приоритет операций


Классический паттерн ошибки, который встречается весьма часто.


bool QQmlImportInstance::resolveType(....){  ....  if (int icID = containingType.lookupInlineComponentIdByName(typeStr) != -1)  {    *type_return = containingType.lookupInlineComponentById(icID);  } else {    auto icType = createICType();    ....  }  ....}

Предупреждение PVS-Studio: V593 [CWE-783] Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. qqmlimport.cpp 754


Значение переменной icID всегда будет иметь значение 0 или 1. Это явно не то, что задумывалось. Причина: в начале происходит сравнение с -1, а только затем инициализация переменной icID.


Современный синтаксис C++ позволяет корректно записать это условие следующим образом:


if (int icID = containingType.lookupInlineComponentIdByName(typeStr);    icID != -1)

Кстати, очень похожую ошибку мы уже обнаруживали в Qt:


char ch;while (i < dataLen && ((ch = data.at(i) != '\n') && ch != '\r'))  ++i;

Но пока на вооружение не будет взят такой анализатор кода, как PVS-Studio, программисты вновь и вновь будут допускать такие ошибки. Никто не совершенен. И да, это тонкий намёк внедрить PVS-Studio :).


Фрагмент N7: коварное деление по модулю


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


if (A % 2 == 1)

Но программисты вновь и вновь ошибаются и пишут что-то типа этого:


if (A % 1 == 1)

Это естественно неправильно, так как остаток от деления по модулю на один это всегда ноль. Не обошлось без этой ошибки и в Qt:


bool loadQM(Translator &translator, QIODevice &dev, ConversionData &cd){  ....  case Tag_Translation: {    int len = read32(m);    if (len % 1) {                                             // <=      cd.appendError(QLatin1String("QM-Format error"));      return false;    }    m += 4;    QString str = QString((const QChar *)m, len/2);  ....}

Предупреждение PVS-Studio: V1063 The modulo by 1 operation is meaningless. The result will always be zero. qm.cpp 549


Фрагмент N8: перезаписывание значения


QString Node::qualifyQmlName(){  QString qualifiedName = m_name;  if (m_name.startsWith(QLatin1String("QML:")))    qualifiedName = m_name.mid(4);  qualifiedName = logicalModuleName() + "::" + m_name;  return qualifiedName;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'qualifiedName' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1227, 1228. node.cpp 1228


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


QString qualifiedName = m_name;if (m_name.startsWith(QLatin1String("QML:")))  qualifiedName = m_name.mid(4);qualifiedName = logicalModuleName() + "::" + qualifiedName;return qualifiedName;

Фрагмент N9: copy-paste


class Q_CORE_EXPORT QJsonObject{  ....  bool operator<(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  bool operator<=(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  ....}

Прудпреждение PVS-Studio: V524 It is odd that the body of '<=' function is fully equivalent to the body of '<' function. qjsonobject.h 155


Такие скучные функции, как операторы сравнения, никто не проверяет. На них обычно не пишут тесты. Их не просматривают или делают это очень быстро на code review. А зря. Статический анализ кода здесь как нельзя кстати. Анализатор не устаёт и с удовольствием проверяет даже такой скучный код.


Реализация операторов < и <= совпадают. Это явно неправильно. Скорее всего, код писался методом Copy-Paste, и затем забыли изменить всё что нужно в скопированном коде. Правильно:


bool operator<(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }bool operator<=(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index <= other.item.index; }

Фрагмент N10: static_cast / dynamic_cast


void QSGSoftwareRenderThread::syncAndRender(){  ....  bool canRender = wd->renderer != nullptr;  if (canRender) {     auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);     if (softwareRenderer)       softwareRenderer->setBackingStore(backingStore);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'softwareRenderer' is always true. qsgsoftwarethreadedrenderloop.cpp 510


В начале рассмотрим вот эту проверку:


bool canRender = wd->renderer != nullptr;if (canRender) {

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


auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);if (softwareRenderer)

Если указатель wd->renderer ненулевой, то и указатель softwareRenderer точно ненулевой. Есть подозрение, что здесь опечатка, которая состоит в том, что на самом деле следовало использовать dynamic_cast. В этом случае код начинает приобретать смысл. Если преобразование типа невозможно, оператор dynamic_cast возвращает nullptr, и это возвращенное значение, естественно, следует проверять. Впрочем, возможно, я неправильно интерпретировал ситуацию и код нужно исправлять другим способом.


Фрагмент N11: скопировали блок кода и забыли изменить


void *QQuickPath::qt_metacast(const char *_clname){  if (!_clname) return nullptr;  if (!strcmp(_clname, qt_meta_stringdata_QQuickPath.stringdata0))    return static_cast<void*>(this);  if (!strcmp(_clname, "QQmlParserStatus"))    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  return QObject::qt_metacast(_clname);}

Предупреждение PVS-Studio: V581 [CWE-670] The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 2719, 2721. moc_qquickpath_p.cpp 2721


Эти две строчки:


if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))  return static_cast< QQmlParserStatus*>(this);

Были размножены с помощью Copy-Paste. После чего они не были модифицированы и не имеют смысла.


Фрагмент N12: переполнение из-за не там поставленной скобки


int m_offsetFromUtc;....void QDateTime::setMSecsSinceEpoch(qint64 msecs){  ....  if (!add_overflow(msecs, qint64(d->m_offsetFromUtc * 1000), &msecs))    status |= QDateTimePrivate::ValidWhenMask;  ....}

Предупреждение PVS-Studio: V1028 [CWE-190] Possible overflow. Consider casting operands of the 'd->m_offsetFromUtc * 1000' operator to the 'qint64' type, not the result. qdatetime.cpp 3922


Программист предвидит ситуацию, что при умножении переменной типа int на 1000 может произойти переполнение. Чтобы этого избежать, он планирует использовать при умножении 64-битный тип qint64. И использует явное приведение типа.


Вот только толку от этого приведения типа никакого нет. В начале всё равно произойдёт переполнение. И только затем выполнится приведение типа. Правильный вариант:


add_overflow(msecs, qint64(d->m_offsetFromUtc) * 1000, &msecs)

Фрагмент N13: не полностью инициализированный массив


class QPathEdge{  ....private:  int m_next[2][2];  ....};inline QPathEdge::QPathEdge(int a, int b)    : flag(0)    , windingA(0)    , windingB(0)    , first(a)    , second(b)    , angle(0)    , invAngle(0){    m_next[0][0] = -1;    m_next[1][0] = -1;    m_next[0][0] = -1;    m_next[1][0] = -1;}

Предупреждения PVS-Studio:


  • V1048 [CWE-1164] The 'm_next[0][0]' variable was assigned the same value. qpathclipper_p.h 301
  • V1048 [CWE-1164] The 'm_next[1][0]' variable was assigned the same value. qpathclipper_p.h 302

Перед нами неудачная попытка инициализировать массив размером 2x2. Два элемента инициализируются повторно, и два остаются неинициализированными. Правильный вариант:


m_next[0][0] = -1;m_next[0][1] = -1;m_next[1][0] = -1;m_next[1][1] = -1;

Я очень люблю такие примеры ошибок, которые встречаются в коде профессиональных разработчиков. Это как раз такой случай. Он показывает, что любой может опечататься, и поэтому статический анализ является вашим другом. Дело в том, что я уже десяток лет веду бой со скептиками, которые уверены, что такие ошибки можно встретить только в лабораторных работах студентов и что они такие ошибки никогда не делают :). Ещё 10 лет назад я написал заметку "Миф второй профессиональные разработчики не допускают глупых ошибок", и с тех пор, естественно, ничего не изменилось. Люди всё также делают такие ошибки и всё также утверждают, что это не так :).


Будь мудр - используй статический анализатор кода


Ошибки в логике


Фрагмент N14: недостижимый код


void QmlProfilerApplication::tryToConnect(){  Q_ASSERT(!m_connection->isConnected());  ++ m_connectionAttempts;  if (!m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds    if (m_verbose) {      if (m_socketFile.isEmpty())        logError(          QString::fromLatin1("Could not connect to %1:%2 for %3 seconds ...")          .arg(m_hostName).arg(m_port).arg(m_connectionAttempts));      else        logError(          QString::fromLatin1("No connection received on %1 for %2 seconds ...")          .arg(m_socketFile).arg(m_connectionAttempts));    }  }  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'm_verbose' is always false. qmlprofilerapplication.cpp 495


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


if (!m_verbose && ....) {  if (m_verbose) {

Фрагмент N15: перетирание значения переменной


void QRollEffect::scroll(){  ....  if (currentHeight != totalHeight) {      currentHeight = totalHeight * (elapsed/duration)          + (2 * totalHeight * (elapsed%duration) + duration)          / (2 * duration);      // equiv. to int((totalHeight*elapsed) / duration + 0.5)      done = (currentHeight >= totalHeight);  }  done = (currentHeight >= totalHeight) &&         (currentWidth >= totalWidth);  ....}

Предупреждение PVS-Studio: V519 [CWE-563] The 'done' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 509, 511. qeffects.cpp 511


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


Фрагмент N16-N20: перетирание значения переменной


Альтернативный вариант перетирания значения переменной.


bool QXmlStreamWriterPrivate::finishStartElement(bool contents){  ....  if (inEmptyElement) {    ....    lastNamespaceDeclaration = tag.namespaceDeclarationsSize;   // <=    lastWasStartElement = false;  } else {    write(">");  }  inStartElement = inEmptyElement = false;  lastNamespaceDeclaration = namespaceDeclarations.size();      // <=  return hadSomethingWritten;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'lastNamespaceDeclaration' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 3030, 3036. qxmlstream.cpp 3036


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


Есть ещё 4 предупреждения, указывающих на такой же паттерн ошибки:


  • V519 [CWE-563] The 'last' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 609, 637. qtextengine.cpp 637
  • V519 [CWE-563] The 'm_dirty' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1014, 1017. qquickshadereffect.cpp 1017
  • V519 [CWE-563] The 'changed' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 122, 128. qsgdefaultspritenode.cpp 128
  • V519 [CWE-563] The 'eaten' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 299, 301. qdesigner.cpp 301

Фрагмент N21: путаница между нулевым указателем и пустой строкой


// this could become a list of all languages used for each writing// system, instead of using the single most common language.static const char languageForWritingSystem[][6] = {    "",     // Any    "en",  // Latin    "el",  // Greek    "ru",  // Cyrillic    ...... // Нулевых указателей нет. Используются пустые строковые литералы.    "", // Symbol    "sga", // Ogham    "non", // Runic    "man" // N'Ko};static void populateFromPattern(....){  ....  for (int j = 1; j < QFontDatabase::WritingSystemsCount; ++j) {    const FcChar8 *lang = (const FcChar8*) languageForWritingSystem[j];    if (lang) {  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'lang' is always true. qfontconfigdatabase.cpp 462


В массиве languageForWritingSystem нет нулевых указателей. Поэтому проверка if(lang) не имеет смысла. Зато в массиве есть пустые строки. Быть может, хотелось сделать проверку именно на пустую строку? Если да, тогда корректный код должен выглядеть так:


if (strlen(lang) != 0) {

Или можно ещё проще написать:


if (lang[0] != '\0') {

Фрагмент N22: странная проверка


bool QNativeSocketEnginePrivate::createNewSocket(....){  ....  int socket = qt_safe_socket(domain, type, protocol, O_NONBLOCK);  ....  if (socket < 0) {    ....    return false;  }  socketDescriptor = socket;  if (socket != -1) {    this->socketProtocol = socketProtocol;    this->socketType = socketType;  }  return true;}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'socket != 1' is always true. qnativesocketengine_unix.cpp 315


Условие socket != -1 всегда истинно, так как выше происходит выход из функции, если значение переменной socket отрицательное.


Фрагмент N23: так что же всё-таки должна вернуть функция?


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count <= 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression '!count' is always false. qsqltablemodel.cpp 1110


Для упрощения выделю самое главное:


if (.... || count <= 0)  return false;....else if (!count)  return true;

Первая проверка говорит нам, что если значение count меньше или равно 0, то это ошибочное состояние и функция должна вернуть false. Однако ниже мы видим точное сравнение этой переменной с нулём, и этот случай уже интерпретируется по-другому: функция должна вернуть true.


Здесь явно что-то не так. Я подозреваю, что на самом деле проверка должна быть не <=, а просто <. Тогда код обретает смысл:


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count < 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Фрагмент N24: лишний статус?


В следующем коде переменная identifierWithEscapeChars выглядит просто как лишняя сущность. Или это логическая ошибка? Или код не дописан? К моменту второй проверки эта переменная в любом случае всегда будет равна true.


int Lexer::scanToken(){  ....  bool identifierWithEscapeChars = false;  ....  if (!identifierWithEscapeChars) {    identifierWithEscapeChars = true;    ....  }  ....  if (identifierWithEscapeChars) {    // <=    ....  }  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'identifierWithEscapeChars' is always true. qqmljslexer.cpp 817


Фрагмент N25: что делать с девятью объектами?


bool QFont::fromString(const QString &descrip){  ....  const int count = l.count();  if (!count || (count > 2 && count < 9) || count == 9 || count > 17 ||      l.first().isEmpty()) {    qWarning("QFont::fromString: Invalid description '%s'",             descrip.isEmpty() ? "(empty)" : descrip.toLatin1().data());    return false;  }  setFamily(l[0].toString());  if (count > 1 && l[1].toDouble() > 0.0)    setPointSizeF(l[1].toDouble());  if (count == 9) {                           // <=    setStyleHint((StyleHint) l[2].toInt());    setWeight(QFont::Weight(l[3].toInt()));    setItalic(l[4].toInt());    setUnderline(l[5].toInt());    setStrikeOut(l[6].toInt());    setFixedPitch(l[7].toInt());  } else if (count >= 10) {  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'count == 9' is always false. qfont.cpp 2142


Как должна себя вести функция, если переменная count равна 9? С одной стороны, функция должна выдать предупреждение и завершить свою работу. Ведь явно написано:


if (.... || count == 9 || ....) {  qWarning(....);  return false;}

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


if (count == 9) {  setStyleHint((StyleHint) l[2].toInt());  setWeight(QFont::Weight(l[3].toInt()));  setItalic(l[4].toInt());  ....}

Этот код, конечно, никогда не выполняется. Код ждёт, чтобы его пришли и исправили :).


Нулевые указатели


Фрагмент N26-N42: использование указателя до его проверки


class __attribute__((visibility("default"))) QMetaType {  ....  const QtPrivate::QMetaTypeInterface *d_ptr = nullptr;};QPartialOrdering QMetaType::compare(const void *lhs, const void *rhs) const{    if (!lhs || !rhs)        return QPartialOrdering::Unordered;    if (d_ptr->flags & QMetaType::IsPointer)        return threeWayCompare(*reinterpret_cast<const void * const *>(lhs),                               *reinterpret_cast<const void * const *>(rhs));    if (d_ptr && d_ptr->lessThan) {        if (d_ptr->equals && d_ptr->equals(d_ptr, lhs, rhs))            return QPartialOrdering::Equivalent;        if (d_ptr->lessThan(d_ptr, lhs, rhs))            return QPartialOrdering::Less;        if (d_ptr->lessThan(d_ptr, rhs, lhs))            return QPartialOrdering::Greater;        if (!d_ptr->equals)            return QPartialOrdering::Equivalent;    }    return QPartialOrdering::Unordered;}

Предупреждение PVS-Studio: V595 [CWE-476] The 'd_ptr' pointer was utilized before it was verified against nullptr. Check lines: 710, 713. qmetatype.cpp 710


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


if (d_ptr->flags & ....)if (d_ptr && ....)

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


Это один из самых распространённых паттернов ошибки в языке C и С++. Пруфы. В исходных Qt кодах тоже встречается немало ошибок этой разновидности:


  • V595 [CWE-476] The 'self' pointer was utilized before it was verified against nullptr. Check lines: 1346, 1351. qcoreapplication.cpp 1346
  • V595 [CWE-476] The 'currentTimerInfo' pointer was utilized before it was verified against nullptr. Check lines: 636, 641. qtimerinfo_unix.cpp 636
  • V595 [CWE-476] The 'lib' pointer was utilized before it was verified against nullptr. Check lines: 325, 333. qlibrary.cpp 325
  • V595 [CWE-476] The 'fragment.d' pointer was utilized before it was verified against nullptr. Check lines: 2262, 2266. qtextcursor.cpp 2262
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1581, 1583. qapplication.cpp 1581
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1593, 1595. qapplication.cpp 1593
  • V595 [CWE-476] The 'newHandle' pointer was utilized before it was verified against nullptr. Check lines: 873, 879. qsplitter.cpp 873
  • V595 [CWE-476] The 'targetModel' pointer was utilized before it was verified against nullptr. Check lines: 454, 455. qqmllistmodel.cpp 454
  • V595 [CWE-476] The 'childIface' pointer was utilized before it was verified against nullptr. Check lines: 102, 104. qaccessiblequickitem.cpp 102
  • V595 [CWE-476] The 'e' pointer was utilized before it was verified against nullptr. Check lines: 94, 98. qquickwindowmodule.cpp 94
  • V595 [CWE-476] The 'm_texture' pointer was utilized before it was verified against nullptr. Check lines: 235, 239. qsgplaintexture.cpp 235
  • V595 [CWE-476] The 'm_unreferencedPixmaps' pointer was utilized before it was verified against nullptr. Check lines: 1140, 1148. qquickpixmapcache.cpp 1140
  • V595 [CWE-476] The 'camera' pointer was utilized before it was verified against nullptr. Check lines: 263, 264. assimpimporter.cpp 263
  • V595 [CWE-476] The 'light' pointer was utilized before it was verified against nullptr. Check lines: 273, 274. assimpimporter.cpp 273
  • V595 [CWE-476] The 'channel' pointer was utilized before it was verified against nullptr. Check lines: 337, 338. assimpimporter.cpp 337
  • V595 [CWE-476] The 'm_fwb' pointer was utilized before it was verified against nullptr. Check lines: 2492, 2500. designerpropertymanager.cpp 2492

Фрагмент N43: использование указателя до его проверки в рамках одного выражения


Та же самая ситуация, что и выше. Однако в этот раз разыменование и проверка указателя находятся в одном выражении. Классическая ошибка, возникающая из-за невнимательности при написании кода и при последующем code-review.


void QFormLayoutPrivate::updateSizes(){  ....  QFormLayoutItem *field = m_matrix(i, 1);  ....  if (userHSpacing < 0 && !wrapAllRows && (label || !field->fullRow) && field)  ....}

Предупреждение PVS-Studio: V713 [CWE-476] The pointer 'field' was utilized in the logical expression before it was verified against nullptr in the same logical expression. qformlayout.cpp 405


Минутка отдыха


Я устал. Думаю, устали и читатели. Тут устанешь, даже если просто бегло просматривать текст статьи :). Поэтому я пошёл за второй чашечкой кофе. Первую я выпил где-то на рубеже 12-ого фрагмента. Приглашаю и читателей сходить за любимым напитком.


И пока все ушли на кухню, рекламная пауза. Приглашаю команду, занимающуюся разработкой проекта Qt рассмотреть вопрос приобретения лицензии на анализатор кода PVS-Studio. Запросить прайс можно здесь. С нашей стороны поддержка и помощь в настройке. Да, согласен, я сегодня более навязчив, чем обычно. Это эксперимент :).


Минутка отдыха с единорогом


Фрагмент N44-N72: нет проверки, что вернула функция malloc


void assignData(const QQmlProfilerEvent &other){  if (m_dataType & External) {    uint length = m_dataLength * (other.m_dataType / 8);    m_data.external = malloc(length);                          // <=    memcpy(m_data.external, other.m_data.external, length);    // <=  } else {    memcpy(&m_data, &other.m_data, sizeof(m_data));  }}

Предупреждение PVS-Studio: V575 [CWE-628] The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 277, 276. qqmlprofilerevent_p.h 277


Нельзя просто взять и использовать указатель, который вернула функция malloc. Нужно обязательно проверить, не является ли этот указатель нулевым, даже если очень лень всем этим заниматься. Для этого есть 4 причины, которые описаны в статье "Почему важно проверять, что вернула функция malloc".


Перед нами один из случаев, где отсутствует необходимая проверка. Есть и другие предупреждения, но из-за их количества включать весь список в статью не хочется. На всякий случай, я выписал 28 предупреждений в файл: qt6-malloc.txt. Но на самом деле разработчикам, конечно, лучше самим перепроверить проект и самостоятельно изучить предупреждения. У меня не было задачи выявить как можно больше ошибок.


Что интересно, на фоне важных забытых проверок есть совершенно ненужные. Речь идёт о вызове оператора new, который в случае ошибки выделения памяти сгенерирует исключение std::bad_alloc. Вот один из примеров такой избыточной проверки:


static QImageScaleInfo* QImageScale::qimageCalcScaleInfo(....){  ....  QImageScaleInfo *isi;  ....  isi = new QImageScaleInfo;  if (!isi)    return nullptr;  ....}

Предупреждение PVS-Studio: V668 [CWE-570] There is no sense in testing the 'isi' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. qimagescale.cpp 245


P.S. Здесь читатели всегда задают вопрос, учитывает ли анализатор placement new или "new (std::nothrow) T"? Да, учитывает и не выдаёт для них ложные срабатывания.


Избыточный код ("код с запахом")


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


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


Часто я вообще не рассматриваю в статьях такие предупреждения. Это неинтересно. Однако в проекте Qt вдруг таких случаев оказалось удивительно много. Явно больше, чем обычно. Поэтому я решил уделить этому немного внимания и разобрать несколько таких случаев. Думаю, будет полезно провести рефакторинг этих и многих других аналогичных мест. Для этого потребуется использовать полный отчёт, не руководствоваться тем, что я выпишу в статью.


Итак, взглянем на несколько показательных случаев.


Фрагмент N73: "код с запахом" обратная проверка


void QQuick3DSceneManager::setWindow(QQuickWindow *window){  if (window == m_window)    return;  if (window != m_window) {    if (m_window)      disconnect(....);    m_window = window;    connect(....);    emit windowChanged();  }}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'window != m_window' is always true. qquick3dscenemanager.cpp 60


Если window==m_window, то функция завершает работу. Последующая обратная проверка не имеет никакого смысла и только загромождает код.


Фрагмент N74: "код с запахом" странная инициализация


QModelIndex QTreeView::moveCursor(....){  ....  int vi = -1;  if (vi < 0)    vi = qMax(0, d->viewIndex(current));  ....}

Предупреждение PVS-Stduio: V547 [CWE-571] Expression 'vi < 0' is always true. qtreeview.cpp 2219


Что это? Зачем так писать?


Что это? Зачем так писать? Код можно упростить до одной строки:


int vi = qMax(0, d->viewIndex(current));

Фрагмент N75: "код с запахом" недостижимый код


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    if (unmetDependencies.isEmpty()) {      if (options->verbose) {        fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",                  qPrintable(sourceFileName));      }    } else {      fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",              qPrintable(sourceFileName),              qPrintable(unmetDependencies.join(QLatin1Char(','))));    }  }  ....}

Предупреждение PVS-Studio: V571 [CWE-571] Recurring check. The 'if (unmetDependencies.isEmpty())' condition was already verified in line 2203. main.cpp 2209


На первый взгляд перед нами респектабельный код, формирующий подсказку. Но давайте приглядимся. Если первый раз условие unmetDependencies.isEmpty() выполнилось, то второй раз этого уже не произойдёт. Это нестрашно, так как автор планировал вывести то же самое сообщение. Настоящей ошибки нет, но код переусложнён. Он может быть упрощен до следующего варианта:


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",            qPrintable(sourceFileName),            qPrintable(unmetDependencies.join(QLatin1Char(','))));  }  ....}

Фрагмент N76: "код с запахом" сложный тернарный оператор


bool QDockAreaLayoutInfo::insertGap(....){  ....  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo)      : widgetItem ? QDockAreaLayoutItem(widgetItem)                    : QDockAreaLayoutItem(placeHolderItem);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'widgetItem' is always true. qdockarealayout.cpp 1167


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


  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo) : QDockAreaLayoutItem(widgetItem);

Фрагмент N77: "код с запахом" избыточная защита


typedef unsigned int uint;ReturnedValue TypedArrayCtor::virtualCallAsConstructor(....){  ....  qint64 l = argc ? argv[0].toIndex() : 0;  if (scope.engine->hasException)    return Encode::undefined();  // ### lift UINT_MAX restriction  if (l < 0 || l > UINT_MAX)    return scope.engine->throwRangeError(QLatin1String("Index out of range."));  uint len = (uint)l;  if (l != len)    scope.engine->throwRangeError(      QStringLiteral("Non integer length for typed array."));  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'l != len' is always false. qv4typedarray.cpp 306


Кто-то очень переживает, что значение 64-битной переменной не вмещается в 32-битную переменную unsigned. И использует сразу две проверки корректности. При этом вторая проверка избыточна.


Вот этого условия более чем достаточно:


if (l < 0 || l > UINT_MAX)

Приведенный ниже фрагмент можно смело удалить, и программа менее надёжной не станет:


uint len = (uint)l;if (l != len)  scope.engine->throwRangeError(    QStringLiteral("Non integer length for typed array."));

Дальше продолжать не буду. Думаю, идею вы поняли.


Здесь можно сделать маленький вывод: результатом использования анализатора PVS-Studio будет не только устранение ошибок, но и упрощение кода.


Другие ошибки


Я остановился после того, как описал 77 дефектов. Это красивое число и выписанного более чем достаточно, чтобы написать статью. Однако это не значит, что нет других ошибок, которые способен выявить PVS-Studio. При изучении лога я был весьма поверхностен и пропускал всё, где нужно было разбираться более пары минут, ошибка это или нет :).


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


Заключение


Статический анализ это круто! После внедрения PVS-Studio будет экономить время и нервы, выявляя множество ошибок сразу после написания кода. Намного лучше искать на code review с коллегами не опечатки, а высокоуровневые ошибки и обсуждать эффективность реализованного алгоритма. Тем более, как показывает практика, эти дурацкие опечатки всё равно отлично прячутся при просмотре кода глазами. Так что пусть их лучше ищет программа, а не человек.


Если у вас ещё остались вопросы или возражения, приглашаю познакомиться со статьёй "Причины внедрить в процесс разработки статический анализатор кода PVS-Studio". С вероятность 90 % вы найдете в ней ответ на ваши вопросы :). В оставшихся 10 % случаев напишите нам, пообщаемся :).


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Date Processing Attracts Bugs or 77 Defects in Qt 6.

Подробнее..

PVS-Studio, Blender цикл заметок о пользе регулярного использования статического анализа

03.03.2021 20:06:54 | Автор: admin

PVS-Studio мониторит код Blender
В статьях мы регулярно повторяем важную мысль: статический анализатор должен использоваться регулярно. В этом случае многие ошибки выявляются на самом раннем этапе, а их исправление максимально дёшево. Однако теория это одно, но намного лучше подкреплять слова практическими примерами. Рассмотрим несколько свежих ошибок, появившихся в новом коде проекта Blender.


Недавно мы настроили регулярную проверку проекта Blender, о чём мой коллега рассказал в статье "Just for fun: команда PVS-Studio придумала мониторить качество некоторых открытых проектов". В дальнейшем планируем начать мониторить ещё некоторые интересные проекты.


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


Итак, давайте посмотрим, что найдено в свежем коде проекта Blender.


Фрагмент первый: double-checked locking


typedef struct bNodeTree {  ....  struct NodeTreeUIStorage *ui_storage;} bNodeTree;static void ui_storage_ensure(bNodeTree &ntree){  /* As an optimization, only acquire a lock if the UI storage doesn't exist,   * because it only needs to be allocated once for every node tree. */  if (ntree.ui_storage == nullptr) {    std::lock_guard<std::mutex> lock(global_ui_storage_mutex);    /* Check again-- another thread may have allocated the storage       while this one waited. */    if (ntree.ui_storage == nullptr) {      ntree.ui_storage = new NodeTreeUIStorage();    }  }}

Предупреждение PVS-Studio. V1036: Potentially unsafe double-checked locking. node_ui_storage.cc 46


Перед нами неправильная реализация блокировки с двойной проверкой. Для пояснения проблемы процитирую фрагмент статьи "C++ and the Perils of Double-Checked Locking", написанной Scott Meyers и Andrei Alexandrescu ещё в 2004 году. Как видите, проблема давно известна, но это не защищает разработчиков от того, чтобы наступать на одни и те же грабли. Хорошо, что анализатор PVS-Studio помогает выявлять подобные проблемы :). Итак, фрагмент из статьи:


Consider again the line that initializes pInstance: pInstance = newSingleton;

This statement causes three things to happen:

Step 1: Allocate memory to hold a Singleton object.

Step 2: Construct a Singleton object in the allocated memory.

Step 3: Make pInstance point to the allocated memory.

Of critical importance is the observation that compilers are not constrainedto perform these steps in this order! In particular, compilers are sometimes allowed to swap steps 2 and 3. Why they might want to do that is a question we'll address in a moment. For now, let's focus on what happens if they do.

Consider the following code, where we've expanded pInstance's initialization line into the three constituent tasks we mentioned above and where we've merged steps 1 (memory allocation) and 3 (pInstance assignment) into a single statement that precedes step 2 (Singleton construction). The idea is not that a human would write this code. Rather, it's that a compiler might generate code equivalent to this in response to the conventional DCLP source code (shown earlier) that a human would write.

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


Подобные ошибки очень коварны! Они могут очень редко проявлять себя. Программа вроде как работает, проходит все тесты и так далее. Но время от времени она неожиданно падает у пользователей, и понять причину может быть крайне проблематично. Воспроизвести такую ошибку может быть очень сложно. Другим словами, исправление этой ошибки по жалобе от пользователей может обойтись в 1000 раз дороже, чем правка кода, сделанная по результату анализа кода с помощью PVS-Studio или аналогичного инструмента.


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


Фрагмент второй: realloc


static void icon_merge_context_register_icon(struct IconMergeContext *context,                                             const char *file_name,                                             struct IconHead *icon_head){  context->read_icons = realloc(context->read_icons,    sizeof(struct IconInfo) * (context->num_read_icons + 1));  struct IconInfo *icon_info = &context->read_icons[context->num_read_icons];  icon_info->head = *icon_head;  icon_info->file_name = strdup(path_basename(file_name));  context->num_read_icons++;}

Анализатор PVS-Studio выдаёт здесь два предупреждения, и это правильно. Здесь действительно допущено сразу две ошибки различного плана.


Первая: V701: realloc() possible leak: when realloc() fails in allocating memory, original pointer 'context->read_icons' is lost. Consider assigning realloc() to a temporary pointer. datatoc_icon.c 252


Если память не удастся выделить, функция realloc вернёт значение NULL. Нулевой указатель будет записан в переменную context->read_icons, а её предыдущее значение будет потеряно. Раз предыдущее значение указателя потеряно, то и невозможно освободить ранее выделенный блок памяти, на который ссылался этот указатель. Произойдёт утечка памяти.


Вторая: V522: There might be dereferencing of a potential null pointer 'context->read_icons'. Check lines: 255, 252. datatoc_icon.c


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


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


Фрагмент третий: разыменование указателя для проверки


static int node_link_invoke(bContext *C, wmOperator *op, const wmEvent *event){  ....  bNodeLinkDrag *nldrag = node_link_init(bmain, snode, cursor, detach);  nldrag->last_picked_multi_input_socket_link = NULL;  if (nldrag) {    op->customdata = nldrag;  ....}

Предупреждение PVS-Studio: V595: The 'nldrag' pointer was utilized before it was verified against nullptr. Check lines: 1037, 1039. node_relationships.c


Один из самых частых паттернов ошибок (proof). В начале указатель nldrag разыменовывается. Но из следующего условного оператора становится видно, что на самом деле этот указатель может быть нулевым.


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


Кстати, нашлась ещё одна такая-же ошибка, но описывать её неинтересно. Приведу только сообщение: V595: The 'seq' pointer was utilized before it was verified against nullptr. Check lines: 373, 385. strip_add.c


Заключение


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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio, Blender: Series of Notes on Advantages of Regular Static Analysis of Code.

Подробнее..

Новые возможности по поиску угроз безопасности и защищённости в PVS-Studio 7.12

12.03.2021 12:15:29 | Автор: admin

Безопасность. Что для вас значит это слово? В наше время компании тратят много усилий для того, чтобы их продукт был защищён от взлома и разного рода утечек информации. PVS-Studio решил помогать своим пользователям в данном направлении и расширять функционал в этой области. Поэтому одним из главных новшеств предстоящего релиза станет появление в анализаторе новых возможностей обеспечения безопасности и защищённости кода. Об этом и будет рассказано в данной статье.

Пару слов о нас в плане безопасности и защищённости

На данный момент PVS-Studio развивается не только как статический анализатор для поиска дефектов качества кода (quality control решение), но и как решение для поиска дефектов защищённости (security) и безопасности (safety). В контексте защищённости мы являемся SAST-решением. SAST (Static Application Security Testing) это разновидность статического анализа кода, ориентированная на поиск потенциальных уязвимостей защищённости. Такой анализ может выявить большое количество дефектов, в том числе даже тех, которые не успели проявить себя. Что касается безопасности, то это уже другое направление, которое ориентировано на обеспечение надёжности и отказоустойчивости программ.

Как вы понимаете из названия данной статьи, мы расширяем свой функционал в данных областях. Ранее у нас уже были различные таблицы соответствия стандартам безопасности и защищённости на нашем сайте. Но пользоваться этим было не очень удобно, ведь в непосредственно результаты работы анализатора эта информация не попадала. Теперь же мы не только делаем использование данных возможностей анализатора более дружелюбным для пользователей (например, путём интеграции в интерфейсы наших IDE плагинов), но и расширяем уже имеющуюся базу, добавляя поддержку новых стандартов. Дополнительным толчком к этому для нас стало упоминание PVS-Studio в отчете Static Application Security Testing, Q3 2020 от компании Forrester Research одного из ведущих исследователей влияния новых и инновационных технологий на бизнес-процессы и рынок. Подробнее об этом и том, как мы развились в SAST и safety решение, вы можете почитать тут.

Новые возможности

Ну и чтобы не тянуть время, давайте сразу обозначим, что конкретно было добавлено. Итак, новое, безопасное и классное в PVS-Studio:

  • В анализатор добавлены новые группы диагностик OWASP ASVS и The AUTOSAR C++14 Coding Guidelines. Раньше соответствие диагностических правил PVS-Studio данным стандартам было доступно только на нашем сайте. Новых диагностических правил получилось более 50 штук!

  • В результатах работы анализатора теперь выдаётся информация о соответствии срабатываний стандарту безопасного написания кода SEI CERT. Раньше эта информация также была доступна только на сайте PVS-Studio.

  • Доработан интерфейс наших плагинов для Visual Studio, JetBrains Rider, IntelliJ IDEA для удобной работы с сообщениями анализатора, имеющими идентификаторы стандартов безопасности и защищённости.

  • Поддержаны новые группы диагностик (OWASP, AUTOSAR) в PlogConverter.

  • Поддержаны новые диагностики (OWASP, AUTOSAR) в SonarQube на уровне тегов. Провели работу по классификации наших диагностических правил по OWASP Top 10.

Примечание. В предыдущих версиях уже были поддержаны такие стандарты безопасности, как MISRA C:2012 и MISRA C++:2008. Для них на момент написания статьи реализовано 74 диагностических правила.

Ещё у нас поддержано соответствие наших диагностик наиболее распространённой классификации потенциальных уязвимостей CWE (Common Weakness Enumeration). Количество диагностик, которые подходят под данную классификацию у нас уже 514.

Новые группы диагностик

Поговорим немного о новых группах диагностик (OWASP и AUTOSAR), которые ранее присутствовали только на нашем сайте в виде сопоставлений. В новом релизе PVS-Studio 7.12 мы добавляем диагностики из этих стандартов в виде отдельных групп правил со своими номерами, документацией и всеми остальными присущими нашим диагностическим правилам вещами. То есть, проверяя проект, анализатор вам будет выдавать предупреждения для новых групп, по аналогии с остальными предупреждениями. Раньше из всех security и safety правил отдельные группы были только у диагностик PVS-Studio, соответствующих стандартам MISRA C и C++.

А вообще, что это за слова такие странные: OWASP, AUTOSAR? Давайте немного разъясним ситуацию.

The AUTOSAR C++14 Coding Guidelines это набор руководств для написания кода на языке C++14, который предназначен для работы в системах, где важны безопасность и отказоустойчивость. Основной сферой применения данного документа является автомобильная промышленность. Но он также может быть использован и в других отраслях, занимающихся разработкой встраиваемых систем.

Для данного стандарта мы создали отдельную группу и выделили ей номера с 3500 по 3999. Сопоставление этих диагностик со стандартом AUTOSAR вы можете посмотреть здесь.

OWASP Application Security Verification Standard это список требований к безопасности приложений и тестов, которые могут использоваться архитекторами программного обеспечения (ПО), разработчиками, тестировщиками, специалистами по защищённости приложений, продавцами и пользователями инструментов для разработки, сборки, тестирования и верификации защищённых приложений.

Как вы поняли, в отличие от стандарта организации AUTOSAR, OWASP ASVS не привязан к какому-то конкретному языку. Поэтому у нас реализованы диагностики этого типа на всех анализируемых нами языках (C, C++, C#, Java). Данные диагностические правила получили свою группу и номера с 5000 по 5999.

Теперь перейдём к CERT. SEI CERT Coding Standard это набор стандартов написания ПО для повышения надёжности и безопасности ПО на языках C, C++, Java и Perl. Эти стандарты разрабатываются координационным центром CERT (CERT Coordination Center, CERT/CC). Их сопоставление с правилами PVS-Studio представлено здесь.

Однако, в случае с CERT, мы не стали создавать новую группу диагностик. Причиной этому послужило то, что под этот стандарт попадает значительная часть наших диагностик общего назначения (General Analysis). Но не переживайте, информацию о том, что диагностика это конкретное CERT правило, вы всё равно узнаете. Она точно так же добавляется в результат работы анализатора, как OWASP ASVS или AUTOSAR C++14 Coding Guidelines.

При этом у нас продолжается поддержка таких стандартов, как MISRA C:2012 и MISRA C++:2008. Это стандарты разработки программного обеспечения, основная цель которых улучшить безопасность, переносимость и надёжность программ для встраиваемых систем (сопоставление).

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

В плагинах

Ну вот мы и добавили новые диагностики. А где же посмотреть результат их работы? Ну конечно же, в наших плагинах! На сегодняшний день мы поддерживаем отображение информации о стандартах безопасности в плагинах для трех IDE. Это Visual Studio (для версий с 2010 по 2019), JetBrains Rider и IntelliJ IDEA. Чтобы плагины могли отображать эти новые срабатывания, были сделаны следующие доработки:

  • Добавлен новый столбец SAST, в который выводится вся информация о MISRA C:2012, MISRA C++:2008, The AUTOSAR C++14 Coding Guidelines, OWASP ASVS, SEI CERT Coding Standard из предупреждений.

  • Убран столбец MISRA. Теперь вся информация заносится в столбец SAST. Этот же столбец будет использоваться в будущем и при поддержке нами новых стандартов.

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

Приведу пару картинок, чтобы вы имели представление, как это выглядит. В плагине для Visual Studio 2019 это выглядит вот так:

Точно такой же функционал мы добавили в Rider и в IntelliJ IDEA. Вот так это выглядит в Rider:

PlogConverter

Мы не могли забыть про нашу утилиту, которая позволяет конвертировать отчеты в различные форматы. Теперь все наши типы отчётов, в которые можно конвертировать результаты работы анализатора, поддерживают OWASP и AUTOSAR. Для примера посмотрим на, возможно, самый часто используемый тип для конвертации FullHtml. Этот тип позволяет вам изучать отчет в браузере: красиво и удобно, если нет возможности напрямую работать с плагином в вашей среде разработки. Плюс такой отчёт или ссылку на него удобно рассылать по почте.

Собственно, быстренько получили нужный файл и давайте теперь его посмотрим. Как вы видите, в заголовке можно увидеть новое поле Total Warnings (OWASP), которое говорит, сколько у вас потенциальных ошибок из этой категории:

Вот так отображается сам SAST столбец:

SonarQube

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

На данный момент мы расширяем возможности нашего плагина, добавляя теги для диагностик, относящихся к OWASP, AUTOSAR группам. Сейчас сообщения группы OWASP выглядят вот так:

Также мы провели работу по классификации наших диагностических правил по OWASP Top 10. OWASP Top 10 это рейтинг самых опасных векторов атак на веб-приложения. Каждое место в этом рейтинге имеет описание и примеры сценариев атаки, а также ссылки на правила из стандарта OWASP ASVS и классификацию CWE, которые к нему относятся. Для примера вы можете посмотреть, как выглядит одно из мест рейтинга.

В OWASP Top 10 входят такие уязвимости, как:

  1. инъекции;

  2. сломанная аутентификация;

  3. раскрытие конфиденциальных данных;

  4. внешние объекты XML;

  5. нарушенный контроль доступа;

  6. неверная конфигурация безопасности;

  7. межсайтовый скриптинг;

  8. небезопасная десериализация;

  9. использование компонентов с известными уязвимостями;

  10. недостаточное ведение журнала и мониторинг.

В SonarQube же они отображаются вот здесь:

Сделано это по аналогии с тем, как у нас уже отображается CWE, который вы тоже можете видеть на скриншоте. Для этого мы используем специальную вкладку Security Category. Приведу пример, как выглядит заполненный CWE:

Заключение

Как вы видите, этот релиз у нас вышел достаточно насыщенным. Анализатор получил новые группы диагностик для стандартов OWASP ASVS и AUTOSAR C++14 Coding Guidelines. В результаты работы анализатора дополнительно стала выводиться информация о соответствии срабатываний стандарту SEI CERT. Интерфейс наших плагинов (Visual Studio, JetBrains Rider, IntelliJ IDEA) обновился для удобной работы с сообщениями анализатора, имеющими идентификаторы стандартов безопасности и защищённости. Ещё и PlogConverter с SonarQube научились работать с новыми группами диагностик (OWASP, AUTOSAR). И все это только то, что касается направления безопасности и защищённости!

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

Будьте счастливы и следите за состоянием своего кода. Спасибо за внимание!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov, Paul Eremeev. PVS-Studio 7.12 New Features for Finding Safety and Security Threats.

Подробнее..

Пример, как в PVS-Studio появляются новые диагностики

20.03.2021 18:12:42 | Автор: admin

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


Всё началось с проверки проекта COVID-19 CovidSim Model и статьи про неинициализированную переменную. Проект оказался маленьким и написанным с использованием современного стандарта языка C++. Это значит, что он отлично может пополнить базу тестовых проектов для регрессионного тестирования ядра анализатора PVS-Studio.


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


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


MISRA С и MISRA C++ диагностики предназначены для разработчиков встраиваемых систем, и их суть сводится к ограничению использования небезопасных конструкций программирования. Например, не рекомендуется применять оператор goto (V2502), так как он провоцирует создание сложного кода, в котором легко допустить логическую ошибку. Подробнее с философией стандарта кодирования MISRA можно познакомиться в статье "Что такое MISRA и как её готовить".


Для прикладного программного обеспечения, которым как раз и является проект CovidSim, включать набор MISRA диагностик не имеет смысла. Пользователь просто утонет в огромном количестве малополезных для него сообщений. Например, экспериментируя с этим набором диагностик, мы получали более миллиона предупреждений для некоторых открытых проектов среднего размера. Грубо говоря, с точки зрения MISRA, в каждой третьей строчке кода может быть что-то не так :). Естественно, никто всё это смотреть и тем более править не будет. Проект или сразу разрабатывается с учётом MISRA рекомендаций, или для него этот стандарт кодирования неактуален.


Но мы отвлеклись от темы. Так вот, бегло просматривая MISRA предупреждения, коллега зацепился взглядом за предупреждение V2507, выданное для этого фрагмента кода.


if (radiusSquared > StateT[tn].maxRad2) StateT[tn].maxRad2 = radiusSquared;{  SusceptibleToLatent(a->pcell);  if (a->listpos < Cells[a->pcell].S)  {    UpdateCell(Cells[a->pcell].susceptible, a->listpos, Cells[a->pcell].S);    a->listpos = Cells[a->pcell].S;    Cells[a->pcell].latent[0] = ai;  }}StateT[tn].cumI_keyworker[a->keyworker]++;

Правило V2507 заставляет оборачивать тела условных операторов в фигурные скобки.


В первый момент наш дотошный коллега подумал, что анализатор дал сбой. Ведь здесь есть блок текста в фигурных скобках! Перед нами ложное срабатывание?


Давайте присмотримся. Код только кажется корректным, но таковым не является! Фигурные скобки не относятся к оператору if.


Отформатируем код для наглядности:


if (radiusSquared > StateT[tn].maxRad2)  StateT[tn].maxRad2 = radiusSquared;{  SusceptibleToLatent(a->pcell);  ....}

Согласитесь, это красивый баг. Он наверняка войдёт в Top10 C++ ошибок, найденных нами в 2021 году.


И что из этого следует? Подход, диктуемый стандартом MISRA, работает! Да, он заставляет писать везде фигурные скобки. Да, это утомительно. Но это оправданная плата за повышение надёжности встроенных приложений, используемых в медицинской технике, автомобилях и других системах высокой ответственности.


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


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


Выдать предупреждение, если для оператора if выполняются следующие условия:


  • весь условный оператор if записан в одну строчку и имеет только then-ветку;
  • следующий statement после if это compound statement, и он находится не на той же строке, что и if.

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


Именно так эта идея сейчас оформлена в нашей системе учёта задач. Возможно, в процессе реализации что-то будет сделано по-другому, но это уже неважно. Главное, появится хорошее диагностическое правило, которое начнёт выявлять новый паттерн ошибки. Далее мы распространим его на C# и Java ядра анализатора PVS-Studio.


Только что мы рассмотрели интересный пример, как было сформулировано новое диагностическое правило, которое затем будет реализовано в PVS-Studio. Скажем спасибо проекту CovidSim, стандарту кодирования MISRA и наблюдательности нашего коллеги.


Спасибо за внимание и следуйте за мной в мир С++ и багов :). Twitter. Facebook.


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


  1. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей.
  2. Под капотом PVS-Studio для Java: разработка диагностик.
  3. Использование машинного обучения в статическом анализе исходного кода программ.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Example of How New Diagnostics Appear in PVS-Studio.

Подробнее..

Как PVS-Studio защищает от поспешных правок кода

24.03.2021 12:04:07 | Автор: admin

Недостижимый код
Хотя только недавно была заметка про проект CovidSim, есть хороший повод вновь про него вспомнить и продемонстрировать пользу регулярного использования PVS-Studio. Бывает, что все мы спешим и вносим правки в код, потеряв сосредоточенность. Статический анализатор может оказаться здесь хорошим помощником.


Всё началось с написания вот этих двух небольших заметок про открытый проект COVID-19 CovidSim Model:



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


Только мы начали проверять этот проект, как результаты не заставили себя ждать :). Сейчас я покажу свежую ошибку, вызванную недавними неаккуратными правками кода. Конечно, мы не станем описывать в дальнейшем каждый баг и недочёт, обнаруженный анализатором. Будем писать только про что-то интересное.


Вот что получилось, после недавних модификаций файла CovidSim.cpp:


Правки


Кто-то решил создавать массивы не на стеке, а в куче. Но был невнимателен, внося изменения. Обратите внимание, что освобождение памяти находится после оператора return:


int GetXMLNode(....){  char* buf = new char[65536];  char* CloseNode = new char[2048];  char* CloseParent = new char[2048];  ....  if (ResetFilePos) fseek(dat, CurPos, 0);  return ret;  delete[] buf;  delete[] CloseNode;  delete[] CloseParent;}

В результате перед нами фрагмент недостижимого кода (unreachable code). И заодно утечка памяти.


Хорошо, что PVS-Studio тут же сообщает про эту ошибку: V779 Unreachable code detected. It is possible that an error is present. CovidSim.cpp 675


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


И последнее, что хочется отметить. Эта ошибка была бы невозможна, если не вручную управлять выделением и освобождением памяти, а применить RAII подход и использовать умные указатели.


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


std::unique_ptr<char[]> buf(new char[65536]);std::unique_ptr<char[]> CloseNode(new char[2048]);std::unique_ptr<char[]> CloseParent(new char[2048]);

Спасибо за внимание. Следуйте за мной в мир С++ и багов :). Twitter. Facebook.

Подробнее..

Теперь PVS-Studio ещё лучше знает, что за зверь такой strlen

27.04.2021 16:10:26 | Автор: admin

0824_DataFlow_And_Strlen_ru/image1.png
Как-то так несправедливо сложилось, что мы почти не уделяем в наших заметках внимание усовершенствованию внутренних механизмов анализатора, в отличие от новых диагностик. Поэтому давайте для разнообразия познакомимся с новым полезным усовершенствованием, коснувшимся анализа потока данных.


Всё началось с твита от JetBrains CLion IDE


На днях я увидел в Twitter пост от JetBrains про новые возможности статического анализатора, встроенного в CLion.


0824_DataFlow_And_Strlen_ru/image2.png


Поскольку мы скоро планируем выпустить плагин PVS-Studio для CLion, то я не мог пройти мимо и не написать, что мы тоже не лыком шиты. И что есть смысл попробовать PVS-Studio как плагин для CLion, чтобы находить ещё больше ошибок.


0824_DataFlow_And_Strlen_ru/image3.png


Ну и ещё немного с ними мило попереписывался:



После всего этого я подумал. А ведь они молодцы! Улучшили анализ потока данных и рассказывают миру. А мы чем хуже? Мы ведь тоже постоянно что-то улучшаем внутри анализатора, в том числе и тот же механизм анализа потока данных. И вот я уже пишу эту заметку.


А что у нас интересного с Data Flow


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


bool foo(){  unsigned N = 2;  for (unsigned i = 0; i < N; ++i)  {    bool stop = (i - 1 == N);    if (stop)      return true;  }  return false;}

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


Почему false? Давайте быстро посчитаем:


  • диапазон значения переменной i = [0; 1];
  • возможные значения выражения i-1 = [0; 0] U [UINT_MAX; UINT_MAX];
  • значение переменной N, равное двойке, не входит в множество { 0, UINT_MAX };
  • условие всегда ложно.

Примечание. Неопределённого поведения здесь нет, так как происходит переполнение (wrap) при работе с беззнаковым типом.


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


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


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


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

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


Предупреждение PVS-Studio: V512 A call of the 'strcpy' function will lead to overflow of the buffer 'astr'. asm.cpp 21


Всё равно не видите ошибку? Давайте внимательно разберём код. Для начала уберём всё не относящееся к делу:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

Есть локальный массив из 128 байт, в который планируется скопировать строчку, переданную в качестве аргумента. Копирование не должно выполняться, если строка пустая или содержит более 127 символов (не считая терминальный ноль).


Пока всё логично и правильно? На первый взгляд, да. Но что это?! Что это за константа 0x127?!


Это вовсе не 127. Совсем не 127 :)


Константа задана в шестнадцатеричной системе. Если перевести в десятичную, то получается 295.


Итак, написанный код эквивалентен следующему:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 295)) return 1;  strcpy(astr,str);  ....}

Как видите, проверка никак не защищает от переполнения буфера, и анализатор совершенно правильно предупреждает о проблеме.


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


Теперь PVS-Studio выводит из выражения, что длина строки str лежит в диапазоне [1..295], а значит, может возникнуть выход за границу массива, если попытаться его скопировать в буфер astr.


0824_DataFlow_And_Strlen_ru/image4.png


Новые вызовы


Описанная ошибка присутствует и в текущей версии кодовой базы проекта FCEUX. Но мы её не найдём, так как код изменился и теперь длина строки сохраняется в переменной. Это разрывает взаимосвязь между строкой и её длиной. Анализатор пока, к сожалению, молчит на новый вариант кода:


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  int len = strlen(str);  if ((!len) || (len > 0x127)) return 1;  strcpy(astr,str);  ....}

Человеку такой код может показаться даже проще, но, с точки зрения статического анализа, он труден для отслеживания значений. Нужно учитывать, что значение переменной len является длиной строки str. Дополнительно требуется аккуратно отслеживать, когда разоврётся эта взаимосвязь при модификации содержимого строки или переменной len.


Пока это анализатор PVS-Studio делать не умеет. Зато видно, куда можно и нужно развиваться! Со временем научимся находить ошибку и в этом новом коде.


Кстати, читатель может задаться вопросом, а почему мы анализируем старый код проектов и не обновляем их регулярно? Всё просто. Если обновлять проекты, мы не сможем проводить регрессионное тестирование. Будет непонятно, появились изменения в работе анализатора из-за правки кода самого анализатора или из-за правки в коде проверяемого проекта. Поэтому мы не обновляем открытые проекты, используемые в качестве базы для тестирования.


А чтобы тестировать анализатор на современном коде, написанном на C++14, C++17 и т.д., мы постепенно пополняем базу новыми проектами. Например, относительно недавно мы добавили коллекцию header-only C++ библиотек (awesome-hpp).


Заключение


Развивать механизмы анализа потока данных интересно и полезно. А если вам интересней в целом больше узнать о работе статических анализаторов кода, то предлагаем вашему вниманию следующие наши публикации:


  1. Анализатор кода не прав, да здравствует анализатор
  2. Ложные срабатывания в PVS-Studio: как глубока кроличья нора
  3. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей
  4. Использование машинного обучения в статическом анализе исходного кода программ

Приглашаю скачать анализатор PVS-Studio и проверить свои проекты.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio Learns What strlen is All About.

Подробнее..

C программист, испытай себя найди ошибку

01.02.2021 16:08:09 | Автор: admin

Анализатор PVS-Studio регулярно пополняется новыми диагностическими правилами. Что интересно, часто диагностики обнаруживают подозрительные фрагменты кода еще до окончания всех работ. Например, в процессе тестирования на open-source проектах. Одной из подобных интересных 'находок' и хотелось бы поделиться сегодня с вами.

Как говорилось выше, один из этапов тестирования диагностического правила - проверка его работы на реальной кодовой базе. Для этого используется набор отобранных open-source проектов, для которых проводится анализ. Очевидное преимущество такого подхода - возможность посмотреть, как ведёт себя диагностическое правило в "реальных условиях". Менее очевидное - иногда находится что-то настолько интересное, что про это и заметку не грех написать.

Итак, предлагаю посмотреть на код из проекта Bouncy Castle C# и найти в нём ошибку:

public static string ToString(object[] a){  StringBuilder sb = new StringBuilder('[');  if (a.Length > 0)  {    sb.Append(a[0]);    for (int index = 1; index < a.Length; ++index)    {      sb.Append(", ").Append(a[index]);    }  }  sb.Append(']');  return sb.ToString();}

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

Уверен, многие не смогли найти ошибку без открытия IDE или документации к классу StringBuilder. Ошибка допущена при вызове конструктора:

StringBuilder sb = new StringBuilder('[');

Собственно, об этом и предупреждает статический анализатор PVS-Studio: V3165 Character literal '[' is passed as an argument of the 'Int32' type whereas similar overload with the string parameter exists. Perhaps, a string literal should be used instead. Arrays.cs 193.

Программист хотел создать экземпляр типа StringBuilder, строка в котором будет начинаться с символа '['. Однако из-за опечатки будет получен объект ёмкостью под 91 элемент, не содержащий символов.

Это произошло из-за того, что вместо двойных кавычек использовались одинарные, что привело к вызову не той перегрузки конструктора:

....public StringBuilder(int capacity);public StringBuilder(string? value);....

При вызове конструктора символьный литерал '[' будет неявно приведен к соответствующему значению типа int (91 в Unicode). Это приведет к вызову конструктора с параметром типа int, задающим начальную вместимость. Программист же хотел вызвать конструктор, задающий начало строки.

Для исправления ошибки следует заменить символьный литерал на строковый (т.е. использовать "[", а не '['), что позволит вызвать правильную перегрузку конструктора.

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

Диагностика, срабатывание которой было описано выше, была добавлена в релизе PVS-Studio 7.11. Вы сами можете загрузить последнюю версию анализатора и посмотреть на что способна не только диагностика V3165, но и другие диагностики для языков C, C++, C# и Java.

Кстати, идеи диагностик нам часто предлагают сами пользователи. Так получилось и в этот раз - спасибо пользователю Krypt с ресурса Habr. Если у вас также есть идеи диагностических правил - не стесняйтесь предлагать их!

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. C# Programmer, It's Time to Test Yourself and Find Error.

Подробнее..

Шпион под прикрытием проверяем исходный код ILSpy с помощью PVS-Studio

04.02.2021 18:19:53 | Автор: admin

В блоге компании PVS-Studio можно найти далеко не одну статью с результатами проверок исходного кода различных компиляторов. С другой стороны, немного обделённым вниманием PVS-Studio кажется другой класс программ - класс декомпиляторов. Дабы сделать мир более гармоничным, был проверен исходный C# код программы ILSpy, как раз относящейся к лагерю декомпиляторов. Давайте же посмотрим, что интересного смог найти PVS-Studio.

Введение

Наверное, каждому программисту хоть раз в жизни приходилось использовать декомпилятор. Цели у каждого из нас могли быть разными: узнать имплементацию какого-то метода, подтвердить или опровергнуть свои подозрения насчёт ошибки в используемой библиотеке, или просто под влиянием любопытства посмотреть близкий к исходному код интересующей нас программы. В мире .NET'а при упоминании слова декомпилятор обычно в голову приходит или dotPeek или ILSpy. Про .NET Reflector сейчас вспоминают меньше. Припоминаю, как, когда в первый раз узнал про подобный класс утилит и декомпилировал чужую библиотеку, в голове пробежала мысль о шпионаже. И такие мысли были не только у меня одного - не спроста же декомпилятор ILSpy получил своё название. Итак, в данной статье я описал интересные и подозрительные с моей точки зрения места, обнаруженные с помощью PVS-Studio в исходном коде проекта ILSpy. Захотелось посмотреть, так сказать, что скрывает наш шпион и, в случае необходимости, "прикрыть" его статическим анализатором.

Откровенно говоря, статья про ILSpy получилась несколько случайно. Среди пользователей нашего анализатора достаточно много студий, занимающихся игровой разработкой. Это одна из причин того, почему мы в компании стараемся сделать наш инструмент как можно более полезным и удобным для разработчиков игр, в том числе использующих движки Unity и Unreal Engine.

Я знаком с клиентами, которые используют PVS-Studio совместно с Unreal Engine, а вот про Unity разработчиков, практикующих использование нашего анализатора, слышу значительно реже. В связи с этим хочется популяризировать анализатор среди Unity сообщества. Одним из способов популяризации могла бы стать статья о проверке open-source игры, разработанной при помощи данного движка. Вот только здесь у меня возникла проблема - я не смог найти такую игру (может вы, читатели, сможете любезно предоставить идеи для таких проверок?). Обстоятельства при поиске игры с открытым исходным кодом складывались немного странно, и на одном сайте в списке самых популярных проектов для Unity оказался ILSpy (для меня остаётся загадкой, как и почему он попал в этот список). К слову, ILSpy входит в пул проектов, на которых мы регулярно тестируем наш C# анализатор внутри компании. Странно, что статьи про этот проект у нас до сих пор не было. Ну что ж, раз не удалось найти Unity проект для анализа, так давайте проверим попавшийся на глаза ILSpy.

Вот что написано в описание проекта на GitHub'е: ILSpy is the open-source .NET assembly browser and decompiler.

Я не нашёл информацию о том, что при разработке проекта используется какой-нибудь статический анализатор. Наверное, так даже интереснее - PVS-Studio будет первым. Перейдём непосредственно к результатам анализа.

Замена без замены

V3038 The '"'"' argument was passed to 'Replace' method several times. It is possible that other argument should be passed instead. ICSharpCode.Decompiler ReflectionDisassembler.cs 772

private static void WriteSimpleValue(ITextOutput output,                                     object value, string typeName){  switch (typeName)  {    case "string":      output.Write(  "'"                   + DisassemblerHelpers                      .EscapeString(value.ToString())                      .Replace("'", "\'")                   // <=                   + "'");      break;    case "type":    ....  }  ....}

Кажется, что автор хотел осуществить замену всех вхождений символа одинарной кавычки на строку, состоящую из двух символов: символа обратной косой черты и символа одинарной кавычки. Из-за невнимательности вышла осечка - символ "'" меняется на самого себя, что является бессмысленной операцией. Между присвоением строке значения "'" и "\'" разницы нет - в любом случае строка будет проинициализирована символом одинарной кавычки. Если мы хотим, чтобы в строку попало значение "\'", то backslash нужно экранировать или воспользоваться символом '@': "\\'" или @"\'". То есть вызов метода Replace следует переписать следующим образом:

Replace("'", @"\'")

Правда и только правда

Предупреждение 1

V3022 Expression 'negatedOp == BinaryOperatorType.Any' is always true. ICSharpCode.Decompiler CSharpUtil.cs

static Expression InvertConditionInternal(Expression condition){  var bOp = (BinaryOperatorExpression)condition;  if (   (bOp.Operator == BinaryOperatorType.ConditionalAnd)      || (bOp.Operator == BinaryOperatorType.ConditionalOr))  {    ....  }  else if (   (bOp.Operator == BinaryOperatorType.Equality)           || (bOp.Operator == BinaryOperatorType.InEquality)            || (bOp.Operator == BinaryOperatorType.GreaterThan)           || (bOp.Operator == BinaryOperatorType.GreaterThanOrEqual)           || (bOp.Operator == BinaryOperatorType.LessThan)            || (bOp.Operator == BinaryOperatorType.LessThanOrEqual))  {    ....  }  else  {    var negatedOp = NegateRelationalOperator(bOp.Operator);    if (negatedOp == BinaryOperatorType.Any)                  // <=      return new UnaryOperatorExpression(....);    bOp = (BinaryOperatorExpression)bOp.Clone();    bOp.Operator = negatedOp;    return bOp;  }}

Анализатор предупреждает, что значение переменной negatedOp всегда равно значению Any из перечисления BinaryOperatorType. Чтобы убедиться в этом, давайте посмотрим на код метода NegateRelationalOperator, из которого данная переменная и получает своё значение.

public static BinaryOperatorType NegateRelationalOperator(BinaryOperatorType op){  switch (op)  {    case BinaryOperatorType.GreaterThan:      return BinaryOperatorType.LessThanOrEqual;    case BinaryOperatorType.GreaterThanOrEqual:      return BinaryOperatorType.LessThan;    case BinaryOperatorType.Equality:      return BinaryOperatorType.InEquality;    case BinaryOperatorType.InEquality:      return BinaryOperatorType.Equality;    case BinaryOperatorType.LessThan:      return BinaryOperatorType.GreaterThanOrEqual;    case BinaryOperatorType.LessThanOrEqual:      return BinaryOperatorType.GreaterThan;    case BinaryOperatorType.ConditionalOr:      return BinaryOperatorType.ConditionalAnd;    case BinaryOperatorType.ConditionalAnd:      return BinaryOperatorType.ConditionalOr;  }  return BinaryOperatorType.Any;}

Если к моменту вызова метода NegateRelationalOperator переменная bOp.Operator имела значение, несоответствующее ни одной метке case, то из метода вернётся значение BinaryOperatorType.Any. Видно, что вызов метода NegateRelationalOperator происходит только в том случае, если условия в вышестоящем операторе if и операторе if else были вычислены как false. А если быть совсем внимательным, то становится заметно, что условия в операторах if и if else покрывают все метки case из метода NegateRelationalOperator. Следовательно, к моменту вызова метода NegateRelationalOperator переменная bOp.Operator не подходит ни под одну метку case, и этот метод в данном случае всегда вернёт значение BinaryOperatorType.Any. Вот и получается, что negatedOp == BinaryOperatorType.Any всегда оценивается как true, и на следующей строке происходит возврат значения из метода. Вдобавок получаем недостижимый код:

bOp = (BinaryOperatorExpression)bOp.Clone();bOp.Operator = negatedOp;return bOp;

К слову, анализатор любезно выдал предупреждение и на это: V3142 Unreachable code detected. It is possible that an error is present. ICSharpCode.Decompiler CSharpUtil.cs 81

Предупреждение 2

V3022 Expression 'pt != null' is always true. ICSharpCode.Decompiler FunctionPointerType.cs 168

public override IType VisitChildren(TypeVisitor visitor){  ....  IType[] pt = (r != ReturnType) ? new IType[ParameterTypes.Length] : null;  ....  if (pt == null)    return this;  else    return new FunctionPointerType(      module, CallingConvention, CustomCallingConventions,      r, ReturnIsRefReadOnly,      pt != null ? pt.ToImmutableArray() : ParameterTypes,    // <=      ParameterReferenceKinds);}

Здесь всё очевидно - ветка else выполняется только в том случае, если переменная pt не равна null. Зачем тогда нужно писать тернарный оператор с проверкой переменной pt на неравенство null, мне непонятно. Возможно, в прошлом не было ветвления if else и первого оператора return. Тогда подобная проверка имела бы смысл, ну а сейчас всё же стоит убрать лишний тернарный оператор:

public override IType VisitChildren(TypeVisitor visitor){  ....  IType[] pt = (r != ReturnType) ? new IType[ParameterTypes.Length] : null;  ....  if (pt == null)    return this;  else    return new FunctionPointerType(      module, CallingConvention, CustomCallingConventions,      r, ReturnIsRefReadOnly,      pt.ToImmutableArray(), ParameterReferenceKinds);}

Предупреждение 3

V3022 Expression 'settings.LoadInMemory' is always true. ICSharpCode.Decompiler CSharpDecompiler.cs 394

static PEFile LoadPEFile(string fileName, DecompilerSettings settings){  settings.LoadInMemory = true;  return new PEFile(    fileName,    new FileStream(fileName, FileMode.Open, FileAccess.Read),    streamOptions: settings.LoadInMemory ?                           // <=      PEStreamOptions.PrefetchEntireImage : PEStreamOptions.Default,    metadataOptions: settings.ApplyWindowsRuntimeProjections ?         MetadataReaderOptions.ApplyWindowsRuntimeProjections :        MetadataReaderOptions.None  );}

Аналогично предыдущему срабатыванию анализатора получаем совершенно ненужный тернарный оператор. Свойству settings.LoadInMemory, проверяемому в тернарном оператора, выше присваивается значение true, которое не меняется вплоть до самого тернарного оператора. Для полноты картины приведу код геттера и сеттера самого свойства:

public bool LoadInMemory {  get { return loadInMemory; }  set {      if (loadInMemory != value)      {        loadInMemory = value;        OnPropertyChanged();      }  }}

Думаю, переписанный метод без тернарного оператора приводить не стоит - тут всё достаточно бесхитростно.

Предупреждение 4

V3022 Expression 'ta' is always not null. The operator '??' is excessive. ICSharpCode.Decompiler ParameterizedType.cs 354

public IType VisitChildren(TypeVisitor visitor){  ....  if (ta == null)      return this;  else      return new ParameterizedType(g, ta ?? typeArguments);     // <=}

Тут уже натыкаемся на ненужный null coalescing оператор. При попадании в ветку else переменная ta всегда имеет значение неравное null. Как следствие, использование оператора ?? тут лишнее.

Всего было найдено 31 предупреждение с номером V3022.

Ты здесь лишний

Предупреждение 1

V3025 Incorrect format. A different number of format items is expected while calling 'Format' function. Arguments not used: End. ICSharpCode.Decompiler Interval.cs 269

public override string ToString(){  if (End == long.MinValue)  {    if (Start == long.MinValue)      return string.Format("[long.MinValue..long.MaxValue]", End); // <=    else      return string.Format("[{0}..long.MaxValue]", Start);  }  else if (Start == long.MinValue)  {    return string.Format("[long.MinValue..{0})", End);  }  else  {    return string.Format("[{0}..{1})", Start, End);  }}

При вызове самого первого метода string.Format строка форматирования не соответствует передаваемым в метод фактическим аргументам. Значение переменной End, передаваемое в качестве аргумента, не будет подставлено в строку форматирования, так как в ней отсутствует элемент форматирования {0}. Исходя из логики данного метода это всё же не ошибка, первый оператор return вернёт именно ту строку, которую и задумывали авторы кода. Это, разумеется, не отменяет того факта, что присутствует бесполезный вызов метода string.Format с неиспользуемым аргументом. Это хорошо бы исправить, дабы не вводить в заблуждения человека, который будет читать этот метод.

Предупреждение 2

V3025 Incorrect format. A different number of format items is expected while calling 'AppendFormat' function. Arguments not used: angle. ILSpy.BamlDecompiler XamlPathDeserializer.cs 177

public static string Deserialize(BinaryReader reader){  ....  var sb = new StringBuilder();  ....  sb.AppendFormat(CultureInfo.InvariantCulture,                  "A{0} {2:R} {2} {3} {4}",                  size, angle, largeArc ? '1' : '0',                  sweepDirection ? '1' : '0', pt1);  ....}

В данном случае за бортом оказалась переменная angle. Несмотря на то, что её передали в метод AppendFormat, из-за того, что в строке форматирования отсутствует элемент форматирования {1} и дважды использован {2}, это переменная остаётся неиспользованной. Скорее всего, авторы хотели написать строку формата следующим образом: "A{0} {1:R} {2} {3} {4}".

Двойные стандарты

Предупреждение 1

V3095 The 'roslynProject' object was used before it was verified against null. Check lines: 96, 97. ILSpy.AddIn OpenILSpyCommand.cs 96

protected Dictionary<string, detectedreference=""> GetReferences(....){  ....  var roslynProject =  owner.Workspace                            .CurrentSolution                            .GetProject(projectReference.ProjectId);  var project = FindProject(owner.DTE.Solution                                 .Projects.OfType<envdte.project>(),                            roslynProject.FilePath);              // <=  if (roslynProject != null && project != null)           // <=  ....}

Сначала мы обращаемся к свойству FilePath объекта roslynProject без какого-либо опасения, что в переменной roslynProject может быть записан null, а буквально строкой ниже мы выполняем проверку равенства этой переменной на null. Такой код выглядит небезопасно и чреват возникновением исключения типа NullReferenceException. Для исправления подобной ситуации стоит обращаться к свойству FilePath, используя null-условный оператор, а в методе FindProject предусмотреть возможность получения потенциального null в качестве последнего параметра.

Предупреждение 2

V3095 The 'listBox' object was used before it was verified against null. Check lines: 46, 52. ILSpy FlagsFilterControl.xaml.cs 46

public override void OnApplyTemplate(){  base.OnApplyTemplate();  listBox = Template.FindName("ListBox", this) as ListBox;  listBox.ItemsSource = FlagGroup.GetFlags(....);         // <=  var filter = Filter;  if (filter == null || filter.Mask == -1)  {    listBox?.SelectAll();                                 // <=  }}

Ситуация аналогична предыдущему примеру. Сначала обращаемся к свойству ItemsSource без какой-либо проверки, что в переменной listBox может быть записан null, а несколькими строками ниже уже видим использование null-условного оператора с переменной listBox. Причём переменная listBox между двумя этими обращениями к полю и методу не получала нового значения.

Всего анализатор нашёл 10 предупреждений с номером V3095. Привожу список данных предупреждений:

  • V3095 The 'pV' object was used before it was verified against null. Check lines: 761, 765. ICSharpCode.Decompiler TypeInference.cs 761

  • V3095 The 'pU' object was used before it was verified against null. Check lines: 882, 886. ICSharpCode.Decompiler TypeInference.cs 882

  • V3095 The 'finalStore' object was used before it was verified against null. Check lines: 261, 262. ICSharpCode.Decompiler TransformArrayInitializers.cs 261

  • V3095 The 'definitionDeclaringType' object was used before it was verified against null. Check lines: 93, 104. ICSharpCode.Decompiler SpecializedMember.cs 93

  • V3095 The 'TypeNamespace' object was used before it was verified against null. Check lines: 84, 88. ILSpy.BamlDecompiler XamlType.cs 84

  • V3095 The 'property.Getter' object was used before it was verified against null. Check lines: 1676, 1684. ICSharpCode.Decompiler CSharpDecompiler.cs 1676

  • V3095 The 'ev.AddAccessor' object was used before it was verified against null. Check lines: 1709, 1717. ICSharpCode.Decompiler CSharpDecompiler.cs 1709

  • V3095 The 'targetType' object was used before it was verified against null. Check lines: 1614, 1657. ICSharpCode.Decompiler CallBuilder.cs 1614

Кстати, если вы хотите проверить свой собственный проект с помощью PVS-Studio или перепроверить тот же ILSpy, чтобы более детально изучить срабатывания, то можете собственноручно попробовать анализатор. Перейдя на сайт PVS-Studio, вы сможете как скачать сам анализатор, так и получить триальную лицензию.

Всё идёт к одному

Предупреждение 1

V3139 Two or more case-branches perform the same actions. ILSpy Images.cs 251

protected override ImageSource GetBaseImage(MemberIcon icon){  ImageSource baseImage;  switch (icon)  {    case MemberIcon.Field:      baseImage = Images.Field;      break;    case MemberIcon.FieldReadOnly:      baseImage = Images.FieldReadOnly;      break;    case MemberIcon.Literal:      baseImage = Images.Literal;             // <=      break;    case MemberIcon.EnumValue:      baseImage = Images.Literal;             // <=      break;    case MemberIcon.Property:      baseImage = Images.Property;      break;    case MemberIcon.Indexer:      baseImage = Images.Indexer;      break;    case MemberIcon.Method:      baseImage = Images.Method;      break;    case MemberIcon.Constructor:      baseImage = Images.Constructor;      break;    case MemberIcon.VirtualMethod:      baseImage = Images.VirtualMethod;      break;    case MemberIcon.Operator:      baseImage = Images.Operator;      break;    case MemberIcon.ExtensionMethod:      baseImage = Images.ExtensionMethod;      break;    case MemberIcon.PInvokeMethod:      baseImage = Images.PInvokeMethod;      break;    case MemberIcon.Event:      baseImage = Images.Event;      break;    default:      throw new ArgumentOutOfRangeException(nameof(icon),                  $"MemberIcon.{icon} is not supported!");  }  return baseImage;}

На мой взгляд, это явная ошибка. В случае, если переменная icon равна MemberIcon.EnumValue, то переменная baseImage в ветке case должна получать значение Images.EnumValue. Это хороший пример ошибки, который легко замечает статический анализатор, и легко пропускает человек при беглом обзоре кода.

Предупреждение 2

V3139 Two or more case-branches perform the same actions. ICSharpCode.Decompiler CSharpConversions.cs 829

bool ImplicitConstantExpressionConversion(ResolveResult rr, IType toType){  ....  switch (toTypeCode)  {    case TypeCode.SByte:      return val >= SByte.MinValue && val <= SByte.MaxValue;    case TypeCode.Byte:      return val >= Byte.MinValue && val <= Byte.MaxValue;    case TypeCode.Int16:      return val >= Int16.MinValue && val <= Int16.MaxValue;    case TypeCode.UInt16:      return val >= UInt16.MinValue && val <= UInt16.MaxValue;    case TypeCode.UInt32:      return val >= 0;                 // <=    case TypeCode.UInt64:      return val >= 0;                 // <=  }  ....}

Утверждать, что анализатор нашёл здесь явную ошибку я не берусь, но предупреждение однозначно имеет смысл. Если метки case для значения TypeCode.UInt32 и TypeCode.UInt64 выполняют один и тот же набор действий, почему бы не написать код более компактно:

bool ImplicitConstantExpressionConversion(ResolveResult rr, IType toType){  switch (toTypeCode)  {      ....      case TypeCode.UInt32:      case TypeCode.UInt64:        return val >= 0;  }  ....}

Анализатор выдал ещё 2 предупреждения с номером V3139:

  • V3139 Two or more case-branches perform the same actions. ICSharpCode.Decompiler EscapeInvalidIdentifiers.cs 85

  • V3139 Two or more case-branches perform the same actions. ICSharpCode.Decompiler TransformExpressionTrees.cs 370

Безопасность превыше всего

V3083 Unsafe invocation of event, NullReferenceException is possible. Consider assigning event to a local variable before invoking it. ILSpy MainWindow.xaml.cs 787class ResXResourceWriter : IDisposable

void assemblyList_Assemblies_CollectionChanged(....){  ....  if (CurrentAssemblyListChanged != null)    CurrentAssemblyListChanged(this, e);      // <=}

Подобный способ вызова событий является достаточно распространённым, но то, что мы встречаем данный паттерн во многих проектах, не является оправданием к его применению. Разумеется, это не критичная ошибка, но, как и говорит текст предупреждения анализатора, - этот вызов не является безопасным, и не исключено возникновение исключения типа NullReferenceException. Если между проверкой CurrentAssemblyListChanged на null и вызовом самого события от него отписались все обработчики (например, в другом потоке исполнения), то произойдёт выброс исключения NullReferenceException. Лучше сразу писать безопасный код, например, следующим образом:

void assemblyList_Assemblies_CollectionChanged(....){  ....  CurrentAssemblyListChanged?.Invoke(this, e);}

PVS-Studio обнаружил ещё 8 подобных случаев, и все их можно исправить аналогично представленному выше способу.

Уверенная неуверенность

V3146 Possible null dereference. The 'FirstOrDefault' can return default null value. ILSpy.BamlDecompiler BamlResourceEntryNode.cs 76

bool LoadBaml(AvalonEditTextOutput output, CancellationToken cancellationToken){  var asm = this.Ancestors().OfType<assemblytreenode>()                            .FirstOrDefault().LoadedAssembly;       // <=  ....  return true;}

Из коллекции, возвращаемой методом OfType, посредством вызова метода FirstOrDefault хотят получить первый встретившийся элемент типа AssemblyTreeNode. Все мы знаем, что, если в коллекции не будет встречено такого элемента, который бы удовлетворял предикату поиска, или, если сама коллекция оказалось пустой, то метод FirstOrDefault вернёт значение по умолчанию - null в нашем случае. Дальнейшее обращение к свойству LoadedAssembly по нулевой ссылке приведёт к возникновению исключения типа NullReferenceException. Соответственно, чтобы избежать подобной ситуации следует использовать null-условный оператор:

bool LoadBaml(AvalonEditTextOutput output, CancellationToken cancellationToken){  var asm = this.Ancestors().OfType<assemblytreenode>()                            .FirstOrDefault()?.LoadedAssembly;     // <=  ....  return true;}

Можно предположить, что разработчик уверен в том, что в данном конкретном месте метод FirstOrDefault никогда не вернёт null. Если подобная ситуация имеет место быть, то тогда лучше воспользоваться методом First вместо FirstOrDefault, так как он подчёркивает нашу уверенность в том, что мы всегда достанем нужный элемент из коллекции. Тем более, если элемент не будет найден в коллекции, то мы получим исключение типа InvalidOperationException (а не NullReferenceException как в случае с использованием FirstOrDefault с последующим обращением к свойству по нулевой ссылке) с понятным сообщением: "Sequence contains no elements".

Небезопасное сканирование

V3105 The 'm' variable was used after it was assigned through null-conditional operator. NullReferenceException is possible. ILSpy MethodVirtualUsedByAnalyzer.cs 137

static bool ScanMethodBody(IMethod analyzedMethod,                            IMethod method, MethodBodyBlock methodBody){  ....  var mainModule = (MetadataModule)method.ParentModule;  ....  switch (member.Kind)  {    case HandleKind.MethodDefinition:    case HandleKind.MethodSpecification:    case HandleKind.MemberReference:      var m = (mainModule.ResolveEntity(member, genericContext) as IMember)              ?.MemberDefinition;      if (   m.MetadataToken == analyzedMethod.MetadataToken               // <=          && m.ParentModule.PEFile == analyzedMethod.ParentModule.PEFile)  // &lt;=      {        return true;      }      break;  }  ....}

При инициализации переменной m использовался null-условный оператор, следовательно, предполагается, что m потенциально может иметь значение null. Интересно, что сразу на следующей строке мы уже без использования null-условного оператора обращаемся к свойствам переменной m, что чревато возникновением исключения типа NullReferenceException. Как и в некоторых других примерах из данной статьи исправляем ситуацию с помощью использования null-условного оператора:

static bool ScanMethodBody(IMethod analyzedMethod,                            IMethod method, MethodBodyBlock methodBody){  ....  var mainModule = (MetadataModule)method.ParentModule;  ....  switch (member.Kind)  {    case HandleKind.MethodDefinition:    case HandleKind.MethodSpecification:    case HandleKind.MemberReference:      var m = (mainModule.ResolveEntity(member, genericContext) as IMember)              ?.MemberDefinition;      if (   m?.MetadataToken == analyzedMethod.MetadataToken          && m?.ParentModule.PEFile == analyzedMethod.ParentModule.PEFile)      {        return true;      }      break;  }  ....}

Старые знакомые

V3070 Uninitialized variable 'schema' is used when initializing the 'ResourceSchema' variable. ICSharpCode.Decompiler ResXResourceWriter.cs 63

class ResXResourceWriter : IDisposable{  ....  public static readonly string ResourceSchema = schema;  ....  static string schema = ....;  ....}

Изначально я не планировал выписывать это предупреждения. Дело в том, что абсолютно идентичная ошибка уже была найдена пять лет назад в результате проверки проекта Mono, но после небольшого обсуждения с коллегой мы пришли к выводу, что упомянуть её в статье всё-таки стоит. Как и описано в статье про проверку Mono, на момент инициализации статического поля ResourceSchema другим статическим полем schema, поле schema ещё само не инициализировано и имеет значение по умолчанию - null. Файл ResXResourceWriter.cs, в котором была найдена ошибка, был любезно позаимствован с сохранением авторских прав из проекта Mono. Файл был расширен некоторым уникальным для проекта ILSpy функционалом. Вот так баги из одно проекта расползаются по сети и кочуют из одного проекта в другой. Кстати, в первоисточнике ошибку до сих пор не поправили.

Заключение

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ilya Gainulin. A Spy Undercover: PVS-Studio to Check ILSpy Source Code.

Подробнее..

Исследование COVID-19 и неинициализированная переменная

05.02.2021 14:16:53 | Автор: admin

0796_covid_sim_ru/image1.png
Существует открытый проект COVID-19 CovidSim Model, написанный на языке C++. Существует статический анализатор кода PVS-Studio, который умеет хорошо находить ошибки. Однажды они встретились. Познайте хрупкость алгоритмов математического моделирования и почему нужно прикладывать максимум усилий к качеству программного кода.


На днях мне понадобилось кое-что найти на GitHub, что является началом этой маленькой истории. Изучая результаты поиска, я случайно набрёл на проект COVID-19 CovidSim Model. Недолго думая, я решил проверить его с помощью анализатора PVS-Studio.


Проект оказался совсем крошечным. В нём всего 13 000 строк кода, если не считать пустые строки и комментарии. И ошибок там тоже почти нет. Но одна ошибка настолько проста и красива, что я не могу пройти мимо!


void CalcLikelihood(int run, std::string const& DataFile,                    std::string const& OutFileBase){  ....  double m = Data[row][col]; // numerator  double N = Data[row][col + 1]; // denominator  double ModelValue;  // loop over all days of infection up to day of sample  for (int k = offset; k < day; k++)  {    // add P1 to P2 to prevent degeneracy    double prob_seroconvert = P.SeroConvMaxSens *      (1.0 - 0.5 * ((exp(-((double)(_I64(day) - k)) * P.SeroConvP1) + 1.0) *      exp(-((double)(_I64(day) - k)) * P.SeroConvP2)));    ModelValue += c * TimeSeries[k - offset].incI * prob_seroconvert;  }  ModelValue += c * TimeSeries[day - offset].S * (1.0 - P.SeroConvSpec);  ModelValue /= ((double)P.PopSize);  // subtract saturated likelihood  LL += m * log((ModelValue + 1e-20) / (m / N + 1e-20)) +        (N - m) * log((1.0 - ModelValue + 1e-20) / (1.0 - m / N + 1e-20));  ....}

Серьёзный научный код. Что-то считается. Формулы. Выглядит всё умно и обстоятельно.


Вот только все эти вычисления разбиваются о человеческую невнимательность. Хорошо, что на помощь может прийти анализатор кода PVS-Studio и указать на баг: V614 [CWE-457] Uninitialized variable 'ModelValue' used. CovidSim.cpp 5412


И действительно, посмотрим внимательнее на это:


double ModelValue;for (int k = offset; k < day; k++){  double prob_seroconvert = ....;  ModelValue += c * TimeSeries[k - offset].incI * prob_seroconvert;}

Перед нами простая и одновременно страшная ошибка: неинициализированная переменная. Этот алгоритм может насчитать всё что угодно.


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


Это уже не первая наша статья на эту тему:



Используйте статический анализатор кода PVS-Studio! Польза от своевременно найденных ошибок может быть колоссальной. Спасибо за внимание.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. COVID-19 Research and Uninitialized Variable.

Подробнее..

Зачем PVS-Studio использует анализ потока данных по мотивам интересной ошибки в Open Asset Import Library

18.02.2021 18:15:13 | Автор: admin

PVS-Studio - Анализ потока данных
Анализ потока данных является неотъемлемой частью любого современного статического анализатора кода. Однако, со стороны, не очень понятно, что это и главное зачем нужно. До сих пор некоторые ассоциируют статический анализ с поиском чего-то в коде по определённому шаблону. Поэтому время от времени мы пишем заметки, в которых демонстрируем, как та или иная технология, используемая в анализаторе PVS-Studio, помогает выявить очередную интересную ошибку. Сегодня как раз такая статья, в которой мы рассмотрим баг в одной из реализаций стандарта кодирования двоичных данных Base64.


Всё началось с проверки свежей версии библиотеки Qt 6. Про это была отдельная классическая статья, где я описал 77 найденных ошибок. Так получилось, что вначале я решил бегло полистать отчёт, ещё не пряча предупреждения, относящиеся к сторонним библиотекам. Другими словами, я не отключил в настройках предупреждения, относящиеся к \src\3rdparty. И так вышло, что я сразу наткнулся на интересный пример ошибки в библиотеки Open Asset Import Library, про которую я решил сделать эту отдельную маленькую заметку.


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


Теперь перейдём, собственно, к ошибке, обнаруженной в Open Asset Import Library (assimp). Файл: \src\3rdparty\assimp\src\code\FBX\FBXUtil.cpp.


std::string EncodeBase64(const char* data, size_t length){    // calculate extra bytes needed to get a multiple of 3    size_t extraBytes = 3 - length % 3;    // number of base64 bytes    size_t encodedBytes = 4 * (length + extraBytes) / 3;    std::string encoded_string(encodedBytes, '=');    // read blocks of 3 bytes    for (size_t ib3 = 0; ib3 < length / 3; ib3++)    {        const size_t iByte = ib3 * 3;        const size_t iEncodedByte = ib3 * 4;        const char* currData = &data[iByte];        EncodeByteBlock(currData, encoded_string, iEncodedByte);    }    // if size of data is not a multiple of 3,    // also encode the final bytes (and add zeros where needed)    if (extraBytes > 0)    {        char finalBytes[4] = { 0,0,0,0 };        memcpy(&finalBytes[0], &data[length - length % 3], length % 3);        const size_t iEncodedByte = encodedBytes - 4;        EncodeByteBlock(&finalBytes[0], encoded_string, iEncodedByte);        // add '=' at the end        for (size_t i = 0; i < 4 * extraBytes / 3; i++)            encoded_string[encodedBytes - i - 1] = '=';    }    return encoded_string;}

Если хотите, то для начала можете попробовать обнаружить ошибку самостоятельно. А чтобы вы случайно сразу не прочитали ответ, приведу пока список некоторых других интересных статей и кратко расскажу, что такое Base64 :). Список дополнительных статей на близкую тематику:


  1. 31 февраля;
  2. Использование машинного обучения в статическом анализе исходного кода программ;
  3. Как внедрить статический анализатор кода в legacy проект и не демотивировать команду.

Ok, продолжим. Перед нами реализация алгоритма кодирования строки байт в кодировку Base64. Это стандарт кодирования двоичных данных при помощи только 64 символов. Алфавит кодирования содержит текстово-цифровые латинские символы A-Z, a-z и 0-9 (62 знака) и 2 дополнительных символа, зависящих от системы реализации. Каждые 3 исходных байта кодируются 4 символами.


Если осталось закодировать только один или два байта, то в результате получаются только первые два или три символа строки, а выходная строка дополняется двумя или одним символами "=". Это предотвращает добавление дополнительных битов к восстановленным данным. Вот этот момент как раз реализован в рассматриваемой функции неправильно.


Если вы нашли ошибку, вы молодец. Если нет, то это тоже нормально. Нужно вникать в код, чтобы заметить, что что-то идёт не так. Анализатор про это "что-то не то" сообщает предупреждением: V547 [CWE-571] Expression 'extraBytes > 0' is always true. FBXUtil.cpp 224


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


// calculate extra bytes needed to get a multiple of 3size_t extraBytes = 3 - length % 3;

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


size_t extraBytes = length % 3;

Тогда, если обрабатывается, например, 5 байт, то получаем 5 % 3 = 2, и нужно дополнительно обработать 2 байта. Если на вход поступило 6 байт, то ничего отдельно обрабатывать не нужно, так как 6 % 3 = 0.


Но программист перемудрил и написал бессмысленный код:


size_t extraBytes = 3 - length % 3;

И как раз при анализе этого кода анализатору и понадобился механизм анализа потока данных. Какое бы значение не находилось в переменной length, после деления по модулю будет получено значение в диапазоне [0..2]. Анализатор PVS-Studio умеет работать с диапазонами, точными значениями и множествами. Т. е. речь идёт про Value Range Analysis. В данном случае будет использован именно диапазон значений.


Продолжим вычисления:


size_t extraBytes = 3 - [0..2];

Получается, что переменная extraBytes никогда не будет равна нулю. Анализатор вычислит следующий возможный диапазон её значений: [1..3].


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


if (extraBytes > 0)

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


Конечно, некорректность работы функции не ограничивается тем, что будет выполнять фрагмент кода, который выполняться не должен. Там вообще всё идёт вкривь и вкось. Предположим, что требуется закодировать 6 символов. В этом случае выходная строка должна содержать 8 символов. Давайте быстренько прикинем, как поведёт себя рассмотренная функция.


// calculate extra bytes needed to get a multiple of 3size_t extraBytes = 3 - length % 3; // 3-6%3 = 3// number of base64 bytessize_t encodedBytes = 4 * (length + extraBytes) / 3; // 4*(6+3)/3 = 12std::string encoded_string(encodedBytes, '=');

Уже получилось, что выходная строка будет содержать 12 символов, а не 8. Дальше тоже всё будет работать неправильно даже нет смысла вдаваться в подробности.


Вот так легко и просто статический анализ нашёл ошибку в коде. А представьте, ведь кто-то будет мучиться и отлаживаться, чтобы понять, почему неправильно происходит кодирование символов в кодировку Base64. Здесь мы, кстати, подходим к вопросу качества используемых сторонних библиотек, который я рассматривал в публикации "Почему важно проводить статический анализ открытых библиотек, которые вы добавляете в свой проект".


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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why PVS-Studio Uses Data Flow Analysis: Based on Gripping Error in Open Asset Import Library.

Подробнее..

Подводные камни в бассейне строк, или ещё один повод подумать перед интернированием экземпляров класса String в C

06.04.2021 16:12:32 | Автор: admin

Будучи разработчиками программного обеспечения, мы всегда хотим, чтобы написанное нами ПО работало быстро. Использование оптимального алгоритма, распараллеливание, применение различных техник оптимизации мы будем прибегать ко всем известным нам средствам, дабы улучшить производительность софта. К одной из таких техник оптимизации можно отнести и так называемое интернирование строк. Оно позволяет уменьшить объём потребляемой процессом памяти, а также значительно сокращает время, затрачиваемое на сравнение строк. Однако, как и везде в жизни, необходимо соблюдать меру не стоит использовать интернирование на каждом шагу. Далее в этой статье будет показано, как можно обжечься и создать своему приложению неочевидный bottleneck в виде метода String.Intern.

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

Есть несколько версий формулы для того, чтобы посчитать, сколько байт занимает строковый объект на куче: вариант от Джона Скита и вариант, показанный в статье Тимура Гуева. На картинке выше я использовал второй вариант. Даже если эта формула верна не на 100 %, мы всё равно можем приблизительно прикинуть размер строковых объектов. К примеру, для того чтобы занять 1 Гб оперативной памяти, достаточно будет, чтобы в памяти процесса было около 4,7 миллионов строк (каждая длиной в 100 символов). Если мы полагаем, что среди строк, с которыми работает наша программа, есть большое количество дубликатов, как раз и стоит воспользоваться встроенным во фреймворк функционалом интернирования. Давайте сейчас быстро вспомним, что вообще такое интернирование строк.

Интернирование строк

Идея интернирования строк состоит в том, чтобы хранить в памяти только один экземпляр типа String для идентичных строк. При старте нашего приложения виртуальная машина создаёт внутреннюю хэш-таблицу, которая называется таблицей интернирования (иногда можно встретить название String Pool). Эта таблица хранит ссылки на каждый уникальный строковый литерал, объявленный в программе. Кроме того, используя два метода, описанных ниже, мы сами можем получать и добавлять ссылки на строковые объекты в эту таблицу. Если наше приложение работает с большим количеством строк, среди которых часто встречаются одинаковые, то нет смысла каждый раз создавать новый экземпляр класса String. Вместо этого можно просто ссылаться на уже созданный на куче экземпляр типа String, получая ссылку на него путём обращения к таблице интернирования. Виртуальная машина сама интернирует все строковые литералы, встреченные в коде (более подробно про многие хитрости интернирования можно прочитать в этой статье). А нам для работы предоставляются два метода: String.Intern и String.IsInterned.

Первый на вход принимает строку и, если идентичная строка уже имеется в таблице интернирования, возвращает ссылку на уже имеющийся на куче объект типа String. Если же такой строки ещё нет в таблице, то ссылка на этот строковый объект добавляется в таблицу интернирования и затем возвращается из метода. Метод IsInterned также на вход принимает строку и возвращает ссылку из таблицы интернирования на уже имеющийся объект. Если такого объекта нет, то возвращается null (про неинтуитивно возвращаемое значение этого метода не упомянул только ленивый).

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

Интернирование строк может дать прирост производительности при сравнении этих самых строк. Взглянем на реализацию метода String.Equals:

public bool Equals(String value){  if (this == null)    throw new NullReferenceException();   if (value == null)    return false;   if (Object.ReferenceEquals(this, value))    return true;    if (this.Length != value.Length)    return false;   return EqualsHelper(this, value);}

До вызова метода EqualsHelper, где производится посимвольное сравнение строк, метод Object.ReferenceEquals выполняет проверку на равенство ссылок. Если строки интернированы, то уже на этапе проверки ссылок на равенство метод Object.ReferenceEquals вернёт значение true при условии равенства строк (без необходимости сравнения самих строк посимвольно). Конечно, если ссылки не равны, то в итоге произойдёт вызов метода EqualsHelper и последующее посимвольное сравнение. Ведь методу Equals неизвестно, что мы работаем с интернированными строками, и возвращаемое значение false методом ReferenceEquals уже свидетельствует о различии сравниваемых строк.

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

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

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

Краткая история того, как всё начиналось

В корпоративном bug tracker'е уже какое-то время значилась задача, в которой требовалось провести исследование: как распараллеливание процесса анализа С++ кода может сократить продолжительность анализа. Хотелось, чтобы анализатор PVS-Studio параллельно работал на нескольких машинах при анализе одного проекта. В качестве программного обеспечения, позволяющего проводить подобное распараллеливание, был выбран IncrediBuild. IncrediBuild позволяет параллельно запускать на машинах, находящихся в одной сети, различные процессы. Например, такой процесс, как компиляция исходных файлов, можно распараллелить на разные машины, доступные в компании (или в облаке), и таким образом добиться существенного сокращения времени сборки проекта. Данное программное обеспечение является довольно популярным решением среди разработчиков игр.

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

Был выбран open source проект Unreal Tournament. Удалось успешно уговорить программистов посодействовать в решении задачи и установить IncrediBuild на их машины. Полученный объединённый кластер включал около 145 ядер.

Так как я анализировал проект Unreal Tournament с помощью применения системы мониторинга компиляции в PVS-Studio, то мой сценарий работы был следующим. Я запускал программу CLMonitor.exe в режиме монитора, выполнял полную сборку проекта Unreal Tournament в Visual Studio. Затем, после прохождения сборки, опять запускал CLMonitor.exe, но уже в режиме запуска анализа. В зависимости от указанного в настройках PVS-Studio значения для параметра ThreadCount, CLMonitor.exe параллельно одновременно запускает соответствующее количество дочерних процессов PVS-Studio.exe, которые и занимаются анализом каждого отдельного исходного С++ файла. Один дочерний процесс PVS-Studio.exe анализирует один исходный файл, а после завершения анализа передаёт полученные результаты обратно в CLMonitor.exe.

Всё просто: выставил параметр ThreadCount в настройках PVS-Studio, равный количеству доступных ядер (145), запустил анализ и сел в ожидании того, что сейчас увижу, как 145 процессов PVS-Studio.exe будут исполняться параллельно на удалённых машинах. У приложения IncrediBuild есть удобная система мониторинга распараллеливания Build Monitor, через которую можно наблюдать, как процессы запускаются на удалённых машинах. Что-то подобное я и наблюдал в процессе анализа:

Казалось, что нет ничего проще: сиди и смотри, как проходит анализ, а после просто зафиксируй время его проведения с использованием IncrediBuild и без. Реальность оказалась несколько интересней...

Обнаружение проблемы, её локализация и устранение

Пока проходил анализ, можно было переключиться на выполнение других задач или просто медитативно поглядывать на бегущие полосы процессов PVS-Studio.exe в окне Build Monitor. После завершения анализа с использованием IncrediBuild я сравнил временные показатели продолжительности анализа с результатами замеров без применения IncrediBuild. Разница ощущалась, но общий результат, как мне показалось, мог бы быть и лучше: 182 минуты на одной машине с 8 потоками и 50 минут с использованием IncrediBuild и 145 потоками. Получалось, что количество потоков возросло в 18 раз, но при этом время анализа уменьшилось всего в 3,5 раза. Напоследок я опять решил взглянуть уже на итоговый результат окна Build Monitor. Прокручивая нижний ползунок, я заметил следующие аномалии на графике:

Было видно, что процессы PVS-Studio.exe запускались и успешно завершались, а затем по какой-то причине происходила некая пауза между запуском следующей партии процессов PVS-Studio.exe. И так повторялось несколько раз. Пауза за паузой. И эти простои между запусками складывались в ощутимую задержку и вносили свою лепту в общее время анализа. Первое, что пришло в голову, это вина IncrediBuild. Наверняка он выполняет какую-то свою внутреннюю синхронизацию между процессами и тормозит их запуск.

Я показал эти аномалии на графиках своему старшему коллеге. Он, в отличие от меня, не был так поспешен в выводах. Он предложил посмотреть на то, что происходит внутри нашего приложения CLMonitor.exe в момент того, как на графике начинают появляться видимые простои. Запустив анализ повторно и дождавшись первого очевидного "провала" на графике, я подключился к процессу CLMonitor.exe с помощью отладчика Visual Studio и поставил его на паузу. Открыв вкладку Threads, мы с коллегой увидели около 145 приостановленных потоков. Перемещаясь по местам в коде, где остановили своё исполнение данные потоки, мы увидели строки кода подобного содержания:

....return String.Intern(settings == null ? path                                 : settings                                 .TransformToRelative(path.Replace("/", "\\"),                                                      solutionDirectory));....analyzedSourceFiles.Add( String.Intern(settings                        .TransformPathToRelative(analyzedSourceFilePath,                                                  solutionDirectory))                       );....

Что общего мы видим в этих строках кода? В каждой из них используется метод String.Intern. Причём его применение кажется оправданным. Дело в том, что это те места, где CLMonitor.exe обрабатывает данные, приходящие от процессов PVS-Studio.exe. Обработка происходит путём записи данных в объекты типа ErrorInfo, инкапсулирующего в себе информацию о найденной анализатором потенциальной ошибке в проверяемом коде. Интернируем мы тоже вполне разумные вещи, а именно пути до исходных файлов. Один исходный файл может содержать множество ошибок, поэтому нет смысла, чтобы объекты типа ErrorInfo содержали в себе разные строковые объекты с одинаковым содержимым. Логично просто ссылаться на один объект из кучи.

После недолгих раздумий стало понятно, что интернирование здесь применено в очень неподходящий момент. Итак, вот какую ситуацию мы наблюдали в отладчике. Пока по какой-то причине 145 потоков висели на выполнении метода String.Intern, кастомный планировщик задач LimitedConcurrencyLevelTaskScheduler внутри CLMonitor.exe не мог запустить новый поток, который в дальнейшем бы породил новый процесс PVS-Studio.exe, а IncrediBuild уже запустил бы этот процесс на удалённой машине. Ведь, с точки зрения планировщика, поток ещё не завершил своё исполнение он выполняет преобразование полученных данных от PVS-Studio.exe в ErrorInfo с последующим интернированием. Ему всё равно, что сам процесс PVS-Studio.exe уже давно завершился и удалённые машины попросту простаивают без дела. Поток ещё активен, и установленный нами лимит в 145 потоков не даёт планировщику породить новый.

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

Убирать интернирование совсем нам не хотелось, так как это привело бы к увеличению объёма потребляемой оперативной памяти процессом CLMonitor.exe. В конечном счёте было найдено довольно простое и элегантное решение: перенести интернирование из потока, выполняющего запуск процесса PVS-Studio.exe в чуть более позднее место исполнения кода (в поток, который занимается непосредственно формированием отчёта об ошибках).

Как сказал мой коллега, обошлись хирургической правкой буквально двух строк и решили проблему с простаиванием удалённых машин. При повторных запусках анализа окно Build Monitor уже не показывало каких-либо значительных промежутков времени между запусками процессов PVS-Studio.exe. А время проведения анализа снизилось с 50 минут до 26, то есть почти в два раза. Теперь если смотреть на общий результат, который мы получили при использовании IncrediBuild и 145 доступных ядер, то общее время анализа уменьшилось в 7 раз. Это впечатляло несколько больше, нежели цифра в 3,5 раза.

String.Intern почему так медленно, или изучаем код CoreCLR

Стоит отметить, что как только мы увидели зависания потоков на местах вызова метода String.Intern, то почти сразу подумали, что под капотом у этого метода присутствует критическая секция с каким-нибудь lock'ом. Раз каждый поток может писать в таблицу интернирования, то внутри метода String.Intern должен быть какой-нибудь механизм синхронизации, чтобы сразу несколько потоков, выполняющих запись в таблицу, не перезаписали данные друг друга. Хотелось подтвердить свои предположения, и мы решили посмотреть имплементацию метода String.Intern на ресурсе reference source. Там мы увидели, что внутри нашего метода интернирования есть вызов другого метода *Thread.GetDomain().GetOrInternString(str)*. Что ж, давайте посмотрим его реализацию:

internal extern String GetOrInternString(String str);

Уже интересней. Этот метод импортируется из какой-то другой сборки. Из какой? Так как интернированием строк всё-таки занимается сама виртуальная машина CLR, то мой коллега направил меня прямиком в репозиторий среды исполнения .NET. Выкачав репозиторий, мы отправились к солюшену с названием CoreCLR. Открыв его и выполнив поиск по всему решению, мы нашли метод GetOrInternString с подходящей сигнатурой:

STRINGREF *BaseDomain::GetOrInternString(STRINGREF *pString)

В нём увидели вызов метода GetInternedString. Перейдя в тело этого метода, заметили код следующего вида:

....if (m_StringToEntryHashTable->GetValue(&StringData, &Data, dwHash)){  STRINGREF *pStrObj = NULL;  pStrObj = ((StringLiteralEntry*)Data)->GetStringObject();  _ASSERTE(!bAddIfNotFound || pStrObj);  return pStrObj;}else{  CrstHolder gch(&(SystemDomain::GetGlobalStringLiteralMap()                                   ->m_HashTableCrstGlobal));  ....  // Make sure some other thread has not already added it.  if (!m_StringToEntryHashTable->GetValue(&StringData, &Data))  {    // Insert the handle to the string into the hash table.    m_StringToEntryHashTable->InsertValue(&StringData, (LPVOID)pEntry, FALSE);  }  ....}....

Поток исполнения попадает в ветку else только в том случае, если метод, занимающийся поиском ссылки на объект типа *String *(метод GetValue) в таблице интернирования, вернёт false. Перейдём к рассмотрению кода, который представлен в ветке else. Интерес тут вызывает строка создания объекта типа CrstHolder с именем gch. Переходим в конструктор CrstHolder и видим код следующего вида:

inline CrstHolder(CrstBase * pCrst)    : m_pCrst(pCrst){    WRAPPER_NO_CONTRACT;    AcquireLock(pCrst);}

Видим вызов метода AcquireLock. Уже хорошо. Код метода AcquireLock:

DEBUG_NOINLINE static void AcquireLock(CrstBase *c){  WRAPPER_NO_CONTRACT;  ANNOTATION_SPECIAL_HOLDER_CALLER_NEEDS_DYNAMIC_CONTRACT;  c->Enter();}

Вот, собственно, мы и видим точку входа в критическую секцию вызов метода Enter. Оставшиеся сомнения в том, что это именно тот метод, который занимается блокировкой, у меня пропали после того, когда я прочитал оставленный к этому методу комментарий: "Acquire the lock". В дальнейшем погружении в код CoreCLR я не видел особого смысла. Получается, версия с тем, что при занесении новой записи в таблицу интернирования поток заходит в критическую секцию, вынуждая все другие потоки ожидать, когда блокировка спадёт, подтвердилась. Создание объекта типа CrstHolder, а следовательно, и заход в критическую секцию происходят сразу перед вызовом метода m_StringToEntryHashTable->InsertValue.

Блокировка пропадает сразу после того, как мы выходим из ветки else. Так как в этом случае для объекта gch вызывается его деструктор, который и вызывает метод ReleaseLock:

inline ~CrstHolder(){  WRAPPER_NO_CONTRACT;  ReleaseLock(m_pCrst);}

Когда потоков немного, то и простой в работе может быть небольшим. Но когда их количество возрастает, например до 145 (как в случае использования IncrediBuild), то каждый поток, пытающийся добавить новую запись в таблицу интернирования, временно блокирует остальные 144 потока, также пытающихся внести в неё новую запись. Результаты этих блокировок мы и наблюдали в окне Build Monitor.

Заключение

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

Благодарю за прочтение.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ilya Gainulin. Pitfalls in String Pool, or Another Reason to Think Twice Before Interning Instances of String Class in C#.

Подробнее..

Ядро macOS, есть ли червячки в этом яблоке?

29.03.2021 12:16:08 | Автор: admin

0818_XNU_MacOS_Kernel_ru/image1.png


В самом начале этого года Apple выложили в открытый доступ исходный код системных компонентов macOS 11.0 Big Sur, включая XNU ядро операционной системы macOS. Пару лет назад исходный код ядра уже проверялся PVS-Studio в связи с выходом анализатора для macOS. Прошло достаточно много времени, и вышел новый релиз исходного кода ядра. Почему бы и не провести повторную проверку.


Что это за проект, Apple и open-source?


XNU X is Not Unix используется и разрабатывается Apple в качестве ядра операционных систем OS X. Исходные коды этого ядра 20 лет назад были опубликованы под лицензией APSL (Apple Public Source License) вместе с OC Darwin. Раньше Darwin можно было даже установить в качестве полноценной операционной системы, однако теперь это стало невозможно. Причиной публикации исходного кода является тот факт, что он во многом основан на других open-source проектах.


Исходные коды компонентов можно найти тут. Для проверки я использовала зеркало проекта на GitHub.


Предыдущая проверка


Как я уже упомянула, этот проект ранее проверялся нами с помощью PVS-Studio. С предыдущими результатами можно познакомиться в статье: "Релиз PVS-Studio для macOS: 64 weaknesses в Apple XNU Kernel". После публикации мой коллега Святослав также отправил статью разработчикам на почту, но ответа не получил. Так что я предполагаю, что наша проверка никак не связана с исправлениями, которые мы дальше рассмотрим. Разработчикам пришлось искать их другим путём. А могли бы просто взять и запустить PVS-Studio :). Сейчас, после публикации статей, мы в основном пишем об этом в GitHub репозиторий проекта.


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


Я приведу здесь несколько примеров исправлений. Но, чтобы не раздувать объём статьи, не буду полностью приводить объяснение ошибок. Если из исправления будет неясно, в чём была проблема, то вы всегда можете обратиться к первой статье по проверке этого проекта. Я не буду разбирать все исправленные фрагменты, большинство из фрагментов всё-таки было поправлено. А фрагментов в предыдущей статье было ни много ни мало 64!


Перейдём к рассмотрению исправлений примеров из прошлой статьи.


Фрагмент N1, в котором член класса сравнивался сам с собой:


intkey_parse(      struct mbuf *m,      struct socket *so){  ....  if ((m->m_flags & M_PKTHDR) == 0 ||      m->m_pkthdr.len != m->m_pkthdr.len) {    ....    goto senderror;  }  ....}

Был исправлен следующим образом:


0818_XNU_MacOS_Kernel_ru/image2.png


Где макрос, из которого получена переменная orglen, выглядит следующим образом:


#define PFKEY_UNUNIT64(a) ((a) << 3)

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


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


0818_XNU_MacOS_Kernel_ru/image3.png


Накосячить в условии assertf одно, но ещё и перезаписать переменную для отладочной версии такое точно стоит поправить.


Фрагменты 6 и 7 были исправлены одинаково. Оказалось, что во вложенной проверке перепутали значение перечислителя для сравнения. Вместо PBUF_TYPE_MBUF во внутренней проверке должен быть элемент PBUF_TYPE_MEMORY в обоих случаях.


0818_XNU_MacOS_Kernel_ru/image4.png


В случае фрагментов N8, 9, 10 исправление было таким:


0818_XNU_MacOS_Kernel_ru/image5.png


На это исправление я обратила внимание, так как серьёзная часть коммита в целом (обновление репозитория до xnu-4903.270.47 от 11 января) содержит помимо прочего много правок код-стайла. Это может указывать на то, что для данной версии кодовая база была подчищена с помощью разных инструментов качества кода. Что сделает эту проверку PVS-Studio более интересной. Ведь видно, что качество кодовой базы уже было улучшено другими инструментами.


Что касается фрагментов 11, 12, 13, 14 был исправлен только фрагмент 11:


0818_XNU_MacOS_Kernel_ru/image6.png


Остальные остались прежними. Похоже, кто-то невнимательно прочитал наш отчёт ;) (или отчёт анализатора, использованный для улучшения качества кода в этом коммите). Приведу здесь код, на который было выдано одно из предупреждений, чтобы не было сомнений в аналогичности ошибки:


static intkauth_resolver_getwork(user_addr_t message){  struct kauth_resolver_work *workp;  int error;  KAUTH_RESOLVER_LOCK();  error = 0;  while ((workp = TAILQ_FIRST(....)) == NULL) { // <=    thread_t thread = current_thread();    struct uthread *ut = get_bsdthread_info(thread);    ut->uu_save.uus_kauth.message = message;    error = msleep0(....);    KAUTH_RESOLVER_UNLOCK();    /*     * If this is a wakeup from another thread in the resolver     * deregistering it, error out the request-for-work thread     */    if (!kauth_resolver_identity) {      printf("external resolver died");      error = KAUTH_RESOLVER_FAILED_ERRCODE;    }    return error; //<=  }  return kauth_resolver_getwork2(message);}

Предупреждение PVS-Studio: V612 An unconditional 'return' within a loop. kern_credential.c 951


Я привела код почти целиком, чтобы можно было сформировать общее представление о том, что происходит в этой функции. В случае отмеченного цикла при выполнении условия входа в него будет совершён один проход по телу цикла, завершающийся возвращением error. Видимо, подразумевалось, что если выполняется условие (workp = TAILQ_FIRST(....)) == NULL, то нужно найти причину ошибки и завершить функцию возвращением информации об ошибке. Однако по какой-то причине вместо if был написан while, как и во фрагменте из предыдущей статьи. Строчка error = msleep0(....) выглядит в коде таким образом:


error = msleep0(&kauth_resolver_unsubmitted,                kauth_resolver_mtx,                PCATCH,                "GRGetWork",                0,                 kauth_resolver_getwork_continue);

Здесь последним аргументом передаётся указатель на функцию kauth_resolver_getwork_continue. В теле этой функции есть условие, аналогичное условию цикла, на который нам указал анализатор. Но в нём уже корректно используется if, а не while.


static intkauth_resolver_getwork_continue(int result){  ....  if (TAILQ_FIRST(&kauth_resolver_unsubmitted) == NULL) {    ....    return error;  }  ....}

В принципе этот код работает немного сложнее, чем я описала. В нём присутствует рекурсия (в методе kauth_resolver_getwork_continue), и, как я поняла, он нацелен на нахождение потоков, которые можно перезагрузить. Но я не стала вдаваться в подробности, так как while всё равно лишний. Возможно, он остался здесь с того времени, когда исходный код выполнял ту же задачу, но без использования рекурсии.


Это примеры из начала статьи. Проскочим в середину и возьмём фрагмент N40. В нём одному и тому же элементу дважды присваивается одно значение:


Предупреждение PVS-Studio: V519 CWE-563 The 'wrap.Seal_Alg[0]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 2070, 2071. gss_krb5_mech.c 2071


Эта ошибка, конечно же, тоже была поправлена:


0818_XNU_MacOS_Kernel_ru/image7.png


Ну и ближе к концу статьи, фрагмент 62 был исправлен так, как и было предложено в предыдущей статье. Причём это было единственной правкой в том файле.


0818_XNU_MacOS_Kernel_ru/image8.png


Фрагменты 63 и 64 также были исправлены, но там код был изменён капитально. Поэтому понять, какое исправление было именно для рассмотренного предупреждения, сложно.


Новые находки


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


К этой проверке cloc насчитал в проекте 1346 *.c файлов, 1822 С/C++ хэдера и 225 *.cpp файлов.


Ну и перейдём к разбору интересных находок.


Фрагмент N1


voidpe_identify_machine(__unused boot_args *args){  ....  // Start with default values.  gPEClockFrequencyInfo.timebase_frequency_hz = 1000000000;  gPEClockFrequencyInfo.bus_frequency_hz      =  100000000;  ....  gPEClockFrequencyInfo.dec_clock_rate_hz =     gPEClockFrequencyInfo.timebase_frequency_hz;  gPEClockFrequencyInfo.bus_clock_rate_hz =   gPEClockFrequencyInfo.bus_frequency_hz;  ....   gPEClockFrequencyInfo.bus_to_dec_rate_den =    gPEClockFrequencyInfo.bus_clock_rate_hz /    gPEClockFrequencyInfo.dec_clock_rate_hz;}

Предупреждение PVS-Studio: V1064 The 'gPEClockFrequencyInfo.bus_clock_rate_hz' operand of integer division is less than the 'gPEClockFrequencyInfo.dec_clock_rate_hz' one. The result will always be zero. pe_identify_machine.c 72


Все используемые здесь поля имеют целочисленный тип:


extern clock_frequency_info_t gPEClockFrequencyInfo;struct clock_frequency_info_t {  unsigned long bus_clock_rate_hz;  unsigned long dec_clock_rate_hz;  unsigned long bus_to_dec_rate_den;  unsigned long long bus_frequency_hz;  unsigned long timebase_frequency_hz;  ....};

Через промежуточные присвоения полю gPEClockFrequencyInfo.bus_clock_rate_hz, являющемуся делимым, присваивается значение 100000000, а полю-делителю gPEClockFrequencyInfo.dec_clock_rate_hz присваивается значение 1000000000. Делитель в этом случае в десять раз больше делимого. Так как все поля здесь являются целочисленными, поле gPEClockFrequencyInfo.bus_to_dec_rate_den окажется равным 0.


Судя по наименованию результирующего поля bus_to_dec_rate_den, делитель и делимое были перепутаны местами. Я допускаю возможность, что код был написан с расчётом на то, что исходные значения изменятся и результат уже не будет равен 0. Но этот код всё равно кажется мне очень подозрительным.


Фрагмент N2


voidsdt_early_init( void ){  ....  if (MH_MAGIC_KERNEL != _mh_execute_header.magic) {  ....  } else {    ....    for (....) {    const char *funcname;    unsigned long best;                           //<=    ....    funcname = "<unknown>";    for (i = 0; i < orig_st->nsyms; i++) {      char *jname = strings + sym[i].n_un.n_strx;      ....      if ((unsigned long)sym[i].n_value > best) { //<=        best = (unsigned long)sym[i].n_value;        funcname = jname;      }    }    .....  }}

Предупреждение PVS-Studio: V614 Uninitialized variable 'best' used. sdt.c 572


Насколько я поняла, этот метод ищет название некоей функции. В алгоритме используется переменная best, возможно, это положение наилучшего кандидата на результат. Однако изначально эта переменная только объявляется без инициализации. Следующее же использование сверяет значение некоего элемента с переменной best, которая будет неинициализированной на тот момент. Еще страннее, что она инициализируется только внутри условия, в котором используется её же значение.


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


Фрагмент N3


intcdevsw_isfree(int index){  struct cdevsw * devsw;  if (index < 0) {    if (index == -1) {      index = 0;    } else {      index = -index;     }    devsw = &cdevsw[index];    for (; index < nchrdev; index++, devsw++) {      if (memcmp(....) == 0) {        break;      }    }  }  if (index < 0 || index >= nchrdev) {    return -1;  }  ....  return index;}

Предупреждение PVS-Studio: V560 A part of conditional expression is always false: index < 0. bsd_stubs.c:236


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


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


Фрагмент N4


intnfs_vinvalbuf_internal(....){  struct nfsbuf *bp;  ....  off_t end = ....;  /* check for any dirty data before the EOF */  if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))  {    /* clip dirty range to EOF */    if (bp->nb_dirtyend > end)    {      bp->nb_dirtyend = end;      if (bp->nb_dirtyoff >= bp->nb_dirtyend)             //<=      {        bp->nb_dirtyoff = bp->nb_dirtyend = 0;      }    }    if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) //<=    {      ....    }  }  ....}

Предупреждения PVS-Studio:


  • V547 Expression 'bp->nb_dirtyoff >= bp->nb_dirtyend' is always false. nfs_bio.c 3858
  • V560 A part of conditional expression is always true: (bp->nb_dirtyoff < end). nfs_bio.c 3862

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


Начнём с первого предупреждения. Анализатор решил, что nb_dirtyoff не может быть больше или равен nb_dirtyend. Разберёмся почему. Перед подозрительной проверкой есть ещё два if с проверками (bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end) и bp->nb_dirtyend > end. А также осуществляется присвоение bp->nb_dirtyend = end.


Почему же третья проверка bp->nb_dirtyoff >= bp->nb_dirtyend будет всегда false?


0818_XNU_MacOS_Kernel_ru/image9.png


Всё просто. Из условий выходит, что nb_dirtyoff меньше, чем end, а nb_dirtyend равно end. В итоге nb_dirtyend точно больше, чем nb_dirtyoff. Присвоение bp->nb_dirtyoff = bp->nb_dirtyend = 0 никогда не будет выполнено.


В итоге вот такой участок кода:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  /* clip dirty range to EOF */  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;    if (bp->nb_dirtyoff >= bp->nb_dirtyend) {  //<=      bp->nb_dirtyoff = bp->nb_dirtyend = 0;    }  }}

Можно упростить хотя бы до такого:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;  }}

Но только если в настоящий момент этот алгоритм работает корректно.


Второе предупреждение указывает на четвёртый if, вложенный в первый.


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))

Здесь анализатор выдаёт предупреждение на основании того, что присвоение нуля никогда не будет выполнено. В итоге во внешнем условии уже была проверка bp->nb_dirtyoff < end и внутренняя проверка из-за ошибки в условии выше становится бессмысленной.


Фрагмент N5


tcp_output(struct tcpcb *tp){  ....  if (isipv6) {    ....    if (len + optlen) {      ....    }  } else {    ....    if (len + optlen) {      ....    }  }  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


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


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


if (len + optlen + ipoptlen > tp->t_maxopd) {  ....}

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


Ещё замечу, что эта функция, сокращённая тут до 16 строк, занимает в оригинале 2268 строк! Ещё один возможный повод для рефакторинга ;)


Второе предупреждение на этот же участок:


V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


Фрагмент N6


intttyinput(int c, struct tty *tp){  ....  if (tp->t_rawq.c_cc + tp->t_canq.c_cc) {  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'tp->t_rawq.c_cc + tp->t_canq.c_cc' statement is a part of the condition. Perhaps, this statement should have been compared with something else. tty.c 568


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


if (   tp->t_rawq.c_cc + tp->t_canq.c_cc > I_HIGH_WATER  3 // <=    && ....) {  ....}

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


Фрагмент N7


errno_tmbuf_adjustlen(mbuf_t m, int amount){  /* Verify m_len will be valid after adding amount */  if (amount > 0) {    int used =  (size_t)mbuf_data(m)              - (size_t)mbuf_datastart(m)              + m->m_len;    if ((size_t)(amount + used) > mbuf_maxlen(m)) {      ....    }  ....  return 0;}

Предупреждение PVS-Studio: V1028 Possible overflow. Consider casting operands of the 'amount + used' operator to the 'size_t' type, not the result. kpi_mbuf.c


Снова ошибка в условии, но уже совсем другого рода. Вместо приведения к size_t операндов сложения, чтобы результат точно поместился в числовой тип, к size_t приводится результат сложения. Если в итоге сложения возникнет переполнение, то с результатом mbuf_maxlen(m) будет сравниваться бессмысленное значение, приведённое к size_t. Раз программист всё-таки хотел защититься от переполнения, то стоит его сделать правильно:


if ((size_t)amount + used > mbuf_maxlen(m))

Таких срабатываний было несколько, стоит обратить на этот момент внимание.


  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1165
  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1131
  • V1028 Possible overflow. Consider casting operands, not the result. audit_worker.c 241
  • V1028 Possible overflow. Consider casting operands of the '((u_int32_t) slp * hz) + 999999' operator to the 'long' type, not the result. tty.c 2199

Фрагмент N8


intfdavail(proc_t p, int n){  ....  char *flags;  int i;  int lim;  ....  lim = (int)MIN(....);  if ((i = lim - fdp->fd_nfiles) > 0 && (n -= i) <= 0) //<=  {    return 1;  }  ....  for (....)  {    if (*fpp == NULL && !(*flags & UF_RESERVED) && --n <= 0)    {      return 1;    }  }  return 0;}

Предупреждение PVS-Studio: V1019 Compound assignment expression 'n -= i' is used inside condition. kern_descrip.c_99 3916


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


i = lim - fdp->fd_nfiles;if (i > 0){  n -= i;  if(n <= 0)    return 1;}

Этот код выглядит менее эффективным, но точно является более понятным. Для быстрой проверки равнозначности эффективности этого кода можно зайти на Godbolt (Compiler Explorer), где, кстати, можно тестировать работу диагностик PVS-Studio. Анализатор легко найти среди инструментов этого сервиса.


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


Но, если обратить внимание на тело этого if, новое значение n в нём не используется. То есть вполне возможно, что никакое присвоение здесь и не нужно. Тогда можно обойтись таким кодом:


i = lim - fdp->fd_nfiles;if (i > 0) {  if(n  i <= 0)    return 1;}

И, более того, исходный код может приводить к ошибке при дальнейшем использовании переменной n. Если выражение (n -= i) <= 0 окажется ложным, то далее будет использоваться уже новое значение n. Так как я не работала вплотную с исходным кодом, мне сложно сказать, какое поведение является верным.


Фрагмент N9


static errno_tvsock_put_message_listening(struct vsockpcb *pcb,                             enum vsock_operation op,                            struct vsock_address src,                             struct vsock_address dst){  switch (op)  {    case VSOCK_REQUEST:      ....      if (....)      {        vsock_pcb_safe_reset_address(pcb, dst, src);        ....      }      ....      done:        ....        break;    case VSOCK_RESET:      error = vsock_pcb_safe_reset_address(pcb, dst, src);      break;    default:      vsock_pcb_safe_reset_address(pcb, dst, src);      ....      break;  }  return error;}

Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 549


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


static errno_tvsock_pcb_safe_reset_address(struct vsockpcb *pcb,                              struct vsock_address src,                              struct vsock_address dst)

При использовании этой функции в данном фрагменте последние два аргумента с аналогичными именами передаются в другом порядке.


Срабатывания на тот же фрагмент:


  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 587
  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 590

Фрагмент N10


intifclassq_tbr_set(struct ifclassq *ifq, ....){  struct tb_regulator *tbr;  ....  tbr = &ifq->ifcq_tbr;  ....  tbr->tbr_rate = TBR_SCALE(rate / 8) / machclk_freq;  ....  tbr->tbr_last = read_machclk();  if (   tbr->tbr_rate > 0               //<=      && (ifp->if_flags & IFF_UP))  {     ....  } else {    ....  }  ....  return 0;}

Предупреждение PVS-Studio: V1051 Consider checking for misprints. It's possible that the 'tbr->tbr_last' should be checked here. classq_subr.c 685


В проекте эта диагностика работала не лучшим образом, так как в коде постоянно над телом условия или цикла инициализировались сторонние переменные с именами, похожими на используемые в условии. Поэтому на этот раз диагностика выдала несколько явно ложных предупреждений. Но рассматриваемое нами срабатывание всё же показалось мне подозрительным, так как проверяемое поле tbr_rate не использовалось в теле условия и было инициализировано на 35 строк выше этой проверки. А вот поле tbr_last, инициализированное прямо перед этой проверкой, больше нигде не используется. Можно предположить, что проверить нужно было его вместо tbr_rate.


Фрагмент N11


voidaudit_arg_mac_string(struct kaudit_record *ar, ....){  if (ar->k_ar.ar_arg_mac_string == NULL)  {    ar->k_ar.ar_arg_mac_string = kheap_alloc(....);  }  ....  if (ar->k_ar.ar_arg_mac_string == NULL)  {    if (ar->k_ar.ar_arg_mac_string == NULL) // <=    {      return;    }  }  ....}

Предупреждение PVS-Studio: V571 Recurring check. The 'if (ar->k_ar.ar_arg_mac_string == NULL)' condition was already verified in line 245. audit_mac.c 246


Предупреждение PVS-Studio: V547 Expression 'ar->k_ar.ar_arg_mac_string == NULL' is always true. audit_mac.c 246


На этот код анализатор выдал сразу два предупреждения.


Сначала взгляд может зацепиться за то, что проверка в самом первом if и во втором совпадает. Но тут всё правильно: внутри тела первой проверки аллоцируется память, а для второй проверки есть пояснение:


/* * XXX This should be a rare event. * If kheap_alloc() returns NULL, * the system is low on kernel virtual memory. To be * consistent with the rest of audit, just return * (may need to panic if required to for audit). */

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


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


Фрагмент N12


intutf8_encodestr(....){  u_int16_t ucs_ch;  int swapbytes = ....;  ....  ucs_ch = swapbytes ? OSSwapInt16(*ucsp++) : *ucsp++;  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'ucsp' variable is modified while being used twice between sequence points. vfs_utfconv.c 298


Макросы очень коварная штука. Возможно, вы даже уже встречались с нашей статьей "Вред макросов для C++ кода". Я обычно при написании статей избегаю срабатываний на макросы. С ними всегда всё оказывается сложно без знакомства с кодовой базой проекта.


Но в случае этой ошибки всё оказалось чуть проще. Хотя, чтобы дойти до причины и развернуть цепочку макросов, пришлось прыгнуть в ту ещё кроличью нору. Собственно, цепочка эта начинается с выражения OSSwapInt16(*ucsp++).


0818_XNU_MacOS_Kernel_ru/image10.png


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


ucs_ch = swapbytes? ( (__uint16_t)(__builtin_constant_p(*ucsp++)   ? ((__uint16_t)(  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)                   | (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)))   : _OSSwapInt16(*ucsp++))): *ucsp++;

Больше всего здесь нас интересует вот этот участок выражения:


  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)| (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)

Никакой из операторов в выражении не является точкой следования. Так как точно неизвестно, какой из аргументов оператора | будет вычисляться первым, значение *uscp оказывается неопределённым.


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


Однако это ещё не всё! Есть очень интересный и важный момент. Готова поспорить, что человек, писавший этот код, планировал увеличить значение *ucsp только один раз. Но, на самом деле, значение увеличится дважды. Это не видно и непонятно. Макросы очень и очень опасны из-за вот таких случаев. Во многих ситуациях лучше написать обыкновенную функцию. Скорее всего, компилятор автоматически выполнит подстановку и никакого ухудшения производительности не произойдёт.


Фрагмент N13


struct pf_status pf_status;intpf_insert_state(struct pf_state *s, ....){  ....  if (....) {    s->id = htobe64(pf_status.stateid++);    ....  }  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'pf_status.stateid' variable is modified while being used twice between sequence points. pf.c 1440


И снова коварные макросы смешали все карты для инкремента. Рассмотрим строку с вызовом htobe64, которая оказалась подозрительной для анализатора после препроцессинга:


s->id = (__builtin_constant_p(pf_status.stateid++) ? ((__uint64_t)((((__uint64_t)(pf_status.stateid++) &0xff00000000000000ULL) >> 56) | (((__uint64_t)(pf_status.stateid++) &0x00ff000000000000ULL) >> 40) | (((__uint64_t)(pf_status.stateid++) &0x0000ff0000000000ULL) >> 24) | (((__uint64_t)(pf_status.stateid++) &0x000000ff00000000ULL) >> 8)  | (((__uint64_t)(pf_status.stateid++) &0x00000000ff000000ULL) << 8)  | (((__uint64_t)(pf_status.stateid++) &0x0000000000ff0000ULL) << 24) | (((__uint64_t)(pf_status.stateid++) &0x000000000000ff00ULL) << 40) | (((__uint64_t)(pf_status.stateid++) &0x00000000000000ffULL) << 56))) : _OSSwapInt64(pf_status.stateid++));

0818_XNU_MacOS_Kernel_ru/image11.png


Проблема собственно та же, что и в предыдущем примере. Во внутренней цепочке с операндами | и & нет точек следования. Поэтому неизвестно, какое значение примет pf_status.stateid на моменте выполнения каждой операции. Результат также неопределён.


И, опять-таки, переменная увеличивается несколько раз подряд, что является неприятным сюрпризом от макроса :).


Вот остальные срабатывания этой диагностики на этом проекте:


  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. ip_id.c 186
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 505
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 497
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 588
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 665
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 1543

Фрагмент N14


__private_extern__ boolean_tipsec_send_natt_keepalive(....){  ....  struct udphdr *uh = (__typeof__(uh))(void *)(  (char *)m_mtod(m)                                                + sizeof(*ip));  ....  if (....)  {    uh->uh_sport = (u_short)sav->natt_encapsulated_src_port;  } else {    uh->uh_sport = htons((u_short)esp_udp_encap_port);  }  uh->uh_sport = htons((u_short)esp_udp_encap_port);  ....}

Предупреждение PVS-Studio: V519 The 'uh->uh_sport' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 4866, 4870. ipsec.c 4870


В этом фрагменте возникла подозрительная ситуация: полю uh_sport в зависимости от определённого условия присваиваются разные значения. Однако сразу после if-else этому же полю снова присваивается значение, такое же как в ветке else. В итоге этот if-else блок теряет смысл, так как значение поля всё равно будет перезаписано.


Фрагмент N15


static kern_return_tvm_shared_region_slide_page_v3(vm_offset_t vaddr, ....){  ....  uint8_t *page_content = (uint8_t *)vaddr;  uint16_t page_entry;  ....  uint8_t* rebaseLocation = page_content;  uint64_t delta = page_entry;  do {    rebaseLocation += delta;    uint64_t value;    memcpy(&value, rebaseLocation, sizeof(value));    ....    bool isBind = (value & (1ULL << 62)) == 1;   // <=    if (isBind) {      return KERN_FAILURE;    }    ....  } while (delta != 0);  ....}

Предупреждение PVS-Studio: V547 Expression '(value & (1ULL << 62)) == 1' is always false. vm_shared_region.c 2820


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


В результате побитового сдвига создаётся маска с единственной единицей в 63-ем бите. Результат побитового & с переменной value может принимать только значения 0 или 0x4000000000000000. А никакое из этих значений не равно 1. В итоге условие всегда будет ложным.


Судя по тому, что выполнение этого условия должно было привести к выходу из функции с возвратом KERN_FAILURE, возможно, как раз значение 0x4000000000000000 является тем самым исключительным случаем, после которого нужно выйти из функции. Тогда результат побитовых операций надо было сравнивать с этим числом, а не с 1. Ну, или написать так:


bool isBind = (value & (1ULL << 62)) != 0;

Фрагмент N16


intvn_path_package_check(char *path, int pathlen, ....){  char *ptr, *end;  int comp = 0;  ....  end = path + 1;  while (end < path + pathlen && *end != '\0') {    while (end < path + pathlen && *end == '/' && *end != '\0') {      end++;    }    ptr = end;    while (end < path + pathlen && *end != '/' && *end != '\0') {      end++;    }    ....  }  ....}

Предупреждение PVS-Studio: V590 Consider inspecting this expression. The expression is excessive or contains a misprint. vfs_subr.c 3589


Эта диагностика всегда указывает на излишний код. Иногда под ним скрывается более серьёзная ошибка. Но здесь это, скорее всего, просто недочёт. Предупреждение было выдано на первый внутренний while. Нет смысла проверять, что символ одновременно равен '/' и не равен '\0'. Достаточно только первой проверки, так как если *end равен '/', то он точно не может быть равен '\0'.


Следующий while содержит столько же проверок. Но из-за того, что в обоих случаях проверяется неравенство, эти проверки могут работать вместе. Возможно, сначала был написан второй while, а первый был скопирован с него с изменённой проверкой для '/'. Тогда перед нами недочёт, возникший из-за копипасты.


Заключение


В этот раз в проекте нашлось несколько меньше ошибок, чем в предыдущей статье. Весьма вероятно, что в процесс разработки XNU был внедрён статический анализ и другие инструменты контроля качества кода. Почти наверняка на проекте используется Clang Static Analyzer. Но ошибки и недочёты всё-таки нашлись. Я не стала приводить здесь некоторые срабатывания на подозрительные места, вывод по которым можно сделать только на основании большего понимания кодовой базы.


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


Если вам интересно, какие ошибки может помочь найти статический анализ в целом и PVS-Studio конкретно, то можете познакомиться с нашей подборкой статей про проверку исходных проектов. Там есть проверки кода не только операционных систем, но и, например, компиляторов и других инструментов программирования, которыми вы, возможно, пользуетесь ежедневно. Например, совсем недавно вышла статья про проверку Qt6.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Victoria Khanieva. MacOS Kernel, Is This Apple Rotten?.

Подробнее..

Опыт команды PVS-Studio повышение производительности C анализатора на Windows при переходе на Clang

31.05.2021 18:12:13 | Автор: admin

С самого своего начала C++ анализатор PVS-Studio для Windows (тогда еще Viva64 версии 1.00 в 2006 году) собирался компилятором MSVC. С выходом новых релизов C++ ядро анализатора научилось работать на Linux и macOS, и структура проекта была переведена на использование CMake. Но под Windows сборка по-прежнему происходила с помощью компилятора MSVC. 29 апреля 2019 года разработчики Visual Studio объявили о включении в свою среду разработки набора утилит LLVM и компилятора Clang. И сейчас у нас наконец дошли руки, чтобы попробовать его в действии.

Тестирование производительности

В качестве бенчмарка воспользуемся нашей утилитой для регрессионного тестирования анализатора под названием SelfTester. Суть её работы заключается в анализе набора разных проектов и сравнении результатов анализа с эталонными. Например, если при каких-то правках в ядре анализатора появились ложные предупреждения или пропали правильные, значит появилась регрессия, которую надо исправить. Более подробно про SelfTester можно прочитать в статье "Лучшее враг хорошего".

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

После того как сборка C++ ядра перешла на Clang, SelfTester стал проходить на 11 минут быстрее.

Выигрыш по производительности в 13% это довольно заметно, учитывая, что достаточно просто поменять компилятор, не так ли?

Минусы тоже есть, но незначительные. Сборка дистрибутива замедлилась на 8 минут, а размер исполняемого файла подрос на 1,6 Мбайт (из них ~500 Кбайт из-за статической линковки рантайма).

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

Далее хочется поделиться подводными камнями, возникшими в процессе перехода.

Генерация сборки под Clang

Скрипты CMake позволяют нам собирать свой код всеми мейнстримными компиляторами под нужные операционные системы.

Прежде всего необходимо установить компоненты компилятора Clang через Visual Studio Installer.

Clang-cl это так называемый "драйвер", который позволяет использовать clang с параметрами от cl.exe. Таким образом, он должен прозрачно взаимодействовать с MSBuild, практически как родной компилятор.

Также можно воспользоваться официальными сборками от проекта LLVM, которые можно найти на их репозитории GitHub. Однако для них нужно установить дополнительный плагин, чтобы Visual Studio смогла найти компиляторы. Имя toolset'а будет llvm, а не clangcl, как показано дальше в примерах.

Указываем toolchain в команде генерации solution для Visual Studio:

cmake -G "Visual Studio 16 2019" -Tclangcl <src>

Либо используем GUI:

Открываем получившийся проект, собираем. И, конечно же, получаем пачку ошибок.

Чиним сборку

Хоть сlang-cl внешне и ведет себя как CL, под капотом это совсем другой компилятор, со своими приколами.

Мы стараемся не игнорировать предупреждения компиляторов, поэтому используем флаги /W4 и /WX. Однако Clang может генерировать дополнительные предупреждения, которые сейчас мешают сборке. Пока выключим их:

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")  ....  if (WIN32)    add_compile_options(-Wno-error=deprecated-declarations                        -Wno-error=reorder-ctor                        -Wno-error=format-security                        -Wno-error=macro-redefined                        -Wno-error=bitwise-op-parentheses                        -Wno-error=missing-field-initializers                        -Wno-error=overloaded-virtual                        -Wno-error=invalid-source-encoding                        -Wno-error=multichar                        -Wno-unused-local-typedef                        -Wno-c++11-narrowing)  ....  endif()endif()

Немного получше.

Компиляторы GCC и Clang имеют встроенную поддержку типа int128, в отличие от MSVC под Windows. Поэтому в своё время была написана обертка с реализацией Int128 для Windows (на ассемблерных вставках и обернутая ifdef'ами, в лучших традициях C/C++). Поправим определения для препроцессора, заменив:

if (MSVC)  set(DEFAULT_INT128_ASM ON)else ()  set(DEFAULT_INT128_ASM OFF)endif ()

на

if (MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")  set(DEFAULT_INT128_ASM ON)else ()  set(DEFAULT_INT128_ASM OFF)endif ()

Обычно библиотеку с builtin'ами линкеру (lld) передает драйвер компилятора, будь то clang.exe или clang-cl.exe. Но в данном случае линкером заправляет MSBuild напрямую, который не знает, что нужно её использовать. Соответственно, драйвер никак не может передать флаги линкеру, поэтому приходится разбираться самим.

if (CMAKE_GENERATOR MATCHES "Visual Studio")  link_libraries("$(LLVMInstallDir)\\lib\\clang\\\${CMAKE_CXX_COMPILER_VERSION}\\lib\\windows\\\clang_rt.builtins-x86_64.lib")else()  link_libraries(clang_rt.builtins-x86_64)endif()

Ура! Сборка заработала. Однако дальше при запуске тестов нас ждала куча ошибок сегментации:

Отладчик при этом показывает какое-то странное значение в IntegerInterval, а на самом деле проблема находится немного дальше:

В разных структурах для Dataflow-механизма активно применяется ранее упомянутый тип Int128, а для работы с ним используются SIMD-инструкции. И падение вызвано невыровненным адресом:

Инструкция MOVAPS перемещает из памяти набор чисел с плавающей запятой в регистры для SIMD-операций. Адрес при этом обязан быть выровнен, в его конце должен стоять 0, а оказалась 8. Придется помочь компилятору, задав правильное выравнивание:

class alignas(16) Int128

Порядок.

Последняя проблема вылезла из-за Docker-контейнеров:

Сборка под MSVC всегда делалась со статической линковкой рантайма, а для экспериментов с Clang рантайм переключили на динамический. Оказалось, что в образах с Windows по умолчанию не установлены Microsoft Visual C++ Redistributable. Решили вернуть статическую линковку, чтобы у пользователей не возникало таких же неприятностей.

Заключение

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

Последующие релизы PVS-Studio на Windows будут собираться с помощью компилятора Clang.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Govorov, Sergey Larin. PVS-Studio Team: Switching to Clang Improved PVS-Studio C++ Analyzer's Performance.

Подробнее..

Как WCF сам себе в ногу стреляет посредством TraceSource

21.06.2021 14:12:41 | Автор: admin

Не так часто удается написать что-то интересное про проблемы, связанные с параллельным программированием. В этот же раз "повезло". Из-за особенностей реализации стандартного метода TraceEvent произошла ошибка с блокировкой нескольких потоков. Хочется предупредить о существующем нюансе и рассказать об интересном случае из поддержки наших пользователей. Причем тут поддержка? Это вы узнаете из статьи. Приятного чтения.

Предыстория

В дистрибутиве PVS-Studio есть одна утилита под названием CLMonitor.exe, или система мониторинга компиляции. Она предназначена для "бесшовной" интеграции статического анализа PVS-Studio для языков C и C++ в любую сборочную систему. Сборочная система должна использовать для сборки файлов один из компиляторов, поддерживаемых анализатором PVS-Studio. Например: gcc, clang, cl, и т.п.

Стандартный сценарий работы данной Windows утилиты очень простой, всего 3 шага:

  1. Выполняем 'CLMonitor.exe monitor';

  2. Выполняем сборку проекта;

  3. Выполняем 'CLMonitor.exe analyze'.

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

И вот в один прекрасный момент описанный сценарий не заработал, анализ просто-напросто не запустился. Ну и чтобы было поинтереснее, возникла эта проблема не у нас, а у пользователя, который обратился к нам в поддержку. У него стабильно после запуска анализа происходило десятиминутное ожидание ответа от сервера с дальнейшим выходом из программы по timeout'у. В чём причина непонятно. Проблема не воспроизводится. Да... беда. Пришлось запросить дампфайл для процесса нашей утилиты, чтобы посмотреть, что там происходит внутри.

Примечание. Проблема у пользователя возникла при использовании Windows утилиты CLMonitor.exe. Поэтому все дальнейшие примеры будут актуальны именно для Windows.

Как работает CLMonitor.exe

Чтобы лучше понять все мои дальнейшие рассуждения о проблеме пользователя, я советую не пропускать данный пункт. Здесь я расскажу о том, как происходит взаимодействие клиента и сервера.

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

Зачем мы вообще отлавливаем процессы

Как вы поняли, история начинается с того, что нужно запустить сервер, который будет отлавливать все процессы. Делаем мы это не просто так. Вообще, более удобный способ проанализировать C++ проект это прямой запуск анализатора через утилиту командной строки PVS-Studio_Cmd. У неё, однако, есть существенное ограничение она может проверять только проекты для Visual Studio. Дело в том, что для анализа требуется вызывать компилятор, чтобы он препроцессировал проверяемые исходные файлы, ведь анализатор работает именно с препроцессированными файлами. А чтобы вызвать препроцессор, нужно знать:

  • какой конкретно компилятор вызывать;

  • какой файл препроцессировать;

  • параметры препроцессирования.

Утилита PVS-Studio_Cmd узнает все необходимое из проектного файла (*.vcxproj). Однако это работает только для "обычных" MSBuild проектов Visual Studio. Даже для тех же NMake проектов мы не можем получить необходимую анализатору информацию, потому что она не хранится в самом проектном файле. И это несмотря на то, что NMake также является .vcxproj. Сам проект при этом является как бы обёрткой для другой сборочной системы. Тут в игру и вступают всяческие ухищрения. Например, для анализа Unreal Engine проектов мы используем прямую интеграцию с *Unreal Build Tool * сборочной системой, используемой "под капотом". Подробнее здесь.

Поэтому, для того чтобы можно было использовать PVS-Studio независимо сборочной системы, даже самой экзотической, у нас и появилась утилита CLMonitor.exe. Она отслеживает все процессы во время сборки проекта и отлавливает вызовы компиляторов. А уже из вызовов компиляторов мы получаем всю необходимую информацию для дальнейшего препроцессирования и анализа. Теперь вы знаете, зачем нам нужно мониторить процессы.

Как клиент запускает анализ

Для обмена данными между сервером и клиентом мы используем программный фреймворк WCF (Windows Communication Foundation). Давайте далее кратко опишем, как мы с ним работаем.

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

static ErrorLevels PerformMonitoring(....) {  using (ServiceHost host = new ServiceHost(                       typeof(CLMonitoringContract),                          new Uri[]{new Uri(PipeCredentials.PipeRoot)}))   {    ....    host.AddServiceEndpoint(typeof(ICLMonitoringContract),                             pipe,                             PipeCredentials.PipeName);    host.Open();         ....  }}

Обратите тут внимание на две вещи: *CLMonitoringContract *и ICLMonitoringContract.

*ICLMonitoringContract * это сервисный контракт. *CLMonitoringContract * реализация сервисного контракта. Выглядит это так:

[ServiceContract(SessionMode = SessionMode.Required,                  CallbackContract = typeof(ICLMonitoringContractCallback))]interface ICLMonitoringContract{  [OperationContract]  void StopMonitoring(string dumpPath = null);} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]class CLMonitoringContract : ICLMonitoringContract{  public void StopMonitoring(string dumpPath = null)  {    ....    CLMonitoringServer.CompilerMonitor.StopMonitoring(dumpPath);  } }

Когда мы запускаем клиент, нам нужно остановить работу сервера и забрать у него все необходимые данные. Благодаря данному интерфейсу мы это и делаем. Вот как выглядит остановка сервера со стороны клиента:

public void FinishMonitor(){  CLMonitoringContractCallback сallback = new CLMonitoringContractCallback();  var pipeFactory = new DuplexChannelFactory<ICLMonitoringContract>(           сallback,            pipe,            new EndpointAddress(....));  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);}

Когда клиент выполняет метод StopMonitoring, он на самом деле выполняется у сервера и вызывает его остановку. А клиент получает данные для запуска анализа.

Всё, теперь вы, хоть немного, имеете представление о внутренней работе утилиты CLMonitor.exe.

Просмотр дамп файла и осознание проблемы

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

**Интересный факт. **Откуда вообще взялись эти 10 минут? Дело в том, что мы задаем время ожидания ответа от сервера намного больше, а именно - 24 часа, как видно в примере кода, приведённом выше. Однако для некоторых операций фреймворк сам решает, что это слишком много и он успеет быстрее. Поэтому берет только часть от изначального значения.

Мы попросили у пользователя снять дамп с двух процессов (клиент и сервер) минуток через 5 после запуска клиента, чтобы посмотреть, что там происходит.

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

Дамп 'клиента'

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

Нас интересует главный поток. Он висит на методе, отвечающем за запрос остановки сервера:

public void FinishMonitor(){  ....  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);            // <=  ....}

Получается, что клиент попросил сервер завершить работу, а ответа не последовало. И это странное поведение, так как обычно данная операция занимает доли секунды. А я напомню, что в данном случае дамп был снят спустя 5 минут от начала работы клиента. Что ж, давайте смотреть, что там на сервере происходит.

Дамп 'сервера'

Открываем его и видим следующий список потоков:

Воу-воу, откуда так много TraceEvent'ов? Кстати, на скриншоте не уместилось, но всего их более 50. Ну давайте подумаем. Данный метод у нас используется, чтобы логировать различную информацию. Например, если отловленный процесс является компилятором, который не поддерживается, произошла ошибка считывания какого-либо параметра процесса и т.д. Посмотрев стеки данных потоков, мы выяснили, что все они ведут в один и тот же метод в нашем коде. А метод этот смотрит, является ли отловленный нашей утилитой процесс компилятором или это нечто иное и неинтересное, и, если мы отловили такой неинтересный процесс, мы это логируем.

Получается, что у пользователя запускается очень много процессов, которые, конкретно для нас, являются 'мусором'. Ну допустим, что это так. Однако данная картина все равно выглядит очень подозрительно. Почему же таких потоков так много? Ведь, по идее, логирование должно происходить быстро. Очень похоже на то, что все эти потоки висят на какой-то точке синхронизации или критической секции и чего-то ждут. Давайте зайдем на ReferenceSource и посмотрим исходный код метода TraceEvent.

Открываем исходники и действительно видим в методе TraceEvent оператор lock:

Мы предположили, что из-за постоянной синхронизации и логирования накапливается большое количество вызовов методов TraceEvent, ждущих освобождения TraceInternal.critSec. Хм, ну допустим. Однако это пока не объясняет, почему сервер не может ответить клиенту. Посмотрим еще раз в дамп файл сервера и заметим один одинокий поток, который висит на методе DiagnosticsConfiguration.Initialize:

В данный метод мы попадаем из метода NegotiateStream.AuthenticateAsServer, выполняющего проверку подлинности со стороны сервера в соединении клиент-сервер:

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

И тут мы замечаем, что в данном методе имеется критическая секция, да еще и на ту же самую переменную. Посмотрев, что вообще такое этот critSec, увидим следующее:

Собственно, у нас уже есть достаточно информации, чтобы подвести итоги.

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

"Also one of the locks, TraceInternal.critSec, is only present if the TraceListener asks for it. Generally speaking such 'global' locks are not a good idea for a high performance logging system (indeed we don't recommend TraceSource for high performance logging at all, it is really there only for compatibility reasons)".

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

Итоги изучения дампов

Итак, что мы имеем:

  1. Клиент общается с сервером с помощью фреймворка WCF.

  2. Клиент не может получить ответа от сервера. После 10 минут ожидания он падает по тайм-ауту.

  3. На сервере висит множество потоков на методе TraceEvent и всего один - на Initialize.

  4. Оба метода зависят от одной и той же переменной в критической секции, притом это статическое поле.

  5. Потоки, в которых выполняется метод TraceEvent, бесконечно появляются и из-за lock не могут быстро сделать свое дело и исчезнуть. Тем самым они долго не отпускают объект в lock.

  6. Метод Initialize возникает при попытке клиента завершить работу сервера и висит бесконечно на lock.

Из этого можно сделать вывод, что сервер получил команду завершения от клиента. Чтобы начать выполнять метод остановки работы сервера, необходимо установить соединение и выполнить метод Initialize. Данный метод не может выполниться из-за того, что объект в критической секции держат методы TraceEvent, которые в этот момент выполняются на сервере. Появление новых TraceEvent'ов не прекратится, потому что сервер продолжает работать и отлавливать новые 'мусорные' процессы. Получается, что клиент никогда не получит ответа от сервера, потому что сервер бесконечно логирует отловленные процессы с помощью TraceEvent. Проблема найдена!

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

Теперь остается только воспроизвести и починить проблему.

Воспроизведение проблемы

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

private void CrazyLogging(){  for (var i = 0; i < 30; i++)  {    var j = i;    new Thread(new ThreadStart(() =>    {      while (!Program.isStopMonitor)        Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());    })).Start();  }}

За работу сервера у нас отвечает метод Trace, поэтому добавляем наше логирование в него. Например, вот сюда:

public void Trace(){  ListenersInitialization();  CrazyLogging();  ....}

Готово. Запускаем сервер (я буду это делать с помощью Visual Studio 2019), приостанавливаем секунд через 5 процесс и смотрим что у нас там с потоками:

Отлично! Теперь запускаем клиент (TestTraceSource.exe analyze), который должен установить связь с сервером и остановить его работу.

Запустив, мы увидим, что анализ не начинается. Поэтому опять останавливаем потоки в Visual Studio и видим ту же самую картину из дамп файла сервера. А именно появился поток, который висит на методе DiagnosticsConfiguration.Initialize. Проблема воспроизведена.

Как же её чинить? Ну для начала стоит сказать, что TraceSource это класс, который предоставляет набор методов и свойств, позволяющих приложениям делать трассировку выполнения кода и связывать сообщения трассировки с их источником. Используем мы его потому, что сервер может быть запущен не приаттаченным к консоли, и консольное логирование будет бессмысленно. В этом случае мы логировали всё в Event'ы операционной системы с помощью метода TraceSource.TraceEvent.

Проблему мы "решили" следующим образом. По умолчанию теперь вся информация логируется в консоль, используя метод Console.WriteLine. В большинстве случаев, даже если эта логируемая информация и будет потеряна из-за не приаттаченной консоли, она всё равно не требуется для решения задач в работе утилиты, а проблема исчезла. Плюс изменения заняли какие-то минуты. Однако мы оставили возможность старого способа логирования с помощью специального флага enableLogger.

Код, воспроизводящий проблему

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

Чтобы запустить имитирование работы сервера, запустите .exe с флагом trace. Чтобы запустить клиент, воспользуйтесь флагом analyze.

**Примечание: **количество потоков в методе CrazyLogging следует подбирать индивидуально. Если проблема у вас не воспроизводится, то попробуйте поиграться с этим значением. Также можете запустить данный проект в Visual Studio в режиме отладки.

Точка входа в программу:

using System.Linq;namespace TestTraceSource{  class Program  {    public static bool isStopMonitor = false;    static void Main(string[] args)    {      if (!args.Any())        return;      if (args[0] == "trace")      {        Server server = new Server();        server.Trace();      }      if (args[0] == "analyze")      {        Client client = new Client();        client.FinishMonitor();      }    }    }}

Сервер:

using System;using System.Diagnostics;using System.ServiceModel;using System.Threading;namespace TestTraceSource{  class Server  {    private static TraceSource Logger;    public void Trace()    {      ListenersInitialization();      CrazyLogging();      using (ServiceHost host = new ServiceHost(                          typeof(TestTraceContract),                           new Uri[]{new Uri(PipeCredentials.PipeRoot)}))      {        host.AddServiceEndpoint(typeof(IContract),                                 new NetNamedPipeBinding(),                                 PipeCredentials.PipeName);        host.Open();        while (!Program.isStopMonitor)        {          // We catch all processes, process them, and so on        }        host.Close();      }      Console.WriteLine("Complited.");    }    private void ListenersInitialization()    {      Logger = new TraceSource("PVS-Studio CLMonitoring");      Logger.Switch.Level = SourceLevels.Verbose;      Logger.Listeners.Add(new ConsoleTraceListener());      String EventSourceName = "PVS-Studio CL Monitoring";      EventLog log = new EventLog();      log.Source = EventSourceName;      Logger.Listeners.Add(new EventLogTraceListener(log));    }    private void CrazyLogging()    {      for (var i = 0; i < 30; i++)      {        var j = i;        new Thread(new ThreadStart(() =>        {          var start = DateTime.Now;          while (!Program.isStopMonitor)            Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());        })).Start();      }    }   }}

Клиент:

using System;using System.ServiceModel;namespace TestTraceSource{  class Client  {    public void FinishMonitor()    {      TestTraceContractCallback сallback = new TestTraceContractCallback();      var pipeFactory = new DuplexChannelFactory<IContract>(                                сallback,                                new NetNamedPipeBinding(),                                new EndpointAddress(PipeCredentials.PipeRoot                                                   + PipeCredentials.PipeName));      IContract pipeProxy = pipeFactory.CreateChannel();      pipeProxy.StopServer();      Console.WriteLine("Complited.");        }  }}

Прокси:

using System;using System.ServiceModel;namespace TestTraceSource{  class PipeCredentials  {    public const String PipeName = "PipeCLMonitoring";    public const String PipeRoot = "net.pipe://localhost/";    public const long MaxMessageSize = 500 * 1024 * 1024; //bytes  }  class TestTraceContractCallback : IContractCallback  {    public void JobComplete()    {      Console.WriteLine("Job Completed.");    }  }  [ServiceContract(SessionMode = SessionMode.Required,                    CallbackContract = typeof(IContractCallback))]  interface IContract  {    [OperationContract]    void StopServer();  }  interface IContractCallback  {    [OperationContract(IsOneWay = true)]    void JobComplete();  }  [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]  class TestTraceContract : IContract  {    public void StopServer()    {      Program.isStopMonitor = true;    }  }}

Вывод

Будьте осторожны со стандартным методом TraceSource.TraceEvent. Если у вас теоретически возможно частое использование данного метода в программе, то вы можете столкнуться с подобной проблемой. Особенно если у вас высоконагруженная система. Сами разработчики в таком случае не рекомендуют использовать всё, что связано с классом TraceSource. Если вы уже сталкивались с чем-то подобным, то не стесняйтесь рассказать об этом в комментариях.

Спасибо за просмотр. Незаметно рекламирую свой Twitter.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov. How WCF Shoots Itself in the Foot With TraceSource.

Подробнее..

Kanban команды PVS-Studio. Часть 1 agile

31.03.2021 16:16:13 | Автор: admin

Эта статья могла появиться на свет гораздо раньше, примерно на год. Ведь около года назад мы в компании PVS-Studio решили, что пришло время поэкспериментировать с agile. Но хотелось накопить пользовательский опыт, собрать статистику и уже потом поведать об этом миру. К тому же, одновременно с agile мы запланировали переход на другой трекер задач (вместо Bitbucket), а также много других изменений в наших внутренних процессах разработки. В общем, до написания статьи руки никак не доходили.

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

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

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

Немного истории

Компания PVS-Studio имеет более чем десятилетнюю историю развития. Все это время работы ведутся над одноименным статическим анализатором кода. Подробнее про это можно почитать в статье "Как 10 лет назад начинался проект PVS-Studio". Таким образом, PVS-Studio фактически компания одного проекта. И это проект для разработчиков. Но мы внутри компании в качестве проектов привыкли также выделять и другие направления работы, которые фиксируются в трекере задач наравне с разработкой: маркетинг, продажи, внутренние интернет-проекты, административная и IT-поддержка офиса. Все это создает достаточно большой поток разнородных задач, с которым нужно как-то справляться.

До перехода на новый трекер мы использовали Bitbucket. Первая задача там датирована 30 июня 2014, а последняя 5 февраля 2021 (примерно с этого момента мы начали использовать новый инструмент, про это будет вторая часть статьи). Всего за это время было создано 5527 задач. До Bitbucket в компании использовали еще какой-то инструмент, но не будем забираться так далеко в прошлое. Примерный подсчет дает около 3.3 новых задач на один рабочий день (при условии 250 рабочих дней в году и 24 рабочих дней в месяце). Давайте округлим это число до целого, не забыв, что не все задачи были доведены до состояния "Решено". Итого мы получим три новых задачи в день на примерно 30 сотрудников компании (с 2014 по 2021 год размер компании увеличился вдвое: с 20 до 40 человек). Вероятно, можно считать, что также и закрывали примерно три задачи в день. Предлагаю не анализировать это число, так как оно ничего не говорит о числе задач, находящихся у сотрудника единовременно в работе. Вот такой показатель был бы нам полезен. Но, к сожалению, получить его можно только экспериментально и при работе с определенной методологией, о чём будет рассказано далее.

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

Несмотря на то, что в нашем случае развитие компании шло достаточно планомерно, мы, как и многие другие, столкнулись с типичным паттерном проблемы: "Еще год назад это работало (хватало), а теперь не работает (не хватает)". Здесь и площадь офиса, и число баночек газировки в холодильнике, и максимум доступных процессорных потоков на сборочном сервере, и пр. Такая же ситуация с организацией работы и управлением. Кажется, еще совсем недавно мы обходились без регулярных one-to-one. Повышение разработчика происходило по принципу "вроде норм кодит". А фраза "верни задачу в бэклог на kanban" могла быть расценена в лучшем случае как неумелый троллинг. Конечно, все это обычная ситуация роста, и говорить о большой проблеме тут излишне сгущать краски. Тем не менее, на мой взгляд, от готовности команды и руководства компании к переменам в таких ситуациях зависит успех бизнеса.

С какими же конкретно проблемами мы столкнулись при увеличении размера компании и росте потока задач? Основная сильная централизация управления процессом разработки. К концу 2019 года вся разработка в компании на всех уровнях контролировалась в основном техническим директором, а в командах не хватало тимлидов. Также не вполне понятен был жизненный цикл задачи, неполно отслеживалась эффективность работы и сложно прогнозировался ее результат. В штате PVS-Studio на тот момент состояло около 34 человек: команды маркетинга и продаж; команды разработки на языках C/С++, C#, Java; команда DevOps, административный персонал и руководство. Примерно половина команды занималась непосредственно разработкой.

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

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

Выбор методологии

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

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

  • простота перехода. Мы предвидели возможность естественного сопротивления команды, поэтому хотелось внедрить новые практики максимально безболезненно для рядовых сотрудников;

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

  • возможность сопровождения своими силами, несложная в освоении методология;

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

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

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

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

Наконец, переход на scrum потребовал бы внесения значительных изменений в привычный ход работы. Преимущества kanban в том, что вначале вы просто описываете с его помощью те процессы, которые у вас есть, ничего не меняя в них. И только затем постепенно начинаете их совершенствовать. Это делает внедрение более плавным и менее тревожит команду.

На самом деле, многое из того, что декларирует agile, мы применяли ранее. Мы проводили ежедневные митинги, хоть и не во всех командах. Команды, как я уже говорил, были достаточно самоорганизованы, а сотрудники отвечали за результат. Также сотрудники имели некоторую свободу при выборе задач, например могли заняться написанием статьи одновременно с процессом разработки, самостоятельно планируя своё рабочее время. Любой сотрудник мог поставить задачу другому, независимо от его уровня в компании (матричная структура). Наконец, у нас были подобия спринтов, привязанные к релизному циклу.

Не хватало только систематизации, ритмичности при работе с потоком задач, а также чётко прописанных правил и требований, которые бы были понятны, однозначно приняты и соблюдались каждым участником процесса. "Баланс и общение" вот практически девиз нашей команды в настоящее время. И kanban здесь подходит как нельзя лучше.

Подпольный kanban

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

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

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

  • по столбцам в первоначальном варианте получилось так: Backlog (общий пул задач), Assignee (ответственный, исполнитель), On hold (буфер задач исполнителя), Analyze (задача на стадии изучения), In progress (задача в работе), Check (оценка результатов, приёмка), Done (задача решена);

  • макет карточки выглядел так:

  • некоторое время потратили на выбор значения лимита незавершенной работы (WIP) для исполнителя. Логичным казалось, что у человека не может быть в работе одновременно более двух задач. Тем не менее, для начала решили остановиться на ограничении в три задачи, учитывая, что разработчики у нас могут заниматься и не связанными с разработкой активностями (написание статей, например), которые можно эффективно совмещать с разработкой. Для kanban такой подход является классическим: вначале выбирается примерное значение, которое затем уточняется в процессе работы. Вообще весь kanban это один сплошной эксперимент в попытке уменьшить значение WIP. :) Спойлер: значение WIP мы в итоге все же установили равное двум;

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

  • за следующие пару недель использования макет доски ещё несколько раз уточнялся. Добавили отдельную строку для задач-эпиков без конкретного исполнителя. Уменьшили ширину столбца "Done" и за счет этого увеличили ширину столбца для бэклога. Добавили столбец "Waiting" (задача на ожидании, например, ответа от клиента, или статья на переводе) между столбцами "Check" и "Done". Решили использовать карточки разных цветов для разделения задач по типу работ: просто задача, правка бага, эпик, написание статьи и т.п.;

  • любая правка доски обсуждалась, при этом обсуждения часто бывали достаточно эмоциональными;

  • пока доска велась скрытно, штат пополнился еще несколькими разработчиками и места в строках перестало хватать. Было решено в дальнейшем купить двустороннюю доску, но уже после ее обнародования;

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

Так прошло чуть больше месяца (с учетом новогодних каникул), и вот в январе 2020 года мы, наконец, решили, что пришло время обнародовать доску (на фотографии ниже видна одна сторона доски, не включающая направление C++).

Kanban в массы

В тематической литературе приводятся различные сценарии использования kanban-досок. Для продвинутых команд предлагается автономное использование, когда разработчики самостоятельно берут (вытягивают) задачи из бэклога. Бэклог пополняется по результатам совещания менеджеров, каждый из которых выступает заказчиком определенных изменений. При этом за перемещение карточек по доске отвечают сами разработчики. Такой подход полностью соответствует философии kanban, где упор делается на самоорганизацию и баланс (kanban, в отличие от scrum, не предполагает привлечения scrum-мастера).

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

Итак, наша доска с актуальными задачами была представлена на всеобщее обозрение и размещена в конференц-зале. Ранее мы уже практиковали ежедневные рабочие собрания, но они не носили системный характер и проводились от случая к случаю. Теперь мы решили организовать работу по расписанию и проводить ежедневные митинги по отдельности для четырёх команд разработки (C/С++, C#, Java; DevOps). Продолжительность митинга установили не более 15 минут. В дополнение к этому в отделе маркетинга велась еще одна независимая доска, которая несколько отличалась, но также вписывалась в нашу общую концепцию работы по kanban.

Собственно, введение ежедневных митингов по расписанию, а не доска, стало причиной ожидаемого сопротивления команды. Основной вопрос был примерно такой: "Зачем нам эти митинги? Мы и так знаем, кто и что делает внутри команды, проводим свои ежедневные обсуждения. Просто трата времени какая-то. Да еще эта доска". Нам кажется, что теперь после более чем года работы по kanban мы можем дать ответы на эти и другие вопросы.

Довольно быстро мы осознали, что kanban-доска является отличным местом для проведения митингов. Идея наших митингов состоит в том, что разработчик рассказывает о своей активности в первую очередь себе (систематизируя работу, выявляя что-то упущенное или не так понятое им), а уже затем коллегам и руководителю. При этом доска помогает видеть весь поток задач (как своих, так и чужих) и понимать, что ты не перегружен, что будет чем заняться дальше (есть буфер и бэклог) и есть вот эта пухлая пачка твоих задач в последнем столбце "Done" по-настоящему осязаемый результат твоей работы за последний релизный цикл. Это действительно работает, проверено.

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

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

Таков путь

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

Примерно в это время мы начали задумываться об электронной kanban-доске. Какое-то время ситуация с COVID была непонятной, мы вели онлайн-митинги в Zoom. При этом каждый участник использовал список задач Bitbucket, чтобы отчитаться о работе. Это было очень неудобно и осложняло и так непростой режим работы online.

Осенью 2020 года стало понятно, что удаленная работа затягивается. Возникла идея не просто внедрить электронный аналог kanban-доски, а действовать более радикально и полностью сменить трекер задач на более удовлетворяющий нашим требованиям на тот момент. В первую очередь, конечно, мы обратили внимание на Jira. Тем более что переход на эту систему с Bitbucket позволил бы легко перенести все ранее созданные задачи. Также мы рассматривали еще одну систему YouTrack, которая очень нравилась тимлиду нашей С++ команды (привет, Филипп). Более того, по этой системе еще летом Филипп даже делал презентацию для менеджеров.

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

Заключение

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

Что касается вообще методологий agile и kanban, их ценности для построения рабочих процессов, сложности внедрения, могу еще раз кратко озвучить наш опыт. Главное не бойтесь экспериментировать. Ведь не зря же эти подходы называют гибкими. Я бы еще добавил, что они достаточно дружелюбны и прощают множество ошибок. Можно внедрить новые подходы максимально безболезненно и даже скрытно на первых порах, а в случае неудач потери будут невелики. И если вы, как в нашем случае, будете внедрять какую-то методологию с нуля, то полученный результат, польза, точно превысят затраты.

Спасибо за внимание и до встречи во второй части.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Khrenov. PVS-Studio Team's Kanban Board. Part 1: Agile.

Подробнее..

Новые возможности PVS-Studio по оповещению разработчиков о найденных ошибках

18.05.2021 16:06:28 | Автор: admin

В поддержку PVS-Studio часто поступают предложения от пользователей по улучшению продукта. Многие из них мы с радостью берёмся реализовывать. Одно из последних таких предложений было связано с доработкой утилиты автоматического оповещения разработчиков (Blame Notifier). Нас попросили научить ее извлекать дату/ревизию кода, на который анализатор выдал сообщение, с помощью blame информации из системы контроля версий. Такая доработка позволила расширить возможности утилиты, о которых мы и поговорим в этой статье.

С чего все началось

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

Так как у клиента большая кодовая база, мы предложили логически разделить использование статического анализатора на до и после внедрения статического анализа, временно подавив ранее найденные предупреждения при помощи нашего механизма подавления. После чего анализатор бы выдавал предупреждения только на новый или измененный код. И в дополнение к этому рассказали о внедрении Blame Notifier, чтобы сразу же оповещать всех причастных лиц о новых предупреждениях. Предложение заинтересовало клиента, но из-за того, что ему хотелось видеть в отчёте как старые, так и новые предупреждения, такой режим работы показался пользователю не очень удобным. Помимо этого, он дал обратную связь по Blame Notifier: "было бы круто, чтобы предупреждения в оповещениях были поинформативней, например добавить дату и номер ревизии".

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

Мы предложили клиенту следующий вариант:

  • результирующий отчёт анализатора вы будете получать со всеми предупреждениями;

  • Blame Notifier можно настроить так, чтобы в HTML-отчёт попадали предупреждения только на свежеизменённый код. Помимо этого, предупреждения будут содержать дату и номер ревизии.

На этом мы и сошлись!

Об утилите

Предназначение Blame Notifier автоматизировать процесс оповещения разработчиков, на код которых выдал предупреждение PVS-Studio, например, после ночных сборок. Утилита позволяет формировать HTML-отчёт как для конкретного разработчика только с его предупреждениями, так и для супер-пользователя, который получает полный отчёт со всеми предупреждениями. Полный отчёт по умолчанию представляет собой сгруппированные предупреждения по разработчикам, а те, в свою очередь, отсортированы в алфавитном порядке. Такая функциональность крайне полезна, так как сразу же будет сигнализировать о новых срабатываниях анализатора всем заинтересованным лицам. Как можно догадаться из названия данной утилиты, она работает на основе blame информации, получаемой для проверяемых анализатором файлов из системы контроля версий пользователя.

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

Что же нового

Из blame информации, помимо имени разработчика, теперь извлекается дата и ревизия последнего изменения кода, на который ругается PVS-Studio. Дополнительная извлекаемая информация позволила нам добавить в утилиту новые опции:

  • --sortByDate (-S) позволяет формировать HTML-отчёт с отсортированными предупреждениями по дате изменения исходного кода, из-за которого было выдано предупреждение анализатора. Предупреждения для конкретной даты группируются, в свою очередь, по разработчикам.

  • --days (-d) в HTML-отчёт попадают предупреждения на код, дата изменения которого меньше N дней от даты текущего запуска утилиты.

Примечание. Извлечение даты/ревизии поддержано для следующих систем контроля версий: SVN, Git и Mercurial.

Формат HTML-отчёта утилиты по умолчанию выглядит следующим образом:

Новый HTML-отчёт, отсортированный по дате:

Как это можно применить

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

Примечание. Если вы уже используете PVS-Studio в связке с SonarQube, то описанное ниже применение будет для вас неактуальным, так как соответствующая обработка новых предупреждений в SonarQube уже встроена. А вот если вы используете PVS-Studio отдельно и нет возможности\желания воспользоваться SonarQube, то это может вас заинтересовать.

Начнем с того, что у SonarQube используется подход к качеству кода, называемый 'Clean as You Code'. Его суть в том, что разработчики должны уделять повышенное внимание надежности и безопасности нового кода, который был только-только добавлен или изменен. Старый код, который уже годами доказывает свою работоспособность в 'production', сместить на второй план и сосредоточиться на том, что происходит "сегодня", тем самым предотвращая появление новых проблем. А к давно существующим проблемам периодически возвращаться и исправлять. С этим подходом подробней можно ознакомиться в блоге разработчиков SonarQube.

Данный подход реализован следующим образом. На главной странице проекта есть выделенная область с новыми проблемами за настраиваемый промежуток времени. Для новых проблем настраивается 'Quality Gate', так называемый индикатор соответствия нового кода заданным пороговым метрикам. Например:

  • количество новых багов;

  • количество новых уязвимостей;

  • коэффициент технического долга;

  • покрытие нового кода тестами;

  • ... и другие.

Например, каждое утро будет загружаться свежий отчёт анализатора, и если пороговое значение какой-либо метрики превышено, то 'Quality Gate' будет сигнализировать вам об этом.

Визуальное представление подхода 'Clean as You Code' интуитивно понятное. Вот пример, как это выглядит в SonarQube 7.9.4:

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

  • разбивает проблемы в коде на существующие и свежие;

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

  • позволяет фильтровать проблемы по критериям;

  • позволяет смотреть предупреждения, найденные инструментами контроля качества кода, непосредственно в проверяемом коде прямо из web-браузера;

  • ... и многое другое.

Если SonarQube уже используется, то, чтобы интегрировать с ним результаты работы анализатора PVS-Studio, вам нужно ознакомиться со статьей.

А вот что делать, если вы ещё не используете SonarQube?

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

  • запуске сервера SonarQube;

  • настройке 'Quality Profiles';

  • настройке 'Quality Gates';

  • использовании 'sonar-scanner';

  • ... и так далее.

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

И здесь-то как раз на помощь приходит наша утилита Blame Notifier. Она и раньше уже могла заместить часть возможностей SonarQube в области уведомления разработчиков. С новыми же возможностями утилиты теперь можно имитировать облегченную версию подхода 'Clean as You Code', где основной метрикой качества кода будет появление новых предупреждений анализатора. В таком режиме рассылка будет содержать предупреждения на код, дата изменения которого меньше N дней от даты текущего запуска утилиты.

Повторим поведение SonarQube. Для этого укажем 10 дней для опции '-d' и отсортируем предупреждения (-S) по дате изменения кода, вызвавшего предупреждение. В таком случае HTML-отчёт будет следующим:

Как видно, отчёт содержит 11 новых предупреждений за последние 10 дней. Выглядит практически так же, как и в SonarQube, но получить такую отчётность в разы легче.

У такого "облегченного" подхода, конечно, есть и свои недостатки:

  • качество нового кода оценивается по одной метрике;

  • срабатывания на код, дата изменения которого за пределами рассматриваемого периода, не будут включены в HTML-отчёт. Для этого необходимо получить полный отчёт за все время при помощи дополнительного запуска 'blame-notifier' без ограничивающих опций.

  • ну и, конечно, отсутствует возможность навигации по проверяемому коду непосредственно с помощью web-браузера

Стоит также упомянуть, что использование утилиты Blame Notifier и PVS-Studio плагина для SonarQube доступно только в Enterprise лицензии PVS-Studio.

Заключение

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

Например, PVS-Studio имеет множество плагинов (для Visual Studio, SonarQube, Jenkins, Gradle/Maven/IntelliJ IDEA), утилиту для более удобной конвертации отчётов PlogConverter, утилиту оповещения разработчиков blame-notifier. Команда PVS-Studio, опираясь на свой опыт и на обратную связь пользователей, постоянно совершенствует свой продукт. Чтобы не пропустить все изменения и идти в ногу с инструментом, не забывайте следить за блогом на официальном сайте.

Новый режим работы Blame Notifier позволяет "легковесно" выполнять часть функционала, который раньше был доступен в нашем продукте только в связке с SonarQube. Однако, хочется ещё раз повторить мы ни в коем случае не призываем отказываться от использования SonarQube. Новый режим позволяет лишь значительно проще попробовать такой подход к работе со статическим анализом. Мы верим, что для многих команд данный режим будет актуален.

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Maxim Stefanov. PVS-Studio New Features for Notifying Developers About Errors Found.

Подробнее..

Эй, PVS-Studio, а где же ивенты?

12.02.2021 18:18:50 | Автор: admin

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

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

Новый год новые возможности!...?

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

Бизнес ивенты

В начале года мы успели посетить лично (офлайн) некоторые конференции, среди которых:

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

Так мои коллеги выступили с докладами на конференции DevGamm online 2020 (Виктория Ханиева "Static Analysis and Unity Projects: why and how" и Георгий Грибков "Качество кода игровых движков: неужели всё так плохо?")

В июне нас поглощает онлайн, и мы пытаемся найти свою нишу, участвуя в некоторых пробных конференциях. Так мы успели засветиться на DotNext Online, C++ Russia Online и C++ on Sea (Юрий Минаев "Hypercritical C++ Code Review").

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

И еще пара конференций в офлайне:

  • TestCon Moscow 2020 (Никита Липилин "Что интересного в тестировании инструмента для тестирования");

  • SQA Days 27 (Георгий Грибков "Как облегчить жизнь себе и разработчикам: статический анализ в деле (на примере Unreal Engine 4)").

Кроме конференций нашим спикерам понравились выступления в различных подкастах.

Юрий Минаев:

Филипп Хандельянц:

Подкаст с участие и Филиппа Хандельянца, и Юрия Минаева:

Из новеньких форматов мы опробовали вебинары: ITVDN - Типичные ошибки в коде на примере С++, С# и Java. Хорошая возможность подготовить спикера в подаче материала перед большими конференциями.

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

Что ж, вроде неплохо для такого "неурожайного" года, но мы и не думали на этом останавливаться. БОльшую часть прошлого года мы посвятили своей команде, её сплочению, развитию, поддержанию и т. д., поэтому мы плавно переходим к следующему разделу.

Внутренние мероприятия

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

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

Корпоративные мероприятия мы открыли с двух праздников 23 февраля и 8 марта. Тут всё как обычно, девочки готовили сюрприз мальчикам, мальчики девочкам. Ребятам мы подготовили подарочки и угощения, а 8 марта они решили проверить нас на скорость реакции и сообразительность, организовав для нас квест.

Также в марте мы успели получить немного адреналина и съездили на картинг.

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

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

Кроме какой-то летней передышки, мы посвятили время внедрению новых сотрудников. Устраивали выездные мероприятия с целью ознакомления с внутренним устройством команды, но на этом мы не останавливались и осенью разъехались по командам: C++ отдел, C# отдел, Java отдел + Tools&Devops и команда маркетинга. Зачем? Всё очень просто: мы по-отдельности проходили обучение по коммуникации.

Некоторые тематические мероприятия мы тоже успели захватить. Так перед самым началом осенних карантинных мер, мы досрочно устроили вечеринку в стиле Halloween.

Что показал 2020

Давайте немного поговорим о сухой статистике. В 2019 году в общей сумме мы захватили ~40 мероприятий (36 бизнес направленности и несколько внутренних). В 2020 году эта цифра упала, но не сильно. Мы успели поучаствовать в 23 бизнес мероприятиях и устроили 13 внутренних. Итого 36 ивентов.

Да, из диаграммы видно, что бизнес мероприятий стало куда меньше, и мы компенсировали их внутренними событиями. Нельзя сказать, что команда была такой проблемной, что нам понадобилось аж столько выездов :D Нет! Внутренние мероприятия перекрывали множество целей, которые потом максимально помогли в развитии сотрудников, их сплочению и продуктивной работе. Плюс, постоянные активности помогли понять, что, не вкладывая в свою команду результатов не достичь. Не покупая участие в онлайн конференциях и других ивентах/проектах, мы вложили эти средства в своё развитие, о чем не жалеем. Да, мы не смогли выполнить некоторые бизнес задачи, но это лучше, чем получить финансовые потери, выгоревшую команду и всё так же недостигнутые бизнес цели.

Заключение

2020 не приговор :D Вы же сами понимаете, что любые трудности это прежде всего точки роста. Да, бывают очень сложные случаи, но сейчас не об этом. Прошлый год перевернул практически все сферы, и все приспосабливались по-своему, внедряя новые методики, процессы, системы и т. д. Мы с головой ушли во внутреннее развитие команды, и это круто! Основной посыл этой статьи в том, что, независимо от условий, будьте креативными и инициативными. "Что-то не получается, потому что вот этот карантин/удаленка/коллега/поставщик мешает, а так бы мы уже сотни миллионов зарабатывали" - так себе оправдание, а вот попробовать увидеть не следствие, а проблему ситуации, а потом еще и несколько решений предложить. Вот это здорово! Это и помогает передавать опыт другим компаниям или получать его, придумывать новые решения, которые потом выходят на мировые рынки. Безусловно, такой взгляд на ситуации ведет к развитию, что очень ценно в современной действительности.

P.S. Все мероприятия проводились в соответствии с указами и постановлениями губернатора Тульской области. И да, у нас всё хорошо. Мы продолжаем активно и успешно работать. Желаем вам тоже побольше активностей и здоровья!

Подробнее..

Roslyn API, или из-за чего PVS-Studio очень долго проект анализировал

22.04.2021 16:18:43 | Автор: admin

Многие ли из вас использовали сторонние библиотеки при написании кода? Вопрос риторический, ведь без применения сторонних библиотек разработка некоторых продуктов затягивалась бы на очень-очень большое время, потому что для решения каждой проблемы приходилось бы "изобретать велосипед". Однако в использовании сторонних библиотек кроме плюсов имеются и минусы. Один из этих минусов недавно коснулся и анализатора PVS-Studio для C#. Анализатор долгое время не мог закончить анализ большого проекта из-за использования метода SymbolFinder.FindReferencesAsync из Roslyn API в диагностике V3083.

Жизнь в PVS-Studio, как обычно, шла своим чередом. Разрабатывались новые диагностики, улучшался статический анализатор, писались новые статьи. Как вдруг! У одного из пользователей нашего анализатора на его большом проекте в течение дня шёл анализ и никак не мог закончиться. Alarm! Alarm! Свистать всех наверх! И мы свистали, получили дампы от пользователя и начали разбираться в причинах долгого анализа. При подробном изучении проблемы выяснилось, что дольше всех работали 3 C# диагностики. Одной из них оказалась диагностика под номером V3083. К этой диагностике уже и раньше было повышенное внимание, но пора было предпринять конкретные действия. V3083 предупреждает о некорректных вызовах C# событий. Например, в коде:

public class IncorrectEventUse{  public event EventHandler EventOne;    protected void InvokeEventTwice(object o, Eventers args)  {    if (EventOne != null)    {      EventOne(o, args);              EventOne.Invoke(o, args);    }  }}

V3083 укажет на вызовы обработчиков события EventOne в методе InvokeEventTwice. О причинах опасности этого кода вы можете узнать более подробно в документации этой диагностики. Со стороны, логика работы V3083 очень простая:

  • найти вызов события;

  • проверить, корректно ли это событие вызывается;

  • выдать предупреждение, если событие вызывается некорректно.

Из-за этой простоты становится еще интереснее понять причину долгой работы диагностики.

Причина замедления

На самом же деле логика чуть-чуть сложнее. V3083 в каждом файле для каждого типа создает только одно срабатывание анализатора на событие, куда записывает номера всех строк (для навигации в различных плагинах: Visual Studio, Rider, SonarQube), где событие некорректно вызывается. Получается, первым делом необходимо найти все места вызова события. Для подобной задачи в Roslyn API уже имеется метод SymbolFinder.FindReferencesAsync, который и был использован в V3083, чтобы не "изобретать велосипед".

Этот метод советуют использовать во многих руководствах: первое, второе, третье и т. д. Возможно, в каких-то простых случаях скорости работы этого метода и достаточно. Однако, чем больше кодовая база проекта, тем дольше этот метод будет работать. На 100 % мы убедились в этом только после изменения V3083.

Ускорение V3083 после изменения

При изменении кода диагностики или ядра анализатора необходимо проверить, что ничего из того, что раньше работало, не сломалось. Для этого у нас имеются позитивные и негативные тесты на каждую диагностику, юнит тесты для ядра анализатора, а также база open-source проектов (которых уже почти 90 штук). Для чего нам база open-source проектов? На ней мы запускаем наш анализатор для проверки его в "боевых условиях", а также этот прогон служит дополнительной проверкой, что мы ничего не сломали в анализаторе. У нас уже имелся прогон анализатора на этой базе до изменения V3083. Все, что нам осталось сделать, это совершить аналогичный прогон после изменения V3083 и выяснить выигрыш во времени. Результаты нас приятно удивили. Без использования SymbolFinder.FindReferencesAsync в V3083 мы получили ускорение на тестах на 9 %. Если кому-то эти цифры показались незначительными, то вот вам характеристики компьютера, на котором производились замеры:

Думаю, после этого даже самые упертые скептики убедились в масштабах проблемы, которая тихо-мирно жила в диагностике V3083.

Заключение

Пусть всем, кто использует Roslyn API, эта заметка будет предостережением! И вы не допустите наших ошибок. Причем, это касается не только метода SymbolFinder.FindReferencesAsync, но и всех других методов класса Microsoft.CodeAnalysis.FindSymbols.SymbolFinder, которые используют один и тот же механизм.

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

Кроме диагностик мы занялись другими оптимизациями анализатора PVS-Studio, о чем расскажем в следующих статьях и заметках.

Изменение диагностики V3083 пока что не попало в релиз, поэтому версия анализатора 7.12 работает с использованием SymbolFinder.FindReferencesAsync.

Как упоминалось ранее, замедление анализатора было найдено еще в двух C# диагностиках, кроме V3083. Ради интереса, предлагаю читателям оставлять свои предположения о том, какие это диагностики в комментариях. Когда предположений станет больше 50, я приоткрою завесу тайны и назову номера этих диагностик.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. Roslyn API: Why PVS-Studio Was Analyzing the Project So Long.

Подробнее..

Дорожная карта PVS-Studio на 2021 год

08.02.2021 16:12:50 | Автор: admin

PVS-Studio Дорожная карта
В начале года наша команда года провела несколько встреч и выработала внутреннюю карту развития компании и продукта PVS-Studio на 2021 год. Мы решили оформить часть наших планов в виде этой статьи. Неопубликованная часть планов секретна? Нет, но вряд ли нашим читателям будут интересны подробности по планам обучения отдельных сотрудников или когда и какой сервер мы планируем приобрести. Здесь мы опишем основные планы развития самого программного продукта PVS-Studio и его позиционирования.


PVS-Studio как продукт в целом


PVS-Studio является статическим анализатором общего назначения, и именно так мы продолжим его развивать. Однако в этом году мы сделаем уклон в сторону его позиционирования как средства статического тестирования защищённости приложений (Static Application Security Testing, SAST). Для этих целей мы сосредоточимся над поддержкой CWE, OWASP, SEI CERT, MISRA, AUTOSAR.


На данный момент PVS-Studio поддерживает анализ программ на языках C, C++, C#, Java. Также поддерживаются некоторые расширения языка C++, например, C++/CLI и C++/CX. В 2021 году мы не планируем реализовывать поддержку новых языков, но планируем развивать анализатор "вширь". А именно, хочется поддержать дополнительно несколько новых компиляторов (C, C++) для микроконтроллеров и некоторые среды разработки, такие как CLion.


Помимо усовершенствований, которые будут описаны ниже, ядра всех анализаторов (C++, C#, Java) будут развиваться по следующим направлениям:


  1. Поддержка новых версий языков программирования;
  2. Улучшение существующих диагностик с целью уменьшения количества ложных срабатываний;
  3. Реализация новых диагностик общего назначения (GA);
  4. Ручная аннотация функций в популярных библиотеках для увеличения количества обнаруживаемых дефектов;
  5. Развитие внутренних механизмов анализаторов, таких, как анализ потоков данных, символьные вычисления, межпроцедурный и межмодульный анализ и т.д.

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


Примечание. Если вы не знакомы с терминами "анализ потока данных" и "ручная аннотация функций", то предлагаем познакомиться с публикацией "Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей".


Сайт


Новый сайт анализатора


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


Email подписка на новости


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


C++


MISRA C, MISRA C++, AUTOSAR


Будет продолжена поддержка стандартов кодирования MISRA C и MISRA C++. Но, помимо этого, пришло время также поддержать более современный набор правил, описываемый в The AUTOSAR C++14 Coding Guidelines. Этот документ является обновлением стандарта MISRA C++:2008, а также основан на других ведущих стандартах написания кода и исследованиях, проделанных организацией AUTOSAR.


Межмодульный анализ потока данных


Ядра анализатора PVS-Studio для C# и Java умеют в том или ином виде осуществлять межмодульный анализ. По ряду причин реализация межмодульного анализа в C++ анализаторе более сложна и постоянно откладывалась. В 2021 году мы чувствуем силы реализовать межмодульный анализ потока данных. Это позволит находить, например, такие ошибки, когда в функцию передаётся нулевой указатель, и эта функция, реализованная в другом юните трансляции, разыменовывает этот указатель, не выполнив предварительную проверку.


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


SAL


Планируется частично поддержать извлечение дополнительной информации из кода, размеченного с помощью языка аннотирования Microsoft Source-Code (SAL).


Компиляторы для embedded платформ


За прошедшие несколько лет PVS-Studio для C и C++ существенно расширила поддержку различных компиляторов, при этом мы концентрировались, в основном, на компиляторах для платформы ARM. В предстоящем году мы уделим внимание и более экзотическим платформам.


Эльбрус


Мы изучим вопрос возможности портировать ядро C++ анализатора на платформу Эльбрус. Около года назад мы уже думали о таком эксперименте, но компилятор под эту платформу отстаёт от современного стандарта языка C++. Анализатор PVS-Studio пишется с использование последних новшеств языка, так как нам ведь надо самим тренироваться с новыми конструкциями языка C++ :). Но здесь написание кода в таком стиле является минусом. Посмотрим, как обстоят дела в этом году, но пока ничего не обещаем.


CLion


Планируется выпустить плагин для среды разработки CLion. Это кроссплатформенная IDE для C и C++ от компании JetBrains. Кстати, если вы хотите стать бета-тестером этого плагина, то вы можете перейти по этой ссылке и заполнить форму. И мы свяжемся с вами, когда у нас появится первая его реализация.


Сейчас анализатор PVS-Studio для C++ можно использовать на Unix-подобных системах (Linux, macOS) совместно с большим количеством IDE и сборочных систем. Однако интеграция с такими кроссплатформенными IDE осуществляется через стандартные средства самой IDE, обычно в виде загрузки отчёта анализатора в формате предупреждений компилятора. Данный способ интеграции, хотя и вполне достаточный для работы, тем не менее проигрывает интеграции анализатора через расширение (плагин) для IDE, как в случае с плагином PVS-Studio для Visual Studio. Через IDE плагин пользователю доступно множество дополнительных возможностей анализатора, таких, как подавление сообщений, удобная разметка ложно-позитивных срабатываний и т.п.


В связи с тем, что для Unix-подобных систем, в отличии от Windows с её Visual Studio, нет одной наиболее распространённой IDE, до данного момента мы откладывали разработку полноценного плагина под какую-либо из кроссплатформенных IDE для языка C++. Тем не менее, так как сейчас мы видим, что популярность JetBrains CLion среди наших пользователей растёт каждый год, и у нас уже есть поддержка "близких" к этой IDE сред IntelliJ IDEA и Rider, мы решили, что пришло время поддержать и CLion.


CSharp (Хабр не умеет заголовок 2-ого уровня с решёткой в режиме Markdown :)


OWASP


При реализации новых диагностик планируется сосредоточиться на OWASP и особенно OWASP Top 10. На наш взгляд статический анализатор для C# очень сильно выиграет от увеличения количества диагностик из сферы безопасности.


.NET


Мы планируем добавить поддержку проектов на .NET 5, а также работу с .NET 5 SDK. Вместе с этим будет добавлен анализ кода, написанного на C# 9.


Если верить roadmap от Microsoft, выпуск новых версий .NET планируется каждый год. Следовательно, в 2021 планируется выпуск уже .NET 6, поддержку которого мы также хотим добавить.


Учесть особенности C# 8 и C# 9 в старых диагностиках


Хочется провести аудит старых диагностик, с целью посмотреть, насколько они учитывают новшества языка, появившиеся в версиях C# 8 и C# 9. Возможно, некоторые диагностики можно улучшить, и они начнут выдавать срабатывания на коде, использующие самые современные конструкции. Вряд ли таких случаев будет много, но провести такое исследование однозначно будет полезно.


Java


Планируется разметить часть существующих диагностик, чтобы они соответствовали OWASP классификации потенциальных уязвимостей. Также планируется создание новых диагностик, ориентируясь на OWASP и особенно на OWASP Top 10.


Прочее


Visual Studio Code


Возможно, в этом году мы реализуем плагин для Visual Studio Code, что позволит удобно просматривать отчёты, сгенерированные консольной версией анализатора. Сейчас для этого можно использовать утилиту C and C++ Compiler Monitoring UI, входящую в дистрибутив PVS-Studio. Или конвертировать отчёт в HTML формат. Это вполне рабочие варианты, но используя плагин для Visual Studio Code будет и удобнее работать с отчётом, и вносить правки в код.


Выше, в разделе про поддержку CLion, мы также писали про существующую ситуацию с "зоопарком" различных IDE для Unix-образных систем. Сейчас мы видим перспективу того, что именно Visual Studio Code, за счёт своей открытости и модульности, имеет шансы стать де-факто самой универсальной IDE для разработки под большое количество языков, компиляторов и платформ. В это ещё одна причина, по которой мы планируем уделить в предстоящем году внимание именно этой IDE.


Offline мероприятия


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


Мы всегда активно участвовали в различных конференциях и других offline мероприятиях (описание, как прошло первое и второе полугодие 2019 года). В 2020 году почти все такие активности были свёрнуты, и 2021 год может пройти аналогично.


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


Offline мероприятия


Поэтому мы подумали и решили попробовать в 2021 году организовать цикл маленьких собственных offline мероприятий. Это будет что-то среднее между бизнес-обедами и семинарами. Мы будем собирать представителей компаний-клиентов, а также тех, кто пока только рассматривает возможность приобретения лицензии. Мы будем рассказывать о новых возможностях PVS-Studio, о способах их интеграции с различными системами, такими как Jenkins, IncrediBuild, Travis CI, SonarQube. Для разнообразия, возможно будем приглашать и сторонних докладчиков, которые захотят рассказать что-то на тему статического анализа кода. И естественно будут дискуссии, ответы на вопросы и демонстрация, как и что работает.


Для простоты начать мы хотим с Москвы. А если нам понравится, как всё проходит, то мы расширим географию мероприятий. Мероприятия будут бесплатные для участников.


Вопрос к читателям. Есть ли здесь кто-то, кто уже хочет принять участие в таком мероприятии как слушатель? Если да, то просим написать нам, что вы хотели бы присутствовать в качестве слушателя или докладчика. Так мы сможем понять заинтересованность аудитории к таким offline активностям, и сможем лучше спланировать их проведение. Заранее спасибо.


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


  1. Как внедрить статический анализатор кода в legacy проект и не демотивировать команду
  2. Релиз PVS-Studio 7.11: IAR Arm, диагностики, FREE-FREE-FREE-FREE
  3. Релиз PVS-Studio 7.10: OWASP, AUTOSAR, SARIF
  4. Релиз PVS-Studio 7.09
  5. Релиз PVS-Studio 7.08: C# для Linux и macOS, JetBrains Rider
Подробнее..

Как делался новый дизайн сайта PVS-Studio

04.06.2021 18:05:38 | Автор: admin

Сайту PVS-Studio в этом году исполнится 15 лет. Это солидный возраст для любого интернет-ресурса. Далёкий 2006-й в России был признан годом гуманитарных наук. В июне появилась никому не знакомая тогда площадка "Хабрахабр". В ноябре Microsoft официально завершила разработку ОС Windows Vista. И в том же месяце был зарегистрирован домен viva64.com.

10-ый юбилей наш сайт отметил редизайном. С тех пор происходящие изменения касались, скорее, размера ресурса и его функционала, но никак не дизайна. За это время количество статей увеличилось настолько, что пришлось вводить теги для удобной навигации по ним. Также сейчас активно развивается наш YouTube-канал, а это значит, что полнится запас видео и на сайте тоже. Количество страниц растёт с огромной скоростью. А вот удобство пользования остаётся там, где и было.

Настало время масштабных изменений!

С чего мы начали

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

  1. Комментарии к статьям, в которых люди искренне удивлялись, что, ОКАЗВАЕТСЯ, у нас есть триальный ключ. И его можно запросить. И сделать это можно достаточно легко на нашем сайте.

  2. Иногда нам задавали вопросы, где же посмотреть изменения по последнему релизу.

Про версию для Linux спрашивают до сих пор. Хотя, будем честны, вопросов стало всё же меньше.

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

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

  3. Также были сложности с рекламой и посадочными страницами под неё. Не всем было интуитивно понятно, что для запроса триала нужно немного пролистать открывшуюся страницу, заполнить форму (выбрать платформу, версию триала, внести почту, имя, фамилию, добавить комментарий (недавно мы сделали это поле необязательным!)...).

Именно от этого мы и стали отталкиваться при прототипировании нового сайта.

Прототип

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

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

К чему мы пришли:

  1. Форма запроса триала должна быть на каждой странице, и она должна быть проще. Ещё проще! Совсем простой!

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

  3. Возможность скачать дистрибутив должна быть реализована по-разному (если кому-то по тем или иным причинам не подошёл один вариант, он мог бы всегда воспользоваться другим).

  4. Выбор трёх платформ должен быть максимально заметен. Нельзя, чтобы кто-то мог просмотреть наличие версий для Linux и MacOS.

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

  6. Основные страницы сайта должны давать ответы на главные вопросы пользователей.

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

Какие решения мы придумали?

  1. Сократили форму триала до нескольких пунктов и сделали ее сквозной по всему сайту.

  2. Добавили в шапку кнопку "Скачать дистрибутив", закрепили шапку при прокрутке страницы.

  3. Сделали разные формы скачивания дистрибутива на странице продукта и загрузки.

  4. Выделили шаги по скачиванию дистрибутива и первым шагом вынесли выбор платформы (Windows, Linux, MacOS).

  5. Сформулировали кратко 10 основных преимуществ статического анализатора кода PVS-Studio, сделали этот блок раскрывающимся.

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

  7. Создали отдельные шаблоны для публикаций и документации. Однако объединили все одной темой и не стали дробить на разные домены.

Дизайн

Как корабль назовёшь, так он и поплывёт. В случае в веб-ресурсом: какой дизайн сайту придумаешь, такой и жди результат. Действительно, от дизайна зависит многое. Запомнится ресурс или нет. Найдутся все полезные элементы на нём или нет. Сбежит ли пользователь с сайта после загрузки страницы или нет...

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

Как правильно говорят: без внятного ТЗ результат будет... таким же невнятным. Мы учли наши ошибки и расписали максимально подробно все правки, которые хотелось бы внести в полученный вариант. По пунктам. С примерами. Видимо, перестарались. В ответ нам пришло письмо:

Так, спустя месяц, мы остались без дизайнера.

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

Пришли к нашему штатному дизайнеру Галине с повинной. Интересно, почему не пришли сразу? Да потому что Галя всегда завалена другими задачами от нас. Она нам и рисует, и анимирует, и печатает, и обрабатывает, и придумывает... Многовато для одного человека, да?

Но, как бы там ни было, Галя нам не отказала. Взяла стопку наших требований:

  1. Цвета приглушенные, берем из логотипа PVS-Studio. Если оставить наш голубой и разбавить его белым, такой контраст режет глаз. Особенно плакали наши любители тёмных тем. Серый как раз решил их проблему. Да и выделил все элементы, став идеальным фоном.

  2. Единорогов оставляем, но снижаем их градус. Не все, к сожалению, ценят нашего маскота. Особенно банки, особенно серьёзные.

  3. Вместо потерянных единорогов добавляем популярную сейчас геометрию.

  4. И, чтобы не отставать от трендов, разбавляем градиентом (заметили, что у многих компаний после ребрендинга появился градиент? ;) ).

  5. Добавляем иконки, отрисовываем инфографику.

  6. Самый важный пункт делаем красиво!

Конечно, ТЗ было более детальным, но сейчас в статье расписывать всё это не имеет смысла.

Дело пошло. Единороги, геометрия, все элементы выделены. Взгляд падает туда, куда нужно.

Верстка

Думали, на дизайне мы испытали все сложности сотрудничества с внешними исполнителями? Нет, это не так. Верстальщика мы тоже искали со стороны, так как в штате у нас сейчас закоренелый бэкендер. Нашли фронтендера, подписали договор, скинули ТЗ, прототип и макет. Сели ждать.

Прождали 2 недели, просим показать хоть что-нибудь. Получаем ответ, что ещё рано. Ждём дальше.

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

По всем законам жанра мы должны были пойти с повинной к нашему штатному разработчику. Но нет. Бэкендер он и есть бэкендер. Мы всё же бросились на ускоренные поиски нового верстальщика на стороне. Грабли у Вселенной закончились. Мы быстро нашли нового фронтендера. Работа пошла.

Первым делом верстальщик попытался заманить нас на Next.js. Мы почти согласились (скорость прогрузки страниц покоряла). Да и с точки зрения SEO проблема легко решалась (поисковикам отдавался контент в html благодаря SPA). Однако недолгие раздумья привели нас к непреодолимому препятствию: кто же будет поддерживать всю эту красоту? Поэтому мы вернулись к старому доброму html+CSS+js.

Бэкенд

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

Смена домена

Одновременно с новым дизайном нам хотелось перейти на новый домен. Домен, который больше не будет вызывать вопросов. Знали бы вы, как часто нам приходилось рассказывать, почему у нас сайт viva64.

Казалось бы, переход максимально прост: создаём новый сайт на новом домене, в нужный момент настраиваем редиректы с viva64.com на pvs-studio.com. Готово!

Однако поисковые системы внесли небольшие корректировки в наш идеальный план. Ведь если одновременно меняется и домен, и контент, то для них это новый сайт (что логично). А новый сайт слишком долго набирает позиции в выдаче. Поэтому пришлось отсрочить смену дизайна. Первым шагом мы совершили переход на новый домен. Отправили запрос в поисковые системы, получили от них благословение. И только после этого представили на ваш суд редизайн сайта pvs-studio.com.

В завершение

Хотя впереди еще много работы, сайт в первом приближении готов. Он ждёт своих посетителей, ценителей, любителей. А мы ждём ваши комментарии: что стало удобней, а что можно сделать ещё удобнее (уверены, что нет предела совершенству).

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Inna Pristyagina. PVS-Studio's New Website: How We Designed It.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru