znsoft
管理员
管理员
  • 注册日期2001-03-23
  • 最后登录2023-10-25
  • 粉丝300
  • 关注6
  • 积分910分
  • 威望14796点
  • 贡献值7点
  • 好评度2410点
  • 原创分5分
  • 专家分100分
  • 社区居民
  • 最爱沙发
  • 社区明星
阅读:2265回复:5

DLLs in Kernel Mode

楼主#
更多 发布于:2004-10-24 11:20
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: DllUnloadn\");
  return STATUS_SUCCESS;
  }
The RegistryPath string passed to DllInitialize is of the form:

RegistryMachineSystemCurrentControlSetServicesSAMPLE

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: %dn\", *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%SYSTEM32DRIVERS, 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:DevKernDLL>dumpbin /exports objfre_w2k_x86i386sample.sys

Microsoft (R) COFF/PE Dumper Version 7.00.9210
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file objfre_w2k_x86i386sample.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 DriverEntryn\"));
  KdPrint ((\"Helper: before is %dn\", xxx));
  SampleDouble(&xxx);
  KdPrint((\"Helper:  after is %dn\", 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 SYSTEM32DRIVERS. 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(REGISTRYMACHINESYSTEMCURRENTCONTROLSETSERVICESSAMPLE)
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.


[编辑 -  3/3/05 by  znsoft]

最新喜欢:

wolfwangwolfwa...
http://www.zndev.com 免费源码交换网 ----------------------------- 软件创造价值,驱动提供力量! 淡泊以明志,宁静以致远。 ---------------------------------- 勤用搜索,多查资料,先搜再问。
RED_spring
驱动中牛
驱动中牛
  • 注册日期2002-07-28
  • 最后登录2016-11-06
  • 粉丝0
  • 关注0
  • 积分3分
  • 威望19点
  • 贡献值0点
  • 好评度17点
  • 原创分0分
  • 专家分0分
  • 社区居民
沙发#
发布于:2004-10-24 12:02
GOOD!
wowocock
VIP专家组
VIP专家组
  • 注册日期2002-04-08
  • 最后登录2016-01-09
  • 粉丝16
  • 关注2
  • 积分601分
  • 威望1651点
  • 贡献值1点
  • 好评度1227点
  • 原创分1分
  • 专家分0分
板凳#
发布于:2004-10-25 09:05
不错,总算找到了,还有感觉KERNEL DLL和RING3的区别不大.
不过编译出来的还是SYS,不过直接改名为DLL,即可使用.
花开了,然后又会凋零,星星是璀璨的,可那光芒也会消失。在这样 一瞬间,人降生了,笑者,哭着,战斗,伤害,喜悦,悲伤憎恶,爱。一切都只是刹那间的邂逅,而最后都要归入死亡的永眠
shenming365
驱动牛犊
驱动牛犊
  • 注册日期2004-04-08
  • 最后登录2008-06-27
  • 粉丝0
  • 关注0
  • 积分157分
  • 威望18点
  • 贡献值0点
  • 好评度13点
  • 原创分0分
  • 专家分0分
地板#
发布于:2005-03-03 14:42

  
有类似LoadLibrary的方法吗。
www.software168.com
bmyyyud
驱动老牛
驱动老牛
  • 注册日期2002-02-22
  • 最后登录2010-01-21
  • 粉丝0
  • 关注0
  • 积分1000分
  • 威望130点
  • 贡献值0点
  • 好评度106点
  • 原创分0分
  • 专家分0分
地下室#
发布于:2005-03-03 15:33
我感觉内核中的东西都象DLL,ntoskrnl.exe后缀虽是个EXE,根本就象个DLL
滚滚长江东逝水 浪花淘尽英雄 是非成败转头空 青山依旧在 几度夕阳红 白发渔樵江渚上 惯看秋月春风 一壶浊酒喜相逢 古今多少事 尽付笑谈中
stormzycq
驱动牛犊
驱动牛犊
  • 注册日期2005-09-16
  • 最后登录2009-06-11
  • 粉丝0
  • 关注0
  • 积分350分
  • 威望48点
  • 贡献值0点
  • 好评度35点
  • 原创分0分
  • 专家分0分
5楼#
发布于:2007-08-28 16:40
遇到一个问题,在写驱动的库的时候,编写了def文件,但却没有正常生效,即便我将DEF中的所有导出函数清空,编译的时候依然按照以前的DEF的内容进行了编译。有人碰到过这个问题吗?
我的工程的结构如下:
kernel\
          inc
               dll1
               dll2
         source
                dll1
                dll2
         lib
               dll1
               dll2
         bin
以前inc中的文件是和source中的文件混合放在一起的,没有任何问题
但将这两部分文件分开,就出现了这个问题。
太头痛了。
游客

返回顶部