批评我的非侵入式堆调试器

Posted

技术标签:

【中文标题】批评我的非侵入式堆调试器【英文标题】:Critique my non-intrusive heap debugger 【发布时间】:2011-02-19 14:17:09 【问题描述】:

这是昨天Critique my heap debugger 的后续行动。正如 bitc 所建议的,我现在将有关已分配块的元数据保存在单独的手写哈希表中。

堆调试器现在检测到以下类型的错误:

    内存泄漏(现在有更详细的调试输出) 传递给 delete 的非法指针(也负责双重删除) 错误的删除形式(数组与非数组) 缓冲区溢出 缓冲区下溢

欢迎讨论,提前致谢!

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <new>

namespace

    // I don't want to #include <algorithm> for a single function template :)
    template <typename T>
    void my_swap(T& x, T& y)
    
        T z(x);
        x = y;
        y = z;
    

    typedef unsigned char byte;

    const byte CANARY[] = 0x5A, 0xFE, 0x6A, 0x8D,
                           0x5A, 0xFE, 0x6A, 0x8D,
                           0x5A, 0xFE, 0x6A, 0x8D,
                           0x5A, 0xFE, 0x6A, 0x8D;

    bool canary_dead(const byte* cage)
    
        bool dead = memcmp(cage, CANARY, sizeof CANARY);
        if (dead)
        
            for (size_t i = 0; i < sizeof CANARY; ++i)
            
                byte b = cage[i];
                printf(b == CANARY[i] ? "__ " : "%2X ", b);
            
            putchar('\n');
        
        return dead;
    

    enum kind_of_memory AVAILABLE, TOMBSTONE, NON_ARRAY_MEMORY, ARRAY_MEMORY;

    const char* kind_string[] = 0, 0, "non-array memory", "    array memory";

    struct metadata
    
        byte* address;
        size_t size;
        kind_of_memory kind;

        bool in_use() const
        
            return kind & 2;
        

        void print() const
        
            printf("%s at %p (%d bytes)\n", kind_string[kind], address, size);
        

        bool must_keep_searching_for(void* address)
        
            return kind == TOMBSTONE || (in_use() && address != this->address);
        

        bool canaries_alive() const
        
            bool alive = true;
            if (canary_dead(address - sizeof CANARY))
            
                printf("ERROR:    buffer underflow at %p\n", address);
                alive = false;
            
            if (canary_dead(address + size))
            
                printf("ERROR:     buffer overflow at %p\n", address);
                alive = false;
            
            return alive;
        
    ;

    const size_t MINIMUM_CAPACITY = 11;

    class hashtable
    
        metadata* data;
        size_t used;
        size_t capacity;
        size_t tombstones;

    public:

        size_t size() const
        
            return used - tombstones;
        

        void print() const
        
            for (size_t i = 0; i < capacity; ++i)
            
                if (data[i].in_use())
                
                    printf(":( leaked ");
                    data[i].print();
                
            
        

        hashtable()
        
            used = 0;
            capacity = MINIMUM_CAPACITY;
            data = static_cast<metadata*>(calloc(capacity, sizeof(metadata)));
            tombstones = 0;
        

        ~hashtable()
        
            free(data);
        

        hashtable(const hashtable& that)
        
            used = 0;
            capacity = 3 * that.size() | 1;
            if (capacity < MINIMUM_CAPACITY) capacity = MINIMUM_CAPACITY;
            data = static_cast<metadata*>(calloc(capacity, sizeof(metadata)));
            tombstones = 0;

            for (size_t i = 0; i < that.capacity; ++i)
            
                if (that.data[i].in_use())
                
                    insert_unsafe(that.data[i]);
                
            
        

        hashtable& operator=(hashtable copy)
        
            swap(copy);
            return *this;
        

        void swap(hashtable& that)
        
            my_swap(data, that.data);
            my_swap(used, that.used);
            my_swap(capacity, that.capacity);
            my_swap(tombstones, that.tombstones);
        

        void insert_unsafe(const metadata& x)
        
            *find(x.address) = x;
            ++used;
        

        void insert(const metadata& x)
        
            if (2 * used >= capacity)
            
                hashtable copy(*this);
                swap(copy);
            
            insert_unsafe(x);
        

        metadata* find(void* address)
        
            size_t index = reinterpret_cast<size_t>(address) % capacity;
            while (data[index].must_keep_searching_for(address))
            
                ++index;
                if (index == capacity) index = 0;
            
            return &data[index];
        

        void erase(metadata* it)
        
            it->kind = TOMBSTONE;
            ++tombstones;
        
     the_hashset;

    struct heap_debugger
    
        heap_debugger()
        
            puts("heap debugger started");
        

        ~heap_debugger()
        
            the_hashset.print();
            puts("heap debugger shutting down");
        
     the_heap_debugger;

    void* allocate(size_t size, kind_of_memory kind) throw (std::bad_alloc)
    
        byte* raw = static_cast<byte*>(malloc(size + 2 * sizeof CANARY));
        if (raw == 0) throw std::bad_alloc();

        memcpy(raw, CANARY, sizeof CANARY);
        byte* payload = raw + sizeof CANARY;
        memcpy(payload + size, CANARY, sizeof CANARY);

        metadata md = payload, size, kind;
        the_hashset.insert(md);
        printf("allocated ");
        md.print();
        return payload;
    

    void release(void* payload, kind_of_memory kind) throw ()
    
        if (payload == 0) return;

        metadata* p = the_hashset.find(payload);

        if (!p->in_use())
        
            printf("ERROR:   no dynamic memory at %p\n", payload);
        
        else if (p->kind != kind)
        
            printf("ERROR:wrong form of delete at %p\n", payload);
        
        else if (p->canaries_alive())
        
            printf("releasing ");
            p->print();
            free(static_cast<byte*>(payload) - sizeof CANARY);
            the_hashset.erase(p);
        
    


void* operator new(size_t size) throw (std::bad_alloc)

    return allocate(size, NON_ARRAY_MEMORY);


void* operator new[](size_t size) throw (std::bad_alloc)

    return allocate(size, ARRAY_MEMORY);


void operator delete(void* payload) throw ()

    release(payload, NON_ARRAY_MEMORY);


void operator delete[](void* payload) throw ()

    release(payload, ARRAY_MEMORY);


int main()

    int* p = new int[1];
    delete p;   // wrong form of delete
    delete[] p; // ok
    delete p;   // no dynamic memory (double delete)

    p = new int[1];
    p[-1] = 0xcafebabe;
    p[+1] = 0x12345678;
    delete[] p; // underflow and overflow prevent release
                // p is not released, hence leak

【问题讨论】:

你不能只是编辑这个问题的昨天版本,而不是开始另一个关于同一主题的问题吗? @Paul R:这个完全不同,因为它可能会起作用。 不要使用异常规范,如果你想让人们知道它,只需将它放在评论中:) @Matt 如果我从operator newoperator new[]operator deleteoperator delete[] 中删除异常规范,编译器会报错。 我尝试使用这个解决方案,我可以看到一些泄漏,但是我只能看到地址。如果我尝试使用 https://***.com/a/1395287/474535 扩展此示例,我的编译器会给出类似 invalid conversion from 'const void*' to 'void*' 的错误。有没有办法让 FILE 和/或 LINEFUNCTION 与这个解决方案一起工作? (编译器 i386-wrs-vxworks-g++,GnuTools 4.1.2) 【参考方案1】:

确实很好。您的金丝雀实际上可以揭示一些实际的上溢/下溢情况(尽管并非所有这些情况都像 Matthieu 指出的那样)。

还有什么。您可能会在使用多线程应用程序时遇到一些问题。也许保护哈希表免受并发访问?

现在您记录了每次分配和解除分配,您可以(如果愿意)提供有关正在测试的程序的更多信息。在任何给定时间了解分配的总数和平均数量可能会很有趣?分配的总字节数、最大字节数、最小字节数和平均字节数,以及分配的平均寿命。

如果你想比较不同的线程,至少对于 Pthreads,你可以用 pthread_self() 来识别它们。这个堆调试器可以成为一个非常有用的分析工具。

【讨论】:

【参考方案2】:

您是否使用了一个非常弱的 malloc,它还没有内置这种东西?因为如果它在那里,你就会将开销翻倍,而收益却微乎其微。此外,这种系统在进行小对象分配时确实很痛苦,或者对它们无效,因为人们自己进行 1 分配和管理内存。

就代码而言,它看起来就像你说的那样,它看起来设计得很好并且易于阅读。但是,如果您要经历这样做的麻烦,为什么不使用托管容器/指针/operator [] 事物在源头捕获缓冲区溢出/溢出。这样一来,您就可以在故障现场进行调试,而不是随意发现发生了什么坏事。

我相信其他人会发现一些效率,但这些只是我在查看您的代码几分钟后的一些想法。

【讨论】:

【参考方案3】:

我想知道下溢/上溢的检测。

我的意思是,如果我有 10 个元素的数组,那么您似乎会检测到我是否写在 -110,但是如果我写在 20 怎么办?下溢或溢出不一定是缓冲区溢出的一部分(连续)。

此外,阻止释放块的意义何在?这个街区(相对)很好,它是你(不幸)破坏的邻居。

无论如何,这对我来说似乎很好,虽然我可能每个函数有多个返回,因为单出口没有意义。你看起来更像一个 C 程序员而不是 C++ 程序员 :)

【讨论】:

在两个方向上检测到最多 16 个字节的下溢和上溢。如果这对您来说还不够,只需增加 CANARY 数组的大小;-)

以上是关于批评我的非侵入式堆调试器的主要内容,如果未能解决你的问题,请参考以下文章

Android基于AOP的非侵入式监控之——AspectJ实战

获取非侵入式升压序列化 C++ 的私有数据成员

如何检测非侵入式电话

Spring 侵入式和非侵入式

Windbg非侵入性调试(用户模式)

基于SpringBoot轻量非侵入式数据库数据告警器