Критикуйте мой ненавязчивый отладчик "кучи"

Это - продолжение Критического анализа мой отладчик "кучи" со вчерашнего дня. Как предложено bitc, я теперь сохраняю метаданные о выделенных блоках в отдельной рукописной хеш-таблице.

Отладчик "кучи" теперь обнаруживает следующие виды ошибок:

  1. утечки памяти (теперь с большей подробной отладочной информацией)
  2. недопустимые указатели передали для удаления (который также заботится о двойных, удаляет),
  3. неправильная форма удаляет (массив по сравнению с немассивом)
  4. переполнение буфера
  5. недостаточные наполнения буфера

Не стесняйтесь обсуждать и заранее спасибо!

#include 
#include 
#include 
#include 

namespace
{
    // I don't want to #include  for a single function template :)
    template 
    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(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(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(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(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(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
}

13
задан 3 revs 23 May 2017 в 12:18
поделиться

3 ответа

Очень мило, правда. Ваши канарейки действительно могут выявить некоторые реальные случаи переполнения / недостаточного заполнения (хотя не все из них, как указал Матье).

Более того. У вас могут возникнуть проблемы с многопоточным приложением.Может быть, защитить хеш-таблицу от одновременного доступа?

Теперь, когда вы регистрируете каждое выделение и освобождение, вы можете (если хотите) предоставить дополнительную информацию о тестируемой программе. Может быть интересно узнать общее и среднее количество распределений в любой момент времени? Общий, максимальный, минимальный и средний размер выделенных байтов, а также средний срок жизни выделенных ресурсов.

Если вы хотите сравнить разные потоки, по крайней мере, с помощью Pthreads, вы можете идентифицировать их с помощью pthread_self (). Этот отладчик кучи может стать весьма полезным инструментом анализа.

5
ответ дан 2 December 2019 в 01:31
поделиться

Вы используете очень слабый malloc, в который еще не встроены подобные вещи? Потому что, если он есть, вы удваиваете накладные расходы без особой выгоды. Кроме того, такая система действительно мешает при выделении небольших объектов или неэффективна с ними, поскольку люди выделяют 1 и сами управляют памятью.

Что касается кода, то похоже, что он будет делать то, что вы говорите, он хорошо разработан и легко читается. Но, если вы собираетесь пройти через это, почему бы не поймать потоки избыточного / недостаточного буфера в источнике с помощью управляемых контейнеров / указателей / элементов оператора []. Таким образом, вы можете отлаживать на месте сбоя, вместо того, чтобы бесплатно обнаруживать, что произошло что-то плохое.

Я уверен, что другие найдут эффективность, но это лишь некоторые мысли, пришедшие мне из головы после просмотра вашего кода в течение нескольких минут.

2
ответ дан 2 December 2019 в 01:31
поделиться

Мне интересно узнать об обнаружении переполнения / потери значимости.

Я имею в виду, что если у меня есть массивы из 10 элементов, то, похоже, вы обнаружите, если я напишу в -1 и 10 , но что, если я напишу в ] 20 ? Незаполнение или переполнение не обязательно происходит как часть переполнения буфера (непрерывного).

Кроме того, какой смысл предотвращать выпуск блока? Этот блок (относительно) в порядке, это соседи, которых вы (к сожалению) испортили.

В любом случае, мне это кажется довольно хорошим, хотя я, вероятно, получу более одного возврата для каждой функции, потому что в Single Exit нет смысла. Вы кажетесь больше программистом на C, чем на C ++ :)

2
ответ дан 2 December 2019 в 01:31
поделиться
Другие вопросы по тегам:

Похожие вопросы: