内核模式的 DLL_内核dll-程序员宅基地

技术标签: 编译器  import  dll  硬件驱动  microsoft  windows  

Tim Roberts


版权所有 (C) 2003,Tim Roberts。保留所有权利


Win32 用户模式程序员已经习惯于使用和创建动态链接库,或者叫 DLL,来划分应用或者达到有效的代码重用。典型的应用程序包括许多 DLL,仔细的设计可以使得这些 DLL 能被多次重用。


内核驱动程序作者常常不知道也可以在内核模式中正确地使用这一概念。标准的 DDK 甚至还带有好几个示例(例如,storage/changers/class)。在本文中,我将演示一个可以工作的(尽管微不足道)内核 DLL 的例子。


基础


从 C 语言源代码看来,内核 DLL 实质上等同于用户模式 DLL。主要的不同在于不能在内核 DLL 中调用任何用户模式 API。这并不奇怪。


使用内核 DLL 就像用户模式 DLL 一样:链接器在构建 DLL 时生成一个导入库,然后将此库包含到将要使用此 DLL
的任何驱动程序的目标库列表中。既不需要注册表技巧,也不需要任何特别的动作来起停该 DLL。内核 DLL
将随任何引用之的其他驱动程序自动加载,而随最后一个引用之的驱动程序自动卸载(注 1)。


也可以从正常的 WDM 驱动程序导出入口点。操作系统中有许多驱动程序为其他驱动程序的使用导出了入口点。例如,无所不在的
NTOSKRNL.EXE —— 其中包含了所有的 Ex、Fs、Io、Ke、Mm、Nt 以及 Zw 入口点,事实上会被每个驱动程序用到 ——
也只不过就是一个标准的带有导出的内核驱动程序,正像我们在这儿讨论的 DLL 一样。


深入


好,现在让我们进入一些细节里来。本工程的所有源文件均可从 http://www.wd-3.com/downloads/kdll.zip 得到。


当创建一个导出的驱动程序时,要进行的最重要的步骤是在 sources 文件中指定 TARGETTYPE 宏:





TARGETTYPE=EXPORT_DRIVER


这个类型告诉创建系统我们的工程将构建一个要导出函数的内核模式驱动程序。如果你像普通内核模式驱动程序那样保持 TARGETTYPE 设置为 DRIVER,则其他驱动程序将不能使用你的导出。


你的 DLL 必须包含标准的 DriverEntry
入口点,不过实际上系统不会调用它。这个需求是创建系统的人为限制,因为它会为每个内核驱动程序把 /ENTRY:DriverEntry
添加到链接器选项中。EXPORT_DRIVER
类型的驱动程序也需要像普通驱动程序那样工作,而且创建系统并不能说明我们会不会调用入口点,因此我们为只导出的 DLL 也必须提供一个伪入口点。


如果你确实需要在加载和卸载时执行一次性操作,那你就应该到处两个特殊的入口点,叫做 DllInitialize 和 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;
}


传入 DllInitialize 的 RegistryPath 字符串具有如下形式:





/Registry/Machine/System/CurrentControlSet/Services/SAMPLE


只需要在只导出类型的 DLL 中包含 DllInitialize 例程 —— 就是说,在一个仅作为 DLL
来使用并且不是真实的硬件驱动程序的驱动程序中。不需要为这种驱动程序定义服务相关的注册表键。因而,RegistryPath
字符串不可能也不希望对你有用,因为它很可能是一个不存在的注册表键的名字。


兼容警告:在 Windows 98 Gold 中有一个缺陷,如果你包含了 DllInitialize 入口点,你的 DLL
将无法加载。另外,Windows 98 第二版以及 Windows Me 从不会调用 DllUnload 入口点。在这些系统中,一个内核 DLL
一旦加载,将永远存在。


声明导出


除去这两个特殊的入口点,你可以创建任何你觉得方便的入口点名称。你只需向链接器标明这些入口点名称。做这件事有两个方法。出于示例目的,我将从我们的 DLL 导出一个功能性入口点:





NTSTATUS SampleDouble(int* pValue)
{
DbgPrint("SampleDouble: %d/n", *pValue);
*pValue *= 2;
return STATUS_SUCCESS;
}


有两个方法来告诉链接器你想导出一个函数。第一个方法是在 .DEF 文件中列举名称。.DEF 文件对任何一个做过 Win16 或者
Win32 编程的人来说很常见。这是一个特殊的文件,用于向编译器给出那些不便在命令行上包括的指令。就目前而言,它要罗列我们想从 DLL
中导出的例程的名称。编译器使用这一列表在 DLL 中创建符号表,并且创建一个可以在其他工程中使用以使之可以调用我们的 DLL 的导入库。我们的
.DEF 文件看起来像这样:





NAME SAMPLE.SYS

EXPORTS
DllInitialize PRIVATE
DllUnload PRIVATE
SampleDouble


DllInitialize 和 DllUnload 必须全部标示为 PRIVATE。这将告诉链接器从 DLL 可执行文件中导出此符号,但不要将其置入创建的导入库中。如果它们没有被标示为 PRIVATE 创建系统会标记一个错误。


导入库是将函数名字映射到包含该函数的 DLL 中去的基础机制。你在 Win32 程序中使用的几乎所有的库都是导入库,包括诸如
ntdll.lib 和 ntoskrnl.lib 等内核库以及像 kernel32.lib、user32.lib 和 gdi32.lib
等用户模式的库。这些库实际上并不包含任何代码。相反,它们包含一组链接器表,表中包含一些有点像这种意思的信息,“将名字
MySampleFunction 映射到 MY.DLL 中的 _MySampleFunction@4”。


链接器把这些信息嵌入到可执行文件中,于是操作系统在 EXE 或者 DLL 最终加载到内存中以后可以把所有这些零碎串起来。


我们必须在 sources 文件中使用特殊的 DLLDEF 宏来指明 .DEF 文件的名字:





DLLDEF=sample.def


标示到处入口点的第二个方法是在源代码中使用 declspec 属性:





__declspec(dllexport) NTSTATUS SampleDouble(int* pValue)
{
...
}


这和在 .DEF
文件中列出名字的作用一样。通常,我乐于减少工程中文件的数目,因为这可以自动减少出错的机会。但是眼下,这儿有一个问题:DllInitialize 和
DllUnload 必须被标志为 PRIVATE 导出,并且就我所知,除了使用 .DEF 文件还没有别的办法可以指定一个导出为
PRIVATE。因此,你将必须使用 .DEF 文件,至少是为这两个名字。其他的导出究竟是包括到 .DEF 文件中还是使用
__declspec(dllexport) 标示它们,完全随你。


本文的示例源代码使用 C 语言。如果你希望从一个用 C++ 写就的 DLL 中导出函数,你还有另外的一个因素需要考虑。因为 C++
允许多个具有不同参数列表的函数同名,C++ 编译器会“修饰”它们的符号名字,使用附加的、用于特别标示返回类型和参数列表的字符。例如,当把
SampleDouble 函数编译到一个 C++ 模块中时,其实际名字是 ?SampleDouble@@YGJPAH@Z。如果你在其他 C++
驱动程序中尝试调用此函数,它可以工作,但如果你试图从一个 C 语言驱动程序中调用它,则外部的名字不能匹配。


解决此问题的方法是基于 extern 声明使用一个特殊的语言修饰符,就像这样:





extern "C" NTSTATUS SampleDouble(int* pValue)
{
...
}


有了这些了解,我们现在可以打开 DDK 命令环境来构建了。对本示例来讲,我们要构建一个叫 sample.sys
的文件。我们将此文件复制到驱动程序的习惯位置,%WINDIR%/SYSTEM32/DRIVERS,则已经准备好使用我们的 DLL
了。可以使用“dumpbin”命令来验证导出,就像对一个用户模式 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
...


我们还可以选择使用 Platform SDK 中附带的 Dependency Walker 小程序(DEPENDS.EXE)来查看此文件:


图从略 —— 译者注。


注意,底部窗格中显示出所有模块的子系统为“Native”。如果在对一个你创建的驱动程序或者内核 DLL 查看依赖时看到了“Win32”子系统,那就意味着你调用了用户模式的 API 函数。


调用 DLL


为了方便调用 DLL 中的入口点,我们可能想创建一个头文件,包含到我们的调用驱动程序中。就此例来说,我们可以使用如下简单的 sample.h:





#pragma once
EXTERN_C DECLSPEC_IMPORT NTSTATUS SampleDouble(int* pValue);


我们在这儿使用了若干个使文件更灵活而且更易于阅读的宏。这些宏定义于 中,该头文件在大多数驱动程序中经由 被自动包含。


EXTERN_C 在 C++ 源文件中展开为 extern "C",而在 C 源文件中展开为简单的老式的 extern。这保证了在调用程序中不会产生不需要的修饰。


DECLSPEC_IMPORT 展开为 Visual C++ 的指定符 __declspec(dllimport)。这是上面用到过的
__declspec(dllexport) 的对照物,用以告诉编译器对该函数的调用将在运行时从一个 DLL
处得到满足,而不是在链接时被加载。这允许编译器和链接器优化对 SampleDouble 的运行时联接(注 2)。


当你定义了这样一个头文件,其中包含了一个 DLL 的函数原型,则把此头文件也包含到 DLL
工程中是个好主意。这样做将使你在编译时可以检查函数的原型是否正确。所有的 DECLSPEC_IMPORT
指示会导致编译器的警告。你可以通过往头文件中放置一些条件编译来消除这个小问题:





#pragma once
#ifdef SAMPLE_INTERNAL
#define SAMPLE_IMPORT
#else
#define SAMPLE_IMPORT DECLSPEC_IMPORT
#endif

EXTERN_C SAMPLE_IMPORT SampleDouble(int* pValue);


在你的 DLL 工程中定义符号 SAMPLE_INTERNAL,会导致头文件中没有任何 __declspec 指示。其他包含此头文件的工程中没有定义这一符号,就意味着头文件里的确包含有这些指示。


测试


为了测试我们的例子,我把以下代码加入到了一个我正在手上的内核驱动程序的 DriverEntry 中:





#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));
...
}


我把 sample.lib 从示例的构建目录复制到了测试程序的构建目录,并且将“sample.lib”加入到了 sources 里的 TARGETLIBS 宏中。实际上,因为我们的测试驱动程序实在是太简单了,整个 sources 文件都在这儿了:





TARGETNAME=dbgdrvr
TARGETPATH=obj
TARGETTYPE=DRIVER

TARGETLIBS=sample.lib

SOURCES=dbgdrvr.c


然后我创建了我的驱动程序并且把二进制文件复制到了 SYSTEM32/DRIVERS。这是一个老式的 NT 4 驱动程序,所以我用“net start”来启动它,用“net stop”来停止。生成的调试日志看起来像这样:





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


注意我们的 DLL 先于调用驱动程序开始执行,而在调用驱动程序关闭后卸载。这,再一次,类似于 Win32 用户模式的 DLL 操作:系统不知道我们是否计划在 DriverEntry 中调用 DLL,因此它在启动调用驱动程序之前确保所有的 DLL 均已就绪。


你可以看一下传递到我的内核 DLL 的 DllInitialize 入口点中的注册表路径。当我告诉你在我的注册表里根本没有这么个路径时,你将不得不相信我,那个字符串确实是个装饰品。


结论


对于仅仅是倍增一个整数来讲,这工作太多了点,但它演示了一个强大的、鲜为人知的概念。使用少许的规划,你可以为你所有感兴趣的例程构建一个集中的知识库,把有时令人生畏的内核 API 的复杂性隐藏到一个简单的封装里,你可以一再地去使用它。


关于作者:

Tim Roberts,一个不可救药的软件工程师,写程序既为娱乐又为金钱。Tim 对计算机编程已经超过了三分之一个世纪,在任何东西上编程,从微控制器到大型主机。


Tim 是 Providenza & Boekelheide 公司的股东,那是个位于硅谷的技术咨询公司,正好在俄勒冈波特兰之外。P&B 提供所有类型的硬件和软件的咨询,尤其是图像、视频和多媒体。


注 1:不过,在 Windows 98 第二版或者 Windows Me 中内核 DLL 永远也不会卸载。我将在后文的讲述关于平台兼容时提及更多。

注 2:如果你告知编译器一个给定的函数将从另一个 DLL
中导入,它将会生成一个使用间接地址表的间接调用。反之,生成一个对外部函数的调用。然后链接器会包含一个转换函数(取自导入库),该转换函数包含一个使
用间接地址表的间接调用。所以,使用 __declspec(dllimport) 会在运行时消除中间的转换并节省少量的机器时钟周期。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/leibniz_zsu/article/details/6312714

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法