Для получения триального ключа
заполните форму ниже
Team License (базовая версия)
Enterprise License (расширенная версия)
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
GBP
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

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

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

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
>
Самая опасная функция в мире С/С++

Самая опасная функция в мире С/С++

03 Дек 2015

Проверяя много лет различные C/C++ проекты, я заявляю: самая неудачная и опасная функция - memset(). При использовании функции memset() допускают наибольшее количество ошибок, в сравнении с использованием других функций. Я понимаю, что мой вывод вряд ли потрясёт основы мироздания или невероятно ценен. Однако я думаю, читателям будет интересно узнать, почему я пришел к такому заключению.

0360_The_most_dangerous_function_memset_ru/image1.png

Здравствуйте

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

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

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

Например, одним из интересных наблюдений было, что программисты допускают ошибки при Copy-Paste чаще всего в самом конце. На эту тему предлагаю вниманию статью "Эффект последней строки".

Новое наблюдение

Теперь у меня есть ещё одно интересное наблюдение. Используя те или иные функции, программисты могут допускать ошибки. При этом вероятность допущения ошибки зависит от используемой функции. Другими словами, какие-то функции провоцируют ошибки, а какие-то - нет.

Так вот, я готов назвать функцию, при использовании которой есть наибольшая вероятность сесть в лужу.

Итак, победитель на глючность - функция memset!

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

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

Всего у меня в базе 9055 ошибок. Это те ошибки, которые умеет находить анализатор PVS-Studio. Понятно, что он умеет далеко не всё. Однако большое количество найденных ошибок позволяет мне быть уверенным в своих выводах. Так вот, я посчитал, что с использованием функции memset() связано 329 ошибок.

Итого, около 3,6% ошибок в базе связано с функцией memset(). Это много!

Примеры

Давайте рассмотрим некоторые типовые примеры ошибок. Рассматривая их, я думаю, вы согласитесь, что с функцией memset() что-то не так. Она притягивает зло.

Для начала освежим в памяти как объявлена эта функция:

void * memset ( void * ptr, int value, size_t num );

  • ptr - Pointer to the block of memory to fill.
  • value - Value to be set. The value is passed as an int, but the function fills the block of memory using the unsigned char conversion of this value.
  • num - Number of bytes to be set to the value. 'size_t' is an unsigned integral type.

Пример N1 (проект ReactOS)

void
Mapdesc::identify( REAL dest[MAXCOORDS][MAXCOORDS] )
{
  memset( dest, 0, sizeof( dest ) );
  for( int i=0; i != hcoords; i++ )
    dest[i][i] = 1.0;
}

Ошибка в том, что в C и в C++ нельзя передавать массивы по значению (подробнее). Аргумент 'dest' является не чем иным как обыкновенным указателем. Поэтому оператор sizeof() вычисляет размер указателя, а не массива.

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

Пример N2 (проект Wolfenstein 3D)

typedef struct cvar_s {
  char *name;
  ...
  struct cvar_s *hashNext;
} cvar_t;

void Cvar_Restart_f( void ) {
  cvar_t  *var;
  ...
  memset( var, 0, sizeof( var ) );
  ...
}

Похожая ошибка. Допущена она скорее всего по невнимательности. Переменная 'var' является указателем. А значит memset() вновь обнулит только часть структуры. На практике будет обнулён только член 'name'.

Пример N3 (проект SMTP Client)

void MD5::finalize () {
  ...
  uint1 buffer[64];
  ...
  // Zeroize sensitive information
  memset (buffer, 0, sizeof(*buffer));
  ...
}

Очень распространенный паттерн ошибки, про который тем не менее осведомлено мало программистов. Дело в том, что функция memset() будет удалена компилятором. Буфер после вызова memset() более не используется. И компилятор в целях оптимизации удаляет вызов функции. С точки зрения языка C/C++ это не оказывает никакого влияния на поведение программы. Это действительно так. То, что приватная информация останется в памяти, никак не повлияет на работу программы.

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

Пример N4 (проект Notepad++)

#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
...
DockingManager::DockingManager()
{
  ...
  memset(_iContMap, -1, CONT_MAP_MAX);
  ...
}

Часто забывают, что третий аргумент функции memset() это не количество элементов, а размер буфера в байтах. Именно так и произошло в приведенном выше фрагменте кода. В результате, заполнена будет только четверть буфера (при условии, что размер типа 'int' равен 4 байтам).

Пример N5 (проект Newton Game Dynamics)

dgCollisionCompoundBreakable::dgCollisionCompoundBreakable(....)
{
  ...
  dgInt32 faceOffsetHitogram[256];
  dgSubMesh* mainSegmenst[256];
  ...
  memset(faceOffsetHitogram, 0, sizeof(faceOffsetHitogram));
  memset(mainSegmenst, 0, sizeof(faceOffsetHitogram));
  ...
}

Имеем дело с опечаткой. Скорее всего кто-то поленился два раза набирать вызов функции memset(). Продублировали строчку. В одном месте заменили 'faceOffsetHitogram' на 'mainSegmenst', а в другом забыли.

Получается, что sizeof() вычисляет размер не того массива, который заполняется нулями. Вроде как функция memset() никак не виновата. Но неправильно будет работать именно она.

Пример N6 (проект CxImage)

static jpc_enc_tcmpt_t *tcmpt_create(....)
{
  ...
  memset(tcmpt->stepsizes, 0,
    sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t)));
  ...
}

Здесь присутствует лишний оператор sizeof(). Правильно размер вычислять так:

tcmpt->numstepsizes * sizeof(uint_fast16_t)

Но написали лишний sizeof() и получилась глупость:

sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t))

Здесь оператор sizeof() вычисляет размер типа size_t. Именно такой тип имеет выражение.

Я знаю, что хочется возразить. Уже не первый раз ошибка связана с оператором sizeof(). Т.е. программист ошибается, вычисляя размер буфера. Однако причиной этих ошибок всё равно является функция memset(). Она устроена так, что приходится делать эти различные вычисления, в которых так легко ошибиться.

Пример N7 (проект WinSCP)

TForm * __fastcall TMessageForm::Create(....)
{
  ....
  LOGFONT AFont;
  ....   
  memset(&AFont, sizeof(AFont), 0);
  ....
}

Функция memset() всеядна. Поэтому спокойно отнесётся, если вы перепутаете 2 и 3 аргумент. Именно так здесь и произошло. Эта функция заполняет 0 байт.

Пример N8 (проект Multi Theft Auto)

А вот ещё одна аналогичная ошибка. Кажется, разработчики Win32 API пошутили, когда создали вот такой макрос:

#define RtlFillMemory(Destination,Length,Fill) \
  memset((Destination),(Fill),(Length))

По смыслу это альтернатива memset(). Но надо быть внимательным. Обратите внимание, что меняется местами 2 и 3 аргумент.

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

#define FillMemory RtlFillMemory
LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs )
{
  ....
  PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol ;
  FillMemory ( pSym , NULL , SYM_BUFF_SIZE ) ;
  ....
}

NULL есть ни что иное, как 0. Поэтому функция memset() заполнила 0 байт.

Пример N9 (проект IPP Samples)

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

Хотя некоторые из приведенных выше ошибок были найдены в кода на языке C++, к C++ они никакого отношения не имеют. Другими словами, это ошибки возникают при программировании в стиле языка C.

Следующая ошибка связана как раз с неправильным использованием memset() в C++ программе. Пример достаточно длинный, поэтому можете в него не всматриваться. Прочитайте описание ниже и всё станет понятно.

class _MediaDataEx {
  ...
  virtual bool TryStrongCasting(
    pDynamicCastFunction pCandidateFunction) const;
  virtual bool TryWeakCasting(
    pDynamicCastFunction pCandidateFunction) const;
};

Status VC1Splitter::Init(SplitterParams& rInit)
{
  MediaDataEx::_MediaDataEx *m_stCodes;
  ...
  m_stCodes = (MediaDataEx::_MediaDataEx *)
    ippsMalloc_8u(START_CODE_NUMBER*2*sizeof(Ipp32s)+
                  sizeof(MediaDataEx::_MediaDataEx));
  ...
  memset(m_stCodes, 0, 
    (START_CODE_NUMBER*2*sizeof(Ipp32s)+
    sizeof(MediaDataEx::_MediaDataEx)));
  ...
}

Функция memset() используется для инициализации массива, состоящих из объектов класса. Самая большая беда в том, что класс содержит виртуальные функции. Соответственно функция memset() не только обнуляет поля класса, но и указатель на таблицу виртуальных методов (vptr). К чему это приведёт неизвестно. Но ничего хорошего в этом точно нет. Нельзя так обращаться с классами.

Заключение

Как видите, функция memset() имеет крайне неудачный интерфейс. В результате, функция memset() больше всех остальных провоцирует появление ошибок. Будьте бдительны!

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

Спасибо всем за внимание и подписывайтесь на мой твиттер @Code_Analysis.

Примечание

Уже после публикации статьи, один из читателей прислал ссылку вот на эту интересную статью "memset is Evil". Решил поделиться ей с вами. Что-ж, ещё одно подтверждение опасности memset().

Популярные статьи по теме
Главный вопрос программирования, рефакторинга и всего такого

Дата: 14 Апр 2016

Автор: Андрей Карпов

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

Дата: 20 Мар 2017

Автор: Андрей Карпов

В своей предыдущей статье я писал, что мне не нравится подход, при котором статические анализаторы кода оцениваются с помощью синтетических тестов. В статье приводился пример, воспринимаемый анализат…
Зло живёт в функциях сравнения

Дата: 19 Май 2017

Автор: Андрей Карпов

Возможно, читатели помнят мою статью под названием "Эффект последней строки". В ней идёт речь о замеченной мной закономерности: ошибка чаще всего допускается в последней строке однотипных блоков текс…
Статический анализ как часть процесса разработки Unreal Engine

Дата: 27 Июн 2017

Автор: Андрей Карпов

Проект Unreal Engine развивается - добавляется новый код и изменятся уже написанный. Неизбежное следствие развития проекта - появление в коде новых ошибок, которые желательно выявлять как можно раньш…
Любите статический анализ кода!

Дата: 16 Окт 2017

Автор: Андрей Карпов

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

Дата: 22 Дек 2018

Автор: Андрей Карпов

В канун празднования нового 2019 года команда PVS-Studio решила сделать приятный подарок всем контрибьюторам open-source проектов, хостящихся на GitHub, GitLab или Bitbucket. Им предоставляется возмо…
Как PVS-Studio оказался внимательнее, чем три с половиной программиста

Дата: 22 Окт 2018

Автор: Андрей Карпов

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

Дата: 31 Май 2014

Автор: Андрей Карпов

Я изучил множество ошибок, возникающих в результате копирования кода. И утверждаю, что чаще всего ошибки допускают в последнем фрагменте однотипного кода. Ранее я не встречал в книгах описания этого …
Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний

Дата: 31 Июл 2017

Автор: Андрей Карпов

После большой статьи про проверку операционной системы Tizen мне было задано много вопросов о проценте ложных срабатываний и о плотности ошибок (сколько ошибок PVS-Studio выявляет на 1000 строк кода)…
PVS-Studio ROI

Дата: 30 Янв 2019

Автор: Андрей Карпов

Время от времени нам задают вопрос, какую пользу в денежном эквиваленте получит компания от использования анализатора PVS-Studio. Мы решили оформить ответ в виде статьи и привести таблицы, которые по…

Комментарии (0)

Следующие комментарии

На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Этот сайт использует куки и другие технологии, чтобы предоставить вам более персонализированный опыт. Продолжая просмотр страниц нашего веб-сайта, вы принимаете условия использования этих файлов. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, покиньте данный сайт. Подробнее →
Принять