delphi开发驱动程序

用Delphi编写驱动程序

实现原理

Delphi能不能开发Windows的驱动程序(这里的驱动程序当然不是指VxD了^_^)一直是广大Delphi fans关注的问题。姑且先不说能或者不能,我们先来看看用Delphi开发驱动程序需要解决哪些技术上问题。

Delphi的链接器是无法生成Windows内核模式程序的,因此用delphi无法直接生成驱动程序。M$的链接器是可以生成Windows内核模式程序的,那么是否可以用Delphi生成目标文件,然后用M$链接呢?要这么做必须要解决以下的问题:

Delphi生成的目标文件是OMF格式的,而M$ link虽然声称支持OMF格式的目标文件,但基本无用。最好能将OMF格式转换成COFF格式,EliCZ大侠的OMF2D正好可以解决这个问题。解决了目标格式的问题,一切都OK了吗?远没这么简单。继续之前,让我们先来看一下著名的DDDK吧。

DDDK(Delphi Driver Development Kit)是The Hacker Defender Project team发布的一个用Delphi开发Windows驱动程序的工具包,目前最新版是0.0.4版。DDDK是将常用的驱动API用Delphi做了层包装放在DDDK单元中,就像下面这样:

代码:

unit DDDK;

interface

const

NtKernel='ntoskrnl.exe'; ??

procedure IoCompleteRequest(Irp:PIrp;PriorityBoost:Integer); stdcall; ……

implementation

procedure krnlIoCompleteRequest(Irp:PIrp;PriorityBoost:Integer); stdcall; external NtKernel name 'IoCompleteRequest';

procedure IoCompleteRequest(Irp:PIrp;PriorityBoost:Integer); stdcall; begin

krnlIoCompleteRequest(Irp,PriorityBoost); end; ??

然后在每次链接驱动文件之前,用omf2d对dddk.obj中需要引入的驱动API做以下的处理:

omf2d inc\\DDDK.obj /U- /CEIoCompleteRequest=_IoCompleteRequest@8 2>nul将DDDK.obj中的IoCompleteRequest改成_IoCompleteRequest@8,为什么要这样做呢?那是因为诸如ntoskrnl.lib之类的导入库都是coff格式的,coff格式就是这样命名的。完成这步以后就可以调用m$ link将目标文件链接成驱动文件了。

这样做虽然可以生成正确的驱动文件,但缺点也是明显的。将驱动API用delphi包装,这些用delphi包装的函数不管是否使用都会被链接到最终生成的驱动文件中,这样会增加驱动文件的尺寸,而且通过delphi的封装函数再去调用驱动API效率也会受影响,还有就是每次链接前都要用omf2d inc\\DDDK.obj /U- /CEIoCompleteRequest=_IoCompleteRequest@8去转换delphi的目标文件,既麻烦又容易出错。有没有更好的办法呢?

omf2d的工作就是将delphi的命名方法转换成coff的_xxxxxxx@xx格式,默认omf2d会去掉前导下划线和@xx后缀,可以用/U_*开关让omf2d不删除前导下划线,如果我们再有没有@xx后缀的导入库,

那问题就简单多了。但m$并没有提供没有@xx后缀的导入库,那就让我们自己做一个吧^_^,其实很简单,比如我们要生成hal.dll的导入库,只需要编辑一个如下内容的hal.def文件:

代码:

LIBRARY HAL.DLL

EXPORTS

ExAcquireFastMutex ExReleaseFastMutex ExTryToAcquireFastMutex HalAcquireDisplayOwnership HalAdjustResourceList HalAllProcessorsStarted ……

然后用LINK /LIB /MACHINE:IX86 /DEF:hal.def /OUT:hal.lib命令就可以生成我们需要的没有@xx后缀的导入库文件了。有了这个文件,事情就好办多了。下面就让我们开始用delphi来开发一个简单的驱动程序beeper吧。

这个驱动程序是从Four-F的KmdKit里的beeper转换过来的,程序的目标就是通过访问端口让PC的扬声器发声,程序通过三种方法让扬声器发声,一种是直接访问端口,一种是调用hal.dll的READ_PORT_UCHAR和WRITE_PORT_UCHAR访问端口,第三种方法则是调用hal.dll的HalMakeBeep函数。

代码:

unit beeper;

interface

uses windows, DDDK, hal;

function _DriverEntry(DriverObject:PDriverObject;RegistryPath:PUnicodeString):NTSTATUS; stdcall;

implementation

const

TIMER_FREQUENCY:DWORD = 1193167; {1,193,167 Hz}

OCTAVE:DWORD = 2; {octave multiplier}

PITCH_C:DWORD = 523; {C - 523,25 Hz} PITCH_Cs:DWORD = 554; {C# - 554,37 Hz} PITCH_D:DWORD = 587; {D - 587,33 Hz} PITCH_Ds:DWORD = 622; {D# - 622,25 Hz} PITCH_E:DWORD = 659; {E - 659,25 Hz} PITCH_F:DWORD = 698; {F - 698,46 Hz} PITCH_Fs:DWORD = 740; {F# - 739,99 Hz} PITCH_G:DWORD = 784; {G - 783,99 Hz} PITCH_Gs:DWORD = 831; {G# - 830,61 Hz} PITCH_A:DWORD = 880; {A - 880,00 Hz} PITCH_As:DWORD = 988; {B - 987,77 Hz} PITCH_H:DWORD = 1047; {H - 1046,50 Hz} { We are going to play c-major chord }

DELAY:DWORD = $18000000; {for my ~800mHz box}

TONE_1:DWORD = 1141; TONE_2:DWORD = 905;

TONE_3:DWORD = 1568; {for HalMakeBeep}

STATUS_DEVICE_CONFIGURATION_ERROR:DWORD = $00C0000182;

procedure MakeBeep1(dwPitch: DWORD); stdcall; assembler; asm cli

mov al, 10110110b out 43h, al

mov eax, dwPitch out 42h, al mov al, ah out 42h, al

{Turn speaker ON} in al, 61h or al, 11b out 61h, al sti

push eax

mov eax, DELAY @@1:

dec eax jnz @@1 pop eax cli

{Turn speaker OFF} in al, 61h

and al, 11111100b out 61h, al

sti end;

procedure MakeBeep2(dwPitch: DWORD); stdcall; var

dwPort, i: DWORD; begin asm cli; end;

WRITE_PORT_UCHAR(PUCHAR($43), $b6);

WRITE_PORT_UCHAR(PUCHAR($42), dwPitch and $FF);

WRITE_PORT_UCHAR(PUCHAR($42), ((dwPitch shr 8) and $FF)); dwPort := READ_PORT_UCHAR(PUCHAR($61)); dwPort := dwPort or 3;

WRITE_PORT_UCHAR(PUCHAR($61), dwPort); asm sti end;

for i := 1 to DELAY do begin end; asm cli end;

{ Turn speaker OFF }

dwPort := READ_PORT_UCHAR(PUCHAR($61)); dwPort := dwPort and $FC;

WRITE_PORT_UCHAR(PUCHAR($61), dwPort); asm sti end; end;

function _DriverEntry(DriverObject:PDriverObject;RegistryPath:PUnicodeString):NTSTATUS; stdcall; var

i: integer; begin

MakeBeep1(TONE_1); MakeBeep2(TONE_2); HalMakeBeep(TONE_3);

for i := 1 to DELAY do begin end;

HalMakeBeep(0);

Result := STATUS_DEVICE_CONFIGURATION_ERROR; end; end. 代码: unit hal;

interface uses

Windows;

const

NtHal = 'hal.dll';

function HalMakeBeep(Frequency: ULONG):BOOLEAN; stdcall; function READ_PORT_UCHAR(Port:PUCHAR):UCHAR; stdcall;

procedure WRITE_PORT_UCHAR(Port: PUCHAR; Value: UCHAR); stdcall;

implementation

function HalMakeBeep(Frequency: ULONG):BOOLEAN; stdcall; external NtHal name '_HalMakeBeep'; function READ_PORT_UCHAR(Port:PUCHAR):UCHAR; stdcall; external NtHal name '_READ_PORT_UCHAR';

procedure WRITE_PORT_UCHAR(Port: PUCHAR; Value: UCHAR); stdcall; external NtHal name '_WRITE_PORT_UCHAR'; end. 四

前面提到,如果你访问已经被交换出去的内存时IRQL >= DISPATCH_LEVEL会导致系统瘫痪!但是事情并不绝对,也许它当时不死机,过一会才死呢!啥时候死?就是当你的系统将内存交换了出去,并且你访问它的时候!

千万不要太钟爱不分页内存,太浪费资源了!它总要占用物理内存!分配的堆使用完后记得一定要释放,你在系统池里分配了内存,无论你的驱动程序发生了什么事情,这些内存不会被回收,除非ExFreePool 被调用。如果你不用ExFreePool显式地释放内存,即使你的驱动程序卸载了,这些内存还驻留在那里。所以呢,你就乖乖地显式地释放内存吧!

系统内存池分配的内存不会被自动清零,最后的使用者可能会留下垃圾。所以呢,使用之前,最好统统置零。

你可以很容易地定义你需要的内存类型了,就两种:分页,不分页。如果你的代码要访问IRQL >= DISPATCH_LEVEL,不用说你也知道,你必须使用不分页类型。代码本身,和涉及的数据都要在不分页内存里。在缺省情况下,驱动程序以不分页内存加载,除非是INIT节区或者名称以\开始的节区。假如你不做任何动作去改变驱动程序的内存属性(例如:别去调用 MmPageEntireDriver使驱动程序的映像分页),你就不用关心驱动程序了,它总是在内存里。

先前的文章中我们讨论了常用的驱动函数(DriverEntry, DriverUnload, DispatchXxx)被调用时所处的IRQL等级。 DDK给了我们关于每一个函数被调用时的IRQL等级的相关信息。例如:在后面的文章中我们会使用IoInitializeTimer函数,该函数的描述这样说的:该函数执行时,时钟事件发生时的等级IRQL = DISPATCH_LEVEL 。这就意味着:这个函数访问的所有内存都必须是不分页的。

如果你不能确定到底是哪个IRQL,你写程序时候可以这样调用KeGetCurrentIrql:

代码:If KeGetCurrentIrql < DISPATCH_LEVEL then begin

{使用任意内存} End else begin

{只能使用不分页内存}

联系客服:779662525#qq.com(#替换为@)