《天书夜读:从汇编语言到windows内核编程》十一 用C++编写内核程序
---恢复内容开始---
1) C++的“高级”特性,是它的优点也是它的缺点,微软对于使用C++写内核程序即不推崇也不排斥,使用C++写驱动需注意:
a)New等操作符不能直接使用,如果要使用,必须进行重载。
b)标准C接口的声明,在包含头文件以及入口例程的前面要声明extern “C”
c)类的静态成员函数的使用:在类内部声明一个静态函数作为分发函数Dispatch,这个静态函数当做普通的C函数调用,而C++声明的非静态函数调用该函数进行分配。
这一部分读下来,其实大多不是和内核相关,而是了解C++语言的汇编代码。
2)下面是一个实例的代码:
extern "C" {
#include <ntifs.h>
}
#define TAG 'abcd'
DRIVER_UNLOAD DriverUnload;
extern "C" DRIVER_INITIALIZE DriverEntry;
class MyDriver
{
public:
MyDriver(PDRIVER_OBJECT driver);
//可派生的Dispath虚函数(在DDK编译环境中可行)
//VS2005编译器环境定义可派生虚函数报错,这里采用重载方式去实现
//原因未知
virtual NTSTATUS OnDispatch(PDEVICE_OBJECT dev,PIRP irp)
{
return STATUS_UNSUCCESSFUL;
}
//静态成员,总是记录被唯一实例化的MyDriver指针
static MyDriver *d_my_driver;
private:
//静态成员函数,用来做为dispatch函数使用
static NTSTATUS sDispatch(PDEVICE_OBJECT dev,PIRP irp);
PDRIVER_OBJECT d_driver;
};
//实现代码
MyDriver *MyDriver::d_my_driver = NULL;
//构造函数
MyDriver::MyDriver(PDRIVER_OBJECT driver):d_driver(driver)
{
size_t i;
; i <= IRP_MJ_MAXIMUM_FUNCTION ; i++)
{
driver->MajorFunction[i] = sDispatch;
}
driver->DriverUnload = DriverUnload;
d_my_driver = this;
}
//静态的分发函数的实现:调用虚函数,以便以后派生
NTSTATUS MyDriver::sDispatch(PDEVICE_OBJECT dev,PIRP irp)
{
return d_my_driver->OnDispatch(dev,irp);
}
//第一个参数size_t是必须外置的,编译器会自动用sizeof(clsName)求取长度并作为第一个参数
void* __cdecl operator new(size_t size,POOL_TYPE pool_type,ULONG pool_tag)
{
ASSERT((pool_type < MaxPoolType) && ( != size));
)
return NULL;
//中断级检查。分发级别和以上的级别只能分配非分页内存
ASSERT(pool_type == NonPagedPool ||(KeGetCurrentIrql() < DISPATCH_LEVEL));
return ExAllocatePoolWithTag(pool_type,static_cast<ULONG>(size),pool_tag);
}
//自己实现delete
void __cdecl operator delete(void* pointer)
{
ASSERT(NULL != pointer);
if (NULL != pointer)
ExFreePool(pointer);
}
//自动实现delete[]操作
void __cdecl operator delete[](void* pointer)
{
ASSERT(NULL != pointer);
if (NULL != pointer)
ExFreePool(pointer);
}
//提供一个Unload函数值是为了让这个程序能动态卸载,方便调试
VOID DriverUnload(PDRIVER_OBJECT driver)
{
//打印一句
KdPrint(("CPP Driver is unloading..."));
}
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg)
{
MyDriver::d_my_driver = new(NonPagedPool, TAG) MyDriver(driver);
KdPrint(("CPP Driver Test!"));
driver->DriverUnload = DriverUnload;
delete MyDriver::d_my_driver;
return STATUS_SUCCESS;
}
效果图:

3)选择WDK的“X86 check build enviroment”编译上述代码(如果没有虚函数,可用VS2005将项目属性中C/C++选项卡下的优化项选择为禁止,然后编译)。下面是DriverEnter函数的反汇编代码(用IDA反汇编,感觉比WIND32ASM好用,带可视化的图形模块),注意一些调用约定:其中的new与delete都在定义时指定为cdecl,而类一般为thiscall,其它默认函数为stdcall,在下一章中,还将使用fastcall调用约定:
a)Stdcall:通常用于win32API中在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。stdcall的调用约定意味着:参数从右向左压入堆栈;函数自身修改堆栈;函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸(如常见的“_function@8”等格式)
b)Cdecl:有的地方写成cdcall,即C语言调用约定,是C语言缺省的调用方式。Cdecl调用约定也是参数从右向左压入堆栈,但是函数本身不清理堆栈,由调用者来清理,由于这种特性,C语言调用约定允许函数参数个数是不定的。
c)Thiscall:thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:参数从右向左入栈;如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈;对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈。
d) Fastcall:fastcall调用约定与stdcall调用几乎完全相同,唯一的区别是,前两个参数不被放入堆栈中传入,而是放入ecx与edx中,ecx保存第一个参数,而edx保存第二个参数,其它参数采用堆栈传递。

DriverEntry反汇编
第一:类的构造函数的调用代码是C++编译器自动生成的,构造函数对程序员来说不能自行调用,也不能定义返回值,但从汇编代码来看,这里是返回了对象实例自身的指针。
第二:var_4、var_C、P从分析上来看存放的都是同一个值,就是类实例的一个指针值。
第三:driver->DriverUnload = DriverUnload被书写了2次(入口函数中一次,构造函数中也写了一次),这个是我的失误,不过不影响代码的执行。在读汇编类似的代码时,可以查看DRIVER_OBJECT结构体具体内容来判断偏移34H到底是哪个域。
第四:[ebp + p]这个局部变量完全没有定义的必要,debug版本反汇编虽然和最初的编写逻辑很对应,但是会发现很多的无用代码,而release版本经编译器优化以后模样大变,实际上看debug版本能了解语言特性,而看release版本才是比较实用的,因为没人会发布debug版本。
4)构造函数反汇编:

C++构造函数反汇编
第一:对象内存空间一开始存放的并不是数据成员,而是虚函数表(假如定义有虚函数)。
第二:对象内存空间在虚函数表之后存放的是数据成员,不包含静态的数据成员。
第三:对象的静态数据成员并不在对象实例内存空间,而采用的全局变量来使用。
5)虚函数派生反汇编,新增派生类:
class MySubDriver:public MyDriver
{
public:
MySubDriver(PDRIVER_OBJECT driver):MyDriver(driver){};
virtual NTSTATUS OnDispatch(PDEVICE_OBJECT dev,PIRP irp)
{
KdPrint(("This is the dispatch function of MySubDriver!\n"));
return STATUS_UNSUCCESSFUL;
}
};
改写DriverEntry下面一句:
MyDriver::d_my_driver = new(NonPagedPool, TAG) MySubDriver(driver);
派生类构造函数:

C++子类构造函数反汇编
第一:从此可解释,C++派生类的构造函数是从最原始的基类向子类来执行的
第二:填写虚函数地址表时,实际上执行的是覆盖。这里在基类中已经写入了基类的虚函数地址,然后当基类构造函数执行完毕以后,派生类会再次写入虚函数地址(如果派生类重写了虚函数,则该地址会覆盖基类的填写的虚函数地址,否则保留基类的虚函数)。
第三:这是一个无用代码的说明,在标准现场保护完毕之后,调用基类的构造函数之前,应该只需要将传入参数driver压入栈,可是之前却还压入了ecx(也就是类实例this指针),从基类构造函数以及派生类构造函数后面的代码中可分析,这个压入的值并没有被使用,也没有再弹出。
虚函数调用代码:

C++虚函数调用反汇编
关于虚函数的调用机制,还得看下所谓的虚函数地址表到底是怎么一回事:

C++虚函数地址表
可见,虚函数地址表实际上是一些常量指针值,这些常量指针值保存了各个虚函数的定义代码的地址值,构造函数在填写虚函数的时候,并没有直接写人虚函数的函数地址,而是写入的常量指针值。
那么,调用虚函数的汇编代码就解释得清楚了,首先从类实例中取得虚函数常量指针值到eax,再通过mov edx,[eax]将虚函数真正所在的地址放入edx,再call edx实行调用。
第一:基类和派生类虚函数地址表各有一份,并不会覆盖,但是在实例化一个类以后,这个实例的内存空间中,维持着一个虚函数地址表,这个表里面对应的是实际使用的虚函数地址(最初是原始基类的虚函数地址表,但随着之后各个类的派生,会随着虚函数的重定义而重新改写表中的数值)。
第二:不同的编译器对虚函数地址的改写方式可能会有不同。
第三:对虚函数地址表的存在,为修改虚函数地址表来实现HOOK提供了可能。
《天书夜读:从汇编语言到windows内核编程》十一 用C++编写内核程序的更多相关文章
- 《天书夜读:从汇编语言到windows内核编程》五 WDM驱动开发环境搭建
(原书)所有内核空间共享,DriverEntery是内核程序入口,在内核程序被加载时,这个函数被调用,加载入的进程为system进程,xp下它的pid是4.内核程序的编写有一定的规则: 不能调用win ...
- 《天书夜读:从汇编语言到windows内核编程》八 文件操作与注册表操作
1)Windows运用程序的文件与注册表操作进入R0层之后,都有对应的内核函数实现.在windows内核中,无论打开的是文件.注册表或者设备,都需要使用InitializeObjectAttribut ...
- 《天书夜读:从汇编语言到windows内核编程》六 驱动、设备、与请求
1)跳入到基础篇的内核编程第7章,驱动入口函数DriverEnter的返回值决定驱动程序是否加载成功,当打算反汇编阅读驱动内核程序时,可寻找该位置. 2)DRIVER_OBJECT下的派遣函数(分发函 ...
- 《天书夜读:从汇编语言到windows内核编程》四 windows内核调试环境搭建
1) 基础篇是讲理论的,先跳过去,看不到代码运行的效果要去记代码是一个痛苦的事情.这里先跳入探索篇.其实今天的确也很痛苦,这作者对驱动开发的编译与调试环境介绍得太模糊了,我是各种尝试,对这个环境的搭建 ...
- 《天书夜读:从汇编语言到windows内核编程》十 线程与事件
1)驱动中使用到的线程是系统线程,在system进程中.创建线程API函数:PsCreateSystemThread:结束线程(线程内自行调用)API函数:PsTerminateSystemThrea ...
- 《天书夜读:从汇编语言到windows内核编程》九 时间与定时器
1)使用如下自定义函数获取自系统启动后经历的毫秒数:KeQueryTimeIncrement.KeQueryTickCount void MyGetTickCount(PULONG msec) { L ...
- 《天书夜读:从汇编语言到windows内核编程》七 内核字符串与内存
1)驱动中的字符串使用如下结构: typedef struct _UNICODE_STRING{ USHORT Length; //字符串的长度(字节数) USHORT MaximumLength; ...
- 《天书夜读:从汇编语言到windows内核编程》三 练习反汇编C语言程序
1) Debug版本算法反汇编,现有如下3×3矩阵相乘的程序: #define SIZE 3 int MyFunction(int a[SIZE][SIZE],int b[SIZE][SIZE],in ...
- 《天书夜读:从汇编语言到windows内核编程》二 C语言的流程与处理
1) Debug与Release的区别:前者称调试版,后者称发行版.调试版基本不优化,而发行版会经过编译器的极致优化,往往与优化前的高级语言执行流程会大相径庭,但是实现的功能是等价的. 2) 如下fo ...
随机推荐
- Linux下将Apache(httpd)新增为系统服务及开机自启动
1. 查看一下/etc/init.d/下是否存在httpd这个服务 ls /etc/init.d/ | grep httpd 如果没有执行下一步 2.将自己安装目录下的apachect1复制到该目录下 ...
- ESLint可共享配置发布,团队自定义ESLint规则新鲜出炉
ESLint于2013年6月份推出,至今4个年头,最新版本v4.8.0.它是目前主流的用于Javascript和JSX代码规范检查的利器,很多大公司比如Airbnb和Google均有一套自己的Java ...
- JAVA提高三:反射总结
为前期学习过反射,再这里再次复习总结下:[转载请说明来源:http://www.cnblogs.com/pony1223/p/7659210.html ] 一.透彻分析反射的基础_Class类 Cla ...
- C# 多线程、异步线程、线程池相关知识
/* 线程池ThreadPool类会在需要时增减池中线程的线程数,直到最大的线程数.池中的最大线程数是可配置的. 在双核CPU中,默认设置为1023个工作线程和1000个I/O线程.也可以指定在创建线 ...
- wpf 制作必输项的*标记
直接引用帮助文档上的话吧,以免下次忘记! AdornedElementPlaceholder 类 .NET Framework 3.5 其他版本 此主题尚未评级 - 评价此主题 更新:20 ...
- Java IO编程全解(一)——Java的I/O演进之路
转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7419117.html JDK1.4之前的早期版本,Java对I/O的支持并不完善,开发人员在开发高性能I/O ...
- Installation of the JDK-9 on ubuntu(linux上安装jdk-9)
Description:Java SE 9 is the latest update to the Java Platform(General Availability on 21 September ...
- 根据Dockerfile创建docker dotnet coer 镜像
那我们先来看看Dockerfile文件内容,注意这个文件是没后缀名的. #依赖原始的镜像,因为我们是要创建dotnet coer镜像,所以我就用了官方给的镜像[microsoft/dotnet:lat ...
- 微服务架构中API网关的角色
[上海尚学堂的话]:本文主要讲述了Mashape的首席技术执行官Palladino对API网关的详细介绍,以及API网关在微服务中所起的作用,同时介绍了Mashape的一款开源API网关Kong. A ...
- OpenCV 学习笔记(模板匹配)
OpenCV 学习笔记(模板匹配) 模板匹配是在一幅图像中寻找一个特定目标的方法之一.这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否"相似",当相似度足够 ...