Writing drivers for KolibriOS: Difference between revisions
No edit summary |
No edit summary |
||
Line 38: | Line 38: | ||
<asm> | <asm> | ||
OS_BASE equ | OS_BASE equ 0x80000000 | ||
new_app_base equ | new_app_base equ 0x00000000 | ||
PROC_BASE equ OS_BASE+0x0080000 | PROC_BASE equ OS_BASE+0x0080000 | ||
</asm> | </asm> | ||
'''OS_BASE''' means the kernel address. For the | '''OS_BASE''' means the kernel address. For the “old” kernel (for example, version 0.6.5.0) it is 0, for “flat kernel” (current kernel) it is 0x80000000 | ||
The '''new_app_base''' constant means the line address where the applications are loaded: all applications are loaded into the same address, but intersection doesn't occur because every process has its own page table and every application is sure that it's loaded into address 0. In ring 3 CS/DS/ES/SS selectors have new_app_base base, and in ring 0 (kernel and drivers) - base 0. So, to | The '''new_app_base''' constant means the line address where the applications are loaded: all applications are loaded into the same address, but intersection doesn't occur because every process has its own page table and every application is sure that it's loaded into address 0. In ring 3 CS/DS/ES/SS selectors have new_app_base base, and in ring 0 (kernel and drivers) - base 0. So, to convert an application address into a kernel pointer it's necessary to add new_app_base to it (believe me if you don't understand it). The incompatibility issue with new_app_base is even worse: in 0.6.5.0 this constant is 0x60400000, in svn.450 - 0x80000000, and in “flat” kernel (current kernel) it is 0 (it is called “flat” because it uses a flat memory model). How can we get real values of OS_BASE and new_app_base for particular kernel? It's elementary - they're declared in const.inc (from kernel sources), so it's enough to find them there. And the KolibriOS memory map is placed in memmap.inc from the kernel sources distribution. | ||
The third constant (PROC_BASE) means nothing in our case, it isn't used at all. | The third constant (PROC_BASE) means nothing in our case, it isn't used at all. |
Revision as of 11:45, 13 November 2007
Driver development for KolibriOS
Introduction
Warning 1. Before writing a driver for KolibriOS for any purposes think if you can reach your aim by application programming interfaces (API), in particular - hardware interface functions 41-46 and 62. There are several reasons to prefer an application to a driver. First of all, a bug in an application can hang this application only, and the operating system will continue working. But a bug in a driver can cause the entire system to crash. Though drivers are very critical for system stability it's difficult to debug them. Developing an application you can hunt for bugs using mtdbg, which is quite an advanced debugger; but you can't use it to debug a driver. Sometimes the embedded Bochs debugger can help, but, of course, it is useless if you need to work with real hardware. You can only send messages to the KolibriOS debug message board, which has many disadvantages.
Warning 2. It's clear that every driver depends on the kernel architecture very much. But KolibriOS kernel is modified several times a week. Of course, the most of changes don't touch the drivers subsystem, but sometimes important system functions exported to drivers are changed or destroyed, or new functions are created. So if you compile the code of this article it's possible that it won't work in its original condition. Read the text carefully - I'll try to pay attention to all possible causes of crashes and problems and required changes. My code is written for svn.450 revision, which is the newest one at the moment (so without modification it won't work in 0.6.5.0 distribution).
As a rule, the only thing we write drivers for is hardware. A real driver includes a huge mass of hardware-dependent code which has nothing to deal with the subject of this article - basic principles of KolibriOS kernel driver subsystem. The process of driver development is shown on the following example: we'll write a driver which catches and writes down all queries applications send to the filesystem, and a program which receives data from the driver and displays them. FASM is used as the main development tool.
The Driver
In KolibriOS kernel sources a basic skeleton of driver is supplied. It is sceletone.asm (it is placed in kernel/drivers in KolibriOS 0.6.5.0 kernel sources distribution or in svn://kolibrios.org/kernel/trunk/drivers on KolibriOS SVN repository). Let's have look at it (it is svn.450 version):
<asm>
- ;;
- Copyright (C) KolibriOS team 2004-2007. All rights reserved. ;;
- Distributed under terms of the GNU General Public License ;;
- ;;
- driver sceletone
format MS COFF
include 'proc32.inc' include 'imports.inc' </asm>
First of all (after several usual comments) we should tell the compiler which binary file format to use. All drivers must be compiled in COFF object file format (“MS” in the middle means that the driver uses a special version of COFF extended by Microsoft. It allows to use “writeable” attributes for sections).
Then the driver plugs in additional files. proc32.inc includes macros for standard procedure definitions and calls (proc/endp, stdcall/ccall/invoke/cinvoke, local for local variables creation), it is placed in the same folder as sceletone.asm. This file is optional, it's up to you to use it or not; in this article proc32.inc is used not to increase the complexity of code (generally, macros can be a bad idea if you wish for maximal effectiveness).
imports.inc includes definitions for all exported kernel functions. Make a look at it, there's nothing special in it, only a mess of trivial constructions. Actually (as it takes place when loading the driver) the list of all exported functions and data is placed in core/exports.inc (kernel_export label), so if you export something feel free to change this file. But don't forget to add your changes to imports.inc to take care of others.
Be careful! A compatibility issue can occur here. The kernel will refuse to load your driver if it uses functions which aren't present in the kernel. It will write “unresolved” and the function name on the debug board.
Well, let's go forward.
<asm> OS_BASE equ 0x80000000 new_app_base equ 0x00000000 PROC_BASE equ OS_BASE+0x0080000 </asm>
OS_BASE means the kernel address. For the “old” kernel (for example, version 0.6.5.0) it is 0, for “flat kernel” (current kernel) it is 0x80000000
The new_app_base constant means the line address where the applications are loaded: all applications are loaded into the same address, but intersection doesn't occur because every process has its own page table and every application is sure that it's loaded into address 0. In ring 3 CS/DS/ES/SS selectors have new_app_base base, and in ring 0 (kernel and drivers) - base 0. So, to convert an application address into a kernel pointer it's necessary to add new_app_base to it (believe me if you don't understand it). The incompatibility issue with new_app_base is even worse: in 0.6.5.0 this constant is 0x60400000, in svn.450 - 0x80000000, and in “flat” kernel (current kernel) it is 0 (it is called “flat” because it uses a flat memory model). How can we get real values of OS_BASE and new_app_base for particular kernel? It's elementary - they're declared in const.inc (from kernel sources), so it's enough to find them there. And the KolibriOS memory map is placed in memmap.inc from the kernel sources distribution.
The third constant (PROC_BASE) means nothing in our case, it isn't used at all.
<asm> struc IOCTL { .handle dd ?
.io_code dd ? .input dd ? .inp_size dd ? .output dd ? .out_size dd ?
}
virtual at 0
IOCTL IOCTL
end virtual </asm>
It's simply a structure definition (speculations with VIRTUAL are commonplace for FASM).
<asm> public START public service_proc public version </asm>
We've already imported necessary functions from kernel (imports.inc). And now we let the kernel know about the driver. Let's start from the end. The version variable which is declared in the following text is not actually the driver version. It is the version of the interface this driver works with. In particular, two version codes are encoded in one DWORD. The lower WORD doesn't play any role in current kernel version, but you should put there the version number of the interface which is “native” for the driver. The higher WORD means minimal version the driver requires to work with. It must be in the interval between DRV_COMPAT and DRV_CURRENT constants which are defined in core/dll.inc file in kernel sources distribution. In 0.6.5.0 the both of them are 3. But unil svn.450 the interface has been already changed and the constants are now 4. What is this stuff necessary for? It's made to solve the following problem. The changes in driver subsystem can be divided into several types: 1) full or partial modification of one or more basic conceptions; 2) deletion of one or more exported kernel functions; 3) function interface modification (parameters in stack/register, additional arguments, arguments order, etc); 4) adding new functions. As a rule, in the situations 1-3 you'll have to change your drivers by hand. But it's not a very good thing to recompile all drivers after adding a new kernel function even if they don't need it. So the kernel supports loading “not very old” drivers.
<asm> version dd 0x00030003 </asm>
The driver skeleton is made for version 3, so it won't work with current kernel. It hasn't been updated since 0.6.5.0 (besides the copyright), so new_app_base and version are old. By the way, keep in mind that the higher WORD consists of the first 3 bytes of DWORD, and the lower one consists of the second 3 bytes because bytes in WORD and words in DWORD are placed in reverse order (of course, I'm sure you know it, but just in case...)
The START procedure is called by the system when loading the driver and when closing it. In the first situation it initializes the driver, in the second - finishes its work. You don't need to export the service_proc procedure - there are other methods to let the kernel know about it. We'll talk about it later. And finally, the last portion of constants in sceletone.asm is the following:
<asm> DEBUG equ 1
DRV_ENTRY equ 1 DRV_EXIT equ -1 STRIDE equ 4 ;size of row in devices table </asm>
The first of them turns on debug output in the blocks if DEBUG/end if. DRV_ENTRY and DRV_EXIT define possible values of the START procedure argument. The last constant is necessary for beauty. Now we see the first executable code. The line
<asm> section '.flat' code readable align 16 </asm>
means the very thing which is written. Then come more interesting things.
<asm> proc START stdcall, state:dword
cmp [state], 1 jne .exit
.entry:
if DEBUG mov esi, msgInit call SysMsgBoardStr end if stdcall RegService, my_service, service_proc ret
.fail: .exit:
xor eax, eax ret
endp </asm>
This is the code of the procedure of initialization/finalization. When loading the driver it is called with argument DRV_ENTRY = 1 and it must return a non-zero value if there are no problems. When halting the system START is called with argument DRV_EXIT = -1. In particular case or driver doesn't work with any hardware, so there's no hardware initialization or finalization code. The procedure includes only minimally required actions necessary to complete the driver loading. It is registration. The RegisterService function is exported by kernel. It accepts two arguments: driver name (up to 16 symbols including the terminating zero) and the pointer to the procedure handling the I/O. RegisterService returns 0 when registration is failed or a non-zero registered handler if everything is well.
By the way, you may ask how to learn what a particular kernel function does. For example, we need to allocate several pages of kernel memory. To get the reference we open core/exports.inc in kernel sources, look through exported names (they're quite human-readable) and notice szKernelAlloc. We page down until kernel_export label and look for szKernelAlloc - it is assigned to a procedure kernel_alloc. Now look for the implementation of this procedure - it is placed in core/heap.inc. There are no comments, but there's a definition which makes clear that this function accepts one argument called size of DWORD type. It's clear that kernel_alloc allocates as much kernel memory as the single argument tells to. And the very first lines of its code show that the size of allocated memory is aligned up to 4096 (i. e. the size of a page), so it's clear that it allocates an integer number of pages, and size is given in bytes. After such a lyric digression let's leturn to our driver.
After the described code of startup procedure these lines follow. It's service_proc procedure - the queries handler.
<asm> handle equ IOCTL.handle io_code equ IOCTL.io_code input equ IOCTL.input inp_size equ IOCTL.inp_size output equ IOCTL.output out_size equ IOCTL.out_size
align 4 proc service_proc stdcall, ioctl:dword
- mov edi, [ioctl]
- mov eax, [edi+io_code]
xor eax, eax ret
endp
restore handle restore io_code restore input restore inp_size restore output restore out_size </asm>
service_proc is called when somebody tries to communicate with the driver. It can be another driver (a driver can call itself through the I/O mechanism, but it's unnecessary) which has got the handler somewhere and called ServiceHandler, it can be even Its Majesty the Kernel (srv_handler, srv_handlerEx from core/dll.inc), or, finally, an application using function 68.17 (it can get the handler when loading the driver with 68.16). A zero return value means success, a non-zero value - an error.
First of all we define shortened names for the members of structure which describes the query. In the handle field the driver handler is placed, io_code - the DWORD-typed query identifier, the other fields should be clear. The returned value is passed to the code which has called the driver (another driver/the kernel/an application). Finally the program restores the values which have been resigned to shortened names of structure's fields. In the particular case it is useless, but in more complex drivers such common name as “input” may be used many times.
The next portion of code in sceletone.asm looks for the specified hardware on the PCI bus. We don't need it, so we shan't describe it here. You'll certainly get dealt with it if you like.
Now we can start writing our own driver. The beginning is trivial.
<asm>
- ;;
- Copyright (C) KolibriOS team 2004-2007. All rights reserved. ;;
- Distributed under terms of the GNU General Public License ;;
- ;;
- FileMon
- driver part
format MS COFF
include 'proc32.inc' include 'imports.inc' </asm>
Write for svn.450 (for 0.6.5.0 you should write new_app_base equ 0x60400000):
<asm> OS_BASE equ 0; new_app_base equ 0x80000000 </asm>
Several definitions:
<asm> struc IOCTL { .handle dd ?
.io_code dd ? .input dd ? .inp_size dd ? .output dd ? .out_size dd ?
}
virtual at 0
IOCTL IOCTL
end virtual
public START public version
DRV_ENTRY equ 1 DRV_EXIT equ -1
section '.flat' code readable align 16 </asm>
There's nothing special in this code. But now we're starting to implement the functions our driver carries. Wait a little! Before putting fingers on keyboard let's think what we want our driver to do.
Our driver will accept 4 codes of I/O query. Let's describe each of them.
Code 0 is always used to get the driver version (now it is the version of the driver, but not of the driver interface). The handler of this request is not required because the kernel is not interested in driver version at all; but a driver always can be updated, and any code which works with our driver may need its version number. It can be returned in any format, in our example we'll return only a DWORD value equal to 1.
Code 1 means “start logging”, code 2 - “return the log till current moment and reset it”, code 3 - “stop logging”. The log will be stored in an internal 16K-sized buffer (we'll call the driver once a second, so it should be enough. If an overflow occurs we'll note it but extra log records will be rejected). The returned log information will have the following format: the entire size of recorded log data; a byte equal to 0 or 1, 1 means that some records haven't fit into the buffer; and, finally, the log data - an array of dynamically sized structures. The first byte of every structure contains the number of called kernel function, it will determine the latter contents of the structure.
The START procedure in our example is trivial because we don't need to initalize anything in it:
<asm> proc START stdcall, state:dword
cmp [state], 1 jne .exit
.entry:
if DEBUG mov esi, msgInit call SysMsgBoardStr end if stdcall RegService, my_service, service_proc ret
.fail: .exit:
xor eax, eax ret
endp </asm>
Query handlers:
<asm> handle equ IOCTL.handle io_code equ IOCTL.io_code input equ IOCTL.input inp_size equ IOCTL.inp_size output equ IOCTL.output out_size equ IOCTL.out_size
proc service_proc stdcall, ioctl:dword
mov edi, [ioctl] mov eax, [edi+io_code] test eax, eax jz .getversion dec eax jz .startlog dec eax jz .getlog dec eax jz .endlog xor eax, eax ret
.getversion:
cmp [edi+out_size], 4 jb .err mov edi, [edi+output] mov dword [edi], 1 ; version of driver
.ok:
xor eax, eax ret
.err:
or eax, -1 ret
.startlog:
mov al, 1 xchg al, [bLogStarted] test al, al jnz .ok mov [logptr], logbuf call hook jnc .ok mov [bLogStarted], 0 jmp .err
.getlog:
cli mov esi, logbuf mov ecx, [logptr] sub ecx, esi add ecx, 5 cmp ecx, [edi+out_size] jbe @f mov ecx, [edi+out_size] mov [bOverflow], 1
@@:
sub ecx, 5 xor eax, eax xchg al, [bOverflow] mov edi, [edi+output] mov [edi], ecx add edi, 4 stosb rep movsb mov [logptr], logbuf sti xor eax, eax ret
.endlog:
xchg al, [bLogStarted] test al, al jz @f call unhook
@@:
xor eax, eax ret
endp
restore handle restore io_code restore input restore inp_size restore output restore out_size </asm>
Our driver doesn't require any input data, so we don't use the fields input/inp_size. In out_size field the calling code must place the size of the output buffer. If the driver is called by an application the address translations are performed by the kernel. So ioctl contains correct pointers.
We've done everything necessary for communicating with the driver subsystem, and now we can feel free to write something more interesting. We catch the filesystem kernel fuctions 6, 32, 33, 58, 70. The system call handler - int 0x40 - calls a concrete kernel function through servetable (the code of the system call handler can be found in core/syscall.inc). We can make 0x40 to call our code instead of system functions by replacing several items of this table with addresses of our handlers. The address of servetable can be got by scanning the code of int 0x40 function. Its address can be easily got from IDT, and the call command currently looks like call dword[servetable+edi*4], or in machine code - FF 14 BD <servetable> (of course nobody can guarantee that it will always stay like this, for example, edi can be potentially changed to eax).
<asm> hook:
cli sub esp, 6 sidt [esp] pop ax ; limit pop eax ; base mov edx, [eax+40h*8+4] mov dx, [eax+40h*8]
- edx contains address of i40
mov ecx, 100
.find:
cmp byte [edx], 0xFF jnz .cont cmp byte [edx+1], 0x14 jnz .cont cmp byte [edx+2], 0xBD jz .found
.cont:
inc edx loop .find sti mov esi, msg_failed call SysMsgBoardStr stc ret
.found:
mov eax, [edx+3]
- eax contains address of servetable
mov [servetable_ptr], eax mov edx, newfn06 xchg [eax+6*4], edx mov [oldfn06], edx mov edx, newfn32 xchg [eax+32*4], edx mov [oldfn32], edx mov edx, newfn33 xchg [eax+33*4], edx mov [oldfn33], edx mov edx, newfn58 xchg [eax+58*4], edx mov [oldfn58], edx mov edx, newfn70 xchg [eax+70*4], edx mov [oldfn70], edx sti clc ret
unhook:
cli mov eax, [servetable_ptr] mov edx, [oldfn06] mov [eax+6*4], edx mov edx, [oldfn32] mov [eax+32*4], edx mov edx, [oldfn33] mov [eax+33*4], edx mov edx, [oldfn58] mov [eax+58*4], edx mov edx, [oldfn70] mov [eax+70*4], edx sti ret
</asm>
Two additional functions:
<asm> write_log_byte:
- in
- al=byte
push ecx mov ecx, [logptr] inc ecx cmp ecx, logbuf + logbufsize ja @f mov [logptr], ecx mov [ecx-1], al pop ecx ret
@@:
mov [bOverflow], 1 pop ecx ret
write_log_dword:
- in
- eax=dword
push ecx mov ecx, [logptr] add ecx, 4 cmp ecx, logbuf + logbufsize ja @f mov [logptr], ecx mov [ecx-4], eax pop ecx ret
@@:
mov [bOverflow], 1 pop ecx ret
</asm>
Writing the handlers be aware that the registers are moved in a cycle in comparison with the case of calling 0x40 from an application and all pointers belong to the 3rd ring.
<asm> newfn06:
cli push [logptr] push eax mov al, 6 ; function 6 call write_log_byte mov eax, ebx ; start block call write_log_dword mov eax, ecx ; number of blocks call write_log_dword mov eax, edx ; output buffer call write_log_dword pop eax push eax push esi lea esi, [eax+new_app_base] ; pointer to file name
@@:
lodsb call write_log_byte test al, al jnz @b pop esi pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f
.nooverflow:
add esp, 4
@@:
sti jmp [oldfn06]
newfn32:
cli push [logptr] push eax mov al, 32 ; function 32 call write_log_byte pop eax push eax push esi lea esi, [eax+new_app_base] ; pointer to file name
@@:
lodsb call write_log_byte test al, al jnz @b pop esi pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f
.nooverflow:
add esp, 4
@@:
sti jmp [oldfn32]
newfn33:
cli push [logptr] push eax mov al, 33 ; function 33 call write_log_byte mov eax, ebx ; input buffer call write_log_dword mov eax, ecx ; number of bytes call write_log_dword pop eax push eax push esi lea esi, [eax+new_app_base] ; pointer to file name
@@:
lodsb call write_log_byte test al, al jnz @b pop esi pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f
.nooverflow:
add esp, 4
@@:
sti jmp [oldfn33]
newfn58:
cli push [logptr] push eax push ebx lea ebx, [eax+new_app_base] mov al, 58 ; function 58 call write_log_byte
- dump information structure
mov eax, [ebx] call write_log_dword mov eax, [ebx+4] call write_log_dword mov eax, [ebx+8] call write_log_dword mov eax, [ebx+12] call write_log_dword push esi lea esi, [ebx+20] ; pointer to file name
@@:
lodsb call write_log_byte test al, al jnz @b pop esi pop ebx pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f
.nooverflow:
add esp, 4
@@:
sti jmp [oldfn58]
newfn70:
cli push [logptr] push eax push ebx lea ebx, [eax+new_app_base] mov al, 70 ; function 70 call write_log_byte
- dump information structure
mov eax, [ebx] call write_log_dword mov eax, [ebx+4] call write_log_dword mov eax, [ebx+8] call write_log_dword mov eax, [ebx+12] call write_log_dword mov eax, [ebx+16] call write_log_dword push esi lea esi, [ebx+20] ; pointer to file name lodsb test al, al jnz @f lodsd lea esi, [eax+new_app_base+1]
@@:
dec esi
@@:
lodsb call write_log_byte test al, al jnz @b pop esi pop ebx pop eax cmp [bOverflow], 0 jz .nooverflow pop [logptr] jmp @f
.nooverflow:
add esp, 4
@@:
sti jmp [oldfn70]
</asm>
That's all with the code. Now we supply the data:
<asm> version dd 0x00040004 my_service db 'fmondrv',0
msg_failed db 'Cannot hook required functions',13,10,0
section '.data' data readable writable align 16
servetable_ptr dd ?
oldfn06 dd ? oldfn32 dd ? oldfn33 dd ? oldfn58 dd ? oldfn70 dd ?
logptr dd ? logbufsize = 16*1024 logbuf rb logbufsize
bOverflow db ? bLogStarted db ? </asm>
The entire code of the driver is placed in fmondrv.asm which is supplied with this article. Then we compile it with
fasm fmondrv.asm
Then we can package it with kpack and decrease its size from 1850 to 750 bytes. The kernel loads kpacked files perfectly. By the way, in +4 shift a timestamp of compilation is placed in the object file. The kernel isn't interested in it, so you can fill it with zeros and save a byte of storage. To install the driver copy it to /rd/1/drivers - now it's ready for loading.
The operating application
The operating program will write text data to console and finish working on Esc. It will require the console library version 3 or newer, 0.6.5.0 is supplied with version 2, so you'll have to download the last version: http://diamondz.land.ru/console.7z. We use testcon.asm as a template (testcon2.asm is suitable, too) with the following changes: in REQ_DLL_VER we put 3 and in the import table (the myimport label) delete con_write_asciiz and add con_printf, con_kbhit, con_getch2 and, of course, write our code after “Now do some work” comment.
<asm> use32
db 'MENUET01' dd 1 dd start dd i_end dd mem dd mem dd 0 dd 0
REQ_DLL_VER = 3
DLL_ENTRY = 1
start:
- First 3 steps are intended to load/init console DLL
- and are identical for all console programs
- load DLL
mov eax, 68 mov ebx, 19 mov ecx, dll_name int 0x40 test eax, eax jz exit
- initialize import
mov edx, eax mov esi, myimport
import_loop:
lodsd test eax, eax jz import_done push edx
import_find:
mov ebx, [edx] test ebx, ebx jz exit;import_not_found push eax
@@:
mov cl, [eax] cmp cl, [ebx] jnz import_find_next test cl, cl jz import_found inc eax inc ebx jmp @b
import_find_next:
pop eax add edx, 8 jmp import_find
import_found:
pop eax mov eax, [edx+4] mov [esi-4], eax pop edx jmp import_loop
import_done:
- check version
cmp word [dll_ver], REQ_DLL_VER jb exit cmp word [dll_ver+2], REQ_DLL_VER ja exit push DLL_ENTRY call [dll_start]
- yes! Now do some work (say helloworld in this case).
push caption push -1 push -1 push -1 push -1 call [con_init]
</asm>
Loading the driver. In case of an error write it on the console and exit.
<asm>
mov eax, 68 mov ebx, 16 mov ecx, drivername int 0x40 mov [hDriver], eax test eax, eax jnz @f
loaderr:
push aCantLoadDriver call [con_printf] add esp, 4 push 0 call [con_exit] jmp exit
@@: </asm>
Check the driver version number by sending it query with code 0.
<asm>
and [ioctl_code], 0 and [inp_size], 0 mov [outp_size], 4 mov [output], driver_ver mov eax, 68 mov ebx, 17 mov ecx, ioctl int 0x40 test eax, eax jnz loaderr cmp [driver_ver], 1 jnz loaderr
</asm>
Starting logging:
<asm>
mov [ioctl_code], 1 and [inp_size], 0 and [outp_size], 0 mov eax, 68 mov ebx, 17 mov ecx, ioctl int 0x40 test eax, eax jnz loaderr
</asm>
The driver has been loaded and is logging now. Let's tell the user about it:
<asm>
push str0 call [con_printf] add esp, 4
</asm>
Now we enter the main cycle. Once a second the program checks if there are any events. Generally, the driver can wake up the application stream itself when a filesystem event occurs, but filesystem function calls can occur very often and it's hardly useful to wake up the application for each of them. The processing of data is a long, but quite easy process (the arguments for different sysfunctions differ from each other, so we need to process every case).
<asm> mainloop:
mov eax, 5 mov ebx, 100 int 0x40 mov [ioctl_code], 2 and [inp_size], 0 mov [outp_size], 1+16*1024 mov [output], logbuf mov eax, 68 mov ebx, 17 mov ecx, ioctl int 0x40 push eax mov ecx, dword [logbuf] mov esi, logbuf+5
message:
test ecx, ecx jz done movzx eax, byte [esi] push eax push str1 call [con_printf] add esp, 8 lodsb cmp al, 6 jz fn06 cmp al, 32 jz fn32 cmp al, 33 jz fn33 cmp al, 58 jz fn58 sub ecx, 1+4*5 ; size of log data for fn70 (excluding filename) lodsd cmp eax, 10 jae fn70unk jmp [fn70+eax*4]
fn70unk:
push dword [esi+12] push dword [esi+8] push dword [esi+4] push dword [esi] push eax push str2 call [con_printf] add esp, 6*4 add esi, 16 jmp print_name
fn70readfile:
push dword [esi+12] push dword [esi+8] push dword [esi] push str3 call [con_printf] add esp, 4*4 add esi, 16 jmp print_name
fn70readfolder:
mov eax, str41 test byte [esi+4], 1 jz @f mov eax, str42
@@:
push dword [esi+12] push dword [esi+8] push dword [esi] push eax push str4 call [con_printf] add esp, 5*4 add esi, 16 jmp print_name
fn70create:
push dword [esi+12] push dword [esi+8] push str5 call [con_printf] add esp, 3*4 add esi, 16 jmp print_name
fn70write:
push dword [esi+12] push dword [esi+8] push dword [esi+4] push str6 call [con_printf] add esp, 4*4 add esi, 16 jmp print_name
fn70setsize:
push dword [esi] push str7 call [con_printf] add esp, 4*2 add esi, 16 jmp print_name
fn70getattr:
push dword [esi+12] push str8 call [con_printf] add esp, 4*2 add esi, 16 jmp print_name
fn70setattr:
push dword [esi+12] push str9 call [con_printf] add esp, 4*2 add esi, 16 jmp print_name
fn70execute:
push str10 call [con_printf] add esp, 4 lodsd test al, 1 jz @f push str10_1 call [con_printf] add esp, 4
@@:
lodsd test eax, eax jz @f push eax push str10_2 call [con_printf] add esp, 8
@@:
add esi, 8 jmp print_name
fn70delete:
push str11 call [con_printf] add esp, 4 add esi, 16 jmp print_name
fn70createfolder:
push str12 call [con_printf] add esp, 4 add esi, 16 jmp print_name
fn58:
sub ecx, 1+4*4 ; size of log data for fn58 (excluding filename) lodsd test eax, eax jz fn58read cmp eax, 1 jz fn58write cmp eax, 8 jz fn58lba cmp eax, 15 jz fn58fsinfo
fn58unk:
push dword [esi+8] push dword [esi+4] push dword [esi] push eax push str13 call [con_printf] add esp, 5*4 add esi, 12 jmp print_name
fn58read:
push dword [esi+8] mov eax, [esi+4] shl eax, 9 push eax mov eax, [esi] shl eax, 9 push eax push str3 call [con_printf] add esp, 4*4 add esi, 12 jmp print_name
fn58write:
push dword [esi+8] push dword [esi+4] push str5 call [con_printf] add esp, 3*4 add esi, 12 jmp print_name
fn58lba:
push dword [esi+8] push dword [esi] push str14 call [con_printf] add esp, 3*4 add esi, 12 jmp print_name
fn58fsinfo:
push str15 call [con_printf] add esp, 4 add esi, 12 jmp print_name
fn33:
sub ecx, 1+2*4 ; size of log data for fn33 lodsd push eax lodsd push eax push str5 call [con_printf] add esp, 3*4 push aRamdisk call [con_printf] add esp, 4 jmp print_name
fn32:
dec ecx ; only filename is logged push str11 call [con_printf] push aRamdisk call [con_printf] add esp, 4+4 jmp print_name
fn06:
sub ecx, 1+3*4 ; size of log data for fn06 push dword [esi+8] mov eax, [esi+4] test eax, eax jnz @f inc eax
@@:
shl eax, 9 push eax lodsd test eax, eax jnz @f inc eax
@@:
dec eax shl eax, 9 push eax push str3 call [con_printf] add esp, 4*4 push aRamdisk call [con_printf] add esp, 4 add esi, 8
print_name:
push esi push str_final call [con_printf] add esp, 8
@@:
lodsb test al, al jnz @b jmp message
done:
cmp byte [logbuf+4], 0 jz @f push str_skipped call [con_printf]
@@:
- we has output all driver data, now check console (did user press Esc?)
call [con_kbhit] test al, al jz mainloop call [con_getch2] cmp al, 27 jnz mainloop
</asm>
Tell the driver to stop logging on pressing Esc.
<asm>
mov [ioctl_code], 3 and [inp_size], 0 and [outp_size], 0 mov eax, 68 mov ebx, 17 mov ecx, ioctl int 0x40
</asm>
Close console:
<asm>
push 1 call [con_exit]
</asm>
And finish the program:
<asm> exit:
or eax, -1 int 0x40
</asm>
The program data:
<asm> dll_name db '/rd/1/console.obj',0 caption db 'FileMon',0 drivername db 'fmondrv',0 aCantLoadDriver db "Can't load driver",13,10,0
str0 db 'Monitoring file system calls... Press Esc to exit',10,0
str1 db 'Fn%2d: ',0 str2 db 'unknown subfunction %d, parameters: 0x%X, 0x%X, 0x%X, 0x%X, name ',0 str3 db 'read file, starting from 0x%X, %d bytes, to 0x%X; name ',0 str4 db 'read folder (%s version), starting from %d, %d blocks, to 0x%X; name ',0 str41 db 'ANSI',0 str42 db 'UNICODE',0 str5 db 'create/rewrite file, %d bytes from 0x%X; name ',0 str6 db 'write file, starting from 0x%X, %d bytes, from 0x%X; name ',0 str7 db 'set file size to %d bytes; name ',0 str8 db 'get file attributes to 0x%X; name ',0 str9 db 'set file attributes from 0x%X; name ',0 str10 db 'execute ',0 str10_1 db '(in debug mode) ',0 str10_2 db '(with parameters 0x%X) ',0 str11 db 'delete ',0 str12 db 'create folder ',0 str13 db 'unknown subfunction %d, parameters: 0x%X, 0x%X, 0x%X, name ',0 str14 db 'LBA read sector 0x%X to 0x%X from device ',0 str15 db '(obsolete!) query fs information of ',0 aRamdisk db '/rd/1/',0 str_final db '%s',10,0 str_skipped db '[Some information skipped]',10,0
align 4 label fn70 dword
dd fn70readfile dd fn70readfolder dd fn70create dd fn70write dd fn70setsize dd fn70getattr dd fn70setattr dd fn70execute dd fn70delete dd fn70createfolder
align 4 myimport: dll_start dd aStart dll_ver dd aVersion con_init dd aConInit con_printf dd aConPrintf con_exit dd aConExit con_kbhit dd aConKbhit con_getch2 dd aConGetch2
dd 0
aStart db 'START',0 aVersion db 'version',0 aConInit db 'con_init',0 aConPrintf db 'con_printf',0 aConExit db 'con_exit',0 aConKbhit db 'con_kbhit',0 aConGetch2 db 'con_getch2',0
i_end:
align 4 ioctl: hDriver dd ? ioctl_code dd ? input dd ? inp_size dd ? output dd ? outp_size dd ?
driver_ver dd ? logbuf rb 16*1024+5
align 4 rb 2048 ; stack mem: </asm>