Chromium: шестая проверка проекта и 250 багов

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

    Андрей Карпов, PVS-Studio

    Предыстория


    Меня зовут Андрей Карпов, и я являюсь евангелистом статического анализа в целом и инструмента статического анализа PVS-Studio в частности. Впрочем, термин «технический евангелист» уже устаревает и на его смену приходит «developer advocate».

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

    Стоит отметить, что это далеко не первая статья, посвященная проекту Chromium. Мои предыдущие публикации:


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


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


    Каждый раз, когда мы проверяли проект, в нём находилось большое количество ошибок. Новая проверка не стала исключением. Более того, поскольку анализатор PVS-Studio всё лучше выявляет ошибки, я просто поначалу не знал, что со всеми ними делать. Бегло просматривая отчёт, я выписал около 250 ошибок и задумался. Описать все 250 ошибок в одной статье? Это будет какой-то ужас: длинно, скучно и неинтересно. Разбить описание не несколько статей? Лучше тоже не станет, вместо одной скучной статьи появится несколько.

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

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

    Проверка проекта


    В самом конце 2017 года мой коллега Святослав Размыслов скачал исходные коды проекта Chromium, как-то поколдовал с ними и выдал мне сгенерированный проект для Visual Studio и отчёт PVS-Studio. К сожалению, работать с решением в среде Visual Studio оказалось невозможно. Среда не пережила решение, включающее в себя 5021 проект.

    5021 проект в решении

    Всё неимоверно тормозит, и вообще среда через некоторое время падает. Поэтому я изучал отчёт, используя PVS-Studio Standalone. Это, конечно, не так удобно, как использовать привычную мне среду Visual Studio, но вполне приемлемо.

    Надо понимать, что проект Chromium — это большой проект. Даже не так сказал. Это БОЛЬШОЙ ПРОЕКТ.

    Проект Chromium и используемые в нём библиотеки состоят из 114 201 файлов на языке C и C++. Количество строк кода 30 263 757. Из них комментарии составляют 16%.

    Только то, что PVS-Studio может проверить проект такого размера, уже является достижением :).

    Что я нашёл


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

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

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

    Ссылка на файл с описанием замеченных дефектов: chromium.txt.

    Почему я не осилил просмотреть отчёт внимательно?


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

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

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

    Итак, анализатор PVS-Studio выдал предупреждение на один из файлов проекта V8:

    V547 CWE-570 Expression 'truncated' is always false. objects.cc 2867

    Найдена ошибка, или это ложное срабатывание? Попробуйте самостоятельно понять в чём тут дело. Я добавил комментарий "// <=" туда, куда указывает анализатор.

    void String::StringShortPrint(StringStream* accumulator,
                                  bool show_details) {
      int len = length();
      if (len > kMaxShortPrintLength) {
        accumulator->Add("<Very long string[%u]>", len);
        return;
      }
    
      if (!LooksValid()) {
        accumulator->Add("<Invalid String>");
        return;
      }
    
      StringCharacterStream stream(this);
    
      bool truncated = false;
      if (len > kMaxShortPrintLength) {
        len = kMaxShortPrintLength;
        truncated = true;
      }
      bool one_byte = true;
      for (int i = 0; i < len; i++) {
        uint16_t c = stream.GetNext();
    
        if (c < 32 || c >= 127) {
          one_byte = false;
        }
      }
      stream.Reset(this);
      if (one_byte) {
        if (show_details)
          accumulator->Add("<String[%u]: ", length());
        for (int i = 0; i < len; i++) {
          accumulator->Put(static_cast<char>(stream.GetNext()));
        }
        if (show_details) accumulator->Put('>');
      } else {
        // Backslash indicates that the string contains control
        // characters and that backslashes are therefore escaped.
        if (show_details)
          accumulator->Add("<String[%u]\\: ", length());
        for (int i = 0; i < len; i++) {
          uint16_t c = stream.GetNext();
          if (c == '\n') {
            accumulator->Add("\\n");
          } else if (c == '\r') {
            accumulator->Add("\\r");
          } else if (c == '\\') {
            accumulator->Add("\\\\");
          } else if (c < 32 || c > 126) {
            accumulator->Add("\\x%02x", c);
          } else {
            accumulator->Put(static_cast<char>(c));
          }
        }
        if (truncated) {                      // <=
          accumulator->Put('.');
          accumulator->Put('.');
          accumulator->Put('.');
        }
        if (show_details) accumulator->Put('>');
      }
      return;
    }

    Разобрались? Сложно?

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

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

    Итак, анализатор утверждает, что условие if (truncated) всегда ложно. Давайте сократим функцию, оставив самую суть:

    void F() {
      int len = length();
      if (len > kMaxShortPrintLength)
        return;
    
      bool truncated = false;
    
      if (len > kMaxShortPrintLength)
        truncated = true;
    
      if (truncated) {                      // <=
        accumulator->Put('.');
        accumulator->Put('.');
        accumulator->Put('.');
      }
    }

    Флаг truncated должен быть равен true, если текст слишком длинный, то есть если выполняется условие if (len > kMaxShortPrintLength). Однако, если текст слишком длинный, то выше происходит выход из функции.

    Именно поэтому truncated всегда равен false и три точки в конце не будут добавлены.

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

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

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

    void ResourcePrefetcher::OnReadCompleted(net::URLRequest* request,
                                             int bytes_read) {
      DCHECK_NE(net::ERR_IO_PENDING, bytes_read);
    
      if (bytes_read <= 0) {
        FinishRequest(request);
        return;
      }
    
      if (bytes_read > 0)
        ReadFullResponse(request);
    }

    Предупреждение PVS-Studio: V547 CWE-571 Expression 'bytes_read > 0' is always true. resource_prefetcher.cc 308

    В отличие от предыдущего случая, здесь всё просто. Анализатор точно прав, заявляя, что второе условие всегда истинно.

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

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

    void ResourcePrefetcher::OnReadCompleted(net::URLRequest* request,
                                             int bytes_read) {
      DCHECK_NE(net::ERR_IO_PENDING, bytes_read);
    
      if (bytes_read <= 0)
        FinishRequest(request);
      else
        ReadFullResponse(request);
    }

    Анализатор здесь промолчит. При этом код стал короче, проще и понятней.

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

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

    Пример скучной избыточной проверки:

    template <typename ConditionT, typename ActionT>
    std::unique_ptr<DeclarativeRule<ConditionT, ActionT>>
    DeclarativeRule<ConditionT, ActionT>::Create(....) {
      ....
      bool bad_message = false;                                 // <=
      std::unique_ptr<ActionSet> actions = ActionSet::Create(
          browser_context, extension, rule->actions, error,
          &bad_message);                                        // <=
      if (bad_message) {                                        // <=
        *error = "An action of a rule set had an invalid "
                 "structure that should have been caught "
                 "by the JSON validator.";
        return std::move(error_result);
      }
      if (!error->empty() || bad_message)                       // <=
        return std::move(error_result);
      ....
    }

    Предупреждение PVS-Studio: V560 CWE-570 A part of conditional expression is always false: bad_message. declarative_rule.h 472

    Условие:

    if (!error->empty() || bad_message)

    можно упростить до:

    if (!error->empty())

    Ещё есть вариант переписать код вот так:

      if (bad_message) {
        *error = "An action of a rule set had an invalid "
                 "structure that should have been caught "
                 "by the JSON validator.";
      }
      if (!error->empty() || bad_message)
        return std::move(error_result);

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

    Процент ложных срабатываний


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

    Если настроить анализатор PVS-Studio, то можно ожидать 10-15% ложных срабатываний. Пример подобной настройки описан в статье "Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний".

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

    Настройка, однозначно, возможна и даст хороший результат. Например, около ПОЛОВИНЫ всех ложных срабатываний связано с использованием в коде макроса DCHECK.

    Вот как выглядит этот макрос:

    #define LAZY_STREAM(stream, condition)                            \
    !(condition) ? (void) 0 : ::logging::LogMessageVoidify() & (stream)
    
    #define DCHECK(condition)                                         \
     LAZY_STREAM(LOG_STREAM(DCHECK), !ANALYZER_ASSUME_TRUE(condition))\
       << "Check failed: " #condition ". "

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

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

    bool Value::Equals(const Value* other) const {
      DCHECK(other);
      return *this == *other;
    }

    PVS-Studio сообщает: V1004 CWE-476 The 'other' pointer was used unsafely after it was verified against nullptr. Check lines: 621, 622. values.cc 622

    С точки зрения анализатора, выполняется проверка указателя other на равенство nullptr. Но независимо от того, является other нулевым указателем или нет, далее произойдёт его разыменование. Подобные действия анализатор считает подозрительными.

    Макрос DCHECK является разновидностью assert-макросов. Но если про assert анализатор знает, то что такое DCHECK – не понимает. Чтобы лучше пояснить происходящее, напишу псевдокод:

    bool Equals(T* ptr) const
    {
      if (!ptr)
        LogMessage();
      return *this == *ptr;
    }

    Вот как видит код анализатор PVS-Studio. Вначале указатель проверяется на равенство nullptr. Если указатель нулевой, то вызывается функция LogMessage. При этом функция не помечена как не возвращающая управление. Значит, независимо от того, является ли ptr нулевым указателем или нет, функция продолжит выполняться.

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

    Кстати, такая реализация макроса сбивает с толку не только PVS-Studio. Поэтому для анализатора, встроенного в Visual Studio, сделана специальная «подпорка»:

    #if defined(_PREFAST_) && defined(OS_WIN)
    // See comments on the previous use of __analysis_assume.
    
    #define DCHECK(condition)                    \
      __analysis_assume(!!(condition)),          \
          LAZY_STREAM(LOG_STREAM(DCHECK), false) \
              << "Check failed: " #condition ". "
    
    #define DPCHECK(condition)                    \
      __analysis_assume(!!(condition)),           \
          LAZY_STREAM(PLOG_STREAM(DCHECK), false) \
              << "Check failed: " #condition ". "
    #else  // !(defined(_PREFAST_) && defined(OS_WIN))

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

    Другие публикации


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

    1. Красивый Chromium и корявый memset.
    2. Оператор break и fallthrough.
    3. Chromium: утечки памяти.
    4. Chromium: опечатки.
    5. Chromium: использование недостоверных данных.
    6. Почему важно проверять, что вернула функция malloc.
    7. Chromium: другие ошибки .



    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Chromium: About the Sixth Check of the Project.
    PVS-Studio 281,87
    Ищем ошибки в C, C++ и C# на Windows, Linux, macOS
    Поделиться публикацией
    Похожие публикации
    Комментарии 36
    • 0
      Некоторые (я и сам иногда так делаю), сразу смотрят комментарии, чтобы понять, стоящая статья или нет. Для них скажу, что суть статьи в этом. А подробности, будут чуть позже.
      • 0

        А, то есть пока не бежать чинить?:)

        • +1
          Может ли Ахилес обогнать черепаху?

          В смысле пока вы эти будете чинить, мы еще опубликуем статью :-).
          • 0
            О, я могу дать ответ на такой вопрос, как это ни странно :). Догонит! Там как раз всё. Даже чуть больше, чем будет в статьях.
          • +1
            Чинить уже можно. Для этого и дал ссылку. А статьи будут не только описывать ошибки и содержать филосовские размышления.
            • +4
              Баг с truncate судя по всему сидит долго-долго… Что-то я даже не знаю как его чинить…

              И гитхаб не хочет git blame показывать, чтоб потыкать автора палочкой :D
        • 0
          ---а его смену приходит «developer advocate».

          В контексте специализации PVS-Studio я бы сказал — «следователь».

          ---Проект Chromium и используемые в нём библиотеки состоят из 114 201 файлов на языке C и C++. Количество строк кода 30 263 757.

          Мне доводилось рабатоть с проектами больше чем 40 мег кода. И оба прекрасно жили в VC 6.0-2005. Может количество проектов в солюшине зашкаливает?

          В 2010 я вашим анализатором гонял Blackberry Server. Даже прикупил 2 лицензии тогда.
          • +1
            Вы путаете количество строк кода и количество мегабайт кстати похоже.
            • +5
              Примечание. Если говорить о Chromium (+библиотеки), то размер C++ кода: 1 189 654 629 байт. Т.е. больше Гигабайта. Сильно больше 40 мегабайт. :)
              • 0
                Опечатался — более 40 гигабайт конечно.

                Были отдельные файлы больше мегaбайта.
                Код тела основного потока сервера — процедура более 10 тыщ строк — пришлось просить Perforce поправить ихной продукт, что мог работать с такими файлами. Компиляция проекта шла порядка 5 часов.
            • 0
              Я с хромиумом работаю, да, там очень много проектов в солюшене и потом ещё студия пытается пройтись по зависимостям. Кое-как можно загрузить в VS основу(/chrome/browser) без third_party. Ну и приходится грузить только солюшены нужных частей. CLion тоже не работает нормально даже если память увеличить.
              • 0
                Если прикинуть что одна строчка в среднем занимает около 40 символов, то размер кода выходит > 1 GiB
              • 0
                В 2010 я вашим анализатором гонял Blackberry Server. Даже прикупил 2 лицензии тогда.


                Ээээ… Константин? RIM?
                • 0
                  Да.
                  • +6
                    Ха! Вот так встреча, очень рад! Константин, давно не виделись. И у нас все ходы записаны. Обалдеть, 2007 год…

                    From: Konstantin ... [mailto: ... @rim.com]
                    Sent: Friday, March 2, 2007 6:54 PM
                    To: Evgeniy Ryzhkov <...@viva64.com>
                    Subject: RE: Viva64


                    Уже и RIM нету…

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

                    Времена…
                • 0
                  Флаг truncated должен быть равен true, если текст слишком длинный, то есть если выполняется условие if (len > kMaxShortPrintLength).

                  Я это понял, глядя глазами в код в течение минуты. Но ведь наверняка у вас на условии if (len > kMaxShortPrintLength) есть ещё одна сработка вроде "expression 'len > kMaxShortPrintLength' is always false". Разве нет? Если так, то сразу становится очевидно, что вторая сработка — следствие первой, надо просто в каждом методе смотреть предупреждения по порядку.

                  • 0
                    Вам бы логотип поменять, т.к. он создаёт негативный образ для продукта.
                    На футболке у Андрея изображен единорог который радугой блюёт на PVS-Studio.

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

                    Я понимаю, что у вас крутой продукт, и искренне желаю вам успехов, но этим логотипом часть клиентов вы от себя отпугиваете.
                    Качественный продукт — это не только хорошее содержание, но и оформление.
                    Стал бы iPhone таким популярным, если бы у него на задней панели из яблока снизу выливалась цветная радуга на название.
                    • +7
                      Стал бы iPhone таким популярным, если бы у него на задней панели из яблока снизу выливалась цветная радуга на название.
                      Не знаю, что ответить. У меня и iPhone такой. :)
                      • 0
                        Для себя рисовать можно что угодно, но для компании надо делать чтобы нравилось пользователям.
                        Все-таки логотип это лицо компании и он не должен вызывать отторжения.

                        Вот на этой толстовке не плохо, только надпись PVS-Studio плохо читается.
                        Ещё у вас логотип CppCat классный был.
                        • +5
                          Ещё у вас логотип CppCat классный был.
                          Вот только проект CppCat закрыт, а PVS-Studio продолжает жить :).

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

                          А PVS-Studio с радужным единорогом, и по мнению многих неправильной стратегией рекламы, лицензирования и т.д., продолжает жить и привлекать новых пользователей ;). Жизнь она такая… Да…
                          • 0
                            К сожалению, наш директор придерживается такого же подхода.
                            Говорит, что прикрутить PVS-Studio к проекту было бы правильно, но доход это не увеличит. Поэтому не будем увеличивать расходы на лицензии и ограничимся пока тестами.
                            • +6
                              PVS-Studio увеличивает доход только нам. Всем остальным PVS-Studio сокращает расходы.
                      • +2
                        На футболке у Андрея изображен единорог который радугой блюёт на PVS-Studio.
                        Единорог блюёт на травку, а травка кишит bug'ами…
                      • –2

                        Q: Купит ли Гугл PVS, чтобы исправить ошибки в Chrome?
                        A: Вряд ли, потому что Гугл настолько крут, что он, вероятно, мог бы создать анализатор, превосходящий PVS-Studio по качеству поиска ошибок в коде Chrome. Но раз это до сих пор не сделано, значит приоритета исправлять мелкие (или не очень) баги в коде Chrome у Гугла нет.
                        Мне это как-то так видится.

                        • +2
                          Неправильно видится. Новые диагностики добавляются в clang регулярно.

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

                          Среагировать на намёк можно, конечно, но это уже выходит за рамки задачи «купить XXX лицензий».
                        • +3
                          Первая статья цикла: Красивый Chromium и корявый memset.
                        • +1
                          • 0
                            Попробуйте самостоятельно понять в чём тут дело. Я добавил комментарий "// <=" туда, куда указывает анализатор.

                            truncated = true никогда не присвается. Блок с присвоением дублирует предыдущий, в котором уход на return.
                            P.S. О, автор сам пояснил абзацем ниже свою загадку.

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

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