Как вы безопасно программируете вне среды управляемого кода?

Если вы являетесь тем, кто программирует на 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. Так же, как вы должны использовать в С#.

Да, мы осторожные кодеры. Так же, как вы должны быть на С#. Единственное различие заключается в том, что ошибки на разных языках различны.