воскресенье, 24 апреля 2011 г.

Безопасный ядерный код

В чем сложность написания безопасного кода в ring0?
Чем это отличается от написания ring3 кода?
Какие инструменты использовать для написания безопасного кода?
Об этом сегодняшняя минизаметка.

Прежде всего нужно сказать о различиях между юзермодным и ядерным кодом:

Во-первых - время разработки. Юзермодный код пишется быстро, он относительно стабилен.
Самое страшное, что может случиться, это падение программы с access violation, а в случае же с драйвером, падение драйвера - это падение всей операционной системы в синий экран смерти(BSOD). Так что волей-неволей, драйверописателю приходится проверять код на виртуальной машине, а это время.

Во-вторых - объем знаний, ring3 разработчику достаточно знать msdn и какие-нибудь библиотеки типа boost'a, если чего-то не известно - открывается документация и там все написано. Для ring0 разработчика знания msdn недостаточно, нужно знать всю систему windows, все её шестеренки и механизмы сформировавшиеся за 25+ летнюю историю этой ОС, которые зачастую нигде не описаны, так что довольно часто приходится самому разбираться в кишках ОС, а для этого есть лишь один источник информации - анализ бинарного кода, а тут требуются знания reverse engineering'a, ассемблера и кучи всяких вспомогательных инструментов/языков помогающих в анализе.

В-третьих - среда. Для той же отладки в юзермоде существуют удобные отладчики с отличной usability. Для ядра существует по сути один инструмент - windbg с неудобным интерфейсом прошлого века(softice вымер, syser нестабилен).

В-четвертых - окружение. Для ring3 программы есть несколько слоев абстракции, навернутых над системными вызовами ядра(int2e/sysenter/syscall), это native api и win api. Они нужны для одной простой цели - для совместимости с разными версиями операционной системы. То есть интерфейсы для юзермодных программ всегда одни и теже, программист об этом даже не думает. В ring0 естественно есть такие же незыблемые интерфейсы предоставляемые ядром, но часто бывают задачи, требующие поиска/выполнения недокументированных ф-ций, что приводит к тестированию этого дела на каждой ОС или даже на каждом сервис паке.

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

Как максимизировать надежность кода?

Просто следовать правилам:

Следовать code-standard'у, использовать SAL Annotations, ASSERT's, проверять аргументы ф-ций, проверять любые данные пришедшие из юзермода (try/except + probe write/read), использовать статические анализаторы кода ( prefast / lint ) которые могут найти какие-то ошибки на этапе компиляции, использовать юнит-тесты и различного рода ревью кода ( self-review/lightweight code review/formal code review ).
Также не нужно забывать о динамических анализаторах, самый известный из которых это driver verifier, он подменяет многие ф-ции своими, следит за утечками памяти, irp'ами, хендлами и т.д. Плюс не надо забывать о контроле выделяемой памяти ( использовать тэги при выделении памяти ), проверяя расход памяти с помощью pooltag/driver verifier/windbg.
Если есть какие-то сомнения, насчет производительности кода, можно использовать всякие профайлеры/счетчики ( wmi/etw/kernrate ).

Ну и финальный барьер - выделенные сервера, на которых могут крутиться ваши разработки + тестеры. Тестеры могут найти баги, о которых вы и понятия не имели ( конечно это зависит напрямую от опыта тестера ).

Когда все эти стадии пройдены, можно сказать, что ваш код готов идти в production. Однако и там будут случаться периодически оказии, и придется фиксить баги. От этого никуда не деться, пока нет инструментов, дающих 100%-ную проверку вашего кода ( по крайней мере для ring0 кода ).

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

Ну вот наверное и все, о чем я хотел рассказать.

5 комментариев:

  1. >> дающих 100%-ную проверку вашего кода

    Такого нет и не будет ни в ring3, ни в ring0. Доказательство 100% корректности программы сводиться к проблеме остановки машины Тьюринга. То есть невозможно. Так что, все, что есть в нашем распоряжении - это различные методики тестирования (парное программирование, юнит-тесты, ревью кода, отслеживание покрытия и тд).

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

    Во-вторых, минимизировать количество ring0 кода. Драйвер должен иметь набор API, используемый из ring3 с помощью классов-оберток.

    В общем, инкапсуляция - наше все!

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

    С остальным согласен.

    ОтветитьУдалить
  3. Ок, пусть число состояний конечно. Но представьте следующее. На каждой ветке if[-else] возможно два варианта поведения программы в зависимости от выражения в if() /* читай - "состояния" */, так? Значит, если в вашей программе 64 оператора if, то для доказания 100%-ой корректности работы программы нужно перебрать как минимум 2^64 состояний программы, а это, мы понимаем, очень много. В реальных программах тысячи условных операторов, потому _доказать_ их 100% корректность _практически_ не представляется возможным. Вот почему нам приходится лишь прогонять серию (возможно, довольно большую) тестов, доказывающую лишь то, что в программе, возможно (но скорее всего это не так), нет ошибок.

    ОтветитьУдалить
  4. Проверять нужно не все if'ы, а лишь те, которые встречаются на графе code-flow от входных данных, т.к. по сути интересны лишь входные данные и их обработка.

    ОтветитьУдалить
  5. bullseye нормально работает как coverage.
    Сначала разработка сводится к наступанием на грабли.
    А вообщем если, программировал под кернел, значит 10 раз просмотрел код, прежде чем запустить unittest.

    ОтветитьУдалить