在Microsoft Wi n d o w s中,每个进程都有它自己的私有地址空间。当使用指针来引用内存时,指针的值将引用你自己进程的地址空间中的一个内存地址。你的进程不能创建一个其引用属于另一个进程的内存指针。因此,如果你的进程存在一个错误,改写了一个随机地址上的内存,那么这个错误不会影响另一个进程使用的内存。
独立的地址空间对于编程人员和用户来说都是非常有利的。对于编程人员来说,系统更容易捕获随意的内存读取和写入操作。对于用户来说,操作系统将变得更加健壮,因为一个应用程序无法破坏另一个进程或操作系统的运行。当然,操作系统的这个健壮特性是要付出代价的,因为要编写能够与其他进程进行通信,或者能够对其他进程进行操作的应用程序将要困难得多。
有些情况下,必须打破进程的界限,访问另一个进程的地址空间,这些情况包括:
• 当你想要为另一个进程创建的窗口建立子类时。
• 当你需要调试帮助时(例如,当你需要确定另一个进程正在使用哪个D L L时)。
• 当你想要挂接其他进程时。
1.插入DLL:一个例子
假设你想为由另一个进程创建的窗口建立一个子类。你可能记得,建立子类就能够改变窗口的行为特性。若要建立子类,只需要调用S e t Wi n d o w L o n g P t r函数,改变窗口的内存块中的窗口过程地址,指向一个新的(你自己的) W n d P r o c。Platform SDK文档说,应用程序不能为另一个进程创建的窗口建立子类。这并不完全正确。为另一个进程的窗口建立子类的关键问题与进程地址空间的边界有关。
当调用下面所示的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,而不是送往窗口的正常窗口过程:
SetWindowLongPtr(hwnd, GWLP_WNDPROC, MySubclassProc);
换句话说,当系统需要将消息发送到指定窗口的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函数。
为另一个进程创建的窗口建立子类时遇到的问题是,建立子类的过程位于另一个地址空间中。图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 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的地址。请注意我说的“试图”二字。这个函数调用并不进行什么操作,它只是返回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的地址,并试图调用这个地址。但是,这时可能遇到一个大问题。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 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怎样才能说明与窗口相关的地址是用于另一个进程中的过程,还是用于同一个进程中的过程呢?
由于对这个问题的解决并没有什么万全之策,因此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改变另一个进程创建的窗口过程。
不过仍然可以为另一个进程创建的窗口建立子类—只需要用另一种方法来进行这项操作。这并不是建立子类的问题,而是进程的地址空间边界的问题。如果能将你的子类过程的代码放入进程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“插入”进程的地址空间。有若干种方法可以用来进行这项操作。下面将逐个介绍它们。
2. 使用注册表来插入DLL
如果你曾经多少使用过Wi n d o w s操作系统,你肯定熟悉注册表的情况。整个系统的配置都是在注册表中维护的,可以通过调整它的设置来改变系统的行为特性。将要介绍的项目是在下面的关键字中:
HKEY_LOCAL_MACHINE/Software/Microsoft /Windows NT/CurrentVersion/Windows/AppInit_DLLs
Windows 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的系统目录中,这样就不必设定路径。在窗口中,我将该值设置为单个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 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并不检查每个库是否已经加载成功,或者初始化是否取得成功。
在插入D L L时所用的所有方法中,这是最容易的一种方法。要做的工作只是将一个值添加到一个已经存在的注册表关键字中。不过这种方法也有它的某些不足:
• 由于系统在初始化时要读取这个关键字的值,因此在修改这个值后必须重新启动你的计算机—即使退出后再登录,也不行。当然,如果从这个关键字的值中删除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 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的地址空间中。如果用户后来决定终止你的应用程序的运行,那么你必须撤消Wo r d P a d的主窗口。在这种情况下,你的D L L将不再需要被插入Wo r d P a d的地址空间。最好是仅在必要时保持D L L的插入状态。
3.使用Windows挂钩来插入DLL
可以使用挂钩将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 S A G E,以便查看系统中的各个窗口处理的消息。该挂钩是通过调用下面的S e t Wi n d o w s H o o k E x函数来安装的:
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hinstDll, 0);
第一个参数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被映射到的进程的地址空间中的虚拟内存地址。最后一个参数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的地址空间,并且将进程B中的D L L映像的自动跟踪计数递增1。
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 t M s g P r o c函数的虚拟内存地址。这个地址可以使用下面的公式来确定:
GetMsgProc B = hinstDll B + (GetMsgProc A - hinstDll A)
将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映像的自动跟踪计数递减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 L中。
与插入D L L的注册表方法不同,这个方法允许你在另一个进程的地址空间中不再需要D L L时删除该D L L的映像,方法是调用下面的函数:
BOOL UnhookWindowsHookEx(HHOOK hhook);
当一个线程调用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个步骤)。这可以防止产生内存访问违规。如果该自动跟踪计数没有递增,那么当进程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函数。
这一切意味着不能撤消该窗口的子类并且立即撤消该挂钩。该挂钩必须在该子类的寿命期内保持有效状态。
4.使用远程线程来插入DLL
插入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的大多数函数允许进程只对自己进行操作。这是很好的一个特性,因为它能够防止一个进程破坏另一个进程的运行。但是,有些函数却允许一个进程对另一个进程进行操作。这些函数大部分最初是为调试程序和其他工具设计的。不过任何函数都可以调用这些函数。
这个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的函数,使我们能够非常容易地在另一个进程中创建线程:
HANDLE CreateRemoteThread(
HANDLE hProcess,
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadId);
好了,现在你已经知道如何在另一个进程中创建线程了,但是,如何才能让该线程加载我们的D L L呢?答案很简单,那就是需要该线程调用L o a d L i b r a r y函数:
如果在对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 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 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函数:
// Get the real address of LoadLibraryA in Kernel32.dll. PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA"); HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, "C://MyLib.dll", 0, NULL);
好了,这就解决了第一个问题。第二个问题与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 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 E x,使得一个进程能够分配另一个进程的地址空间中的内存:
PVOID VirtualAllocEx( HANDLE hProcess, PVOID pvAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);另一个函数则使我们能够释放该内存:
BOOL VirtualFreeEx( HANDLE hProcess, PVOID pvAddress, SIZE_T dwSize, DWORD dwFreeType);
一旦为该字符串分配了内存,我们还需要一种方法将该字符串从我们的进程的地址空间拷贝到远程进程的地址空间中。Wi n d o w s提供了一些函数,使得一个进程能够从另一个进程的地址空间中读取数据,并将数据写入另一个进程的地址空间。
BOOL ReadProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOID pvBufferLocal, DWORD dwSize, PDWORD pdwNumBytesRead); BOOL WriteProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOID pvBufferLocal, DWORD dwSize, PDWORD pdwNumBytesWritten);
远程进程由h P r o c e s s参数来标识。参数p 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 e s s M e m o r y函数,将D L L的路径名拷贝到第一个步骤中已经分配的内存中。
3) 使用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或L o a d L i b r a t y W函数的实地址(在K e r n e l 3 2 . d l l中)。
4) 使用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函数,为它传递第一个步骤中分配的内存的地址。
这时, D L L已经被插入远程进程的地址空间中,同时D L L的D l l M a i n函数接收到一个D L L _ P R O C E S S _ AT TA C H通知,并且能够执行需要的代码。当D l l M a i n函数返回时,远程线程从它对L o a d L i b r a r y的调用返回到B a s e T h r e a d S t a r t 函数(第6 章中已经介绍)。然后B a s e T h r e a d S t a r t调用E x i t T h r e a d,使远程线程终止运行。
现在远程进程拥有第一个步骤中分配的内存块,而D L L则仍然保留在它的地址空间中。若要将它删除,需要在远程线程退出后执行下面的步骤:
5) 使用Vi r t u a l F r e e E x函数,释放第一个步骤中分配的内存。
6) 使用G e t P r o c A d d r e s s函数,获得F r e e L i b r a r y函数的实地址(在K e r n e l 3 2 . d l l中)。
7) 使用C r e a t e R e m o t e T h r e a d函数,在远程进程中创建一个线程,它调用F r e e L i b r a r y函数,传递远程D L L的H I N S TA N C E。
这就是它的基本操作步骤。这种插入D L L的方法存在的唯一一个不足是, Windows 98并不支持这样的函数。只能在Windows 2000上使用这种方法。
5.使用特洛伊DLL来插入DLL
插入D L L的另一种方法是取代你知道进程将要加载的D L L。例如,如果你知道一个进程将要加载X y z . d l l,就可以创建你自己的D L L,为它赋予相同的文件名。当然,你必须将原来的X y z . d l l改为别的什么名字。
在你的X y z . d l l中,输出的全部符号必须与原始的X y z . d l l输出的符号相同。使用函数转发器(第2 0章做了介绍),很容易做到这一点。虽然函数转发器使你能够非常容易地挂接某些函数,你应该避免使用这种方法,因为它不具备版本升级能力。例如,如果你取代了一个系统D L L,而M i c r o s o f t在将来增加了一些新函数,那么你的D L L将不具备它们的函数转发器。引用这些新函数的应用程序将无法加载和执行。
如果你只想在单个应用程序中使用这种方法,那么可以为你的D L L赋予一个独一无二的名字,并改变应用程序的. e x e模块的输入节。更为重要的是,输入节只包含模块需要的D L L的名字。你可以仔细搜索文件中的这个输入节,并且将它改变,使加载程序加载你自己的D L L。这种方法相当不错,但是必须要非常熟悉. e x e和D L L文件的格式。
6.将DLL作为调试程序来插入
调试程序能够对被调试的进程执行特殊的操作。当被调试进程加载时,在被调试进程的地址空间已经作好准备,但是被调试进程的主线程尚未执行任何代码之前,系统将自动将这个情况通知调试程序。这时,调试程序可以强制将某些代码插入被调试进程的地址空间中(比如使用Wr i t e P r o c e s s M e m o r y函数来插入),然后使被调试进程的主线程执行该代码。
这种方法要求你对被调试线程的C O N T E X T结构进行操作,意味着必须编写特定C P U的代码。必须修改你的源代码,使之能够在不同的C P U平台上正确地运行。另外,必须对你想让被调试进程执行的机器语言指令进行硬编码。而且调试程序与它的被调试程序之间必须存在固定的关系。如果调试程序终止运行,Wi n d o w s将自动撤消被调试进程。而你则无法阻止它。
7.用CreateProcess插入代码
如果你的进程生成了你想插入代码的新进程,那么事情就会变得稍稍容易一些。原因之一是,你的进程(父进程)能够创建暂停运行的新进程。这就使你能够改变子进程的状态,而不影响它的运行,因为它尚未开始运行。但是父进程也能得到子进程的主线程的句柄。使用该句柄,可以修改线程执行的代码。你可以解决上一节提到的问题,因为可以设置线程的指令指针,以便执行内存映射文件中的代码。
下面介绍一种方法,它使你的进程能够控制子进程的主线程执行什么代码:
1) 使你的进程生成暂停运行的子进程。
2) 从. e x e模块的头文件中检索主线程的起始内存地址。
3) 将机器指令保存在该内存地址中。
4) 将某些硬编码的机器指令强制放入该地址中。这些指令应该调用L o a d L i b r a r y函数来加载D L L。
5) 继续运行子进程的主线程,使该代码得以执行。
6) 将原始指令重新放入起始地址。
7) 让进程继续从起始地址开始执行,就像没有发生任何事情一样。
上面的步骤6和7要正确运行是很困难的,因为你必须修改当前正在执行的代码。不过这是可能的。
这种方法具有许多优点。首先,它在应用程序执行之前就能得到地址空间。第二,它既能在Windows 98上使用,也能在Windows 2000上使用。第三,由于你不是调试者,因此能够很容易使用插入的D L L来调试应用程序。最后,这种方法可以同时用于控制台和G U I应用程序。
当然,这种方法也有某些不足。只有当你的代码是父进程时,才能插入D L L。另外,这种方法当然不能跨越不同的C P U来运行,必须对不同的C P U平台进行相应的修改。