Visual C++: Safe C and C++ Libraries для Visual Studio 2005 - библиотеки для создания безопасного кода

Источник: msdn

Когда вышел Visual Studio 2005, обнаружилось, что в библиотеки Visual C++ внесены существенные усовер­шенствования - результат полного пересмотра функ­ций, содержащихся в C Runtime Library (CRT), Standard C++ Library (SCL), Active Template Library (ATL) и Microsoft Foundation Classes (MFC), с точки зрения бе­зопасности. Углубленный анализ показал, что имеет смысл внести изменения, которые повысят безопас­ность и надежность приложений.

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

В этой статье рассказывается о библиотеках Safe C and C++ Libraries, доступных в Visual C++ 2005. Я рас­смотрю архитектурные принципы, которыми руко­водствовались при создании этих библиотек, и приведу примеры различных методик написания с их помощью более безопасного кода. Я также кратко расскажу о том, как работают другие библиотеки, взаимодействующие с Safe Libraries. И наконец, дам рекомендации по пере­ходу, которые помогут вам перенести в Visual C++ 2005 код, рассчитанный на предыдущие версии Visual C++.

Примеры кода, прилагаемые к статье, тестировались в предварительной версии Visual Studio 2005. Советую вам получить последнюю бета-версию и создавать свои приложения в ней. Наверное, это лучший способ озна­комиться с основными изменениями.

Принципы, на которых основаны Safe Libraries

При проектировании Safe Libraries мы (сотрудники группы разработки Visual C++) руководствовались не­сколькими принципами построения архитектуры. Зна­комство с этими принципами поможет вам понять, по­чему мы сделали тот или иной выбор. Но имейте в виду, что никакие библиотеки или методики разработки, до­кументирования или анализа не могут гарантировать полную безопасность кода. Применение наших библи­отек позволит сделать существующий код безопаснее, внеся небольшие изменения, но ничто не может поме­шать вам выполнять какие-либо рискованные опера­ции в своем коде или некорректно использовать наш код. В конечном итоге, даже если вы задействуете все возможности Safe Libraries, следует применять передо­вые методики, лежащие в основе разработки безопасно­го кода: моделирование угроз, анализ кода и тестирова­ние на проникновение (penetration testing). Эти вопро­сы рассматриваются в книге Дэвида Лебланка (David LeBlanc) и Майкла Говарда (Michael Howard) «Writing Secure Code» (Microsoft Press 2002).

«Secure by default» - один из принципов Trustworthy Computing, помогающих решать эти проблемы. Мы зна­ем, что объявление функции strcpy устаревшей и гене­рация предупреждения при каждом ее использовании повысят издержки перехода. Но мы стремились к тому, чтобы только что установленная система автоматичес­ки обеспечивала наиболее безопасное поведение даже для проектов, переносимых из старых версий. Конеч­но, вы всегда можете отключить предупреждения, от­ложив исправление кода до момента, когда вы будете к нему готовы.

При проектировании библиотек мы старались сокра­тить количество изменений парадигм. Сколько програм­мистов, столько и мнений о том, как писать самый качес­твенный и безопасный код. Я считаю, что наиболее бе­зопасный подход к написанию кода, манипулирующего строками, - использовать строковые классы C++, такие как std::string, или CString из MFC/ATL. Но сотрудни­кам группы разработки Visual C++ известно, что сущес­твуют огромные объемы кода в стиле C, который прихо­дится поддерживать или постепенно развивать, и что, возможно, нет смысла идти на риск и издержки и пере­водить этот код на C++ и строковые классы. Поэтому мы позаботились, чтобы Safe Libraries поддерживали код, используемый вами в настоящее время. Наши библиоте­ки применимы даже к старому, менее гибкому коду.

В библиотеках для Visual C++ 7.1 мы проделали кое-какую работу в области безопасности: полностью пе­ресмотрели код и внесли массу изменений и исправле­ний. Однако наша основная цель состояла в том, чтобы не нарушить совместимость с существующим исход­ным кодом, поскольку эта версия была лишь усовершенствованием предыдущей. При работе над новой версией мы применили другой подход, чтобы выйти на новый уровень безопасности. Мы по-прежнему старались не нарушать совместимость, однако, когда мы обнаружи­вали проблемы, которые никак нельзя было решить без нарушения совместимости, мы шли на это. Так, в коде на C невозможно безопасно использовать strcpy, не пе­редавая дополнительный параметр, задающий размер буфера-приемника. Все такие библиотечные функции помечены в документации как устаревшие, получить сведения о них не составляет труда. Мы не могли раз­работать максимально безопасные библиотеки, не вы­нудив вас внести некоторые изменения в свой код, поэ­тому при работе с новыми библиотеками вам придется модифицировать код. И поскольку современные угрозы сильно отличаются от угроз, существовавших во време­на разработки C Standard Library, нужно было уделять внимание несколько другим вещам.

Несмотря на упомянутые выше причины, по кото­рым вам понадобится изменять код, мы понимаем, что это дорогое удовольствие. Всякий раз, когда выход но­вой версии Visual Studio вынуждает вносить изменения, вам приходится тратить силы, сталкиваться с риском возникновения новых «багов» и выполнять тестирова­ние, чтобы обеспечить прежнее качество. В большинс­тве случаев нам удалось повысить безопасность библи­отек, не требуя от вас вообще никаких изменений, а там, где без изменений не обойтись, мы постарались упрос­тить их внесение, сделав новые функции похожими на старые и используя единообразные шаблоны.

Мы считаем расширение продукта своей обязан­ностью перед сообществом разработчиков, поэтому я был очень рад, что мне представилась возможность со­трудничать с комитетом стандартов языка C при работе над проектом Safe Libraries. Мы получили от комитета множество предложений и отзывов, которые помогли нам усовершенствовать функции. Надеемся, что вскоре этот комитет выпустит наш технический отчет по дан­ному вопросу. Текущий рабочий вариант технического отчета см. по ссылке www.open-std.org/jtc1/sc22/wg14.

Архитектура Safe Libraries

Моя группа занимается созданием Visual C++ Libraries, в которые входит как новейший код, созданный подраз­делением по разработке (например ATL Server), так и самый старый код продукта (например CRT). Проана­лизировав код, мы обнаружили, что за последние двад­цать лет общепринятые стандарты кодирования сущес­твенно изменились. Мы увидели, что кое-какой старый код писали во времена, когда каждый байт был на вес золота, поэтому иногда параметры не полностью прове­рялись на допустимость.

Новый код изобилует всевозможными проверка­ми, которые, как мы полагаем, действительно полезны при отладке. Мы уделили много внимания тому, что­бы окончательный код был так же надежен при неожи­данных ситуациях, как и отлаживаемый. Эти проверки не только облегчают отладку - они делают код библи­отеки более безопасным. При разработке Safe Libraries ключевая роль отводилась добавлению кода проверки на допустимость в большинство библиотечных функ-ций. Это повлияло и на отлаживаемый, и на оконча­тельный код. Например, если вы передадите библио­течной функции некорректные флаги, она выявит это при проверке и сообщит вам об ошибке.

Изучив множество хакерских атак, мы извлекли следующий урок: злоумышленники часто используют то, что приложение не может выявить ошибку, или его «терпимость» к неожиданным ситуациям, например к ситуациям, когда стандартная функция возвращает значение, свидетельствующее об ошибке. Мы приня­ли это во внимание при разработке средства безопас­ной генерации кода (/GS) для Visual C++ 7.0. Когда код, скомпилированный с ключом /GS, обнаруживает пере­полнение буфера стека, библиотечный код немедленно прекращает выполнение этого кода, при этом в процессе выполняется минимальное количество дополнительно­го кода, что сокращает «область атаки» (attack surface). Мы еще больше сократили эту область в Visual C++ 2005 (см. статью Стефена Тауба (Stephen Toub) «C++: Write Faster Code with the Modern Language Features of Visual C++ 2005» в «MSDN Magazine» за май 2004 г. по ссылке msdn.microsoft.com/msdnmag/issues/04/05/VisualC2005). Дело в том, что любой код вашего процес­са, который выполняется после того, как библиотеч­ный код обнаружил сбой, может быть использован зло­умышленником в своих целях, поэтому объем такого кода надо свести к минимуму.

Точно такая же логика действует в отношении ко­да проверки на допустимость. Если вы передадите биб-лиотечному коду некорректный параметр, тот сообщит об этом на этапе отладки, что поможет выявить ошиб­ку. Если вы посмотрите стек вызовов при возникнове­нии ошибочной ситуации, то сможете пройти по стеку вызовов от сообщения об ошибке до фрагмента вашего кода, который был причиной проблемы. В окончатель­ной версии приложения библиотечный код запустит свой обработчик некорректного параметра, который вызовет средство Windows Error Reporting, при необ­ходимости позволяющее получить аварийный дамп и вызвать JIT-отладчик.

В этом контексте некорректным является параметр, приводящий к очевидной ошибке, которую вы могли бы обнаружить при написании кода. Такое часто случает­ся из-за отсутствия жестких рамок в стандарте языка C. Например, если ваш код с помощью функции sprintf записывает строку в буфер, задаваемый указателем NULL, библиотечный код сообщит о некорректном па­раметре. Но если вы с помощью функции malloc запро­сите, чтобы библиотечный код выделил 10 Мб памяти, а система располагает только 5 Мб свободной памяти, функция просто вернет NULL без сообщения об ошиб­ке. Это ожидаемая ошибка периода выполнения, а не ошибка в программировании.

Одна из проблем, связанных с такими ошибками периода выполнения, - не все функции возвращают информацию об ошибке непосредственно вызвавше­му коду. Одни C-функции присваивают значение пере­менной errno, а другие не делают даже этого. Добавляя новые функции, мы старались, чтобы они напрямую возвращали информацию об ошибках.

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

Многие разработчики постоянно имеют дело со строками. Поэтому переполнение буфера - распро­страненная причина проблем. Более того, один из крупнейших источников проблем в библиотечном коде Visual C++ - набор традиционных функций C Runtime Library, работающих со строками.

Бурные споры о том, какой вид строкового буфера лучше и безопаснее, будут идти вечно. Выбор между строками, оканчивающимися нулем, и строками, дли­на которых задана в префиксе, издавна был предметом войны между разработчиками низкоуровневых языков. Не желая вносить крупные изменения, мы должны бы­ли оставаться в рамках существующей практики при­менения C. Язык C и его библиотеки неразрывно связа­ны со строками, оканчивающимися нулем. Философия их архитектуры отражает приоритеты и ограничения тридцатилетней давности, когда в первую очередь бес­покоились о производительности и размере кода. Одна­ко, чтобы в наших библиотеках не возникали перепол­нения буфера, мы подошли к работе со строками более строго, чем в предыдущих библиотеках.

Простейшее правило, из-за которого разработчи­кам и проектировщикам библиотек приходится делать много дополнительной работы: никогда не записывай­те данные в строковый буфер, не зная его размера. Ар­хитектура strcpy - одно из самых крупных нарушений этого правила. Функция strcpy не может безопасно за­писывать данные в буфер-приемник, поскольку не «зна­ет», сколько памяти выделил вызвавший код.

Чтобы соблюсти это простое правило, потребова­лось добавить много новых функций, отличительная черта которых - дополнительный целый параметр пос­ле буфера-приемника: strcpy(dest, src) превращается в strcpy_s(dest, destsize, src).

Вас может заинтересовать, почему библиотеки не требуют, чтобы передавался и размер исходного буфе­ра. Этот вопрос затрагивает ключевое архитектурное решение Safe Libraries. Я уже говорил, что мы должны были поддерживать строки, оканчивающиеся нулем. Запомните: длина строки с завершающим нулем, из которой считываются данные, уже известна. Передача еще одного значения длины привела бы к возникнове­нию дополнительных ошибочных ситуаций, связанных с усечением строки. Кроме того, при анализе существу­ющего кода мы увидели, что многие строки хранятся в буферах фиксированного размера, содержащих только саму строку. В этом случае добавление размера вход­ного буфера не дает никакой новой информации. На­конец, мы решили, что передача размера входной стро­ки приведет к запутыванию кода многих приложений. В итоге мы пришли к неизбежному выводу - функции должны доверять своим входным строкам.

В действительности это не так удивительно, как на первый взгляд. Не забывайте: библиотечный код принимает на веру и то, что размер строки-приемника соответствует размеру памяти, выделенному для нее. При использовании «родного» кода на C и C++ вы всег­да можете передать библиотеке некорректные данные, «заложив мину» под приложение.

Мы применили в Safe C++ Libraries тот же принцип: библиотечный код доверяет вашему вводу, но требует, чтобы вы предоставили информацию о выводе. Если вы передаете пару входных итераторов, библиотека до­веряет вам и рассчитывает, что вы не выйдете за грани­цы контейнера. Но если вы передаете выходной итера­тор, то это должен быть итератор с проверкой (checked iterator), который «знает», сколько пространства до­ступно для вывода. Традиционные итераторы без про­верки не годятся, поскольку они могут записать данные за конец буфера.

Очевидно, существует один важный случай, когда вы, разумеется, не хотели бы, чтобы библиотечный код предполагал, что входная строка допустима. Это ситу­ация, где такая строка получена из недоверяемого ис­точника. Например, представьте, что вы анализируете пакет данных, поступивший от другого процесса или по сети. В этом случае злоумышленник может подгото­вить специальный пакет с пропущенным завершающим символом, рассчитывая на то, что в вашем коде возник­нут какие-либо проблемы. Мы добавили специальную функцию (strnlen), чтобы определять длину не доверяе­мых строк. В отличие от других новых функций она не используется вместо strlen, а применяется в определен­ных случаях, когда приходится иметь дело с не доверяе­мыми данными.

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

Применение библиотека практике

Теперь рассмотрим код в разделе A на рис. 1 и посмот­рим, как исправить его с помощью Safe Libraries. Это очень короткий фрагмент кода, манипулирующий стро­ками с помощью библиотеки CRT, тем не менее в него можно внести различные изменения, демонстрирую­щие, какие решения предстоит принимать при модифи­кации своего кода.

В разделе A показан классический код, подвержен­ный риску переполнения буфера. Наверное, разработ­чик этого кода полагал, что строка никогда не будет длиннее 10 символов, поэтому размера в 20 байтов бу­дет достаточно. При компиляции этого кода в Visual C++ 2005 будут выводиться предупреждения об ис­пользовании устаревших функций для строк, помечен­ных соответствующим комментарием.

В разделе B представлен самый простой вариант исправления этого кода. При этом исправлении прос­то вводится в действие предположение первоначально­го программиста. Если src и «…» действительно имеют достаточно малый размер и помещаются в dest, все бу­дет прекрасно работать. Если нет, библиотека вызовет _invalid_parameter_handler и прекратит операцию, не допустив переполнения буфера. Для определения раз­мера буфера в примере используется функция _countof. Это новая функция, добавленная в CRT: она получает количество элементов массива и использует шаблоны (только в C++-коде), чтобы ее нельзя было применять к указателям.

В разделе C дан альтернативный вариант исправле­ния, когда разработчик просто усекает исходную стро­ку. Важно понимать разницу между функцией wcscpy_s (которая предполагает, что вы выделили достаточно памяти, и, если памяти не хватает, вызывает ошибку) и функцией wcsncpy_s (которая усекает строку, если она не умещается в доступной для нее памяти).

В разделе D предполагается, что вашей программе известен максимальный размер любой строки, зада­ваемой переменной src (например путь не может быть длиннее MAX_PATH). В этом случае вы по-прежнему можете использовать статический буфер, содержащий­ся в стеке, и вернуться к использованию wcscpy_s. Как и в разделе B, если для строки не хватит места, опера­ция не будет выполнена, но разработчик такого кода, по-видимому, полностью уверен, что никогда не столк­нется со строкой длиннее MAX_SRC_SIZE.

В разделе E приведена версия функции, действи­тельно способная справиться со сколь угодно длинны­ми строками за счет использования памяти кучи. Если памяти не хватит, выполнение кода также прервется

на функции wcscpy_s, но это может произойти только при «баге» в логике программы. Заметьте: использует­ся функция calloc, а не malloc, чтобы сделать невозмож­ным переполнение при умножении целых чисел, когда вычисляется размер в байтах.

В разделе F показано, что вы написали бы, если бы у вас было время на то, чтобы по-настоящему вычистить свой код. Предполагается, что не в каждом приложении это удастся сделать, но в некоторых случаях такие из­менения имеют смысл.

Safe C Library

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

Но имеется гораздо меньшая группа функций, в ко­торые нам пришлось добавить параметры или поведе­ние которых сильно изменилось (табл. 1). В этом спис­ке более сотни функций, однако, как видите, многие из них, по сути, дублируются. Например, объявляя функцию strcpy устаревшей, мы объявляем устаревшими и функции wcscpy и _mbscpy.

Каждая устаревшая функция заменена более бе­зопасной, у которой к имени добавлено _s. Функции с префиксом - это расширения Microsoft или POSIX-расширения стандартных функций языка C. Функ­ции без подчеркивания являются частью стандарта языка C. Мы сотрудничаем с комитетом по стандартам языка C, разрабатывая набор функций, которые займут их место.

Каждая из перечисленных в таблице функций по­мечена как устаревшая. Устаревшие функции - отно­сительно новый механизм компилятора Visual C++, позволяющий библиотеке предупреждать вас, когда вы используете средства, не рекомендуемые ее раз­работчиками. В объявлении функции указывается __declspec(deprecated), и вы при каждом вызове такой функции получаете предупреждение.

Мы группировали устаревшие функции с помощью макросов. Все небезопасные функции содержат в заго­ловках строку _CRT_INSECURE_DEPRECATE. Чтобы эти функции не считались устаревшими, можно задать на этапе компиляции специальный макрос - _CRT_SECURE_NO_ DEPRECATE, и вы не будете получать предупреждения об устаревших функциях.

Мы добавляли новые функции для решения опре­деленных проблем, которые нельзя решить, оставив функцию в существующем виде. В большинстве слу­чаев проблемой было отсутствие параметра для зада­ния размера буфера. Почти всем строковым функци­ям и многим другим функциям библиотек для языка C свойственна проблема задания размера буфера. Те­перь библиотечные функции всегда принимают раз­мер буфера в параметре, который идет сразу за выход­ным буфером.

Тот факт, что размер буфера не задается, особенно ярко проявляется при использовании функции scanf. Работая с функцией scanf_s и передавая параметры, описываемые строкой формата, вы должны указывать размер буфера после каждого параметра функции scanf, задающего буфер.

Еще одна типичная проблема - функции, некор­ректно завершающие строки. В Standard C Library эта проблема возникает при работе с функциями strncpy и snprintf. Все новые функции корректно завершают вы­вод и требуют, чтобы в буфере было место для заверша­ющего символа строки.

При использовании библиотеки мы обнаружили, что нужно использовать две модели строковых функ­ций. В большинстве случаев, если буфер для хранения строки переполняется, это означает, что в программе допущена ошибка, и библиотека прекращает опера­цию; так ведет себя strcpy_s. Реже встречаются ситуа­ции, когда корректным поведением является усечение строки. Чтобы обеспечить их поддержку, мы добавили в функцию strncpy_s новый режим. Когда в последнем параметре передается _TRUNCATE, пользователь по­лучает часть исходной строки, способную уместиться в выходном буфере (с учетом завершающего символа); если строка усечена, возвращается STRUNCATE.

В некоторых случаях мы не добавляли в функции дополнительные параметры, зато принципиально из­менили поведение функций. Например, мы сделали, чтобы функция fopen_s по умолчанию открывала фай­лы в монопольном режиме, а не в режиме совместного доступа. Это гораздо более безопасный режим по умол­чанию, тем не менее, мы изменили имя функции, чтобы вы выбирали новое поведение явно.

В табл. 2 показана небольшая группа функций, ко­торые мы добавили, хотя их аналоги не устарели. В каж­дом из этих случаев мы добавляли функцию, поскольку она обеспечивала большую безопасность в определен­ных ситуациях, но не рассчитывали, что они вытеснят другие функции. Например, в qsort_s добавлена пере­менная контекста (context variable), которая может ока­заться крайне полезной, когда вы сортируете данные и желаете передать дополнительную информацию о кон­тексте, не хранящуюся в самих сортируемых объектах. Раньше разработчики часто использовали для этой це­ли статические переменные, что было чревато пробле­мами с реентерабельностью и многопоточным досту­пом. А функция memcpy_s принимает дополнительный параметр, задающий размер буфера-приемника. Чаще всего этот размер будет совпадать с размером исходного буфера, но некоторые разработчики полагают, что код удобнее читать, когда заданы оба размера.

Одно из главных преимуществ новых функций в том, что им всегда известен размер буфера, в который они записывают данные. Это позволило нам добавить в CRT новую отладочную функциональность. Всякий раз, когда вы вызываете функцию (например strcpy_s) в режиме _DEBUG, библиотечный код полностью за­полняет выходной буфер. Благодаря этому, если вы не­правильно зададите размер буфера, ошибку удастся обнаружить гораздо быстрее. Кроме того, это помогает выявить другие трудно уловимые ошибки, например использование переменных, которые уже уничтожены или вышли за область видимости.

Эти изменения относятся к библиотеке для языка C, но мы добавили и кое-какой код, специфичный для C++, чтобы еще больше сократить издержки из-за по­вышения безопасности кода. На рис. 2 показано, как это работает.

В разделе A показан еще один простой фрагмент кода на языке C. При использовании Safe Libraries для всех трех строк кода выводятся предупреждения, пос­кольку не заданы размеры буферов. Как видите, на са­мом деле эта функция может обойтись без временного буфера, но я буду использовать его для иллюстрации.

В разделе B представлено простое исправление, ко­торое уже рассматривалось выше. В нем, чтобы задать размер выходного буфера, функции wcscpy_s передает­ся результат вызова функции _countof.

Раздел C демонстрирует еще более простое исправ­ление, но оно работает, только если вы компилируете этот код как C++-код. Данный код гораздо сильнее на­поминает первоначальный. Вместо добавления пара­метров мы просто изменили имена первых двух функ­ций. Мы применили шаблоны, чтобы при первых двух вызовах определялся размер буфера. Поскольку temp - локальный буфер фиксированного размера, механизм шаблонов может автоматически определить размер бу­фера и передать его функции wcscpy_s.

В разделе D мы возвращаемся к первоначально­му коду, но используем #define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES. Этот режим не ис­пользуется по умолчанию, поскольку может привести к проблемам при применении некоторых нестандартных шаблонов кода. Но его задание значительно сокращает затраты труда. После указания #define мы не получаем предупреждений при первых двух вызовах. Всякий раз, когда используется буфер-приемник фиксированного размера, вызов wcscpy с помощью шаблона преобразо­вывается в вызов wcscpy_s. Конечно, если буфер объяв­лен вне функции или под него выделена память из ку­чи, вам придется задавать его размер, поэтому для по-следней строки выводится предупреждение.

В разделе E показано, какие минимальные доработ­ки нужны, чтобы исправить функцию при использова­нии _CRT_SECURE_CPP_OVERLOAD_ STANDARD_NAMES. Вам требуется лишь внести изменение для случая, когда используется динамический выходной буфер. Остальной код будет компилироваться без изме­нений. Мы применили такой подход, когда в конце про­шлого года адаптировали к Safe Libraries кодовую ба­зу целого подразделения по разработке, и это действи­тельно позволило сократить затраты труда.

Safe C++ Library

Новая библиотека Standard C++ Library безопаснее и требует от программиста меньших усилий, поскольку ее разработали недавно и поскольку благодаря инкапсуля­ции, свойственной C++-классам, обеспечивается боль­шая гибкость при внесении изменений. Основная про­блема при использовании Standard C++ Library состоит в том, что ее создатели приложили огромные усилия, чтобы сделать ее такой же эффективной, как традицион­ные итерации в языке C, основанные на указателях. Это означает, что во многих случаях простому итератору из­вестно о своем контексте не более, чем указателю, кото­рый ссылается на какой-либо элемент внутри массива.

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

Очевидно, что здесь приходится выбирать между эффективностью и контролем границ. В предыдущих версиях библиотечный код по умолчанию выполнял эффективные итерации, а в новой версии добавлен бо­лее безопасный режим, гарантирующий, что итератор работает корректно. На рис. 3 приведен пример исполь­зования этого режима.

В разделе A показана простая функция, выводящая известную всем строку. Этот код отлично работает, но при компиляции вы получите предупреждение о том, что _Copy_opt устарела. Предупреждение означает, что вы не задали размер буфера-приемника.

В разделе B дана исправленная версия этого ко­да, где используется новый итератор с проверкой. При объявлении такого итератора указываются обертывае­мый указатель и размер. При реализации Standard C++ Library нам посчастливилось сотрудничать с компанией Dinkumware. Мы добавляли в библиотеки более безопас­ные итераторы и переводили код Dinkumware на исполь­зование функций Safe C Runtime, а Dinkumware занима­лась реализацией новой отладочной функциональности итераторов. Эта функциональность доступна только в режиме _DEBUG, тем не менее, она позволяет выявить множество ошибок, например недопустимые итераторы или итераторы, для которых некорректно задан набор.

На рис. 4 приведен код, демонстрирующий эти два вида проверок в период выполнения. Как осуществля­ется проверка, зависит от режима компиляции прило­жения (отладочная версия или окончательная). Когда в окончательной версии кода происходит ошибка в пе­риод выполнения, C++ предоставляет два логичных ва­рианта. Вы можете прервать процесс, как это делается в C, или сгенерировать исключение, что обычно дела­ется в C++-коде. Доступны оба варианта. Если вы при­своите _SECURE_SCL_THROWS значение 1, то, когда вы выйдете за границы контейнера, вместо прерывания процесса возникнет исключение.

Другие усовершенствования Visual Studio 2005в области безопасности

В Safe Libraries появились и другие усовершенствова­ния в области безопасности, заслуживающие внима­ния. Объем статьи не позволяет подробно рассмотреть каждое из них, но я вкратце расскажу об этих усовер­шенствованиях, чтобы вы знали, с чего начать ознаком­ление с ними.

Мы проделали огромную работу над MFC и ATL, но, поскольку эти библиотеки имеют высокоуровне­вую природу, нам не пришлось вносить в них сущест­венные изменения, чтобы все их функции корректно работали со строковыми буферами. Мы перевели их на использование функций Safe C Library вместо старых функций. В некоторых случаях в MFC или ATL приме­нялись функции, в которых не задавался размер выход­ного буфера, - мы объявляли такие функции устарев­шими и добавляли новые перегруженные версии с до­полнительными параметрами.

В новую версию также внесены существенные из­менения в модель развертывания и обновления. Было важно обеспечить, чтобы разработчики ПО распростра­няли вместе со своими приложениями обновленные ко­пии библиотек. Когда мы выпускаем сервисный пакет для Visual Studio, мы не обновляем автоматически DLL на компьютере каждого пользователя. Однако в Visual C++ 2005 мы поддерживаем DLL, существующие в не­скольких версиях (side-by-side execution), при установке и использовании библиотек. Каждому исполняемому файлу, собранному в Visual C++, требуется манифест, указывающий, откуда берутся копии MSVCR80 и дру­гих библиотечных DLL. Поддержка DLL, существую­щих в нескольких версиях, позволяет устанавливать DLL в общий каталог (%systemroot%WinSxS) или в ло­кальный каталог приложения.

Эта технология дает ряд преимуществ, в том чис­ле избавляет от так называемой проблемы DLL Hell. Однако с точки зрения безопасности ключевым пре­имуществом является то, что в экстренных случаях мы можем напрямую обновить библиотеки на вашем ком­пьютере с помощью Windows Update. Это централизо­ванное обновление влияет на все приложения, в том числе и на те, у которых библиотеки установлены в ло­кальный каталог приложения. Конечно, такое обновле­ние - крайняя мера. Мы прилагаем огромные усилия, чтобы в наших библиотеках не было проблемного кода. Но, если в распространяемых компонентах обнаружит­ся серьезная проблема, связанная с безопасностью, мы можем установить исправления прямо на компьютеры заказчиков.

Заключение

Благодаря работе в группе разработки Visual C++ мне представилась возможность сделать код лучше. Safe Libraries позволят вам и вашему коллективу существен­но повысить надежность и безопасность своих приложе­ний, приложив совсем немного усилий.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=2698