Writing drivers for KolibriOS/ru: Difference between revisions

From KolibriOS wiki
Jump to navigation Jump to search
m (Added interlinks for versions and apps)
(Global update of the article, part 1e)
 
(18 intermediate revisions by 8 users not shown)
Line 1: Line 1:
= Пишем драйвер для КолибриОС =
{{DISPLAYTITLE:Пишем драйвер для КолибриОС}}
 
''' ВНИМАНИЕ: Данная статья находится в разработке и содержит множество устаревших и непроверенных данных. В данный момент использование этой статьи в качестве справочного материала не рекомендуется.  '''
 
'''ПРЕДУПРЕЖДЕНИЕ: Данная статья устарела и переписывается. НЕ ИСПОЛЬЗУЙТЕ ЭТУ СТАТЬЮ.'''


== Вступление ==
== Вступление ==


'''Предупреждение 1.''' Прежде чем писать драйвер, хорошо подумайте, нельзя ли обойтись средствами прикладных [http://ru.wikipedia.org/wiki/API API], в частности, функций работы с оборудованием 41-46 и 62. Во-первых, от ошибки в кривом приложении пострадает только это кривое приложение, а кривой драйвер способен без особого труда обрушить всю систему. Во-вторых, для приложений можно вылавливать баги в отладчике [[App:MTDBG|MTDBG]], обладающем определёнными возможностями, а для драйверов этот путь закрыт (разве что встроенный отладчик эмулятора [http://ru.wikipedia.org/wiki/Bochs Bochs], но он заведомо непригоден для отладки с реальным железом), так что единственным средством остаётся отладочный вывод на доску отладки [[App:BOARD|BOARD]] со всеми недостатками.
'''Предупреждение 0.''' Данная статья описывает написание драйверов для основной ветки Колибри ОС и не включает в себя исторические моменты. Предыдущая версия статьи находится на форуме по [http://board.kolibrios.org/viewtopic.php?p=10987#p10987 ссылке].
 
'''Предупреждение 1.''' Прежде чем писать драйвер, хорошо подумайте, нельзя ли обойтись средствами прикладных [http://ru.wikipedia.org/wiki/API API], в частности, функций работы с оборудованием [[SysFn46/ru|46]] и [[SysFn62/ru|62]]. Во-первых, от ошибки в кривом приложении пострадает только это кривое приложение, а кривой драйвер способен без особого труда обрушить всю систему. Во-вторых, для приложений можно вылавливать баги в отладчике [[Mtdbg/ru|MTDBG]], обладающем определёнными возможностями, а для драйверов этот путь закрыт (разве что встроенный отладчик эмулятора [http://ru.wikipedia.org/wiki/Bochs Bochs], но он заведомо непригоден для отладки с реальным железом), так что единственным средством остаётся отладочный вывод на доску отладки [[Board/ru|BOARD]] со всеми недостатками.


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


'''Предупреждение 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].
Архив к статье находится здесь.
Архив к статье находится здесь.
== Описание работы с драйверной подсистемой ==
Драйверная подсистема позволяет ядру загружать драйвера в формате PE и работать с ними. Для загрузки драйверов предусмотрено 2 системных вызова: 68.16 и 68.21, а для управления драйвером системный вызов 68.17. Драйвер должен иметь точку входа с функцией, которая должна возвращать хандлер структуры драйвера в случае успеха, и ноль в случае, если драйвер не инициализировался. Для работы драйвер импортирует некоторые функции ядра, которые экспортируются ядром как core.dll.


== Драйвер ==  
== Драйвер ==  


Специально для желающих написать свой драйвер предоставляется каркас драйвера. Он находится в 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):
Специально для желающих написать свой драйвер предоставляется каркас драйвера. Он находится в git-репозитории в папке [https://git.kolibrios.org/KolibriOS/kolibrios/src/branch/main/drivers drivers]. Ну что же, давайте посмотрим [https://git.kolibrios.org/KolibriOS/kolibrios/src/branch/main/drivers/sceletone.asm sceletone.asm]):


<asm>
<syntaxhighlight lang="asm">
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                                                              ;;
;;                                                              ;;
Line 26: Line 36:
;driver sceletone
;driver sceletone


format MS COFF
format PE DLL native 0.05
entry START


section '.flat' code readable writable executable
include 'proc32.inc'
include 'proc32.inc'
include 'imports.inc'
include 'struct.inc'
</asm>
include 'macros.inc'
include 'peimport.inc'
 
</syntaxhighlight>


Ну, "[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]" - он и есть копирайт, комментарий в следующей строке извещает нас, куда же мы собственно попали, это неинтересно. Дальше мы должны известить компилятор, какой формат мы хотим получить. Драйвера должны иметь формат [https://en.wikipedia.org/wiki/Portable_Executable PE]. В общем, так должно быть для всех драйверов. Дальше идёт указание функции START как точки входа, которую вызовет ядро после загрузки драйвера. После этого объявляется секция ".flat", в которой будет располагаться код нашего драйвера, после объявления секции подключаем вспомогательные файлы. [https://git.kolibrios.org/KolibriOS/kolibrios/src/branch/main/drivers/proc32.inc proc32.inc] содержит макросы для определения и вызова стандартных процедур (<tt>proc/endp</tt>, <tt>stdcall/ccall/invoke/cinvoke</tt>, <tt>local</tt> для создания локальных переменных) и находится в той же папке, что и sceletone.asm. Его можно включать или не включать, макросы оттуда можно использовать или не использовать, в этой статье они используются, чтобы не усложнять восприятие (вообще говоря, при стремлении к максимальной эффективности использование макросов может повредить, но это тема отдельных жарких споров). [https://git.kolibrios.org/KolibriOS/kolibrios/src/branch/main/drivers/peimport.inc peimport.inc] содержит объявления для всех экспортируемых функций ядра. Загляните туда, ничего сложного там нет, просто куча стереотипных конструкций. На самом деле (в смысле того, как разрешается импорт при загрузке драйвера) список всех экспортируемых функций и данных ядра находится в файле {{#svn:/kernel/trunk/core/exports.inc|core/exports.inc}} (метка <tt>kernel_export</tt>), так что если вам как программисту ядра вдруг понадобиться что-нибудь своё экспортировать, лезьте туда (ну peimport.inc отредактируйте из вежливости к другим).


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


<asm>
<syntaxhighlight lang="asm">
OS_BASE        equ 0;
DEBUG        equ 1 ; for debug output in board
new_app_base    equ 0x60400000
 
PROC_BASE       equ OS_BASE+0x0080000
API_VERSION    equ 0 ;version api this driver
</asm>
 
STRIDE       equ 4      ;size of row in devices table
 
SRV_GETVERSION  equ 0 ; number function for get api version


Константа <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> из исходников ядра.
</syntaxhighlight>


Едем дальше:
Это просто объявление констант


<asm>
<syntaxhighlight lang="asm">
struc IOCTL
{  .handle      dd ?
  .io_code    dd ?
  .input      dd ?
  .inp_size    dd ?
  .output      dd ?
  .out_size    dd ?
}


virtual at 0
proc START c, state:dword, cmdline:dword
  IOCTL IOCTL
end virtual
</asm>


Это просто объявление структуры (махинации с <tt>virtual</tt> - стандарт для FASM).
        cmp    [state], 1
        jne    .exit
.entry:


<asm>
        push    esi
public START
    if DEBUG
public service_proc
        mov    esi, msgInit
public version
        invoke  SysMsgBoardStr
</asm>
    end if
        call    detect
        pop    esi
        test    eax, eax
        jz      .fail


Выше мы импортировали из ядра нужные нам функции (<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. Для чего нужны все эти сложности? Дело в следующем. Изменения в драйверной подсистеме могут быть следующих типов: полная или частичная переделка одной из базовых концепций; удаление одной из экспортируемых функций ядра; модификация функции (вчера функция принимала аргумент в стеке, а сегодня для эффективности аргумент передаётся в регистре; или добавился ещё какой-то аргумент; или изменился смысл аргументов и т.п.); добавление функции. В первом и третьем случае, собственно, ничего не поделаешь, драйверы переписывать надо. Второй тоже приводит к несовместимости. Но обидно перекомпилировать все драйвера только из-за того, что появилась новая функция, без которой эти драйвера прекрасно обходились. Вот и поддерживается загрузка "устаревших, но не слишком" драйверов.
        invoke  RegService, my_service, service_proc
        ret
.fail:
.exit:
        xor    eax, eax
        ret
endp


<asm>
</syntaxhighlight>
version      dd 0x00030003
</asm>


Каркас драйвера рассчитан на... версию 3, т.е. с текущим ядром он не пойдёт! Дело в том, что этот каркас в общем-то не обновлялся (если не считать копирайта) с [[Version:0.6.5.0|0.6.5.0]], так что <tt>new_app_base</tt> и <tt>version</tt> остались старые. Попутно отмечу, что старшее слово - это первая тройка, а младшее - вторая в силу обратного расположения байт в слове и слов в двойном слове (вообще-то я уверен, что вы и так это знаете, но для очистки совести...)
Выше мы создали функцию START, о которой уже говорилось ранее. Данная функция принимает 2 аргумента: state и cmdline и возвращает хэндлер драйвера, полученный при вызове RegService. Если эта функция вернёт 0, то это будет означать, что драйвер по какой-то причине не может работать(либо нет нужного оборудования, с которым бы драйвер работал, либо его что-то не устраивает и чтобы не создавать ошибок, драйвер завершает работу). Параметр cmdline это командная строка в ascii кодировке, state это действие, которое требует от этой функции ядро. Сейчас есть 2 значения этого аргумента: DRV_ENTRY передаваемое при старте драйвера и DRV_EXIT передаваемое при завершении работы драйвера. Эти значения определены в файле  {{#svn:/drivers/macros.inc|macros.inc}}. В этом коде вызываются 2 функции импортированные из ядра: SysMsgBoardStr и RegService, функции ядра имеют разные соглашения о вызовах, в одних параметры передаются через стек, в других через регистры. Описание соглашения о вызовах функций можно найти в файле {{#svn:/kernel/trunk/core/exports.inc|exports.inc}} в комментарии возле экспортируемой функции.


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


<asm>
<syntaxhighlight lang="asm">
DEBUG      equ 1
DEBUG      equ 1


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


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


<asm>
<syntaxhighlight lang="asm">
section '.flat' code readable align 16
section '.flat' code readable align 16
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
proc START stdcall, state:dword
proc START stdcall, state:dword


Line 120: Line 135:
           ret
           ret
endp
endp
</asm>
</syntaxhighlight>


Это код процедуры инициализации/финализации. При загрузке драйвера она вызывается с аргументом <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>:


<asm>
<syntaxhighlight lang="asm">
handle    equ  IOCTL.handle
handle    equ  IOCTL.handle
io_code    equ  IOCTL.io_code
io_code    equ  IOCTL.io_code
Line 150: Line 165:
restore  output
restore  output
restore  out_size
restore  out_size
</asm>
</syntaxhighlight>


Процедура обработки запросов вызывается, когда какой-то внешний код возжаждал общения именно с нашим драйвером. Это может быть как другой драйвер (формально драйвер может вызывать сам себя через механизм 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-шине], нам он без надобности, при необходимости разберитесь сами.


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


<asm>
<syntaxhighlight lang="asm">
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                                                              ;;
;;                                                              ;;
Line 174: Line 189:
include 'proc32.inc'
include 'proc32.inc'
include 'imports.inc'
include 'imports.inc'
</asm>
</syntaxhighlight>


Ориентируемся на 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>
<syntaxhighlight lang="asm">
OS_BASE        equ 0;
OS_BASE        equ 0;
new_app_base    equ 0x80000000
new_app_base    equ 0x80000000
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
struc IOCTL
struc IOCTL
{  .handle      dd ?
{  .handle      dd ?
Line 206: Line 221:


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


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


<asm>
<syntaxhighlight lang="asm">
proc START stdcall, state:dword
proc START stdcall, state:dword


Line 224: Line 239:
           ret
           ret
endp
endp
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
handle    equ  IOCTL.handle
handle    equ  IOCTL.handle
io_code    equ  IOCTL.io_code
io_code    equ  IOCTL.io_code
Line 309: Line 324:
restore  output
restore  output
restore  out_size
restore  out_size
</asm>
</syntaxhighlight>


Здесь стоит отметить, что входных данных для драйвера не нужно, поля 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>
<syntaxhighlight lang="asm">
hook:
hook:
cli
cli
Line 379: Line 394:
sti
sti
ret
ret
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
write_log_byte:
write_log_byte:
; in: al=byte
; in: al=byte
Line 415: Line 430:
pop ecx
pop ecx
ret
ret
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
newfn06:
newfn06:
cli
cli
Line 593: Line 608:
sti
sti
jmp [oldfn70]
jmp [oldfn70]
</asm>
</syntaxhighlight>


На этом код заканчивается. Теперь используемые данные (мы ориентируемся на 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>
<syntaxhighlight lang="asm">
version dd 0x00040004
version dd 0x00040004
my_service db 'fmondrv',0
my_service db 'fmondrv',0
Line 619: Line 634:
bOverflow db ?
bOverflow db ?
bLogStarted db ?
bLogStarted db ?
</asm>
</syntaxhighlight>


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


== Управляющая программа ==
== Управляющая программа ==
Line 631: Line 646:
Управляющая программа у нас будет выводить текстовую информацию на консоль и завершать работу при нажатии Esc. Для этого потребуется консольная [http://ru.wikipedia.org/wiki/DLL DLL] версии как минимум 3, причём в дистрибутив [[Version:0.6.5.0|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" пишем свой код.
Управляющая программа у нас будет выводить текстовую информацию на консоль и завершать работу при нажатии Esc. Для этого потребуется консольная [http://ru.wikipedia.org/wiki/DLL DLL] версии как минимум 3, причём в дистрибутив [[Version:0.6.5.0|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>
<syntaxhighlight lang="asm">
use32
use32
         db      'MENUET01'
         db      'MENUET01'
Line 706: Line 721:
         push    -1
         push    -1
         call    [con_init]
         call    [con_init]
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
         mov    eax, 68
         mov    eax, 68
         mov    ebx, 16
         mov    ebx, 16
Line 726: Line 741:
         jmp    exit
         jmp    exit
@@:
@@:
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
         and    [ioctl_code], 0
         and    [ioctl_code], 0
         and    [inp_size], 0
         and    [inp_size], 0
Line 743: Line 758:
         cmp    [driver_ver], 1
         cmp    [driver_ver], 1
         jnz    loaderr
         jnz    loaderr
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
         mov    [ioctl_code], 1
         mov    [ioctl_code], 1
         and    [inp_size], 0
         and    [inp_size], 0
Line 757: Line 772:
         test    eax, eax
         test    eax, eax
         jnz    loaderr
         jnz    loaderr
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
         push    str0
         push    str0
         call    [con_printf]
         call    [con_printf]
         add    esp, 4
         add    esp, 4
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
mainloop:
mainloop:
         mov    eax, 5
         mov    eax, 5
Line 1,038: Line 1,053:
         cmp    al, 27
         cmp    al, 27
         jnz    mainloop
         jnz    mainloop
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
         mov    [ioctl_code], 3
         mov    [ioctl_code], 3
         and    [inp_size], 0
         and    [inp_size], 0
Line 1,050: Line 1,065:
         mov    ecx, ioctl
         mov    ecx, ioctl
         int    0x40
         int    0x40
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
         push    1
         push    1
         call    [con_exit]
         call    [con_exit]
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
exit:
exit:
         or      eax, -1
         or      eax, -1
         int    0x40
         int    0x40
</asm>
</syntaxhighlight>


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


<asm>
<syntaxhighlight lang="asm">
dll_name db '/rd/1/console.obj',0
dll_name db '/rd/1/console.obj',0
caption db 'FileMon',0
caption db 'FileMon',0
Line 1,149: Line 1,164:
rb 2048 ; stack
rb 2048 ; stack
mem:
mem:
</asm>
</syntaxhighlight>
 
== Загрузка и установка драйвера ==
В приведённом выше примере загрузкой драйвера занимается управляющая программа, вызывая соответствующие системные функции ядра. Но управляющая программа нужна далеко не всем видам драйверов и во многих случаях является просто приятным бонусом к основному функционалу драйвера, например для драйверов запоминающих устройств управляющая программа не обязательна и важны лишь экспортируемые ядром функции для работы с дисковой подсистемой.
 
По этой причине для загрузки внешних драйверов можно применить другие методы. Одним из оптимальных методов является использование программы "loaddrv", которая принимает в качестве аргумента командной строки имя драйвера без его расширения и загружает его через системную функцию 68.16. Например, при исполнении команды "loaddrv sdhci" программа загрузит драйвер "/sys/drivers/sdhci.sys". Данный способ загрузки позволяет без дополнительных изменений ядра добавить загрузку драйвера, прописав его в файл "/sys/settings/autorun.dat". Этот метод предпочтителен при добавлении новых драйверов в основной образ системы.
 
Но существуют ситуации когда драйвер не может быть добавлен в основной образ(образ дискеты) по причине слишком большого размера файла для этого образа. В данной ситуации целесообразнее написать собственный загрузчик драйвера, который определит путь к расположению файла и сможет передать необходимую для загрузки командную строку.
 
Существует также ситуация, когда драйвер необходимо загрузить во время инициализации самого ядра, например для работы USB подсистемы ядро само загружает драйвера хост-контроллеров и драйвера классов устройств. Загрузка драйверов из ядра аналогична загрузке через системную функцию 68.16. Данный метод не рекомендуется, но может быть применён в специализированных под определённое железо сборках ядра.
 
== Краткое описание экспортируемых ядром функций ==
Описание подробностей применения функций ядра можно достаточно легко посмотреть на основе имеющихся драйверов, но это требует достаточно хорошего знания языка ассемблера fasm и понимания общей структуры ядра. Для более краткого описания в данной статье описаны некоторые из интерфейсов ядра, предоставляемых драйверам. Описания достаточны для написания некоторых типов драйверов.
 
=== Мьютексы и семафоры ===
В большинстве случаев для обработки нескольких одновременных вызовов функций необходимы средства синхронизации и блокировки доступа к ресурсам, с которыми работает данная функция. Для обеспечения такой блокировки драйвер импортирует функции, реализующие мьютексы и семафоры
<syntaxhighlight lang="C">
struct mutex {
struct list_head wait_list;
atomic_t            count;
};
 
void fastcall mutex_init(struct mutex *lock);
void fastcall mutex_lock(struct mutex *lock);
void fastcall mutex_unlock(struct mutex *lock);
void fastcall init_rwsem(struct rw_semaphore *sem);
void fastcall down_read(struct rw_semaphore *sem);
void fastcall down_write(struct rw_semaphore *sem);
void fastcall up_read(struct rw_semaphore *sem);
void fastcall up_write(struct rw_semaphore *sem);
</syntaxhighlight>
=== Работа с подсистемой событий ===
Подсистема событий является одной из важнейших частей ядра, необходимой для реализации сложных драйверов, требующих надёжный метод коммуникации между различными потоками и функциями драйвера. Подробное описание этой подсистемы находится  [http://wiki.kolibrios.org/wiki/Kernel_Event/ru в отдельной статье].
 
=== Интерфейс работы с дисковой подсистемой ===
Ядро предоставляет интерфейс для добавления, удаления и работы с логическими дисками. Данный интерфейс позволяет реализовывать драйвера различных дисковых устройств, вне зависимости от их физического интерфейса, в том числе и виртуальные. <br>
Для добавления нового диска используется функция DiskAdd. Описание передаваемых в неё параметров приведено ниже по тексту.<br>
Для удаления диска используется функция DiskDel, в которую передаётся полученный ранее указатель. Эта функция удаляет диск из единого списка логических дисков.
<syntaxhighlight lang="C">
void* DiskAdd(DISKFUNC* functions, const char* name, uint32_t userdata, uint32_t flags);
void DiskDel(void* hDisk);
void DiskMediaChanged(void* hDisk, int newstate);
; Flags for add new disk
DISK_NO_INSERT_NOTIFICATION = 1
</syntaxhighlight>
 
Некоторые диски имеют возможность изменения содержимого, например CD приводы, и по этому в ядре предусмотрена функция "DiskMediaChanged".
Эта функция информирует ядро о том, что носитель был вставлен, удален или изменен. Значение "newstate" должно быть равно нулю, если в данный момент носитель не вставлен, и ненулевым в противном случае. Эта функция не должна вызываться с ненулевым значением newstate ни из одной callback функции. Эта функция не должна вызываться, если активен другой вызов этой функции.<br>
<br>
Если при добавлении диска был установлен флаг DISK_NO_INSERT_NOTIFICATION, то драйвер не должен вызывать функцию "DiskMediaChanged" и ядро будет проверять наличие носителя при каждой операции. Данный подход может использоваться для  дисков, подключение или изъятие носителей которых не может быть определено драйвером, например данный флаг применяется в драйвере контроллера floppy дисков.<br>
 
==== Callback функции драйвера диска ====
Как уже было описано выше, при добавлении нового диска, драйвер должен передать ядру указатель на структуру DISKFUNC, которая содержит общий размер структуры(поле strucsize) и массив указателей на функции. Если функция отсутствует, то вместо неё должен быть записан ноль.
<syntaxhighlight lang="asm">
struct  DISKFUNC
        strucsize      dd ?
        close          dd ?
        closemedia      dd ?
        querymedia      dd ?
        read            dd ?
        write          dd ?
        flush          dd ?
        adjust_cache_size      dd ?
        LoadTray        dd ?
ends
 
; Error codes for callback functions.
DISK_STATUS_OK              = 0 ; success
DISK_STATUS_GENERAL_ERROR  = -1; if no other code is suitable
DISK_STATUS_INVALID_CALL    = 1 ; invalid input parameters
DISK_STATUS_NO_MEDIA        = 2 ; no media present
DISK_STATUS_END_OF_MEDIA    = 3 ; end of media while reading/writing data
DISK_STATUS_NO_MEMORY      = 4 ; insufficient memory for driver operation
</syntaxhighlight>
 
<syntaxhighlight lang="C">
void close(void* userdata);
</syntaxhighlight>
Необязательная функция. Функция которая освобождает все ресурсы, зависящие от драйвера, для диска.<br>
 
<syntaxhighlight lang="C">
void closemedia(void* userdata);
</syntaxhighlight>
Необязательная функция, может отсутствовать если носитель не является съемным. Функция, вызов которой информирует драйвер о том, что ядро завершило всю обработку с текущим носителем. Если носитель удален, драйвер должен отклонять все запросы к этому носителю с помощью команды DISK_STATUS_NO_MEDIA, даже если вставлен новый носитель, до тех пор, пока не будет вызвана эта функция. Если носитель удален, новый вызов 'disk_media_changed' не разрешен до тех пор, пока не будет вызвана эта функция. <br>
 
<syntaxhighlight lang="C">
int querymedia(void* userdata, DISKMEDIAINFO* info);
 
; Media flags. Represent bits in DISKMEDIAINFO.Flags.
DISK_MEDIA_READONLY = 1
 
struct  DISKMEDIAINFO
        Flags          dd ? ; Combination of DISK_MEDIA_* bits.
        SectorSize      dd ? ; Size of the sector.
        Capacity        dq ? ; Size of the media in sectors.
        LastSessionSector      dd ? ; Number last session sectors for CDFS
ends
 
</syntaxhighlight>
Обязательная функция, которая производит заполнение структуры DISKMEDIAINFO и возвращает DISK_STATUS_* код.<br>
 
<syntaxhighlight lang="C">
int read(void* userdata, void* buffer, __int64 startsector, int* numsectors);
</syntaxhighlight>
Обязательная функция. Функция для чтения секторов диска в буфер "buffer", начиная с сектора "startsector". Количество считываемых секторов находится по указателю "numsectors". Функция должна возвращать DISK_STATUS_* код и записать количество успешно прочитанных секторов по указателю "numsectors". Указатель на буфер является виртуальным адресом. <br>
 
<syntaxhighlight lang="C">
; int write(void* userdata, void* buffer, __int64 startsector, int* numsectors);
</syntaxhighlight>
Необязательная функция. Функция для записи секторов диска из буфера "buffer", начиная с сектора "startsector". Количество записываемых секторов находится по указателю "numsectors".Функция должна возвращать DISK_STATUS_* код и записать количество успешно записанных секторов по указателю "numsectors". Указатель на буфер является виртуальным адресом. <br>
 
<syntaxhighlight lang="C">
int flush(void* userdata);
</syntaxhighlight>
Необязательная функция. Функция очищает внутренний кэш устройства и возвращает DISK_STATUS_* код. Обратите внимание, что функции чтения/записи вызываются менеджером кэша, поэтому драйвер не должен создавать программный кэш. Эта функция реализована для очистки аппаратного кэша, если он существует. <br>
 
<syntaxhighlight lang="C">
unsigned int adjust_cache_size(void* userdata, unsigned int suggested_size);
</syntaxhighlight>
Необязательная функция. Функция возвращает размер кэша для данного устройства в байтах. При возврате нуля программный кэш не используется. <br>
 
<syntaxhighlight lang="C">
int LoadTray(void* userdata, int flags);
</syntaxhighlight>
Необязательная функция. Функция для загрузки/выгрузки носителя, операция определяется флагом: 0 - загрузить, 1 - выгрузить. Функция возвращает DISK_STATUS_* код.<br>
 
=== Интерфейс работы со встроенными устройствами ===
Встроенные устройства включают в себя всевозможные устройства, расположенные непосредственно на материнской плате или подключаемым к внутренним шинам, например ISA или PCIe.
 
==== Прерывания ====
Прерывания используются многими встроенными устройствами для оповещения драйверов об изменении их состояния, например, для оповещения о подключении сетевого кабеля или нажатие на клавишу клавиатуры.
Для добавления своего обработчика прерывания, драйвер должен вызвать функцию в которую передаётся номер прерывания, указатель на функцию обработчика прерывания и необходимые ей данные в виде 4 байт.
<syntaxhighlight lang="C">
int32_t stdcall AttachIntHandler(uint32_t irq, void* handler, uint32_t userdata);
</syntaxhighlight>
Обработчик прерывания должен соблюдать CDECL соглашение о вызовах и принимать необходимые ей данные в виде 4 байт.
<syntaxhighlight lang="C">
int32_t cdecl irq_handler(uint32_t userdata);
</syntaxhighlight>
Если вызванный обработчик прерывания не обнаружил взаимодействия от настроенного на него контроллера, то обработчик должен вернуть 1 в качестве ответа. В иных случаях обработчик должен вернуть ноль.
 
==== Шина PCI ====
Большинство подключённых к ПК устройств используют шину PCI(PCIe). Для работы с этой шиной драйвера импортируют ряд функций, через которые можно прочесть и записать данные в конфигурационное пространство PCI устройства. Подробнее об этом говорится в статье [http://wiki.kolibrios.org/wiki/PCI/ru PCI]. <br>
Ядро экспортирует как сами функции работы с PCI шиной, так и функцию GetPCIList для получения указателя на список найденных им устройств. Этот список устройств представляет из себя двусвязный список структур PCIDEV. На основе этой структуры можно произвести поиск PCI устройства без обращения к самой шине, что упрощает написание самого драйвера.
<syntaxhighlight lang="asm">
struct  PCIDEV
        bk              dd ?
        fd              dd ?
        vendor_device_id dd ?
        class          dd ?
        devfn          db ?
        bus            db ?
                        rb 2
        owner          dd ? ; pointer to SRV or 0
ends
</syntaxhighlight>
Для чтения и записи используется набор функций со схожим интерфейсом.
<syntaxhighlight lang="C">
uint8_t  stdcall PciRead8(uint23_t bus, uint32_t devfn, uint32_t reg);
uint16_t stdcall PciRead16(uint23_t bus, uint32_t devfn, uint32_t reg);
uint32_t stdcall PciRead32(uint23_t bus, uint32_t devfn, uint32_t reg);
 
void stdcall PciWrite8(uint23_t bus, uint32_t devfn, uint32_t reg, uint32_t value);
void stdcall PciWrite16(uint23_t bus, uint32_t devfn, uint32_t reg, uint32_t value);
void stdcall PciWrite32(uint23_t bus, uint32_t devfn, uint32_t reg, uint32_t value);
 
PCIDEV* fastcall GetPCIList();
</syntaxhighlight>
Кроме этих функций существует также функция PciApi, через которую также возможно осуществить чтение и запись конфигурационного пространства.
Интерфейс этой функции повторяет интерфейс системной функции [[SysFn62/ru|62]].
 
==== Шина USB ====
Подробное описание интерфейса находится в статье [http://wiki.kolibrios.org/wiki/USB_API/ru USB API].
 
==== Порты ввода/вывода ====
Драйверы могут взаимодействовать со всеми портами ввода/вывода, но для избежание вредоносного взаимодействия со стороны пользовательского ПО необходимо зарезервировать необходимые порты через функцию ReservePortArea.
<syntaxhighlight lang="asm">
;reserve/free group of ports
;  * eax = 46 - number function
;  * ebx = 0 - reserve, 1 - free
;  * ecx = number start arrea of ports
;  * edx = number end arrea of ports (include last number of port)
;Return value:
;  * eax = 0 - succesful
;  * eax = 1 - error
;  * The system has reserve this ports:
;    0..0x2d, 0x30..0x4d, 0x50..0xdf, 0xe5..0xff (include last number of port).
;destroys all registers
ReservePortArea
</syntaxhighlight>
Кроме этого ядро при загрузке само резервирует некоторый диапазон портов:
* 0-45
* 48-77
* 80-223
* 229-255
 
Многие устройства предоставляют интерфейс работы через спроецированные на память регистры контроллера. Для работы с этими регистрами используется функция MapIoMem, в которую передаётся базовый адрес физической памяти, на который указывает устройство(во многих PCI устройствах такой адрес будет находится в BAR регистрах конфигурационного пространства PCI) размер и флаги страниц памяти. Функция вернёт указатель на базовый адрес в виртуальной памяти либо ноль в случае неудачи.
<syntaxhighlight lang="C">
void* stdcall MapIoMem(void* base, uint32_t size, uint32_t flags);
</syntaxhighlight> 
 
=== Интерфейс взаимодействия с сетевой подсистемой ===
 
Краткое описание интерфейса находится в статье [http://wiki.kolibrios.org/wiki/Writing_network_drivers_for_KolibriOS о сетевых драйверах].
 
=== Интерфейс взаимодействия с графической подсистемой ===
Взаимодействие с графической подсистемой происходит в основном через экспортируемую ядром структуру display_t и нескольких дополнительных функций. Получить структуру display_t можно через вызов функции "GetDisplay".
==== Регистрация аппаратного курсора ====
 
Для изменения функций курсора указатели на них изменяются в структуре display_t.
TODO: написать описание функций и пример аппаратного курсора для AMD видеокарты.
 
==== Работа с фреймбуфером ====
Для изменения размеров области вывода изображения в фреймбуфер, без изменения его физического расположения(адрес не меняется ), применяется функция "SetScreen".
<syntaxhighlight lang="asm">
; in:
; eax - new Screen_Max_X
; ecx - new BytesPerScanLine
; edx - new Screen_Max_Y
set_screen:
</syntaxhighlight> 
 
Кроме изменения размера возможно изменить физическое расположение самого фреимбуфера с помощью функции "SetFramebuffer"
<syntaxhighlight lang="asm">
struct FRB
        list            LHEAD
        magic          rd 1
        handle          rd 1
        destroy        rd 1
 
        width          rd 1
        height          rd 1
        pitch          rd 1
        format          rd 1
        private        rd 1
        pde            rd 8
ends
</syntaxhighlight>
<syntaxhighlight lang="C">
void fastcall SetFramebuffer(struct FBR* _fbr);
</syntaxhighlight>
 
==== Работа с оконной подсистемой ====
Для взаимодействия с оконной подсистемой ядро экспортирует функцию "GetWindowRect" с помощью которой можно получить координаты границ окна текущего потока.
<syntaxhighlight lang="C">
void fastcall get_window_rect(struct RECT* rc);
</syntaxhighlight>
 
=== Остальные импортируемые ядром функции ===
==== Функция для драйверов мыши ====
<syntaxhighlight lang="C">
void stdcall SetMouseData(uint32_t BtnState, uint32_t XMoving, uint32_t YMoving, uint32_t VScroll, uint32_t HScroll);
</syntaxhighlight>
 
==== Функции для драйверов клавиатуры ====
<syntaxhighlight lang="C">
KEYBOARD* stdcall RegKeyboard(KBDFUNC* func, uint32_t userdata);
</syntaxhighlight>
 
<syntaxhighlight lang="C">
void stdcall DelKeyboard(KEYBOARD* handle);
</syntaxhighlight>
 
<syntaxhighlight lang="C">
void fastcall SetKeyboardData(uint32_t scancode);
</syntaxhighlight>
==== Функции работы с потоками ====
 
==== Функции таймера ====
<syntaxhighlight lang="C">
void* TimerHS(unsigned int deltaStart, unsigned int interval,
              void* timerFunc, void* userData);
void CancelTimerHS(void* hTimer);
</syntaxhighlight>
TODO
 
==== Функции вывода в доску отладки ====
 
==== Функции выделения памяти ====
 
== Звуковая подсистема ==
 
Краткое описание интерфейса находится в статье [http://wiki.kolibrios.org/wiki/Writing_sound_drivers_for_KolibriOS о драйверах звуковых карт].
 
== Полезные макросы языка fasm и особенности их применения ==
 
Для языка fasm использование макросов достаточно сильно может упростить написание программ и драйверов.
Кроме пользы, макросы могут внести путаницу в поиске ошибок реализации любой программы.
 
=== DEBUGF ===
 
[[Category:Руководства]]

Latest revision as of 09:59, 25 July 2024


ВНИМАНИЕ: Данная статья находится в разработке и содержит множество устаревших и непроверенных данных. В данный момент использование этой статьи в качестве справочного материала не рекомендуется.

ПРЕДУПРЕЖДЕНИЕ: Данная статья устарела и переписывается. НЕ ИСПОЛЬЗУЙТЕ ЭТУ СТАТЬЮ.

Вступление

Предупреждение 0. Данная статья описывает написание драйверов для основной ветки Колибри ОС и не включает в себя исторические моменты. Предыдущая версия статьи находится на форуме по ссылке.

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

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

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

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

Описание работы с драйверной подсистемой

Драйверная подсистема позволяет ядру загружать драйвера в формате PE и работать с ними. Для загрузки драйверов предусмотрено 2 системных вызова: 68.16 и 68.21, а для управления драйвером системный вызов 68.17. Драйвер должен иметь точку входа с функцией, которая должна возвращать хандлер структуры драйвера в случае успеха, и ноль в случае, если драйвер не инициализировался. Для работы драйвер импортирует некоторые функции ядра, которые экспортируются ядром как core.dll.

Драйвер

Специально для желающих написать свой драйвер предоставляется каркас драйвера. Он находится в git-репозитории в папке drivers. Ну что же, давайте посмотрим sceletone.asm):

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

;driver sceletone

format PE DLL native 0.05
entry START

section '.flat' code readable writable executable
include 'proc32.inc'
include 'struct.inc'
include 'macros.inc'
include 'peimport.inc'

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

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

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

DEBUG        equ 1 ; for debug output in board

API_VERSION     equ 0  ;version api this driver

STRIDE       equ 4      ;size of row in devices table

SRV_GETVERSION  equ 0 ; number function for get api version

Это просто объявление констант

proc START c, state:dword, cmdline:dword

        cmp     [state], 1
        jne     .exit
.entry:

        push    esi
     if DEBUG
        mov     esi, msgInit
        invoke  SysMsgBoardStr
     end if
        call    detect
        pop     esi
        test    eax, eax
        jz      .fail

        invoke  RegService, my_service, service_proc
        ret
.fail:
.exit:
        xor     eax, eax
        ret
endp

Выше мы создали функцию START, о которой уже говорилось ранее. Данная функция принимает 2 аргумента: state и cmdline и возвращает хэндлер драйвера, полученный при вызове RegService. Если эта функция вернёт 0, то это будет означать, что драйвер по какой-то причине не может работать(либо нет нужного оборудования, с которым бы драйвер работал, либо его что-то не устраивает и чтобы не создавать ошибок, драйвер завершает работу). Параметр cmdline это командная строка в ascii кодировке, state это действие, которое требует от этой функции ядро. Сейчас есть 2 значения этого аргумента: DRV_ENTRY передаваемое при старте драйвера и DRV_EXIT передаваемое при завершении работы драйвера. Эти значения определены в файле {{#svn:/drivers/macros.inc|macros.inc}}. В этом коде вызываются 2 функции импортированные из ядра: SysMsgBoardStr и RegService, функции ядра имеют разные соглашения о вызовах, в одних параметры передаются через стек, в других через регистры. Описание соглашения о вызовах функций можно найти в файле {{#svn:/kernel/trunk/core/exports.inc|exports.inc}} в комментарии возле экспортируемой функции.

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

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

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

DEBUG      equ 1

STRIDE     equ 4      ;size of row in devices table

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

section '.flat' code readable align 16

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

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

Это код процедуры инициализации/финализации. При загрузке драйвера она вызывается с аргументом 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:

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

Процедура обработки запросов вызывается, когда какой-то внешний код возжаждал общения именно с нашим драйвером. Это может быть как другой драйвер (формально драйвер может вызывать сам себя через механизм 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-шине, нам он без надобности, при необходимости разберитесь сами.

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                                                              ;;
;; 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'

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

OS_BASE         equ 0;
new_app_base    equ 0x80000000

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

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

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

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

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

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

Здесь стоит отметить, что входных данных для драйвера не нужно, поля 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; тогда нужно будет соответственно менять код).

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

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

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

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

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]

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

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	?

Собрав весь приведённый код в один файл 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" пишем свой код.

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]

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

        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
@@:

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

        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

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

        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

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

        push    str0
        call    [con_printf]
        add     esp, 4

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

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

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

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

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

        push    1
        call    [con_exit]

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

exit:
        or      eax, -1
        int     0x40

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

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:

Загрузка и установка драйвера

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

По этой причине для загрузки внешних драйверов можно применить другие методы. Одним из оптимальных методов является использование программы "loaddrv", которая принимает в качестве аргумента командной строки имя драйвера без его расширения и загружает его через системную функцию 68.16. Например, при исполнении команды "loaddrv sdhci" программа загрузит драйвер "/sys/drivers/sdhci.sys". Данный способ загрузки позволяет без дополнительных изменений ядра добавить загрузку драйвера, прописав его в файл "/sys/settings/autorun.dat". Этот метод предпочтителен при добавлении новых драйверов в основной образ системы.

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

Существует также ситуация, когда драйвер необходимо загрузить во время инициализации самого ядра, например для работы USB подсистемы ядро само загружает драйвера хост-контроллеров и драйвера классов устройств. Загрузка драйверов из ядра аналогична загрузке через системную функцию 68.16. Данный метод не рекомендуется, но может быть применён в специализированных под определённое железо сборках ядра.

Краткое описание экспортируемых ядром функций

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

Мьютексы и семафоры

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

struct mutex {
	struct list_head	wait_list;
	atomic_t            	count;
};

void fastcall mutex_init(struct mutex *lock);
void fastcall mutex_lock(struct mutex *lock);
void fastcall mutex_unlock(struct mutex *lock);
void fastcall init_rwsem(struct rw_semaphore *sem);
void fastcall down_read(struct rw_semaphore *sem);
void fastcall down_write(struct rw_semaphore *sem);
void fastcall up_read(struct rw_semaphore *sem);
void fastcall up_write(struct rw_semaphore *sem);

Работа с подсистемой событий

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

Интерфейс работы с дисковой подсистемой

Ядро предоставляет интерфейс для добавления, удаления и работы с логическими дисками. Данный интерфейс позволяет реализовывать драйвера различных дисковых устройств, вне зависимости от их физического интерфейса, в том числе и виртуальные.
Для добавления нового диска используется функция DiskAdd. Описание передаваемых в неё параметров приведено ниже по тексту.
Для удаления диска используется функция DiskDel, в которую передаётся полученный ранее указатель. Эта функция удаляет диск из единого списка логических дисков.

void* DiskAdd(DISKFUNC* functions, const char* name, uint32_t userdata, uint32_t flags);
void DiskDel(void* hDisk);
void DiskMediaChanged(void* hDisk, int newstate);
; Flags for add new disk
DISK_NO_INSERT_NOTIFICATION = 1

Некоторые диски имеют возможность изменения содержимого, например CD приводы, и по этому в ядре предусмотрена функция "DiskMediaChanged". Эта функция информирует ядро о том, что носитель был вставлен, удален или изменен. Значение "newstate" должно быть равно нулю, если в данный момент носитель не вставлен, и ненулевым в противном случае. Эта функция не должна вызываться с ненулевым значением newstate ни из одной callback функции. Эта функция не должна вызываться, если активен другой вызов этой функции.

Если при добавлении диска был установлен флаг DISK_NO_INSERT_NOTIFICATION, то драйвер не должен вызывать функцию "DiskMediaChanged" и ядро будет проверять наличие носителя при каждой операции. Данный подход может использоваться для дисков, подключение или изъятие носителей которых не может быть определено драйвером, например данный флаг применяется в драйвере контроллера floppy дисков.

Callback функции драйвера диска

Как уже было описано выше, при добавлении нового диска, драйвер должен передать ядру указатель на структуру DISKFUNC, которая содержит общий размер структуры(поле strucsize) и массив указателей на функции. Если функция отсутствует, то вместо неё должен быть записан ноль.

struct  DISKFUNC
        strucsize       dd ?
        close           dd ?
        closemedia      dd ?
        querymedia      dd ?
        read            dd ?
        write           dd ?
        flush           dd ?
        adjust_cache_size       dd ?
        LoadTray        dd ?
ends

; Error codes for callback functions.
DISK_STATUS_OK              = 0 ; success
DISK_STATUS_GENERAL_ERROR   = -1; if no other code is suitable
DISK_STATUS_INVALID_CALL    = 1 ; invalid input parameters
DISK_STATUS_NO_MEDIA        = 2 ; no media present
DISK_STATUS_END_OF_MEDIA    = 3 ; end of media while reading/writing data
DISK_STATUS_NO_MEMORY       = 4 ; insufficient memory for driver operation
void close(void* userdata);

Необязательная функция. Функция которая освобождает все ресурсы, зависящие от драйвера, для диска.

void closemedia(void* userdata);

Необязательная функция, может отсутствовать если носитель не является съемным. Функция, вызов которой информирует драйвер о том, что ядро завершило всю обработку с текущим носителем. Если носитель удален, драйвер должен отклонять все запросы к этому носителю с помощью команды DISK_STATUS_NO_MEDIA, даже если вставлен новый носитель, до тех пор, пока не будет вызвана эта функция. Если носитель удален, новый вызов 'disk_media_changed' не разрешен до тех пор, пока не будет вызвана эта функция.

int querymedia(void* userdata, DISKMEDIAINFO* info);

; Media flags. Represent bits in DISKMEDIAINFO.Flags.
DISK_MEDIA_READONLY = 1

struct  DISKMEDIAINFO
        Flags           dd ? ; Combination of DISK_MEDIA_* bits.
        SectorSize      dd ? ; Size of the sector.
        Capacity        dq ? ; Size of the media in sectors.
        LastSessionSector       dd ? ; Number last session sectors for CDFS
ends

Обязательная функция, которая производит заполнение структуры DISKMEDIAINFO и возвращает DISK_STATUS_* код.

int read(void* userdata, void* buffer, __int64 startsector, int* numsectors);

Обязательная функция. Функция для чтения секторов диска в буфер "buffer", начиная с сектора "startsector". Количество считываемых секторов находится по указателю "numsectors". Функция должна возвращать DISK_STATUS_* код и записать количество успешно прочитанных секторов по указателю "numsectors". Указатель на буфер является виртуальным адресом.

; int write(void* userdata, void* buffer, __int64 startsector, int* numsectors);

Необязательная функция. Функция для записи секторов диска из буфера "buffer", начиная с сектора "startsector". Количество записываемых секторов находится по указателю "numsectors".Функция должна возвращать DISK_STATUS_* код и записать количество успешно записанных секторов по указателю "numsectors". Указатель на буфер является виртуальным адресом.

int flush(void* userdata);

Необязательная функция. Функция очищает внутренний кэш устройства и возвращает DISK_STATUS_* код. Обратите внимание, что функции чтения/записи вызываются менеджером кэша, поэтому драйвер не должен создавать программный кэш. Эта функция реализована для очистки аппаратного кэша, если он существует.

unsigned int adjust_cache_size(void* userdata, unsigned int suggested_size);

Необязательная функция. Функция возвращает размер кэша для данного устройства в байтах. При возврате нуля программный кэш не используется.

int LoadTray(void* userdata, int flags);

Необязательная функция. Функция для загрузки/выгрузки носителя, операция определяется флагом: 0 - загрузить, 1 - выгрузить. Функция возвращает DISK_STATUS_* код.

Интерфейс работы со встроенными устройствами

Встроенные устройства включают в себя всевозможные устройства, расположенные непосредственно на материнской плате или подключаемым к внутренним шинам, например ISA или PCIe.

Прерывания

Прерывания используются многими встроенными устройствами для оповещения драйверов об изменении их состояния, например, для оповещения о подключении сетевого кабеля или нажатие на клавишу клавиатуры. Для добавления своего обработчика прерывания, драйвер должен вызвать функцию в которую передаётся номер прерывания, указатель на функцию обработчика прерывания и необходимые ей данные в виде 4 байт.

int32_t stdcall AttachIntHandler(uint32_t irq, void* handler, uint32_t userdata);

Обработчик прерывания должен соблюдать CDECL соглашение о вызовах и принимать необходимые ей данные в виде 4 байт.

int32_t cdecl irq_handler(uint32_t userdata);

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

Шина PCI

Большинство подключённых к ПК устройств используют шину PCI(PCIe). Для работы с этой шиной драйвера импортируют ряд функций, через которые можно прочесть и записать данные в конфигурационное пространство PCI устройства. Подробнее об этом говорится в статье PCI.
Ядро экспортирует как сами функции работы с PCI шиной, так и функцию GetPCIList для получения указателя на список найденных им устройств. Этот список устройств представляет из себя двусвязный список структур PCIDEV. На основе этой структуры можно произвести поиск PCI устройства без обращения к самой шине, что упрощает написание самого драйвера.

struct  PCIDEV
        bk              dd ?
        fd              dd ?
        vendor_device_id dd ?
        class           dd ?
        devfn           db ?
        bus             db ?
                        rb 2
        owner           dd ? ; pointer to SRV or 0
ends

Для чтения и записи используется набор функций со схожим интерфейсом.

uint8_t  stdcall PciRead8(uint23_t bus, uint32_t devfn, uint32_t reg);
uint16_t stdcall PciRead16(uint23_t bus, uint32_t devfn, uint32_t reg);
uint32_t stdcall PciRead32(uint23_t bus, uint32_t devfn, uint32_t reg);

void stdcall PciWrite8(uint23_t bus, uint32_t devfn, uint32_t reg, uint32_t value);
void stdcall PciWrite16(uint23_t bus, uint32_t devfn, uint32_t reg, uint32_t value);
void stdcall PciWrite32(uint23_t bus, uint32_t devfn, uint32_t reg, uint32_t value);

PCIDEV* fastcall GetPCIList();

Кроме этих функций существует также функция PciApi, через которую также возможно осуществить чтение и запись конфигурационного пространства. Интерфейс этой функции повторяет интерфейс системной функции 62.

Шина USB

Подробное описание интерфейса находится в статье USB API.

Порты ввода/вывода

Драйверы могут взаимодействовать со всеми портами ввода/вывода, но для избежание вредоносного взаимодействия со стороны пользовательского ПО необходимо зарезервировать необходимые порты через функцию ReservePortArea.

;reserve/free group of ports
;  * eax = 46 - number function
;  * ebx = 0 - reserve, 1 - free
;  * ecx = number start arrea of ports
;  * edx = number end arrea of ports (include last number of port)
;Return value:
;  * eax = 0 - succesful
;  * eax = 1 - error
;  * The system has reserve this ports:
;    0..0x2d, 0x30..0x4d, 0x50..0xdf, 0xe5..0xff (include last number of port).
;destroys all registers
ReservePortArea

Кроме этого ядро при загрузке само резервирует некоторый диапазон портов:

* 0-45
* 48-77
* 80-223
* 229-255

Многие устройства предоставляют интерфейс работы через спроецированные на память регистры контроллера. Для работы с этими регистрами используется функция MapIoMem, в которую передаётся базовый адрес физической памяти, на который указывает устройство(во многих PCI устройствах такой адрес будет находится в BAR регистрах конфигурационного пространства PCI) размер и флаги страниц памяти. Функция вернёт указатель на базовый адрес в виртуальной памяти либо ноль в случае неудачи.

void* stdcall MapIoMem(void* base, uint32_t size, uint32_t flags);

Интерфейс взаимодействия с сетевой подсистемой

Краткое описание интерфейса находится в статье о сетевых драйверах.

Интерфейс взаимодействия с графической подсистемой

Взаимодействие с графической подсистемой происходит в основном через экспортируемую ядром структуру display_t и нескольких дополнительных функций. Получить структуру display_t можно через вызов функции "GetDisplay".

Регистрация аппаратного курсора

Для изменения функций курсора указатели на них изменяются в структуре display_t. TODO: написать описание функций и пример аппаратного курсора для AMD видеокарты.

Работа с фреймбуфером

Для изменения размеров области вывода изображения в фреймбуфер, без изменения его физического расположения(адрес не меняется ), применяется функция "SetScreen".

; in:
; eax - new Screen_Max_X
; ecx - new BytesPerScanLine
; edx - new Screen_Max_Y
set_screen:

Кроме изменения размера возможно изменить физическое расположение самого фреимбуфера с помощью функции "SetFramebuffer"

struct FRB
        list            LHEAD
        magic           rd 1
        handle          rd 1
        destroy         rd 1

        width           rd 1
        height          rd 1
        pitch           rd 1
        format          rd 1
        private         rd 1
        pde             rd 8
ends
void fastcall SetFramebuffer(struct FBR* _fbr);

Работа с оконной подсистемой

Для взаимодействия с оконной подсистемой ядро экспортирует функцию "GetWindowRect" с помощью которой можно получить координаты границ окна текущего потока.

void fastcall get_window_rect(struct RECT* rc);

Остальные импортируемые ядром функции

Функция для драйверов мыши

void stdcall SetMouseData(uint32_t BtnState, uint32_t XMoving, uint32_t YMoving, uint32_t VScroll, uint32_t HScroll);

Функции для драйверов клавиатуры

KEYBOARD* stdcall RegKeyboard(KBDFUNC* func, uint32_t userdata);
void stdcall DelKeyboard(KEYBOARD* handle);
void fastcall SetKeyboardData(uint32_t scancode);

Функции работы с потоками

Функции таймера

void* TimerHS(unsigned int deltaStart, unsigned int interval,
              void* timerFunc, void* userData);
void CancelTimerHS(void* hTimer);

TODO

Функции вывода в доску отладки

Функции выделения памяти

Звуковая подсистема

Краткое описание интерфейса находится в статье о драйверах звуковых карт.

Полезные макросы языка fasm и особенности их применения

Для языка fasm использование макросов достаточно сильно может упростить написание программ и драйверов. Кроме пользы, макросы могут внести путаницу в поиске ошибок реализации любой программы.

DEBUGF