以文本方式查看主题 - 计算机科学论坛 (http://bbs.xml.org.cn/index.asp) -- 『 C/C++编程思想 』 (http://bbs.xml.org.cn/list.asp?boardid=61) ---- 摆脱DLL"地狱"困扰[分享] (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=52521) |
-- 作者:一分之千 -- 发布时间:9/11/2007 1:08:00 PM -- 摆脱DLL"地狱"困扰[分享] 摆脱DLL"地狱"困扰之目前的困境 在 Windows 系统中,动态库版本冲突实在是一个老大难的问题了,为了解决这个问题,除了使用大量现有的工具外,你还可以利用丰富的 Windows APIs 函数构造自己的调试工具和实用程序。作为例子,本文将提供三个这种类型的工具,讨论如何利用它们来解决动态库的冲突问题。这三个工具分别是: 本文我们先用一些现有的工具来考察系统中的运行进程,然后系统地研究本文提供的三个工具: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。如图二: 除了列出被某个进程使用的 DLLs 之外,还可以用这个工具了解某个程序用到了哪个 kernel 对象,从版本3.11之后,Process Explorer 还可以让你在两个快照之间轻松扫描到新的或未使用的对象。 有时候在你用 Process Explorer 扫描到某个进程之前,它可能已经被加载然后又在很短的时间内被卸载了。碰到这种情况时,你需要另外一种类型的工具,我们将在后文中讨论。 为了操纵进程和 DLLs,首先你必须知道每一个被加载的 DLL 被哪些进程使用。本文的例子程序 DllSpy 实现目的即在于此。如图三所示: DllSpy程序上面的窗格列出的是所有已经加载的 DLL,每选中一个DLL,在下面的窗格中就会列出使用该 DLL 的所有进程。 而 ProcessSpy 例子程序的功能正好与 DllSpy 相反,它在上面窗格列出系统中所有的运行进程,每选中一个进程,在下面窗格便显示出此进程使用的所有 DLLs,如图四所示: 下面窗格还反映了 DLL 加载的地址是实际地址还是首选地址,以及它们的从属性是静态的还是动态的。这些工具的源代码和可执行程序都可以从本文的下载链接中下载,它们也许不完全满足你的需要,但可以作为技术参考,对编程工作肯定是有所裨益的。
[此贴子已经被作者于2007-9-11 13:39:25编辑过]
|
-- 作者:一分之千 -- 发布时间:9/11/2007 1:10:00 PM -- 摆脱DLL"地狱"的困扰之获取进程信息 摘要 本文讨论用各种不同的方法来获取系统中运行的进程信息,比如,进程列表,枚举列表中的进程,然后获取关于进程的详细信息。 如何获取运行进程列表 有三种方法来获取Win32运行进程的信息,参见表一: (表一) 本文不打算讨论 TOOLHELP32,因为 MSDN 中提供了很多使用 TOOLHELP32 函数的例子代码。性能计数器提供的信息更多,不仅仅是进程清单。如果你想获取远程机器的信息,那么性能计数器是再好不过的工具了。如果你总是想得到另外一台机器的进程列表信息,那么就用性能计数器吧! 进程状态 API(PSAPI 全称是 Process Status API)是微软 SDK 中一个很有用的工具,在例子程序中有一个类 CProcessList,其实现文件 Process.cpp 对 PSAPI 进行了打包,用这个类可以获取进程清单。只要 Refresh 一被调用,通过某个进程的ID便可获得此进程的描述信息,并很容易用 GetFirst 和 GetNext 列举出其它进程: //用 CProcessList 列出运行进程 // 存储当前进程列表 // 获取进程快照 // 将CProcess 对象捆绑到每一个进程ID // 将进程信息添加到映射 // 第二次循环需要知道此进程的子进程清单 如何获取进程信息 有了运行进程的列表,接下来就是根据 EnumProcesses 返回的进程IDs尽可能多的获取每一个进程的详细信息,然后根据这些信息建立有用的工具。用PROCESS_QUERY_INFORMATION | PROCESS_VM_READ作为参数,调用OpenProcess获取进程句柄,然后用AttachProcess(参见Process.cpp文件中的CProcess类实现)方法创建进程描述。表二中列出的是CProcess用于获取进程细节信息的函数: (表二) 这里有几个关于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)来获取进程的宿主。 |
-- 作者:一分之千 -- 发布时间:9/11/2007 1:15:00 PM -- 摆脱DLL"地狱"的困扰之定制调试诊断工具 本文假设你熟悉 Win32,DLL 如何获取进程的命令行 上表列出的方法中有一个是 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 第三行的参数/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程序的输出,然后解析这些输出,从而获得命令行。在本文的后面部分,我们会对这个方法进行完全的讨论。 |
-- 作者:一分之千 -- 发布时间:9/11/2007 1:16:00 PM -- 摆脱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 30 running processes ProcessXP 的源代码包含在本文可下载的压缩包中。WTS 有一个与注册表类似特性,那就是允许你获取另外一台机器的信息。这就是为什么WTS枚举 APIs 函数的第一个参数都是一个服务器句柄。WTS_CURRENT_SERVER_HANDLE 用于当前的机器。第二个参数是保留参数,值应该为0。第三个参数希望的版本,其值应该是1。最后两个参数用于存放返回的信息。一个用于存放会话数或进程数。另一个是结构数组的指针,结构可以是描述会话信息的结构,也可以是描述进程信息的结构。就看你是使用哪个枚举API,是枚举会话还是枚举进程。因为数组的存储空间是由 WTS 分配的,你必须要记住用 WTSFreeMemory 释放这个空间。 下面是描述会话的结构:WTS_SESSION_INFO: typedef struct _WTS_SESSION_INFO 下面是描述进程的结构 WTS_PROCESS_ INFO: typedef struct _WTS_PROCESS_INFO |
-- 作者:一分之千 -- 发布时间:9/11/2007 1:36:00 PM -- 列举加载的模块 任何时候通过 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遍历模块 // // 迭代快照中每一个模块 for (fModWalkContinue = pfnModule32First(hSnapshotModule,&modEntry); // 确定是否为我们已有的DLL // 如果以前没有见过,则将它加入列表 // 将此进程加入到使用此DLL的进程列表 其实并不是程序有什么瑕疵,主要是时过境迁,导致代码中一个if语句的使用无效,毕竟 Matt Pietrek 写那篇文章的时候(其代码是1998.9 在 MSJ 上发布的),Windows 2000 还不知道在哪里呢! 实际上,如果失败,hSnapshotModule的值为INVALID_HANDLE_VALUE或-1,并且这个if语句是捕获不到它的,这到没什么,关键是如何发现这个bug。当我在Windows 2000上测试ProcessSpy时,一切运行正常,只是当列表框即便为空的时候,程序也没有返回某些进程的出错信息。由于错误处理代码本身是错的,执行跳过了循环,Module32First调用失败,但没有任何实质性的错误。如果你在Windows 2000环境用Matt Pietrek的这篇文章提供的ModuleList工具,你将得到不正确的结果。 存取器 我前面提到过,想要获取模块的全路径名需要一点诀窍。由于一些原因,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 文件。 存取器 (表五) 标志 (表六) 标志 本文以后的内容将讨论几种用非常规方式来获取一些附加的信息源。也就是说如果在没有可借助的 API 的情况下,你就可以用这几种非常规方式。其中包括我至今未曾提到的一个主要信息源,那就是 Windows 的外壳(Shell)。在模块文件中隐藏一个文件的时候, 关于某个文件的信息,没有人比 Windows 资源管理器知道的更多。如图十八所示: SHELLEXECUTEINFO sei; 在 ProcessSpy 程序界面底部窗格中任何一个模块记录上双击鼠标便可以调出文件的属性对话框,相应模块文件的描述信息一目了然。注意 Windows XP 中不支持多个 ShellExecuteEx 调用,当你调用第二次时,线程冻结,也不会有任何提示。 [URL=http://www.vckbase.com/document/viewdoc/?id=1482]如何用 Win32 APIs 枚举应用程序窗口和进程[/URL]
|
W 3 C h i n a ( since 2003 ) 旗 下 站 点 苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》 |
125.000ms |