Если вы программируете на C или C ++, не имея преимуществ управляемого языка, таких как управление памятью, проверка типов или защита от переполнения буфера, с использованием арифметики указателей, как вы можете убедиться, что ваши программы безопасны? Вы используете много юнит-тестов или просто осторожный программист? У вас есть другие методы?
Как безопасно программировать вне среды управляемого кода?
Ответы (9)
Все вышеперечисленное. Я использую:
- Большая осторожность
- Умные указатели как можно больше
- Структуры данных, которые были протестированы, много стандартной библиотеки
- Юнит-тесты все время
- Инструменты проверки памяти, такие как MemValidator и AppVerifier
- Молитесь каждую ночь, чтобы он не вылетел на сайте клиента.
На самом деле я просто преувеличиваю. Это не так уж плохо и на самом деле не так уж и сложно контролировать ресурсы, если вы правильно структурируете свой код.
Интересное замечание. У меня есть большое приложение, которое использует DCOM и имеет управляемые и неуправляемые модули. Неуправляемые модули, как правило, труднее отлаживать во время разработки, но они очень хорошо работают на сайте заказчика из-за того, что на нем выполняется множество тестов. Управляемые модули иногда страдают от плохого кода, потому что сборщик мусора настолько гибкий, что программисты ленивы проверять использование ресурсов.
Я использую множество утверждений и создаю как «отладочную», так и «выпускную» версию. Моя отладочная версия работает намного медленнее, чем моя окончательная версия, со всеми проверками, которые она выполняет.
Я часто использую Valgrind, и мой код не имеет утечек памяти. Нуль. Гораздо проще сохранить программу без утечек, чем взять программу с ошибками и исправить все утечки.
Кроме того, мой код компилируется без предупреждений, несмотря на то, что у меня компилятор настроен на дополнительные предупреждения. Иногда предупреждения выглядят глупо, но иногда они указывают прямо на ошибку, и я исправляю ее без необходимости искать ее в отладчике.
Я пишу на чистом C (я не могу использовать C ++ в этом проекте), но я делаю C очень последовательным образом. У меня есть объектно-ориентированные классы с конструкторами и деструкторами; Приходится называть их вручную, но последовательность помогает. И если я забываю вызвать деструктор, Valgrind бьет меня по голове, пока я не исправлю его.
В дополнение к конструктору и деструктору я пишу функцию самопроверки, которая просматривает объект и решает, работает он или нет; например, если дескриптор файла равен нулю, но связанные с ним данные файла не обнулены, это указывает на какую-то ошибку (либо дескриптор затерт, либо файл не был открыт, но в этих полях объекта есть мусор). Кроме того, у большинства моих объектов есть поле «подпись», которое должно быть установлено на определенное значение (специфичное для каждого отдельного объекта). Функции, использующие объекты, обычно утверждают, что объекты вменяемы.
Каждый раз, когда у меня malloc() какая-то память, моя функция заполняет память 0xDC значениями. Структура, которая не полностью инициализирована, становится очевидной: счетчики слишком велики, указатели недействительны (0xDCDCDCDC), и когда я смотрю на структуру в отладчике, становится очевидно, что она не инициализирована. Это намного лучше, чем заполнение памяти нулями при вызове malloc(). (Конечно, заливка 0xDC есть только в отладочной сборке; нет необходимости для сборки выпуска тратить это время.)
Каждый раз, когда я освобождаю память, я стираю указатель. Таким образом, если у меня есть глупая ошибка, когда код пытается использовать указатель после того, как его память была освобождена, я мгновенно получаю исключение с нулевым указателем, которое указывает мне прямо на ошибку. Мои функции деструктора не принимают указатель на объект, они принимают указатель на указатель и стирают указатель после разрушения объекта. Кроме того, деструкторы стирают свои объекты перед их освобождением, поэтому, если какой-то фрагмент кода имеет копию указателя и пытается использовать объект, утверждение проверки работоспособности срабатывает мгновенно.
Valgrind сообщит мне, если какой-либо код записывает конец буфера. Если бы у меня этого не было, я бы поставил «канареечные» значения после концов буферов и проверил их при проверке работоспособности. Эти канареечные значения, как и значения подписи, будут предназначены только для отладки, поэтому в версии выпуска не будет раздувания памяти.
У меня есть набор модульных тестов, и когда я вношу какие-либо серьезные изменения в код, запускать модульные тесты очень удобно, и я уверен, что я ничего ужасно не сломал. Конечно, я запускаю модульные тесты как для отладочной, так и для окончательной версии, поэтому у всех моих утверждений есть шанс найти проблемы.
Установка всей этой структуры на место потребовала дополнительных усилий, но они окупаются каждый день. И я чувствую себя вполне счастливым, когда срабатывает assert и указывает мне прямо на ошибку, вместо того, чтобы запускать ошибку в отладчике. В конечном итоге, постоянно содержать вещи в чистоте становится меньше работы.
Наконец, я должен сказать, что мне действительно нравятся венгерские обозначения. Я работал в Microsoft несколько лет назад и, как и Джоэл, выучил Apps венгерский, а не сломанный вариант. Это действительно заставляет неправильный код выглядеть неправильно.
Не менее важно - как вы убедиться, что ваши файлы и сокеты закрыты, ваши блокировки сняты, да-а-а. Память - не единственный ресурс, и с GC вы неизбежно теряете надежное / своевременное уничтожение.
Ни сборщик мусора, ни сборщик мусора не превосходят автоматически. У каждого есть свои преимущества, у каждого своя цена, и хороший программист должен уметь справиться с обоими.
Я сказал об этом в ответе на этот вопрос.
Я использую C ++ 10 лет. Я использовал C, Perl, Lisp, Delphi, Visual Basic 6, C #, Java и различные другие языки, которые я не могу припомнить.
Ответ на ваш вопрос прост: вы должны знать, что делаете, больше, чем C # / Java. Больше чем - вот что порождает такие тирады, как Джефф Этвуд, относительно " Школы Java ".
Большинство ваших вопросов в каком-то смысле бессмысленны. «Проблемы», которые вы поднимаете, - это просто факты того, как на самом деле работает оборудование. Я бы хотел попросить вас написать ЦП и ОЗУ на VHDL / Verilog и посмотреть, как все работает на самом деле, даже когда действительно упрощено. Вы начнете понимать, что C # / Java - это абстракция, ограничивающая аппаратное обеспечение.
Более простой задачей было бы запрограммировать простую операционную систему для встроенной системы с момента первого включения; он также покажет вам то, что вам нужно знать.
(Я также написал C # и Java)
Мы пишем на C для встраиваемых систем. Помимо использования некоторых методов, общих для любого языка программирования или среды, мы также используем:
- Инструмент статического анализа (например, PC-Lint).
- Соответствие MISRA-C (обеспечивается инструментом статического анализа).
- Нет вообще никакого распределения динамической памяти.
Эндрю хороший ответ, но я бы также добавил к этому списку дисциплину. Я считаю, что после достаточной практики с C ++ вы довольно хорошо понимаете, что безопасно, а что просит велоцирапторов съесть вы. Вы склонны развивать стиль кодирования, который будет удобен при соблюдении безопасных методов и заставит вас чувствовать себя неловко, если вы попытаетесь, скажем, вернуть умный указатель на необработанный указатель и передать его чему-то еще.
Мне нравится думать об этом как об электроинструменте в магазине. Это достаточно безопасно, если вы научитесь правильно им пользоваться и при условии, что вы всегда соблюдаете все правила безопасности. Вы получаете травму, когда думаете, что можете отказаться от защитных очков.
Я работал как на C ++, так и на C #, и я не вижу всей шумихи вокруг управляемого кода.
Да ладно, есть сборщик мусора для памяти, это полезно ... если, конечно, вы не воздержитесь от использования простых старых указателей в C ++, если вы используете только smart_pointers, у вас не так много проблем.
Но тогда я хотел бы знать ... защищает ли ваш сборщик мусора от:
- держать соединения с базой данных открытыми?
- блокирует файлы?
- ...
Управление ресурсами - это гораздо больше, чем управление памятью. Хорошая вещь в C ++ заключается в том, что вы быстро узнаете, что означает управление ресурсами и RAII, так что это становится рефлексом:
- если мне нужен указатель, я хочу auto_ptr, shared_ptr или weak_ptr
- если мне нужно соединение с БД, мне нужен объект «Соединение»
- если я открываю файл, мне нужен объект File
- ...
Что касается переполнения буфера, ну, мы не везде используем char * и size_t. У нас есть некоторые вещи, называемые 'string', 'iostream' и, конечно же, уже упомянутый метод vector :: at, который освобождает нас от этих ограничений.
Протестированные библиотеки (stl, boost) хороши, используйте их и решайте более функциональные задачи.
Помимо множества полезных советов, приведенных здесь, мой самый важный инструмент - DRY - Don't Repeat Yourself. Я не распространяю код, подверженный ошибкам (например, для обработки выделения памяти с помощью 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 подходит для работы. Представление? Преждевременная оптимизация! И одно-единственное место, чтобы начать с него после того, как оно окажется узким местом.
В C ++ есть все упомянутые вами функции.
Есть управление памятью. Вы можете использовать интеллектуальные указатели для очень точного управления. Или есть пара сборщиков мусора, хотя они не являются частью стандарта (но в большинстве случаев смарт-указатели более чем подходят).
C ++ - это строго типизированный язык. Прямо как C #.
Мы используем буферы. Вы можете использовать версию интерфейса с проверкой границ. Но если вы знаете, что это не проблема, вы можете использовать непроверенную версию интерфейса.
Сравнить метод at () (отмечен) с оператором [] (не отмечен).
Да, мы используем модульное тестирование. Точно так же, как вы должны использовать в C #.
Да, мы осторожные кодеры. Точно так же, как вы должны быть в C #. Единственная разница в том, что подводные камни в двух языках разные.
std::stringчеловек. И если вы все еще используете C вместо C ++, не надо. - person Tom Hawtin - tackline   schedule 30.11.2009