Writing drivers for KolibriOS/ru: Difference between revisions

From KolibriOS wiki
Jump to navigation Jump to search
m (Added interlinks for versions and apps)
m (Added links to SVN files/revisions)
Line 7: Line 7:
Далее допустим, что вы всё ещё читаете эту статью. Мало ли, может, вы всегда пишете код с первого раза безошибочно (чего только на свете не бывает), или в совершенстве владеете отладкой прямо в мозгу и считаете всякие отладочные средства баловством, или просто считаете, что настоящий мужчина (настоящая леди?) не боится трудностей и несколькими строчками текста вас не напугать.
Далее допустим, что вы всё ещё читаете эту статью. Мало ли, может, вы всегда пишете код с первого раза безошибочно (чего только на свете не бывает), или в совершенстве владеете отладкой прямо в мозгу и считаете всякие отладочные средства баловством, или просто считаете, что настоящий мужчина (настоящая леди?) не боится трудностей и несколькими строчками текста вас не напугать.


'''Предупреждение 2.''' Драйвера, естественно, тесно связаны с ядром. А в ядро КолибриОС вносятся изменения несколько раз в неделю. Разумеется, большинство изменений никак не касается драйверной подсистемы, но иногда добавляются/исчезают/изменяются важные системные функции, экспортируемые драйверам. Поэтому если вы возьмёте и скомпилируете прилагаемый к статье код, то, возможно, он прямо в таком виде работать не будет. Так что внимательно читайте текст - я постараюсь выделить по возможности все причины неработоспособности в будущем и требуемые модификации. Прилагаемый к статье код рассчитан на ревизию svn.450, последнюю на момент написания этих строк (в дистрибутиве [[Version:0.6.5.0|0.6.5.0]] работать в таком виде не будет).
'''Предупреждение 2.''' Драйвера, естественно, тесно связаны с ядром. А в ядро КолибриОС вносятся изменения несколько раз в неделю. Разумеется, большинство изменений никак не касается драйверной подсистемы, но иногда добавляются/исчезают/изменяются важные системные функции, экспортируемые драйверам. Поэтому если вы возьмёте и скомпилируете прилагаемый к статье код, то, возможно, он прямо в таком виде работать не будет. Так что внимательно читайте текст - я постараюсь выделить по возможности все причины неработоспособности в будущем и требуемые модификации. Прилагаемый к статье код рассчитан на ревизию {{#svn_rev:450}}, последнюю на момент написания этих строк (в дистрибутиве [[Version:0.6.5.0|0.6.5.0]] работать в таком виде не будет).


Вообще-то основная задача драйверов - обеспечить работу с оборудованием. Но поскольку эта статья ставит своей целью показать принципы работы драйверов, а для реализации основной задачи нужно много кода, работающего именно с железом и не имеющего никакого отношения к драйверной подсистеме, то процесс написания драйвера показан на следующем примере: создадим драйвер, перехватывающий и записывающий все обращения приложений к файловой системе, и управляющую программу, которая получает данные от драйвера и отображает их. В качестве средства разработки используется [http://ru.wikipedia.org/wiki/FASM FASM].
Вообще-то основная задача драйверов - обеспечить работу с оборудованием. Но поскольку эта статья ставит своей целью показать принципы работы драйверов, а для реализации основной задачи нужно много кода, работающего именно с железом и не имеющего никакого отношения к драйверной подсистеме, то процесс написания драйвера показан на следующем примере: создадим драйвер, перехватывающий и записывающий все обращения приложений к файловой системе, и управляющую программу, которая получает данные от драйвера и отображает их. В качестве средства разработки используется [http://ru.wikipedia.org/wiki/FASM FASM].
Line 14: Line 14:
== Драйвер ==  
== Драйвер ==  


Специально для желающих написать свой драйвер предоставляется каркас драйвера. Он находится в svn-репозитории вместе с ядром, точнее, в папке [svn://kolibrios.org/kernel/trunk/drivers svn://kolibrios.org/kernel/trunk/drivers]. В исходниках дистрибутива [[Version:0.6.5.0|0.6.5.0]] этот путь соответствует папке <tt>kernel/drivers</tt>. Ну что же, давайте посмотрим (<tt>sceletone.asm</tt> из svn.450):
Специально для желающих написать свой драйвер предоставляется каркас драйвера. Он находится в svn-репозитории вместе с ядром, точнее, в папке {{#svn:/kernel/trunk/drivers|svn://kolibrios.org/kernel/trunk/drivers}}. В исходниках дистрибутива [[Version:0.6.5.0|0.6.5.0]] этот путь соответствует папке {{#svn:/kernel/trunk/drivers|kernel/drivers}}. Ну что же, давайте посмотрим ({{#svn:/kernel/trunk/drivers/sceletone.asm|sceletone.asm из #450|450}}):


<asm>
<asm>
Line 32: Line 32:
</asm>
</asm>


Ну, "[http://ru.wikipedia.org/wiki/Copyright Copyright]" - он и есть копирайт, комментарий в следующей строке извещает нас, куда же мы собственно попали, это неинтересно. Дальше мы должны известить компилятор, какой формат мы хотим получить. Драйвера должны иметь формат объектных файлов [http://en.wikipedia.org/wiki/COFF COFF]. Вот это и сказано. Матерное (для кого-то) слово посередине всего лишь означает (не подумайте ничего плохого!), что используется расширение формата [http://en.wikipedia.org/wiki/COFF COFF], введённое [http://ru.wikipedia.org/wiki/Microsoft Microsoft] и позволяющее указывать у секций атрибуты типа "writeable". В общем, так должно быть для всех драйверов. Дальше идёт включение вспомогательных файлов. <tt>proc32.inc</tt> содержит макросы для определения и вызова стандартных процедур (<tt>proc/endp</tt>, <tt>stdcall/ccall/invoke/cinvoke</tt>, <tt>local</tt> для создания локальных переменных) и находится в той же папке, что и <tt>sceletone.asm</tt>. Его можно включать или не включать, макросы оттуда можно использовать или не использовать, в этой статье они используются, чтобы не усложнять восприятие (вообще говоря, при стремлении к максимальной эффективности использование макросов может повредить, но это тема отдельных жарких споров). <tt>imports.inc</tt> содержит объявления для всех экспортируемых функций ядра. Загляните туда, ничего сложного там нет, просто куча стереотипных конструкций. На самом деле (в смысле того, как разрешается импорт при загрузке драйвера) список всех экспортируемых функций и данных ядра находится в файле <tt>core/exports.inc</tt> (метка <tt>kernel_export</tt>), так что если вам как программисту ядра вдруг понадобиться что-нибудь своё экспортировать, лезьте туда (ну и <tt>imports.inc</tt> отредактируйте из вежливости к другим).
Ну, "[http://ru.wikipedia.org/wiki/Copyright Copyright]" - он и есть копирайт, комментарий в следующей строке извещает нас, куда же мы собственно попали, это неинтересно. Дальше мы должны известить компилятор, какой формат мы хотим получить. Драйвера должны иметь формат объектных файлов [http://en.wikipedia.org/wiki/COFF COFF]. Вот это и сказано. Матерное (для кого-то) слово посередине всего лишь означает (не подумайте ничего плохого!), что используется расширение формата [http://en.wikipedia.org/wiki/COFF COFF], введённое [http://ru.wikipedia.org/wiki/Microsoft Microsoft] и позволяющее указывать у секций атрибуты типа "writeable". В общем, так должно быть для всех драйверов. Дальше идёт включение вспомогательных файлов. {{#svn:/kernel/trunk/proc32.inc|proc32.inc}} содержит макросы для определения и вызова стандартных процедур (<tt>proc/endp</tt>, <tt>stdcall/ccall/invoke/cinvoke</tt>, <tt>local</tt> для создания локальных переменных) и находится в той же папке, что и {{#svn:/kernel/trunk/drivers/sceletone.asm|sceletone.asm|450}}. Его можно включать или не включать, макросы оттуда можно использовать или не использовать, в этой статье они используются, чтобы не усложнять восприятие (вообще говоря, при стремлении к максимальной эффективности использование макросов может повредить, но это тема отдельных жарких споров). {{#svn:/kernel/trunk/imports.inc|imports.inc}} содержит объявления для всех экспортируемых функций ядра. Загляните туда, ничего сложного там нет, просто куча стереотипных конструкций. На самом деле (в смысле того, как разрешается импорт при загрузке драйвера) список всех экспортируемых функций и данных ядра находится в файле {{#svn:/kernel/trunk/core/exports.inc|core/exports.inc}} (метка <tt>kernel_export</tt>), так что если вам как программисту ядра вдруг понадобиться что-нибудь своё экспортировать, лезьте туда (ну и {{#svn:/kernel/trunk/imports.inc|imports.inc}} отредактируйте из вежливости к другим).


Внимание! Здесь появляется возможность несовместимости: если вы используете какие-нибудь функции, которых (ещё или уже) нет в ядре, на которое вы рассчитываете, ядро откажется грузить ваш драйвер (ругнувшись на доске отладки нехорошим словом на ненашем языке "unresolved" с указанием имени функции).
Внимание! Здесь появляется возможность несовместимости: если вы используете какие-нибудь функции, которых (ещё или уже) нет в ядре, на которое вы рассчитываете, ядро откажется грузить ваш драйвер (ругнувшись на доске отладки нехорошим словом на ненашем языке "unresolved" с указанием имени функции).
Line 44: Line 44:
</asm>
</asm>


Константа <tt>OS_BASE</tt> означает адрес загрузки ядра. Для "официального" ядра (в частности, в [[Version:0.6.5.0|0.6.5.0]]) это 0, для "плоского" ядра это 0x80000000 - вот вам ещё одна несовместимость. Имейте в виду, что в будущем (возможно даже, что в скором) "плоское" ядро станет (хотя, может быть, и не станет - мало ли что?) "официальным", так что не рассчитывайте, что <tt>OS_BASE</tt> всегда будет нулём. Константа <tt>new_app_base</tt> означает линейный адрес, по которому загружаются приложения: все приложения загружаются по одному и тому же адресу, наложения не происходит, поскольку каждый процесс имеет свою таблицу страниц, при этом каждое приложение искренне уверено, что загружено по нулевому адресу - это достигается за счёт сегментной адресации - в 3-кольце селекторы <tt>cs/ds/es/ss</tt> имеют базу <tt>new_app_base</tt>, а в 0-кольце (в ядре и драйверах) - нулевую базу. Таким образом, для перевода адреса в приложении в указатель ядра нужно прибавить к нему <tt>new_app_base</tt> (если непонятно, почему, примите это как факт). С <tt>new_app_base</tt> несовместимость ещё хуже: в [[Version:0.6.5.0|0.6.5.0]] она равна 0x60400000, в svn.450 - уже 0x80000000, в "плоском" ядре просто 0 (собственно, потому оно и "плоское", что использует плоскую модель памяти). Как узнать конкретные значения <tt>OS_BASE</tt> и <tt>new_app_base</tt> для данного ядра? Очень просто - они прописаны именно под такими именами в <tt>const.inc</tt> (из исходников ядра), так что достаточно найти их там. Третья из определяемых констант нужна для отвода глаз, в данном случае она не используется. Кстати, карта памяти Колибри располагается в <tt>memmap.inc</tt> из исходников ядра.
Константа <tt>OS_BASE</tt> означает адрес загрузки ядра. Для "официального" ядра (в частности, в [[Version:0.6.5.0|0.6.5.0]]) это 0, для "плоского" ядра это 0x80000000 - вот вам ещё одна несовместимость. Имейте в виду, что в будущем (возможно даже, что в скором) "плоское" ядро станет (хотя, может быть, и не станет - мало ли что?) "официальным", так что не рассчитывайте, что <tt>OS_BASE</tt> всегда будет нулём. Константа <tt>new_app_base</tt> означает линейный адрес, по которому загружаются приложения: все приложения загружаются по одному и тому же адресу, наложения не происходит, поскольку каждый процесс имеет свою таблицу страниц, при этом каждое приложение искренне уверено, что загружено по нулевому адресу - это достигается за счёт сегментной адресации - в 3-кольце селекторы <tt>cs/ds/es/ss</tt> имеют базу <tt>new_app_base</tt>, а в 0-кольце (в ядре и драйверах) - нулевую базу. Таким образом, для перевода адреса в приложении в указатель ядра нужно прибавить к нему <tt>new_app_base</tt> (если непонятно, почему, примите это как факт). С <tt>new_app_base</tt> несовместимость ещё хуже: в [[Version:0.6.5.0|0.6.5.0]] она равна 0x60400000, в {{#svn_rev:450}} - уже 0x80000000, в "плоском" ядре просто 0 (собственно, потому оно и "плоское", что использует плоскую модель памяти). Как узнать конкретные значения <tt>OS_BASE</tt> и <tt>new_app_base</tt> для данного ядра? Очень просто - они прописаны именно под такими именами в <tt>const.inc</tt> (из исходников ядра), так что достаточно найти их там. Третья из определяемых констант нужна для отвода глаз, в данном случае она не используется. Кстати, карта памяти Колибри располагается в {{#svn:/kernel/trunk/memmap.inc|memmap.inc}} из исходников ядра.


Едем дальше:
Едем дальше:
Line 71: Line 71:
</asm>
</asm>


Выше мы импортировали из ядра нужные нам функции (<tt>imports.inc</tt>). А теперь мы даём ядру знать о себе. Начнём с конца. Переменная <tt>version</tt>, объявленная гораздо ниже в тексте, - это... нет, не версия драйвера, как можно было бы подумать! Это версия драйверного интерфейса, которую этот драйвер понимает. Ещё точнее, в одном <tt>dword</tt> закодированы два кода версии. Младшее слово в текущей реализации ядра не проверяется никак, но туда следует помещать номер версии интерфейса, "родной" для драйвера. Старшее слово означает минимальную версию, с которой драйвер ещё может работать. Это слово должно лежать на отрезке от <tt>DRV_COMPAT</tt> до <tt>DRV_CURRENT</tt>, константы определены в исходниках ядра в <tt>core/dll.inc</tt>, в [[Version:0.6.5.0|0.6.5.0]] обе эти константы равны 3, в svn.450 интерфейс уже изменился и теперь обе константы равны 4. Для чего нужны все эти сложности? Дело в следующем. Изменения в драйверной подсистеме могут быть следующих типов: полная или частичная переделка одной из базовых концепций; удаление одной из экспортируемых функций ядра; модификация функции (вчера функция принимала аргумент в стеке, а сегодня для эффективности аргумент передаётся в регистре; или добавился ещё какой-то аргумент; или изменился смысл аргументов и т.п.); добавление функции. В первом и третьем случае, собственно, ничего не поделаешь, драйверы переписывать надо. Второй тоже приводит к несовместимости. Но обидно перекомпилировать все драйвера только из-за того, что появилась новая функция, без которой эти драйвера прекрасно обходились. Вот и поддерживается загрузка "устаревших, но не слишком" драйверов.
Выше мы импортировали из ядра нужные нам функции ({{#svn:/kernel/trunk/imports.inc|imports.inc}}). А теперь мы даём ядру знать о себе. Начнём с конца. Переменная <tt>version</tt>, объявленная гораздо ниже в тексте, - это... нет, не версия драйвера, как можно было бы подумать! Это версия драйверного интерфейса, которую этот драйвер понимает. Ещё точнее, в одном <tt>dword</tt> закодированы два кода версии. Младшее слово в текущей реализации ядра не проверяется никак, но туда следует помещать номер версии интерфейса, "родной" для драйвера. Старшее слово означает минимальную версию, с которой драйвер ещё может работать. Это слово должно лежать на отрезке от <tt>DRV_COMPAT</tt> до <tt>DRV_CURRENT</tt>, константы определены в исходниках ядра в {{#svn:/kernel/trunk/core/dll.inc|core/dll.inc}}, в [[Version:0.6.5.0|0.6.5.0]] обе эти константы равны 3, в {{#svn_rev:450}} интерфейс уже изменился и теперь обе константы равны 4. Для чего нужны все эти сложности? Дело в следующем. Изменения в драйверной подсистеме могут быть следующих типов: полная или частичная переделка одной из базовых концепций; удаление одной из экспортируемых функций ядра; модификация функции (вчера функция принимала аргумент в стеке, а сегодня для эффективности аргумент передаётся в регистре; или добавился ещё какой-то аргумент; или изменился смысл аргументов и т.п.); добавление функции. В первом и третьем случае, собственно, ничего не поделаешь, драйверы переписывать надо. Второй тоже приводит к несовместимости. Но обидно перекомпилировать все драйвера только из-за того, что появилась новая функция, без которой эти драйвера прекрасно обходились. Вот и поддерживается загрузка "устаревших, но не слишком" драйверов.


<asm>
<asm>
Line 122: Line 122:
</asm>
</asm>


Это код процедуры инициализации/финализации. При загрузке драйвера она вызывается с аргументом <tt>DRV_ENTRY</tt> = 1 и должна вернуть ненулевое значение при успехе. При завершении системы она вызывается с аргументом <tt>DRV_EXIT</tt> = -1. В нашем случае драйвер не работает ни с каким железом, так что ни инициализации никакого железа, ни вообще никакой финализации нет, а есть только минимально необходимые действия, чтобы драйвер считался загруженным, а именно, регистрация. Функция <tt>RegService</tt> экспортируется ядром и принимает два аргумента: имя драйвера (до 16 символов, включая завершающий 0) и указатель на процедуру обработки I/O, а возвращает 0 при неудаче или (ненулевой) зарегистрированный хэндл при успехе. Кстати, как узнать, что делает та или иная экспортируемая функция? Допустим, нам позарез нужно выделить пару страниц памяти ядра. Лезем в исходники ядра, файл <tt>core/exports.inc</tt>, просматриваем экспортируемые имена (они осмысленны) и видим <tt>szKernelAlloc</tt>. Пролистываем вниз до метки <tt>kernel_export</tt> и ищем <tt>szKernelAlloc</tt> - обнаруживаем, что ему соответствует процедура <tt>kernel_alloc</tt>. Теперь ищем реализацию <tt>kernel_alloc</tt>, она обнаруживается в <tt>core/heap.inc</tt>. Комментариев около функции нет, но есть объявление <tt>proc</tt>, из которого следует, что функция принимает один аргумент <tt>size</tt> типа <tt>dword</tt>. Теперь по названию ясно, что <tt>kernel_alloc</tt> выделяет память ядра в размере, равном единственному аргументу. Причём первые же три строчки кода функции показывают, что размер выравнивается вверх на границу 4096 (т.е. размер одной страницы), следовательно, функция выделяет некоторое целое количество страниц, а размер задаётся в байтах.
Это код процедуры инициализации/финализации. При загрузке драйвера она вызывается с аргументом <tt>DRV_ENTRY</tt> = 1 и должна вернуть ненулевое значение при успехе. При завершении системы она вызывается с аргументом <tt>DRV_EXIT</tt> = -1. В нашем случае драйвер не работает ни с каким железом, так что ни инициализации никакого железа, ни вообще никакой финализации нет, а есть только минимально необходимые действия, чтобы драйвер считался загруженным, а именно, регистрация. Функция <tt>RegService</tt> экспортируется ядром и принимает два аргумента: имя драйвера (до 16 символов, включая завершающий 0) и указатель на процедуру обработки I/O, а возвращает 0 при неудаче или (ненулевой) зарегистрированный хэндл при успехе. Кстати, как узнать, что делает та или иная экспортируемая функция? Допустим, нам позарез нужно выделить пару страниц памяти ядра. Лезем в исходники ядра, файл {{#svn:/kernel/trunk/core/exports.inc|core/exports.inc}}, просматриваем экспортируемые имена (они осмысленны) и видим <tt>szKernelAlloc</tt>. Пролистываем вниз до метки <tt>kernel_export</tt> и ищем <tt>szKernelAlloc</tt> - обнаруживаем, что ему соответствует процедура <tt>kernel_alloc</tt>. Теперь ищем реализацию <tt>kernel_alloc</tt>, она обнаруживается в {{#svn:/kernel/trunk/core/heap.inc|core/heap.inc}}. Комментариев около функции нет, но есть объявление <tt>proc</tt>, из которого следует, что функция принимает один аргумент <tt>size</tt> типа <tt>dword</tt>. Теперь по названию ясно, что <tt>kernel_alloc</tt> выделяет память ядра в размере, равном единственному аргументу. Причём первые же три строчки кода функции показывают, что размер выравнивается вверх на границу 4096 (т.е. размер одной страницы), следовательно, функция выделяет некоторое целое количество страниц, а размер задаётся в байтах.


Дальше идёт процедура обработки запросов <tt>service_proc</tt>:
Дальше идёт процедура обработки запросов <tt>service_proc</tt>:
Line 152: Line 152:
</asm>
</asm>


Процедура обработки запросов вызывается, когда какой-то внешний код возжаждал общения именно с нашим драйвером. Это может быть как другой драйвер (формально драйвер может вызывать сам себя через механизм I/O, но смысла в этом нет), надыбавший где-то наш хэндл и вызвавший ServiceHandler, или даже само ядро (srv_handler, srv_handlerEx из core/dll.inc), так и приложение функцией 68.17 (хэндл приложение может добыть при загрузке драйвера функцией 68.16). Нулевое возвращаемое значение означает успех, ненулевое соответствует ошибке.
Процедура обработки запросов вызывается, когда какой-то внешний код возжаждал общения именно с нашим драйвером. Это может быть как другой драйвер (формально драйвер может вызывать сам себя через механизм I/O, но смысла в этом нет), надыбавший где-то наш хэндл и вызвавший ServiceHandler, или даже само ядро (srv_handler, srv_handlerEx из {{#svn:/kernel/trunk/core/dll.inc|core/dll.inc}}), так и приложение функцией 68.17 (хэндл приложение может добыть при загрузке драйвера функцией 68.16). Нулевое возвращаемое значение означает успех, ненулевое соответствует ошибке.
Вначале определяем сокращённые имена для членов структуры, описывающей запрос. В поле handle содержится хэндл драйвера (такой же, как и возвращаемое значение RegService), io_code - dword-идентификатор запроса, остальные поля вопросов вызывать не должны. Возвращаемое значение напрямую передаётся вызвавшему нас коду (драйвер/ядро/приложение). В конце восстанавливаем значения, переназначенные было на короткие имена членов структуры. В данном случае это без надобности, но в случае сложных драйверов короткие имена типа "input" запросто могут встречаться не один раз.
Вначале определяем сокращённые имена для членов структуры, описывающей запрос. В поле handle содержится хэндл драйвера (такой же, как и возвращаемое значение RegService), io_code - dword-идентификатор запроса, остальные поля вопросов вызывать не должны. Возвращаемое значение напрямую передаётся вызвавшему нас коду (драйвер/ядро/приложение). В конце восстанавливаем значения, переназначенные было на короткие имена членов структуры. В данном случае это без надобности, но в случае сложных драйверов короткие имена типа "input" запросто могут встречаться не один раз.


Дальше в sceletone.asm содержится код поиска заданного оборудования на [http://ru.wikipedia.org/wiki/PCI PCI-шине], нам он без надобности, при необходимости разберитесь сами.
Дальше в {{#svn:/kernel/trunk/drivers/sceletone.asm|sceletone.asm|450}} содержится код поиска заданного оборудования на [http://ru.wikipedia.org/wiki/PCI PCI-шине], нам он без надобности, при необходимости разберитесь сами.


Итак, с каркасом драйвера разобрались. А теперь будем писать свой драйвер.
Итак, с каркасом драйвера разобрались. А теперь будем писать свой драйвер.
Line 176: Line 176:
</asm>
</asm>


Ориентируемся на svn.450 (для [[Version:0.6.5.0|0.6.5.0]] нужно было бы писать "new_app_base equ 0x60400000"):
Ориентируемся на {{#svn_rev:450}} (для [[Version:0.6.5.0|0.6.5.0]] нужно было бы писать "new_app_base equ 0x60400000"):


<asm>
<asm>
Line 313: Line 313:
Здесь стоит отметить, что входных данных для драйвера не нужно, поля input/inp_size мы не используем. В поле out_size вызывающий код должен поместить размер буфера output. Если нас вызывает приложение, то все манипуляции с переводом указателей приложения в указатели ядра осуществляет ядро, а в структуре ioctl передаются уже подправленные указатели.
Здесь стоит отметить, что входных данных для драйвера не нужно, поля input/inp_size мы не используем. В поле out_size вызывающий код должен поместить размер буфера output. Если нас вызывает приложение, то все манипуляции с переводом указателей приложения в указатели ядра осуществляет ядро, а в структуре ioctl передаются уже подправленные указатели.


Ну а теперь часть, отвечающая за взаимодействие с драйверной подсистемой, закончилась и начинается собственно работа. Мы перехватываем функции файловой системы 6,32,33,58,70. Для этого мы используем тот факт, что общий обработчик int 0x40 вызывает конкретную функцию косвенным вызовом из таблицы servetable (код этот обработчика располагается в файле core/syscall.inc). Следовательно, если подменить нужные элементы в этой таблице на адреса наших обработчиков, то вызываться будет наш код. Узнать адрес servetable можно сканированием кода обработчика int 0x40. Адрес функции i40 легко узнать из IDT, а команда вызова в текущей реализации имеет вид "call dword [servetable+edi*4]", в машинном коде "FF 14 BD <servetable>" (в принципе никто не гарантирует, что так будет и дальше, в частности, потенциально возможна замена edi на eax; тогда нужно будет соответственно менять код).
Ну а теперь часть, отвечающая за взаимодействие с драйверной подсистемой, закончилась и начинается собственно работа. Мы перехватываем функции файловой системы 6,32,33,58,70. Для этого мы используем тот факт, что общий обработчик int 0x40 вызывает конкретную функцию косвенным вызовом из таблицы servetable (код этот обработчика располагается в файле {{#svn:/kernel/trunk/core/syscall.inc|core/syscall.inc}}). Следовательно, если подменить нужные элементы в этой таблице на адреса наших обработчиков, то вызываться будет наш код. Узнать адрес servetable можно сканированием кода обработчика int 0x40. Адрес функции i40 легко узнать из IDT, а команда вызова в текущей реализации имеет вид "call dword [servetable+edi*4]", в машинном коде "FF 14 BD <servetable>" (в принципе никто не гарантирует, что так будет и дальше, в частности, потенциально возможна замена edi на eax; тогда нужно будет соответственно менять код).


<asm>
<asm>
Line 595: Line 595:
</asm>
</asm>


На этом код заканчивается. Теперь используемые данные (мы ориентируемся на svn.450, для [[Version:0.6.5.0|0.6.5.0]] version должна быть 0x00030003):
На этом код заканчивается. Теперь используемые данные (мы ориентируемся на {{#svn_rev:450}}, для [[Version:0.6.5.0|0.6.5.0]] version должна быть 0x00030003):


<asm>
<asm>
Line 625: Line 625:
  fasm fmondrv.asm
  fasm fmondrv.asm


После этого по желанию можно упаковать fmondrv.obj с помощью kpack, ядро прекрасно загружает kpack'ованные файлы, а такая мера в данном случае уменьшает размер с 1850 байт до 757 байт. Кстати, маленькая хитрость: по смещению +4 в [http://en.wikipedia.org/wiki/COFF COFF-объектнике] хранится штамп даты/времени компиляции, ядру на него глубоко наплевать, так что можно забить его нулями любым [http://ru.wikipedia.org/wiki/HEX-редактор hex-редактором], после чего сжатый файл будет чуть-чуть меньше (в данном случае 756 байт). Для установки драйвера скопируйте его в /rd/1/drivers, после этого он готов к загрузке.
После этого по желанию можно упаковать fmondrv.obj с помощью [[App:KPACK|KPACK]], ядро прекрасно загружает kpack'ованные файлы, а такая мера в данном случае уменьшает размер с 1850 байт до 757 байт. Кстати, маленькая хитрость: по смещению +4 в [http://en.wikipedia.org/wiki/COFF COFF-объектнике] хранится штамп даты/времени компиляции, ядру на него глубоко наплевать, так что можно забить его нулями любым [http://ru.wikipedia.org/wiki/HEX-редактор hex-редактором], после чего сжатый файл будет чуть-чуть меньше (в данном случае 756 байт). Для установки драйвера скопируйте его в /rd/1/drivers, после этого он готов к загрузке.


== Управляющая программа ==
== Управляющая программа ==

Revision as of 02:56, 31 August 2007

Пишем драйвер для КолибриОС

Вступление

Предупреждение 1. Прежде чем писать драйвер, хорошо подумайте, нельзя ли обойтись средствами прикладных API, в частности, функций работы с оборудованием 41-46 и 62. Во-первых, от ошибки в кривом приложении пострадает только это кривое приложение, а кривой драйвер способен без особого труда обрушить всю систему. Во-вторых, для приложений можно вылавливать баги в отладчике MTDBG, обладающем определёнными возможностями, а для драйверов этот путь закрыт (разве что встроенный отладчик эмулятора Bochs, но он заведомо непригоден для отладки с реальным железом), так что единственным средством остаётся отладочный вывод на доску отладки BOARD со всеми недостатками.

Далее допустим, что вы всё ещё читаете эту статью. Мало ли, может, вы всегда пишете код с первого раза безошибочно (чего только на свете не бывает), или в совершенстве владеете отладкой прямо в мозгу и считаете всякие отладочные средства баловством, или просто считаете, что настоящий мужчина (настоящая леди?) не боится трудностей и несколькими строчками текста вас не напугать.

Предупреждение 2. Драйвера, естественно, тесно связаны с ядром. А в ядро КолибриОС вносятся изменения несколько раз в неделю. Разумеется, большинство изменений никак не касается драйверной подсистемы, но иногда добавляются/исчезают/изменяются важные системные функции, экспортируемые драйверам. Поэтому если вы возьмёте и скомпилируете прилагаемый к статье код, то, возможно, он прямо в таком виде работать не будет. Так что внимательно читайте текст - я постараюсь выделить по возможности все причины неработоспособности в будущем и требуемые модификации. Прилагаемый к статье код рассчитан на ревизию {{#svn_rev:450}}, последнюю на момент написания этих строк (в дистрибутиве 0.6.5.0 работать в таком виде не будет).

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

Драйвер

Специально для желающих написать свой драйвер предоставляется каркас драйвера. Он находится в svn-репозитории вместе с ядром, точнее, в папке {{#svn:/kernel/trunk/drivers|svn://kolibrios.org/kernel/trunk/drivers}}. В исходниках дистрибутива 0.6.5.0 этот путь соответствует папке {{#svn:/kernel/trunk/drivers|kernel/drivers}}. Ну что же, давайте посмотрим ({{#svn:/kernel/trunk/drivers/sceletone.asm|sceletone.asm из #450|450}}):

<asm>

;;
Copyright (C) KolibriOS team 2004-2007. All rights reserved. ;;
Distributed under terms of the GNU General Public License  ;;
;;
driver sceletone

format MS COFF

include 'proc32.inc' include 'imports.inc' </asm>

Ну, "Copyright" - он и есть копирайт, комментарий в следующей строке извещает нас, куда же мы собственно попали, это неинтересно. Дальше мы должны известить компилятор, какой формат мы хотим получить. Драйвера должны иметь формат объектных файлов COFF. Вот это и сказано. Матерное (для кого-то) слово посередине всего лишь означает (не подумайте ничего плохого!), что используется расширение формата COFF, введённое Microsoft и позволяющее указывать у секций атрибуты типа "writeable". В общем, так должно быть для всех драйверов. Дальше идёт включение вспомогательных файлов. {{#svn:/kernel/trunk/proc32.inc|proc32.inc}} содержит макросы для определения и вызова стандартных процедур (proc/endp, stdcall/ccall/invoke/cinvoke, local для создания локальных переменных) и находится в той же папке, что и {{#svn:/kernel/trunk/drivers/sceletone.asm|sceletone.asm|450}}. Его можно включать или не включать, макросы оттуда можно использовать или не использовать, в этой статье они используются, чтобы не усложнять восприятие (вообще говоря, при стремлении к максимальной эффективности использование макросов может повредить, но это тема отдельных жарких споров). {{#svn:/kernel/trunk/imports.inc|imports.inc}} содержит объявления для всех экспортируемых функций ядра. Загляните туда, ничего сложного там нет, просто куча стереотипных конструкций. На самом деле (в смысле того, как разрешается импорт при загрузке драйвера) список всех экспортируемых функций и данных ядра находится в файле {{#svn:/kernel/trunk/core/exports.inc|core/exports.inc}} (метка kernel_export), так что если вам как программисту ядра вдруг понадобиться что-нибудь своё экспортировать, лезьте туда (ну и {{#svn:/kernel/trunk/imports.inc|imports.inc}} отредактируйте из вежливости к другим).

Внимание! Здесь появляется возможность несовместимости: если вы используете какие-нибудь функции, которых (ещё или уже) нет в ядре, на которое вы рассчитываете, ядро откажется грузить ваш драйвер (ругнувшись на доске отладки нехорошим словом на ненашем языке "unresolved" с указанием имени функции).

Ну что же, идём дальше:

<asm> OS_BASE equ 0; new_app_base equ 0x60400000 PROC_BASE equ OS_BASE+0x0080000 </asm>

Константа OS_BASE означает адрес загрузки ядра. Для "официального" ядра (в частности, в 0.6.5.0) это 0, для "плоского" ядра это 0x80000000 - вот вам ещё одна несовместимость. Имейте в виду, что в будущем (возможно даже, что в скором) "плоское" ядро станет (хотя, может быть, и не станет - мало ли что?) "официальным", так что не рассчитывайте, что OS_BASE всегда будет нулём. Константа new_app_base означает линейный адрес, по которому загружаются приложения: все приложения загружаются по одному и тому же адресу, наложения не происходит, поскольку каждый процесс имеет свою таблицу страниц, при этом каждое приложение искренне уверено, что загружено по нулевому адресу - это достигается за счёт сегментной адресации - в 3-кольце селекторы cs/ds/es/ss имеют базу new_app_base, а в 0-кольце (в ядре и драйверах) - нулевую базу. Таким образом, для перевода адреса в приложении в указатель ядра нужно прибавить к нему new_app_base (если непонятно, почему, примите это как факт). С new_app_base несовместимость ещё хуже: в 0.6.5.0 она равна 0x60400000, в {{#svn_rev:450}} - уже 0x80000000, в "плоском" ядре просто 0 (собственно, потому оно и "плоское", что использует плоскую модель памяти). Как узнать конкретные значения OS_BASE и new_app_base для данного ядра? Очень просто - они прописаны именно под такими именами в const.inc (из исходников ядра), так что достаточно найти их там. Третья из определяемых констант нужна для отвода глаз, в данном случае она не используется. Кстати, карта памяти Колибри располагается в {{#svn:/kernel/trunk/memmap.inc|memmap.inc}} из исходников ядра.

Едем дальше:

<asm> struc IOCTL { .handle dd ?

  .io_code     dd ?
  .input       dd ?
  .inp_size    dd ?
  .output      dd ?
  .out_size    dd ?

}

virtual at 0

 IOCTL IOCTL

end virtual </asm>

Это просто объявление структуры (махинации с virtual - стандарт для FASM).

<asm> public START public service_proc public version </asm>

Выше мы импортировали из ядра нужные нам функции ({{#svn:/kernel/trunk/imports.inc|imports.inc}}). А теперь мы даём ядру знать о себе. Начнём с конца. Переменная version, объявленная гораздо ниже в тексте, - это... нет, не версия драйвера, как можно было бы подумать! Это версия драйверного интерфейса, которую этот драйвер понимает. Ещё точнее, в одном dword закодированы два кода версии. Младшее слово в текущей реализации ядра не проверяется никак, но туда следует помещать номер версии интерфейса, "родной" для драйвера. Старшее слово означает минимальную версию, с которой драйвер ещё может работать. Это слово должно лежать на отрезке от DRV_COMPAT до DRV_CURRENT, константы определены в исходниках ядра в {{#svn:/kernel/trunk/core/dll.inc|core/dll.inc}}, в 0.6.5.0 обе эти константы равны 3, в {{#svn_rev:450}} интерфейс уже изменился и теперь обе константы равны 4. Для чего нужны все эти сложности? Дело в следующем. Изменения в драйверной подсистеме могут быть следующих типов: полная или частичная переделка одной из базовых концепций; удаление одной из экспортируемых функций ядра; модификация функции (вчера функция принимала аргумент в стеке, а сегодня для эффективности аргумент передаётся в регистре; или добавился ещё какой-то аргумент; или изменился смысл аргументов и т.п.); добавление функции. В первом и третьем случае, собственно, ничего не поделаешь, драйверы переписывать надо. Второй тоже приводит к несовместимости. Но обидно перекомпилировать все драйвера только из-за того, что появилась новая функция, без которой эти драйвера прекрасно обходились. Вот и поддерживается загрузка "устаревших, но не слишком" драйверов.

<asm> version dd 0x00030003 </asm>

Каркас драйвера рассчитан на... версию 3, т.е. с текущим ядром он не пойдёт! Дело в том, что этот каркас в общем-то не обновлялся (если не считать копирайта) с 0.6.5.0, так что new_app_base и version остались старые. Попутно отмечу, что старшее слово - это первая тройка, а младшее - вторая в силу обратного расположения байт в слове и слов в двойном слове (вообще-то я уверен, что вы и так это знаете, но для очистки совести...)

Процедура START - это процедура, которая вызывается системой при загрузке драйвера и при завершении работы. В первом случае она должна инициализировать драйвер, во втором - наоборот. О ней речь пойдёт чуть позже.

Процедуру service_proc экспортировать совершенно ненужно, о её размещении ядро узнаёт по другим каналам, о ней речь пойдёт ещё позже.

Последняя порция констант

<asm> DEBUG equ 1

DRV_ENTRY equ 1 DRV_EXIT equ -1 STRIDE equ 4 ;size of row in devices table </asm>

(из которых первая включает код отладочного вывода в блоках if DEBUG/end if, две следующие характеризуют возможные значения аргумента у процедуры START, последняя нужна для красоты и ни для чего больше) и мы наконец-то переходим к изучению кода:

<asm> section '.flat' code readable align 16 </asm>

означает ровно-таки то, что написано;

<asm> proc START stdcall, state:dword

          cmp [state], 1
          jne .exit

.entry:

    if DEBUG
          mov esi, msgInit
          call SysMsgBoardStr
    end if
          stdcall RegService, my_service, service_proc

ret .fail: .exit:

          xor eax, eax
          ret

endp </asm>

Это код процедуры инициализации/финализации. При загрузке драйвера она вызывается с аргументом DRV_ENTRY = 1 и должна вернуть ненулевое значение при успехе. При завершении системы она вызывается с аргументом DRV_EXIT = -1. В нашем случае драйвер не работает ни с каким железом, так что ни инициализации никакого железа, ни вообще никакой финализации нет, а есть только минимально необходимые действия, чтобы драйвер считался загруженным, а именно, регистрация. Функция RegService экспортируется ядром и принимает два аргумента: имя драйвера (до 16 символов, включая завершающий 0) и указатель на процедуру обработки I/O, а возвращает 0 при неудаче или (ненулевой) зарегистрированный хэндл при успехе. Кстати, как узнать, что делает та или иная экспортируемая функция? Допустим, нам позарез нужно выделить пару страниц памяти ядра. Лезем в исходники ядра, файл {{#svn:/kernel/trunk/core/exports.inc|core/exports.inc}}, просматриваем экспортируемые имена (они осмысленны) и видим szKernelAlloc. Пролистываем вниз до метки kernel_export и ищем szKernelAlloc - обнаруживаем, что ему соответствует процедура kernel_alloc. Теперь ищем реализацию kernel_alloc, она обнаруживается в {{#svn:/kernel/trunk/core/heap.inc|core/heap.inc}}. Комментариев около функции нет, но есть объявление proc, из которого следует, что функция принимает один аргумент size типа dword. Теперь по названию ясно, что kernel_alloc выделяет память ядра в размере, равном единственному аргументу. Причём первые же три строчки кода функции показывают, что размер выравнивается вверх на границу 4096 (т.е. размер одной страницы), следовательно, функция выделяет некоторое целое количество страниц, а размер задаётся в байтах.

Дальше идёт процедура обработки запросов service_proc:

<asm> handle equ IOCTL.handle io_code equ IOCTL.io_code input equ IOCTL.input inp_size equ IOCTL.inp_size output equ IOCTL.output out_size equ IOCTL.out_size

align 4 proc service_proc stdcall, ioctl:dword

mov edi, [ioctl]
mov eax, [edi+io_code]

xor eax, eax ret endp

restore handle restore io_code restore input restore inp_size restore output restore out_size </asm>

Процедура обработки запросов вызывается, когда какой-то внешний код возжаждал общения именно с нашим драйвером. Это может быть как другой драйвер (формально драйвер может вызывать сам себя через механизм I/O, но смысла в этом нет), надыбавший где-то наш хэндл и вызвавший ServiceHandler, или даже само ядро (srv_handler, srv_handlerEx из {{#svn:/kernel/trunk/core/dll.inc|core/dll.inc}}), так и приложение функцией 68.17 (хэндл приложение может добыть при загрузке драйвера функцией 68.16). Нулевое возвращаемое значение означает успех, ненулевое соответствует ошибке. Вначале определяем сокращённые имена для членов структуры, описывающей запрос. В поле handle содержится хэндл драйвера (такой же, как и возвращаемое значение RegService), io_code - dword-идентификатор запроса, остальные поля вопросов вызывать не должны. Возвращаемое значение напрямую передаётся вызвавшему нас коду (драйвер/ядро/приложение). В конце восстанавливаем значения, переназначенные было на короткие имена членов структуры. В данном случае это без надобности, но в случае сложных драйверов короткие имена типа "input" запросто могут встречаться не один раз.

Дальше в {{#svn:/kernel/trunk/drivers/sceletone.asm|sceletone.asm|450}} содержится код поиска заданного оборудования на PCI-шине, нам он без надобности, при необходимости разберитесь сами.

Итак, с каркасом драйвера разобрались. А теперь будем писать свой драйвер. Начало стандартное:

<asm>

;;
Copyright (C) KolibriOS team 2004-2007. All rights reserved. ;;
Distributed under terms of the GNU General Public License  ;;
;;
FileMon
driver part

format MS COFF

include 'proc32.inc' include 'imports.inc' </asm>

Ориентируемся на {{#svn_rev:450}} (для 0.6.5.0 нужно было бы писать "new_app_base equ 0x60400000"):

<asm> OS_BASE equ 0; new_app_base equ 0x80000000 </asm>

Небольшая порция объявлений:

<asm> struc IOCTL { .handle dd ?

  .io_code     dd ?
  .input       dd ?
  .inp_size    dd ?
  .output      dd ?
  .out_size    dd ?

}

virtual at 0

 IOCTL IOCTL

end virtual

public START public version

DRV_ENTRY equ 1 DRV_EXIT equ -1

section '.flat' code readable align 16 </asm>

Пока что всё стереотипно. Но прежде чем писать код, нужно определиться, чего мы от этого кода хотим. Итак, наш драйвер будет понимать четыре кода запроса ввода/вывода. Код 0 для всех драйверов специально предназначен для получения версии драйвера (здесь уже идёт работа с версией самого драйвера, а не версией интерфейса ядра). Строго говоря, реализовывать обработку этого запроса необязательно (ядру глубоко наплевать на версию драйвера), но весьма желательно, поскольку практически всегда драйвер можно развивать и изменять, а тогда для кода, использующего наш драйвер, знать версию всегда полезно. Версию драйвера можно возвращать в любом формате, в нашем примере для простоты будем использовать просто dword-номер версии, равный 1. Далее, код 1 означает "начать лог", код 2 - "выдать лог до текущего момента и сбросить", код 3 - "остановить лог". Лог будет записываться во внутренний буфер размера 16 Кб (учитывая, что опрашивать драйвер мы будем раз в секунду, этого должно хватить за глаза, но если вдруг не хватит, присутствие лишних записей мы сигнализируем, но сами записи не принимаем). Формат выдаваемой информации о логе: вначале общий размер записанных данных лога; потом байт со значением 0 или 1, причём 1 означает, что какие-то записи не поместились в буфере; потом массив структур переменного размера, первый байт которых содержит номер вызванной функции файловой системы и определяет дальнейшее содержимое записи. Инициализировать что-либо в драйвере нам не надо, так что процедура START выглядит так:

<asm> proc START stdcall, state:dword

          cmp [state], DRV_ENTRY
          jne .exit

.entry:

          stdcall RegService, my_service, service_proc

ret .fail: .exit:

          xor eax, eax
          ret

endp </asm>

Далее - обработка запросов:

<asm> handle equ IOCTL.handle io_code equ IOCTL.io_code input equ IOCTL.input inp_size equ IOCTL.inp_size output equ IOCTL.output out_size equ IOCTL.out_size

proc service_proc stdcall, ioctl:dword mov edi, [ioctl] mov eax, [edi+io_code] test eax, eax jz .getversion dec eax jz .startlog dec eax jz .getlog dec eax jz .endlog xor eax, eax ret .getversion: cmp [edi+out_size], 4 jb .err mov edi, [edi+output] mov dword [edi], 1 ; version of driver .ok: xor eax, eax ret .err: or eax, -1 ret .startlog: mov al, 1 xchg al, [bLogStarted] test al, al jnz .ok mov [logptr], logbuf call hook jnc .ok mov [bLogStarted], 0 jmp .err .getlog: cli mov esi, logbuf mov ecx, [logptr] sub ecx, esi add ecx, 5 cmp ecx, [edi+out_size] jbe @f mov ecx, [edi+out_size] mov [bOverflow], 1 @@: sub ecx, 5 xor eax, eax xchg al, [bOverflow] mov edi, [edi+output] mov [edi], ecx add edi, 4 stosb rep movsb mov [logptr], logbuf sti xor eax, eax ret .endlog: xchg al, [bLogStarted] test al, al jz @f call unhook @@: xor eax, eax ret endp

restore handle restore io_code restore input restore inp_size restore output restore out_size </asm>

Здесь стоит отметить, что входных данных для драйвера не нужно, поля input/inp_size мы не используем. В поле out_size вызывающий код должен поместить размер буфера output. Если нас вызывает приложение, то все манипуляции с переводом указателей приложения в указатели ядра осуществляет ядро, а в структуре ioctl передаются уже подправленные указатели.

Ну а теперь часть, отвечающая за взаимодействие с драйверной подсистемой, закончилась и начинается собственно работа. Мы перехватываем функции файловой системы 6,32,33,58,70. Для этого мы используем тот факт, что общий обработчик int 0x40 вызывает конкретную функцию косвенным вызовом из таблицы servetable (код этот обработчика располагается в файле {{#svn:/kernel/trunk/core/syscall.inc|core/syscall.inc}}). Следовательно, если подменить нужные элементы в этой таблице на адреса наших обработчиков, то вызываться будет наш код. Узнать адрес servetable можно сканированием кода обработчика int 0x40. Адрес функции i40 легко узнать из IDT, а команда вызова в текущей реализации имеет вид "call dword [servetable+edi*4]", в машинном коде "FF 14 BD <servetable>" (в принципе никто не гарантирует, что так будет и дальше, в частности, потенциально возможна замена edi на eax; тогда нужно будет соответственно менять код).

<asm> hook: cli sub esp, 6 sidt [esp] pop ax ; limit pop eax ; base mov edx, [eax+40h*8+4] mov dx, [eax+40h*8]

edx contains address of i40

mov ecx, 100 .find: cmp byte [edx], 0xFF jnz .cont cmp byte [edx+1], 0x14 jnz .cont cmp byte [edx+2], 0xBD jz .found .cont: inc edx loop .find sti mov esi, msg_failed call SysMsgBoardStr stc ret .found: mov eax, [edx+3]

eax contains address of servetable

mov [servetable_ptr], eax mov edx, newfn06 xchg [eax+6*4], edx mov [oldfn06], edx mov edx, newfn32 xchg [eax+32*4], edx mov [oldfn32], edx mov edx, newfn33 xchg [eax+33*4], edx mov [oldfn33], edx mov edx, newfn58 xchg [eax+58*4], edx mov [oldfn58], edx mov edx, newfn70 xchg [eax+70*4], edx mov [oldfn70], edx sti clc ret

unhook: cli mov eax, [servetable_ptr] mov edx, [oldfn06] mov [eax+6*4], edx mov edx, [oldfn32] mov [eax+32*4], edx mov edx, [oldfn33] mov [eax+33*4], edx mov edx, [oldfn58] mov [eax+58*4], edx mov edx, [oldfn70] mov [eax+70*4], edx sti ret </asm>

Две вспомогательные функции:

<asm> write_log_byte:

in
al=byte

push ecx mov ecx, [logptr] inc ecx cmp ecx, logbuf + logbufsize ja @f mov [logptr], ecx mov [ecx-1], al pop ecx ret @@: mov [bOverflow], 1 pop ecx ret

write_log_dword:

in
eax=dword

push ecx mov ecx, [logptr] add ecx, 4 cmp ecx, logbuf + logbufsize ja @f mov [logptr], ecx mov [ecx-4], eax pop ecx ret @@: mov [bOverflow], 1 pop ecx ret </asm>

При написании самих обработчиков следует учитывать, что регистры циклически сдвигаются по сравнению с вызовом int 0x40 в приложении и что все указатели - это указатели 3-кольца.

<asm> newfn06: cli push [logptr] push eax mov al, 6 ; function 6 call write_log_byte mov eax, ebx ; start block call write_log_dword mov eax, ecx ; number of blocks call write_log_dword mov eax, edx ; output buffer call write_log_dword pop eax push eax push esi lea esi, [eax+new_app_base] ; pointer to file name @@: lodsb call write_log_byte test al, al jnz @b pop esi pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f .nooverflow: add esp, 4 @@: sti jmp [oldfn06]

newfn32: cli push [logptr] push eax mov al, 32 ; function 32 call write_log_byte pop eax push eax push esi lea esi, [eax+new_app_base] ; pointer to file name @@: lodsb call write_log_byte test al, al jnz @b pop esi pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f .nooverflow: add esp, 4 @@: sti jmp [oldfn32]

newfn33: cli push [logptr] push eax mov al, 33 ; function 33 call write_log_byte mov eax, ebx ; input buffer call write_log_dword mov eax, ecx ; number of bytes call write_log_dword pop eax push eax push esi lea esi, [eax+new_app_base] ; pointer to file name @@: lodsb call write_log_byte test al, al jnz @b pop esi pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f .nooverflow: add esp, 4 @@: sti jmp [oldfn33]

newfn58: cli push [logptr] push eax push ebx lea ebx, [eax+new_app_base] mov al, 58 ; function 58 call write_log_byte

dump information structure

mov eax, [ebx] call write_log_dword mov eax, [ebx+4] call write_log_dword mov eax, [ebx+8] call write_log_dword mov eax, [ebx+12] call write_log_dword push esi lea esi, [ebx+20] ; pointer to file name @@: lodsb call write_log_byte test al, al jnz @b pop esi pop ebx pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f .nooverflow: add esp, 4 @@: sti jmp [oldfn58]

newfn70: cli push [logptr] push eax push ebx lea ebx, [eax+new_app_base] mov al, 70 ; function 70 call write_log_byte

dump information structure

mov eax, [ebx] call write_log_dword mov eax, [ebx+4] call write_log_dword mov eax, [ebx+8] call write_log_dword mov eax, [ebx+12] call write_log_dword mov eax, [ebx+16] call write_log_dword push esi lea esi, [ebx+20] ; pointer to file name lodsb test al, al jnz @f lodsd lea esi, [eax+new_app_base+1] @@: dec esi @@: lodsb call write_log_byte test al, al jnz @b pop esi pop ebx pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f .nooverflow: add esp, 4 @@: sti jmp [oldfn70] </asm>

На этом код заканчивается. Теперь используемые данные (мы ориентируемся на {{#svn_rev:450}}, для 0.6.5.0 version должна быть 0x00030003):

<asm> version dd 0x00040004 my_service db 'fmondrv',0

msg_failed db 'Cannot hook required functions',13,10,0

section '.data' data readable writable align 16

servetable_ptr dd ?

oldfn06 dd ? oldfn32 dd ? oldfn33 dd ? oldfn58 dd ? oldfn70 dd ?

logptr dd ? logbufsize = 16*1024 logbuf rb logbufsize

bOverflow db ? bLogStarted db ? </asm>

Собрав весь приведённый код в один файл fmondrv.asm, получаем окончательный исходник драйвера. Кроме того, этот файл входит в архив к статье. Компиляция:

fasm fmondrv.asm

После этого по желанию можно упаковать fmondrv.obj с помощью KPACK, ядро прекрасно загружает kpack'ованные файлы, а такая мера в данном случае уменьшает размер с 1850 байт до 757 байт. Кстати, маленькая хитрость: по смещению +4 в COFF-объектнике хранится штамп даты/времени компиляции, ядру на него глубоко наплевать, так что можно забить его нулями любым hex-редактором, после чего сжатый файл будет чуть-чуть меньше (в данном случае 756 байт). Для установки драйвера скопируйте его в /rd/1/drivers, после этого он готов к загрузке.

Управляющая программа

Управляющая программа у нас будет выводить текстовую информацию на консоль и завершать работу при нажатии Esc. Для этого потребуется консольная DLL версии как минимум 3, причём в дистрибутив 0.6.5.0 входит версия 2, так что скачивайте последнюю версию из http://diamondz.land.ru/console.7z. В качестве шаблона используем testcon.asm (можно было бы и testcon2.asm) со следующими изменениями: в REQ_DLL_VER подставляем 3, в таблице импорта (метка myimport) убираем con_write_asciiz и добавляем con_printf, con_kbhit, con_getch2 и, разумеется, после строчки с комментарием "Now do some work" пишем свой код.

<asm> use32

       db      'MENUET01'
       dd      1
       dd      start
       dd      i_end
       dd      mem
       dd      mem
       dd      0
       dd      0

REQ_DLL_VER = 3 DLL_ENTRY = 1

start:

First 3 steps are intended to load/init console DLL
and are identical for all console programs
load DLL
       mov     eax, 68
       mov     ebx, 19
       mov     ecx, dll_name
       int     0x40
       test    eax, eax
       jz      exit
initialize import
       mov	edx, eax
       mov     esi, myimport

import_loop:

       lodsd
       test    eax, eax
       jz      import_done
       push    edx

import_find:

       mov     ebx, [edx]
       test    ebx, ebx
       jz      exit;import_not_found
       push    eax

@@:

       mov     cl, [eax]
       cmp     cl, [ebx]
       jnz     import_find_next
       test    cl, cl
       jz      import_found
       inc     eax
       inc     ebx
       jmp     @b

import_find_next:

       pop     eax
       add     edx, 8
       jmp     import_find

import_found:

       pop     eax
       mov     eax, [edx+4]
       mov     [esi-4], eax
       pop     edx
       jmp     import_loop

import_done:

check version
       cmp     word [dll_ver], REQ_DLL_VER
       jb      exit
       cmp     word [dll_ver+2], REQ_DLL_VER
       ja      exit
       push    DLL_ENTRY
       call    [dll_start]
yes! Now do some work (say helloworld in this case).
       push    caption
       push    -1
       push    -1
       push    -1
       push    -1
       call    [con_init]

</asm>

Загружаем драйвер, при ошибке ругаемся на консоли и выходим, оставляя консоль на экране:

<asm>

       mov     eax, 68
       mov     ebx, 16
       mov     ecx, drivername
       int     0x40
       mov     [hDriver], eax
       test    eax, eax
       jnz     @f

loaderr:

       push    aCantLoadDriver
       call    [con_printf]
       add     esp, 4
       push    0
       call    [con_exit]
       jmp     exit

@@: </asm>

Проверяем версию драйвера, для чего посылаем ему запрос с кодом 0:

<asm>

       and     [ioctl_code], 0
       and     [inp_size], 0
       mov     [outp_size], 4
       mov     [output], driver_ver
       mov     eax, 68
       mov     ebx, 17
       mov     ecx, ioctl
       int     0x40
       test    eax, eax
       jnz     loaderr
       cmp     [driver_ver], 1
       jnz     loaderr

</asm>

Запускаем лог - запрос с кодом 1:

<asm>

       mov     [ioctl_code], 1
       and     [inp_size], 0
       and     [outp_size], 0
       mov     eax, 68
       mov     ebx, 17
       mov     ecx, ioctl
       int     0x40
       test    eax, eax
       jnz     loaderr

</asm>

Итак, драйвер загружен и ведёт лог. Сообщим пользователю, что происходит:

<asm>

       push    str0
       call    [con_printf]
       add     esp, 4

</asm>

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

<asm> mainloop:

       mov     eax, 5
       mov     ebx, 100
       int     0x40
       mov     [ioctl_code], 2
       and     [inp_size], 0
       mov     [outp_size], 1+16*1024
       mov     [output], logbuf
       mov     eax, 68
       mov     ebx, 17
       mov     ecx, ioctl
       int     0x40
       push    eax
       mov     ecx, dword [logbuf]
       mov     esi, logbuf+5

message:

       test    ecx, ecx
       jz      done
       movzx   eax, byte [esi]
       push    eax
       push    str1
       call    [con_printf]
       add     esp, 8
       lodsb
       cmp     al, 6
       jz      fn06
       cmp     al, 32
       jz      fn32
       cmp     al, 33
       jz      fn33
       cmp     al, 58
       jz      fn58
       sub     ecx, 1+4*5      ; size of log data for fn70 (excluding filename)
       lodsd
       cmp     eax, 10
       jae     fn70unk
       jmp     [fn70+eax*4]

fn70unk:

       push    dword [esi+12]
       push    dword [esi+8]
       push    dword [esi+4]
       push    dword [esi]
       push    eax
       push    str2
       call    [con_printf]
       add     esp, 6*4
       add     esi, 16
       jmp     print_name

fn70readfile:

       push    dword [esi+12]
       push    dword [esi+8]
       push    dword [esi]
       push    str3
       call    [con_printf]
       add     esp, 4*4
       add     esi, 16
       jmp     print_name

fn70readfolder:

       mov     eax, str41
       test    byte [esi+4], 1
       jz      @f
       mov     eax, str42

@@:

       push    dword [esi+12]
       push    dword [esi+8]
       push    dword [esi]
       push    eax
       push    str4
       call    [con_printf]
       add     esp, 5*4
       add     esi, 16
       jmp     print_name

fn70create:

       push    dword [esi+12]
       push    dword [esi+8]
       push    str5
       call    [con_printf]
       add     esp, 3*4
       add     esi, 16
       jmp     print_name

fn70write:

       push    dword [esi+12]
       push    dword [esi+8]
       push    dword [esi+4]
       push    str6
       call    [con_printf]
       add     esp, 4*4
       add     esi, 16
       jmp     print_name

fn70setsize:

       push    dword [esi]
       push    str7
       call    [con_printf]
       add     esp, 4*2
       add     esi, 16
       jmp     print_name

fn70getattr:

       push    dword [esi+12]
       push    str8
       call    [con_printf]
       add     esp, 4*2
       add     esi, 16
       jmp     print_name

fn70setattr:

       push    dword [esi+12]
       push    str9
       call    [con_printf]
       add     esp, 4*2
       add     esi, 16
       jmp     print_name

fn70execute:

       push    str10
       call    [con_printf]
       add     esp, 4
       lodsd
       test    al, 1
       jz      @f
       push    str10_1
       call    [con_printf]
       add     esp, 4

@@:

       lodsd
       test    eax, eax
       jz      @f
       push    eax
       push    str10_2
       call    [con_printf]
       add     esp, 8

@@:

       add     esi, 8
       jmp     print_name

fn70delete:

       push    str11
       call    [con_printf]
       add     esp, 4
       add     esi, 16
       jmp     print_name

fn70createfolder:

       push    str12
       call    [con_printf]
       add     esp, 4
       add     esi, 16
       jmp     print_name

fn58:

       sub     ecx, 1+4*4      ; size of log data for fn58 (excluding filename)
       lodsd
       test    eax, eax
       jz      fn58read
       cmp     eax, 1
       jz      fn58write
       cmp     eax, 8
       jz      fn58lba
       cmp     eax, 15
       jz      fn58fsinfo

fn58unk:

       push    dword [esi+8]
       push    dword [esi+4]
       push    dword [esi]
       push    eax
       push    str13
       call    [con_printf]
       add     esp, 5*4
       add     esi, 12
       jmp     print_name

fn58read:

       push    dword [esi+8]
       mov     eax, [esi+4]
       shl     eax, 9
       push    eax
       mov     eax, [esi]
       shl     eax, 9
       push    eax
       push    str3
       call    [con_printf]
       add     esp, 4*4
       add     esi, 12
       jmp     print_name

fn58write:

       push    dword [esi+8]
       push    dword [esi+4]
       push    str5
       call    [con_printf]
       add     esp, 3*4
       add     esi, 12
       jmp     print_name

fn58lba:

       push    dword [esi+8]
       push    dword [esi]
       push    str14
       call    [con_printf]
       add     esp, 3*4
       add     esi, 12
       jmp     print_name

fn58fsinfo:

       push    str15
       call    [con_printf]
       add     esp, 4
       add     esi, 12
       jmp     print_name

fn33:

       sub     ecx, 1+2*4      ; size of log data for fn33
       lodsd
       push    eax
       lodsd
       push    eax
       push    str5
       call    [con_printf]
       add     esp, 3*4
       push    aRamdisk
       call    [con_printf]
       add     esp, 4
       jmp     print_name

fn32:

       dec     ecx             ; only filename is logged
       push    str11
       call    [con_printf]
       push    aRamdisk
       call    [con_printf]
       add     esp, 4+4
       jmp     print_name

fn06:

       sub     ecx, 1+3*4      ; size of log data for fn06
       push    dword [esi+8]
       mov     eax, [esi+4]
       test    eax, eax
       jnz     @f
       inc     eax

@@:

       shl     eax, 9
       push    eax
       lodsd
       test    eax, eax
       jnz     @f
       inc     eax

@@:

       dec     eax
       shl     eax, 9
       push    eax
       push    str3
       call    [con_printf]
       add     esp, 4*4
       push    aRamdisk
       call    [con_printf]
       add     esp, 4
       add     esi, 8

print_name:

       push    esi
       push    str_final
       call    [con_printf]
       add     esp, 8

@@:

       lodsb
       test    al, al
       jnz     @b
       jmp     message

done:

       cmp     byte [logbuf+4], 0
       jz      @f
       push    str_skipped
       call    [con_printf]

@@:

we has output all driver data, now check console (did user press Esc?)
       call    [con_kbhit]
       test    al, al
       jz      mainloop
       call    [con_getch2]
       cmp     al, 27
       jnz     mainloop

</asm>

По нажатию Esc сообщим драйверу, что больше логгинг нам не нужен,

<asm>

       mov     [ioctl_code], 3
       and     [inp_size], 0
       and     [outp_size], 0
       mov     eax, 68
       mov     ebx, 17
       mov     ecx, ioctl
       int     0x40

</asm>

завершим работу с консолью, убрав с экрана её окно,

<asm>

       push    1
       call    [con_exit]

</asm>

и завершим работу программы.

<asm> exit:

       or      eax, -1
       int     0x40

</asm>

Данные программы:

<asm> dll_name db '/rd/1/console.obj',0 caption db 'FileMon',0 drivername db 'fmondrv',0 aCantLoadDriver db "Can't load driver",13,10,0

str0 db 'Monitoring file system calls... Press Esc to exit',10,0

str1 db 'Fn%2d: ',0 str2 db 'unknown subfunction %d, parameters: 0x%X, 0x%X, 0x%X, 0x%X, name ',0 str3 db 'read file, starting from 0x%X, %d bytes, to 0x%X; name ',0 str4 db 'read folder (%s version), starting from %d, %d blocks, to 0x%X; name ',0 str41 db 'ANSI',0 str42 db 'UNICODE',0 str5 db 'create/rewrite file, %d bytes from 0x%X; name ',0 str6 db 'write file, starting from 0x%X, %d bytes, from 0x%X; name ',0 str7 db 'set file size to %d bytes; name ',0 str8 db 'get file attributes to 0x%X; name ',0 str9 db 'set file attributes from 0x%X; name ',0 str10 db 'execute ',0 str10_1 db '(in debug mode) ',0 str10_2 db '(with parameters 0x%X) ',0 str11 db 'delete ',0 str12 db 'create folder ',0 str13 db 'unknown subfunction %d, parameters: 0x%X, 0x%X, 0x%X, name ',0 str14 db 'LBA read sector 0x%X to 0x%X from device ',0 str15 db '(obsolete!) query fs information of ',0 aRamdisk db '/rd/1/',0 str_final db '%s',10,0 str_skipped db '[Some information skipped]',10,0

align 4 label fn70 dword

       dd      fn70readfile
       dd      fn70readfolder
       dd      fn70create
       dd      fn70write
       dd      fn70setsize
       dd      fn70getattr
       dd      fn70setattr
       dd      fn70execute
       dd      fn70delete
       dd      fn70createfolder

align 4 myimport: dll_start dd aStart dll_ver dd aVersion con_init dd aConInit con_printf dd aConPrintf con_exit dd aConExit con_kbhit dd aConKbhit con_getch2 dd aConGetch2

               dd      0

aStart db 'START',0 aVersion db 'version',0 aConInit db 'con_init',0 aConPrintf db 'con_printf',0 aConExit db 'con_exit',0 aConKbhit db 'con_kbhit',0 aConGetch2 db 'con_getch2',0

i_end:

align 4 ioctl: hDriver dd  ? ioctl_code dd  ? input dd  ? inp_size dd  ? output dd  ? outp_size dd  ?

driver_ver dd  ? logbuf rb 16*1024+5

align 4 rb 2048 ; stack mem: </asm>