Windows核心编程022.pdf

上传人:asd****56 文档编号:70321916 上传时间:2023-01-19 格式:PDF 页数:50 大小:2.85MB
返回 下载 相关 举报
Windows核心编程022.pdf_第1页
第1页 / 共50页
Windows核心编程022.pdf_第2页
第2页 / 共50页
点击查看更多>>
资源描述

《Windows核心编程022.pdf》由会员分享,可在线阅读,更多相关《Windows核心编程022.pdf(50页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。

1、下载第2 2章插入D L L和挂接A P I在Microsoft Wi n d o w s中,每个进程都有它自己的私有地址空间。当使用指针来引用内存时,指针的值将引用你自己进程的地址空间中的一个内存地址。你的进程不能创建一个其引用属于另一个进程的内存指针。因此,如果你的进程存在一个错误,改写了一个随机地址上的内存,那么这个错误不会影响另一个进程使用的内存。Windows 98在Windows 98下运行的各个进程共享2 GB的地址空间,该地址空间从0 x 8 0 0 0 0 0 0 0至0 x F F F F F F F F。只有内存映像文件和系统组件才能映射到这个区域。详细说明参见第1 3、

2、1 4章和第1 7章的内容。独立的地址空间对于编程人员和用户来说都是非常有利的。对于编程人员来说,系统更容易捕获随意的内存读取和写入操作。对于用户来说,操作系统将变得更加健壮,因为一个应用程序无法破坏另一个进程或操作系统的运行。当然,操作系统的这个健壮特性是要付出代价的,因为要编写能够与其他进程进行通信,或者能够对其他进程进行操作的应用程序将要困难得多。有些情况下,必须打破进程的界限,访问另一个进程的地址空间,这些情况包括:当你想要为另一个进程创建的窗口建立子类时。当你需要调试帮助时(例如,当你需要确定另一个进程正在使用哪个 D L L时)。当你想要挂接其他进程时。本章将介绍若干种方法,可以用

3、来将D L L插入到另一个进程的地址空间中。一旦你的 D L L进入另一个进程的地址空间,就可以对另一个进程为所欲为。这一定会使你非常害怕,因此,究竟应该怎样做,要三思而后行。22.1 插入D L L:一个例子假设你想为由另一个进程创建的窗口建立一个子类。你可能记得,建立子类就能够改变窗口的行为特性。若要建立子类,只需要调用S e t Wi n d o w L o n g P t r函数,改变窗口的内存块中的窗口过程地址,指向一个新的(你自己的)W n d P r o c。Platform SDK文档说,应用程序不能为另一个进程创建的窗口建立子类。这并不完全正确。为另一个进程的窗口建立子类的关

4、键问题与进程地址空间的边界有关。当调用下面所示的S e t Wi n d o w s L o n g P t r函数,建立一个窗口的子类时,你告诉系统,发送到或者显示在h w n d设定的窗口中的所有消息都应该送往M y S u b c l a s s P r o c,而不是送往窗口的正常窗口过程:换句话说,当系统需要将消息发送到指定窗口的 W n d P r o c时,要查看它的地址,然后直接调用W n d P r o c。在本例中,系统发现M y S u b c l a s s P r o c函数的地址与窗口相关联,因此就直接调用M y S u b c l a s s P r o c函数。

5、为另一个进程创建的窗口建立子类时遇到的问题是,建立子类的过程位于另一个地址空间中。图2 2-1显示了一个简化了的图形,说明窗口过程是如何接受消息的。进程 A正在运行,并且已经创建了一个窗口。文件 U s e r 3 2.d l l被映射到进程A的地址空间中。对U s e r 3 2.d l l文件的映射是为了接收和发送在进程 A中运行的任何线程创建的任何窗口中发送和显示的消息。当U s e r 3 2.d l l的映像发现一个消息时,它首先要确定窗口的W n d P r o c的地址,然后调用该地址,传递窗口的句柄、消息和w P a r a m和l P a r a m值。当W n d P r

6、o c处理该消息后,U s e r 3 2.d l l便循环运行,并等待另一个窗口消息被处理。图22-1 进程B中的线程试图为进程A中的线程创建的窗口建立子类现在假设你的进程是进程B,你想为进程A中的线程创建的窗口建立子类。你在进程B中的代码必须首先确定你想要建立子类的窗口的句柄。这个操作使用的方法很多。图 2 2-1显示的例子只是调用F i n d Wi n d o w函数来获得需要的窗口。接着,进程B中的线程调用S e t Wi n d o w L o n g P t r函数,试图改变窗口的W n d P r o c的地址。请注意我说的“试图”二字。这个函数调用并不进行什么操作,它只是返回

7、N U L L。S e t Wi n d o w L o n g P t r函数中的代码要查看是否有一个进程正在试图改变另一个进程创建的窗口的W n d P r o c地址,然后将忽略这个函数的调用。如果S e t Wi n d o w L o n g P t r函数能够改变窗口的 W n d P r o c,那将出现什么情况呢?系统将把M y S u b c l a s s P r o c的地址与特定的窗口关联起来。然后,当有一条消息被发送到这个窗口中时,进程A中的U s e r 3 2代码将检索该消息,获得M y S u b c l a s s P r o c的地址,并试图调用这个地址。但

8、是,这时可能遇到一个大问题。M y S u b c l a s s P r o c将位于进程B的地址空间中,而进程 A却是个活动进程。显然,如果 U s e r 3 2想要调用该地址,它就要调用进程 A的地址空间中的一个地址,这就可能造成内存访问的违规。为了避免这个问题的产生,应该让系统知道 M y S u b c l a s s P r o c是在进程B的地址空间中,然后,在调用子类的过程之前,让系统执行一次上下文转换。M i c r o s o f t没有实现这个辅助函数功能,原因是:应用程序很少需要为其他进程的线程创建的窗口建立子类。大多数应用程序只是为它们自己创建的窗口建立子类,Wi

9、n d o w s的内存结构并不阻止这种创建操作。切换活动进程需要占用许多C P U时间。进程B中的线程必须执行M y S u b c l a s s P r o c中的代码。系统究竟应该使用哪个线程呢?是现有的线程,还是新线程呢?U s e r 3 2.d l l怎样才能说明与窗口相关的地址是用于另一个进程中的过程,还是用于同一个516计计第四部分动态链接库下载进程A进程B进程中的过程呢?由于对这个问题的解决并没有什么万全之策,因此 M i c r o s o f t决定不让 S e t Wi n d o w s L o n g P t r改变另一个进程创建的窗口过程。不过仍然可以为另一个进

10、程创建的窗口建立子类只需要用另一种方法来进行这项操作。这并不是建立子类的问题,而是进程的地址空间边界的问题。如果能将你的子类过程的代码放入进程A的地址空间,就可以方便地调用 S e t Wi n d o w L o n g P t r函数,将进程A的地址传递给M y S u b c l a s s P r o c函数。我将这个方法称为将D L L“插入”进程的地址空间。有若干种方法可以用来进行这项操作。下面将逐个介绍它们。22.2 使用注册表来插入D L L如果你曾经多少使用过Wi n d o w s操作系统,你肯定熟悉注册表的情况。整个系统的配置都是在注册表中维护的,可以通过调整它的设置来改

11、变系统的行为特性。将要介绍的项目是在下面的关键字中:Windows 98Windows 98将忽略注册表的这个关键字。在Windows 98下,无法使用该方法插入D L L。图2 2-2显示了使用Registry Editor(注册表编辑器)时该关键字中的各个项目的形式。该关键字的值包含一个D L L文件名或者一组D L L文件名(用空格或逗号隔开)。由于空格用来将文件名隔开,因此必须避免使用包含空格的文件名。列出的第一个D L L文件名可以包含一个路径,但是包含路径的其他D L L均被忽略。由于这个原因,最好将你的 D L L放入Wi n d o w s的系统目录中,这样就不必设定路径。在窗

12、口中,我将该值设置为单个 D L L路径名C:M y L i b.d l l。图22-2 注册表窗口当重新启动计算机及 Wi n d o w s进行初始化时,系统将保存这个关键字的值。然后,当U s e r 3 2.d l l库被映射到进程中时,它将接收到一个 D L L _ P R O C E S S _ AT TA C H通知。当这个通知被处理时,U s e r 3 2.d l l便检索保存的这个关键字中的值,并且为字符串中指定的每个 D L L调用L o a d L i b r a r y函数。当每个库被加载时,便调用与该库相关的D l l M a i n函数,其f d w R e a

13、s o n的值是D L L _ P R O C E S S _ AT TA C H,这样,每个库就能够对自己进行初始化。由于插入的 D L L在进程的寿命期中早早地就进行了加载,因此在调用函数时应该格外小心。调用 k e r n e l 3 2.d l l中的函数时应该不会出现什么问题,不过调用其他 D L L中的函数时就可能产生一些问题。U s e r 3 2.d l l并第 22章 插入DLL 和挂接 API计计517下载不检查每个库是否已经加载成功,或者初始化是否取得成功。在插入D L L时所用的所有方法中,这是最容易的一种方法。要做的工作只是将一个值添加到一个已经存在的注册表关键字中。

14、不过这种方法也有它的某些不足:由于系统在初始化时要读取这个关键字的值,因此在修改这个值后必须重新启动你的计算机即使退出后再登录,也不行。当然,如果从这个关键字的值中删除 D L L,那么在计算机重新启动之前,系统不会停止对库的映射操作。你的D L L只会映射到使用U s e r 3 2.d l l的进程中。所有基于G U I的应用程序均使用U s e r 3 2.d l l,不过大多数基于C U I的应用程序并不使用它。因此,如果需要将 D L L插入编译器或链接程序,这种方法将不起作用。你的D L L将被映射到每个基于G U I的应用程序中,但是必须将你的库插入一个或几个进程中。你的D L

15、L映射到的进程越多,“容器”进程崩溃的可能性就越大。毕竟在这些进程中运行的线程是在执行你的代码。如果你的代码进入一个无限循环,或者访问的内存不正确,就会影响代码运行时所在进程的行为特性和健壮性。因此,最好将你的库插入尽可能少的进程中。你的D L L将被映射到每个基于G U I的应用程序中。这与上面的问题相类似。在理想的情况下,你的D L L只应该映射到需要的进程中,同时,它应该以尽可能少的时间映射到这些进程中。假设在用户调用你的应用程序时你想要建立 Wo r d P a d的主窗口的子类。在用户调用你的应用程序之前,你的 D L L不必映射到Wo r d P a d的地址空间中。如果用户后来决

16、定终止你的应用程序的运行,那么你必须撤消 Wo r d P a d的主窗口。在这种情况下,你的D L L将不再需要被插入Wo r d P a d的地址空间。最好是仅在必要时保持D L L的插入状态。22.3 使用Wi n d o w s挂钩来插入D L L可以使用挂钩将D L L插入进程的地址空间。为了使挂钩能够像它们在 1 6位Wi n d o w s中那样工作,M i c r o s o f t不得不设计了一种方法,使得D L L能够插入另一个进程的地址空间中。下面让我们来看一个例子。进程A(类似Microsoft Spy+的一个实用程序)安装了一个挂钩W N _ G E T M E S

17、S A G E,以便查看系统中的各个窗口处理的消息。该挂钩是通过调用下面的S e t Wi n d o w s H o o k E x函数来安装的:第一个参数W H _ G E T M E S S A G E用于指明要安装的挂钩的类型。第二个参数G e t M s g P r o c用于指明窗口准备处理一个消息时系统应该调用的函数的地址(在你的地址空间中)。第三个参数h i n s t D l l用于指明包含G e t M s g P r o c函数的D L L。在Wi n d o w s中,D L L的h i n s t D l l的值用于标识D L L被映射到的进程的地址空间中的虚拟内存地

18、址。最后一个参数 0用于指明要挂接的线程。对于一个线程来说,它可以调用 S e t Wi n d o w s H o o k E x函数,传递系统中的另一个线程的 I D。通过为这个参数传递0,就告诉系统说,我们想要挂接系统中的所有G U I线程。现在让我们来看一看将会发生什么情况:1)进程B中的一个线程准备将一条消息发送到一个窗口。2)系统查看该线程上是否已经安装了W H _ G E T M E S S A G E挂钩。3)系统查看包含G e t M s g P r o c函数的D L L是否被映射到进程B的地址空间中。4)如果该D L L尚未被映射,系统将强制该 D L L映射到进程B的地

19、址空间,并且将进程 B中的D L L映像的自动跟踪计数递增1。518计计第四部分动态链接库下载5)当D L L的h i n s t D l l用于进程B时,系统查看该函数,并检查该 D L L的h i n s t D l l是否与它用于进程A时所处的位置相同。如果两个h i n s t D l l是在相同的位置上,那么 G e t M s g P r o c函数的内存地址在两个进程的地址空间中的位置也是相同的。在这种情况下,系统只需要调用进程 A的地址空间中的G e t M s g P r o c函数即可。如果h i n s t D l l的位置不同,那么系统必须确定进程B的地址空间中G e

20、t M s g P r o c函数的虚拟内存地址。这个地址可以使用下面的公式来确定:将GetMsgProc A的地址减去hinstDll A的地址,就可以得到 G e t M s g P r o c函数的地址位移(以字节为计量单位)。将这个位移与hinstDll B的地址相加,就得出G e t M s g P r o c函数在用于进程B的地址空间中该D L L的映像时它的位置。6)系统将进程B中的D L L映像的自动跟踪计数递增1。7)系统调用进程B的地址空间中的G e t M s g P r o c函数。8)当G e t M s g P r o c函数返回时,系统将进程B中的D L L映像的

21、自动跟踪计数递减1。注意,当系统插入或者映射包含挂钩过滤器函数的 D L L时,整个D L L均被映射,而不只是挂钩过滤器函数被映射。这意味着D L L中包含的任何一个函数或所有函数现在都存在,并且可以从进程B的环境下运行的线程中调用。若要为另一个进程中的线程创建的窗口建立子类,首先可以在创建该窗口的挂钩上设置一个W H _ G E T M E S S A G E挂钩,然后,当G e t M s g P r o c函数被调用时,调用S e t Wi n d o w L o n g P t r函数来建立窗口的子类。当然,子类的过程必须与G e t M s g P r o c函数位于同一个D L

22、L中。与插入D L L的注册表方法不同,这个方法允许你在另一个进程的地址空间中不再需要 D L L时删除该D L L的映像,方法是调用下面的函数:当一个线程调用U n h o o k Wi n d o w s H o o k E x函数时,系统将遍历它必须将 D L L插入到的各个进程的内部列表,并且对 D L L的自动跟踪计数进行递减。当自动跟踪计数递减为 0时,D L L就自动从进程的地址空间中被删除。应该记得,就在系统调用 G e t M s g P r o c函数之前,它对D L L的自动跟踪计数进行了递增(见上面的第6个步骤)。这可以防止产生内存访问违规。如果该自动跟踪计数没有递增,

23、那么当进程B的线程试图执行G e t M s g P r o c函数中的代码时,系统中运行的另一个线程就可以调用U n l o o k Wi n d o w s H o o k E x函数。这一切意味着不能撤消该窗口的子类并且立即撤消该挂钩。该挂钩必须在该子类的寿命期内保持有效状态。桌面项目位置保存器实用程序清单2 2-2中列出的D I P S.e x e应用程序使用窗口挂钩将一个D L L插入E x p l o r e r.e x e的地址空间。该应用程序和D L L的源代码和资源文件均位于本书所附光盘的2 2-D I P S和2 2-D I P S l i b目录下。我基本上将我的计算机用

24、于与商务有关的操作,我发现1152 x 864的屏幕分辨率最适合我。但是在计算机上玩游戏时,大多数游戏设计时使用的分辨率是 640 x 480。因此,当我想要玩游戏时,我打开控制面板,使用 D i s p l a y小应用程序,将分辨率改为 640 x 480。不玩游戏时,我又使用D i s p l a y小应用程序将分辨率重新改为1152 x 864。使用这种方法在运行过程中改变显示器的分辨率是非常麻烦的,但是它是 Wi n d o w s的一个受欢迎的特性。不过我忽略了改变显示器分辨率时的一个问题,那就是桌面图标无法记住它原第 22章 插入DLL 和挂接 API计计519下载来的位置。我的

25、桌面上有若干个图标,可以立即访问各个应用程序,并可打开经常使用的文件。当改变显示器的分辨率时,桌面窗口便改变其大小,我的图标重新安排其位置,使我无法找到我要的东西。然后,当我将显示器的分辨率改为原来的样子时,我的所有图标又重新安排其位置,采用一种新的顺序。为了解决这个问题,我不得不用手工将桌面上的所有图标重新改为我喜欢的样子。真是烦死人了。我非常讨厌用手工方式改变这些图标的位置,因此创建了桌面项目位置保存器实用程序 D I P S。D I P S包含一个很小的可执行文件和一个很小的 D L L。当运行这个可执行文件时,就会出现图2 2-3所示的消息框。这个消息框显示了该实用程序如何使用的情况。

26、当你将 S作为命令行参数传递给D I P S时,它就创建下面这个注册表子关键字,并且给桌面窗口上的每个项目添加一个值:每个项目都有一个与它一起保存的位置值。当改变屏幕分辨率以便玩游戏之前,运行 D I P SS。当玩完游戏后,将屏幕的分辨率改为原来的状态,并且运行 DIPS R。这使得D I P S打开注册表子关键字,对于桌面上与注册表中保存的项目相匹配的每个项目来说,当运行 DIPS S时,项目的位置将被重新设置为原来的值。最初你可能认为,D I P S的实现是非常容易的,毕竟你只需要获得桌面的 L i s t Vi e w控件的窗口句柄,为它发送枚举各个项目的消息,获得它们的位置,然后将这

27、些信息保存在注册表中就行了。但是,如果进行具体操作,就会发现事情并不那么简单。问题是大多数常用的控件窗口消息,比如LV M _ G E T I T E M和LV M _ G E T I T E M P O S I T I O N,不能跨越进程的边界来运行。原因是,LV M _ G E T I T E M消息要求你为消息的L PA R A M参数传递一个LV _ I T E M数据结构的地址。由于这个内存地址只对发送消息的进程有意义,接收消息的进程无法保证能够使用它。因此,为了使 D I P S能够按原定的要求来工作,必须将代码插入E x p l o r e r.e x e,以便将LV M _

28、G E T I T E M和LV M _ G E T I T E M P O S I T I O N消息成功地发送到桌面的L i s t Vi e w控件中。注意可以跨越进程的边界发送窗口消息,以便与内置控件(如按钮、编辑框、静态框、组合框和列表框等)进行交互操作,但是,对一些新的常用控件不能这样做。例如,可以将一个L B _ G E T T E X T消息发送给另一个进程中的线程创建的列表框控件,其中的L PA R A M参数指向发送方进程中的一个字符串缓冲区。这是可行的,因为M i c r o s o f t专门查看L B _ G E T T E X T消息是否已经发送。如果已经发送,操作

29、系统将在内部创建内存映射文件,并且跨越进程的边界来拷贝该字符串数据。为什么M i c r o s o f t决定对内置控件这样做而不对新的常用控件这样做呢?答案是为了实现可移植性。在1 6位Wi n d o w s中,所有应用程序都在单个地址空间中运行,一个应用程序可以将一个 L B _ G E T T E X T消息发送给另一个应用程序创建的窗口。为了使这些1 6位应用程序能够非常容易地移植到 Wi n 3 2中,M i c r o s o f t采取了一些辅助措施来确保跨越进程的消息发送仍然能够进行。但是 1 6位Wi n d o w s中不存在新的常用控件,因此不存在移植问题,所以M i

30、 c r o s o f t没有为常用控件采取辅助的措施。当运行D I P S.e x e时,它首先得到桌面的L i s t Vi e w控件的窗口句柄:520计计第四部分动态链接库下载图22-3 桌面项目位置保存器实用工具窗口该代码首先寻找一个窗口,它的类是P r o g M a n。尽管Program Manager(程序管理器)应用程序正在运行,新外壳程序仍然要创建这个类的一个窗口,以便与较老版本的 Wi n d o w s设计的应用程序实现向后兼容。该P r o g M a n窗口拥有单个子窗口,它的类是S H E L L D L L _ D e f Vi e w。这个子窗口也拥有单个

31、子窗口,它的类是S y s L i s t Vi e w 3 2。该S y s L i s t Vi e w 3 2窗口是桌面的L i s t Vi e w控件窗口(顺便说一下,我是使用S p y+获得所有这些信息的)。一旦拥有L i s t Vi e w的窗口句柄,通过调用G e t Wi n d o w T h r e a d P r o c e s s I d函数,就能够确定创建窗口的线程的 I D。将这个 I D传递给 S e t D I P S H o o k函数(在 D I P S L i b.c p p中实现)。S e t D I P S H o o k负责在该线程上安装一个W

32、H _ G E T M E S S A G E挂钩,然后调用下面的函数,以强制Windows Explorer的线程醒来:由于已经在该线程上安装了一个 W H _ G E T M E S S A G E挂钩,因此操作系统能够自动将D I P S L i b.d l l文件插入E x p l o r e r的地址空间,并且调用G e t M s g P r o c函数。该函数首先查看它是否是初次被调用,如果是,那么它就创建一个隐藏的窗口,其标题是“Richter DIPS。”请记住,E x p l o r e r的线程正在创建这个隐藏窗口。当它进行这项操作时,D I P S.e x e线程从S

33、e t D I P S H o o k返回,然后调用下面的函数:这次函数调用将使线程进入睡眠状态,直到队列中显示一条消息为止。尽管 D I P S.e x e并没有创建它自己的任何窗口,但是它仍然有一个消息队列,同时,只有调用 P o s t T h r e a d M e s s a g e函数,才能将消息放入该队列。如果观察D I P S L i b.c p p的G e t M s g P r o c函数中的代码,将会发现,在对C r e a t e D i a l o g的调用的后面,紧接着就是对 P o s t T h r e a d M e s s a g e函数的调用,该函数将使S

34、 I P S.e x e线程再次醒来。线程的I D保存在S e t D I S P H o o k函数的共享变量中。注意,我将线程的消息队列用于线程的同步。这样做绝对没有什么错误,并且有时能够比使用各种内核对象(如互斥对象、信标和事件等)更容易实现线程的同步。Wi n d o w s拥有丰富的A P I,应该充分利用它们。当D I P S可执行文件中的线程醒来时,它知道服务器对话框已经创建,并调用 F i n d Wi n d o w函数来获得窗口的句柄。这时可以使用窗口消息在客户机(D I P S应用程序)与服务器(隐藏的对话框)之间进行通信。由于在Windows Explorer的进程环境

35、中运行的一个线程创建了这个对话框,因此在使用Windows Explorer时将会遇到一些限制。若要让对话框保存或者还原桌面图标的位置,只需要发送一条消息:我对该对话框的过程进行了编码,以便查找W M _ A P P消息。当它收到该消息时,W PA R A M参数就会指明被操作的L i s t Vi e w控件的句柄,而L PA R A M参数则是个布尔值,用于指明当前项目的位置是否应该保存在注册表中,或者指明是否应该根据从注册表中读取的保存信息来改变项目的位置。由于我使用S e n d M e s s a g e而不是P o s t m e s s a g e,因此该函数直到运行完成才返回。

36、如果愿意的话,可以将消息添加给对话框的过程,使该程序能够进一步控制 E x p l o r e r的进程。当完成与对话框的通信时,并且(因此)想要终止服务器的运行时,我发送了一个 W M _ C L O S E消息,告诉对话框将自己关闭。第 22章 插入DLL 和挂接 API计计521下载最后,就在D I P S应用程序终止运行之前,它再次调用 S e t D I P S H o o k函数,但是传递0作为线程的I D。0是个标记值,用于告诉函数撤消 W N _ G E T M E S S A G E挂钩。当该挂钩被卸载时,操作系统自动从E x p l o r e r的地址空间中卸载D I P

37、 S L i b.d l l文件。对话框首先撤消,然后卸载挂钩,这一点很重要,否则对话框接收到的下一个消息将会导致 E x p l o r e r的线程引发一次访问违规。如果发生这种情况,操作系统就会终止 E x p l o r e r的运行。当使用D L L的插入操作时,必须非常小心。清单22-1 DIPS实用程序522计计第四部分动态链接库下载第 22章 插入DLL 和挂接 API计计523下载524计计第四部分动态链接库下载第 22章 插入DLL 和挂接 API计计525下载526计计第四部分动态链接库下载第 22章 插入DLL 和挂接 API计计527下载528计计第四部分动态链接库下

38、载第 22章 插入DLL 和挂接 API计计529下载530计计第四部分动态链接库下载22.4 使用远程线程来插入D L L插入D L L的第三种方法是使用远程线程。这种方法具有更大的灵活性。它要求你懂得若干个Wi n d o w s特性、如进程、线程、线程同步、虚拟内存管理、D L L和U n i c o d e等(如果对这些特性不清楚,请参阅本书中的有关章节)。Wi n d o w s的大多数函数允许进程只对自己进行操作。这是很好的一个特性,因为它能够防止一个进程破坏另一个进程的运行。但是,有些函数却允许一个进程对另一个进程进行操作。这些函数大部分最初是为调试程序和其他工具设计的。不过任何

39、函数都可以调用这些函数。这个D L L插入方法基本上要求目标进程中的线程调用 L o a d L i b r a r y函数来加载必要的D L L。由于除了自己进程中的线程外,我们无法方便地控制其他进程中的线程,因此这种解决方案要求我们在目标进程中创建一个新线程。由于是自己创建这个线程,因此我们能够控制它执行什么代码。幸好,Wi n d o w s提供了一个称为C r e a t e R e m o t e T h r e a d的函数,使我们能够非常容易地在另一个进程中创建线程:C r e a t e R e m o t e T h r e a d与C r e a t e T h r e a

40、 d很相似,差别在于它增加了一个参数h P r o c e s s。该参数指明拥有新创建线程的进程。参数 p f n S t a r t A d d r指明线程函数的内存地址。当然,该内存地址与远程进程是相关的。线程函数的代码不能位于你自己进程的地址空间中。注意 在Windows 2000中,更常用的函数CreateThread是在内部以下面的形式来实现的:Windows 98在Windows 98中,C r e a t e R e m o t e T h r e a d函数不存在有用的实现代码,它只是返回N U L L。调用G e t L a s t E r r o r函数将返回E R R

41、O R _ C A L L _ N O T _ I M P L E M E N T E D(C r e a t e T h r e a d函数包含用于在调用进程中创建线程的完整的实现代码)。由于C r e a t e R e m o t e T h r e a d没有实现,因此,在 Windows 98下,不能使用本方法来插入D L L。好了,现在你已经知道如何在另一个进程中创建线程了,但是,如何才能让该线程加载我们的D L L呢?答案很简单,那就是需要该线程调用L o a d L i b r a r y函数:如果观察Wi n B a s e.h文件中的L o a d L i b r a r

42、y函数,你将会发现下面的代码:第 22章 插入DLL 和挂接 API计计531下载实际上有两个L o a d L i b r a r y函数,即L o a d L i b r a r y A和L o a d L i b r a r y W。这两个函数之间存在的唯一差别是,传递给函数的参数类型不同。如果将库的文件名作为 A N S I字符串来存储,那么必须调用L o a d L i b r a r y A(A是指A N S I)。如果将文件名作为U n i c o d e字符串来存储,那么必须调用L o a d L i b r a r y W(W是指通配符)。不存在单个 L o a d L i

43、b r a r y的情况,只有 L o a d L i b r a r y A和L o a d L i b r a r y W。对于大多数应用程序来说,L o a d L i b r a r y宏可以扩展为L o a d L i b r a r y A。幸好L o a d L i b r a r y函数的原型与一个线程函数的原型是相同的。下面是一个线程函数的原型:这两个函数的原型并不完全相同,不过它们非常相似。两个函数都接受单个参数,并且都返回一个值。另外,两个函数都使用相同的调用规则。这是非常幸运的,因为我们要做的事情是创建一个新线程,并且使线程函数的地址成为 L o a d L i b r

44、 a r y A或L o a d L i b r a r y W函数的地址。本质上,我们必须进行的操作是执行类似下面的一行代码:或者,如果喜欢U n i c o d e,则执行下面这行代码:当在远程进程中创建新线程时,该线程将立即调用L o a d L i b r a r y A(或L o a d L i b r a r y W)函数,并将D L L的路径名的地址传递给它。这是非常容易的。但是这里存在另外两个问题。第一个问题是,不能像我在上面展示的那样,将 L o a d L i b r a r y A或L o a d L i b r a r y W作为第四个参数传递给C r e a t e

45、R e m o t e T h r e a d。原因很简单。当你编译或者链接一个程序时,产生的二进制代码包含一个输入节(第1 9章中做了介绍)。这一节由一系列输入函数的形式替换程序(t h u n k)组成。所以,当你的代码调用一个函数如 L o a d L i b r a r y A时,链接程序将生成一个对你模块的输入节中的形实替换程序的调用。接着,该形实替换程序便转移到实际的函数。如果在对C r e a t e R e m o t e T h r e a d的调用中使用一个对L o a d L i b r a r y A的直接引用,这将在你的模块的输入节中转换成L o a d L i b

46、r a r y A的形实替换程序的地址。将形实替换程序的地址作为远程线程的起始地址来传递,会导致远程线程开始执行一些令人莫名其妙的东西。其结果很可能造成访问违规。若要强制直接调用 L o a d L i b r a r y A函数,避开形实替换程序,必须通过调用G e t P r o c A d d r e s s函数,获取L o a d L i b r a r y A的准确内存位置。对C r e a t e R e m o t e T h r e a d进行调用的前提是,K e r n e l 3 2.d l l已经被同时映射到本地和远程进程的地址空间中。每个应用程序都需要 K e r n

47、e l 3 2.d l l,根据我的经验,系统将 K e r n e l 3 2.d l l映射到每个进程的同一个地址。因此,必须调用下面的C r e a t e R e m o t e T h r e a d函数:532计计第四部分动态链接库下载或者,如果喜欢U n i c o d e的话,调用下面的函数:好了,这就解决了第一个问题。第二个问题与D L L路径名字符串有关。字符串“C:M y L i b.d l l”是在调用进程的地址空间中。该字符串的地址已经被赋予新创建的远程线程,该线程将它传递给L o a d L i b r a r y A。但是,当L o a d L i b r a r

48、 y A取消对内存地址的引用时,D L L路径名字符串将不再存在,远程进程的线程就可能引发访问违规;向用户显示一个未处理的异常条件消息框,并且远程进程终止运行。记住,这是远程进程终止运行,不是你的进程终止运行。你可能成功地终止另一个进程的运行,而你的进程则继续正常地运行。为了解决这个问题,必须将 D L L的路径名字符串放入远程进程的地址空间中。然后,当C r e a t e R e m o t e T h r e a d函数被调用时,我们必须将我们放置该字符串的地址(相对于远程进程的地址)传递给它。同样,Wi n d o w s提供了一个函数,即Vi r t u a l A l l o c

49、E x,使得一个进程能够分配另一个进程的地址空间中的内存:另一个函数则使我们能够释放该内存:这两个函数与它们的非E x版本的函数(第1 5章已经做了介绍)是类似的。唯一的差别是这两个函数需要一个进程的句柄作为其第一个参数。这个句柄用于指明执行操作时所在的进程。一旦为该字符串分配了内存,我们还需要一种方法将该字符串从我们的进程的地址空间拷贝到远程进程的地址空间中。Wi n d o w s提供了一些函数,使得一个进程能够从另一个进程的地址空间中读取数据,并将数据写入另一个进程的地址空间。第 22章 插入DLL 和挂接 API计计533下载远程进程由h P r o c e s s参数来标识。参数p

50、v A d d r e s s R e m o t e用于指明远程进程中的地址,参数p v B u ff e r L o c a l是本地进程中的内存地址,参数d w S i z e是需要传送的字节数,p d w N u m B y t e s R e a d和p d w N u m B y t e s Wr i t t e n用于指明实际传送的字节数。当函数返回时,可以查看这两个参数的值。既然已经知道了要进行操作,下面让我们将必须执行的操作步骤做一个归纳:1)使用Vi r t u a l A l l o c E x函数,分配远程进程的地址空间中的内存。2)使用Wr i t e P r o c

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 技术资料 > 其他杂项

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知得利文库网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号-8 |  经营许可证:黑B2-20190332号 |   黑公网安备:91230400333293403D

© 2020-2023 www.deliwenku.com 得利文库. All Rights Reserved 黑龙江转换宝科技有限公司 

黑龙江省互联网违法和不良信息举报
举报电话:0468-3380021 邮箱:hgswwxb@163.com