Комментарии 86
Абсолютное значение количества ошибок для функции не имеет смысла.
Относительное значение, скажем, вероятность допустить ошибку при использовании функции гораздо информативнее.
Относительное значение, скажем, вероятность допустить ошибку при использовании функции гораздо информативнее.
Я и говорю. Среди всех функций, которые имеются в фрагментах кода с ошибкой, больше всего вреда от memset(). :)
Нет, нужно посмотреть сколько раз функция была использована, и сколько раз она была использована неправильно.
Тогда победит какая-то очень редко используемая функция, которая из 100 проектов встретится только в 2. И в одном из проектов она будет использоваться неправильно. Получим, что в 50% она используется неправильно. Бессмысленное исследование.
Я же показал, какая функция наносит наибольший вред при программировании на Си/Си++.
Я же показал, какая функция наносит наибольший вред при программировании на Си/Си++.
Редко используемые функции можно исключить из исследования, введя минимальную планку употребления. Скажем, 20% проектов.
Почетное второе место занимает функция printf() и её разновидности. Думаю, это никого не удивит. Про опасность функции printf() не писал только ленивый.Ждём статью про опасную printf().
Не зная брода, не лезь в воду. Часть вторая.
СТОП. Подожди читатель, не проходи мимо. Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.
СТОП. Подожди читатель, не проходи мимо. Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.
Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.Та статья действительно по существу, про уязвимости и атаки. Если бы вы описали уязвимости memset, вместо «банальной истории о том, что функция не контролирует типы передаваемых аргументов», было бы очень полезно!
Чтобы этого избежать, используется сглаживание Лапласа. Например, если функция использовалась n раз и из них k раз с ошибкой, то оценка вероятности ошибки (k+2)/(n+4).
Источник: Alan Agresti, Brian Caffo — Simple and Effective Confidence Intervals for Proportions and Differences of Proportions Result from Adding Two Successes and Two Failures
Источник: Alan Agresti, Brian Caffo — Simple and Effective Confidence Intervals for Proportions and Differences of Proportions Result from Adding Two Successes and Two Failures
Пример N1 оператор sizeof() вычиялет размер указателя, а не массива
Пример N2 Переменная 'var' является указателем
Пример N3 компилятор в целях оптимизации удаляет вызов функции
Пример N4 третий аргумент функции memset() это не количество элементов, а размер буфера в байтах
Пример N5 Имеем дело с опечаткой
Пример N6 Здесь присутствует лишний оператор sizeof()
Пример N7 memset(&AFont, sizeof(AFont), 0);
Пример N8 разработчики Win32 API пошутили, когда создали вот такой макрос
Пример N9 Нельзя так обращаться с классами.Во всех абсолютно примерах приведены ошибки программистов по невнимательности или по незнанию. Из того, что кто-то не умеет программировать делается просто чудесный вывод
функция memset() имеет крайне неудачный интерфейсИмея молоток можно отбить себе палец.
Я думал будет что-то по существу, а так статья ни о чём.
Из того, что кто-то не умеет программировать делается просто чудесный вывод
Эти кто-то, разработчики таких проектов как OpenCV, CoreCLR, UnrealEngine4, Haiku, Qt, Chromium,…
Люди учатся на ошибках. Если вы падали на велосипеде, когда учились ездить, это не делает его самым опасным транспортом.
Если вы падали на велосипеде, когда учились ездить,это не значит, что вы не упадете на нем, даже если научились на нем ездить.
Спасибо. Интересная ссылка.
Вы тоже полезную ссылку на printf привели. А вашу статью я бы дополнил полезными примерами про:
1. memset can silently set the wrong value
2. memset can silently set an invalid value
3. memset is overkill when used on a string
4. memset can silently attack the wrong memory
5. memset must not be used on a struct nor on a class
и тогда бы тезис статьи был раскрыт по существу.
1. memset can silently set the wrong value
2. memset can silently set an invalid value
3. memset is overkill when used on a string
4. memset can silently attack the wrong memory
5. memset must not be used on a struct nor on a class
и тогда бы тезис статьи был раскрыт по существу.
Ну не каждая статья получается удачной. :)
Если вы не будете писать дополнение, я могу написать перевод приведённой ссылки
Напишите. Я думаю эта статья достойна перевода.
Если будете переводить, обязательно исправьте ошибки в самом начале, а то я даже не знаю стоит ли читать дальше шапки, в которой 4 из 7 примеров неправильные. Вот они:
char p1[25] = "" ; // will set the first character to 0 (нет, не только первый)
// ...
wchar_t p3[25] = L"" ; // will set the first character to 0 (нет, не только первый)
// ...
int p5[37] = {-1} ; // will set the 37 values to -1 (нет, только первый)
unsigned int p6[10] = {89} ; // will set the 10 values to 89 (нет, только первый)
Спасибо. Начал исследовать вопрос корректности статьи, тоже нашёл ошибки в примерах, поэтому перевода не будет, слишком много нюансов, на которые я сам не знаю ответ, но дополнение и некоторое исследование постараюсь сделать в ближайшее время.
Прошу сюда memset — сторона тьмы
> memset must not be used on a struct
Как в C занулить структуру?
Помню, в проекте GCompris была проблема с тем, что на определенных архитектурах (amd64, например) мыша не работала.
Путем копания в исходниках выяснилось, что в GcomprisProperties были добавлены какие-то поля (defaultcursor, nosursor), которые забыли добавить в функцию инициализации.
По счастливой случайности после вызова malloc(sizeof (GcomprisProperties)) для x86 в nocursor был 0, и оно нормально работало. А в amd64 там был какой-то мусор.
Проблема решилась заменой malloc на calloc. А если бы в дальнейшем ее потребовалось занулить, то наверняка бы использовали memset.
Т.к. не всегда при добавлении полей в структуры, программист пробегает по всему коду, ища все её использования и проверяет инициализацию. В языках с конструкторами (C++) это было бы проще, там всё локализовано.
Как в C занулить структуру?
Помню, в проекте GCompris была проблема с тем, что на определенных архитектурах (amd64, например) мыша не работала.
Путем копания в исходниках выяснилось, что в GcomprisProperties были добавлены какие-то поля (defaultcursor, nosursor), которые забыли добавить в функцию инициализации.
По счастливой случайности после вызова malloc(sizeof (GcomprisProperties)) для x86 в nocursor был 0, и оно нормально работало. А в amd64 там был какой-то мусор.
Проблема решилась заменой malloc на calloc. А если бы в дальнейшем ее потребовалось занулить, то наверняка бы использовали memset.
Т.к. не всегда при добавлении полей в структуры, программист пробегает по всему коду, ища все её использования и проверяет инициализацию. В языках с конструкторами (C++) это было бы проще, там всё локализовано.
Точнее, проблема решилась с принудительной инициализацией этих полей, и на будущее в предотвращение этих эксцессов заменой malloc на calloc.
Я бы сказал, что memcpy даёт на один вариант выстрела в ногу больше, чем memset.
Интересный продукт эта ваша студия, но сообщения об ошибках невнятные.
Без толмача не обойтись.
Без толмача не обойтись.
Не согласен, что дело в функции memset. Исходя из приведённых примеров, только в примере №7 можно с натяжкой назвать причиной ошибки кривой интерфейс функции. А какая альтернатива? был бы другой порядок аргументов — также путались бы. А именование фактических параметров функций в конце 60х в языкостроении не было распространено (да и сейчас можно по пальцам руки, наверное, пересчитать такие языки). Все остальные — ошибки связанные с управлением памятью (в широком смысле).
Как говорится, в программировании на Cи всего 1 существенная трудность — управление памятью и ошибки на единицу. :)
Как говорится, в программировании на Cи всего 1 существенная трудность — управление памятью и ошибки на единицу. :)
Ага. Неправильно используется sizeof, а виноват memset.
Да, но ведь этот sizeof потребовался для memset().
А memset() потребовался для main() — вот где корень зла!
Понятно, что функция напрямую работающая с памятью потенциально опасна и на нее все грехи свалить намного проще, чем на безобидную функцию, возвращающую результат вычислений или что-то подобное. Но корень зла-то именно в некорректном использовании sizeof(), который, наверняка, в вашей статистике встретится не только в контексте memset(), но и во множестве других мест.
Понятно, что функция напрямую работающая с памятью потенциально опасна и на нее все грехи свалить намного проще, чем на безобидную функцию, возвращающую результат вычислений или что-то подобное. Но корень зла-то именно в некорректном использовании sizeof(), который, наверняка, в вашей статистике встретится не только в контексте memset(), но и во множестве других мест.
Понятно, что функция напрямую работающая с памятью потенциально опасна
… и люди, пришедшие к этому выводу изобрели много языков, в которых такие функции отсутствуют в принципе.
Программировать на Си, не влезая напрямую в память, можно (хотя, конечно, только довольно простые программы). Но memset и memcpy — две первых функции, за которые вынужденно берутся новички (ранее не имевшие дело с памятью напрямую). Вот и получается, что они — опасные. А вместе с ними и printf(str).
Это ряд функций для работы с ПАМЯТЬЮ. Поэтому у них и приставка mem.
И sizeof вычисляет не размер типа в 6-ом примере
а размер получаемого значения.
Просто кое-кто стандарт C не читал и пытается делать выводы из знаний по C++.
И вот эти разработчики упомянутые — тоже. Как можно ошибиться при использовании memset()? Только если точно не знаешь, как она работает и для чего вообще делалась.
И sizeof вычисляет не размер типа в 6-ом примере
Здесь оператор sizeof() вычисляет размер типа size_t. Именно такой тип имеет выражение.
а размер получаемого значения.
printf("%d\n", sizeof "ab");
printf("%d\n", sizeof 1);
printf("%d\n", sizeof 1.0f);
printf("%d\n", sizeof 1.0);
printf("%d\n", sizeof 1.0L);
Просто кое-кто стандарт C не читал и пытается делать выводы из знаний по C++.
И вот эти разработчики упомянутые — тоже. Как можно ошибиться при использовании memset()? Только если точно не знаешь, как она работает и для чего вообще делалась.
И sizeof вычисляет не размер типа в 6-ом примере а размер получаемого значения.Чем размер значения отличается от размера его типа?
Чем размер значения отличается от размера его типа?
Не у всех типов есть размер.
#include <stdio.h>
int main(void)
{
printf("%d\n", sizeof (main));
return 0;
}
[guest@localhost c]$ .ansi t.c -o t
t.c: В функции «main»:
t.c:6:27: предупреждение: недопустимое применение «sizeof» к типу функции [-Wpedantic]
printf("%d\n", sizeof main);
^
[guest@localhost c]$ ./t
1
[guest@localhost c]$
Повторяю код (не успел там стереть скобки, заблокировался коммент).
#include <stdio.h>
int main(void)
{
printf("%d\n", sizeof main);
return 0;
}
[guest@localhost c]$ .ansi t.c -o t
t.c: В функции «main»:
t.c:6:27: предупреждение: недопустимое применение «sizeof» к типу функции [-Wpedantic]
printf("%d\n", sizeof main);
^
[guest@localhost c]$ ./t
1
[guest@localhost c]$
Спасибо, я знаю о том что не у всех типов есть размер.
Но все-таки, чем размер значения отличается от размера его типа?
Но все-таки, чем размер значения отличается от размера его типа?
У типов нет размеров, размеры есть у значений. И вот этот код я тебе не просто так привёл. У типа функции нет размера, однако возвращается какое-то значение, у которого есть размер. А какого оно типа?
У типов нет размеров, размеры есть у значений.В таком случае что вообще означает запись вида
sizeof(int)
? Что это, если не размер типа int?У типа функции нет размера, однако возвращается какое-то значение, у которого есть размер. А какого оно типа?Возвращается кем? Функция main как бы возвращает int, оператор sizeof — size_t.
Или вы про тип значения main, размер которого измеряет sizeof в вашем коде? Тогда это
int()
И да, у типа функции, с точки зрения gcc при ваших настройках компилятора — очень даже есть размер. Это 1 байт, как вам и написала программа.
В таком случае что вообще означает запись вида sizeof(int)? Что это, если не размер типа int?
Он по значению вычисляет; какое значение туда поместится, его размер он и возвращает. Длина участка памяти в байтах.
И да, у типа функции, с точки зрения gcc при ваших настройках компилятора — очень даже есть размер. Это 1 байт, как вам и написала программа.
А что это за один байт?
Мои настройки просты, .ansi — это просто
[guest@localhost ~]$ alias .ansi
alias .ansi='gcc -ansi -pedantic -Wall'
[guest@localhost ~]$
то есть соответствие стандарту C89 с выдачей предупреждений при его нарушении.
А что это за один байт?Спросите лучше у разработчиков gcc, какой именно байт они имели в виду :)
Он по значению вычисляет; какое значение туда поместится, его размер он и возвращает. Длина участка памяти в байтах.Так все-таки, чем отличается размер значения от размера типа?
Получается, что sizeof() вычисляет размер не того массива, который заполняется нулями. Вроде как функция memset() никак не виновата. Но неправильно будет работать именно она.
Теперь всегда буду винить в своих опечатках memset, даже если я при этом её не использую и вообще пишу на совсем другом языке с автоматическим управлением памятью.
Скажите, а какой следовало бы сделать интерфейс у memset, чтобы ошибок стало меньше? Мне кроме шаблонной версии (чтобы размер не в байтах принимать) как-то ничего в голову сходу не приходит.
Позвольте предложить идею вместо автора статьи.
Мне кажется, эта функция — результат плохого проектирования языка. Учитывая регулярную необходимость обнулять массивы не только при инициализации, можно было сделать для этого некоторое синтаксическое средство. Например, разрешить запись вида arr = { 0 } в любой точке кода (пусть она генерирует правильный вызов memset).
Хотя учитывая «родовую травму» Си, в котором длина массива — фантомное понятие, живущее только в голове разработчика, даже с этим будут проблемы… Так что memset — это своего рода плата за " int[] = int* "
Мне кажется, эта функция — результат плохого проектирования языка. Учитывая регулярную необходимость обнулять массивы не только при инициализации, можно было сделать для этого некоторое синтаксическое средство. Например, разрешить запись вида arr = { 0 } в любой точке кода (пусть она генерирует правильный вызов memset).
Хотя учитывая «родовую травму» Си, в котором длина массива — фантомное понятие, живущее только в голове разработчика, даже с этим будут проблемы… Так что memset — это своего рода плата за " int[] = int* "
Хороший вопрос. Я немного думал над этим, но понял, но что не знаю, как следовало бы сделать мир лучше. Стоит учитывать, что memset() это функция языка Си, а следовательно особенно не нафантазируешь. Единственное что я мог бы предложить, это сделать тип второго аргумента byte (unsigned char). По крайней мере было бы предупреждение, когда туда пытаются засунуть большое число, путая его с размером объекта.
Вот кстати да, а почему второй аргумент int, если он все равно к uchar приводится потом? Как-то это уж совсем бессмысленно.
Просто `memset` появился чуток раньше возможности объявления прототипов функций в C (которые появились в C89), вот и все дела.
А без объявления прототипа с типом char нельзя передать char в функцию, даже явный литерал 'a' означает не char, а int (в C).
А без объявления прототипа с типом char нельзя передать char в функцию, даже явный литерал 'a' означает не char, а int (в C).
Что мешало memset обновить в том же С89 (или в С99 хотя бы) — лично мне не очень понятно.
1) backward compatibility — наверняка есть исходники, где туда передаётся int
2) с int на самом деле производительнее, ведь char всё равно преобразуется в int перед пушем на стэк на большинстве платформ, а значит если объявлять как char, то потом лишние инструкции для преобразования. Это были те года, когда каждый такт процессора был на счету.
2) с int на самом деле производительнее, ведь char всё равно преобразуется в int перед пушем на стэк на большинстве платформ, а значит если объявлять как char, то потом лишние инструкции для преобразования. Это были те года, когда каждый такт процессора был на счету.
Я могу поверить в необходимость совместимости с С89, но в необходимость совместимости с С из 70-х уже не верю.
По-моему, это из той же оперы что и неявно возвращать int, если у функции не прописан тип возвращаемого значения. В С99 это запретили, не побоялись. Поломали совместимость. И ничего, все живы.
По-моему, это из той же оперы что и неявно возвращать int, если у функции не прописан тип возвращаемого значения. В С99 это запретили, не побоялись. Поломали совместимость. И ничего, все живы.
Не понимаю вообще зачем вообще эти конструкции? Фатальный недостаток?
#define RtlFillMemory(Destination,Length,Fill) \
memset((Destination),(Fill),(Length))
RtlFillMemory() также реализована в kernel32.dll и экспортируется из нее. То же самое с рядом других функций.
Этот #define предписывает препроцессору подставить вызовы memset(), которые компилятор сможет потом оптимизировать по месту.
Этот #define предписывает препроцессору подставить вызовы memset(), которые компилятор сможет потом оптимизировать по месту.
Не понял. RtlFillMemory будет препроцессором заменён на memset и какой тогда толк в том что функция с аналогичным именем присутствует в kernel32.dll.
Смысл в том, что не в каждом языке программирования есть memset, поэтому аналог (RtlFillMemory) включен в WinAPI. А раз в WinAPI есть описанная в документации функция RtlFillMemory — то она должна быть в каждом языке программирования, даже если там уже есть нормальный memset. Но так как современный memset работает быстрее чем RtlFillMemory из WinAPI — то RtlFillMemory в целях оптимизации тихо заменили на макрос.
> memset (buffer, 0, sizeof(*buffer));
тут проблема не только в том, что memset уберут, но еще и размер вычисляется от одного инта, нет?
а вообще самая опасная функция — это strncpy, потому что она не добавляет ноль в конце если строка заполнилась, и поэтому почти каждое ее использование в любом проекте неверно.
тут проблема не только в том, что memset уберут, но еще и размер вычисляется от одного инта, нет?
а вообще самая опасная функция — это strncpy, потому что она не добавляет ноль в конце если строка заполнилась, и поэтому почти каждое ее использование в любом проекте неверно.
Очень часто проблема с memset возникает когда люди переводят проэкты на юникод.
поэтому обнуляет тока половину буффера.
поэтому обнуляет тока половину буффера.
char buff[100];
::memset(buff, 0x00, sizeof(buff));
Кстати, да. Забыл упомянуть такой паттерн.
А можно глупый вопрос?
Почему половину? sizeof(buff) == 100 байт, ведь char всегда 1 байт, независимо от того, юникод это или нет.
Почему половину? sizeof(buff) == 100 байт, ведь char всегда 1 байт, независимо от того, юникод это или нет.
Это пока. А потом поменяют char на wchar_t и будет беда. Правда не в этом примере. Здесь всё будет хорошо, так как используется sizeof() от буфера. Но в реальном коде будет масса проблем. Я описывал это в статье "Команда PVS-Studio расширяет кругозор, выполняя разработку на заказ" (см. раздел «Замена char на wchar_t»).
Мне интересно, а сколько кода в принципе заполняет массивы чем-то кроме 0? Может, иногда нужны единицы, но я реально не представляю, для чего может пригодиться побайтовое заполнение массива чем-то типа 10101010.
думал самая опасная это strncpy )
>Впрочем, это не уменьшает интересность и полезность моих статей.
В общем, я согласен с этим утверждением. Но звучит оно как-то ну уж очень нескромно.
В общем, я согласен с этим утверждением. Но звучит оно как-то ну уж очень нескромно.
Вот вам еще один интересный подопытный кролик. Проверите?
Очепятка в слове «вычиялет»
Данная статья относится к серии «ужасы для программистов». Продолжаю эту тематику и предлагаю почитать: Ноль, один, два, Фредди заберёт тебя.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Самая опасная функция в мире С/С++