《Windows 用户态程序高效排错》市场价元 特价元 购买>> 不可思议一个API同时打开了两个文件 问题描述 有一天一个电话打进来客户非常气愤地抱怨调用ShellExecute这个API传入本地的一个文本文件的路径有时候会在打开这个TXT的同时还打开另一个不相干的文件!客户非常明确地告诉我所有的参数肯定没有传错而且ShellExecute的返回值也正确 第一印象 我仔细思考了两分钟后告诉客户不可能发生上述问题如果参数正确API的行为肯定是唯一的(在往下面阅读以前请您花两分钟思考这有可能吗?) 我的第一感觉是用户的参数传递错了比如打开的是一个BAT文件或者打开方式被用户修改过这样才有可能同时打开两个文件但如果真的是这样程序的行为也应该是唯一的而不会有时候才出现 事实是我错了在查看了用户的截图并且用客户的代码在本地重现了一次问题后我不得不相信一次ShellExecute调用会偶尔打开两个文件 在下面的分析中你会看到该问题是如何发生的但是请先记住第一点相信事实不要相信经验如果我胸有成竹地告诉客户这种问题肯定跟代码不相关唯一的可能性是出自一些防病毒之类的程序(防病毒程序的确是很多问题的根源但是有时也是挡箭牌)那么就失去了找到真相的机会 深入分析 背景是这样客户用MFC开发了一个Dialog Application 上面放了一个HTMLView Control这个Control会显示本地的一个HTML文件这个HTML文件会显示一个GIF图片当用户在这个GIF图片上点鼠标右键默认的IE右键菜单被用户自定义的菜单代替当用户点击其中一个菜单命令后在客户的消息响应函数中调用ShellExecute打开本地的一个TXT文件TXT文件类型跟UltraEdit绑定所以文件会被UltraEdit打开当问题发生的时候UltraEdit会在打开TXT的同时打开另外一个二进制的文件经过分析这个二进制的文件就是那个 HTML中显示的GIF文件 客户在PreTranslateMessage函数中用下面的代码弹出自定义菜单代替IE默认菜单 if(pMsg>message==WM_RBUTTONDOWN) {CMenu menu; menuLoadMenu(IDR_MENU_MSG_OPEN); CMenu *pMenu = menuGetSubMenu(); if (pMenu) { CPoint pt; GetCursorPos(&pt); pMenu>TrackPopupMenu(TPM_LEFTALIGN ptx pty this); } return FALSE;} 有了这样的背景问题跟用户的代码就联系起来了额外打开的文件(GIF)的确是跟这个程序(HTMLView Control)相关的那下一步我们该做什么呢? 当时我的思路是这样的ShellExecue能打开这个GIF文件肯定在某种程度上这个GIF跟ShellExecue的调用有联系 至少ShellExecute要知道这个GIF的路径才可以打开它为了进一步分析可以选择的步骤是 打开Windbg(和Visual Studio Debugger类似的调试工具后面有详细介绍)在ShellExecute上设断点然后开始调试一步一步走下去看这个GIF文件是如何打开的 通过阅读客户的代码和做进一步的测试来缩小问题的范围 仔细思考后我放弃了第一种原因是首先打开文件的操作不是在ShellExecute中完成的而是在UltraEdit中完成的目标和范围都太大操作起来很难其次问题是偶尔发生的所以无法保证每次调试都能够重现问题 通过上面的分析和测试没有花费太多的精力就找到了解决问题的方法但是这个时候就开始高兴未免太早这里把False修改成True给客户带来了新问题上面的分析也不足以揭示为什么简单的True/False就给ShellExecute带来这么大的影响让我们继续走下去 首先解释一下带来的新问题是什么在菜单的消息响应函数中客户除了用ShellExecute打开文本文件外还需要用HTMLDocument的一些属性和操作来获取当前用户是在HTML页面中的哪一个tag上点的鼠标比如是在<img>标签呢还是<div>标签如果修改成True上面的功能就无法正常工作这个问题很好解释由于HTMLView无法继续处理鼠标消息当然得不到用户的操作信息 问题的另一方面还是在于为何会打开两个文件PreTranslateMessage的返回值带来两种不同的表现所以我考虑分析ShellExecute在两种情况下表现的差异首先反汇编ShellExecute发现里面的实现非常复杂不可能首先读懂代码后再进行调试一个高效的办法是通过Windbg的wt命令(这里不详细介绍这些命令的用法留到下一章)观察ShellExecute内部的调用栈(callstack)研究后发现ShellExecute是通过DDE的方法来打开目标文件的在研究了MSDN的说明以后了解到DDE其实内部是依靠WindowsMessage来完成进程之间通信的于是又在PostMessageW/SendMessageW上设定条件断点每次调用这两个函数的时候就把WindowsMessage的详细信息和callstack在windbg中打印出来以便比较不同情况下的差异最后看到发生问题时候的callstack是这样的您能想清楚原因吗? |