Как вы безопасно программируете вне среды управляемого кода?
Если вы являетесь тем, кто программирует на C или С++, без преимуществ управления памятью на управляемом языке, проверки типов или защиты от переполнения буфера, используя арифметику указателя, как вы убедитесь, что ваши программы в безопасности? Вы используете много модульных тестов, или вы просто осторожный кодер? У вас есть другие методы?
Ответы
Ответ 1
Все вышесказанное. Я использую:
- Много осторожности
- Умные указатели как можно больше
- Структуры данных, которые были протестированы, много стандартная библиотека
- Единичные тесты все время
- Инструменты проверки достоверности памяти, такие как MemValidator и AppVerifier
- Молитесь каждую ночь, когда он не падает на клиентском сайте.
Собственно, я просто преувеличиваю. Это не так уж плохо, и на самом деле не слишком сложно контролировать ресурсы, если вы правильно структурируете свой код.
Интересное примечание. У меня большое приложение, которое использует DCOM и имеет управляемые и неуправляемые модули. Неуправляемые модули, как правило, сложнее отлаживать во время разработки, но очень хорошо работают на сайте клиента из-за многих тестов, которые выполняются на нем. Управляемые модули иногда страдают от плохого кода, потому что сборщик мусора настолько гибкий, программисты ленивы в проверке использования ресурсов.
Ответ 2
Я использую множество и множество утверждений, а также создаю "отладочную" версию и версию "выпуска". Моя версия отладки работает намного медленнее, чем моя версия выпуска, со всеми проверками.
Я часто запускаюсь под Valgrind, а мой код имеет нулевые утечки памяти. Нуль. Гораздо проще уберечь программу от утечки, чем использовать глючную программу и исправить все утечки.
Кроме того, мой код компилируется без предупреждений, несмотря на то, что у меня есть компилятор для дополнительных предупреждений. Иногда предупреждения глупы, но иногда они указывают на ошибку, и я исправляю ее без необходимости находить ее в отладчике.
Я пишу чистый C (я не могу использовать С++ в этом проекте), но я делаю C очень последовательным образом. У меня есть объектно-ориентированные классы, с конструкторами и деструкторами; Я должен называть их вручную, но последовательность помогает. И если я забуду назвать деструктора, Вальгринд ударит меня по голове, пока я не исправлю его.
В дополнение к конструктору и деструктору я пишу функцию самопроверки, которая просматривает объект и решает, является ли он нормальным или нет; например, если дескриптор файла равен null, но связанные данные файла не обнуляются, это указывает на какую-либо ошибку (либо обработанный захват, либо файл не был открыт, но те поля в объекте имеют мусор в них). Кроме того, у большинства моих объектов есть поле "подпись", которое должно быть установлено на определенное значение (определенное для каждого другого объекта). Функции, которые используют объекты, обычно утверждают, что объекты являются разумными.
В любое время, когда я malloc()
некоторая память, моя функция заполняет память с помощью значений 0xDC
. Структура, которая не полностью инициализирована, становится очевидной: подсчеты слишком велики, указатели недействительны (0xDCDCDCDC
), и когда я смотрю на структуру отладчика, очевидно, что она не инициализирована. Это намного лучше, чем нулевая память при вызове malloc()
. (Конечно, заполнение 0xDC
происходит только в сборке отладки, при этом нет необходимости в том, чтобы сборка релиза была потеряна в это время.)
В любой момент, когда я освобождаю память, я удаляю указатель. Таким образом, если у меня есть глупая ошибка, когда код пытается использовать указатель после того, как его память была освобождена, я сразу получаю исключение с нулевым указателем, которое указывает мне на ошибку. Мои функции деструктора не принимают указатель на объект, они берут указатель на указатель и сжимают указатель после разрушения объекта. Кроме того, деструкторы уничтожают их объекты, прежде чем освобождать их, поэтому, если в некотором фрагменте кода есть копия указателя и пытается использовать объект, утверждение проверки работоспособности немедленно срабатывает.
Valgrind скажет мне, если какой-либо код записывает конец буфера. Если бы у меня этого не было, я бы поставил "канарейские" значения после концов буферов и проверил их проверку на работоспособность. Эти значения канарейки, такие как значения подписи, будут иметь только отладку-сборку, поэтому в версии выпуска не будет раздуваться память.
У меня есть набор модульных тестов, и когда я делаю какие-либо серьезные изменения в коде, очень приятно запускать модульные тесты и иметь некоторую уверенность, что я не ужасно нарушал ситуацию. Конечно, я запускаю модульные тесты в версии отладки, а также в версии выпуска, поэтому у всех моих утверждений есть шанс найти проблемы.
Помещение всей этой структуры на место было немного дополнительным усилием, но оно окупается каждый день. И я чувствую себя вполне счастливым, когда утверждаю, что огонь срабатывает и указывает мне на ошибку, вместо того, чтобы запускать ошибку в отладчике. В конечном счете, это просто меньше усилий, чтобы держать вещи в чистоте все время.
Наконец, я должен сказать, что мне действительно нравится венгерская нотация. Несколько лет назад я работал в Microsoft, и, как и Джоэл, я изучил Apps Hungarian, а не сломанный вариант. Это действительно означает сделать неправильный код неправильным.
Ответ 3
Как важно - как вы гарантируете, что ваши файлы и сокеты закрыты, ваши блокировки выпущены, yada yada. Память не является единственным ресурсом, и с GC вы по сути теряете надежное/своевременное уничтожение.
Ни GC, ни non-GC автоматически не превосходят. У каждого есть преимущества, у каждого есть своя цена, и хороший программист должен уметь справляться с обоими.
Я так сказал в ответе этот вопрос.
Ответ 4
Я использую С++ в течение 10 лет. Я использовал C, Perl, Lisp, Delphi, Visual Basic 6, С#, Java и другие языки, которые я не могу вспомнить с головы.
Ответ на ваш вопрос прост: вы должны знать, что делаете, больше, чем С#/Java. Более того, что порождает такие рифы, как Джефф Этвуд относительно Java Schools.
Большинство ваших вопросов, в некотором смысле, бессмысленно. "Проблемы", которые вы поднимаете, - это просто факты того, как аппаратные средства действительно работают. Я хотел бы бросить вызов вам написать процессор и оперативную память в VHDL/Verilog и посмотреть, как действительно работает материал, даже когда он действительно упрощен. Вы начнете понимать, что способ С#/Java - это абстракция, связанная с аппаратным обеспечением.
Более простой задачей было бы программировать элементарную операционную систему для встроенной системы от первоначального включения питания; он также покажет вам, что вам нужно знать.
(Я также написал С# и Java)
Ответ 5
Мы записываем в C для вложенных систем. Помимо использования некоторых методов, общих для любого языка программирования или среды, мы также используем:
- Инструмент статического анализа (например PC-Lint).
- Соответствие MISRA-C (применяется инструментом статического анализа).
- Отсутствует динамическое распределение памяти.
Ответ 6
Эндрю ответ хороший, но я также добавил бы дисциплину в список. Я нахожу, что после достаточной практики с С++ вы получите довольно хорошее представление о том, что безопасно, а что попрошайничество для велоцирапторов, чтобы съесть вас. Вы имеют тенденцию разрабатывать стиль кодирования, который чувствует себя комфортно, следуя безопасным методам, и оставляет вам ощущение heebie-jeebies, если вы попытаетесь, скажем, отбросить умный указатель обратно к необработанному указателю и передать его чему-то еще.
Мне нравится думать об этом как инструмент в магазине. Это достаточно безопасно, как только вы научитесь правильно использовать его и до тех пор, пока вы всегда будете соблюдать все правила безопасности. Это когда вы думаете, что можете отказаться от защитных очков, которые вам причиняют боль.
Ответ 7
Я сделал как С++, так и С#, и я не вижу всех обманности об управляемом коде.
О, хорошо, есть сборщик мусора для памяти, это полезно... если вы не воздерживаетесь от использования простых старых указателей на С++, конечно, если вы используете только smart_pointers, тогда у вас не так много проблем.
Но тогда я хотел бы знать... ваш сборщик мусора защитит вас от:
- открыть подключения к базе данных?
- сохранение блокировок в файлах?
- ...
Существует гораздо больше ресурсов для управления ресурсами, чем управление памятью. Хорошо, что С++ - это то, что вы быстро узнаете, что такое управление ресурсами и RAII, чтобы он стал рефлексом:
- Если я хочу указатель, я хочу auto_ptr, shared_ptr или weak_ptr
- Если я хочу подключиться к БД, мне нужен объект "Connection"
- Если я открою файл, мне нужен объект "Файл"
- ...
Что касается переполнения буфера, ну, это не так, как мы используем char * и size_t везде. У нас есть некоторые вещи, которые называют "string", "iostream" и, конечно, уже упомянутый вектор:: методом, который освобождает нас от этих ограничений.
Тестируемые библиотеки (stl, boost) хороши, используют их и переходят к более функциональным проблемам.
Ответ 8
Помимо большого количества полезных советов, приведенных здесь, мой самый важный инструмент - СУХОЙ - не повторяйте себя. Я не распространяю код, подверженный ошибкам (например, для обработки распределений памяти с помощью malloc() и free()) по всей моей кодовой базе. У меня есть только одно место в моем коде, где вызываются malloc и free. Он находится в функциях оболочки MemoryAlloc и MemoryFree.
Существует все проверки аргументов и начальная обработка ошибок, которые обычно указываются как повторяющийся шаблонный код вокруг вызова в malloc. Кроме того, он позволяет все, что нужно, изменить только одно местоположение, начиная с простых проверок отладки, таких как подсчет успешных вызовов в malloc и бесплатно, и проверить при завершении программы, что оба числа равны, вплоть до всех видов расширенных проверок безопасности.
Иногда, когда я читаю вопрос здесь как "Мне всегда нужно убедиться, что strncpy завершает строку, есть ли альтернатива?"
strncpy(dst, src, n);
dst[n-1] = '\0';
а затем дни обсуждения, я всегда задаюсь вопросом, является ли искусство извлечения повторных функций в функции потерянным искусством высшего программирования, которое больше не преподается в программировании лекций.
char *my_strncpy (dst, src, n)
{
assert((dst != NULL) && (src != NULL) && (n > 0));
strncpy(dst, src, n);
dst[n-1] = '\0';
return dst;
}
Первичная проблема дублирования кода решена - теперь подумайте, действительно ли strncpy - правильный инструмент для работы. Представление? Преждевременная оптимизация! И одно место, чтобы начать с него после того, как это окажется узким местом.
Ответ 9
В С++ есть все функции, которые вы упоминаете.
Имеется управление памятью.
Вы можете использовать Smart Pointers для очень точного управления. Или есть несколько сборщиков мусора, хотя они не являются частью стандарта (но в большинстве случаев Smart Pointers более чем адекватны).
С++ - это строго типизированный язык. Также как С#.
Мы используем буферы. Вы можете использовать ограниченную проверку интерфейса интерфейса.
Но если вы знаете, что нет проблем, вы можете использовать непроверенную версию интерфейса.
Сравните метод at() (checked) с оператором [] (Unchecked).
Да, мы используем Unit Testing.
Так же, как вы должны использовать в С#.
Да, мы осторожные кодеры.
Так же, как вы должны быть на С#. Единственное различие заключается в том, что ошибки на разных языках различны.