万星星@豌豆荚 欢迎加入我们
一个吃软饭的男人!!!!!我只想写程序####
微博:http://weibo.com/wanlianwen
posts - 172,  comments - 1253,  trackbacks - 0


很久没有写技术方面的东西了,这半年主要是在学习别人的东西,对自己提高比较大,算是一次技术回笼吧,这次学习之旅目的是结束技术方面的专注,开始向应用方面找突破口,也就是完成技术积累或者为技术的积累做坚实的准备。

c/C++的一个让人疯狂的地方就是内存管理,非法访问、越界、野指针、泄漏、内存分配器等诸多问题,有时候一个编程老手也会迷惘困惑。Crt有一些堆栈检查的函数可以完成基本的内存状况检查,MFC也有一些简单的对象检查机制,当然好的算是java、.net等sdk的超重量级封装了,即使发生对象错误也能把堆栈信息明明白白的告诉你(至少表面上是这样,具体我对这两种语言没有做过开发)。下面介绍的是某牛公司实现的内存分配工具,基本实现了内存泄漏检查,对象合法性检查,对于我来说已经够用了。


为了对内存分配块进行跟踪,设计如下结构体:

//+--------------------------------------------------------------
//
// 每个请求分配内存块的前缀结构体
// 用来跟踪所有请求分配块以及请求分配名称
//
//---------------------------------------------------------------
struct DBGALLOCHDR
{
    DBGALLOCHDR
*    pdbgahPrev; // 前一个内存块头
    DBGALLOCHDR*    pdbgahNext; // 后一个内存块头
    DWORD           iAllocated; // 记录是第几次请求分配操作
    DWORD           tid;        // 请求分配线程的ID
    size_t          cbRequest;  // 请求分配大小
    char            szName[64]; // 请求分配块名称
    DWORD           adwGuard[4];// 保护头
}
;

//+--------------------------------------------------------------
//
// 每个请求分配内存块的后缀结构体
// 使用特定的数据填充用来检测指针是合法
//
//---------------------------------------------------------------
struct DBGALLOCFOOT
{
    DWORD adwGuard[
4];
}
;

// 内存跟踪块的根,通过根可以获取所有分配块
DBGALLOCHDR g_dbgahRoot =
{
    
&g_dbgahRoot,
    
&g_dbgahRoot,
    
0,
    (DWORD)
-1
}
;


为了实现多线程内存分配跟踪,采用Tls技术使用线程局部对象保存当前分配信息:
// 线程局部对象结构体,辅助实现每个线程的请求内存分配记录
struct DBGTHREADSTATE
{
    DBGTHREADSTATE
* ptsNext;
    DBGTHREADSTATE
* ptsPrev;

    
// Add globals below
    void*           pvRequest;  // 线程最后一次请求分配内存的指针
    size_t          cbRequest;  // 线程最后一次请求分配内存的大小
}
;



// 调试期间实际分配内存大小=请求分配+分配头+分配尾
size_t _ActualSizeFromRequestSize(size_t cb)
{
    
return cb+sizeof(DBGALLOCHDR)+sizeof(DBGALLOCFOOT);
}


主要实现的内存分配工具有如下这些:
void*   _MemAlloc(ULONG cb);
void*   _MemAllocClear(ULONG cb);
HRESULT _MemRealloc(
void** ppv, ULONG cb);
ULONG   _MemGetSize(
void* pv);
void    _MemFree(void* pv);
HRESULT _MemAllocString(LPCTSTR pchSrc, LPTSTR
* ppchDst);
HRESULT _MemAllocString(ULONG cch, LPCTSTR pchSrc, LPTSTR
* ppchDst);
HRESULT _MemReplaceString(LPCTSTR pchSrc, LPTSTR
* ppchDest);

#define MemAlloc(cb)                            _MemAlloc(cb)
#define MemAllocClear(cb)                       _MemAllocClear(cb)
#define MemRealloc(ppv, cb)                     _MemRealloc(ppv, cb)
#define MemGetSize(pv)                          _MemGetSize(pv)
#define MemFree(pv)                            _MemFree(pv)
#define MemAllocString(pch, ppch)               _MemAllocString(pch, ppch)
#define MemAllocStringBuffer(cch, pch, ppch)    _MemAllocString(cch, pch, ppch)
#define MemReplaceString(pch, ppch)             _MemReplaceString(pch, ppch)
#define MemFreeString(pch)                      _MemFree(pch)


通过宏实现类的new delete重写:
#define DECLARE_MEMALLOC_NEW_DELETE() \
    inline 
void* __cdecl operator new(size_t cb)    return(MemAlloc(cb)); } \
    inline 
void* __cdecl operator new[](size_t cb)  return(MemAlloc(cb)); } \
    inline 
void __cdecl operator delete(void* pv)   { MemFree(pv); }

#define DECLARE_MEMCLEAR_NEW_DELETE() \
    inline 
void* __cdecl operator new(size_t cb)    return(MemAllocClear(cb)); } \
    inline 
void* __cdecl operator new[](size_t cb)  return(MemAllocClear(cb)); } \
    inline 
void __cdecl operator delete(void* pv)   { MemFree(pv); }


在应用的时候可以重写全局new delete:
// 测试全局new delete
void* __cdecl operator new(size_t cb)    return(MemAlloc(cb)); }
void* __cdecl operator new[](size_t cb)  return(MemAlloc(cb)); }
void __cdecl operator delete(void* pv)   { MemFree(pv); }


使用注意:
进程启动时候需要调用:
_DbgDllProcessAttach();
_afxGlobalData._hProcessHeap = GetProcessHeap();

进程退出的时候需要调用:
_DbgDllProcessDetach();

测试用例:

// 测试基本类型
void TestBuiltin()
{
    
// 基本类型
    int* pInt = new int(10);
    
int* pIntAry = new int[10];
    
char* pStr = new char[100];
    MemSetName((pStr, 
"String"));
}


// 测试class
void TestClass()
{
    Cls
* pCls = new Cls();
}


// 测试释放
void TestOk()
{
    Cls
* pCls = new Cls();
    delete pCls;
    pCls 
= NULL;
}


DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
    
int* pIntAry = new int[100];
    
return 0;
}


// 测试多线程
void TestMultiThread()
{
    HANDLE hHandle 
= CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    WaitForSingleObject(hHandle, 
-1);
}


int main(int argc, char* argv[])
{
    _DbgDllProcessAttach();

    _afxGlobalData._hProcessHeap 
= GetProcessHeap();

    TestBuiltin();
    TestClass();
    TestMultiThread();
    TestOk();

    _DbgDllProcessDetach();
    
return 0;
}


调试输出窗口结果:
A +    4 -    0 = [      4]
A +   40 -    0 = [     44]
A +  100 -    0 = [    144]
A +    8 -    0 = [    152]
A +  400 -    0 = [    552]
The thread 0x1D38 has exited with code 0 (0x0).
A +    8 -    0 = [    560]
F +    0 -    8 = [    552]
---------- Leaked Memory Blocks ----------
p=0x00144354  cb=400  #=4    TID:0x1d38
p=0x00144294  cb=8    #=3    TID:0x1878
p=0x001441a4  cb=100  #=2    TID:0x1878 String
p=0x001440ec  cb=40   #=1    TID:0x1878
p=0x00142a54  cb=4    #=0    TID:0x1878
total size 552, peak size 560
---------- Leaked Memory Blocks End ------


其中A表示分配 F表示释放

 

该工具本人初试没有中毒症状,打算纳入个人小宝库中,希望大家喜欢!

下载

posted on 2009-06-03 16:16 万连文 阅读(2759) 评论(11)  编辑 收藏 引用 所属分类: 乱七八糟

FeedBack:
# re: 技术回归01-Windows内存分配工具
2009-06-03 16:51 | OwnWaterloo
operator delete[]呢?  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-03 17:33 | 万连文
因为底层实现是Heap**操作,所以delete和delete[]都会调用HeapFree,所以恰好可以完成任务。  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-03 21:30 | OwnWaterloo
@万连文
某某牛库是哪个库?
其实跟有没有库这样做没关系,跟该库牛不牛也没什么关系。
不能跟着别人错。

底层实现是HeapXXX?
其实跟底层实现到底是什么没关系。
C++标准没要求底层实现怎么做, 但是要求重载operator new必须有其对应形式的operator delete重载。


跟测不测试也没关系。
一次不带TT不会怀上不等于次次都不会怀上。
一个编译器上测试成功,不等于每个编译器都能测试成功。
在某个编译器上能测试通过,并不能证明或证伪。


编写operator new/delete要遵守一些规范。
HeapAlloc能否处理 size=0?
HeapAlloc分配失败时的new_hanlder处理?
对其他形式new的隐藏?
"重载"(而不是覆盖全局标准)op new时,如果不提供相应形式的op delete,是否也能正确工作?
与其在不同平台,不同编译器,甚至是相同编译器的不同版本下进行测试,不如一开始就按照标准形式编写。  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 07:42 | 万连文
感谢你的指导, 我没有说跨平台, 也没有说实用多个编译器,我一直使用VC系列工具在Windows上面写程序,其他的编译器和OS了解很少,对C++标准也研究不够,出发点只是想找到一个经过验证的(大的、成功的产品使用过的)简便的工具。 我想软件工具都不是完美的,考虑太多就没完没了了,我坚持实用原则。  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 10:32 | OwnWaterloo
  1#include <stddef.h>
  2#include <stdlib.h>
  3#include <stdio.h>
  4#include <vector>
  5
  6#include <stdexcept>
  7
  8
  9#if defined(_MSC_VER) && _MSC_VER<1400
 10#include <new.h>
 11namespace std using ::set_new_handler; using ::new_handler; }
 12#else
 13#include <new>
 14#endif
 15
 16
 17void* allocate(size_t size) {
 18    if (size==0) size = 1;
 19    for (void* p=0;;) {
 20        p = malloc(size);
 21        if (p) {
 22            printf("allocate %p\n",p);
 23            return p;
 24        }

 25        std::new_handler handler = std::set_new_handler(0);
 26        std::set_new_handler(handler);
 27        if (handler)
 28            handler();
 29        else
 30            throw std::bad_alloc();
 31    }

 32}

 33
 34void deallocate(void* ptr) {
 35    printf("deallocate %p\n",ptr);
 36    free(ptr);
 37}

 38
 39void* operator new(size_t size) return allocate(size); }
 40void* operator new[](size_t size) return allocate(size); }
 41void operator delete(void* ptr) { deallocate(ptr); }
 42
 43class C {
 44    static int count;
 45public:
 46    static bool fail;
 47    C() {
 48        if (fail)
 49            throw std::exception();
 50        printf("C::C(),%d\n",++count);
 51    }

 52    ~C() {
 53        printf("C::~C(),%d\n",count--);
 54    }

 55    C(const C& ) {
 56        printf("C::(const C&),%d\n",++count);
 57    }

 58
 59
 60    //void* operator new(size_t,void* place) { return place; }
 61    void* operator new(size_t size) return allocate(size); }
 62    void* operator new[](size_t size) return allocate(size); }
 63    void operator delete(void* ptr) { deallocate(ptr); }
 64}
;
 65bool C::fail;
 66int C::count;
 67
 68struct S {
 69    static bool fail;
 70    S() {
 71        if (fail)
 72            throw std::exception();
 73        printf("construct\n");
 74    }

 75    ~S() {
 76        printf("destroy\n");
 77    }

 78}
;
 79bool S::fail;
 80
 81void test_class(int dim) {
 82    if (dim<=0)
 83        return;
 84    C::fail = dim==4;
 85    C* arr = new C[dim];
 86    delete[]  arr;
 87}

 88
 89
 90void test_global(int dim) {
 91    if (dim<=0)
 92        return;
 93    S::fail = dim==4;
 94    S* arr = new S[dim];
 95    delete[] arr;
 96}

 97
 98int main() {
 99    using namespace std;
100    int dim = 0;
101    for (printf("input dim: ");scanf("%d",&dim)==1;printf("input dim: "))
102    {
103        try {
104            test_class(dim);
105        }

106        catch (std::exception& ) {
107            printf("  ---- catch an exception ----\n");
108        }

109        try {
110            test_global(dim);
111        }

112        catch (std::exception& ) {
113            printf("  ---- catch an exception ----\n");
114        }

115    }

116}

117
 
有两个底层分配函数allocate和deallocate,它们使用malloc和free。
用这两个函数实现全局的3个op new,op new[], op delete,没有op delete[]
还用这两个函数实现了C的3个op new,op new[], op delete,同样没有op delete[]
用如下参数编译
cl /EHsc /MD /analyze /W3
 
你看看结果吧。
我用vc8、9测过(vc6不支持动态crt库,vc10我没装)。
反正两处delete[] arr;都没有调用 op delete。
它们调用那个全局的,没有被重写的op delete[]。
如果静态链接,该全局默认的op delete[]会被inline, 再调用该编译单元中定义的op delete。
如果动态链接,op delete[]不会被inline,会调用crt库中提供的op delete。
总之,这两处delete[] arr;都没有调用deallocate。
 
当然, 你可以说你只静态链接到crt库。
也可以说你的allocate和deallocate底层实现绝对会一直保持与vc提供的crt兼容
但是,你的代码的用户了解么?
难道你打算在文档中写“使用我的库的代码者,使用的crt库必须满足XXX要求,必须自己测试YYY”,只是为了你自己可以少写一个 op delete[]
这不是程序库开发者的态度。
 
还有两个小问题。
C* pc = static_cast<C*>(malloc(sizeof(*pc));
new (pc) C; // 编译错误
C* pc2 = new (std::nothrow) C; // 编译错误
 
当然, 你还是可以说你绝对不会用这种东西, 你是实用主义嘛。
但是你的库的使用者呢?
 
 
“出发点只是想找到一个经过验证的(大的、成功的产品使用过的)简便的工具”
你觉得这可以说明该产品中的每个细节都是无可挑剔的么?
越是大的产品,测试越不容易,更不容易暴露其中的问题,同时也许忽悠客户也更容易。
 
 
确实没有任何事物都是完美的,但不能连追求完美的心都舍弃了。
同时,从实用角度出发,让该库变得更完美,所付出的代价非常小,“按规则”办事就可以了,10来行代码的事,何乐而不为?
规则可以见《EffCpp》或者《C++CodingStandard》。
  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 10:32 | OwnWaterloo
sorry…… 不小心提交了2次 ……
我自己删掉一个 ……  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 11:29 | 万连文
因为我是看微软这么做的,自己确实没有发现什么错误,我没法不承认她的实现可以为我所用,可能就是中毒太深吧。您上面写的那些已经超出了我的知识范围,我需要好好学习。至于 vc6不支持动态crt库 好像可以通过修改/MLd到/MDd实现。  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 12:04 | OwnWaterloo
@万连文
微软的哪? MFC?

上面的命令行 cl /MD 已经告诉编译器链接到动态库了。
vc6不支持是因为它只有静态库。 crt的动态库是跟.net那一套东西一起出来的。  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 12:13 | 万连文
微软的什么我没有必要告诉你,crt的动态库我不晓得你怎么想,用dependency工具看看就晓得依赖不依赖了,我对自己的认识也不是十足的把握。   回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-04 17:59 | 99网上书城
感谢你的指导  回复  更多评论
  
# re: 技术回归01-Windows内存分配工具
2009-06-05 17:50 | Touchsoft
struct DBGALLOCHDR
这个结构好像在调试时见到过。  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


简历下载
联系我

<2009年6月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

常用链接

留言簿(66)

随笔分类

随笔档案

相册

搜索

  •  

最新评论

阅读排行榜

评论排行榜