|
|
|
|
|
|
|
C/C++本地代码调试
帖子发起人: dos 发起时间: 2008-08-18 17:29 下午 回复: 11
|
帖子排序:
|
|
|
|
2008-08-18, 17:29 下午
|
dos
注册: 2008-08-05
发 贴: 17
|
请教一个单线程库在多线程环境中使用的问题
|
|
|
|
前阵子写程序,为一个服务器产品做客户端开发,使用该服务器产品的客户端 C++接口API。
该产品客户端和服务器使用CORBA协议通讯,好在我不需要用CORBA协议写客户端程序,该产品的C++ API接口已经封装了CORBA通讯的细节,我只需要直接调用API的DLL导出方法就好了。
一切似乎很简单明了,程序没有什么困难地写好了。开始进入调测阶段,发现程序不定期地崩溃,崩溃时弹出的框框里写着“Runtime error! Programm:........ This application has requested the Runtime to terminate it in an unusual way,please contact.....”
然后只有一个“确定”按钮等着按...
为了发现为什么会这样,就在VC的调试器里用debug模式运行,发现也会弹出这样的框,然后用Windbg启动程序,居然也会弹出这样的框框,最后索性在调用客户端API的方法时,全部加上try catch ,结果,还是弹这个框框!
我奇怪的是,即便那个客户端API的DLL发现我误用它的导出函数,抛出异常了,可是我用try catch包住了所有的API调用,为什么却抓不到这个异常呢?我甚至不知道在什么时候调用了什么,导致了这个runtime error,郁闷...
后来我在出现这个框的时候,使用windbg的dump功能,dump了一个文件,80多M吧,看了下堆栈,好像也看不出什么线索,当然可能也是我不会看...
后来发现客户端API的DLL自己也会记日志,在出现弹框错误的时候,发现API的日志如下(部分):
!Misuse of toolkit memory allocator, multiple threads in singlethreaded code, error 1
....
注意第一句,好像是说它是单线程的,我却在多线程下使用了(我的程序确实多线程地调用它的DLL导出函数了)。我想问题的根源找到了,改了程序,框框不弹了。
不过我依然很困惑的是:
1、如果客户端API自己不记日志,我如何发现这个问题呢?
2、为什么在调试环境下,甚至加了try catch,那个runtime error的框还是会执着地弹出呢?能不能在弹出前截获这个异常,我至少可以看看这时候的调用堆栈,看看是什么函数出问题了啊?
3、还有一个困惑就是,在已经弹出runtime error框框后,再dump,是不是已经没有用了呢(我指失去分析是什么导致了runtime error的线索)?还是其实有用只是我不会看呢?
诸多困惑,困扰数日,希望能指点一下分析思路或方向。
先谢过咯~
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-18, 22:24 下午
|
格蠹老雷
注册: 2005-12-19
发 贴: 1,303
|
|
|
问题1,其实你可以在错误框弹出时,将被调试程序中断到调试器(在WinDBG中按Ctrl+Break,在VC菜单中选Break),然后观察每个线程的栈回溯。上面这个错误应该是同步的,所以应该可以从栈回溯中看到线索。
问题2,这个运行期错应该就是运行库检查到不合适的用法后,直接弹出错误框,我推测它就是个普通的:
if(something error)
{
msgbox(...);
}
根本没有异常发生。当然,报告运行期错误的过程要经历很多步骤(参考《软件调试》的清单21-12)。
问题3,对于这个问题在错误框弹出时,做DUMP来得及,打开DUMP后,首先设置好符号后,可以使用~* kPL命令来显示各个线程的栈回溯,然后找到错误框所在的线程,分析其来龙去脉。
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-19, 11:30 上午
|
dos
注册: 2008-08-05
发 贴: 17
|
|
|
谢谢张老师指点迷津!
在问题2,张老师猜测可能是服务器提供的API开发库直接用MessageBox弹出的框,其实并没有异常。
我研究了一下,那个框好像是VC运行库弹出的著名Runtime error框。后来用Windbg打开保存的crash dump文件,用~* kPL看了下栈回溯,发现确实是msvcr71模块弹的(从这一点,可以推测这个产品提供的客户端API的DLL应该是用VS .net 2003编译的,因为我用的是VS 2008,对应模块应该是msvcr90了)。
下面是crash dump的部分栈回溯:
0:004> ~* kPL
0 Id: 13b0.15dc Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr
0013e438 77d19418 ntdll!KiFastSystemCallRet
0013e470 77d249c4 user32!NtUserWaitMessage+0xc
0013e498 77d3a956 user32!InternalDialogBox+0xd0
0013e758 77d3a2bc user32!SoftModalMessageBox+0x938
0013e8a8 77d663fd user32!MessageBoxWorker+0x2ba
0013e900 77d664a2 user32!MessageBoxTimeoutW+0x7a
0013e934 77d50877 user32!MessageBoxTimeoutA+0x9c
0013e954 77d5082f user32!MessageBoxExA+0x1b
0013e970 7c34c224 user32!MessageBoxA+0x45
0013e9a4 7c348e6c msvcr71!__crtMessageBoxA(
char * lpText = 0x0013e9d8 "Runtime Error!..Program: ...???",
char * lpCaption = 0x7c37f480 "Microsoft Visual C++ Runtime Library",
unsigned int uType = 0x12010)+0xf4
0013ebdc 7c34cf83 msvcr71!_NMSG_WRITE(
int rterrnum = 10)+0x12e
0013ec1c 7c34214f msvcr71!abort(void)+0x7
0013ec28 7c34f65c msvcr71!_unlock(
int locknum = )+0x13
0013ec30 7c34f742 msvcr71!_unlock_file(
void * pf = 0x05134ae6)+0x22
0013eca4 04f13352 msvcr71!fflush(
struct _iobuf * stream = 0x0515a444)+0x4e
0013edfc cccccccc SoftAIC!CSoftAICCtrl::CancelTransfer(void)+0x1e2
WARNING: Frame IP not in any known module. Following frames may be wrong.
0013ee00 cccccccc 0xcccccccc
0013ee04 cccccccc 0xcccccccc
0013ee08 cccccccc 0xcccccccc
0013ee0c cccccccc 0xcccccccc
后面的几行比较奇怪,有可能就是导致runtime error的罪魁祸首的地方吧。不过,由于当时的pdb文件没有保存下来,我参考了张老师那本书第30章推荐的"加载不严格匹配的符号文件",加载了修改代码后程序可以正确运行的pdb文件,所以,栈回溯也可能不太对。
现在的困惑是2个:
1、这个runtime error的框是怎么弹出的呢?
2、难道不能拦到弹runtime error前的异常么?
由于我没有客户端API DLL的源代码(厂家也应该不会提供给我源代码,呵呵~),所以不知道我干了什么,那个DLL就激发出msvcr71的runtime error框。
我就想能不能自己DIY一个DLL,导出一个函数,看能不能也让程序弹出个runtime error框。
我就做了下面的试验:
1、建一个mfc 的dll工程,只导出一个函数ThrowExceptionFun,代码很简单:
extern "C" BOOL PASCAL EXPORT ThrowExceptionFun(bool bThrow)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
// normal function body here
if(bThrow)
{
throw(1);
}
return true;
}
2、建一个mfc的exe工程,对话框,增加两个按钮,第一个按钮按下就调用ThrowExceptionFun(false),第二个按钮按下,就调用ThrowExceptionFun(true).响应第二个按钮按下的函数如下:
void CTestThrowDllDlg::OnBnClickedException()
{
// TODO: Add your control notification handler code here
ThrowExceptionFun(true);
}
使用release方式编译后,双击运行exe程序,鼠标点第一个按钮,什么也没有发生,跟预期的一样;鼠标点第二个按钮,出现框框了,就是那个著名的VC runtime library error框!除了中间那行标识程序路径及名称的"Program:...."跟最初的不一样外,其它的信息,包括框框的标题,都是一样的!
于是心中窃喜,猜测那个产品的客户端开发API DLL是不是也是使用了throw exception的方式,导致我的程序弹runtime error框呢?
不过验证这个猜测先放一边,我现在的兴趣已经转移到:难道throw except不能用try catch捕获么?(因为我曾经把那个客户端API调用每个都加了try catch,结果还是会弹框的,张老师也因此推测是不是根本没有异常而仅仅是调用弹框函数)
于是我就把那个mfc的exe对话框程序改了下,第二个按钮按下的代码如下(注意,原来是没有try catch的):
void CTestThrowDllDlg::OnBnClickedException()
{
// TODO: Add your control notification handler code here
try
{
ThrowExceptionFun(true);
}
catch(...)
{
MessageBox("Catch you!");
}
}
release方式编译后,双击运行exe,点第二个按钮,倒~...真的没有catch住,那个runtime error框执着地弹了出来,而不是Catch you的对话框!
下面是在弹出runtime error后,用windbg的attach to a process显示的栈回溯:
0:001> ~* kPL
0 Id: 15b0.115c Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr
0013e7f8 77d19418 ntdll!KiFastSystemCallRet
0013e830 77d249c4 USER32!NtUserWaitMessage+0xc
0013e858 77d3a956 USER32!InternalDialogBox+0xd0
0013eb18 77d3a2bc USER32!SoftModalMessageBox+0x938
0013ec68 77d663fd USER32!MessageBoxWorker+0x2ba
0013ecc0 77d664a2 USER32!MessageBoxTimeoutW+0x7a
0013ecf4 77d50877 USER32!MessageBoxTimeoutA+0x9c
0013ed14 77d5082f USER32!MessageBoxExA+0x1b
0013ed30 7858d748 USER32!MessageBoxA+0x45
0013ed68 78542675 MSVCR90!__crtMessageBoxA+0x160
0013ed90 78592211 MSVCR90!_NMSG_WRITE+0x16f
0013f0c8 7857bb47 MSVCR90!abort+0x26
0013f0f8 00401ea6 MSVCR90!terminate+0x33
0013f100 7c864031 TestThrowDll!__CxxUnhandledExceptionFilter(
struct _EXCEPTION_POINTERS * pPtrs = 0x0013f398)+0x3c
0013f370 7c843892 kernel32!UnhandledExceptionFilter+0x1c7
0013f378 7c839b21 kernel32!BaseProcessStart+0x39
0013f3a0 7c9232a8 kernel32!_except_handler3+0x61
0013f3c4 7c92327a ntdll!ExecuteHandler2+0x26
0013f474 7c92e46a ntdll!ExecuteHandler+0x24
0013f474 7c812aeb ntdll!KiUserExceptionDispatcher+0xe
不知道msvcr90的__crtMessageBoxA参数就显示不出来了...参考上面的msvcr71的__crtMessageBoxA,它可是明明白白地显示了参数...
百思不得其解后,又猜测,是不是运行的程序不是自己修改后加了try catch的代码编译出来的那个exe啊(有时候搞错路径也是常有的),所以为了保险起见,我就把代码改了一下,让它在产生异常之前先弹一个框,这样就可以确认运行的程序是不是我修改后的那个exe了,代码如下:
void CTestThrowDllDlg::OnBnClickedException()
{
// TODO: Add your control notification handler code here
try
{
MessageBox("Before Catch!");
ThrowExceptionFun(true);
}
catch(...)
{
MessageBox("Catch you!");
}
}
release编译后,双击exe后,点第二个按钮,弹了一个框,框里显示"Before Catch!",嗯,表明我还没有糊涂,双击的exe就是修改代码加了try catch后编译出来的程序;
点“确定”后,奇怪的事情发生了!
是的,又弹了一个框,框里面写着"Catch you!",倒~....
我终于糊涂了,这个框表示的意思是显而易见的,可是我除了在调用会产生异常的函数之前加了个MessageBox之外,什么也没做啊。
还望哪位达人能解心中疑惑:
1、为什么try catch 好像抓不到第三方模块用throw抛出的异常呢?(不放假设那个mfc dll就是第三方模块嘛,虽然没有哪个第三方模块有这么简陋的,呵呵~)
2、为什么调用前加了MessageBox后,又似乎能抓到第三方模块用throw抛出的异常呢?
先谢谢啦....
BTW,再次感谢张老师在深夜还耐心地对我这种比较菜鸟的问题给予关注。
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-19, 12:54 下午
|
格蠹老雷
注册: 2005-12-19
发 贴: 1,303
|
|
|
dos,
仔细读一下《软件调试》576页-577页上方的描述,你就会明白了。
简单来说,在有调试器的时候,确实会有异常发生,也就是被调试进程中的CRT与调试器通信的异常,但是这个异常会先发送给调试器(《软件调试》P945),而且VC调试器会宣布处理它,所以你的try{}catch()就catch不到了。
当没有被调试时,CRT的错误报告函数不再会抛出异常。
CRT使用的异常代码是很特殊的代码,VC是可以识别的,因此上面的逻辑不会影响你用throw关键字抛出的异常。
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-19, 15:33 下午
|
dos
注册: 2008-08-05
发 贴: 17
|
|
|
嗯~谢谢张老师解惑。
因为张老师的书太厚,不方便随身携带,等我回家仔细研究下~....
考虑是不是建议部门也买一本,放办公室,大家查阅也方便,呵呵~..:)
在仔细看书前,还是忍不住想请教张老师:
您刚刚简单地说了下“在有调试器的时候”的情况,以及“没有被调试时”的情况。可是我在做试验时,每次运行exe,都是在没有在调试器参与下运行的。步骤是:
1、修改代码;
2、编译链接成exe;
3、跑到release文件夹下,双击exe图标。
在程序运行期间也没有attach任何调试器。只是在已经出现了CRT的runtime error框之后,才attach了Windbg,列了一下栈回溯。
也就是说,两个编译出的exe在代码上唯一区别就是:
一个在调用函数ThrowExceptionFun(true) 前,加了一个弹框函数MessageBox("Before Catch!");
另一个没有加Messagebox调用。
而这两个exe的运行结果是不同的(都是在没有调试器参与的情况下运行的):
加了自己的弹框函数的会走到catch里,弹了“Catch you”的框;
没加的就没有走到catch里,直接弹出了CRT的runtime error框。
所以才让我感到惊奇:难道MessageBox调用会影响到try catch对throw异常的捕获么?还是我的机器Gflag配置啥的有问题呢。我挺想把我的VC工程作为附件附在帖子上的,想看看别人编译运行后是不是也这样。可惜我没有找到在哪儿贴附件...:(
不管怎么说~..还是先回去仔细研究下张老师的书再说,希望能有答案...也谢谢张老师百忙之中耐心解答。
唉~书到用时方恨少啊,时间怎么过得这么慢,等不及下班走人了,呵呵~...
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-19, 23:03 下午
|
dos
注册: 2008-08-05
发 贴: 17
|
|
|
看完奥运会男子3米跳板决赛,中国得了一金一铜,真是不容易,向他们表示祝贺~...
...
看奥运间隙,也不忘研读张老师的著作,当然,重点看了576-577,也看了前后相关的数页内容,参考了P945。感觉张老师对CRT的分析是很细致的...
不过,真的很惭愧,我还是没有发现为什么MessageBox调用会影响到try catch的运作。
唉~希望我没有“死读书”,或者“读死书”了,呵呵~...
张老师提到的576、577页,谈到的应该还是在有调试器的环境下的CRT处理,可是正如我上一个帖子说的,我测试发现一次可以catch到exception一次不可以,都是没有调试器参与的,都是是在文件夹里双击运行的程序的。
不知道是不是vc++ 2008编译器有什么特别。明天试一下用2005或2003,或vc6试一下...
不管怎样,看张老师的书,毫无疑问地获得了很多意想不到的知识...
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-20, 13:20 下午
|
格蠹老雷
注册: 2005-12-19
发 贴: 1,303
|
|
|
dos,
我很愿意看一下你的试验代码,你可以发到我的信箱中:yinkui.zhang@gmail.com
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-20, 15:39 下午
|
dos
注册: 2008-08-05
发 贴: 17
|
|
|
已经发了,谢谢张老师关注!
正如信中说的,在MSDN的帮助下,发现exe程序catch不到dll导出函数的异常,可能跟exe的“异常处理模型”/EH编译选项有关。在某些选项下,甚至如下面的代码都不会catch到异常,直接崩溃:
try
{
int* p = NULL;
*p = 0;
}
catch(...)
{
//do something
}
具体原因可以在MSDN的“/EH(异常处理模型)”中找到答案。
现在最大的困惑是,在同样的选项下,增加一个系统函数MessageBox的调用(甚至还没有调用,因为我把它移到“会产生异常的DLL导出函数”之后,实际上根本就不会执行到它,也能达到同样的效果),为什么会引起try catch的不同反应呢?
我猜测应该是编译器做了手脚。我想异常处理最终可能会利用windows的SEH吧,而SEH处理机制是不会因为我的代码加不加MessageBox而发生改变的。那一定是编译器悄悄作了不同处理。
有机会IDA一下看看...
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-20, 21:04 下午
|
格蠹老雷
注册: 2005-12-19
发 贴: 1,303
|
|
|
dos,
你的最后两句话离答案已经很近了。其实使用WinDBG或者VS观察一下汇编代码,答案就有了。
下面是没有MessageBox时,OnBnClickedException函数的代码:
TestThrowDll!CTestThrowDllDlg::OnBnClickedException: 00401460 6a01 push 1 00401462 e837000000 call TestThrowDll!ThrowExceptionFun (0040149e) 00401467 c3 ret
而下面是有MessageBox时的代码:
TestThrowDll!CTestThrowDllDlg::OnBnClickedException: 00401460 55 push ebp 00401461 8bec mov ebp,esp 00401463 6aff push 0FFFFFFFFh 00401465 68b0224000 push offset TestThrowDll!_allmul+0x40 (004022b0) 0040146a 64a100000000 mov eax,dword ptr fs:[00000000h] 00401470 50 push eax 00401471 83ec08 sub esp,8 00401474 53 push ebx 00401475 56 push esi 00401476 57 push edi 00401477 a120504000 mov eax,dword ptr [TestThrowDll!__security_cookie (00405020)] 0040147c 33c5 xor eax,ebp 0040147e 50 push eax 0040147f 8d45f4 lea eax,[ebp-0Ch] 00401482 64a300000000 mov dword ptr fs:[00000000h],eax 00401488 8965f0 mov dword ptr [ebp-10h],esp 0040148b 894dec mov dword ptr [ebp-14h],ecx 0040148e 6a00 push 0 00401490 6a00 push 0 00401492 68a0354000 push offset TestThrowDll!`string' (004035a0) 00401497 c745fc00000000 mov dword ptr [ebp-4],0 0040149e e8e5010000 call TestThrowDll!CWnd::MessageBoxA (00401688) 004014a3 6a01 push 1 004014a5 e864000000 call TestThrowDll!ThrowExceptionFun (0040150e) 004014aa 8b4df4 mov ecx,dword ptr [ebp-0Ch] 004014ad 64890d00000000 mov dword ptr fs:[0],ecx 004014b4 59 pop ecx 004014b5 5f pop edi 004014b6 5e pop esi 004014b7 5b pop ebx 004014b8 8be5 mov esp,ebp 004014ba 5d pop ebp 004014bb c3 ret 004014bc 8b4dec mov ecx,dword ptr [ebp-14h] 004014bf 6a00 push 0 004014c1 6a00 push 0 004014c3 68b0354000 push offset TestThrowDll!`string' (004035b0) 004014c8 e8bb010000 call TestThrowDll!CWnd::MessageBoxA (00401688) 004014cd b8aa144000 mov eax,offset TestThrowDll!CTestThrowDllDlg::OnBnClickedException+0x4a (004014aa) 004014d2 c3 ret
也就是说,当OnBnClickedException方法中的Try{}catch块中只调用了一个ThrowExceptionFun函数时,编译器将异常处理代码优化掉了。
优化掉的原因如你在EMAIL中说的,“默认的exe的release方式编译选项里是/EHsc,按MSDN上的说明,就是"仅捕获 C++ 异常并通知编译器假定 extern C 函数从未引发 C++ 异常"。而DLL导出函数恰好是用extern C声明了。”既然try{}catch中只有一段肯定不抛出异常的函数调用,那么这个异常处理就“没有用”了。从上面的汇编清单可以看到,裁掉后的确节约了很多空间(EXE的大小)和时间(运行时少执行很多准备异常捕捉的代码)。
加上了MessageBox调用后,因为MessageBox被认为是可能抛出异常,所以异常保护代码就在了。这也是为什么把MessageBox加在Throw前面还是后面都可以的原因。
找到了根源后,只要在OnBnClickedException方法前使用一下编译器指令禁止优化:
#pragma optimize( "", off )
那么即使没有MessageBox调用,那么也会捕捉到异常了。
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2008-08-21, 12:04 下午
|
dos
注册: 2008-08-05
发 贴: 17
|
|
|
如果从MSDN里发现编译器有不同的“异常处理模型”是“雾里看花”的话,张老师的分析就是让“云开雾散”了。非常感谢张老师的帮忙。
编译器的“优化”功能真是恐怖,要是不懂汇编语言,还真的难以发现事情的真相。
记得还有一个编译器优化的案例,就是关于全局变量的问题,如果不声明为“易挥发”的,你在一个线程的循环体里,试图根据这个全局变量的值来跳出循环的话,可能就会陷入死循环。看了汇编你会发现,原来编译器根据某种“优化法则”,把“对这个全局变量的判断”优化到循环体外面去了...
回头想想,人家优化得也没错,理由很充分啊:你自己明明已经告诉人家编译器那个函数“肯定不会抛出异常”了嘛(加了/EHsc编译选项),那try...catch不就是个摆设嘛,当然要把你优化掉。
听起来也没错。
不过这倒让我想起另一件事情:保险公司业务员忽悠顾客买保险的时候,顾客多少还是会仔细看看保单上各种索赔规定、条文的,觉得没啥问题,就买了。等有一天出险了,顾客拿出保单索赔的时候,保险公司告知不予理赔,并指着保单上的某条某款说,你看这里写得清清楚楚嘛,你自己也签了字的...经保险公司一解释,听起来也没错。
顾客终于明白那些经过保险公司高薪请来的法律专家字斟句酌写出的条文,原来包含如此多的机巧和涵义...我想,此刻顾客想学习法律知识的欲望,跟一个被“编译器优化功能”忽悠的程序员想学汇编的欲望,可能是同样强烈的...
怕被忽悠的顾客可以因为不懂法律条文措辞的微妙而不买保险,程序员却不能不写程序。我想,如果一个程序员能从上倒下,从业务系统,到高级编言、到操作系统、到汇编、到CUP,都门儿清,那写起程序来,感觉就跟“庖丁解牛”一样了吧,呵呵~..
可惜这样的程序员并不多...甚至意识到这回事的程序员可能都不多。
这也不能怪程序员们,“速食时代”不是程序员造成的,应该还是市场、还是那些雇用程序员的公司一起造成的吧...
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2009-01-12, 15:48 下午
|
releaseman
注册: 2009-01-12
发 贴: 1
|
|
|
以前,小弟也碰到过类似的情况,就是一场死活catch不住,搞的很郁闷,本来是搜"InternalDialogBox" 这个关键字的,却转到这里.
看了帖子,以及回复,收益匪浅,解决了困扰许久的问题,谢谢!
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
2009-01-13, 00:40 上午
|
格蠹老雷
注册: 2005-12-19
发 贴: 1,303
|
|
|
releaseman,
欢迎你!谢谢你把你的想法写出来。欢迎你多参与更多的讨论。
|
|
|
IP 地址: 已记录
|
报告
|
|
|
|
高端调试 » 软件调试 » C/C++本地代码调试 » Re: 请教一个单线程库在多线程环境中使用的问题
|
|
|
|
|
|