WinCE 的(内存和资源)泄露检测





5.00/5 (3投票s)
这是“WinCE内存泄漏检测”的替代方案。
引言
(注意:本文档涉及http://sourceforge.net/projects/crtdbg4wince/ 项目的 alpha 0.06 或更高版本。)
资源泄漏总是令人头疼。但在Windows CE或Windows Mobile设备上,与桌面系统相比资源有限,这种痛苦会更加强烈。不仅内存泄漏会明显耗尽你“永远不够”的RAM,其他泄漏如句柄、打开的文件等也会导致有问题的应用程序运行缓慢。
最后,所谓的“空闲列表碎片化”会导致Windows CE设备有时无法使用,即使所有资源都已释放。但“空闲列表碎片化”在WinCE 6中有所改善,并且无论如何都超出了本文的范围。
停止!一个问题
为什么一个文件句柄(指针,4字节)比内存泄漏更令人讨厌?
嗯,你这是在兜圈子。好吧,我希望我的回答能让你更清晰地看待这个问题:你说得对——表面上看,文件句柄只是一个指向文件系统驱动程序(FSD)内部不透明结构的指针。或者句柄是这样一个不透明结构表的索引。告诉我哪种泄漏最令人痛苦
// which leak would be the most painful?
FILE* stream = fopen( "filename.txt", "r" );
HANDLE block = CreateFile( "filename.txt", .... );
HANDLE = CreateEvent( NULL,FALSE,FALSE,NULL );
char *VeryMuchBytes = new char[ 256 ];
有人会说:256字节的块会造成最大的泄漏。他会指向
// winnt.h
typedef void *HANDLE;
大多数win32平台上的sizeof(void*)
是4,那为什么还要关注4个字节呢?
这有3个原因
- 我们没有丢失指针,而是丢失了它指向的内容。
- 在底层,大多数情况下是由FSD分配、由FSD拥有并直到你关闭该文件的所有句柄才释放的一个结构(如果文件被多次打开,则进行引用计数递减)。我打赌:
stream
会造成最大的泄漏,因为它指向一个FSD结构和一个streambuffer
,而streambuffer
本身通常会超过256字节。
对于各种资源,情况基本相同。想想Brush
对象、套接字等等。一个事件句柄或互斥句柄*可能*确实很小。但是,谁知道呢?而且这根本不重要 - 大小不重要!
再次停止!你为什么说“大小不重要”?我女朋友高兴地告诉我情况不同……
她是对的,因为她(希望)一次只考虑一件事。但对于计算机来说,我们有一群实例。想象一下,你创建了一个事件对象来等待。但是由于编程错误,你创建了这个对象10,000次
int larger_loop = 10000;
while( larger_loop-- )
{
...
HANDLE WaitMe = CreateEvent( NULL,FALSE,FALSE,NULL );
...
}
WaitForSingleObject(WaitMe,1000);
你真的相信Windows (CE)调度程序在只有1个事件实例的情况下会以同样的速度运行吗?
考虑:大小不重要,如果数量太多。调度程序维护一个可等待对象的列表。这个列表的排序和重新排序在调度程序内部得到了高度优化。但是没有人能优化如此严重迟钝的程序行为。没有人,除了有问题的程序开发者。
到目前为止的结论
- 内存块大的泄漏很糟糕
- 大量泄漏的、列表管理的资源很糟糕——即使它们的内存占用很小
- 大多数现代资源(几乎所有)都隐藏在
void*
指针后面是有原因的,但这可能导致对真实大小的忽视 - 我们生成的泄漏是什么类型的并不重要。我们应该努力避免所有这些泄漏。
方法和工具...
如今,你通常可以访问各种泄漏检测工具。但没有一个工具能一次性满足你所有的需求
- 静态分析工具
- 运行时分析工具
- 与你的工具链或工具链的升级版本集成的工具
- 与你的操作系统集成的工具
- 与你的C运行时(CRT)集成或可选的工具
- 与操作系统无关的工具
- 与操作系统相关的工具
- ……等等……
……以及一个不完整的比较
静态分析工具,例如我首选的“PC-Lint”(不要与免费但功能较弱的“lint”混淆)
优点:能够找到很多很多东西。
缺点:找到太多,你需要判断它是恶意的还是仅仅是糟糕的风格
缺点:无法找到依赖于运行时的泄漏,例如:Internet服务器 - 当你注销时,用户名字符串缓冲区会被释放,但如果你在同一会话中重新登录,它会被分配第2次,泄漏第1次。
动态(运行时)分析工具,例如MicroSoft的CE Application Verifier
优点:有时易于使用
缺点:有时不起作用(在CE6上经常无法解析符号和行号)
缺点:只能找到你触发的泄漏,所以它取决于你的测试深度和代码覆盖率
动态(运行时)分析工具,例如Microsoft的_CrtDbg
优点:易于使用
缺点:不适用于基于CE的嵌入式Windows版本(直到上周)
缺点:只能找到你触发的泄漏,所以它取决于你的测试深度和代码覆盖率
与操作系统无关的工具
优点:如果已经为其他操作系统使用过,则易于学习
缺点:只能找到C或C++标准API中的泄漏,而不是OS相关的API
背景
本文的重点将放在_CrtDbg
上,但它将是针对基于Windows CE的平台(PPC2003、WinCE 4.20、WinCE 5、WiMo 6/6.5、WinCE6)的特殊版本。
作为一名经验丰富的Win32桌面平台开发者,我学会了在Milkman工具链中使用CodeGuard。但后来,我不得不切换到Visual Studio。这很痛苦,需要处理稍有不同的API,并且缺少CodeGuard这样的助手!但后来我学会了将_CrtDbg.h作为我的新朋友。虽然不如CG强大,但仍然非常有帮助!
曾经,有人扩展了我的工作任务,让我为WinCE编写工具。再次痛苦:没有可用的泄漏查找器!我的一些工具和应用程序被设计成可以在桌面终端和移动终端上交叉编译。所以,我可以使用桌面工具进行桌面构建,然后祈祷1000次,希望WinCE构建也能如此。PC-Lint提供了额外的检查,所以最后,我或多或少能睡个好觉。大多数时候。
最后,我发现了“AppVerify”,并将其确立为质量保证流程的一部分。Appverify很烦人,经常抱怨单例、全局打开的日志文件等等。在忙碌的时候很难使用。
最后,在从eVC3 -> eVC4 -> VS2005 -> VS2008以及CE3/4/5/6的转换过程中,Appverify已不再经常奏效。
- 我梦想着_CrtDbg.h以及它的易用性。
- 我梦想着CG/Appverify能够检测出比
malloc()
/new泄漏更多的东西
所以我一遍又一遍地在网上搜索。找到了很多。但没有一个完全满足我的需求。直到上周。现在,我有了一个热门候选:“CrtDbg for WinCE”。
这个项目已经在SourceForge上托管了几周。它不再支持eVC3/eVC4,但它帮助我修复了一些严重的泄漏。crtdbg4wince
的许可证是所谓的“CFU”——“商业用途便宜,但非营利和教育用途免费”。听起来对我来说不算太坏。
请阅读我的文章,如果你喜欢,请考虑支持开发者,这样他就能继续开发这个项目并满足我所有的需求。;)
这是链接:http://sourceforge.net/projects/crtdbg4wince/ 到提到的项目。在我看来,值得注意的是,这个项目已经朝着检测malloc或new以外的泄漏方向发展。
Using the Code
因为它声称是_CrtDbg
子集的移植,所以你可以使用几乎与Microsoft原始版本相同的API。开始之前,你应该注意一两个细节
- 头文件目前名为“mm_CrtDbg.h”,而不是“CrtDbg.h”。这是因为Modem Man(开发者)告诉我,他经常同时使用M$ CrtDbg.h和自己的模块来检查“mm_CrtDbg.h”的代码。如果你喜欢,可以把他的文件重命名为“CrtDbg.h”。
- Modem Man引入了一些新的标志,这些标志可能用
_WIN32_WCE
括起来
...
int DbgMode = _CRTDBG_LEAK_CHECK_DF;
#ifdef _WIN32_WCE
DbgMode |= _CRTDBG_MM_BOUNDSCHECK;
#endif
_CrtSetDbgFlag( DbgMode );
...
以确保你的代码能够同时用桌面和移动编译器进行编译。
- 自0.06版以来,你现在可以在构建Release版本时包含mm_CrtDbg.h。它只是不生成任何代码,也不会给你带来烦人的
#ifdef _DEBUG
工作。
- 还有一些众所周知的CrtDbg.h函数尚未实现或作为存根实现。但这并没有困扰我。我以前从未使用过这种调用。所以,为什么要抱怨?;)
如果你想交叉编译使用这些函数的代码,你可以简单地将它们定义为空宏或放入
#ifdef
块中
#ifndef _WIN32_WCE
_RPT0(_CRT_WARN,"file a message\n");
#endif
介绍够了,现在开始实操!
好的,现在我们来做“真正”的工作
只需编写任何一种简单的WinCE C/C++程序(控制台或GUI),并包含mm_CrtDbg.h
// the very simplest usage example:
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <new>
#include <mm_CrtDbg.h>
int wmain(int argc, WCHAR* argv[])
{
然后,将行为设置为在程序结束时报告。这与你在Win32桌面目标平台上的操作方式相同
_CrtSetDbgFlag( _CRTDBG_LEAK_CHECK_DF ); /* Leak check at program exit */
告诉泄漏查找器也将所有信息报告到IDE的“Output
”窗口(如果无法使用ActiveSync,则为调试通道或调试UART)
_CrtSetReportMode( _CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_WNDW );
_CrtSetReportMode( _CRT_WARN , _CRTDBG_MODE_DEBUG );
_CrtSetReportMode( _CRT_ERROR , _CRTDBG_MODE_DEBUG );
上面,我添加了_CRTDBG_MODE_WNDW
,以便在紧急情况下弹出消息框。
最后,我们需要有问题的代码。你可以使用下面的例子开始,或者用你自己的有bug的代码开始。我建议从示例开始,以获得第一感觉
// do something very stupid:
TCHAR * lost1 = (TCHAR*) malloc( 10 * sizeof(TCHAR)); //for testing, remove sizeof(TCHAR)
_tcscpy( lost1, _T("looser!") );
TCHAR * lost2 = _tcsdup( lost1 );
free( lost1 );
//
char *alsolost = new char[10];
alsolost = new char[20];
delete [] alsolost;
// the report will come up after executing the return below:
return 0;
}
我们将泄漏lost2
和alsolost
,分别是8字节和10字节。
让我们看看IDE“output”选项卡中的输出
程序终止前的输出
c:\cpp\crtdbg4wince\sample1.cxx(18) : 'wmain': malloc(0x003986e8,10 byte) registered, ok.
c:\cpp\crtdbg4wince\sample1.cxx(20) : 'wmain': malloc(0x00398790,8 byte) registered, ok.
c:\cpp\crtdbg4wince\sample1.cxx(21) : 'wmain':
free for malloc(0x003986e8,10) in c:\cpp\crtdbg4wince\sample1.cxx(18) : 'wmain': , ok
c:\cpp\crtdbg4wince\sample1.cxx(23) : 'wmain': new(0x003986e8,10 byte) registered, ok.
c:\cpp\crtdbg4wince\sample1.cxx(24) : 'wmain': new(0x00398838,20 byte) registered, ok.
c:\cpp\crtdbg4wince\sample1.cxx(24) : 'wmain':
delete for new(0x00398838,20) in c:\cpp\crtdbg4wince\sample1.exe(0) : 'unknown_func': , ok
这里,我们看到了所有的活动。我们可以通过以下方式使其不那么冗长:
_CrtSetReportMode( _CRT_WARN, 0 );
但这取决于你的品味。我个人不喜欢将这种“统计”输出映射到_CRT_WARN
。Modem Man告诉我,他也在考虑引入第四个通道_CRT_STAT
。我期待他的解决方案。
如果你有非常复杂的资源情况,你可以用它做一些Perl解析——随你喜欢。
但现在让我们继续看真正有趣的东西。
程序终止后的输出
终止日志的标题总是总结一些全局统计信息。这是对原始Microsoft方式的改进
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': ============================================
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': Leakage Summary at program termination point
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': ============================================
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': peak ever used malloc(): 18 byte, just for information.
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': peak ever used new(): 30 byte, just for information.
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': CreateFile/CloseHandle are okay (or never used).
c:\cpp\crtdbg4wince\sample1.exe(0) : 'at exit': fopen/fclose are okay (or never used).
然后我们找到列出的error
s
c:\cpp\crtdbg4wince\sample1.exe(0) : error R0001: 'at exit': still 8 byte in use by malloc()!
c:\cpp\crtdbg4wince\sample1.exe(0) : error R0001: 'at exit': still 10 byte in use by new()!
是的!正如“鹰眼Sarge”所预测的,我们从malloc泄漏了8字节,从new泄漏了10字节。
接下来的几行将告诉我们具体在哪里
c:\cpp\crtdbg4wince\sample1.cxx(23) : error R0001:
'wmain': delete(0x003986e8,10) missing for this allocation, or two times allocated to same pointer.
c:\cpp\crtdbg4wince\sample1.exe(0) : assertion A0001:
'at exit': delete(0x003986e8,10) missing near here. See previous line for new() location.
c:\cpp\crtdbg4wince\sample1.cxx(20) : error R0001:
'wmain': free(0x00398790,8) missing for this allocation, or two times allocated to same pointer.
c:\cpp\crtdbg4wince\sample1.exe(0) : assertion A0001:
'at exit': free(0x00398790,8) missing near here. See previous line for malloc() location.
所有带<code><code>filename(linenumber):
的行都可以用鼠标点击,使焦点立即跳转到给定的源代码行。在几乎所有情况下,它都能跳转到资源分配点,有时也能跳转到有问题的释放行!比Microsoft还好!酷!!!
正如你记得的,我们配置了assertion
行来调用消息框处理程序。我不想在这里用截图来烦你。想象一下就行了。
这就是它能做的全部吗?
不。这里有一个更复杂的示例:
// more complex example:
#include <winsock.h>
#include <mm_CrtDbg.h>
上面,我们包含了winsock,因为我还想展示WSAStartup-leaks
。接下来,我们声明一个函数并定义一个虚拟类
void Setup_CrtDbg_Mode( HANDLE CrtFile );
class dummyC
{
public:
dummyC() : x(-1) {OutputDebugString( L"ctor okay\r\n" );}
~dummyC() {OutputDebugString( L"dtor okay\r\n" );}
private:
int x;
};
只在调试器选项卡中有输出并不总是方便的。或者你只有一个低带宽的调试通道之类的。所以,创建一个文件来立即在设备上收集所有消息。这与MS CrtDbg
没什么不同
int wmain(int argc, WCHAR* argv[])
{
// open your logfile [optional]
HANDLE CrtFile = CreateFile( "LogFile.txt", GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
连接文件与泄漏查找器。详情将在稍后显示在Setup_CrtDbg_Mode()
函数中
Setup_CrtDbg_Mode( CrtFile );
再次做愚蠢的事情,但这次更复杂
// do something very normal (stupidity comes later):
for( int i=3 ; i>0 ; i-- )
{
dummyC C = new dummyC;
WSADATA WSA;
int wsa = WSAStartup( MAKEWORD( 2, 2 ), &WSA );
HANDLE File1 = CreateFile( L"123.txt", GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
HANDLE File2;
DuplicateHandle( GetCurrentProcess(), File1, GetCurrentProcess(),
&File2, 0, FALSE, DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS );
// do something stupid: forget to free ressources sometimes
if(i!=3) {CloseHandle( File2 );};
if(i!=2) {WSACleanup( &WSA );};
if(i!=1) {delete C;};
_CrtDumpMemoryLeaks();
} // end for 3 loop
在程序结束时不做任何特殊操作。报告将独立出现。
// a final report will also come up after executing the 'return':
return 0;
}
Setup_CrtDbg_Mode()
辅助函数几乎只包含你会用于桌面平台的调用
// helper function for complex setup of _CrtDbg global settings
void Setup_CrtDbg_Mode( HANDLE CrtFile )
{
int DbgMode;
DbgMode = _CrtSetDbgFlag( _CRTDBG_LEAK_CHECK_DF /* Leak check at program exit */
| _CRTDBG_CHECK_ALWAYS_DF /* Check heap every alloc/dealloc */
| _CRTDBG_CHECK_CRT_DF /* Do Leak check/diff CRT blocks */
);
上面将CrtDbg
定向到
- 在退出时报告,就像我们之前做的那样
- 检查每一个资源的分配/释放
- 也检查CRT内部块
- 返回默认预设和给定的3个
DbgMode
然后关闭尚未支持的“也检查CRT内部块”功能,并添加新的缓冲区溢出/下溢标志。最后,禁用一个非常冗长的alloc/free并将所有这些位组合在一起
DbgMode &= ~_CRTDBG_CHECK_CRT_DF;
DbgMode |= _CRTDBG_MM_BOUNDSCHECK; /* new flag */
DbgMode &= ~_CRTDBG_MM_CHATTY_ALLOCFREE; /* new flag by maik */
_CrtSetDbgFlag(DbgMode);
与之前一样,我们需要设置3个“通道”,但这次也设置为我们打开的文件
_CrtSetReportMode( _CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE | _CRTDBG_MODE_WNDW );
_CrtSetReportMode( _CRT_WARN , _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE );
_CrtSetReportMode( _CRT_ERROR , _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE );
(上面再次与桌面平台100%相同)。
最后一步是将文件句柄与所有想要的“通道”连接起来。我想让所有东西都被写入,所以我将文件分配给所有3个通道
if( INVALID_HANDLE_VALUE != CrtFile )
{
_CrtSetReportFile( _CRT_ASSERT, CrtFile );
_CrtSetReportFile( _CRT_WARN , CrtFile );
_CrtSetReportFile( _CRT_ERROR , CrtFile );
}
}}
让我们启动程序,看看它报告了什么。
程序终止前的输出再次很有帮助
最有趣的话题是这里的warning。我浓缩了很多消息,实际上比这多得多
c:\cpp\crtdbg4wince\sample2.cxx(36) : 'wmain': new(0x003d87a8,16 byte) registered, ok.
ctor okay
c:\cpp\crtdbg4wince\sample2.cxx(38) : 'wmain': WSAStartup(0x00000000,1 refcount) registered, ok.
c:\cpp\crtdbg4wince\sample2.cxx(41) : 'wmain':
CreateFile("123.txt", 0x00000fb0,1 F_handle) registered, ok.
c:\cpp\crtdbg4wince\sample2.cxx(44) : 'wmain':
CreateFile(0x00000fac,1 F_handle) registered, ok.
c:\cpp\crtdbg4wince\sample2.cxx(44) : 'wmain':
CloseHandle for <win32api>("123.txt",0x00000fb0,1) in c:\cpp\crtdbg4wince\sample2.cxx(41) :
'wmain': , ok
c:\cpp\crtdbg4wince\sample2.cxx(38) : 'wmain':
WSACleanup for WSAStartup(0x00000000,1) in c:\cpp\crtdbg4wince\sample2.exe(0) : 'unknown_func': , ok
dtor okay
c:\cpp\crtdbg4wince\sample2.cxx(36) : 'wmain':
delete for new(0x003d87a8,16) in c:\cpp\crtdbg4wince\sample2.exe(0) : 'unknown_func': , ok
c:\cpp\crtdbg4wince\sample2.cxx(50) : 'wmain': ===[ CrtDumpMemoryLeaks start]===================
...
c:\cpp\crtdbg4wince\sample2.cxx(50) : warning W0001: 'wmain': still 2 F_handle in use by CreateFile()!
c:\cpp\crtdbg4wince\sample2.cxx(44) :
warning W0001: 'wmain': CloseHandle(0x00000fac,1) missing for this allocation,
or two times allocated to same pointer, or just not closed yet.
c:\cpp\crtdbg4wince\sample2.cxx(50) :
warning W0001: 'wmain': CloseHandle(0x00000fac,1) missing near here.
See previous line for CreateFile() location.
c:\cpp\crtdbg4wince\sample2.cxx(50) : 'wmain': ===[ CrtDumpMemoryLeaks stopp]===================
...
同样,一切都是鼠标可点击的,所以你可以立即跳转到可疑的行。一些解释
-
WSAStartup(0x00000000,1 refcount)
WSAStartup
不创建内存块(0x00000000)WSAStartup
增加1个引用计数。
CreateFile("123.txt", 0x00000fb0,1 F_handle)
CreateFile
打开了一个名为“123.txt”的文件,这可能是一个非const的运行时值。CreateFile
获得了句柄0x00000fb0
,并将引用计数增加了1
。- 特别:
CreateFile(0x00000fac,1 F_handle)
和CloseHandle("123.txt",0x00000fb0,1)
CreateFile
如何不知道文件名?
这是因为我们在这里看到了用文件句柄调用的DuplicateHandle()
。
并且因为我们设置了DUPLICATE_CLOSE_SOURCE
,它然后调用了“123.txt”前一个句柄的CloseHandle()
。
程序终止时的输出
最有趣的话题是这里的error和assertion。assertion
s也调用了MessageBox
,因为
_CrtSetReportMode( _CRT_ASSERT, ..... | _CRTDBG_MODE_WNDW );
但请看可点击的(精简的)输出。统计数据和整洁的API
..
c:\cpp\crtdbg4wince\sample2.exe(0) : 'at exit': Leakage Summary at program termination point
...
c:\cpp\crtdbg4wince\sample2.exe(0) : 'at exit': peak ever used new(): 16 byte, just for information.
c:\cpp\crtdbg4wince\sample2.exe(0) : 'at exit':
peak ever used CreateFile(): 3 F_handle, just for information.
c:\cpp\crtdbg4wince\sample2.exe(0) : 'at exit': malloc/free are okay (or never used).
然后是问题点
c:\cpp\crtdbg4wince\sample2.exe(0) : error R0001: 'at exit': still 16 byte in use by new()!
c:\cpp\crtdbg4wince\sample2.exe(0) : error R0001: 'at exit': still 2 F_handle in use by CreateFile()!
c:\cpp\crtdbg4wince\sample2.cxx(36) : error R0001: 'wmain':
delete(0x003d88b8,16) missing for this allocation, or two times allocated to same pointer.
c:\cpp\crtdbg4wince\sample2.exe(0) : assertion A0001: 'at exit':
delete(0x003d88b8,16) missing near here. See previous line for new() location.
c:\cpp\crtdbg4wince\sample2.cxx(38) : error R0001: 'wmain':
WSACleanup(0x00000000,1) missing for this allocation, or two times allocated to same pointer.
c:\cpp\crtdbg4wince\sample2.exe(0) : assertion A0001: 'at exit':
WSACleanup(0x00000000,1) missing near here. See previous line for WSAStartup() location.
c:\cpp\crtdbg4wince\sample2.cxx(44) : error R0001: 'wmain':
CloseHandle(0x00000fac,1) missing for this allocation, or two times allocated to same pointer.
c:\cpp\crtdbg4wince\sample2.exe(0) : assertion A0001: 'at exit':
CloseHandle(0x00000fac,1) missing near here. See previous line for CreateFile() location.
关注点
我认为这个项目很有趣,因为它能找到各种alloc
、new
、CreateFile
。并且它声称很快能找到更多。
历史
- 2012-05-26:我修复了一些拼写错误(抱歉,荷兰语是我母语)。期间,Modem Man信守承诺修复了DEBUG/RELEASE问题。Modem Man还修复了一些altcecrt.h的问题并发布了0.06版本,该版本现在也可以用未修改的PPC2003 SDK编译。他所有的更改都已在本文章中进行了修订。我添加了我所有的示例代码,以及Visual Studio项目文件。
- 2012-05-25:Tim Corey和Dave Kreskowiak建议我改进这篇文章。已完成。怎么样?
- 2012-05-24:我首次介绍了该项目,得到了crtdbg4wince的作者的一些建议,并且我反过来帮助他修复了0.05版本的一个bug。