您的位置:首页 > 产品设计 > 产品经理

Driver Development Part 1: Introduction to Drivers

2013-04-09 23:04 609 查看
//来自codeproject的文章,共有六篇,感觉写的挺好的。因为不想经常翻墙,所以先收藏。
//对于初学者是不错的文章,虽然是英文但是咬咬牙还是可以看懂的,建议不要百度、google翻译了
//MSDN、WDK的帮助文档都是英文 哈哈!
Driver Development Part 1: Introduction to Drivers
By Toby Opferman,
5 Feb 2005

· Download source files
- 10.4 Kb

Introduction
This tutorial will attempt to describe how to write a simple device driverfor Windows NT. There are various resources and tutorials on the internet forwriting device drivers, however, they are somewhat scarce as compared
towriting a “hello world” GUI program for Windows. This makes the search forinformation on starting to write device drivers a bit harder. You may thinkthat if there’s already one tutorial, why do you need more? The answer is thatmore information is always
better especially when you are first beginning tounderstand a concept. It is always good to see information from differentperspectives. People write differently and describe certain pieces ofinformation in a different light depending on how familiar they are
with acertain aspect or how they think it should be explained. This being the case, Iwould recommend anyone who wants to write device drivers not to stop here orsomewhere else. Always find a variety of samples and code snippets and researchthe differences.
Sometimes there are bugs and things omitted. Sometimes thereare things that are being done that aren’t necessary, and sometimes there’sinformation incorrect or just incomplete.
This tutorial will describe how to create a simple device driver,dynamically load and unload it, and finally talk to it from user mode.
Creating a Simple Device Driver
What is a subsystem?
I need to define a starting ground before we begin to explain how to writea device driver. The starting point for this article will be the compiler. Thecompiler and linker generate a binary in a format that the Operating
Systemunderstands. In Windows, this format is “PE” for “Portable Executable” format.In this format, there is an idea called a
subsystem. A subsystem, alongwith other options specified in the PE header information, describes how toload an executable which also includes the entry point into the binary.
Many people use the VC++ IDE to simply create a project with some defaultpre-set options for the compiler’s (and linker) command line. This is why a lotof people may not be familiar with this concept even though they
are mostlikely already using it if they have ever written Windows applications. Haveyou ever written a console application? Have you ever written a GUI applicationfor Windows? These are different subsystems in Windows. Both of these willgenerate a PE binary
with the appropriate subsystem information. This is alsowhy a console application uses “main” where a WINDOWS application uses “WinMain”.
When you choose these projects, VC++ simply creates aproject with /SUBSYSTEM:CONSOLE or /SUBSYSTEM:WINDOWS. If you accidentallychoose the wrong project, you can simply change this in the linker options menurather than needing to create a new project.
There’s a point to all of this? A driver is simply linked using adifferent subsystem called “NATIVE”.

MSDN Subsystem compiler options.
The Driver’s “main”
After the compiler is setup with the appropriate options, it’s probablygood to start thinking about the entry point to a driver. The first sectionlied a little bit about the subsystem. “NATIVE” can also be used to
runuser-mode applications which define an entry point called “NtProcessStartup”. This is the “default” type of executable that is madewhen specifying “NATIVE” in the same way “WinMain”
and “main” are found when the linker is creating an application. You can overridethe default entry point with your own, simply by using the“-entry:<functionname>” linker option. If we know
we want this to be adriver, we simply need to write an entry point whose parameter list and returntype matches that of a driver. The system will then load the driver when weinstall it and tell the system that it is a driver.
The name we use can be anything. We can call it
BufferFly() if we want. The most common practice used by driverdevelopers and Microsoft is using the name “DriverEntry” as
its initial entry point. This means we add“-entry:DriverEntry” to the linker’s command line options. If you are using theDDK, this is done for you when you specify “DRIVER” as the type of executableto build. The DDK contains an environment that has pre-set
options in thecommon make file directory which makes it simpler to create anapplication as it specifies the default options. The actual driver developercan then override these settings in the
make file or simply use them asa connivance. This is essentially how “DriverEntry” became the somewhat “official” name for driver entrypoints.
Remember, DLLs actually are also compiled specifying “WINDOWS” as thesubsystem, but they also have an additional switch called /DLL. There is aswitch which can also be used for drivers: /DRIVER:WDM (which also sets
NATIVEbehind the scenes) as well as a /DRIVER:UP which means this driver cannot beloaded on a multi-processor system.
The linker builds the final binary, and based on what the options are inthe PE header and how the binary is attempting to be loaded (run as an EXEthrough the loader, loaded by
LoadLibrary, or attempting to be loaded as a driver) will define how the loadingsystem behaves. The loading system attempts to perform some level ofverification, that the image being loaded
is indeed supposed to be loaded inthis manner, for example. There is even, in some cases, startup code added tothe binary that executes before your entry point is reached (WinMainCRTStartup
calling WinMain, for example, to initialize the CRT). Your job is to simply write theapplication based on how you want it to be loaded and then set the correctoptions in the linker so it knows
how to properly create the binary. There arevarious resources on the details of the PE format which you should be able tofind if you are interested in further investigation into this area.
The options we will set for the linker will end up being the following:
Collapse |
Copy Code

/SUBSYSTEM:NATIVE /DRIVER:WDM –entry:DriverEntry

Before creating the “DriverEntry”
There are some things we need to go over before we simply sit down andwrite the “DriverEntry”. I know that a lot of peoplesimply want to jump right
into writing the driver and seeing it work. This isgenerally the case in most programming scenarios as you usually just take thecode, change it around, compile it, and test it out. If you remember back towhen you were first learning Windows development, it
was probably the same way.Your application probably didn’t work right away, probably crashed, or justdisappeared. This was a lot of fun and you probably learned a lot, but you knowthat with a driver, the adventure is a little different. Not knowing what to
docan end up in blue screening the system, and if your driver is loaded on bootand executes that code, you now have a problem. Hopefully, you can boot in safemode or restore to a previous hardware configuration. That being the case, wehave a few things to
go over before you write the driver in order to helpeducate you on what you are doing before you actually do it.
The first rule of thumb is do not just take a driver and compile it withsome of your changes. If you do not understand how the driver is working or howto program correctly in the environment, you are likely to cause
problems.Drivers can corrupt the integrity of the whole system, they can have bugs thatdon’t always occur but in some rare circumstances. Application programs canhave the same type of bugs in behavior but not in root cause. As an example,there are times when
you cannot access memory that is pagable. If you know howVirtual Memory works, you know that the Operating System will remove pages frommemory to pull in pages that are needed, and this is how more applications canrun than would have been physically possible
given the memory limitations ofthe machine. There are places, however, when pages cannot be read into memoryfrom disk. At these times, those “drivers” who work with memory can only accessmemory that cannot be paged out.
Where am I going with this? Well, if you allow a driver which runs underthese constraints to access memory that is “pagable”, it may not crash as theOperating System usually tries to keep all pages in memory as long
as possible.If you close an application that was running, it may still be in memory, forexample! This is why a bug like this may go undetected (unless you try doingthings like driver verifier) and eventually may trap. When it does, if you donot understand
the basic concepts like this, you would be lost as to what theproblem is and how to fix it.
There are a lot of concepts behind everything that will be described inthis document. On IRQL alone, there is a twenty page document you can find onMSDN. There’s an equally large document on IRP. I will not attempt
to duplicatethis information nor point out every single little detail. What I will attemptto do is give a basic summary and point you in the direction of where to findmore information. It’s important to at least know that these concepts exist andunderstand
some basic idea behind them, before writing the driver.
What is IRQL?
The IRQL is known as the “Interrupt
ReQuest Level”.The processor will be executing code in a thread at a particular IRQL. The IRQLof the processor essentially helps determine how that thread is allowed to beinterrupted. The thread can only be
interrupted by code which needs to run at ahigher IRQL on the same processor. Interrupts requiring the same IRQL or lowerare masked off so only interrupts requiring a higher IRQL are available forprocessing. In a multi-processor system, each processor operates
independentlyat its own IRQL.
There are four IRQL levels which you generally will be dealing with, whichare “Passive”, “APC”, “Dispatch” and “DIRQL”. Kernel APIs documented in MSDNgenerally have a note which specifies the IRQL level at which you
need to berunning in order to use the API. The higher the IRQL you go, the less APIs thatare available for use. The documentation on MSDN defines what IRQL theprocessor will be running at when the particular entry point of the driver iscalled. “DriverEntry”,
for example, will be calledat PASSIVE_LEVEL.
PASSIVE_LEVEL
This is the lowest IRQL. No interrupts are masked off and this is thelevel in which a thread executing in user mode is running. Pagable memory isaccessible.
APC_LEVEL
In a processor running at this level, only APC level interrupts aremasked. This is the level in which

Asynchronous Procedure Calls occur. Pagable memory is stillaccessible. When an APC occurs, the processor is raised to APC level. This, inturn, also disables other APCs from occurring. A driver can manually raise itsIRQL
to APC (or any other level) in order to perform some synchronization withAPCs, for example, since APCs can’t be invoked if you are already at APC level.There are some APIs which can’t be called at APC level due to the fact thatAPCs are disabled, which, in
turn, may disable some I/O Completion APCs.
DISPATCH_LEVEL
The processor running at this level has DPC level interrupts and lowermasked off. Pagable memory
cannot be accessed, so all memory beingaccessed must be non-paged. If you are running at Dispatch Level, the APIs thatyou can use greatly decrease since you can only deal with non-paged memory.
DIRQL
(Device IRQL)
Generally, higher level drivers do not deal with IRQLs at this level, butall interrupts at this level or less are masked off and do not occur. This isactually a range of IRQLs, and this is a method to determine which
devices havepriority over other devices.
In this driver, we will basically only be working at
PASSIVE_LEVEL, so we won’t have to worry about the gotchas. However,it is necessary for you to be aware of what IRQL is, if you intend to continuewriting device drivers.
For more information on IRQLs and thread scheduling, refer to thefollowing

documentation, and another good source of information is

here.
What is an IRP?
The “IRP” is called the “I/O Request Packet”, and it is passeddown from driver to driver in the driver stack. This is a data structure thatallows drivers to
communicate with each other and to request work to be done bythe driver. The I/O manager or another driver may create an IRP and pass itdown to your driver. The IRP includes information about the operation that isbeing requested.
A description of the IRP data structure can be found

here.
The description and usage of an IRP can go from simple to complex veryeasily, so we will only be describing, in general, what an IRP will mean toyou. There is an article on MSDN which describes in a lot more detail
(abouttwenty pages) of what exactly an IRP is and how to handle them. That articlecan be found

here.
The IRP will also contain a list of “sub-requests” also known as the “IRPStack Location”. Each driver in the device stack will generally have its own“sub request” of how to interpret the IRP. This data structure is
the “IO_STACK_LOCATION”and is described on MSDN.
To create an analogy of the IRP and
IO_STACK_LOCATION, perhaps you have three people who do different jobssuch as carpentry, plumbing and welding. If they were going to build a house,they could have a common overall design and perhaps
a common set of tools liketheir tool box. This includes things like power drills, etc. All of thesecommon tools and overall design of building a house would be the IRP. Each ofthem has an individual piece they need to work on to make this happen, forexample,
the plumber needs the plans on where to put the pipe, how much pipe hehas, etc. These could be interpreted as the
IO_STACK_LOCATION as his specific job is to do the piping. The carpentercould be building the framework for the house and the details of that would bein his
IO_STACK_LOCATION. So, while theentire IRP is a request to build a house, each person in the stack of peoplehas their own job as defined by the
IO_STACK_LOCATION to make this happen. Once everyone has completed theirjob, they then complete the IRP.
The device driver we will be building will not be that complex and willbasically be the only driver in the stack.
Things to Avoid
There are a lot of pitfalls that you will need to avoid but they aremostly unrelated to our simple driver. To be more informed, however, here is alist of items called “things
to avoid” when it comes to driver development.
Create the DriverEntry routine
There is so much to explain, however, I think it’s time we simply startedto develop the driver and explain as we go. It is hard to digest theory or evenhow code is supposed to work, without actually doing anything.
You need somehands on experience so you can bring these ideas out of space and into reality.
The prototype for the
DriverEntry is the following.
Collapse |
Copy Code

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRINGpRegistryPath);

The DRIVER_OBJECT is a data structure used torepresent this driver. The
DriverEntry routine will use it to populate it with other entry points to the driverfor handling specific I/O requests. This object also has a pointer to a
DEVICE_OBJECT which is a data structure which represents a particulardevice. A single driver may actually advertise itself as handling multipledevices, and as such, the
DRIVER_OBJECT maintains a linked list pointer to all the devices this particular driverservices request for. We will simply be creating one device.
The “Registry Path” is a string which points to the location in theregistry where the information for the driver was stored. The driver can usethis location to store driver specific information.
The next part is to actually put things in the
DriverEntry routine. The first thing we will do is create thedevice. You may be wondering how we are going to create a device and what typeof device we should create. This is generally because
a driver is usuallyassociated with hardware but this is not the case. There are a variety ofdifferent types of drivers which operate at different levels, not all driverswork or interface directly with hardware. Generally, you maintain a stack ofdrivers each
with a specific job to do. The highest level driver is the onethat communicates with user mode, and the lowest level drivers generally justtalk to other drivers and hardware. There are network drivers, display drivers,file system drivers, etc., and each has
their own stack of drivers. Each placein the stack breaks up a request into a more generic or simpler request for thelower level driver to service. The highest level drivers are the ones whichcommunicate themselves to user mode, and unless they are a special
device witha particular framework (like display drivers), they can behave generally thesame as other drivers just as they implement different types of operations.
As an example, take the hard disk drive. The driver which communicates touser mode does not talk directly to hardware. The high level driver simplymanages the file system itself and where to put things. It then communicateswhere
it wants to read or write from the disk to the lower level driver whichmay or may not talk directly to hardware. There may be another layer which thencommunicates that request to the actual hardware driver which then physicallyreads or writes a particular
sector off a disk and then returns it to thehigher level. The highest level may interpret them as file data, but the lowestlevel driver may simply be stupid and only manage requests as far as when toread a sector based off where the read/write head is located
on the disk. Itcould then determine what sector read requests to service, however, it has noidea what the data is and does not interpret it.
Let’s take a look at the first part of our “DriverEntry”.
Collapse |
Copy Code

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{

NTSTATUS NtStatus =STATUS_SUCCESS;

UINT uiIndex = 0;

PDEVICE_OBJECT pDeviceObject =NULL;

UNICODE_STRING usDriverName,usDosDeviceName;

DbgPrint("DriverEntryCalled \r\n");

RtlInitUnicodeString(&usDriverName, L"\\Device\\Example");
RtlInitUnicodeString(&usDosDeviceName,L"\\DosDevices\\Example");

NtStatus =IoCreateDevice(pDriverObject, 0,
&usDriverName,

FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,

FALSE, &pDeviceObject);

The first thing you will notice is the
DbgPrint function. This works just like “printf” and it prints messages out to the debugger or debugoutput window. You can
get a tool called “DBGVIEW” from
www.sysinternals.com andall of the information in those messages will be displayed.
You will then notice that we use a function called “RtlInitUnicodeString” which basically initializes a
UNICODE_STRING data structure. This data structure contains basicallythree entries. The first is the size of the current Unicode string, the secondis the maximum size that the Unicode string
can be, and the third is a pointerto the Unicode string. This is used to describe a Unicode string and usedcommonly in drivers. The one thing to remember with
UNICODE_STRING is that they are
not required to be NULL terminated since there is a size parameter in thestructure! This causes problems for people new to driver development as theyassume a
UNICODE_STRING is
NULL terminated, and they blue-screen the driver. MostUnicode strings passing into your driver will not be
NULL terminated, so this is something you need to be awareof.
Devices have names just like anything else. They are generally named
\Device\<somename>and this is the string we were creating to pass into
IoCreateDevice. The second string, “\DosDevices\Example”, we will getinto later as it’s not used in the driver yet. To the
IoCreateDevice, we pass in the driver object, a pointer to the Unicodestring we want to call the driver, and we pass in a type of driver “UNKNOWN”
as it’s not associated with any particular type ofdevice, and we also pass in a pointer to receive the newly created deviceobject. The parameters are explained in more detail at “IoCreateDevice”.
The second parameter we passed 0, and it says to specify the number ofbytes to create for the device extension. This is basically a data structurethat the driver writer can define which is unique to that device. This
is howyou can extend the information being passed into a device and create devicecontexts, etc. in which to store instance data. We will not be using this forthis example.
Now that we have successfully created our
\Device\Example devicedriver, we need to setup the Driver Object to call into our driver when certainrequests are made. These requests are called IRP Major requests. There are alsoMinor requests which are sub-requests of these and can be found in the
stacklocation of the IRP.
The following code populates certain requests:
Collapse |
Copy Code

for(uiIndex = 0; uiIndex< IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
pDriverObject->MajorFunction[uiIndex] = Example_UnSupportedFunction;

pDriverObject->MajorFunction[IRP_MJ_CLOSE] = Example_Close;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = Example_Create;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Example_IoControl;
pDriverObject->MajorFunction[IRP_MJ_READ] = Example_Read;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = USE_WRITE_FUNCTION;

We populate the Create,
Close,
IoControl, Read and
Write. What do these refer to? When communicating with theuser-mode application, certain APIs call directly to the driver and pass inparameters!
· CreateFile ->
IRP_MJ_CREATE
· CloseHandle ->
IRP_MJ_CLEANUP & IRP_MJ_CLOSE
· WriteFile ->
IRP_MJ_WRITE
· ReadFile->
IRP_MJ_READ
· DeviceIoControl ->
IRP_MJ_DEVICE_CONTROL
To explain, one difference is
IRP_MJ_CLOSE is not called in the context of the process which created the handle. Ifyou need to perform process related clean up, then you need to handle
IRP_MJ_CLEANUP as well.
So as you can see, when a user mode application uses these functions, itcalls into your driver. You may be wondering why the user mode API says “file”when it doesn’t really mean “file”. That is true, these APIs can
talk to anydevice which exposes itself to user mode, they are not only for accessingfiles. In the last piece of this article, we will be writing a user modeapplication to talk to our driver and it will simply do
CreateFile,
WriteFile, CloseHandle. That’s how simple it is.
USE_WRITE_FUNCTION is a constant I will explain later.
The next piece of code is pretty simple, it’s the driver unload function.
Collapse |
Copy Code

pDriverObject->DriverUnload = Example_Unload;

You can technically omit this function but if you want to unload yourdriver dynamically, then it must be specified. If you do not specify thisfunction once your driver is loaded, the system will not allow it to beunloaded.
The code after this is actually using the
DEVICE_OBJECT, not the
DRIVER_OBJECT. These two data structures may get a little confusing since they bothstart with “D” and end with “_OBJECT”, so it’s easy to confuse which one we’reusing.
Collapse |
Copy Code

pDeviceObject->Flags |=IO_TYPE;

pDeviceObject->Flags&= (~DO_DEVICE_INITIALIZING);

We are simply setting the flags. “IO_TYPE” is actually a constant which defines the type of I/O wewant to do (I defined it in
example.h). I will explain this in thesection on handling user-mode write requests.
The “DO_DEVICE_INITIALIZING” tells the I/OManager that the device is being initialized and not to send any I/O requeststo the driver. For devices
created in the context of the “DriverEntry”, this is not needed since the I/O Manager will clearthis flag once the “DriverEntry”
is done. However, if youcreate a device in any function outside of the DriverEntry, you need to manually clear this flag for any device youcreate with
IoCreateDevice. This flag is actually set bythe
IoCreateDevice function. We cleared it herejust for fun even though we weren’t required to.
The last piece of our driver is using both of the Unicode strings wedefined above. “\Device\Example” and “\DosDevices\Example”.
Collapse |
Copy Code

IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);

“IoCreateSymbolicLink” does just that, itcreates a “Symbolic Link” in the object manager. To view the object manager,you may download my tool “QuickView”,
or go to www.sysinternals.com and download “WINOBJ”. A Symbolic Linksimply maps a “DOS Device Name” to an “NT Device Name”. In this example,“Example” is our DOS Device
Name and “\Device\Example” is our NT Device Name.
To put this into perspective, different vendors have different drivers andeach driver is required to have its own name. You cannot have two drivers withthe same NT Device name. Say, you have a memory stick which can
display itselfto the system as a new drive letter which is any available drive letter such asE:. If you remove this memory stick and say you map a network drive to E:.Application can talk to E: the same way, they do not care if E: is a CD ROM,Floppy Disk,
memory stick or network drive. How is this possible? Well, thedriver needs to be able to interpret the requests and either handle them withinthemselves such as the case of a network redirector or pass them down to theappropriate hardware driver. This is done
through symbolic links. E: is asymbolic link. The network mapped drive may map E: to
\Device\NetworkRedirectorand the memory stick may map E: to \Device\FujiMemoryStick, for example.
This is how applications can be written using a commonly defined namewhich can be abstracted to point to any device driver which would be able tohandle requests. There are no rules here, we could actually map
\Device\Exampleto E:. We can do whatever we wish to do, but in the end, however, theapplication attempts to use the device as how the device driver needs torespond and act. This means supporting IOCTLs commonly used by those devices asapplications
will try to use them. COM1, COM2, etc. are all examples of this.COM1 is a DOS name which is mapped to an NT Device name of a driver whichhandles serial requests. This doesn’t even need to be a real physical serialport!
So we have defined “Example” as a DOS Device which points to “\Device\Example”.In the “communicating with usermode” portion, we will learn more about how touse this mapping.
Create the Unload Routine
The next piece of code we will look at is the unload routine. This isrequired in order to be able to unload the device driver dynamically. Thissection will be a bit smaller as there is not much to explain.
Collapse |
Copy Code

VOID Example_Unload(PDRIVER_OBJECT DriverObject)
{

UNICODE_STRING usDosDeviceName;

DbgPrint("Example_UnloadCalled \r\n");

RtlInitUnicodeString(&usDosDeviceName,L"\\DosDevices\\Example");
IoDeleteSymbolicLink(&usDosDeviceName);

IoDeleteDevice(DriverObject->DeviceObject);
}

You can do whatever you wish in your unload routine. This unload routineis very simple, it just deletes the symbolic link we created and then deletesthe only device that we created which was
\Device\Example.
Creating the IRP_MJ_WRITE
The rest of the functions should be self explanatory as they don’t doanything. This is why I am only choosing to explain the “Write” routine. Ifthis article is liked, I may write a second tutorial on implementing
the IOControl function.
If you have used WriteFile and
ReadFile, you know that you simply pass a buffer ofdata to write data to a device or read data from a device. These parameters aresent to the device in the IRP as we explained previously. There
is more to thestory though as there are actually three different methods that the I/O Managerwill use to marshal this data before giving the IRP to the driver. That alsomeans that how the data is marshaled is how the driver’s Read and Writefunctions need to
interpret the data.
The three methods are “Direct I/O”, “Buffered I/O” and “Neither”.
Collapse |
Copy Code

#ifdef __USE_DIRECT__
#define IO_TYPE DO_DIRECT_IO
#define USE_WRITE_FUNCTION Example_WriteDirectIO
#endif

#ifdef __USE_BUFFERED__
#define IO_TYPE DO_BUFFERED_IO
#define USE_WRITE_FUNCTION Example_WriteBufferedIO
#endif

#ifndef IO_TYPE
#define IO_TYPE 0
#define USE_WRITE_FUNCTION Example_WriteNeither
#endif

The code was written so if you define “__USE_DIRECT__” in the header, then
IO_TYPE is now
DO_DIRECT_IO and
USE_WRITE_FUNCTION is now
Example_WriteDirectIO. If you define “__USE_BUFFERED__” in the header, then
IO_TYPE is now
DO_BUFFERED_IO and
USE_WRITE_FUNCTION is now
Example_WriteBufferedIO. If you don’t define
__USE_DIRECT__ or
__USE_BUFFERED__, then
IO_TYPE is defined as 0 (neither) and the writefunction is
Example_WriteNeither.
We will now go over each type of I/O.
Direct I/O
The first thing I will do is simply show you the code for handling directI/O.
Collapse |
Copy Code

NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{

NTSTATUS NtStatus =STATUS_SUCCESS;

PIO_STACK_LOCATION pIoStackIrp =NULL;

PCHAR pWriteDataBuffer;

DbgPrint("Example_WriteDirectIO Called \r\n");

/*

* Each time the IRP is passeddown

* the driver stack a new stacklocation is added

* specifying certain parametersfor the IRP to the driver.

*/

pIoStackIrp =IoGetCurrentIrpStackLocation(Irp);

if(pIoStackIrp)

{

pWriteDataBuffer =
MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);

if(pWriteDataBuffer)

{

/*

* We need to verifythat the string

* is NULL terminated.Bad things can happen

* if we access memorynot valid while in the Kernel.

*/

if(Example_IsStringTerminated(pWriteDataBuffer,
pIoStackIrp->Parameters.Write.Length))

{
DbgPrint(pWriteDataBuffer);

}

}

}

return NtStatus;
}

The entry point simply provides the device object for the device for whichthis request is being sent for. If you recall, a single driver can createmultiple devices even though we have only created one. The other parameter
isas was mentioned before which is an IRP!
The first thing we do is call “IoGetCurrentIrpStackLocation”, and this simply provides us with our
IO_STACK_LOCATION. In our example, the only parameter we need from this isthe length of the buffer provided to the driver, which is at
Parameters.Write.Length.
The way buffered I/O works is that it provides you with a “MdlAddress” which is a “Memory Descriptor List”. This is adescription of the user mode
addresses and how they map to physical addresses.The function we call then is “MmGetSystemAddressForMdlSafe” and we use the
Irp->MdlAddress to do this. This operation will then give us a systemvirtual address which we can then use to read the memory.
The reasoning behind this is that some drivers do not always process auser mode request in the context of the thread or even the process in which itwas issued. If you process a request in a different thread which
is running inanother process context, you would not be able to read user mode memory acrossprocess boundaries. You should know this already, as you run two applicationsthey can’t just read/write to each other without Operating System support.
So, this simply maps the physical pages used by the user mode process intosystem memory. We can then use the returned address to access the buffer passeddown from user mode.
This method is generally used for larger buffers since it does not requirememory to be copied. The user mode buffers are locked in memory until the IRPis completed which is the downside of using direct I/O. This is
the onlydownfall and is why it’s generally more useful for larger buffers.
Buffered I/O
The first thing I will do is simply show you the code for handlingbuffered I/O.
Collapse |
Copy Code

NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{

NTSTATUS NtStatus =STATUS_SUCCESS;

PIO_STACK_LOCATION pIoStackIrp =NULL;

PCHAR pWriteDataBuffer;

DbgPrint("Example_WriteBufferedIOCalled \r\n");

/*

* Each time the IRP is passeddown

* the driver stack a new stacklocation is added

* specifying certain parametersfor the IRP to the driver.

*/

pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);

if(pIoStackIrp)

{

pWriteDataBuffer =(PCHAR)Irp->AssociatedIrp.SystemBuffer;

if(pWriteDataBuffer)

{

/*

* We need to verifythat the string

* is NULL terminated. Bad things canhappen

* if we access memorynot valid while in the Kernel.

*/
if(Example_IsStringTerminated(pWriteDataBuffer,

pIoStackIrp->Parameters.Write.Length))
{
DbgPrint(pWriteDataBuffer);

}

}

}

return NtStatus;
}

As mentioned above, the idea is to pass data down to the driver that canbe accessed from any context such as another thread in another process. Theother reason would be to map the memory to be non-paged so the driver
can alsoread it at raised IRQL levels.
The reason you may need to access memory outside the current processcontext is that some drivers create threads in the SYSTEM process. They thendefer work to this process either asynchronously or synchronously. A
driver ata higher level than your driver may do this or your driver itself may do it.
The downfall of using “Buffered I/O” is that it allocates non-paged memoryand performs a copy. This is now overhead in processing every read and writeinto the driver. This is one of the reasons this is best used on
smallerbuffers. The whole user mode page doesn’t need to be locked in memory as withDirect I/O, which is the plus side of this. The other problem with using thisfor larger buffers is that since it allocates non-paged memory, it would needto allocate a large
block of sequential non-paged memory.
Neither Buffered nor Direct
The first thing I will do is show you the code for handling neitherBuffered nor Direct I/O.
Collapse |
Copy Code

NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{

NTSTATUS NtStatus =STATUS_SUCCESS;

PIO_STACK_LOCATION pIoStackIrp =NULL;

PCHAR pWriteDataBuffer;

DbgPrint("Example_WriteNeither Called \r\n");

/*

* Each time the IRP is passeddown

* the driver stack a new stacklocation is added

* specifying certain parametersfor the IRP to the driver.

*/

pIoStackIrp =IoGetCurrentIrpStackLocation(Irp);

if(pIoStackIrp)

{

/*

* We need this in anexception handler or else we could trap.

*/

__try {

ProbeForRead(Irp->UserBuffer,

pIoStackIrp->Parameters.Write.Length,

TYPE_ALIGNMENT(char));

pWriteDataBuffer =Irp->UserBuffer;

if(pWriteDataBuffer)

{

/*

* We need toverify that the string

* is NULLterminated. Bad things can happen

* if we accessmemory not valid while in the Kernel.

*/
if(Example_IsStringTerminated(pWriteDataBuffer,

pIoStackIrp->Parameters.Write.Length))

{
DbgPrint(pWriteDataBuffer);

}

}

} __except(EXCEPTION_EXECUTE_HANDLER ) {

NtStatus =GetExceptionCode();

}

}

return NtStatus;
}

In this method, the driver accesses the user mode address directly. TheI/O manager does not copy the data, it does not lock the user mode pages inmemory, it simply gives the driver the user mode address buffer.
The upside of this is that no data is copied, no memory is allocated, andno pages are locked into memory. The downside of this is that you must processthis request in the context of the calling thread so you will
be able to accessthe user mode address space of the correct process. The other downside of thisis that the process itself can attempt to change access to the pages, free thememory, etc., on another thread. This is why you generally want to use “ProbeForRead”
and “ProbeForWrite” functions and surround all the code in an exception handler. There’s noguarantee that at any time the pages could be invalid, you can simply attemptto make sure they are,
before you attempt to read or write. This buffer isstored at Irp->UserBuffer.
What’s this #pragma stuff?
These directives you see simply let the linker know what segment to putthe code and what options to set on the pages. The “DriverEntry”, for example,
is set as “INIT” which is a discardable page. This is because you onlyneed that function during initialization.
Homework!
Your homework is to create the Read routines for each type of I/Oprocessing. You can use the Write routines as reference to figure out what youneed to do.
Dynamically Loading andUnloading the Driver
A lot of tutorials will go and explain the registry, however, I havechosen not to at this time. There is a simple user mode API that you can use toload and unload the driver without having to do anything else. This
is what wewill use for now.
Collapse |
Copy Code

int _cdecl main(void)
{

HANDLE hSCManager;

HANDLE hService;

SERVICE_STATUS ss;

hSCManager = OpenSCManager(NULL,NULL, SC_MANAGER_CREATE_SERVICE);

printf("LoadDriver\n");

if(hSCManager)

{

printf("CreateService\n");

hService =CreateService(hSCManager, "Example",
"Example Driver",

SERVICE_START | DELETE| SERVICE_STOP,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,

SERVICE_ERROR_IGNORE,

"C:\\example.sys",
NULL, NULL, NULL, NULL, NULL);

if(!hService)

{

hService =OpenService(hSCManager, "Example",

SERVICE_START| DELETE | SERVICE_STOP);

}

if(hService)

{

printf("StartService\n");

StartService(hService,0, NULL);

printf("Press Enterto close service\r\n");

getchar();

ControlService(hService,SERVICE_CONTROL_STOP, &ss);

DeleteService(hService);

CloseServiceHandle(hService);

}

CloseServiceHandle(hSCManager);

}

return 0;
}

This code will load the driver and start it. We load the driver with “SERVICE_DEMAND_START” which means this driver must be physically started. Itwill
not start automatically on boot, that way we can test it, and if weblue-screen, we can fix the issue without having to boot to safe mode.
This program will simply pause. You can then run the application thattalks to the service, in another window. The code above should be pretty easyto understand that you need to copy the driver to
C:\example.sys inorder to use it. If the service fails to create, it knows it has already beencreated and opens it. We then start the service and pause. Once you pressEnter, we stop the service, delete it from the list of services, and exit. Thisis
very simple code and you can modify it to serve your purposes.
Communicating to the DeviceDriver
The following is the code that communicates to the driver.
Collapse |
Copy Code

int _cdecl main(void)
{

HANDLE hFile;

DWORD dwReturn;

hFile =CreateFile("\\\\.\\Example",

GENERIC_READ |GENERIC_WRITE, 0, NULL,

OPEN_EXISTING, 0, NULL);

if(hFile)

{

WriteFile(hFile, "Hellofrom user mode!",

sizeof("Hellofrom user mode!"), &dwReturn, NULL);

CloseHandle(hFile);

}

return 0;
}

This is probably simpler than you thought. If you compile the driver threetimes using the three different methods of I/O, the message sent down from usermode should be printed in DBGVIEW. As you notice, you simply
need to open theDOS Device Name using \\.\<DosName>. You could even open
\Device\<NtDevice Name> using the same method. You will then create a handle to thedevice and you can call
WriteFile,
ReadFile, CloseHandle,
DeviceIoControl! If you want to experiment,simply perform actions and use DbgPrint to show what code is being executed inyour driver.
Conclusion
This article showed a simple example of how to create a driver, installit, and access it via a simple user mode application. You may use theassociated source files to change and experiment. If you wish to write drivers,it’s
best to read up on many of the basic concepts of drivers, especially, someof the ones linked to in this tutorial.
License
This article has no explicit license attached to it but may contain usageterms in the article text or the download files themselves. If in doubt pleasecontact the author via the discussion board below.
A list of licenses authors might use can be found
here

窗体底端
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: