新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 中文XML论坛 - 专业的XML技术讨论区计算机技术与应用『 C/C++编程思想 』 → 摆脱DLL"地狱"困扰[分享] 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 9128 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: 摆脱DLL"地狱"困扰[分享] 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客楼主
    发贴心情 摆脱DLL"地狱"困扰[分享]

    本文源码下载

    摆脱DLL"地狱"困扰之目前的困境

    在 Windows 系统中,动态库版本冲突实在是一个老大难的问题了,为了解决这个问题,除了使用大量现有的工具外,你还可以利用丰富的 Windows APIs 函数构造自己的调试工具和实用程序。作为例子,本文将提供三个这种类型的工具,讨论如何利用它们来解决动态库的冲突问题。这三个工具分别是:
    DllSpy——列出加载到系统中的所有 Dlls 以及所有使用这些 Dlls 的进程;
    ProcessSpy——列出系统中所有正在运行的进程以及它们正在使用的 Dlls;
    ProcessXP——显示 Windows XP 所有并发运行的会话(Sessions)清单;
      从所周知,动态库“地狱”(DLL Hell)已经不是什么新鲜玩意儿了,如果你使用第三方的 Dlls,肯定会碰到不少与它有关的问题,如找不到入口点,或者库版本不兼容等。.NET 中允许组件的并行执行,减少了产生这种问题的几率,但是如果你还没有升级到 .NET 环境,那怎么办?针对这种情况,可用的方法是用不同的工具跟踪 DLL 的依赖性。但是用标准工具跟踪时,你可能最后得不到所要的信息。许多工具都没有你需要的功能,比如自动写日志文件,跟踪分析,仅在控制台操作脚本控制等。

      本文我们先用一些现有的工具来考察系统中的运行进程,然后系统地研究本文提供的三个工具:DllSpy, ProcessSpy 和 ProcessXP,以便在今后的开发或调试中使用这些工具和技术。

      现有的工具

      Depends.exe 是 Visual C++ 自带的一个工具。它可能是我们经常使用的工具中最简单的一个工具了,其功能是列出某个应用程序或 DLL 需要的 DLLs。这个程序在本站可以下载(更新版本请到下面这个地址下载:http://www.dependencywalker.com)。如果你需要看某个 DLL 或可执行文件的全路经,可以用它的上下文菜单进行设置:如图一:

    按此在新窗口浏览图片
    图一 察看全路经

      对于静态加载的情况(即应用程序在链接过程中将 dlls 对应的 lib 文件链接到程序中),这个工具非常好用,但对于版本较新的系统,大多使用 COM 编程接口,或者说是用 COM 对象编程模型,而 COM 对象的实例化都是运行时加载或者说动态加载某个 DLL 文件,然后通过 LoadLibrary 和 GetProcAddress 调用其中某个特殊的函数来实现的。你不知道这个 DLL 是何时、从哪里被加载的。

      一种确定 DLLs 被动态加载的方法是找出需要被每一个进程加载的 DLL。Sysinternal 公司(http://www.sysinternals.com)提供了一个工具软件 ListDlls.exe。它是一个控制台程序,其图形用户界面(GUI)版本为 Process Explorer。如图二:

    按此在新窗口浏览图片
    图二 Process Explorer 运行画面

      除了列出被某个进程使用的 DLLs 之外,还可以用这个工具了解某个程序用到了哪个 kernel 对象,从版本3.11之后,Process Explorer 还可以让你在两个快照之间轻松扫描到新的或未使用的对象。

      有时候在你用 Process Explorer 扫描到某个进程之前,它可能已经被加载然后又在很短的时间内被卸载了。碰到这种情况时,你需要另外一种类型的工具,我们将在后文中讨论。

      为了操纵进程和 DLLs,首先你必须知道每一个被加载的 DLL 被哪些进程使用。本文的例子程序 DllSpy 实现目的即在于此。如图三所示:

    按此在新窗口浏览图片
    图三 DllSpy 运行画面

      DllSpy程序上面的窗格列出的是所有已经加载的 DLL,每选中一个DLL,在下面的窗格中就会列出使用该 DLL 的所有进程。

      而 ProcessSpy 例子程序的功能正好与 DllSpy 相反,它在上面窗格列出系统中所有的运行进程,每选中一个进程,在下面窗格便显示出此进程使用的所有 DLLs,如图四所示:

    按此在新窗口浏览图片
    图四 ProcessSpy 运行画面

      下面窗格还反映了 DLL 加载的地址是实际地址还是首选地址,以及它们的从属性是静态的还是动态的。这些工具的源代码和可执行程序都可以从本文的下载链接中下载,它们也许不完全满足你的需要,但可以作为技术参考,对编程工作肯定是有所裨益的。

    [此贴子已经被作者于2007-9-11 13:39:25编辑过]

       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/11 13:08:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客2
    发贴心情 
    摆脱DLL"地狱"的困扰之获取进程信息

    摘要

      本文讨论用各种不同的方法来获取系统中运行的进程信息,比如,进程列表,枚举列表中的进程,然后获取关于进程的详细信息。

      如何获取运行进程列表

      有三种方法来获取Win32运行进程的信息,参见表一:

      (表一)
    方法 平台 备注
    PSAPI Windows NT,Windows2000,Windows XP 获取进程,驱动器,模块,内存和工作集信息
    性能计数器 Windows NT,Windows2000,Windows XP 提供除进程清单以外的关于进程的更多信息,可在远程机器上使用。
    TOOLHELP32 Windows 9x,Windows2000,Windows XP 获取进程,线程,模块和堆信息

      本文不打算讨论 TOOLHELP32,因为 MSDN 中提供了很多使用 TOOLHELP32 函数的例子代码。性能计数器提供的信息更多,不仅仅是进程清单。如果你想获取远程机器的信息,那么性能计数器是再好不过的工具了。如果你总是想得到另外一台机器的进程列表信息,那么就用性能计数器吧!

      进程状态 API(PSAPI 全称是 Process Status API)是微软 SDK 中一个很有用的工具,在例子程序中有一个类 CProcessList,其实现文件 Process.cpp 对 PSAPI 进行了打包,用这个类可以获取进程清单。只要 Refresh 一被调用,通过某个进程的ID便可获得此进程的描述信息,并很容易用 GetFirst 和 GetNext 列举出其它进程:

       //用 CProcessList 列出运行进程
       // 一个挨一个获取进程
       CProcess* pProcess = NULL;
       POSITION  Pos = 0;
       for (
             pProcess = ProcessList.GetFirst(Pos);
             (pProcess != NULL);
             pProcess = ProcessList.GetNext(Pos)
           )
       {
          if (pProcess != NULL)
          {
          // 对进程信息进行处理
          }
       }  
      Refresh 的实现用到了 EnumProcesses 函数(在PSAPI中):
    //刷新进程列表
    void CProcessList::Refresh()
    {
    // 不要忘了重置和释放当前的进程列表
       DefaultReset();

    // 存储当前进程列表
       DWORD aProcesses[MAX_PROCESS];
       DWORD cbNeeded = 0;

    // 获取进程快照
       if (!g_PSAPI.EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded))
          return;
        
    // 计算返回了多少个进程IDs
       DWORD cProcesses = cbNeeded / sizeof(DWORD);

    // 将CProcess 对象捆绑到每一个进程ID
       DWORD     dwProcessID;
       CProcess* pProcess;
       for (
             DWORD dwCurrentProcess = 0;
             dwCurrentProcess < cProcesses;
             dwCurrentProcess++
           )
       {
          dwProcessID = aProcesses[dwCurrentProcess];

       // 将进程信息添加到映射
          pProcess = new CProcess(TRUE);
          if (pProcess != NULL)
          {
          // 填写当前进程ID的进程信息--捆绑
             if (!pProcess->AttachProcess(aProcesses[dwCurrentProcess]))
                delete pProcess;
             else
             // 存到映射表中
                m_ProcessMap[(LPVOID)dwProcessID] = pProcess;
          }
       }

    // 第二次循环需要知道此进程的子进程清单
       SetChildrenList();
    }  
      如果你至今还在支持 Windows 的 16 位代码,比如在任务管理器的 ntvdm.exe 项下列出的应用程序就属于此列,那么枚举进程更棘手。有关细节参见本文[URL=http://www.vckbase.com/document/viewdoc/?id=1592#参考资料]参考资料[/URL]中的知识库文章,以及 Matt Pietrek 的专栏文章:August 1998 和 September 1998。

      如何获取进程信息

      有了运行进程的列表,接下来就是根据 EnumProcesses 返回的进程IDs尽可能多的获取每一个进程的详细信息,然后根据这些信息建立有用的工具。用PROCESS_QUERY_INFORMATION | PROCESS_VM_READ作为参数,调用OpenProcess获取进程句柄,然后用AttachProcess(参见Process.cpp文件中的CProcess类实现)方法创建进程描述。表二中列出的是CProcess用于获取进程细节信息的函数:

      (表二)
    方法 描述
    GetName 以NULL作为参数,调用 GetModuleBaseName ,最后去掉扩展名 “.EXE”
    GetFileName 以NULL作为参数,调用 GetModuleFileNameEx
    GetMainWindowHandle 参见GetMainWindowHandle
    GetMainWindowTitle
    GetParentProcessID 用ProcessBasicInformation作为参数调用NtQueryInformationProcess
    GetKERNELHandleCount 用ProcessHandleCount作为参数调用NtQueryInformationProcess  
    GetUSERHandleCount 用GR_USEROBJECTS作为参数调用GetGuiResources
    GetGDIHandleCount 用GR_GDIOBJECTS作为参数调用GetGuiResources
    GetWorkingSet 调用GetProcessMemoryInfo
    GetCmdLine 参见GetProcessCmdLine
    GetOwner 参见GetProcessOwner的细节
    GetSessionID ProcessIdToSessionId (参见对快速用户转换的讨论部分——Windows XP的一个新特性)
    GetModuleList CModuleList是一个对EnumProcessModules 和GetModuleFileNameEx的打包类
    GetChildrenCount 以及子进程清单 要获取某个进程的子进程列表,目前还没这样的API(即便有也未公开)可供使用。但是,因为某个进程的父进程是已知的,所以将某个进程加到其父进程的子进程列表中不难(参见SetChildrenList的实现)

      这里有几个关于AttachProcess的细节问题需要解释一下。首先,为了避免与PSAPI或 NTDLL这样的操作系统特有的DLLs进行静态链接,编写一个类对我们需要的从这些DLLs中输出的函数进行打包是值的得--有关细节可以参考例子代码中的Wrappers.h和Wrappers.cpp文件。这样的话,你只需要定义一个CPSAPIWrapper对象并调用它的GetModuleFileNameEx方法即可,不用链接到PSAPI库。另外,你应该调用IsValid方法来检查这些DLLs在你运行系统中是可用的。如此一来你的代码便可以运行在任何Windows平台而不会产生诸如某某函数未定义之类的链接错误。注意在使用某个专门的特性之前,你应该检查一下Windows的版本或IsValid的返回结果(参见DllSpy例子代码中的DllSpyApp::InitInstance部分)。

      注意PSAPI中的GetModuleFileNameEx函数返回的文件名很奇怪:如:"\SystemRoot\ System32\ smss.exe"或者"\??\C:\WINNT\system32\winlogon.exe"等等。谁知道这是什么意思?在Helper.cpp中有一个函数TranslateFilename专门对此进行转换,将这些文件名转换成更容易理解的名字。稍候会我们还会谈到这个函数。

      接下来我们讨论如何寻找某个进程的主窗口,EnumWindows有一个参数是回调函数,此回调函数的作用是接收顶层窗口句柄,在这个问调函数中,我们要调用GetWindowThreadProcessId来获取创建相应窗口的进程ID,如果找到这个窗口(可见的)便停止枚举(详情请参见GetMainWindow实现)。函数GetWindowText可以被用来获取某个不同进程的窗口标题。

      在Windows NT 和Windows 2000里,为了获取与创建某个特定窗口的进程对应的文件名字,不能像以前那样用GetWindowModuleFileName函数,你会毫无所获,这个函数总是返回当前运行进程的路径名。

      获取某个进程主窗口的详细过程描述可以参考Jeff Prosise在MSJ Aug99上的Wicked Code专栏文章。现在你已经知道了如何通过某个已知的进程ID,调用PSAPI函数来获取全路径名。然后利用这个路径名并调用GetWindowThreadProcessID函数获取创建某个特定窗口的进程文件名。

      在AttachProcess中必须调用OpenProcess来获取大多数的进程信息,但是有可能出现拒绝访问的错误。如果出现这种情况,我用了一个Keith Brown给出的方法,参见他在MSJ Aug99中的"Security Briefs"专栏文章,其中详细讨论了如何用高级别权限获取进程句柄。细节请参见例子代码的Helpers.cpp文件,其中有一个函数名叫GetProcessHandleWithEnoughRights,就是出自Keith Brown之手。

      当某个进程是作为另外一个用户账号计划任务而运行的时候,就会出现上述提到的拒绝访问问题。即便是Windows任务管理器都无法终止这样的进程,它只显示一个象下面这样的对话框。如图九:


    按此在新窗口浏览图片
    图九 无法终止的进程

      如果你在ProcessSpy例子程序里的某个进程上双击[URL=http://diy.yesky.com/input/]鼠标[/URL]按键,它将终止这个进程。你看一下在例子代码中的SlayProcess函数就会知道。此辅助函数调用了GetProcessHandleWithEnoughRights来获取进程句柄,但访问权限参数是PROCESS_TERMINATE,而不是在AttachProcess里所用的PROCESS_QUERY_INFORMATION | PROCESS_VM_READ。

      最后是用GetProcessOwner获取进程运行的用户账号(格式为\\Domain\User),通过用TokenUser作为参数调用GetTokenInformation,然后用LookupAccountSid将返回的用户SID转换为人可读的域名和用户名。有时OpenProcessToken会因为遇到象System这样的进程而调用失败,甚至是Windows 2000 资源开发包中的PULIST.EXE遇到这种情况都无法显示出拥有进程的用户。只有ProcessExplorer(Sysinterals公司开发的一个工具软件)能成功找到此"安全的"应用的所有者。本文稍后会讨论Windows XP中如何用WTS APIs(也就是Windows Terminal Services API--Windows终端服务API)来获取进程的宿主。

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/11 13:10:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客3
    发贴心情 
    摆脱DLL"地狱"的困扰之定制调试诊断工具

      本文假设你熟悉 Win32,DLL

      如何获取进程的命令行
      方法 描述
    GetName 以NULL作为参数,调用 GetModuleBaseName ,最后去掉扩展名 “.EXE”
    GetFileName 以NULL作为参数,调用 GetModuleFileNameEx
    GetMainWindowHandle 参见GetMainWindowHandle
    GetMainWindowTitle
    GetParentProcessID 用ProcessBasicInformation作为参数调用NtQueryInformationProcess
    GetKERNELHandleCount 用ProcessHandleCount作为参数调用NtQueryInformationProcess  
    GetUSERHandleCount 用GR_USEROBJECTS作为参数调用GetGuiResources
    GetGDIHandleCount 用GR_GDIOBJECTS作为参数调用GetGuiResources
    GetWorkingSet 调用GetProcessMemoryInfo
    GetCmdLine 参见GetProcessCmdLine
    GetOwner 参见GetProcessOwner的细节
    GetSessionID ProcessIdToSessionId (参见对快速用户转换的讨论部分——Windows XP的一个新特性)
    GetModuleList CModuleList是一个对EnumProcessModules 和GetModuleFileNameEx的打包类
    GetChildrenCount 以及子进程清单 要获取某个进程的子进程列表,目前还没这样的API(即便有也未公开)可供使用。但是,因为某个进程的父进程是已知的,所以将某个进程加到其父进程的子进程列表中不难(参见SetChildrenList的实现)

      上表列出的方法中有一个是 GetCmdLine,这个函数返回进程的命令行。实际上,它并不是真正地返回命令行,而是返回进程启动时接收的参数。例如,如果你安装了微软Power Toys中的TweakUI程序(可以在vckbase站点下载),当你在Windows的资源管理器中的任何文件夹上单击右键,则在弹出的上下文菜单中都会有一个"Run Command Prompt Here"菜单项。执行这个菜单命令后,会弹出命令提示窗口,并且所点击的目录为当前工作目录。

      但是你怎么知道cmd.exe被调用的时候用的是什么参数呢?那么在微软的调试工具(可以在vckbase站点下载)中有一个命令行程序TLIST.EXE,用它可以知道cmd.exe运行的参数,TLIST.EXE是个控制台程序,其输出的信息包括运行进程清单以及命令行,如果用进程ID作为参数调用此程序,例如用cmd.exe的进程ID作为参数载C:盘根目录(C:\)运行TLIST:

    C:\>tlist 632
    632 CMD.EXE           C:\WINNT\System32\cmd.exe - tlist 632
       CWD:     C:\
       CmdLine: C:\WINNT\System32\cmd.exe /k cd "C:\"
       VirtualSize:    13408 KB   PeakVirtualSize:    13412 KB
       WorkingSetSize:   948 KB   PeakWorkingSetSize:   952 KB
       NumberOfThreads: 1
        968 Win32StartAddr:0x4ad1a420 LastErr:0x000000cb State:Waiting
      5.0.2195.1600 shp  0x4ad00000  cmd.exe
      5.0.2195.1600 shp  0x77f80000  ntdll.dll
      5.0.2195.1600 shp  0x77e80000  KERNEL32.dll
      5.0.2195.1600 shp  0x77e10000  USER32.dll
      5.0.2195.1340 shp  0x77f40000  GDI32.DLL
      5.0.2195.1600 shp  0x77db0000  ADVAPI32.dll
      5.0.2195.1615 shp  0x77d40000  RPCRT4.DLL
         6.1.8637.0 shp  0x78000000  MSVCRT.dll

      第三行的参数/k cd "C:\"用于外壳调用cmd.exe。如果指定了/k,则cmd.exe执行指定的命令,但是不退出。由于这时调用的应用程序,所以在你构造的工具中这样使用行不通的,必须通过API函数调用。

      TLIST工具的源代码可以在MSDN中找到,也可以在vckbase站点下载。可惜用这个代码只能得到进程的ID,名称和主窗口。可以用三种不同的途径来获取某个进程的命令行。第一种是比较猛的一种方法:那就是在汇编一级钻研TLIST程序。Process.cpp文件中的GetProcessCmdLine 就是钻研的成果,这个函数可以在特定的进程的地址空间中找到进程的命令行。指向命令行(Unicode字符)的指针存储在一个内存块中,这个内存块就是进程环境块--Process Environment Block(PEB)中的一个域所指的地址。有关PEB结构的详细内容将在下一部分讨论。

      第二种方法是在Web上搜索,看看是否有人已经解决了这个问题!GetCommandLine可以得到命令行,但仅用于调用进程。最好是用CreateRemoteThread在另一个进程上下文中执行这个调用。此方法的细节请参考 Felix Kasza 的文章:[URL=http://www.mvps.org/win32/processes/remthread.html]CreateRemoteThread[/URL]。

      第三种方法是代码重用,或者说得更确切一些是输出重用。你可以通过捕获TLIST程序的输出,然后解析这些输出,从而获得命令行。在本文的后面部分,我们会对这个方法进行完全的讨论。

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/11 13:15:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客4
    发贴心情 
    摆脱DLL"地狱"的困扰之WTS API

    如何通过 WTS APIs(Windows终端服务 APIs)获取进程信息

      Windows XP 有一个新特性叫做“快速用户转换——Fast User Switching”,这个特性允许多个用户同时在一台机器上登陆。当一个用户登陆后,另一个用户启动的进程仍然能够运行。这个神奇的特性所倚仗的是 WTS APIs。

      Windows XP为每一个登陆用户创建一个WTS会话(Session)。每个运行进程总是与这样一个Session关联。Windows XP的任务管理器允许你列出进程清单,不论是针对所有会话的还是仅仅针对自己的会话,任务管理器对话框的进程标签中有一个"显示所有用户的进程"复选框可以对此进行选择。(如图十一所示):


    按此在新窗口浏览图片
    图十一 任务管理其的进程列表

      如果你想了解某个进程隶属的 session ID,可以调用 kernel32.dll 输出的一个 API 函数 ProcessIdToSessionId。给定一个进程的ID,他返回相应的 session ID。有趣的是这个 API 函数不是由 wtsapi32.dll 输出的,而是出自于 kernel32.dll,前者是所有 Windows 终端服务 APIs 的输出动态库。实际上,即使 Windows 终端服务没有运行起来,Windows 2000 和 Windows XP 都将 session ID 存储在 PEB 中。

      注意 Windows NT 既不在 PEB 中存储 session ID,也不从 kernel32.dll 中输出 ProcessIdToSessionId 函数。当你调用 ProcessIdToSessionId,而 WTS 又没有运行,这时其返回值总是0。

      除了允许你列出打开的会话之外,WTS 还有一个 API 用于枚举运行的进程,其实现方式与 PSAPI 和 TOOLHELP32 的实现方式是不同的。我写了一个类 CWTSWrapper 来打包 WTS 中与进程和会话有关的函数,以便避免与 wtsapi32.dll 进行静态链接。这个类的实现细节请参考下载的源代码,见 common 目录的 wrappers.cpp 文件。用 CWTSWrapper 很容易构造象 ProcessXP 这样的控制台应用程序。下面是ProcessXP 程序的输出,它列出了与登陆用户对应的打开的会话以及会话项下的运行进程。ProcessXP 程序的输出如下:

    3 open sessions
    ----------------------------
    ID State Window Station
    ----------------------------
    0 (WTSActive) Console [Administrator]
    1 (WTSDisconnected) [standard]
    2 (WTSDisconnected) [Player]

    30 running processes
    ----------------------------
    0 0 ?
    0 4 System \\NT AUTHORITY\SYSTEM
    0 388 smss.exe \\NT AUTHORITY\SYSTEM
    0 600 csrss.exe \\NT AUTHORITY\SYSTEM
    0 632 winlogon.exe \\NT AUTHORITY\SYSTEM
    0 676 services.exe \\NT AUTHORITY\SYSTEM
    0 688 lsass.exe \\NT AUTHORITY\SYSTEM
    0 856 svchost.exe \\NT AUTHORITY\SYSTEM
    0 968 svchost.exe \\NT AUTHORITY\SYSTEM
    0 1160 svchost.exe \\NT AUTHORITY\NETWORK SERVICE
    0 1192 svchost.exe \\NT AUTHORITY\LOCAL SERVICE
    0 1252 spoolsv.exe \\NT AUTHORITY\SYSTEM
    0 1888 explorer.exe \\MACHINE\Administrator
    0 2004 msmsgs.exe \\MACHINE\Administrator
    0 104 svchost.exe \\NT AUTHORITY\SYSTEM
    1 1496 csrss.exe \\NT AUTHORITY\SYSTEM
    1 1172 winlogon.exe \\NT AUTHORITY\SYSTEM
    1 1640 explorer.exe \\MACHINE\standard
    1 1900 ctfmon.exe \\MACHINE\standard
    1 352 notepad.exe \\MACHINE\standard
    1 1896 freecell.exe \\MACHINE\standard
    2 416 csrss.exe \\NT AUTHORITY\SYSTEM
    2 268 winlogon.exe \\NT AUTHORITY\SYSTEM
    2 1784 explorer.exe \\MACHINE\Player
    0 1820 msiexec.exe \\NT AUTHORITY\SYSTEM
    2 1544 ctfmon.exe \\MACHINE\Player
    2 1632 msmsgs.exe \\MACHINE\Player
    2 1268 wordpad.exe \\MACHINE\Player
    0 1696 wuauclt.exe \\MACHINE\Administrator
    0 1996 ProcessXP.exe \\MACHINE\Administrator
      从上面的输出可以看出,名为 MACHINE 的机器上打开的会话有三各。第一个会话的 ID 为0,状态为活动(WTSActive 因为它就是运行中的 ProcessXP 所在的会话),产生这个会话的登陆用户为 Administrator。第二个会话的ID是1,处于断开状态(WTSDisconnected),产生这个会话的用户为标准用户,此用户启动了 Notepad 和 Freecell 程序,用户Player打开了会话2,并运行WordPad,但目前状态是断开的。

      ProcessXP 的源代码包含在本文可下载的压缩包中。WTS 有一个与注册表类似特性,那就是允许你获取另外一台机器的信息。这就是为什么WTS枚举 APIs 函数的第一个参数都是一个服务器句柄。WTS_CURRENT_SERVER_HANDLE 用于当前的机器。第二个参数是保留参数,值应该为0。第三个参数希望的版本,其值应该是1。最后两个参数用于存放返回的信息。一个用于存放会话数或进程数。另一个是结构数组的指针,结构可以是描述会话信息的结构,也可以是描述进程信息的结构。就看你是使用哪个枚举API,是枚举会话还是枚举进程。因为数组的存储空间是由 WTS 分配的,你必须要记住用 WTSFreeMemory 释放这个空间。

      下面是描述会话的结构:WTS_SESSION_INFO:

    typedef struct _WTS_SESSION_INFO
    {
        DWORD SessionId;
        LPTSTR pWinStationName;
        WTS_CONNECTSTATE_CLASS State;
    } WTS_SESSION_INFO, * PWTS_SESSION_INFO;
      结构中除了会话的 SessionId,还有会话名 pWinStationName,当前会话的名字是“console”,而其它的会话是无名的。当前的会话状态为 WTSActive,其它则为 WTSDisconnected。

      下面是描述进程的结构 WTS_PROCESS_ INFO:

    typedef struct _WTS_PROCESS_INFO
    {
        DWORD SessionId;
        DWORD ProcessId;
        LPTSTR pProcessName;
        PSID pUserSid;
    } WTS_PROCESS_INFO, * PWTS_PROCESS_INFO;
      SessionId 与 ProcessIdToSessionId 所要找的值一样,ProcessId 不用说了,是进程ID。最后一个成员 pUserSid 指向安全标示符,描述用户账号,用户正是在这个账号下运行进程。使用 LookupAccountSid,你可以获得从 pUserSid 中获得用户名。这个信息已经可以通过 CProcess 类中的 GetProcessOwner 获得,但它是通过进程记号(token),而不是通过 WTS。某些情况下,即便由 WTSEnumerateProcesses 控制对它的提供,要想获得进程记号也是不可能的,这就是在 Windows XP 环境下要用 WTS API 而不用 PSAPI 或 TOOLHELP32 的缘故。

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/11 13:16:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客5
    发贴心情 
    列举加载的模块

      任何时候通过 PSAPI 或 TOOLHELP32 都可以列出某个进程加载的 DLLs 列表。在写此文前的调研过程中,我研究了 Matt Pietrek 以前在 [URL=http://www.microsoft.com/msj/0998/hood0998.aspx]MSJ Under The Hood[/URL] 专栏中的一篇文章,其内容是讨论如何使用 TOOLHELP32 来实现前述的功能,我发现在 Windows 2000 和 Windows XP 环境中是有问题的,代码不能正常工作,现将其代码摘录如下:

    用TOOLHELP32遍历模块

    //
    //通过取得ToolHelp32 进程快照,枚举此进程的模块列表
    //
    HANDLE hSnapshotModule;
    hSnapshotModule = pfnCreateToolhelp32Snapshot( TH32CS_SNAPMODULE,
                                               procEntry.th32ProcessID );
    if ( !hSnapshotModule )
       continue;

    // 迭代快照中每一个模块
    MODULEENTRY32 modEntry = { sizeof(MODULEENTRY32) };
    BOOL fModWalkContinue;

    for (fModWalkContinue = pfnModule32First(hSnapshotModule,&modEntry);
        fModWalkContinue;
        fModWalkContinue = pfnModule32Next(hSnapshotModule,&modEntry) )
    {
       // 确定是否为EXE文件本身,如果是,则不将它加入模块列表
       if ( 0 == stricmp( modEntry.szExePath, procEntry.szExeFile ) )
           continue;

       // 确定是否为我们已有的DLL
       PModuleInstance pModInst = modList.Lookup(modEntry.hModule,
                                                 modEntry.szExePath );

       // 如果以前没有见过,则将它加入列表
       if ( !pModInst )
           pModInst = modList.Add( modEntry.hModule, modEntry.szExePath );

       // 将此进程加入到使用此DLL的进程列表
       pModInst->AddProcessReference( procEntry.th32ProcessID );       
    }
    CloseHandle( hSnapshotModule ); // 完成模块列表快照

      其实并不是程序有什么瑕疵,主要是时过境迁,导致代码中一个if语句的使用无效,毕竟 Matt Pietrek 写那篇文章的时候(其代码是1998.9 在 MSJ 上发布的),Windows 2000 还不知道在哪里呢!
      那个无效的 if 语句是这样的:由于 CreateToolhelp32Snapshot 调用失败时不会返回 NULL,所以下面的错误处理代码是无效的:
    if ( !hSnapshotModule )
       continue;

      实际上,如果失败,hSnapshotModule的值为INVALID_HANDLE_VALUE或-1,并且这个if语句是捕获不到它的,这到没什么,关键是如何发现这个bug。当我在Windows 2000上测试ProcessSpy时,一切运行正常,只是当列表框即便为空的时候,程序也没有返回某些进程的出错信息。由于错误处理代码本身是错的,执行跳过了循环,Module32First调用失败,但没有任何实质性的错误。如果你在Windows 2000环境用Matt Pietrek的这篇文章提供的ModuleList工具,你将得到不正确的结果。
      为了搞清楚代码运行中发生的事情,用本文实例代码包含的Helpers.cpp 文件中提供的GetLastErrorMessage辅助函数可以有助于你看得更清楚。他调用GetLastError 和 FormatMessage以纯文本形式获取相应的失败原因。失败原因都一样:Access Denied,也就是拒绝存取。但是使用PSAPI函数时,当获得相同进程的模块列表时不存在存取问题。
      之所以发生存取问题,是由于缺乏优先级。使用TOOLHELP32 的代码要正常工作必须得有 SE_DEBUG_NAME 优先级。有关这个问题的详细信息,请参考 1998.3 MSJ 的 [URL=http://www.microsoft.com/msj/0398/Win320398.aspx]Q&A Win32[/URL] 专栏以及 1999.8 的 [URL=http://www.microsoft.com/msj/0899/security/security0899.aspx]Security Briefs[/URL] 专栏


    关于 DLL 的方方面面


      用 PSAPI 和 TOOLHELP32 两种途径获得的某个进程所加载的模块列表只反映地址,在这个地址处,DLL被映射到地址空间。下一步便是尽可能完整地获取关于DLL的描述。我的实现并不象在CProcess中所做的那样提供单独的 AttachModule 方法。因为要获取某些细节信息代价实在太高,因此我选择将它们分割成不同的函数。最不值钱的信息从 CModule 的构造函数获得,其它信息的获取要到相应的存取器方法被调用(通过 Refresh 函数)。实现细节请参考 Module.cpp 文件。其 Refresh 方法模仿了 Matt Pietrek 的 CModuleList 中的Refresh/RefreshTOOLHELP32 方法。表三列出了 CModule 的存取器方法:

      存取器
    说明

    HMODULE GetModuleHandle
    DLL被映射的地址

    CString& GetFullPathName
    源自TOOLHELP32::Module32xxx 或PSAPI::GetModuleFilenameEx

    CString& GetPathName
    同GetFullPathName

    CString& GetModuleName
    同GetFullPathName

      我前面提到过,想要获取模块的全路径名需要一点诀窍。由于一些原因,GetModuleFilenameEx 或 TOOLHELP32 模块函数返回的模块名很奇怪,它们不遵循 Win32 的命名标准。例如以smss为例,返回的名字是"\SystemRoot\System32\smss.exe",这里"\SystemRoot"必须用Windows文件夹的实际名字来替换。又如 wonlogon.程序,返回的名字是"\??\C:\WINNT\system32\winlogon.exe",应该转换成"C:\WINNT\system32\winlogon.exe"。"\??\"前缀是 Windows NT 名字空间的残留物,是 kernel 模式中的东西,即便是Win32编程也很少用到它。我写了一个辅助函数 TranslateFilename 用于将这些文件名转换成更标准的形式。此函数的细节请参考下载源代码中的Helpers.cpp 文件。
      我用 Refresh 方法采集其余的模块描述,具体实现请参考 Module.cpp 文件,下面是对它的一个概述,详细的存取函数见表四:

    存取器
    说明

    DWORD GetBaseAddress
    使用PE_EXE::GetImageBase 来获得首选的加载地址

    void GetFileTime(FILETIME& ft)
    用KERNEL32.DLL 输出的API GetFileTime来获悉何时被创建、修改和最后一次存取

    CString& GetFileTime
    获得与上一个函数相同的信息,但这里是文本格式,使用GetFileDateAsString/GetFileTimeAsString 辅助函数

    DWORD GetFileSize
    用PE_EXE::GetFileSize 获取文件的大小,以字节为单位

    CString& GetSubSystem
    用PE_EXE::GetSubSystem 获悉IMAGE_SUBSYSTEM_xxx模块子系统之一,在winnt.h 文件中定义,在这个文件的最新版本中可以找到IMAGE_SUBSYSTEM_XBOX

    void GetLinkTime(FILETIME& ft)
    用PE_EXE::GetTimeDateStamp 获取模块的链接时间

    CString& GetLinkTime
    获得与上一个函数相同的信息,但这里是文本格式,使用GetFileDateAsString/GetFileTimeAsString辅助函数

    WORD GetLinkVersion
    用PE_EXE::GetLinkerVersion 获取用于构造此模块的链接器版本


      大部分的描述信息都是从文件本身吸取出来的,同时借助了 Matt Pietrek 所写的几篇文章中有关PE格式的知识。
    如果你想了解更多有关 PE 文件的细节,请阅读 Matt Pietrek 的这些文章,其中重点是 PE_EXE 类和 PEDUMP 实现。其代码对于诸位具有很高的参考价值。
      GetBaseAddress 一个有趣的使用方法是将它的返回值与 GetModuleHandle 的返回值进行比较。后者是实际的地址,正是在这个地址,模块被加载到进程地址空间里,而前者的地址是模块希望被加载的地址。这正好用来发现加载是否冲突。
    当一个进程启动时,Windows 加载程序自动加载静态DLLs。这些静态链接的东西很容易用PE格式和 MODULE_DEPENDENCY_LIST 类通过编程获得。没有哪个API能扫描到这些模块与那些用 LoadLibrary 或 CoCreateInstance 动态加载的模块之间的差别。如果一个DLL被某个进程使用,但它又不在静态链接之列,那么它就应该是动态加载的。
      在 ProcessSpy 的输出画面中,如图四,底下的窗格中每一个模块都有一个前缀图符,圆形的D表示动态加载的,方形的S表示静态加载的。它们的颜色也有不同的意思,红色表示这个模块的基地址与其加载地址是不同的,反之则为浅蓝色。
      除了从文件本身吸取描述信息外,还可以从它的资源版本中获取其它描述信息。Paul DiLascia 在他的 [URL=http://www.microsoft.com/msj/0498/c0498.aspx]C++Q&A[/URL] 专栏(MSJ 1998.4)文章中为我们提供了一个很帅的打包类 CModuleVersion,用这个类可以方便地获得资源版本中对模块的描述信息。对于每一项VS_VERSION_INFO 细节都有存取函数,这些函数返回 CString 引用,都是由 CModuleVersion::GetFileVersion 用相应的串填写。GetCompanyName 就是一个很好的例子。
      为了满足我的需要,我对 Paul DiLascia 的代码进行了修改。GetFileVersionInfo 方法应该得到模块的名字,而不是真正的文件名。为了获取相应的文件名,调用 GetModuleHandle。如果在当前的进程空间中查找模块失败(这种情况罕见)。为了解决这个问题,当给定的模块名就是实际的执行文件名时(用 GetFileAttributes 可以判断出来),则直接使用它即可。
      Windows 提供的资源信息不仅仅限于公司名这么简单,通常还有更多的东西,例如,从中可以很容易知道应用程序是否为Debug版本,是否是私隐或特别版。你必须看一下 VS_FIXEDFILEINFO 结构中的 dwFileFlags 标志。MSDN文档对它的描述是包含一个位码(bitmask)值,这些位码值的含义请参考表五:按照版本信息对文件进行分类:

    (表五)

    标志
    描述

    VS_FF_DEBUG
    包含调试信息或者编译时是按可调试方式编译的

    VS_FF_INFOINFERRED
    动态创建版本结构,因此这个结构中的某些成员可能为空或不正确。在文件的VS_VERSIONINFO数据中决不能设置此标志

    VS_FF_PATCHED
    已经被修改并且与原来同一版本号的文件不相同了

    VS_FF_PRERELEASE
    开发版本,非商业发布产品

    VS_FF_PRIVATEBUILD
    没有用标准的发布过程构造,如果设置了此标志,则StringFileInfo 结构应该包含PrivateBuild 项

    VS_FF_SPECIALBUILD
    由原公司用标准的发布过程构造,但是相同版本号的标准文件的变种。如果设置此标志,则StringFileInfo 结构应该包含SpecialBuild 项


    在相同版本的结构中,dwFileType域定义了文件类型,参见表六:dwFileType域中的标志

    (表六)

    标志
    描述

    VFT_UNKNOWN
    系统未知

    VFT_APP
    包含一个应用程序

    VFT_DLL
    包含一个动态链接库(DLL)

    VFT_DRV
    包含一个设备驱动程序,如果dwFileType  是VFT_DRV,则dwFileSubtype 包含进一步的关于此驱动程序的描述

    VFT_FONT
    包含一种字体,如果dwFileType 是VFT_FONT,则dwFileSubtype 包含进一步的字体文件描述

    VFT_VXD
    包含一个虚拟设备

    VFT_STATIC_LIB
    包含一个静态链接库


    ProcessSpy 使用这些标志来表示版本栏(Version),用D表示 Debug,用P表示补丁,参见图四。


    下一回内容预告

      本文以后的内容将讨论几种用非常规方式来获取一些附加的信息源。也就是说如果在没有可借助的 API 的情况下,你就可以用这几种非常规方式。其中包括我至今未曾提到的一个主要信息源,那就是 Windows 的外壳(Shell)。在模块文件中隐藏一个文件的时候, 关于某个文件的信息,没有人比 Windows 资源管理器知道的更多。如图十八所示:


    按此在新窗口浏览图片
    图十八 用资源管理器查看文件信息


      那么如何从自己的程序中打开或者调用 Windows 资源管理器文件属性对话框呢?关于这个请参考[URL=http://www.vckbase.com/bbs/prime/viewprime.asp?id=409]精华区的一小段代码[/URL]。其关键是先填写 SHELLEXECUTEINFO 结构,注意结构中的 fMask 成员一定要用 SEE_MASK_INVOKEIDLIST 赋值,然后调用 ShellExecuteEx API 函数,如:

    SHELLEXECUTEINFO sei;
    ZeroMemory(&sei,sizeof(sei));
    sei.cbSize = sizeof(sei);
    sei.lpFile = szFilename;
    sei.lpVerb = _T("properties");
    sei.fMask  = SEE_MASK_INVOKEIDLIST;
    ShellExecuteEx(&sei);

      在 ProcessSpy 程序界面底部窗格中任何一个模块记录上双击鼠标便可以调出文件的属性对话框,相应模块文件的描述信息一目了然。注意 Windows XP 中不支持多个 ShellExecuteEx 调用,当你调用第二次时,线程冻结,也不会有任何提示。
      正如你所看到的,有许多方法都可以获得加载 DLLs 以及活动进程的信息。我在本文中提供的几个工具可以作为一个很好的学习开端,你完全可以借鉴文本描述的方法以及所提供的 C++ 类来定制满足自己需要的调试工具。
    参考资料

    [URL=http://www.vckbase.com/document/viewdoc/?id=1482]如何用 Win32 APIs 枚举应用程序窗口和进程[/URL]
    [URL=http://www.vckbase.com/document/viewdoc/?id=1575]如何在 Windows NT、Windows 2000 和 Windows XP 中使用VDMDBG函数?[/URL]
    [URL=http://www.vckbase.com/bbs/prime/viewprime.asp?id=409]如何从程序中调用外壳的文件属性对话框?[/URL]
    [URL=http://www.vckbase.com/bbs/prime/viewprime.asp?id=658]Windows NT 系统中如何启动和终止 16 位 Windows 应用程序?[/URL]
    在后续文章中,我将介绍从系统获取信息的新方法。

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/11 13:36:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客6
    发贴心情 
    摘要

      本文前面讨论了用几种不同的方法来获取进程及其相关 DLLs 的信息,例如通过 PSAPI、NTDLL 以及TOOLHELP32 库提供的 APIs,在这一部分,作者给出了几种获得系统级信息的非常规方法,你可以轻松将它们集成到自己的工具包中。本文范例包含三个实用工具:

    LoadLibrarySpy,监视并扫描应用程序加载了哪些 DLLs;
    WindowDump,获取任何窗口的的内容以及窗口的详细描述信息;
    FileUsage,重定向控制台程序,揭示哪个进程正在使用打开的文件;

    --------------------------------------------------------------------------------

      本文前面的部分讨论了如何用有着良好文档描述的 API 函数来获取运行进程列表以及它们加载的 DLLs 信息。接下来我将用不同的方法,或者说是非正式的方法来获取系统级信息,首先,我将深入分析 Win32 调试 API 以及 Windows 加载器(Windows Loader)提供的痕迹来揭示给定进程是如何加载 DLL 的。我将借助我的 CApplicationDebugger 可重用类,用几种不同的方法来分析 DLL 重定位的原因。
      接着,我将生成两个工具。LoadLibrarySpy 扫描 DLL 重定位。WindowDump 窃取任何窗口的内容和详细描述信息。最后,在讨论进程环境块(PEB)内部结构之前,我会向你展示如何操纵控制台程序产生的输出以便摸索寻找一些未公开的信息。

    回到 DLL Hell

      前面我们已经看到获取所有静态或动态加载的 DLLs 列表是很容易的事情。但是对动态加载的DLL而言,情况比想象的稍微复杂一些。例如,DllSpy 和 ProcessSpy 两个工具依据某个时间点获得的快照。因此,有可能出现来不及扫描某个被快速加载和卸载的DLL。Win32 调试 API 提供了对这个问题的解决办法:在调试程序时, 这些 API 可以对被调试程序加载和卸载的任何DLL了如指掌。
      要实现我的意图,并不需要一个功能完整,名副其实的调试器,但我必须侦测到新 DLL 何时被加载到进程地址空间。因此,我将讨论 Win32 调试 API 的基本知识以及它们在 Windows NT、Windows 2000 和 Windows XP 操作系统中有用的扩展。
      为了调试一个程序,你首先必须使用用下面这些特殊的标志之一调用 CreateProcess 来启动拟调试的程序。DEBUG_PROCESS 表示请求来自被调试程序以及被调试程序启动的每一个进程的事件。DEBUG_ONLY_THIS_PROCESS 表示只请求来自被调试程序的事件(而不是来自其子进程的事件)。
      使用 DEBUG_ONLY_THIS_PROCESS 标志时,调试器将接收不到来自被调试程序启动的进程事件。性能监视器(perfmon.exe)就是一个很好的例子,此标志对这个程序没有作用。性能监视器是一个简单的打包程序,其作用 只不过是启动另外一个程序——微软管理控制台(MMC),并传递任何所需的参数使它显示性能计数器。
      在被调试程序的生命期内,Windows 通知调试器 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig1]Figure 1[/URL] 所列出的事件。这些事件由 DEBUG_EVENT 结构描述,如 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig2]Figure 2[/URL] 所示。
      为了接收这些事件,调试器必须调用 WaitForDebugEvent。该函数阻塞调试器的运行,直到被调试程序发生 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig1]Figure 1[/URL] 所列的事件之一,或者超时参数中给定的秒数为止。当调试器处理某个事件时,它调用 ContinueDebugEvent 让被调试程序继续其生命之旅。注意:在调试器中,当 WaitForDebugEvent 解除阻塞时,所有被调试者线程被冻结,在调用 ContinueDebugEvent 期间被解冻。参见 Figure 3:

    按此在新窗口浏览图片
    Figure 3 调试事件流

    CApplicationDebugger

      调用 CreateProcess 的线程必须是进入调试循环的线程。既然调试器阻塞于 WaitForDebugEvent,因此最好让这部分代码运行在一个与主UI线程不同的专门线程中。本文将其行为包装在 CApplicationDebugger 类中,其声明参见本文附带源代码中的 ApplicationDebugger.h 文件,这个类的一部分灵感还来自 Matt Pietrek 的 LoadProf32(参见 [URL=http://download.microsoft.com/download/0/6/7/0678184e-905e-4783-9511-d4dca1f492b4/MSJJul95.exe]MSJJul95.exe[/URL])。
      CApplicationDebugger 是一个虚拟类,因为你得从它派生并实现自己的重写版本,以便特定的调试事件发生时进行相应的调用。这个类被用于生成 LoadLibrarySpy(参见 Figure 4),这是一个调试程序和监控 DLL 加载和卸载的工具,不论是静态加载还是动态加载,也不论是不是有加载地址冲突,它都能监控。

    按此在新窗口浏览图片
    Figure 4 LoadLibrarySpy

      调用 CreateProcess 是在 CApplicationDebugger::LoadTheProcess 中进行的,为简单起见,参数使用 DEBUG_ONLY_THIS_PROCESS。如果需要,你可以将 CApplicationDebugger 扩展成能处理来自多个被调试进程的事件,对于 MMC 管理单元(snap-ins)很有用。
      CLoadLibrarySpyDlg 类负责对话框自身的处理,同时也是暗中监视 CApplicationDebugger 派生类的线程宿主。CModuleListCtrl 类负责显示附属到每个DLL的详细信息 CModuleInfo*;针对每个 DLL,这个类存储的详细信息见 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig5]Figure 5[/URL]。
      当某个 DLL 被加载,对话框便调用 AddModule 方法;反之卸载DLL时,则执行 RemoveModule 方法。这两个方法都以 UpdateModule 方法告终,从而更新与该 DLL 对应的 CModuleObject 对象的 m_nLoaded 或 m_nRemoved。如果不存在这样的对象,则会创建一个新的对象,并将它添加到列表框中。
      不要为 m_nLoaded 或 m_nRemoved 而困惑。如果你针对某一行的相同 DLL 多次调用 LoadLibrary,调试器只会收到 LOAD_DLL_DEBUG_EVENT 一次,并且 m_nLoaded 被赋值为 1。如果调试器接收到某个 DLL 的 UNLOAD_DLL_DEBUG_EVENT,你便可以确定该 DLL 不再被该进程使用。因此,对于静态 DLLs 而言,你决不会收到此事件,即使可能在进程被启动后,它们被动态加载并用 LoadLibrary/FreeLibrary 卸载。

    处理被调试程序的事件

      一旦被调试程序的进程启动后,调试器便等待某些事件的发生。这就是为什么它应该在一个与主 UI 线程不同的单独线程中的原因,当主窗口是一个模式对话框时尤其如此!
      为了在 CLoadLibrarySpyDlg 中有效地使用 CApplicationDebugger,GoThreadProc 线程过程首先声明一个 CApplicationDebugger 对象,指定要执行的命令行并说明是否截获来自被调试程序的 OutputDebugString 或 TRACE 输出。接着,DebugProcess 阻塞,直到被调试程序终止(接收 EXIT_PROCESS_DEBUG_EVENT 或第二次的未处理异常),或者重写的方法之一未返回 DBG_CONTINUE。
      线程与对话框之间的沟通机制很简单:当某个被调试事件发生时,调试器线程将 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig6]Figure 6[/URL] 中所列的消息发送到对话框。其中第一个消息是在加载了所有静态链接的 DLLs 时发送;也就是说,当 Windows 触发第一个(伪)断点时,便发信号给调试器,然后调试器调用可重写的 OnProcessRunning 将消息发送给对话框。第二个消息是当被调试程序卸载某个 DLL 时,由可重写的 OnUnloadDLLDebugEvent 调试事件处理例程发送
      第三个消息需要所解释几句,为了创建 CModuleInfo,需要 DLL 的全路径名。而在本文第一部分中,我们没有提供任何方法直接从其 hModule 或加载地址获取 DLL 文件名。即便是当调试器接收到此事件时(因为它可能浏览到了它的 PE 头),DLL已经被映射到被调试程序的地址空间,这时,Windows 还没有初始化 PSAPI 所需的数据结构。
      事实上,LoadDll.lpImageName 域是一个 LOAD_DLL_DEBUG_INFO 结构成员,LOAD_DLL_DEBUG_INFO 来自 DEBUG_EVENT 结构中的联合 u(参见 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig12]Figure 2[/URL]),LoadDll.lpImageName 总是指向被调试程序地址空间中一块具备读/写/执行权限的奇怪的内存区域,LOAD_DLL_DEBUG_INFO 结构定义如下:

    typedef struct _LOAD_DLL_DEBUG_INFO {
      HANDLE hFile;
      LPVOID lpBaseOfDll;
      DWORD  dwDebugInfoFileOffset;
      DWORD  nDebugInfoSize;
      LPVOID lpImageName;
      WORD fUnicode;
    } LOAD_DLL_DEBUG_INFO, *LPLOAD_DLL_DEBUG_INFO;   
    被加载的DLL的路径名就包含在此内存块中。MSDN 在线帮助文档是这样描述 IpImageName 的:
        “...与 hFile 关联的文件名指针。该成员可能为 NULL,也可能包含被调试进程地址空间中的串指针地址。这个地址可能为 NULL 或者指向实际的文件名。
      如果 fUnicode 是一个非零值,则名字串是 Unicode,否则是 ANSI 串。该成员是可选项。调试器必须考虑处理 lpImageName 为 NULL 或 *lpImageName(在被调试进程的地址空间中)为 NULL 的情况。很显然,系统决不会为某个创建进程事件提供映像名,同时它也不可能为第一个 DLL 事件传递映像名。系统也决不会在源于 DebugActiveProcess 函数调用的调试事件中提供这个信息。”

      OnLoadDLLDebugEvent 可重写方法将上述解释翻译为在 99% 的情况下可工作的纯 C++ 代码。其余 1% 不工作的情况是指加载 ntdll.dll:这种情况既是文档中所说的第一个 DLL 事件。即使延迟到下一个被调试程序事件发生时(参见 CLoadLibraryDebugger 的 OnDebugEvent)才获取路径名。在文档的描述中,可以调用 SearchPath 从模块名获得全路径名,“system32”对于 ntdll.dll 并不感到惊讶。这个 API 函数使用与 LoadLibrary 同样的算法在文件系统中查找某个 DLL。从理论上讲,因为它是由调试器调用的,有可能返回的文件并不是被调试程序加载的那个文件——例如,在调试器文件夹中存在另外一个版本的 ntdll.dll。在实际应用中,ntdll.dll 得不到打补丁的机会,并且被拷贝到了某个与 system32 不同的目录。

    防止泄漏

      文档中关于 Win32 调试 API 的另一方面的描述是必须释放不同的 XXX_DEBUG_EVENT 结构返回的句柄。Matt Pietrek 在其 November 1995 MSJ“Under the Hood”专栏文章中指出:在 XXX_DEBUG_EVENT 结构中返回到调试器的句柄应该被关闭。事实上,几乎每个句柄都必须用 CloseHandle 关闭。只有一个例外,就是存储在 CREATE_THREAD_DEBUG_EVENT 中的线程句柄,它应该在进程终止时由系统来关闭。其它的句柄如果不关闭,便会造成增长速度非常快的系统资源泄漏,有关的句柄如 [URL=http://www.vckbase.com/document/journal/vckbase47/figures/EscapeFromDLLHellPart2Fig.htm#fig7]Figure 7[/URL] 所示。这类垃圾的收集由 CApplicationDebugger::HandleDebugEvent 自动处理。
      不论你使用哪种清除方法,每次你调试某个进程时,系统不可避免地要泄漏两个句柄:信号机(semaphore )和端口(port),两者都没有命名。为了让你确信 CApplicationDebugger 不负责处理这种泄漏,请允许我指出:用 [URL=http://www.sysinternals.com/]sysinternals[/URL] 的 ProcessExplorer 或 Windows Resource Kit 中的 DH.EXE 可以观察到 Visual Studio 6.0 和 Visual Studio .NET 中同样的泄漏行为。
      现在你已经看到了如何用 Win32 调试 API 来获取某个进程执行期间在其地址空间中加载和卸载的 DLLs 确切列表。Windows 本身提供了另外一个途径来获取有关 DLLs 的其它详细信息。

    参考资料

    [URL=http://msdn.microsoft.com/library/en-us/dndebug/html/msdn_debugEH.asp]The Win32 Debugging Application Programming Interface[/URL];
    [URL=http://msdn.microsoft.com/msdnmag/issues/0400/bugslayer/default.aspx]Bugslayer: Windows 2000 and LDR Messages, A COM Symbol Engine, Finding Bloated Functions, and More DEB Sample: Debug Event Browser[/URL];
    [URL=http://msdn.microsoft.com/isapi/gosupport.asp?TARGET=/?id=190351]Spawn Console Processes with Redirected Standard Handles[/URL];
    [URL=http://msdn.microsoft.com/isapi/gosupport.asp?TARGET=/?id=228469]GetWindowModuleFileName & GetModuleFileName Work Only with the Calling Process[/URL]
    在后续文章中,我将介绍 Windows Loader,它知道一切。

    (待续)
     

    按此在新窗口浏览图片 作者简介
        Christophe Nasarre
    是法国 Business Objects 公司的技术经理(technical manager)。他在 Windows 平台上(3.0 以后的版本)编写了若干个低级工具。他的联系方式:cnasarre@montataire.net.
    .
    本文出自 [URL=http://msdn.microsoft.com/msdnmag/default.aspx]MSDN Magazine[/URL] 的 [URL=http://msdn.microsoft.com/msdnmag/issues/02/08/default.aspx]August 2002[/URL] 期刊,可通过当地报摊获得,或者最好是 [URL=http://msdn.microsoft.com/msdnmag/subscribe.aspx]订阅[/URL]

    按此在新窗口浏览图片本文由 [URL=http://www.vckbase.com/vckbase/msdnmtt]VCKBASE MTT [/URL]翻译

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/11 13:37:00
     
     GoogleAdSense射手座1984-11-30
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/5/10 21:40:39

    本主题贴数6,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    125.000ms