В чем сложность написания безопасного кода в 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 кода ).
Также хочется сказать насчет соло-разработчиков, как бы хороши вы не были, ваш код - это решето до тех пор, пока его не проревьювили другие люди, до тех пор, пока их не проверили тестеры. Это факт.
Ну вот наверное и все, о чем я хотел рассказать.
Чем это отличается от написания 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 кода ).
Также хочется сказать насчет соло-разработчиков, как бы хороши вы не были, ваш код - это решето до тех пор, пока его не проревьювили другие люди, до тех пор, пока их не проверили тестеры. Это факт.
Ну вот наверное и все, о чем я хотел рассказать.
>> дающих 100%-ную проверку вашего кода
ОтветитьУдалитьТакого нет и не будет ни в ring3, ни в ring0. Доказательство 100% корректности программы сводиться к проблеме остановки машины Тьюринга. То есть невозможно. Так что, все, что есть в нашем распоряжении - это различные методики тестирования (парное программирование, юнит-тесты, ревью кода, отслеживание покрытия и тд).
Что делать для стабилизации кода? В первую очередь, минимизировать использование недокументированных возможностей, а там, где они используются, производить тестирование с особой тщательностью, ибо тут вероятность ошибки самая большая.
Во-вторых, минимизировать количество ring0 кода. Драйвер должен иметь набор API, используемый из ring3 с помощью классов-оберток.
В общем, инкапсуляция - наше все!
Машина Тьюринга это абстракция работающая с бесконечным числом состояний, а мы дело имеем с конечным числом состояний, так что слово "невозможно" некорректно. Тут нет ничего невозможного, просто еще нет подобных инструментов.
ОтветитьУдалитьС остальным согласен.
Ок, пусть число состояний конечно. Но представьте следующее. На каждой ветке if[-else] возможно два варианта поведения программы в зависимости от выражения в if() /* читай - "состояния" */, так? Значит, если в вашей программе 64 оператора if, то для доказания 100%-ой корректности работы программы нужно перебрать как минимум 2^64 состояний программы, а это, мы понимаем, очень много. В реальных программах тысячи условных операторов, потому _доказать_ их 100% корректность _практически_ не представляется возможным. Вот почему нам приходится лишь прогонять серию (возможно, довольно большую) тестов, доказывающую лишь то, что в программе, возможно (но скорее всего это не так), нет ошибок.
ОтветитьУдалитьПроверять нужно не все if'ы, а лишь те, которые встречаются на графе code-flow от входных данных, т.к. по сути интересны лишь входные данные и их обработка.
ОтветитьУдалитьbullseye нормально работает как coverage.
ОтветитьУдалитьСначала разработка сводится к наступанием на грабли.
А вообщем если, программировал под кернел, значит 10 раз просмотрел код, прежде чем запустить unittest.