Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Третья проверка кода плана Chromium с поддержкой анализатора PVS-Studio

Anna | 25.06.2014 | нет комментариев
Браузер Chromium дюже стремительно прогрессирует. Скажем, когда в 2011 году мы впервой проверили данный план (solution), он состоял из 473 планов. Теперь, он состоит теснее из 1169 планов. Нам было увлекательно, сумели ли разработчики Google сберечь высочайшее качество кода, при такой скорости становления Chromium. Да, сумели.

Chromium

Chromium — веб-браузер с открытым начальным кодом, разработанный компанией Google. На основе Chromium создаётся браузер Google Chrome. На странице “Get the Code” дозволено узнать, как скачать начальный код этого плана.

Немножко всеобщей информации

Прежде мы теснее проверяли план Chromium, о чём имеется две статьи: первая проверка (23.05.2011), вторая проверка (13.10.2011). И всё время находили ошибки. Это тонкий намёк о пользе анализаторов кода.

Теперь (начальный код плана скачен в июле 2013) Chromium состоит из 1169 планов. Всеобщий объем начального кода на языке Си/Си составляет 260 мегабайт. Добавочно к этому дозволено прибавить ещё 450 мегабайт используемых внешних библиотек.

Если взять нашу первую проверку плана Chromium в 2011 году, то дозволено подметить — объем внешних библиотек в целом не изменился. Но код, самого плана значительно подрос c 155 мегабайт до 260 мегабайт.

Ради интереса посчитаем цикломатическую трудность

В анализаторе PVS-Studio есть вероятность поиска функций с огромный цикломатической трудностью. Как правило, такие функции являются кандидатами для рефакторинга. Проверив 1160 планов, мне безусловно стало увлекательно, какой из них дозволено назвать рекордсменом в номинации «самая трудная функция».

Самая максимальная цикломатическая трудность, равная 2782, принадлежит функции ValidateChunkAMD64() в плане Chromium. Но её пришлось дисквалифицировать из состязания. Функция находится в файле validator_x86_64.c, тот, что является автогенерируемым. Жалко. А то был бы эпичный рекордсмен. Я и близко с такой цикломатической трудностью не сталкивался.

Таким образом, первые три места получают следующие функции:

  1. Библиотека WebKit. Функция HTMLTokenizer::nextToken() в файле htmltokenizer.cpp. Цикломатическая трудность 1106.
  2. Библиотека Mesa. Функция _mesa_glsl_lex() в файле glsl_lexer.cc. Цикломатическая трудность 1088.
  3. Библиотека usrsctplib (какой-то безызвестный спортсмен). Функция sctp_setopt() в файле htmltokenizer.cpp. Цикломатическая трудность 1026.

Если кто-то не знает, что такое цикломатическая трудность 1000, то пускай и не знает. Душевное здоровье будет отменнее :) . В всеобщем, много это.

Качество кода

Что я могу сказать о качестве кода плана Chromium? Качество по-бывшему изумительное. Да, как и в любом большом плане, неизменно дозволено обнаружить ошибки. Но если поделить их число на объем кода, их плотность будет жалка. Это дюже отличный код с дюже малым числом ошибок. Вручаю медальку за чистый код. Предыдущая медалька досталась плану Casablanca (C REST SDK) от компании Mcorosoft.
Рисунок 1. Медалька создателям Chromium.
Рисунок 1. Медалька создателям Chromium.

За компанию совместно с Chromium были проверенные входящие в него сторонние библиотеки. Но описывать обнаруженные в них ошибки не увлекательно. Тем больше я просматривал отчёт дюже поверхностно. Нет, я совсем не дрянный человек. Я бы посмотрел на вас, если бы вы испробовали полновесно исследовать отчёт о проверке 1169 планов. То, что я подметил при беглом просмотре, я разместил в базу примеров ошибок. В этой статье я хочу коснуться только тех ошибок, которые поспел подметить в коде самогоChromium (его плагинов и тому сходственного).

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

Анализатор смог прожевать десятки тысяч файлов, всеобщим объемом 710 мегабайт, и не загнулся от этого. Не смотря на то, что план разрабатывается высококвалифицированными разработчиками и проверяется разными инструментами, PVS-Studio всё равно умудрился выявить недостатки. Это восхитительное достижение! И последнее — он сделал это за умное время (около 5 часов) за счёт параллельной проверки (AMD FX-8320/3.50 GHz/eight-core processor, 16.0 GB RAM).

Некоторые из обнаруженных ошибок

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

Подмеченное N1 — опечатки

Vector3dF
Matrix3F::SolveEigenproblem(Matrix3F* eigenvectors) const
{
  // The matrix must be symmetric.
  const float epsilon = std::numeric_limits<float>::epsilon();
  if (std::abs(data_[M01] - data_[M10]) > epsilon ||
      std::abs(data_[M02] - data_[M02]) > epsilon ||
      std::abs(data_[M12] - data_[M21]) > epsilon) {
    NOTREACHED();
    return Vector3dF();
  }
  ....
}

V501 There are identical sub-expressions to the left and to the right of the ‘-’ operator: data_[M02] — data_[M02] matrix3_f.cc 128

Нужно проверить, что матрица размером 3×3 симметрична.
Рисунок 2. Матрица 3x3.
Рисунок 2. Матрица 3×3.

Для этого нужно сравнить следующие элементы:

  • M01 и M10
  • M02 и M20
  • M12 и M21

Скорее каждого, код писался с поддержкой спецтехнологии Copy-Paste. В итоге ячейка M02 сравнивается сама с собой. Вот такой вот комичный класс матрицы.

Иная простая опечатка:

bool IsTextField(const FormFieldData& field) {
  return
    field.form_control_type == "text" ||
    field.form_control_type == "search" ||
    field.form_control_type == "tel" ||
    field.form_control_type == "url" ||
    field.form_control_type == "email" ||
    field.form_control_type == "text";
}

V501 There are identical sub-expressions ‘field.form_control_type == «text»’ to the left and to the right of the ‘||’ operator. autocomplete_history_manager.cc 35

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

Подмеченное N2 — противоположные данные

static void ParseRequestCookieLine(
    const std::string& header_value,
    ParsedRequestCookies* parsed_cookies)
{
  std::string::const_iterator i = header_value.begin();
  ....
  if (*i == '"') {
    while (i != header_value.end() && *i != '"')   i;
  ....
}

V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 500, 501. web_request_api_helpers.cc 500

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

if ( A == 'X' ) {
  while ( .... && A != 'X' ) ....;

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

if (*i == '"') {
    i;
  while (i != header_value.end() && *i != '"')   i;

Подмеченное N3 — неудачное удаление элементов

void ShortcutsProvider::DeleteMatchesWithURLs(
  const std::set<GURL>& urls)
{
  std::remove_if(matches_.begin(),
                 matches_.end(),
                 RemoveMatchPredicate(urls));
  listener_->OnProviderUpdate(true);
}

V530 The return value of function ‘remove_if’ is required to be utilized. shortcuts_provider.cc 136

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

  • Взамен одного из ‘return’, должно быть написано ‘continue’. Но тоже подозрительно.
  • Скорее каждого, перед последним ‘return’ пропущено какое-то условие.

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

scoped_ptr<ActionInfo> ActionInfo::Load(....)
{
  ....
  for (base::ListValue::const_iterator iter = icons->begin();
        iter != icons->end();   iter)
  {
    std::string path;
    if (....);
      return scoped_ptr<ActionInfo>();
    }

    result->default_icon.Add(....);
    break;
  }
  ....
}

V612 An unconditional ‘break’ within a loop. action_info.cc 76

const BluetoothServiceRecord* BluetoothDeviceWin::GetServiceRecord(
    const std::string& uuid) const
{
  for (ServiceRecordList::const_iterator iter =
         service_record_list_.begin();
       iter != service_record_list_.end();
         iter)
  {
    return *iter;
  }
  return NULL;
}

V612 An unconditional ‘return’ within a loop. bluetooth_device_win.cc 224

Подмеченное N9 — неинициализированные переменные

HRESULT IEEventSink::Attach(IWebBrowser2* browser) {
  DCHECK(browser);
  HRESULT result;
  if (browser) {
    web_browser2_ = browser;
    FindIEProcessId();
    result = DispEventAdvise(web_browser2_, &DIID_DWebBrowserEvents2);
  }
  return result;
}

V614 Potentially uninitialized variable ‘result’ used. ie_event_sink.cc 240

Если указатель ‘browser’ равен нулю, то функция вернет неинициализированную переменную.

Иной фрагмент кода:

void SavePackage::GetSaveInfo() {
  ....
  bool skip_dir_check;
  ....
  if (....) {
    ....->GetSaveDir(...., &skip_dir_check);
  }
  ....
  BrowserThread::PostTask(BrowserThread::FILE,
                          FROM_HERE,
                          base::Bind(..., skip_dir_check, ...));
}

V614 Potentially uninitialized variable ‘skip_dir_check’ used. Consider checking the fifth actual argument of the ‘Bind’ function. save_package.cc 1326

Переменная ‘skip_dir_check’ может остаться неинициализированной.

Подмеченное N10 — выравнивание кода не соответствует логике его работы

void OnTraceNotification(int notification) {
  if (notification & TraceLog::EVENT_WATCH_NOTIFICATION)
      event_watch_notification_;
    notifications_received_ |= notification;
}

V640 The code’s operational logic does not correspond with its formatting. The statement is indented to the right, but it is always executed. It is possible that curly brackets are missing. trace_event_unittest.cc 57

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

Вот ещё пара мест с ДЮЖЕ подозрительным выравниванием кода:

  • nss_memio.c 152
  • nss_memio.c 184

Подмеченное N11 — проверка указателя позже new

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

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

Разглядим пример:

static base::DictionaryValue* GetDictValueStats(
    const webrtc::StatsReport& report)
{
  ....
  DictionaryValue* dict = new base::DictionaryValue();
  if (!dict)
    return NULL;

  dict->SetDouble("timestamp", report.timestamp);

  base::ListValue* values = new base::ListValue();
  if (!values) {
    delete dict;
    return NULL;
  }
  ....
}

V668 There is no sense in testing the ‘dict’ pointer against null, as the memory was allocated using the ‘new’ operator. The exception will be generated in the case of memory allocation error. peer_connection_tracker.cc 164

V668 There is no sense in testing the ‘values’ pointer against null, as the memory was allocated using the ‘new’ operator. The exception will be generated in the case of memory allocation error. peer_connection_tracker.cc 169

Первая проверка «if (!dict) return NULL;» скорее каждого урона не принесёт. А вот со 2-й проверкой дело обстоит дрянней. Если при создании объекта с поддержкой «new base::ListValue()» не удастся выделить память, то будет сгенерировано исключение ‘std::bad_alloc’. На этом работа функции GetDictValueStats() завершится.

В итоге, вот данный код:

if (!values) {
  delete dict;
  return NULL;
}

никогда не уничтожит объект, адрес которого хранится в переменной ‘dict’.

Верным решением тут будет проведение рефакторинга кода и применение мудрых указателей (smart pointers).

Разглядим иной фрагмент кода:

bool Target::Init() {
{
  ....
  ctx_ = new uint8_t[abi_->GetContextSize()];

  if (NULL == ctx_) {
    Destroy();
    return false;
  }
  ....
}

V668 There is no sense in testing the ‘ctx_’ pointer against null, as the memory was allocated using the ‘new’ operator. The exception will be generated in the case of memory allocation error. target.cc 73

В случае ошибки выделения памяти не будет вызвана функция Destroy().

Дальше писать про это не увлекательно. Я легко приведу список других подмеченных мною допустимо опасных мест в коде:

  • ‘data’ pointer. target.cc 109
  • ‘page_data’ pointer. mock_printer.cc 229
  • ‘module’ pointer. pepper_entrypoints.cc 39
  • ‘c_protocols’ pointer. websocket.cc 44
  • ‘type_enum’ pointer. pin_base_win.cc 96
  • ‘pin_enum’ pointer. filter_base_win.cc 75
  • ‘port_data’. port_monitor.cc 388
  • ‘xcv_data’ pointer. port_monitor.cc 552
  • ‘monitor_data’. port_monitor.cc 625
  • ‘sender_’ pointer. crash_service.cc 221
  • ‘cache’ pointer. crash_cache.cc 269
  • ‘current_browser’ pointer. print_preview_dialog_controller.cc 403
  • ‘udp_socket’ pointer. network_stats.cc 212
  • ‘popup_’ pointer. try_chrome_dialog_view.cc 90

Подмеченное N12 — тесты, которые нехорошо тестируют

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

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

std::string TestAudioConfig::TestValidConfigs() {
  ....
  static const uint32_t kRequestFrameCounts[] = {
    PP_AUDIOMINSAMPLEFRAMECOUNT,
    PP_AUDIOMAXSAMPLEFRAMECOUNT,
    1024,
    2048,
    4096
  };
  ....
  for (size_t j = 0;
    j < sizeof(kRequestFrameCounts)/sizeof(kRequestFrameCounts);
    j  ) {
  ....
}

V501 There are identical sub-expressions ‘sizeof (kRequestFrameCounts)’ to the left and to the right of the ‘/’ operator. test_audio_config.cc 56

В цикле выполнятся только один тест. Оплошность в том, что «sizeof(kRequestFrameCounts)/sizeof(kRequestFrameCounts)» равняется единице. Верное выражение: «sizeof(kRequestFrameCounts)/sizeof(kRequestFrameCounts[0])».

Иной ложный тест:

void DiskCacheEntryTest::ExternalSyncIOBackground(....) {
  ....
  scoped_refptr<net::IOBuffer> buffer1(new net::IOBuffer(kSize1));
  scoped_refptr<net::IOBuffer> buffer2(new net::IOBuffer(kSize2));
  ....
  EXPECT_EQ(0, memcmp(buffer2->data(), buffer2->data(), 10000));
  ....
}

V549 The first argument of ‘memcmp’ function is equal to the second argument. entry_unittest.cc 393

Функция «memcmp()» сопоставляет буфер сам с собой. В итоге тест не исполняет требуемую проверку. Видимо, тут должно быть:

EXPECT_EQ(0, memcmp(buffer1->data(), buffer2->data(), 10000));

А вот тест, тот, что может невзначай сломать другие тесты:

static const int kNumPainters = 3;

static const struct {
  const char* name;
  GPUPainter* painter;
} painters[] = {
  { "CPU CSC   GPU Render", new CPUColorPainter() },
  { "GPU CSC/Render", new GPUColorWithLuminancePainter() },
};

int main(int argc, char** argv) {
  ....
  // Run GPU painter tests.
  for (int i = 0; i < kNumPainters; i  ) {
    scoped_ptr<GPUPainter> painter(painters[i].painter);
  ....  
}

V557 Array overrun is possible. The value of ‘i’ index could reach 2. shader_bench.cc 152

Допустимо, прежде массив ‘painters’ состоял из 3 элементов. Сейчас их только два. А значение константы ‘kNumPainters’ осталось равно 3.

Некоторые другие места в тестах, которые, как мне кажется, заслуживают внимания:

V579 The string function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the second argument. syncable_unittest.cc 1790

V579 The string function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the second argument. syncable_unittest.cc 1800

V579 The string function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the second argument. syncable_unittest.cc 1810

V595 The ‘browser’ pointer was utilized before it was verified against nullptr. Check lines: 5489, 5493. testing_automation_provider.cc 5489

V595 The ‘waiting_for_.get()’ pointer was utilized before it was verified against nullptr. Check lines: 205, 222. downloads_api_unittest.cc 205

V595 The ‘pNPWindow’ pointer was utilized before it was verified against nullptr. Check lines: 34, 35. plugin_windowed_test.cc 34

V595 The ‘pNPWindow’ pointer was utilized before it was verified against nullptr. Check lines: 16, 20. plugin_window_size_test.cc 16

V595 The ‘textfield_view_’ pointer was utilized before it was verified against nullptr. Check lines: 182, 191. native_textfield_views_unittest.cc 182

V595 The ‘message_loop_’ pointer was utilized before it was verified against nullptr. Check lines: 53, 55. test_flash_message_loop.cc 53

Подмеченное N13 — Функция с переменным числом доводов

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

Пример:

DWORD GetLastError(VOID);

void TryOpenFile(wchar_t *path, FILE *output) {
  wchar_t path_expanded[MAX_PATH] = {0};
  DWORD size = ::ExpandEnvironmentStrings(
    path, path_expanded, MAX_PATH - 1);
  if (!size) {
    fprintf(output,
            "[ERROR] Cannot expand "%S". Error %S.rn",
            path, ::GetLastError());
  }
  ....
}

V576 Incorrect format. Consider checking the fourth actual argument of the ‘fprintf’ function. The pointer to string of wchar_t type symbols is expected. fs.cc 17

Если переменная ‘size’ равна нулю, программа попытается записать в файл текстовое сообщение. Но это сообщение, скорее каждого, будет содержать в конце биллиберду. Больше того, данный код может привести кaccess violation.

Для записи использует функция fprintf(). Это функция не контролирует типы своих доводов. Она ждет, что последним доводом должен быть указатель на строку. Но на самом деле, фактическим доводом является число (код ошибки). Это число будет преобразован в адрес и как дальше поведёт себя программа, неведомо.

Незамеченное

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

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

bool ManagedUserService::UserMayLoad(
  const extensions::Extension* extension,
  string16* error) const
{
  if (extension_service &&
      extension_service->GetInstalledExtension(extension->id()))
    return true;

  if (extension) {
    bool was_installed_by_default =
      extension->was_installed_by_default();
    .....
  }
}

V595 The ‘extension’ pointer was utilized before it was verified against nullptr. Check lines: 277, 280. managed_user_service.cc 277

В начале указатель ‘extension’ разыменовывается в выражении «extension->id()». После этого данный указатель проверяют на равенство нулю.

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

Вот ещё пример диагностики, которую я предпочёл не подметить:

bool WebMClusterParser::ParseBlock(....)
{
  int timecode = buf[1] << 8 | buf[2];
  ....
  if (timecode & 0x8000)
    timecode |= (-1 << 16);
  ....
}

V610 Undefined behavior. Check the shift operator ‘<<. The left operand ‘-1′ is negative. webm_cluster_parser.cc 217

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

О неверных срабатываниях

Мне Зачастую задают вопрос:

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

И я неизменно не знаю, что сходу ответить на данный вопрос. У меня есть два противоположных результата: 1-й — много, 2-й — немного. Все зависит, как подойти к рассмотрению выданного списка сообщения. Теперь на примере Chromium я испробую объяснить суть такой противоречивости.

Анализатор PVS-Studio выдал 3582 предупреждений первого яруса (комплект правил всеобщего назначения GA). Это дюже много. И в большинстве этих сообщений являются ложными. Если подойти «в лоб» и начать сразу просматривать каждый список, то это дюже стремительно надоест. И ощущение будет страшное. Одни сплошные однотипные ложные срабатывания. Ничего увлекательного не попадается. Дрянной инструмент.

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

Впрочем так не неизменно получается. Так не получилось и с Chromium. Скажем, большое число неверных сообщений появилось из-за макроса ‘DVLOG’. Данный макрос что-то логирует. Он написан хитроумно и PVS-Studio ложно посчитал, что в нем может быть оплошность. От того что, макрос дюже энергично применяется, неверных сообщений получилось дюже много. Дозволено сказать, что сколько раз применяется макрос DVLOG, столько неверных предупреждений попадет в отчет. А именно, о макросе было выдано около 2300 неверных сообщений «V501 There are identical sub-expressions…..».

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

//-V:DVLOG:501

Глядите, таким простым действием мы вычтем из 3582 сообщений, 2300 неверных. Мы сразу отсеяли 65% сообщений. И сейчас нам не нужно их напрасно просматривать.

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

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

Напутствие читателям

Пользуясь случаем, хочу передать здравствуй родителям. Ой нет, это что-то не то. Пользуясь случаем, хочу передать здравствуй программистам и напомнить, что:

  • Результат на вопрос «Вы осведомили о обнаруженных в плане ошибках разработчикам?», находится в заметке “Результаты на вопросы, которые Зачастую задают позже прочтения наших статей“.
  • С нами отменнее каждого связаться и задать вопросы, применяя форму обратной связи на нашем сайте. Умоляю не применять для этого twitter, комментарии к статьям где-то на сторонних сайтах и так дальше.
  • Приглашаю присоединиться к нашему твиттеру: @Code_Analysis. Я непрерывно собираю и публикую увлекательные ссылки по тематике программирования и языку Си .

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB