Windows内核(驱动)编程中的线程问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows内核(驱动)编程中的线程问题相关的知识,希望对你有一定的参考价值。
看了《天书夜读》这本书初学内核编程,正在看《寒江独钓》,才看了几页。有这么一个疑问:在R3下多线程编程使用CreatThread等函数创建线程,管理线程。在R0下有相对应的内核API吗?还是内核中的线程不是这样简简单单地创建使用呢?粗略的百度了一下。没有找到相关答案,所以在此请教一下。
可以使用函数PsCreateSystemThread,用起来和ring3差不多,ring0编程更要注意线程同步问题,否则很容易BSOD(蓝屏死机) 参考技术A 其实Windows内核编程不但有用,而且常用。很多我们每天都使用的软件,就毫无疑问的使用了Windows内核编程的技术。最典型的就是实时监控的杀毒软件。此外还有防火墙、虚拟光驱、以及90%的驱动程序。这些程序的有一个共同的特点,他们的一部分组件,是作为Windows的一部分,能对 Windows上运行的所有的应用程序起作用。因此内核编程的应用,往往给传统软件带来更强的功能,实现技术上的飞跃。
举个例子。我们常常听说,对文件进行加密,可以使文档更加安全。对文件加密并不需要任何内核组件。我们可以写一个应用程序,读入文件,加密数据,然后重写为一个加密文件。解密也可以同样如此。
但是实际上这并不满足一般的用户需求。对一个公司的员工来说,那些“重要的文档”很可能就是每天工作所用的文件。想象一下,他必须要每天从服务器上下载加密的文件,然后用解密工具解密。然后用Office开始工作。工作完毕后,用加密工具加密,再上传,然后删除工作文档。且不说大部分时间文档是以解密的方式保存在硬盘上的不安全性,这个工作流程是可以接受的吗?没有人会接受的。
比较“人性化”的方式就是让Office可以直接打开已经加密的文档。保存的时候,直接就保存成加密的文档。硬盘上,这个文档始终是加密的。而且对合法的用户透明。对非法的用户,则只能看见密文,从而无法编辑也无法阅读。而且也不仅仅Office,还有AutoCAD、Visual Studio、Photoshop等等用户可能用于编辑机密文件的所有的工具。这是可以实现的吗?如果我们不能去修改Office和其他的工作软件。
这当然是可以实现的。既然我们编写Windows内核程序,当然可以让Windows的文件系统从硬盘读取文件的时候,对特定的进程进行特别的解密。等这些软件读取到数据的时候,它们读到的已经是正常的数据了。这个过程和实时扫描病毒的原理是一样的,使用一个文件过滤驱动程序。这就是读者可能已经听到过的文件透明加密技术。
在和《天书夜读:从汇编语言到Windows内核编程》一书同一系列的《寒江独钓——Windows内核编程与信息安全》(预计明年出版)中,对键盘过滤、硬盘过滤、文件过滤、网络过滤等安全相关的内核编程,都有详尽的讲解和例子。
内核编程的另一个特点是:这些代码运行在R0级。R0级别是最高特权级别。对CPU有完全控制的能力。这非常的适合一些安全软件,当然也适合做破坏的工作。因为内核程序有最高(也就是根)权限,这样的技术在安全领域(或者破坏领域)被称为rootkit技术。rootkit技术是当前安全领域最热门的技术之一。
许多病毒使用了rootkit技术。用来隐藏病毒文件,窃取密码、发送攻击包等等。rootkit病毒感染后极难清除,在感染前提前防范是最有效的办法。
Windows内核确实没有公开源代码。但是MS提供Windows内核程序的开发包:WDK。WDK实际上主要用于开发驱动程序。而驱动程序基本上都是内核程序。WDK提供的头文件以及部分源代码,实际上就是Windows内核的代码的一部分。有部分驱动程序(比如FAT32文件系统)的代码是完全公开的。我们也可以在这里看到Windows内核开发者的代码风格。同时,微软也提供了所有Windows版本的符号表在网上供研究者下载。并提供了功能无比强大的调试器WinDbg。有了它们,你就可以轻松的调试Windows内核了。无论是你自己写的代码的部分,还是Windows内核开发者们编写的部分。虽然看到的是汇编语言,但是函数名和全局变量名都是存在的。而且,所有的这些(WDK、WinDBG,符号表)都是免费的。
回炉重造之重读Windows核心编程-006-线程
线程也是有两部分组成的:
- 线程的内核对象,操作系统用来管理线程和统计线程信息的地方。
- 线程堆栈,用于维护现场在执行代码的时候用到的所有函数参数和局部变量。
进程是线程的容器,如果进程中有一个以上的线程,这些线程将共享进程的地址空间,操作空间中的数据,执行相同的代码,对相同的数据操作,甚至内核对象句柄(因为它是依托进程而不是线程存在的)。
所以进程使用的系统资源比线程多的多,线程只需要一个内核对象和一个堆栈。既然线程比进程需要的开销少,因此始终都应该设法用增加线程来解决编程问题。当然这也不是一成不变的,应该懂得权衡利弊。
何时运行线程
前面的章节已经提到过,当进程被初始化时,系统就要为进程创建一个主线程,用以和CC++运行期库的启动代码一起运行,然后进入入口点函数,然后运行至入口点函数返回并CC++运行期库的启动代码调用ExitProcess为止。
每个计算机都有一个功能强大的资源,CPU。让它闲置起来是没有道理的,应该让它处于繁忙之中,执行各种各样的工作:
- 打开Windows2K开始就自带的内容索引服务程序,它能创建一个低优先级的线程,以定期打开磁盘上的文件内容并为之做索引。这样可以大大加快查找文件的效率。
- 还可以使用磁盘碎片整理软件,使用低优先级线程运行,在系统空项期整理文件碎片。
- 自动编译源代码文件、实时查看错误和警告信息。
- 电子表格应用程序能够在后台运算。
- 文字处理程序能够执行重新分页、拼写、后台打印和语法检查。
- 文件内容后台拷贝到其他介质中。
- Web浏览器和其他的服务器通信。
最重要的是,多线程可以简化用户界面。设计一个拥有多线程的应用程序,可以扩大应用程序的功能。每一个线程都被分配了一个cpu,因此如果计算机有多个CPU,就可以让所有cpu都处于繁忙状态。
不能创建线程的情况
多线程并发至少有一个问题:代码的重入,数据访问的冲突。
一般来说一个应用程序有一个用户界面线程用于创建所有窗口,以及一个GetMessage循环。其他的线程都是工作线程,比用户界面线程优先级要低。
编写第一个线程函数
主线程有对应的入口点,那么线程函数也有个入口函数,作为开始执行第一条代码的地方。和主线程一样,执行代码到结束就释放资源,线程内核对象的引用计数递减。
- 和主线程不同,线程函数可以是任何名字。
- 线程函数也可以传递参数。
- 线程函数必须返回一个值,作为退出代码。
- 线程函数应该尽可能地使用函数参数和局部变量。如果访问静态和全局的数据,就会有同步的问题。
CreateThread函数
调用下面的WindowsAPI就可以创建一个线程:
上面的函数被调用的时候,系统创建一个内核对象,用来管理与线程相关的数据结构,和进程是类似的。系统从进程的地址空间中分配内存,给线程的堆栈使用。新线程的运行环境与创建线程的环境相同。这样使得同一个进程的多个线程之间的通信时相对方便的。
注意:CreateThread函数是Windows用来创建线程的函数。可是如果你在写CC++代码的话,就不应该使用它了,而是使用VisualC++的运行时函数_beginthreadex。如果不是VisualC++编译器,你的编译器供应商会提供CreateThread的替代函数。反正不能用CreateThread。
psa参数是指向SECURITY_ATTRIBUTES结果的指针,一般使用默认值。具体的用法请看第三章。
cbStack参数用于设置线程堆栈大小,可以用链接程序的/STACK:[reserve][.commit]开关控制这个值。reserve参数用来设定系统为线程堆栈保留的地址控件量,默认1MB;commit参数用于设定“应该承诺用于堆栈保留区的物理存储器的容量”,默认值是1页。这个参数即便传递了值,函数仍然会检查链接器已经设置的值,哪个大用哪个。如果把0传进来,就使用Stack开关中设置的值。无论如何,这个值应该有个上限,否则如果存在递归则有可能消耗完所有的资源。
pfnStartAddr和pvParam分别是入口地址和参数。
fdwCreate参数可以是0或者CREATE_SUSPEND。前者表示线程创建后立即调度,后者表示创建线程后先暂停运行。然而后者并不常用。
pdwThreadID参数用来存放线程ID。
终止线程的运行
- 线程函数返回(最好的办法)如果线程可以返回,就可以确保下列事项的实现:
- 在现场函数中创建的所有C++对象都能通过它们的析构函数销毁。
- 操作系统正确地释放线程堆栈使用的内存。
- 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
- 系统递减线程内核对象的引用计数。
- 调用ExitThread函数,线程自行撤销(最好不用)。如果使用,C++资源将不会被回收。实在要用也是用VisualC++提供的_endthreadex,或者你的编译器供应商提供的替代函数。
- 使用TerminateThread函数(避免使用)。它能撤销线程,线程的内核对象的引用计数也会递减。
- TerminateThread函数是异步运行的,想知道线程终止运行,就要调用WaitForSingleObject或者类似的函数。(设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤销的通知,线程不能被正确地清除,并不能防止自己被撤销)
- 使用这个函数,系统是不回收这个线程的堆栈资源的。
- 线程终止运行的话,DLL通常接受清楚通知。而使用这个函数,DLL就不接受通知了,这就阻挡了适当的清除。
- 包含线程的进程终止运行(避免使用)
- 就像对剩余的每个线程调用TerminateThread一样。显然这意味着正确的应用程序清除没有发生:C++对象的析构函数没有被调用, 数据没有转至磁盘等。
线程如果终止
- 线程拥有的用户对象全被释放。
- 线程的退出代码从STILL_ACTIVE传递给ExitThread或者TerminateThread。
- 线程的内核对象变为已通知。
- 如果是最后一个线程,系统也将进程视为已经终止运行。
- 线程内核对象的引用计数递减1。
一旦线程不再运行,系统中就没有别的线程能够处理这个线程的句柄,而别的线程可以调用GetExitCodeThread来检查hThread标识的线程是否时间终止运行。如果是这样,就确定它的退出代码:
如果尚未终止运行,函数就返回STILL_ACTIVE标识符(定义为0x103),放进pdwExitCode。如果成功就返回TRUE。
线程的其他性质
SP和IP分别是参数和线程函数的入口!
如果调用CreateThread创建了一个内核对象,下面的事情就会发生:
- 这个对象的引用计数是2。
- 线程的内核对象的其他属性也被初始化,引用计数被设置为1,退出代码设置为STILL_ACTIVE(0x103),该对象的已通知设置为未通知状态。
一旦内核对象创建完成:
- 系统就从进程的地址空间中,给线程堆栈分配内存。
- 系统先后把参数和入口地址写入堆栈。
每个线程都有自己的一套CPU寄存器,成为线程的上下文,用以反映线程上次运行时寄存器的状态。这些寄存器保存在CONTEXT结构里,这个结构本身则保存在线程的内核对象中。
ESP和EIP是线程上下文中两个最重要的寄存器。
以上是关于Windows内核(驱动)编程中的线程问题的主要内容,如果未能解决你的问题,请参考以下文章