tiechui_lesson08_内存的分配和链表

主要是将链表结构的使用,在内核开发中使用起来比较方便的一种数据结构【LIST_ENTRY】。

一、内存的分配

主要是学习一些基本操作。现在推荐使用的动态分配函数【ExAllocatePoolWithTag

PVOID tempbuffer = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'xxaa');

 这样可以通过工具来找到标签对应的内存地址,我没找到视频里的【PoolMonEX.exe】,但是找到了【PoolMonX.exe】这个程序也可以用🤣

可以看到实际上的内容是和代码里边的顺序相反的,而且我之前申请的【‘aaxx’】非分页内存块,在驱动卸载后没有被释放掉(故意没free,驱动卸载不会自动释放),这样就只能重启操作系统了。

这个函数的第一参数确定这块内存的形式:

  • 【 PagedPool 】分页池—在需要时能够将页面换出的内存池
  • 【 NonPagedPool 】非分页池—永远不会换出页面,保证驻留在RAM里的内存池

 引用书中的描述,我觉非常好,虽然第一次看的时候没什么感觉😗😗😗

很显然,非分页池是一个“更好”的内存池,因为它不会导致页 面错误。在本书的后面我们会看到一些需要从非分页池分配的例子。 驱动程序要尽可能少地使用非分页池,除非必需。其他任何情况驱动 程序都应该使用分页池。POOL_TYPE这个枚举类型表示内存池的类型。 这个枚举类型包括很多内存池的“类型”,但是只有三种可以被驱动 程序使用:PagedPool、NonPagedPool和NonPagePoolNx(没有执行权限 的非分页池)。

 除此之外,在tiechuiDL视频中提到了更多的参数,在头文件【wdm.h】有这样的定义:

 【NonPagedPoolCacheAligned】传入这个参数,有一个对齐的概念,就是要求操作系统分配的这个内存是宽度对齐的。宽度的大小和寻址基线有关系,比方说32位系统就是四字节对齐,64位就是8字节对齐。

 【NonPagedPoolMustSucceed】传入这个参数,就要求操作系统必须分配成功一个非分页内存。

【NonPagedPoolCacheAlignedMustS】传入这个参数,是上边的超级组合:非分页+字节对齐+必须成功!

1.调试看细节

接下来是双机调试的骚操作,真的炫呀,tiechuiDL的双机调试连接怎么这么快!我就暂时用了WinDbg来看(VS2019调试Win7一直gg😒):

先搞一下入口断点:

bu ListEntry!driverentry

进入入口函数后使用Windbg打断点:

 之后使用g命令跳出,就和视频里边一样命中断点调试了...(还是好奇为啥tiechuiDL这么快???

【 bl 】查看已经打过的断点:

 【 dv /v 】查看变量状态

 【 db 0xfffffa80`1b178000 】查看tempbuffer内存地址的变化

 清零之后【RtlZeroMemory(tempbuffer, 0x1000);

 填充之后【RtlFillMemory(tempbuffer, 0x1000, 0xcc);

释放之后【ExFreePoolWithTag(tempbuffer, 'dcba');】中断一下再看,被别的进程用了...

调试就这样了,学到了咋通过命令看内存,然后介绍了两个运行时函数,用来内存比较:

  •     RtlCompareMemory
  •     RtlEqualMemory

RtlEqualMemory其实是宏定义而且用的比较多,主要是看一些区域相不相等。

2. Lookaside

最后还有一个概念是【Lookaside】

 Windows内存管理中使用了类似于容器的东西,叫做Lookaside对象,每次程序员申请内存都会从Lookaside里面申请,只有不足的时候,Lookaside才会向内存又一次申请内存空间,这样减少了频繁申请内存而导致的内存碎片

当Lookaside对象内部有大量没有使用的内存时候,它会自动让windows回收一部分内存,总之,Lookaside很智能。

 一般Lookaside用于以下情况:

  1. 程序员每次申请固定的内存大小
  2. 申请和回收内存的次数较多,很频繁

其实这个在官网的例子还出现挺频繁的,不过不在这里展开了,下次一定😉

二、链表 LIST_ENTRY

接下来是想通过链表来存储进程信息。

1.定义链表

链表结构的定义:

//
// 链表结构定义
//
typedef struct _MyStruct
{
	LIST_ENTRY list;			// 加入链表

	HANDLE pid;					// 进程PID
	PEPROCESS peprocesspbj;		// 进程对象
	BYTE processname[16];		// 进程名

}MyStruct,*PMyStruct;

新建一个LIST_ENTRY变量:

// 链表变量
LIST_ENTRY listhead = { 0 };

在使用的时候初始化这个链表变量 ,初始化的时候就是在填充结构中的值(和对象初始化好像😗):

InitializeListHead(&listhead);	// 初始化是填充节点的指针值
DbgPrint("%p %p %p\n", &listhead, listhead.Flink, listhead.Blink);

运行看一下:

 是一样的地址。

2.获取进程

创建一个进程通知回调:

//设置进程创建回调
PsSetCreateProcessNotifyRoutine(ProcessNotifyFun,FALSE);

PsSetCreateProcessNotifyRoutine 例程将驱动程序提供的回调例程添加到或从中删除该例程列表,每当创建或删除进程时调用该例程。

这个API在前边的程序中也有使用过,在这里发现了一个查看回调参数的方法,算是一个新收获:去查看具体的结构。

通过这个结构,新创建的回调函数参数就是这样定义:

这样就连成线了✅

 为了获取进程名需要声明一个半文档化(也就是不公开但是存在)函数【PsGetProcessImageFileName】,需要开头声明一下才能使用:

/** 向前声明 */
NTKERNELAPI
UCHAR* PsGetProcessImageFileName(__in PEPROCESS Process);

 回调函数具体的当前样子是:

//08 链表
VOID ProcessNotifyFun(HANDLE pid, HANDLE pid2, BOOLEAN bcareaf)
{
	UNREFERENCED_PARAMETER(pid);
	if (bcareaf)
	{
		DbgPrint("process create,PID is %d", pid2);

		//PEPROCESS tempep = PsGetCurrentProcess();	//这是获取自身进程ID 
		PEPROCESS tempep = NULL;
		PsLookupProcessByProcessId(pid2, &tempep);
		if (!tempep)
		{
			return;
		}

		PUCHAR	processname = PsGetProcessImageFileName(tempep);
		DbgPrint("process name  is %s", processname);

	}
	return;
}

 这里有个小插曲,如果使用PsGetCurrentProcess获取只能是自身的进程就是那个“浏览.exe”,换成PsLookupProcessByProcessId这个函数就可以通过pid进行搜索了。

PsLookupProcessByProcessId 例程接受进程的进程 ID,并返回指向进程的 EPROCESS 结构的引用指针。

3. 存储信息

如果CPU是多核的话,一个核在创建进程,另一个核也在创建进程,那么通过链表保存的时候,就会有冲突,需要通过上锁来进行保护。

上边获取到进程名之后,在回调函数中继续插入到链表中:

//08 链表
VOID ProcessNotifyFun(HANDLE pid, HANDLE pid2, BOOLEAN bcareaf)
{
	UNREFERENCED_PARAMETER(pid);
	if (bcareaf)
	{
		DbgPrint("process create,PID is %d", pid2);

		//PEPROCESS tempep = PsGetCurrentProcess();	//这是获取自身进程ID 
		PEPROCESS tempep = NULL;
		PsLookupProcessByProcessId(pid2, &tempep);
		if (!tempep)
		{
			return;
		}

		ObDereferenceObject(tempep);	// 解引用????

		PUCHAR	processname = PsGetProcessImageFileName(tempep);
		DbgPrint("process name  is %s", processname);

		//
		//保存进程信息
		//
		PMyStruct ptempptr = ExAllocatePoolWithTag(NonPagedPool, sizeof(MyStruct), 'qwer');
		if (ptempptr)
		{
			KIRQL oldirql = 0;
			//PLIST_ENTRY templist = NULL;

			RtlZeroMemory(ptempptr, sizeof(MyStruct));

			//
			//成员赋值
			//
			ptempptr->peprocesspbj = tempep;
			ptempptr->pid = pid2;
			RtlCopyMemory(ptempptr->processname, processname, strlen((const char *)processname));

			//通过CONTAINING_RECORD查找链表
			//templist = CONTAINING_RECORD(ptempptr, MyStruct, list);

			//上锁
			KeAcquireSpinLock(&spinlock, &oldirql);

			//插入信息
			InsertTailList(&listhead, &(ptempptr->list));

			//解锁
			KeReleaseSpinLock(&spinlock,oldirql);
		}

	}
	return;
}

 不太明白为什么要解引用

ObDereferenceObject(tempep);	// 解引用????

问一下AI:

ObReferenceObject 是 Windows 内核提供的一个宏,用于增加对象的引用计数。在 Windows 内核编程中,对象是内核中的一种基本数据结构,可能是进程、线程、文件、事件等等。由于内核对象被多个线程和进程共享,所以需要使用引用计数来管理对象的生命周期。

当一个线程或进程需要访问某个对象时,会调用 ObReferenceObject 宏来增加对象的引用计数。这样可以确保在对象正在被使用时,其他线程或进程不会将其释放掉。当对象不再被使用时,调用 ObDereferenceObject 来减少对象的引用计数,如果引用计数为 0,则表示对象可以被释放掉。

总之,ObReferenceObject 宏的主要作用是增加内核对象的引用计数,避免对象在使用期间被意外删除。

那什么是【引用计数】:

引用计数是一种常见的内存管理技术,它被广泛应用于操作系统、程序库和其他软件系统中。引用计数是一种跟踪对象在程序中被使用次数的方法,当一个对象被创建时,其引用计数初始化为 1。每当有代码引用该对象时,其引用计数加 1;当不再需要该对象时,其引用计数减 1。当引用计数变为 0 时,该对象就可以被释放,以便回收其占用的内存。

引用计数是一种内存管理技术,它的基本原理是跟踪每个对象被引用的次数。当对象的引用计数变为零时,系统就会自动回收该对象所占用的内存空间。

在使用引用计数的语言中,每个对象都有一个引用计数器,用于记录有多少个指针引用了该对象。当有新的指针指向该对象时,该对象的引用计数将会增加1,当指针不再指向该对象时,该对象的引用计数将会减少1。当引用计数器变为0时,表示该对象已经没有被任何指针引用了,此时系统将会回收该对象所占用的内存空间。这种方式可以避免内存泄漏和重复释放内存等问题,同时也可以提高程序运行效率。

ObReferenceObject 宏标记的对象需要再删除引用吗?

是的,使用 ObReferenceObject 宏标记的对象,在使用完成后需要调用ObDereferenceObject 函数进行引用计数的减少。

ObReferenceObject 宏用于增加内核对象的引用计数。当内核对象被打开或者传递给驱动程序处理时,必须先使用 ObReferenceObject 宏增加它的引用计数,这样可以确保该对象在使用期间不会被释放掉。

ObDereferenceObject 函数用于减少内核对象的引用计数。当一个内核对象不再被使用时,必须调用此函数将其引用计数减少。当引用计数为0时,内核对象的内存空间才会被释放。

需要注意的是,在使用 ObReferenceObject 宏和 ObDereferenceObject 函数时,需要遵循一定的规则,例如不要在调用 ObDereferenceObject 函数之前修改被引用对象的句柄等。否则可能会导致未定义的行为或者系统崩溃。

 原来这里调用的是用解引用,那应该是之前的函数有什么地方增加引用了,我又去MSDN搜了一下,发现是有解引用说明的:

如果对 PsLookupProcessByProcessId 的调用成功, PsLookupProcessByProcessID 会增加 Process 参数中返回对象的引用计数。 因此,当驱动程序使用 Process 参数完成时,驱动程序必须调用 ObDereferenceObject取消引用从 PsLookupProcessByProcessID 例程收到的 Process 参数。

看来是谁家的工具就得找谁家的说明书啊,解决完这个疑问之后贴下代码再看下效果:

//卸载函数
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
    ······

	//记得卸载
	PsSetCreateProcessNotifyRoutine(ProcessNotifyFun, TRUE);


	//
	// 只遍历不删除
	//	
	for (templist = listhead.Flink; templist!=&listhead; templist=templist->Flink)
	{
		DbgPrint("FOR");

		tempptr = CONTAINING_RECORD(templist, MyStruct, list);
		DbgPrint(" %d %p %s\n", tempptr->pid, tempptr->peprocesspbj, tempptr->processname);
	}

	//
	//	遍历链表并删除
	//
	PLIST_ENTRY templist = NULL;
	PMyStruct tempptr = NULL;
	while (listhead.Blink!=&listhead)
	{
		DbgPrint("WHILE");

		templist = RemoveTailList(&listhead);
		tempptr = CONTAINING_RECORD(templist, MyStruct, list);
		DbgPrint(" %d %p %s\n", tempptr->pid, tempptr->peprocesspbj, tempptr->processname);
		//DbgPrint("进程名 %s \n",  tempptr->processname);

		ExFreePoolWithTag(tempptr, 'qwer');
	}


}

三、小结 

 视频实现的是尾插法,还有其他的插入链表的方法。然后就是可以尝试和应用层联动,来开启数据存储和数据发送等。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值