Обработка структурных исключений
Приемы, описанные выше, реализуются с без особых усилий и излишних накладных расходов. Единственным серьезным недостатком является их несовместимость со стандартными библиотеками, т.к. они интенсивно используют завершающий символ нуля и не умеют по указателю на начало буфера определять его размер. Частично эта проблема может быть решена написанием "оберток" – слоя переходного кода, "посредничающего" между стандартными библиотеками и вашей программой.
Но следует помнить, что описанные подходы сам по себе еще не защищает от ошибок переполнения, а только уменьшают вероятность их появления. Они исправно работают в том, и только в том случае, когда разработчик всегда помнит необходимости постоянного контроля за границами массивов.
Практически гарантировать выполнение такого требования невозможно и в любой "полновесной" программе, состоящей из сотен и более тысяч строк, ошибки всегда есть. Это – аксиома, не требующая доказательств.
К тому же, чем больше проверок делает программа, тем "тяжелее" и медлительнее получается откомпилированный код и тем вероятнее, что хотя бы одна из проверок реализована неправильно или по забывчивости не реализована вообще!
Можно ли, избежав нудных проверок, в то же время получить высокопроизводительный код, гарантированно
защищенный от ошибок переполнения?
Несмотря на смелость вопроса, ответ положительный, да – можно! И поможет в этом обработка структурных исключений (SEH). В общих чертах смысл идеи следующий – выделяется некий буфер, с обоих сторон "окольцованный" несуществующими страницами памяти и устанавливается обработчик исключений, "отлавливающий" прерывания, вызываемые процессором при попытке доступа к несуществующей странице (вне зависимости от того, был ли запрос на запись или чтение).
Необходимость постоянного контроля границ массива при каждом к нему обращении отпадает! Точнее, теперь она ложится на плечи процессора, а от программиста требуется всего лишь написать несколько строк кода, возвращающего ошибку или увеличивающего размер буфера при его переполнении.
Единственным незакрытым лазом останется возможность прыгнув далеко-далеко за конец буфера случайно попасть на не имеющую к нему никакого отношения, но все-таки существующую страницу. В этом случае прерывание вызвано не будет и обработчик исключений ничего не узнает о факте нарушения. Однако, такая ситуация достаточно маловероятна, т.к. чаще всего буфера читаются и пишутся последовательно, а не в разброс, поэтому, ей можно пренебречь.
Преимущество от использования технологии обработки структурных исключений заключаются в надежности, компактности и ясности, использующего его программного кода, не отягощенного беспорядочно разбросанными проверками, затрудняющими его понимание.
Основной недостаток – плохая переносимость и системно - зависимость. Не всякие операционные системы позволяют прикладному коду манипулировать на низком уровне со страницами памяти, а те, что позволяют – реализуют это по-своему. Операционные системы семейства Windows такую возможность к счастью поддерживают, причем на довольно продвинутом уровне.
Функция VirtualAlloc обеспечивает выделение региона виртуальной памяти, (с которым можно обращаться в точности как и с обычным динамическим буфером), а вызов VirtualProtect позволят изменить его атрибуты защиты. Можно задавать любой требуемый тип доступа, например, разрешить только чтение памяти, но не запись или исполнение. Это позволяет защищать критически важные структуры данных от их разрушения некорректно работающими функциями. А запрет на исполнение кода в буфере даже при наличие ошибок переполнения не дает злоумышленнику никаких шансов запустить собственноручно переданный им код.
Использование функций, непосредственно работающих с виртуальной памятью, воистину позволяет творить настоящие чудеса, на которые принципиально не способны функции стандартной библиотеки Си/Cи ++.
Единственный их недостаток заключается в непереносимости. Однако, эта проблема может быть решена написанием собственной реализации функций VirtualAlloc, VirtualProtect и некоторых других, пускай в некоторых случаях на уровне компонентов ядра, а обработка структурных исключений изначально заложена в С++.
Таким образом, затраты на портирование приложений, построенных с учетом описанных выше технологий программирования, в принципе возможны, хотя и требует значительных усилий. Но эти усилия не настолько чрезмерны, что бы не окупить полученный результат.