Writing drivers for KolibriOS/ru: Difference between revisions

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


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


'''Предупреждение 1.''' Прежде чем писать драйвер, хорошо подумайте, нельзя ли обойтись средствами прикладных [http://ru.wikipedia.org/wiki/API API], в частности, функций работы с оборудованием 41-46 и 62. Во-первых, от ошибки в кривом приложении пострадает только это кривое приложение, а кривой драйвер способен без особого труда обрушить всю систему. Во-вторых, для приложений можно вылавливать баги в отладчике <tt>mtdbg</tt>, обладающем определёнными возможностями, а для драйверов этот путь закрыт (разве что встроенный отладчик эмулятора [http://ru.wikipedia.org/wiki/Bochs Bochs], но он заведомо непригоден для отладки с реальным железом), так что единственным средством остаётся отладочный вывод на доску отладки <tt>board</tt> со всеми недостатками.
'''Предупреждение 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, последнюю на момент написания этих строк (в дистрибутиве 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]. В исходниках дистрибутива 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> означает адрес загрузки ядра. Для "официального" ядра (в частности, в 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> несовместимость ещё хуже: в 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>, в 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, т.е. с текущим ядром он не пойдёт! Дело в том, что этот каркас в общем-то не обновлялся (если не считать копирайта) с 0.6.5.0, так что <tt>new_app_base</tt> и <tt>version</tt> остались старые. Попутно отмечу, что старшее слово - это первая тройка, а младшее - вторая в силу обратного расположения байт в слове и слов в двойном слове (вообще-то я уверен, что вы и так это знаете, но для очистки совести...)
Выше мы создали функцию START, о которой уже говорилось ранее. Данная функция принимает 2 аргумента: state и cmdline и возвращает хэндлер драйвера, полученный при вызове RegService. Если эта функция вернёт 0, то это будет означать, что драйвер по какой-то причине не может работать(либо нет нужного оборудования, с которым бы драйвер работал, либо его что-то не устраивает и чтобы не создавать ошибок, драйвер завершает работу). Параметр cmdline это командная строка в ascii кодировке, sta