Как вы программируете безопасно вне среды управляемого кода? - PullRequest
8 голосов
/ 07 октября 2009

Если вы программируете на C или C ++, не пользуясь преимуществами управления памятью, проверки типов или защиты от переполнения буфера на управляемом языке, используя арифметику указателей, как вы убедитесь, что ваши программы безопасны? Вы используете много модульных тестов, или вы просто осторожный кодер? У вас есть другие методы?

Ответы [ 9 ]

24 голосов
/ 07 октября 2009

Все вышеперечисленное. Я использую:

  1. Очень осторожно
  2. Интеллектуальные указатели в максимально возможной степени
  3. Структуры данных, которые были протестированы, много стандартной библиотеки
  4. Все время юнит-тесты
  5. Инструменты проверки памяти, такие как MemValidator и AppVerifier
  6. Молитесь каждую ночь, чтобы она не вылетала на сайте клиента.

На самом деле, я просто преувеличиваю. Это не так уж плохо, и на самом деле не слишком сложно сохранять контроль над ресурсами, если вы правильно структурируете свой код.

Интересная заметка. У меня есть большое приложение, которое использует DCOM и имеет управляемые и неуправляемые модули. Неуправляемые модули, как правило, сложнее отлаживать во время разработки, но они очень хорошо работают на площадке заказчика из-за множества тестов, выполняемых на нем. Управляемые модули иногда страдают от плохого кода, потому что сборщик мусора настолько гибок, что программистам лень проверять использование ресурсов.

16 голосов
/ 07 октября 2009

Я использую много-много утверждений и строю как «отладочную» версию, так и «релизную» версию. Моя отладочная версия работает намного медленнее, чем моя релизная версия, со всеми проверками, которые она делает.

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

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

Я пишу чистый C (я не могу использовать C ++ в этом проекте), но я делаю C очень последовательным образом. У меня есть объектно-ориентированные классы, с конструкторами и деструкторами; Я должен назвать их вручную, но последовательность помогает. И если я забуду вызвать деструктора, Вальгринд бьет меня по голове, пока я не исправлю это.

В дополнение к конструктору и деструктору я пишу функцию самопроверки, которая просматривает объект и решает, является ли он нормальным или нет; например, если дескриптор файла равен нулю, но связанные данные файла не обнуляются, это указывает на какую-то ошибку (либо дескриптор был засорен, либо файл не был открыт, но в этих полях объекта есть мусор). Кроме того, большинство моих объектов имеют поле «подпись», которое должно быть установлено на определенное значение (специфичное для каждого отдельного объекта). Функции, которые используют объекты, обычно утверждают, что объекты являются нормальными.

Каждый раз, когда у меня malloc() появляется память, моя функция заполняет память значениями 0xDC. Структура, которая не полностью инициализирована, становится очевидной: число слишком велико, указатели недействительны (0xDCDCDCDC), и когда я смотрю на структуру в отладчике, становится очевидно, что она неинициализирована. Это намного лучше, чем заполнение нулями памяти при вызове malloc(). (Конечно, заполнение 0xDC только в отладочной сборке; сборка выпуска не должна тратить это время впустую.)

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

Valgrind сообщит мне, записывает ли какой-либо код конец буфера. Если бы у меня этого не было, я бы поставил «канареечные» значения после концов буферов и проверил бы их проверку работоспособности. Эти канарские значения, как и значения сигнатур, будут только для отладочной сборки, поэтому в версии выпуска не будет раздувания памяти.

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

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

Наконец, я должен сказать, что мне действительно нравятся венгерские обозначения. Я работал в Microsoft несколько лет назад, и, как и Джоэл, я выучил Apps венгерский, а не сломанный вариант. Это действительно делает неправильный код неправильным .

13 голосов
/ 07 октября 2009

Точно так же - как вы гарантируют, что ваши файлы и сокеты закрыты, ваши блокировки сняты, яда яда Память - не единственный ресурс, и с помощью GC вы неизбежно теряете надежное / своевременное уничтожение.

Ни GC, ни GC не превосходят автоматически. У каждого есть свои преимущества, у каждого есть своя цена, и хороший программист должен уметь справляться с обоими.

Я сказал столько же в ответ на этот вопрос .

3 голосов
/ 08 октября 2009

Мы пишем на C для встроенных систем. Помимо использования некоторых методов, общих для любого языка программирования или среды, мы также используем:

  • Инструмент статического анализа (например, PC-Lint ).
  • Соответствие MISRA-C (обеспечивается инструментом статического анализа).
  • Нет динамического выделения памяти.
3 голосов
/ 07 октября 2009

Я использую C ++ в течение 10 лет. Я использовал C, Perl, Lisp, Delphi, Visual Basic 6, C #, Java и другие языки, которые я не могу вспомнить из головы.

Ответ на ваш вопрос прост: Вы должны знать, что делаете , больше, чем C # / Java. больше, чем - это то, что порождает такие недовольства, как у Джеффа Этвуда в отношении "школ Java" .

Большинство ваших вопросов в каком-то смысле бессмысленны. Возникающие «проблемы» - это просто факты того, как аппаратное обеспечение действительно работает . Я хотел бы попросить вас написать процессор и оперативную память на VHDL / Verilog и посмотреть, как все работает на самом деле, даже когда действительно упрощен. Вы начнете понимать, что путь C # / Java - это абстракция, работающая над аппаратными средствами.

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

(я также написал C # и Java)

2 голосов
/ 07 октября 2009

Эндрю ответ хороший, но я бы также добавил дисциплину в список. Я нахожу, что после достаточной практики с C ++ вы достаточно хорошо чувствуете, что безопасно и что требует от вас велоцирапторов съесть вас. Вы склонны развивать стиль кодирования, который удобен при соблюдении безопасных практик и оставляет у вас чувство хэби-джиби, если вы попытаетесь, скажем, привести умный указатель к необработанному указателю и передать его чему-то другому.

Мне нравится думать об этом как о механическом инструменте в магазине. Это достаточно безопасно, если вы научились правильно его использовать и при условии, что вы всегда будете соблюдать все правила безопасности. Когда вы думаете, что можете отказаться от защитных очков, вам больно.

1 голос
/ 07 октября 2009

Помимо множества хороших советов, приведенных здесь, мой самый важный инструмент - СУХОЙ - не повторяйте себя. Я не распространяю подверженный ошибкам код (например, для обработки выделения памяти с помощью malloc () и free ()) по всей моей кодовой базе. У меня есть ровно одно место в моем коде, где вызываются malloc и free. Именно в оболочке есть функции MemoryAlloc и MemoryFree.

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

Иногда, когда я читаю здесь вопрос типа «Мне всегда нужно убедиться, что 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 является подходящим инструментом для работы. Спектакль? Преждевременная оптимизация! И одна единица, с которой нужно начинать, после того, как она оказывается узким местом.

1 голос
/ 07 октября 2009

Я сделал и C ++, и C #, и я не вижу всей шумихи по поводу управляемого кода.

Ах да, есть сборщик мусора для памяти, это полезно ... если, конечно, вы не воздержитесь от использования простых старых указателей в C ++, конечно, если вы используете только smart_pointers, то у вас не так много проблем.

Но тогда я хотел бы знать ... защищает ли ваш сборщик мусора от:

  • держать соединения с базой данных открытыми?
  • хранить блокировки на файлах?
  • ...

Управление ресурсами - это гораздо больше, чем управление памятью. Преимущество C ++ состоит в том, что вы быстро узнаете, что означает управление ресурсами и RAII, и это становится рефлексом:

  • если я хочу указатель, я хочу auto_ptr, shared_ptr или слабый_ptr
  • если я хочу соединение с БД, я хочу объект 'Connection'
  • если я открываю файл, мне нужен объект 'File'
  • ...

Что касается переполнения буфера, то мы не везде используем char * и size_t. У нас есть некоторые вещи, называющие 'string', 'iostream' и, конечно, уже упомянутый метод vector :: at, который освобождает нас от этих ограничений.

Проверенные библиотеки (stl, boost) хороши, используйте их и приступайте к более функциональным проблемам.

0 голосов
/ 07 октября 2009

C ++ имеет все функции, которые вы упомянули.

Есть управление памятью. Вы можете использовать Smart Pointers для очень точного управления. Или есть пара сборщиков мусора, хотя они не являются частью стандарта (но в большинстве случаев Smart Pointers более чем адекватны).

C ++ - строго типизированный язык. Так же, как C #.

Мы используем буферы. Вы можете использовать проверенную версию интерфейса. Но если вы знаете, что проблемы нет, вы можете использовать непроверенную версию интерфейса.

Сравните метод в () (проверено) с оператором [] (не проверено).

Да, мы используем модульное тестирование. Так же, как вы должны использовать в C #.

Да, мы осторожные кодеры. Так же, как вы должны быть в C #. Разница лишь в том, что ловушки различны в двух языках.

...