分享
 
 
 

(转载)DLLs in Kernel Mode

王朝other·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

DLLs in Kernel Mode

July 15, 2003

Tim Roberts

Copyright © 2003, Tim Roberts. All rights reserved

Win32 user-mode programmers are accustomed to using and creating dynamic link libraries, or DLLs, to compartmentalize their applications and enable efficient code reuse. The typical application includes many DLLs, and careful design allows those DLLs to be reused many times over.

Kernel driver writers are often not aware that they can use exactly the same concept in kernel mode. The standard DDK even includes several samples (for example, storage/changers/class). In this article, I will show you a working (though trivial) example of a kernel DLL.

The Basics

In terms of the C source code, a kernel DLL is virtually identical to a user-mode DLL. The primary difference is that you may not call any user-mode APIs from a kernel DLL. That should not be surprising.

You use a kernel DLL just like a user-mode DLL: the linker builds an import library when it builds your DLL, and you include that library in the target library list for any driver that needs to use the DLL. No registry magic is required, and no special action is needed to start or stop the DLL. Your kernel DLL will be automatically loaded as soon as any other driver makes a reference to it, and is automatically unloaded when the last referring driver unloads.1

You can export DLL entry points from a normal WDM driver as well. There are many drivers in the operating system that export entry points for other drivers to use. For example, the ubiquitous NTOSKRNL.EXE, which contains all of the Ex, Fs, Io, Ke, Mm, Nt, and Zw entry points used by virtually every driver, is nothing more than a standard kernel driver with exports, exactly like the DLL we will describe here.

Digging In

OK, now let's get in to a few of the details. All of the source files for this project are available at http://www.wd-3.com/downloads/kdll.zip.

The most important step to take when creating an export driver is to specify the TARGETTYPE macro in the "sources" file:

TARGETTYPE=EXPORT_DRIVER

This type tells the build system that our project will build a kernel-mode driver that is exporting functions. If you leave TARGETTYPE set to DRIVER, as with a normal kernel-mode driver, your exports will not be available to other drivers.

Your DLL must include the standard DriverEntry entry point, but the system won't actually call that entry point. This requirement is an artifact of the build system, which adds /ENTRY:DriverEntry to the linker options for every kernel driver. An EXPORT_DRIVER can also function as a normal driver, and the build system cannot tell whether we want to do that or not, so we have to supply this dummy entry point for an export-only DLL.

If you do need to take special one-time action on loading and unloading, you should export two special enty points called DllInitialize and DllUnload:

NTSTATUS DllInitialize(IN PUNICODE_STRING RegistryPath)

{

DbgPrint("SAMPLE: DllInitialize(%wZ)\n", RegistryPath);

return STATUS_SUCCESS;

}

NTSTATUS DllUnload()

{

DbgPrint("SAMPLE: DllUnload\n");

return STATUS_SUCCESS;

}

The RegistryPath string passed to DllInitialize is of the form:

\Registry\Machine\System\CurrentControlSet\Services\SAMPLE

You would only include a DllInitialize routine in an export-only DLL -- that is, in a driver that's used exclusively as a DLL and not as a real driver for hardware. You do not need to define a service key for such a driver. Consequently, the RegistryPath string is not probably not going to be useful to you, because it likely names a registry key that doesn't even exist.

Compatibility caution: A bug in Windows 98 Gold will prevent your DLL from loading if you include a DllInitialize entry point. Further, Windows 98 Second Edition and Windows Millennium will never call the DllUnload entry point. A kernel DLL in these systems, once loaded, is permanent.

Declaring Exports

Beyond those two special entry points, you can create whatever entry point names you find convenient. You just need to identify those entry point names to the linker. There are two ways to do that. For our sample purposes, I will be exporting one functional entry point from our DLL:

NTSTATUS SampleDouble(int* pValue)

{

DbgPrint("SampleDouble: %d\n", *pValue);

*pValue *= 2;

return STATUS_SUCCESS;

}

There are two ways to tell the linker that you want to export a function. The first method is to enumerate the names in a .DEF file. The .DEF file is familiar to anyone who has done Win16 or Win32 programming. It is a special file used to give instructions to the linker that cannot be easily included on the command line. In this case, it enumerates the names of the routines we want to export from the DLL. The linker uses this list to create the symbol tables in the DLL, and to create an import library that we can use in other projects to call into our DLL. Our .DEF file looks like this:

NAME SAMPLE.SYS

EXPORTS

DllInitialize PRIVATE

DllUnload PRIVATE

SampleDouble

DllInitialize and DllUnload must both be marked as PRIVATE. This tells the linker to export the symbol from the DLL executable file, but not to include it in the import library it builds. The build system will flag an error if these are not marked PRIVATE.

The import library is the fundamental mechanism used to map a function's name to the DLL that contains that function. Almost all of the libraries you use in Win32 program are import libraries, including kernel libraries such as ntdll.lib and ntoskrnl.lib, and user-mode libraries such as kernel32.lib, user32.lib, and gdi32.lib. Such libraries do not actually contain any code. Instead, they contain a set of linker tables that contains information that means something like, "The name MySampleFunction maps to _MySampleFunction@4 in MY.DLL".

The linker embeds this information into the executable file, so that the operating system can tie all of the loose ends together when the EXE or DLL is finally loaded into memory.

We have to use the special DLLDEF macro in the "sources" file to identify the name of our .DEF file:

DLLDEF=sample.def

The second way to identify your exported entry points is to use a declspec attribute in your source code:

__declspec(dllexport) NTSTATUS SampleDouble(int* pValue)

{

...

}

This has the same effect as listing the name in the .DEF file. In general, I am in favor of reducing the number of files in my project, since that automatically reduces the number of chances for error. However, in this case, there is a catch: DllInitialize and DllUnload must be marked as PRIVATE exports, and to my knowledge, there is no way to mark an export PRIVATE without using a .DEF file. Thus, you will HAVE to use the .DEF file at least for those two names. Whether you include your other exports in the .DEF file or mark them with __declspec(dllexport) is completely up to you.

The sample source code for this article is in C. If you wish to export functions from a DLL written in C++, you have an additional complication to consider. Because C++ allows multiple functions with the same name but different argument lists, C++ compilers "decorate" their symbol names with extra characters that specifically identify the return type and argument list. For example, the actual name of the SampleDouble function when compiled in a C++ module is ?SampleDouble@@YGJPAH@Z. If you try to call this function from another C++ driver, it would work, but if you try to call it from a C driver, the external names won't line up.

The way to fix this is to use a special language modifier on the extern declaration, like this:

extern "C" NTSTATUS SampleDouble(int* pValue)

{

...

}

With that information, we can now bring up a DDK command shell and do a build. For this example, we build a file called sample.sys. We copy this file to the traditional location for drivers, %WINDIR%\SYSTEM32\DRIVERS, and we are ready to use our DLL. We can verify the exports using the "dumpbin" command, just like you would with a user-mode DLL:

C:\Dev\KernDLL>dumpbin /exports objfre_w2k_x86\i386\sample.sys

Microsoft (R) COFF/PE Dumper Version 7.00.9210

Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file objfre_w2k_x86\i386\sample.sys

File Type: EXECUTABLE IMAGE

Section contains the following exports for SAMPLE.SYS

00000000 characteristics

3EEEB656 time date stamp Mon Jun 16 23:33:58 2003

0.00 version

1 ordinal base

3 number of functions

3 number of names

ordinal hint RVA name

1 0 0000031B DllInitialize

2 1 00000347 DllUnload

3 2 00000368 SampleDouble

...

Alternatively, you can use the Dependency Walker applet (DEPENDS.EXE) from the Platform SDK to view the file:

Notice that the Subsystem displayed for all the modules in the bottom pane is "Native". If you ever see a "Win32" subsystem when you're looking at the dependencies for a driver or kernel DLL you've created, it means that you've called a user-mode API function.

Calling Into the DLL

To make it convenient to call the entry points in our DLL, we probably want to create a header file to include in our calling driver. For this sample, we can use this simple sample.h:

#pragma once

EXTERN_C DECLSPEC_IMPORT NTSTATUS SampleDouble(int* pValue);

We use several custom macros here to make the file flexible and easier to read. These macros are defined in <ntdef.h>, which is included automatically in most drivers via <wdm.h>

EXTERN_C expands to extern "C" in a C++ source file, and to plain old extern in a C source file. This ensures that no unwanted decoration will occur in the calling program.

DECLSPEC_IMPORT expands to the Visual C++ specifier __declspec(dllimport). This is the complement of the __declspec(dllexport) macro we used above, and tells the compiler that the call will be satisfied from a DLL at run-time, rather than being loaded at link-time. This allows the compiler and the linker to optimize the runtime linkage to SampleDouble.2

When you define a header file like this that contains function prototypes for a DLL, it's a good idea to include that header in the DLL project too. Doing that gives you a compile-time check that you've correctly prototyped the functions. All those DECLSPEC_IMPORT directives will cause warning messages from the compiler, however. You can cure that minor problem by putting some conditional compilation into the header:

#pragma once

#ifdef SAMPLE_INTERNAL

#define SAMPLE_IMPORT

#else

#define SAMPLE_IMPORT DECLSPEC_IMPORT

#endif

EXTERN_C SAMPLE_IMPORT SampleDouble(int* pValue);

You define the symbol SAMPLE_INTERNAL in your DLL project, which causes the header to not have any __declspec directives. Other projects that include the header don't define this symbol, which means that the header does contain the directives.

Testing

To test our sample, I added the following code to the DriverEntry of a kernel driver I have been working on:

#include "sample.h"

NTSTATUS DriverEntry(...)

{

PDEVICE_OBJECT deviceObject = NULL;

NTSTATUS ntStatus;

WCHAR deviceNameBuffer[] = L"\\Device\\dbgdrvr";

UNICODE_STRING deviceNameUnicodeString;

WCHAR deviceLinkBuffer[] = L"\\DosDevices\\DBGDRVR";

UNICODE_STRING deviceLinkUnicodeString;

int xxx = 19;

KdPrint(("HELPER.SYS: entering DriverEntry\n"));

KdPrint (("Helper: before is %d\n", xxx));

SampleDouble(&xxx);

KdPrint(("Helper: after is %d\n", xxx));

...

}

I copied "sample.lib" from my sample build directory into the test build directory, and added "sample.lib" to the TARGETLIBS macro in "sources". In fact, because my test driver is so simple, the entire sources file is here:

TARGETNAME=dbgdrvr

TARGETPATH=obj

TARGETTYPE=DRIVER

TARGETLIBS=sample.lib

SOURCES=dbgdrvr.c

I then built my driver and copied the binary to SYSTEM32\DRIVERS. This is an old-style NT 4 driver, so I started it up with "net start" and stopped it with "net stop". The resulting debug log looked like this:

SAMPLE: DllInitialize(\REGISTRY\MACHINE\SYSTEM\CURRENTCONTROLSET\SERVICES\SAMPLE)

HELPER.SYS: entering DriverEntry

Helper: before is 19

SampleDouble: 19

Helper: after is 38

HELPER.SYS: unloading

SAMPLE: DllUnload

Note that our DLL loads before the calling driver starts to execute, and unloads after the calling driver shuts down. This, again, is similar to the way a Win32 user-mode DLL operates: the system does not know whether we intend to call the DLL within our DriverEntry or not, so it ensures that all DLLs are in place an initialized before launching the referring driver.

You can see the registry path being passed to the DllInitialize entry point of my kernel DLL. You'll have to trust me when I tell you there is no such path in my registry; the string is just for decoration.

Conclusion

This is a lot of work just to double an integer, but it demonstrates a powerful and little-known concept. With a little forethought, you can build a centralized repository for all of your interesting overhead routines, hiding the sometimes daunting complexity of the kernel APIs in a simple wrapper that you can use over and over again.

About the author:

Tim Roberts is a hopeless software engineer who programs both for fun and for profit. Tim has been programming computers for more than a third of a century, on everything from microcontrollers to mainframes.

Tim is a partner in Providenza & Boekelheide, Inc., a technology consulting company in the Silicon Forest just outside of Portland, Oregon. P&B provides all kinds of hardware and software consulting, specializing in graphics, video, and multimedia.

1 -- Kernel DLLS are never unloaded in Windows 98 Second Edition or in Windows Millennium, though. I'll say more about platform compatibility later on in the article.

2 -- If you give the compiler the clue that a given function will be imported from another DLL, it generates an indirect call through the module's indirect address table. If you don't give the compiler this clue, it generates a call to an external function. The linker then includes a function thunk (taken from the import library) that contains an indirect call through the indirect address table. Thus, using __declspec(dllimport) eliminates the thunk in the middle and saves a few machine cycles at run time.

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有