在一个大型项目的开发中,偶遇到多线程轮询操作出错,为了解决问题,查阅了一些相关资料,结合实际程序调试中的一些经验,把几个调试中的小技巧总结如下,希望对同行们有所帮助。
在多线程程序中取得线程名和线程ID
由于多个线程中的执行是不确定的,所以对于多线程的调试是一件比较困难的事情,而C++编译器提供了直接获取系统级信息的能力,对于每个线程来说,都有一个称为TIB的数据结构存储它本身的信息,通过在调式过程中直接获取TIB的信息,达到判断错误位置和错误原因的目的。
根据Matt Pietrek在1996年5月提出的TIB概念,运行在WIN32系统中每一个线程都有一个TIB,而且FS寄存器指向TIB的地址。并且通过查阅一些相关的程序代码,下面是来自CodeGuru上的一段代码,它包含了活动线程TIB地址的函数以及TIB的具体应用函数:
#ifndef XTIB_H #define XTIB_H // Call SetThreadName(), and then paste this expression into the watch window: // (char*)(DW(@TIB+0x14)) #pragma pack(1) typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD * pNext; FARPROC pfnHandler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD; typedef struct tagXTIB { PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list PVOID pvStackUserTop; // 04h Top of user stack PVOID pvStackUserBase; // 08h Base of user stack union // 0Ch (NT/Win95 differences) { struct // Win95 fields { WORD pvTDB; // 0Ch TDB WORD pvThunkSS; // 0Eh SS selector used for thunking to 16 bits DWORD unknown1; // 10h } WIN95; struct // WinNT fields { PVOID SubSystemTib; // 0Ch ULONG FiberData; // 10h } WINNT; } TIB_UNION1; PVOID pvArbitrary; // 14h Available for application use struct _tib *ptibSelf; // 18h Linear address of TIB structure union // 1Ch (NT/Win95 differences) { struct // Win95 fields { WORD TIBFlags; // 1Ch WORD Win16MutexCount; // 1Eh DWORD DebugContext; // 20h DWORD pCurrentPriority; // 24h DWORD pvQueue; // 28h Message Queue selector } WIN95; struct // WinNT fields { DWORD unknown1; // 1Ch DWORD processID; // 20h DWORD threadID; // 24h DWORD unknown2; // 28h } WINNT; } TIB_UNION2; PVOID* pvTLSArray; // 2Ch Thread Local Storage array union // 30h (NT/Win95 differences) { struct // Win95 fields { PVOID* pProcess; // 30h Pointer to owning process database } WIN95; } TIB_UNION3; // Internal function to get the TIB /*---------------------------------------------------------------------- FUNCTION : GetTIB DESCRIPTION : Returns pointer to TIB for current thread. PARAMETERS : None. RETURNS : NULL - A really bad thing !NULL - Pointer to TIB for current thread ----------------------------------------------------------------------*/ static tagXTIB * GetTIB() { tagXTIB * pTib; __asm { MOV EAX , FS:[18h] MOV pTib , EAX } return pTib; } /*---------------------------------------------------------------------- FUNCTION : SetThreadName DESCRIPTION : SetThreadName provides a way to "name" your threads so that you can see at a glance which thread is active when you are in the debugger. Calling SetThreadName sets the string pointer parameter into the Thread Information Block (TIB) pvArbitrary field (offset 0x14). Matt Pietrek discussed the TIB structure in his May 1996 "Under the Hood" column. I included Matt's TIB.h as part of this project if you want to see the rest of the fields in the TIB. pvArbitrary is an unused spot in the TIB that applications can use as they wish. SetThreadName does the right thing and checks if the pvArbitrary is not 0 and will not write the string pointer to avoid tromping on any other data written there. To view which thread is active in the Watch window, use "(char*)(DW(@TIB+0x14))" As you swap threads, you can now tell at a glance which thread you are in! PARAMETERS : szName - A pointer to the string which you would like to name the current thread. You should make the string pointer a constant name. RETURNS : TRUE - The thread name was set. FALSE - Something else overwrote the pvArbitrary field. ----------------------------------------------------------------------*/ static BOOL SetThreadName(LPCTSTR szName) { // Grab the TIB. tagXTIB * pTib = GetTIB(); // If someone has already written to the arbitrary field, I don't // want to be overwriting it. if (pTib->pvArbitrary != NULL) return FALSE; // Nothing's there. Set the name. pTib->pvArbitrary = (void*)szName; return TRUE; } /*---------------------------------------------------------------------- FUNCTION : GetThreadName DESCRIPTION : Returns the string pointer to the name assigned to the current thread. PARAMETERS : None. RETURNS : NULL - No name was assigned. !NULL - The value at the TIB pvArbitrary offset. Please note that the pointer could be invalid of if something other than SetThreadName used the pvArbitrary offset. ----------------------------------------------------------------------*/ static LPCTSTR GetThreadName() { // Grab the TIB. tagXTIB * pTib = GetTIB(); return (LPCTSTR)pTib->pvArbitrary; } } XTIB; #pragma pack() #endif //XTIB_H
使用过程就是在线程回调函数中调用XTIB::SetThreadName(szThreadName)函数,调试时,在此函数处设置断点,在Watch窗口中键入(char*)(DW(@TIB+0x14))和DW(@tib+0x24)就可以取得当前线程名和线程ID。如下图:
获得系统函数错误代码
一个Windows函数返回的错误代码对确认函数为什么会运行失败常常很有用,Microsoft公司编译了所有可能错误代码的列表,并且为 每个错误代码分配了一个32位的号码。从系统内部来讲,当一个windows函数检测到一个错误时,它会使用所以个称为线程本地存储器[2](thread-local storage)的机制,将相应的错误代码号码与调用的线程关联起来。这将使线程能独立的运行,而不会影响各自的错误代码。当函数返回时,它的返回值就能指明一个错误已经发生,若要确定这个错误,通过调用GetLastError()函数可以取得线程的32位错误代码(一个DWORD类型变量的值)。当取得32位错误代码后,必须将其转换为有用的某种对象,而WinError.h头文件中包含了Microsoft公司定义的错误代码列表。从此文件中可得到一个错误有3种表示方法:一个消息ID,消息文本和一个号码,具体的细节内容可查阅WinError.h头文件。
值得注意的是当Windows函数运行失败时,应该立即调用GetLasstError函数。如果在一个函数运行失败后,还调用了其他的函数,则其错误代码就可能被改写。
在进行调试时,在VC6中可以配置Watch窗口,以便始终显示错误代码的号码和该错误的描述。要实现这个,必须在Watch窗口的某一行键入“@err,hr”。这样就能始终显示当前执行的函数返回的错误代码。如下图:
同时VC6中还提供了一个Error Lookup的小工具,可以将相应的错误代码转换成文本描述。见下图:
而且Windows本身也提供了诸如FormatMessage一类的函数,可以把错误代码转换成文本描述,反馈给用户,这里不一一详述。
自定义函数错误
同样,你也可以自己编写的函数向其他的调用者显示错误代码。这可以通过两种方式实现,
第一,你可以使用WinError.h头文件中已经存在的32位错误代码,通过SetLastError函数来实现。
第二,是如果WinError.h头文件中已经存在的32位错误代码都不能表示你的错误信息,那么你可以自定义自己的32位错误返回代码。定义错误代码时,必须明白错误代码域,如下图:
错误代码域
要注意的是用户自定义错误代码必须把错误代码域中的29位设置为1,实现办法就是添加一个错误代码的宏定义,然后通过和第一种情况一样设置就可以了。
Release版本的调试
在很多程序员的观点中,只有Debug版本才能调试,其实对于Release版本的调试是一样可以进行的,有人也许会说,在Debug版本下调试就够了,能在Debug版本下运行很好的程序在Release版本下是不会有问题的,其实这个观点是错误的,我在写程序的过程中就遇到过很多次,一个程序在Debug下能很好的运行,但在Release下运行就出错,检查原因就是缓冲区越界,如果能在Release版本下调试的话,就能很快的找到问题,其实,Release版本的调试是很简单的,只要修改一下VC编译选项的设置就可以实现了。具体实现方法如下:
1、打开VC集成环境——>Project——>Setting对话框。
2、在Serttings For组合框中选择All Configuration选项。
3、选中C/C++ Tab,在Debug info组合框中选择Program Database选项。
4、切换到Link Tab,在Category组合框选择Debug选项,然后选中下面的debug info检查框和Microsoft Format标志。
在设定完成后,你就能够象调试 DEBUG版本一样调试你的 Release版本了,不过要注意的是,由于在 Release版本中,编译器的优化作用,程序代码中的某些行可能不会被执