Page 1 of 3

Как писать драйвера

Posted: Sat Apr 07, 2007 1:18 pm
by diamond
Вот, написал статью, демонстрирующую программирование драйвера.
http://diamondz.land.ru/writedrv.htm
Приведённый там пример может быть полезен и безотносительно к обучению - написан фильтр, показывающий все обращения программ к файловой системе.
Spoiler:Пишем драйвер для КолибриОС.
Вступление

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

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

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

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

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

;driver sceletone

format MS COFF

include 'proc32.inc'
include 'imports.inc'

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

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

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

OS_BASE equ 0;
new_app_base equ 0x60400000
PROC_BASE equ OS_BASE+0x0080000

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

Едем дальше:

struc IOCTL
{ .handle dd ?
.io_code dd ?
.input dd ?
.inp_size dd ?
.output dd ?
.out_size dd ?
}

virtual at 0
IOCTL IOCTL
end virtual

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

public START
public service_proc
public version

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

version dd 0x00030003

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

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

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

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

DEBUG equ 1

DRV_ENTRY equ 1
DRV_EXIT equ -1
STRIDE equ 4 ;size of row in devices table

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

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 при неудаче или (ненулевой) зарегистрированный хэндл при успехе. Кстати, как узнать, что делает та или иная экспортируемая функция? Допустим, нам позарез нужно выделить пару страниц памяти ядра. Лезем в исходники ядра, файл core/exports.inc, просматриваем экспортируемые имена (они осмысленны) и видим szKernelAlloc. Пролистываем вниз до метки kernel_export и ищем szKernelAlloc - обнаруживаем, что ему соответствует процедура kernel_alloc. Теперь ищем реализацию kernel_alloc, она обнаруживается в 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 из core/dll.inc), так и приложение функцией 68.17 (хэндл приложение может добыть при загрузке драйвера функцией 68.16). Нулевое возвращаемое значение означает успех, ненулевое соответствует ошибке.
Вначале определяем сокращённые имена для членов структуры, описывающей запрос. В поле handle содержится хэндл драйвера (такой же, как и возвращаемое значение RegService), io_code - dword-идентификатор запроса, остальные поля вопросов вызывать не должны. Возвращаемое значение напрямую передаётся вызвавшему нас коду (драйвер/ядро/приложение). В конце восстанавливаем значения, переназначенные было на короткие имена членов структуры. В данном случае это без надобности, но в случае сложных драйверов короткие имена типа "input" запросто могут встречаться не один раз.

Дальше в sceletone.asm содержится код поиска заданного оборудования на 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.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 (код этот обработчика располагается в файле 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.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:

Posted: Sat Apr 07, 2007 5:22 pm
by Serge
Есть два дополнения.

Это не обязательно, но желательно иметь функцию для получения версии API драйвера. Для этого зарезервирован ioctl.io_code=0

вот пример из недоделанного драйвера uart.

Code: Select all

SRV_GETVERSION equ 0

; retval
;  ebx= service version
;  eax= error code
;    0= no error
;   -1= common error

align 4
init_uart:
           mov eax, 68
           mov ebx, 16
           mov ecx, szUart
           int 0x40

           mov [Uart], eax
           test eax, eax
           jz .fail

           push 0              ;storage for version
           mov eax, esp        ;eax= pointer to output buffer
           xor ebx, ebx

           push 4              ;.out_size
           push eax            ;.output
           push ebx            ;.inp_size
           push ebx            ;.input
           push SRV_GETVERSION ;.code
           push [Uart]         ;.handle

           mov eax, 68
           mov ebx, 17
           mov ecx, esp        ;address of IOCTL in app stack
           int 0x40
           add esp, 24         ;sizeof IOCTL
           pop ebx             ;load version
           ret
.fail:
           or eax, -1
           ret
68.16 получает логический номер драйвера и сохраняет его в переменной Uart. После чего в стеке формируется стрктура IOCTL. Чтобы избежать проблем в будущем рекомендую заполнять все поля структуры. В данном примере это происходит в обратном порядке. Последним в стек помещается логический номер драйвера после чего esp становится указателем на структуру IOCTL. После вызова стек восстанавливается командой add esp,24 а pop ebx загружает возвращённый драйвером номер версии.

Второе замечание по кодам ошибок.
Вызовы 68.17 возвращают в eax коды ошибок. Это 0 в случае успеха и ненулевое значение в случае неудачи, обычно -1 как признак общей ошибки, другие коды неопределены. Поэтому драйвер не должен использовать регистр eax для
возврата значений приложению. Все данные от драйвера должны возвращаться в буфере IOCTL.output

Кстати, есть смысл определить стандартные коды ошибок при вызовах 68.17 Например от -1 коды ошибок ядра: драйвер не установлен, неправильный логический номер драйвера и т.д. от +1 коды внутренних ошибок драйвера

Posted: Sat Apr 07, 2007 5:36 pm
by N†OSKRNL
diamond - очень захватывающая статья, в высоком и живом стиле. :) Думаю еще раз пять перечитаю, и на досуге отважусь с подобными экспериментами c колибри (0.6.5.0).

Posted: Sat Apr 07, 2007 6:59 pm
by Serial
diamond, спасибо за интересную статью.

Posted: Sat Apr 07, 2007 7:39 pm
by diamond
Serge
Спасибо, будет исправлено (к понедельнику).

Posted: Sat Apr 07, 2007 10:08 pm
by Serge
diamond
Хорошая статья, но есть ещё замечание.
По собственному опыту я бы переписал вызов драйвера в mainloop

Code: Select all

           push 0              ;storage for output logsize
           mov eax, esp        ;eax= pointer to output data

           push 16*1024        ;logsize
           push logbuf         ;pointer to log buffer
           mov  ebx, esp       ;pointer to input data
           push 4              ;.out_size
           push eax            ;.output
           push 8              ;.inp_size
           push ebx            ;.input
           push 2              ;.code
           push [handle]       ;.handle

           mov eax, 68
           mov ebx, 17
           mov ecx, esp        ;address of IOCTL in app stack
           int 0x40
           add esp, 24+8       ;sizeof IOCTL+ptr_logbuf+log_size
           pop ecx             ;load output logsize
Так конечно длиннее и выглядит сложнее, но есть смысл рассматривать .input как указатель на структуру содержащую входные данные а .output на структуру содержащую выходные данные. В данном случае входные данные адрес logbuf и его размер, а выходные количество записанных байт. Если передаётся и возвращается только одно двойное слово всегда хочется сделать это в полях .input и .output. Но думаю что лучше так не делать потому что надо добавить в ядро проверку адресов .input и .output и страниц памяти на присутствие/запись/право доступа. Сейчас передав неправильный указатель можно обрушить всю систему.

Posted: Mon Apr 09, 2007 5:59 pm
by diamond
Serge
Старые замечания учтены, статья обновлена, ссылка та же. Только у меня появился вопрос: номер версии для ioctl=0 следует возвращать в каком-то фиксированном формате или совершенно произвольном? (текущая реализации ядра это вообще игнорирует, но мало ли какие могут быть планы...) В первом случае, возможно, вместо банального dword имеет смысл стандартный формат с возможностью контроля совместимости версий?
По поводу последнего замечания: в статье .input и .output содержат действительно указатели, только на глобальные переменные, а не стековые, как в твоём примере. Конечно, если нужно посылать запросы из разных потоков многопоточной программы, нужен стек, но подобное встречается редко. Соответственно если вставить в ядро проверку на принадлежность приложению буферов, на которые указывают .input/.output, размера .inp_size/.outp_size, то разобранный в статье пример по-прежнему будет работать. А размер передаётся в поле .outp_size (причём как in/out - на входе в драйвер он содержит размер пользовательского буфера, на выходе - размер записанных данных), а иначе зачем вообще нужно это поле?

Posted: Tue Apr 10, 2007 7:27 am
by Mario79
diamond
Статья конечно интересная и полезная, однако у меня возник вопрос при прочтении - "Та организация, которую некоторые считают ругательным словом, не будет иметь претензии на формат заголовка?" Может быть, использовать другой более свободный вариант?
И еще подменять можно всю функцию целиком или подфункцию тоже можно?

Posted: Tue Apr 10, 2007 7:33 am
by Serge
diamond

С этими версиями одна головная боль. Для ядра важно только старшее слово - версия драйверной модели так что младшее может хранить что угодно, например ревизию svn. Для звука там хранится текущая версия API а приложения получают диапазон версий SOUND_VERSION.

SOUND_VERSION equ 0x01000100 ;старшее слово - минимально совместимая, младшее - текущая версия.
version dd (4 shl 16) or (SOUND_VERSION and 0xFFFF)

Если есть идеи по формату и контролю версий, предлагай. У меня идеи уже закончились.

По поводу .input и .output. Глобальные или локальные конечно не важно. Если программа делает много разных вызовов размещение в стеке удобней и уменьшает размер кода. Дело в передаче параметров. Все поля ioctl для драйвера входные данные - указатель на структуру входных данных, размер структуры, указатель на структуру выходных данных, размер структуры.
То есть поля не предназначены для возврата значения. Сейчас все вызовы обрабатываются в контексте вызвавшего потока. Указатель на ioctl передаётся непосредственно. если способ передачи изменится и структура будет копироваться код перестанет работать.

Posted: Wed Apr 11, 2007 6:01 pm
by diamond
Mario79
Думаю, не будет. Всё-таки базовым для MS COFF (и PE, между прочим) является COFF, а он изначально был под *nix.
Подфункцию подменять тоже можно. Самый простой способ - перехватить всю функцию и в начале обработчика проверить, вызвана ли нужная нам подфункция и если нет, то передать управление ядерному обработчику (в коде из статьи - командой jmp [oldfn70]).
Serge
Ну раз поля не предназначены для возврата значения, тогда надо немного изменить код. Будет исправлено.

Posted: Sat Apr 21, 2007 11:32 am
by Ghost
[offtop]Добавь в статью её адресс, т.к. статья обновляется - всегда можно будет легко проверить соответствие сохранённой и web версий[/offtop]

Posted: Mon Apr 23, 2007 5:19 pm
by diamond
Да я бы не сказал, что она обновляется - вот сейчас исправил последнее замечание Serge и никаких изменений в будущем не предвидится... А дата последнего обновления всегда указана на http://diamondz.land.ru

Posted: Fri May 11, 2007 2:05 pm
by Mihail
Кто как и когда эти драйверы запускает?

Я скомпилировал текст

Code: Select all

format MS COFF

include 'proc32.inc'
include 'main.inc'
include 'imports.inc'

DEBUG            equ 1

OS_BASE          equ 0
new_app_base     equ 0x60400000
PROC_BASE        equ OS_BASE+0x0080000


struc IOCTL
{  .handle           dd ?
   .io_code          dd ?
   .input            dd ?
   .inp_size         dd ?
   .output           dd ?
   .out_size         dd ?
}

virtual at 0
  IOCTL IOCTL
end virtual

section '.flat' code readable align 16

proc START stdcall, state:dword

        mov    esi,msgStart
        call   boot_log
        mov esi, msgStart
        call SysMsgBoardStr
	jmp $
endp


version       dd 0x00030003

msgStart      db 'start...',13,10,0

Записал полученный объектник в каталог drivers , но никакого эффекта не увидел.

Posted: Fri May 11, 2007 2:34 pm
by k@sTIg@r
Есть 2 варианта.
1) В нужном месте в ядре вызываешь ф-цию load_driver , которая принимает один параметр - имя драйвера без .obj (загружает /rd/1/drivers/name.obj)
2) загружаешь драйвер программно с помощью 68.16(смотри подробное описание ф-ции).
Для начала советовал бы 2 вариант.

Posted: Fri May 11, 2007 5:23 pm
by Mihail
k@sTIg@r wrote:Есть 2 варианта.
В обоих случаях возвращается 0 в EAX.
С другими драйверами тоже.
Я вставлял диагностику при
stdcall load_driver, szHwMouse
в kernel\video\cursors.inc
возвращается 0.