|
|
March 18 http://www.diybl.com/ _beginthread还是CreateThread 发表于 2006-01-28 01:56 AM 作者: polyrandom 程序员对于Windows程序中应该用_beginthread还是CreateThread来创建线程,一直有所争论。本文将从对CRT源代码出发探讨这个问题。
I. 起因
今天一个朋友问我程序中究竟应该使用_beginthread还是CreateThread,并且告诉我如果使用不当可能会有内存泄漏。其实我过去对这个问题也是一知半解,为了对朋友负责,专门翻阅了一下VC的运行库(CRT)源代码,终于找到了答案。
II. CRT
CRT(C/C++ Runtime Library)是支持C/C++运行的一系列函数和代码的总称。虽然没有一个很精确的定义,但是可以知道,你的main就是它负责调用的,你平时调用的诸如strlen、strtok、time、atoi之类的函数也是它提供的。我们以Microsoft Visual.NET 2003中所附带的CRT为例。假设你的.NET 2003安装在C:\Program Files\Microsoft Visual Studio .NET 2003中,那么CRT的源代码就在C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\crt\src中。既然有了这些实现的源代码,我们就可以找到一切解释了。
III. _beginthread/_endthread
这个函数究竟做了什么呢?它的代码在thread.c中。阅读代码,可以看到它最终也是通过CreateThread来创建线程的,主要区别在于,它先分配了一个_tiddata,并且调用了_initptd来初始化这个分配了的指针。而这个指针最后会被传递到CRT的线程包装函数 _threadstart中,在那里会把这个指针作为一个TLS(Thread Local Storage)保存起来。然后_threadstart会调用我们传入的线程函数,并且在那个函数退出后调用_endthread。这里也可以看到, _threadstart用一个__try/__except块把我们的函数包了起来,并且在发生异常的时候,调用exit退出。(_threadstart和endthread的代码都在thread.c中) 这个_tiddata是一个什么样的结构呢?它在mtdll.h中定义,它的成员被很多CRT函数所用到,譬如int _terrno,这是这个线程中的错误标志;char* _token,strtok以来这个变量记录跨函数调用的信息,...。 那么_endthread又做了些什么呢?除了调用浮点的清除代码以外,它还调用了_freeptd来释放和这个线程相关的tiddata。也就是说,在 _beginthread里面分配的这块内存,以及在线程运行过程中其它CRT函数中分配并且记录在这个内存结构中的内存,在这里被释放了。 通过上面的代码,我们可以看到,如果我使用_beginthread函数创建了线程,它会为我创建好CRT函数需要的一切,并且最后无需我操心,就可以把清除工作做得很好,可能唯一需要注意的就是,如果需要提前终止线程,最好是调用_endthread或者是返回,而不要调用ExitThread,因为这可能造成内存释放不完全。同时我们也可以看出,如果我们用CreateThread函数创建了线程,并且不对C运行库进行调用(包括任何间接调用),就不必担心什么问题了。
IV. CreateThread和CRT
或许有人会说,我用CreateThread创建线程以后,我也调用了C运行库函数,并且也使用ExitThread退出了,可是我的程序运行得好好的,既没有因为CRT没有初始化而崩溃,也没有因为忘记调用 _endthread而发生内存泄漏,这是为什么呢,让我们继续我们的CRT之旅。 假设我用CreateThread创建了一个线程,我调用 strtok函数来进行字符串处理,这个函数肯定是需要某些额外的运行时支持的。strtok的源代码在strtok.c中。从代码可见,在多线程情况下,strtok的第一句有效代码就是_ptiddata ptd = _getptd(),它通过这个来获得当前的ptd。可是我们并没有通过_beginthread来创建ptd,那么一定是_getptd捣鬼了。打开 tidtable.c,可以看到_getptd的实现,果然,它先尝试获得当前的ptd,如果不能,就重新创建一个,因此,后续的CRT调用就安全了。可是这块ptd最终又是谁释放的呢?打开dllcrt0.c,可以看到一个DllMain函数。在VC中,CRT既可以作为一个动态链接库和主程序链接,也可以作为一个静态库和主程序链接,这个在Project Setting->Code Generations里面可以选。当CRT作为DLL链接到主程序时,DllMain就是CRT DLL的入口。Windows的DllMain可以由四种原因调用:Process Attach/Process Detach/Thread Attach/Thread Detach,最后一个,也就是当线程函数退出后但是线程还没有销毁前,会在这个线程的上下文中用Thread Detach调用DllMain,这里,CRT做了一个_freeptd(NULL),也就是说,如果有ptd,就free掉。所以说,恰巧没有发生内存泄漏是因为你用的是动态链接的CRT。 于是我们得出了一个更精确的结论,如果我没有使用那些会使用_getptd的CRT函数,使用CreateThread就是安全的。
V. 使用ptd的函数
那么,究竟那些函数使用了_getptd呢?很多!在CRT目录下搜索_getptd,你会发觉很多意想不到的函数都用到了它,除了strtok、 rand这类需要保持状态的,还有所有的字符串相关函数,因为它们要用到ptd中的locale信息;所有的mbcs函数,因为它们要用到ptd中的 mbcs信息,...。
VI. 测试代码
下面是一段测试代码(leaker中用到了atoi,它需要ptd):
代码: #include <windows.h> #include <process.h> #include <iostream> #include <CRTDBG.H>
volatile bool threadStarted = false;
void leaker() { std::cout << atoi( "0" ) << std::endl; }
DWORD __stdcall CreateThreadFunc( LPVOID ) { leaker(); threadStarted = false; return 0; }
DWORD __stdcall CreateThreadFuncWithEndThread( LPVOID ) { leaker(); threadStarted = false; _endthread(); return 0; }
void __cdecl beginThreadFunc( LPVOID ) { leaker(); threadStarted = false; }
int main() { for(;;) { while( threadStarted ) Sleep( 5 ); threadStarted = true; // _beginthread( beginThreadFunc, 0, 0 );//1 CreateThread( NULL, 0, CreateThreadFunc, 0, 0, 0 );//2 // CreateThread( NULL, 0, CreateThreadFuncWithEndThread, 0, 0, 0 );//3 } return 0; } 如果你用VC的多线程+静态链接CRT(/MT或者/MTD)选项去编译这个程序,并且尝试打开1、2、3之中的一行,你会发觉只有2打开的情况下,程序才会发生内存泄漏(可以在任务管理器里面明显的观察到)。3之所以不会出现内存泄漏是因为主动调用了_endthread。
VII. 总结
如果你使用了DLL方式链接的CRT库,或者你只是一次性创建少量的线程,那么你或许可以采取鸵鸟策略,忽视这个问题。上面一节代码中第3种方法基于对 CRT库的了解,但是并不保证这是一个好的方法,因为每一个版本的VC的CRT可能都会有些改变。看来,除非你的头脑清晰到可以记住这一切,或者你可以不厌其烦的每调用一个C函数都查一下CRT代码,否则总是使用 _beginthread(或者它的兄弟_beginthreadex)是一个不错的选择。
[后记] 网友condor指出 本文的一个错误:在dllcrt0.c 中,DllMain的Thread Detach所释放的ptd,其实是dllcrt0.c的DllMain中的Thread Attach所创建的。也就是说,当你用CRT DLL的时候,DllMain对线程做了一切初始化/清除工作。我查看源代码,thread.c中的_threadstart函数,在设置TLS之前做了检查,这其实就是为了避免重复设置导致的内存泄漏。 文章出处:http://www.diybl.com/course/3_program/c++/cppsl/2008831/139196.html
这也是很多人用在了dll工程中,很难发现出现问题的原因吧! 大部分工程都是设置的 /md _BeginThread 使用了 TLS 线程本地存储。 createthread 无,只是sdk 还有afxbeginthread 更好,这个MFC函数能够检查资源是否是当前线程分配的。如一个窗口句柄在一个线程创建,另一个线程使用就会出问题。用afxbeginthread 可以防止这个问题
March 11 #include<iostream> #include <boost/tokenizer.hpp> #include<string> int main() { using namespace std; using namespace boost; string s = "12252001"; int offsets[] = {1,2,3,4}; offset_separator f(offsets, offsets+4); typedef token_iterator_generator<offset_separator>::type Iter; Iter beg = make_token_iterator<string>(s.begin(),s.end(),f); Iter end = make_token_iterator<string>(s.end(),s.end(),f); // 上面这行语句也可以这样写: // Iter end; for(;beg!=end;++beg) { cout << *beg << "\n"; }
string str = "just test;;Hello|world||-foo--bar;yow;baz|"; typedef tokenizer<char_separator<char> > token; char_separator<char> sep(" -;|"); token tokens(str, sep); for (token::iterator tok_iter = tokens.begin(); tok_iter != tokens.end(); ++tok_iter) { cout << "<" << *tok_iter << "> "; } cout << "\n"; return EXIT_SUCCESS; }
结果:
1 22 520 01 <just> <test> <Hello> <world> <foo> <bar> <yow> <baz> tokenizer 正如其意,是用来拆分字符串的boost库。第一个例子是按照offset_separator 指定的数组元素来拆分字符串。 第二个则是按照char_separator指定的字符作为拆分字符。来拆分字符串。 注token结构不大,没有默认的构造函数。直接在栈上构造,构造是指定好参数。
March 06 关于C++中异常的争论何其多也,但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性,幸运的是,随着C++社区经验的积累,今天我们已经有足够的知识轻松编写异常安全的代码了,而且编写异常安全的代码一般也不会对性能造成影响。 使用异常还是返回错误码?这是个争论不休的话题。大家一定听说过这样的说法:只有在真正异常的时候,才使用异常。那什么是“真正异常的时候”?在回答这个问题以前,让我们先看一看程序设计中的不变式原理。 对象就是属性聚合加方法,如何判定一个对象的属性聚合是不是处于逻辑上正确的状态呢?这可以通过一系列的断言,最后下一个结论说:这个对象的属性聚合逻辑上是正确的或者是有问题的。这些断言就是衡量对象属性聚合对错的不变式。 我们通常在函数调用中,实施不变式的检查。不变式分为三类:前条件,后条件和不变式。前条件是指在函数调用之前,必须满足的逻辑条件,后条件是函数调用后必须满足的逻辑条件,不变式则是整个函数执行中都必须满足的条件。在我们的讨论中,不变式既是前条件又是后条件。前条件是必须满足的,如果不满足,那就是程序逻辑错误,后条件则不一定。现在,我们可以用不变式来严格定义异常状况了:满足前条件,但是无法满足后条件,即为异常状况。当且仅当发生异常状况时,才抛出异常。 关于何时抛出异常的回答中,并不排斥返回值报告错误,而且这两者是正交的。然而,从我们经验上来说,完全可以在这两者中加以选择,这又是为什么呢?事实上,当我们做出这种选择时,必然意味着接口语意的改变,在不改变接口的情况下,其实是无法选择的(试试看,用返回值处理构造函数中的错误)。通过不变式区别出正常和异常状况,还可以更好地提炼接口。 对于异常安全的评定,可分为三个级别:基本保证、强保证和不会失败。 基本保证:确保出现异常时程序(对象)处于未知但有效的状态。所谓有效,即对象的不变式检查全部通过。 强保证:确保操作的事务性,要么成功,程序处于目标状态,要么不发生改变。 不会失败:对于大多数函数来说,这是很难保证的。对于C++程序,至少析构函数、释放函数和swap函数要确保不会失败,这是编写异常安全代码的基础。 首先从异常情况下资源管理的问题开始.很多人可能都这么干过:
Type* obj = new Type; try{ do_something...} catch(...){ delete obj; throw;} 不要这么做!这么做只会使你的代码看上去混乱,而且会降低效率,这也是一直以来异常名声不大好的原因之一. 请借助于RAII技术来完成这样的工作:
auto_ptrobj_ptr(new Type); do_something... 这样的代码简洁、安全而且无损于效率。当你不关心或是无法处理异常时,请不要试图捕获它。并非使用try...catch才能编写异常安全的代码,大部分异常安全的代码都不需要try...catch。我承认,现实世界并非总是如上述的例子那样简单,但是这个例子确实可以代表很多异常安全代码的做法。在这个例子中,boost::scoped_ptr是auto_ptr一个更适合的替代品。 现在来考虑这样一个构造函数:
Type() : m_a(new TypeA), m_b(new TypeB){} 假设成员变量m_a和m_b是原始的指针类型,并且和Type内的申明顺序一致。这样的代码是不安全的,它存在资源泄漏问题,构造函数的失败回滚机制无法应对这样的问题。如果new TypeB抛出异常,new TypeA返回的资源是得不到释放机会的.曾经,很多人用这样的方法避免异常:
Type() : m_a(NULL), m_b(NULL){ auto_ptrtmp_a(new TypeA); auto_ptrtmp_b(new TypeB); m_a = tmp_a.release(); m_b = tmp_b.release(); } 当然,这样的方法确实是能够实现异常安全的代码的,而且其中实现思想将是非常重要的,在如何实现强保证的异常安全代码中会采用这种思想.然而这种做法不够彻底,至少析构函数还是要手动完成的。我们仍然可以借助RAII技术,把这件事做得更为彻底:shared_ptrm_a; shared_ptrm_b;这样,我们就可以轻而易举地写出异常安全的代码:
Type() : m_a(new TypeA), m_b(new TypeB){} 如果你觉得shared_ptr的性能不能满足要求,可以编写一个接口类似scoped_ptr的智能指针类,在析构函数中释放资源即可。如果类设计成不可复制的,也可以直接用scoped_ptr。强烈建议不要把auto_ptr作为数据成员使用,scoped_ptr虽然名字不大好,但是至少很安全而且不会导致混乱。 RAII技术并不仅仅用于上述例子中,所有必须成对出现的操作都可以通过这一技术完成而不必try...catch.下面的代码也是常见的: a_lock.lock();
try{ ...} catch(...) {a_lock.unlock();throw;} a_lock.unlock(); 可以这样解决,先提供一个成对操作的辅助类:
struct scoped_lock{ explicit scoped_lock(Lock& lock) : m_l(lock){m_l.lock();} ~scoped_lock(){m_l.unlock();} private: Lock& m_l; }; 然后,代码只需这样写:
scoped_lock guard(a_lock); do_something... 清晰而优雅!继续考察这个例子,假设我们并不需要成对操作, 显然,修改scoped_lock构造函数即可解决问题。然而,往往方法名称和参数也不是那么固定的,怎么办?可以借助这样一个辅助类:
template struct pair_guard{ pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();} ~pair_guard(){m_fe();} private: FEnd m_fe; ...//禁止复制 }; typedef pair_guard, function>simple_pair_guard; 好了,借助boost库,我们可以这样来编写代码了:
simple_pair_guard guard(bind(&Lock::unlock, a_lock), bind(&Lock::lock, a_lock) ); do_something... 我承认,这样的代码不如前面的简洁和容易理解,但是它更灵活,无论函数名称是什么,都可以拿来结对。我们可以加强对bind的运用,结合占位符和reference_wrapper,就可以处理函数参数、动态绑定变量。所有我们在catch内外的相同工作,交给pair_guard去完成即可。 考察前面的几个例子,也许你已经发现了,所谓异常安全的代码,竟然就是如何避免try...catch的代码,这和直觉似乎是违背的。有些时候,事情就是如此违背直觉。异常是无处不在的,当你不需要关心异常或者无法处理异常的时候,就应该避免捕获异常。除非你打算捕获所有异常,否则,请务必把未处理的异常再次抛出。try...catch的方式固然能够写出异常安全的代码,但是那样的代码无论是清晰性和效率都是难以忍受的,而这正是很多人抨击C++异常的理由。在C++的世界,就应该按照C++的法则来行事。 如果按照上述的原则行事,能够实现基本保证了吗?诚恳地说,基础设施有了,但技巧上还不够,让我们继续分析不够的部分。
C++异常处理机制核心观点
0.如果使用普通的处理方式:ASSERT,return等已经足够简洁明了,请不要使用异常处理机制. 1.比C的setjump,longjump优秀. 2.可以处理任意类型的异常. 你可以人为地抛出任何类型的对象作为异常. throw 100; throw \"hello\"; 3.需要一定的开销,频繁执行的关键代码段避免使用 C++异常处理机制. 4.其强大的能力表现在: A.把可能出现异常的代码和异常处理代码隔离开,结构更清晰. B.把内层错误的处理直接转移到适当的外层来处理,化简了处理流程.传统的手段是通过一层层返回错误码把错误处理转移到上层,上层再转移到上上层,当层数过多时将需要非常多的判断, 以采取适当的策略. C.局部出现异常时,在执行处理代码之前,会执行堆栈回退,即为所有局部对象调用析构函数,保证局部对象行为良好. D.可以在出现异常时保证不产生内存泄漏.通过适当的try,catch布局,可以保证delete pobj;一定被执行. E.在出现异常时,能够获取异常的信息,指出异常原因.并可以给用户优雅的提示. F.可以在处理块中尝试错误恢复.保证程序几乎不会崩溃. 通过适当处理,即使出现除0异常,内存访问违例,也能让程序不崩溃,继续运行,这种能力在某些情况下及其重要. 以上ABCDEF可以使你的程序更稳固,健壮,不过有时让程序崩溃似乎更容易找到原因,程序老是不崩溃,如果处理结果有问题,有时很难查找. 5.并不是只适合于处理’灾难性的’事件.普通的错误处理也可以用异常机制来处理,不过如果将此滥用的话,可能造成程序结构混乱,因为异常处理机制本质上是程序处理流程的转移,不恰当的,过度的转移显然将造成混乱.许多人认为应该只在’灾难性的’事件上使用异常处理,以避免异常处理机制本身带来的开销,你可以认为这句话通常是对的. 6.先让程序更脆弱,再让程序更坚强.首先,它使程序非常脆弱,稍有差错,马上执行流程跳转掉,去寻找相应的处理代码,以求适当的解决方式.很像一个人身上带着许多药品,防护工具出行,稍有头晕,马上拿出清凉油;遇到蚊子立刻拿出电蚊拍灭之. WINDOWS: 7.将结构化异常处理结合/转换到C++异常对象,可以更好地处理WINDOWS程序出现的异常. 8.尽一切可能使用try,catch,而不是win32本身的结构化异常处理或者MFC中的TRY,CATCH宏. 用得恰到好处,方显C++异常之美妙!catch(…) { } 这三个点并不是说要省略什么.相反,你需要在程序中实际输入这三个点.这是一个很好的默认CATCH块,应该把它放在其他所有CATCH块之后. 1.7异常规范: Double safe_divide(int top,int bottom) throw(DivideByZero); 假如在函数中抛出一个异常,但异常规范中并未列出这个异常(也没有在函数内部捕捉),会发生什么事情?在这种情况下,程序会终止.尤其要注意的是,假如一个异常在函数中招聘但既没有在异常规范中列出,也没有在函数内部捕捉,那么它不会被任何catch块捕捉,而是直接导致程序终止.记住,如果完全没有异常规范列表,就连空白的都没有,那么效果等同于在规范列表中列出所有异常.在这种情况下,抛出一个异常不会终止程序. 注意,异常规范是为那些准备跑到函数外部的异常而准备的.如果它们不跑到函数外部,就不归入异常规范.如果它们要跑到函数外部,就应该归入异常规范,无论它们起源于何处.如果在函数定义内部的一个try块中抛出一个异常,而且在函数定义内部的一个catch块中捕捉这个异常这个异常的类型就不需要在异常规范中列出.如果函数定义包括对另一个函数的调用,而另一个函数可能招聘一个它自已不会被捕捉的异常就应该在异常规范中列出异常的类型. 要表示一个函数不应抛出任何不在函数内部捕捉的异常,需要使用一个空白异常规范. Void some_function() throw(); 几种方式可以总结如下: Void some_funtion() throw(DivideByZero,OtherExecption); //DivideByZero或OtherException类型的异常会被正常处理. //至于其他任何异常,如果抛出后未在函数主体中捕捉,就会终止程序. Void some_function()throw(); //空异常列表:一旦抛出任何未在函数主体中捕捉的异常就会终止程序. Void some_function(); //正常处理所有类型的所有异常. 1.8陷阱:派生类中的异常规范 在派生类中重定义或覆盖一个函数定义时,它应具有与基类中一亲友的异常规范,或至少应该在新的异常规范中给出基类异常规范的一个子集.换言之,重定义或覆盖一个函数定义时,不可在异常规范中添加新异常.但是,如果愿意,可删减基类中原有的异常.之所以有这个要求,是因为在能够使用基类对象的任何地方,都能使用一个派生类对象.因此,重定义或覆盖的函数必须兼容于为基类对象编写的任何代码. 下面的内容是我从<<C++面向对象程序设计(第五版)>>找到值得学习的内容assert语句我们曾用以下语句测试一个名为in_stream的文件是否成功打开: If(in_stream.fail()) { Cout<<”input file opening failed.\\n”; Exit(1); } 可用assert(断言)语句编写同样的测试,如下所示:assert(!in_stream.fail()); 注意,这种情况下,我们要插入一个求反操作符(!),才能获得同样的效果,因为我们要断言的是文件打开操作”没有失败”. Assert语句由标识符assert,一个逻辑表达式(包含在一对圆括号内)各一个分号构成.可以使用任何逻辑表达式.如果逻辑表达式false,程序就终止运行,并给出一个错误消息.如果逻辑表达式示值为true,就什么情况都不会发生,程序继续执行assert语句之后的下一条语句.因此,assert 语句是在程序中进行错误检查的一种精简方式. Assert语句在cassert库中定义,所以使用assert语句的任何程序都必须包含以下 include预编译指令: #include<cassert> Assert是一个宏(类似于函数的一种结构),所以有必要在一个库中定义它. 使用assert语句的一个好处是可以将其关闭.你可在自己的程序中用assert语句来高度程序,再将其关闭使用户看不到他们无法理解的错误消息.关闭assert语句,还能减少程序执行这些语句的开销.要关闭程序中的所有assert语句,请在include预编译指令之前添加#define NDEBUG,如下所示: #define NDEBUG #include<cassert> 因此,如果在进行了全面高度的程序中插入#define NDEBUG,就会关闭程序中的所有assert语句.如果以后改动了程序,可删除程序中的#define NDEBUG重新打开assert语句第6点补充为: 6.先让程序更脆弱,再让程序更坚强.首先,它使程序非常脆弱,稍有差错,马上执行流程跳转掉,去寻找相应的处理代码,以求适当的解决方式,如果找不到任何解决办法(异常没有被捕获并处理),立刻(结束程序运行)。 很像一个人身上带着许多药品,防护工具出行,稍有头晕,马上拿出清凉油;遇到蚊子立刻拿出电蚊拍灭之,遇到哪怕是蚊虫叮咬,如果找不到电蚊拍,他马上自杀!(这个家伙真是脆弱:) 以致于我们必须为他准备所有的防护工具一旦我们为他准备了全部适当的防护工具,他就变成一个非常坚强的人了!)。 当然,代价是这个家伙变得比较笨重,行动迟缓。
近似的公理: 1 异常是同步的。即只能发生在函数边界。预定义类型的算术操作、预定义类型的不会导致异常 2 对象的销毁是异常安全的。即析构函数、operator delete 和operator delete[] 是异常安全的。 3 swap函数不会导致异常 Author : Kevin Lynx
当软件作为release模式被发布给用户时,当程序崩溃时我们很难去查找原因。常见的手法是输出LOG文件,根据LOG文件分析 程序崩溃时的运行情况。我们可以通过SEH来捕获程序错误,然后输出一些有用的信息作为我们分析错误的资料。一般我们需要 输出的信息包括:系统信息、CPU寄存器信息、堆栈信息、调用堆栈等。而调用堆栈则是最有用的部分,它可以直接帮我们定位 到程序崩溃时所处的位置(在何处崩溃)。(codeproject上关于这个专题的常见开场白 = =#)
要获取call stack(所谓的调用堆栈),就需要查看(unwind)stack的内容。We could conceivably attempt to unwind the stack ourselves using inline assembly. But stack frames can be organized in different ways, depending on compiler optimizations and calling conventions, so it could become complicated to do it that way.(摘自vld文档)要获取栈的 内容,我们可以自己使用内联汇编获取,但是考虑到兼容性,内联汇编并不是一个好的解决方案。我们可以使用微软的dbghelp 中的StackWalk64来获取栈的内容。
StackWalk64声明如下: BOOL StackWalk64( DWORD MachineType, HANDLE hProcess, HANDLE hThread, LPSTACKFRAME64 StackFrame, PVOID ContextRecord, PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress );
具体每个参数的含义可以参见MSDN。这里说下ContextRecord参数,该参数指定了CPU各个寄存器的内容。StackFrame指定了stack frame的内容。stack frame是什么,我也不知道。(= =) StackWalk64函数需要用户指定当前frame的地址,以及当前程序的指令地址。这两个信息都被填充进ContextRecord,然后传进StackWalk64函数。
那么如何获取当前的stack frame地址和当前程序指令地址呢?如前所说,你可以使用内联汇编。(对于程序指令地址,因为要获取EIP寄存器的内容,而该寄存器不能被软件访问)也可以使用GetThreadContext一次性获取当前线程当前运行情况下的CPU各个寄存器内容。补充下,当前frame地址被放在EBP寄存器里,当前程序指令地址放在EIP寄存器里。但是,如同MSDN对GetThreadContext函数的说明一样,该函数可能获取到错误的寄存器内容(You cannot get a valid context for a running thread)。
另一种获取Context(包含EBP and EIP)的方法就是使用SEH(结构化异常处理),在__except中使用GetExceptionInformation获取。 GetExceptionInformation 传回一个LPEXCEPTION_POINTERS指针,该指针指向一个EXCEPTION_POINTERS结构,该结构里包含一个Context的指针,即达到目标,可以使用StackWalk函数。
补充一下,你可以直接使用StackWalk函数,StackWalk被define为StackWalk64(windows平台相关)。 unwind栈后,可以进一步获取一个stack frame的内容,例如函数名。这里涉及到SymFromAddr函数,该函数可以根据一个地址返回符号名(函数名)。还有一个有意思的函数:SymGetLineFromAddr,可以获取函数对应的源代码的文件名和行号。
当然,这一切都依赖于VC产生的程序数据库文件(pdb),以及提供以上API函数的dbghelp.dll。
参考一段简单的代码:
#include <windows.h> #include <stdio.h> #include <dbghelp.h>
#pragma comment( lib, "dbghelp.lib" )
void dump_callstack( CONTEXT *context ) { STACKFRAME sf; memset( &sf, 0, sizeof( STACKFRAME ) );
sf.AddrPC.Offset = context->Eip; sf.AddrPC.Mode = AddrModeFlat; sf.AddrStack.Offset = context->Esp; sf.AddrStack.Mode = AddrModeFlat; sf.AddrFrame.Offset = context->Ebp; sf.AddrFrame.Mode = AddrModeFlat;
DWORD machineType = IMAGE_FILE_MACHINE_I386;
HANDLE hProcess = GetCurrentProcess(); HANDLE hThread = GetCurrentThread();
for( ; ; ) { if( !StackWalk(machineType, hProcess, hThread, &sf, context, 0, SymFunctionTableAccess, SymGetModuleBase, 0 ) ) { break; }
if( sf.AddrFrame.Offset == 0 ) { break; } BYTE symbolBuffer[ sizeof( SYMBOL_INFO ) + 1024 ]; PSYMBOL_INFO pSymbol = ( PSYMBOL_INFO ) symbolBuffer;
pSymbol->SizeOfStruct = sizeof( symbolBuffer ); pSymbol->MaxNameLen = 1024;
DWORD64 symDisplacement = 0; if( SymFromAddr( hProcess, sf.AddrPC.Offset, 0, pSymbol ) ) { printf( "Function : %s\n", pSymbol->Name ); } else { printf( "SymFromAdd failed!\n" ); }
IMAGEHLP_LINE lineInfo = { sizeof(IMAGEHLP_LINE) }; DWORD dwLineDisplacement;
if( SymGetLineFromAddr( hProcess, sf.AddrPC.Offset, &dwLineDisplacement, &lineInfo ) ) { printf( "[Source File : %s]\n", lineInfo.FileName ); printf( "[Source Line : %u]\n", lineInfo.LineNumber ); } else { printf( "SymGetLineFromAddr failed!\n" ); } } }
DWORD excep_filter( LPEXCEPTION_POINTERS lpEP ) { /**//// init dbghelp.dll if( SymInitialize( GetCurrentProcess(), NULL, TRUE ) ) { printf( "Init dbghelp ok.\n" ); }
dump_callstack( lpEP->ContextRecord );
if( SymCleanup( GetCurrentProcess() ) ) { printf( "Cleanup dbghelp ok.\n" ); }
return EXCEPTION_EXECUTE_HANDLER; }
void func1( int i ) { int *p = 0; *p = i; }
void func2( int i ) { func1( i - 1 ); }
void func3( int i ) { func2( i - 1 ); }
void test( int i ) { func3( i - 1 ); }
int main() { __try { test( 10 ); } __except( excep_filter( GetExceptionInformation() ) ) { printf( "Some exception occures.\n" ); }
return 0; }
以上代码在release模式下需要关掉优化,否则调用堆栈显示不正确(某些函数被去掉了?),同时需要pdb文件。当用户使用时可以只抛出挂掉时候的地址,然后开发者通过crashfinder这样的软件,启动拥有pdb的程序 March 04 使用智能指针不会因为忘记delete指针而造成内存泄露。还有当第三方的lib中某些函数返回指针,这样的返回的指针被client使用的时候,lib就会失去对返回的指针的控制,这样delete的指针的任务一般就会交给调用方client,但是如果client忘记调用delete或是调用的时机不正确,都有可能导致问题,在这种情况下最好使用智能指针。 使用智能指针的典型情况: 使用智能指针可以保证异常安全 保证程序在有异常抛出时仍然无内存泄露。 智能指针更好的解决了资源的所有权共享
shared_ptr<boost/shared_ptr.hpp>:使用shared_ptr进行对象的生存期自动管理,使得分享资源所有权变得有效且安全. scoped_ptr<boost/scoped_ptr.hpp>: 用于确保能够正确地删除动态分配的对象。scoped_ptr 有着与std::auto_ptr类似的特性,而最大的区别在于它不能转让所有权而auto_ptr可以。事实上,scoped_ptr永远不能被复制或被赋值!scoped_ptr 拥有它所指向的资源的所有权,并永远不会放弃这个所有权。 weak_ptr<boost/weak_ptr.hpp>:weak_ptr 是 shared_ptr 的观察员。它不会干扰shared_ptr所共享的所有权。当一个被weak_ptr所观察的 shared_ptr 要释放它的资源时,它会把相关的 weak_ptr的指针设为空。使用此辅助指针一般是防止悬空指针。intrusive_ptr<boost/intrusive_ptr.hpp>:shared_ptr的插入是版本,一般不使用,因为需要对使用此指针的类型中增加ref计数功能。但是可以保证不增加指针的大小。 scoped_array<boost/scoped_array.hpp>: scoped_array 为数组做了scoped_ptr为单个对象的指针所做的事情:它负责释放内存。 shared_array<boost/shared_array.hpp>: shared_array 用于共享数组所有权的智能指针。一般指向std::vector的shared_ptr提供了比shared_array更多的灵活性,所以一般使用std::vector<shared_ptr>。
环境boost1_38_0 #include <iostream> #include <string> #include <vector> #include <boost/smart_ptr.hpp> #include <boost/any.hpp> #include <boost/format.hpp>
void PrintIfString(const boost::any& Any) { const boost::shared_ptr<std::string> *s = boost::any_cast<boost::shared_ptr<std::string> >(&Any); if (s && *s) { std::cout << **s << std::endl; }
const boost::weak_ptr<std::string>* s1 = boost::any_cast< boost::weak_ptr<std::string> >(&Any); if(s1) { const boost::shared_ptr<std::string> s = s1->lock(); if(s) { std::cout<<"weak: "<< *s <<std::endl; } }
}
int main(int argc, char* argv[]) { std::vector<boost::any> Stuff; boost::shared_ptr<std::string> SharedString1(new std::string( "Share me.By the way,Boost.any is another useful Boost library")); boost::shared_ptr<std::string> SharedString2(SharedString1); boost::shared_ptr<int> SharedInt1(new int(42)); boost::shared_ptr<int> SharedInt2 (SharedInt1); boost::weak_ptr<std::string> ShareString3(SharedString2); //SharedString1.reset(); 重置智能指针为NULL //SharedString2.reset(); Stuff.push_back(SharedString1); Stuff.push_back(SharedString2); Stuff.push_back(SharedInt1); Stuff.push_back(SharedInt2); Stuff.push_back(ShareString3); for_each(Stuff.begin(), Stuff.end(), PrintIfString); Stuff.clear();
return 0; }
scoped_prt 源码中,支持 if(smartptr) 判断的实现 typedef T * this_type::*unspecified_bool_type; operator unspecified_bool_type() const { return px == 0? 0: &this_type::px; }
令人发晕的类型unspecified_bool_type,这种类型实际上是"指向类的某内部成员变量的指针",不要认为它是指针类型(c必知必会上有说明)。(可以参考<<深入c++对象模型>>来查看这样的偏移值是多少)。unspecified_bool_type可以被当作bool类型使用,它要么为0,要么为1(&this_type::px总是1,错误这个是类内部成员变量的偏移值,cout输出为1是因为转换为bool类型了,可以用printf(”%p”,xx)来打印值。注意vc做了特殊处理,第一个元素偏移值打印为0,不像深入c++对象模型说的是1. 但实际上是转换判断还是为 true 的。
注意这样使用: boost::shared_ptr<int> SharedInt1(new int(42)); 而不是先new 然后赋值智能指针。在函数参数上new可以保证异常安全。除了weak_ptr都可以按这种方法来使用。
在C++标准4.12节中, 对指针与布尔值之间的关系有大致如下的描述: 任何一个指针或成员对象指针都可以转换成布尔值,空指针转换成false, 其它值转换成true. 这样我们就可以利用成员指针来进行转换。通常成员指针不可能(至少不那种容易)被隐式转换(转换的前提毕竟是类型要匹配,或可向上转换)。
|