定制调试诊断工具和实用程序

本文假设你熟悉 Win32,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 加载的地址是实际地址还是首选地址,以及它们的从属性是静态的还是动态的。这些工具的源代码和可执行程序都可以从本文的下载链接中下载,它们也许不完全满足你的需要,但可以作为技术参考,对编程工作肯定是有所裨益的。

    摘要

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


    如何获取运行进程列表


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

    (表一)

    方法平台备注
    PSAPIWindows NT,Windows2000,Windows XP获取进程,驱动器,模块,内存和工作集信息
    性能计数器Windows NT,Windows2000,Windows XP提供除进程清单以外的关于进程的更多信息,可在远程机器上使用。
    TOOLHELP32Windows 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 项下列出的应用程序就属于此列,那么枚举进程更棘手。有关细节参见本文参考资料中的知识库文章,以及 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的细节
    GetSessionIDProcessIdToSessionId (参见对快速用户转换的讨论部分——Windows XP的一个新特性)
    GetModuleListCModuleList是一个对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例子程序里的某个进程上双击鼠标按键,它将终止这个进程。你看一下在例子代码中的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)来获取进程的宿主。

    如何获取进程的命令行
     

    方法描述
    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的细节
    GetSessionIDProcessIdToSessionId (参见对快速用户转换的讨论部分——Windows XP的一个新特性)
    GetModuleListCModuleList是一个对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 的文章:CreateRemoteThread
      第三种方法是代码重用,或者说得更确切一些是输出重用。你可以通过捕获TLIST程序的输出,然后解析这些输出,从而获得命令行。在本文的后面部分,我们会对这个方法进行完全的讨论。


    参考资料

    下回我们将讨论如何通过 WTS APIs(Windows 终端服务 APIs)获取进程信息。

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


      Windows XP 有一个新特性叫做“快速用户转换——Fast User Switching”,这个特性允许多个用户同时在一台机器上登陆。当一个用户登陆后,另一个用户启动的进程仍然能够运行。这个神奇的特性所倚仗的是 WTS APIs。如果你想了解更多有关 WTS 的内容,可以参考 MSJ Oct99 的一篇文章:“Windows NT和 Windows 2000 终端服务APIs介绍”,作者是 Frank Kim。
      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 的缘故。

    列举加载的模块

      任何时候通过 PSAPI 或 TOOLHELP32 都可以列出某个进程加载的 DLLs 列表。在写此文前的调研过程中,我研究了 Matt Pietrek 以前在 MSJ Under The Hood 专栏中的一篇文章,其内容是讨论如何使用 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 的 Q&A Win32 专栏以及 1999.8 的 Security Briefs 专栏


    关于 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 在他的 C++Q&A 专栏(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 资源管理器文件属性对话框呢?关于这个请参考 精华区的一小段代码。其关键是先填写 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++ 类来定制满足自己需要的调试工具。

    • 0
      点赞
    • 1
      收藏
      觉得还不错? 一键收藏
    • 0
      评论
    基本简介   Process Explorer让使用者能了解看不到的在后台执行的处理程序,能显示目前已经载入哪些模块,分别是正在被哪些程序使用着,还可显示这些程序所调用的 DLL进程,以及他们所打开的句柄。Process Explorer最大的特色就是可以中终任何进程,甚至包括系统的关键进程!   Process Explorer 的显示有两个子窗口所组成,窗口上方会显示出当前活跃的程序进程,包括它们的详细描述,窗口下方则会显示出通过资源管理器模式运行的应用程序的相关信息,以及它们所调用的DLL和打开的句柄,该版本可以运行在windows NT/2K/XP/2003系统平台 [编辑本段]Process Explorer的独特之处   1.显示被执行的映像文件的完整路径   2.显示进程安全令牌   3.加亮显示进程和线程列表中的变化   4.显示作业中的进程,以及作业的细节   5.显示运行。NET/WinFX应用的进程,以及与.NET相关的细节   6.显示进程和线程的启动时间   7.显示内存映射文件的完整列表   8.能够挂起一个进程   9.能够杀死一个线程 [编辑本段]Process Explorer的现状   由Sysinternals开发的一个高级的Windows系统和应用程序监视工具,目前已并入微软旗下。此版本的Process Explorer 不仅结合了Filemon(文件监视器)和Regmon(注册表监视器)两个工具的功能,还增加了多项重要的增强功能。包括稳定性和性能改进、强大的过滤选项、修正的进程树对话框(增加了进程存活时间图表)、可根据点击位置变换的右击菜单过滤条目、集成带源代码存储的堆栈跟踪对话框、更快的堆栈跟踪、可在 64位 Windows 上加载 32位 日志文件的能力、监视映像(DLL和内核模式驱动程序)加载、系统引导时记录所有操作等。   现在Process Explorer 11.21已并入Windows成为Microsoft Process Monitor 1.37。

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

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

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值