Приветствую читатель!

Для тех кто со мной впервые вот оглавление:

Часть 1

Часть 2

Часть 3

Код лежит тут

Подразумевается что читатель знаком с архитектурой аллокатора из части 3 и понимает алгоритм неявного списка свободных блоков который был освещен в части 1

Аллокатор работает стабильно, все тесты зеленые, включая тесты на стабильность. И следующим шагом логично бы реализовать перегрузки new и delete для abi, но вот незадача: там есть версии принимающие дополнительный аргумент, а именно выравнивание. Эту фичу я реализовать как раз забыл. В архитектуре которая рассматривается в предыдущей статье это оказалось простой, но интересной задачей. Ее мы и обсудим ниже.

Решение потребовало реализации функции mem_malloc_aligned которая выделит бОльший кусок памяти с учетом запрошенного выравнивания что бы мы там точно нашли правильно выровненный адрес.

Но что если адрес указателя из mem_malloc_aligned не совпадает с адресом указателя который вернул mem_malloc? Что делать в mem_free? Что делать в mem_realloc? Как мне работать с указателем перед которым не хедера?

Для начала я решил применить технику добавления смещения перед payload выровненного блока вместо хедера, смещения до payload изначального блока у которого есть хедер и футер.

Но как мне отличить offset от header? Я решил добавить magic number в хедер и футер увеличив тем самым размер оверхеда в 2 раза и раз уж от него считалось внутреннее выравнивание блоков памяти в аллокаторе и минимальный размер блока, то теперь минимальный размер блока стал 32 байта, а с оверхедом все 64. Теперь можно просто проверять magic number и если он не совпадает, то интерпретировать число на месте хедера как смещение до payload блока который вернул mem_malloc и далее получив на него указатель работать с блоком стандартным образом.

Самым простым способом добавить magic number было сделать его частью хедера и футера записывая его сразу же после байта с размером и состоянием в футере и перед ним в хедере.

Вот код:

static void mem_block_put_to_header(void *_p, size_t _sz, size_t state)
{
    auto header = mem_block_header(_p);
    mem_block_pack(header, _sz, state);
    *mem_block_size_t_ptr(header + kMagicNumberSize) = kMagicNumber;
}

static void mem_block_put_to_footer(void *_p, size_t _sz, size_t state)
{
    auto footer = mem_block_footer(_p);
    mem_block_pack(footer, _sz, state);
    *mem_block_size_t_ptr(footer - kMagicNumberSize) = kMagicNumber;
}

Теперь внутреннее устройство аллокатора выглядит как на диаграмме ниже. Диаграмма отображает на диаграмме хедер, футер и мейджик как разные вещи, но это лишь для наглядности. По сути как говорилось выше теперь с точки зрения арифметики указателей аллокатора мейджик это часть хедера и футера.

Рассмотрим реализацию mem_malloc_aligned. Она просто считает новый размер блока так что бы там точно оказался указатель выровненный по нужной границе и размер пейлоада был верным, далее она просто вызывает mem_malloc, там накладываются требования на внутреннее выравнивание и получается еще бОльший кусок памяти, часть которого неизбежно будет неиспользованной ...

Вот код:

// считаем размер памяти с выравниванием что бы мы 100% нашли там
// адрес выровненный как надо
static size_t mem_aligned_mem_size(size_t size,
                                   size_t align)
{
    return size
        // в теории на ARM размер size_t и его выравнивание могу быть разными
        + max(sizeof(size_t), alignof(size_t))
        + align - 1;
}
// возвращает адрес памяти выровненный по границе alignment
void *mem_malloc_aligned(size_t size, size_t alignment)
{
    if (alignment >= kAlignment) {
        size_t size_with_alignment = mem_aligned_mem_size(size, alignment);
// выделяем память обычным mem_malloc
        void *ptr = mem_malloc(size_with_alignment);

        if (ptr) {
// считаем выровненный адрес той же формулой что и обычно считали размер для блока
// только теперь учитываем sizeof(size_t) для offset
            auto address = reinterpret_cast<size_t>(ptr);
            auto aligned_ptr =
                reinterpret_cast<void *>(alignment
                    * ((address + sizeof(size_t) /* offset */ + alignment - 1) / alignment));

            if (aligned_ptr) {
// считаем смещение до блока с которым можем работать, т.е. до памяти которую
// выделил mem_malloc и записываем его как хедер выровненного блока
                mem_block_size_t_ptr(aligned_ptr)[-1] = mem_block_char_ptr(aligned_ptr) - mem_block_char_ptr(ptr);
                return aligned_ptr;
            }

            mem_free(ptr);
        }
    }

    return nullptr;
}

Вот и вся магия!

Вот так выглядит блок который отдает mem_malloc_aligned:

Как видно на диаграмме часть блока не используется из за требований к выравниванию адресов, так же как видно у нас есть offset вместо хедера и мейджик после хедера и перед футером. Таким образом мы можем реализовать резолвинг блока как проверку мейджика и если он кривой, то блок либо сломан, либо был выделен mem_malloc_aligned

Кот код проверки:

static void *mem_block_resolve_from_align(void *ptr)
{
    auto p = ptr;

    if (!mem_block_check_block(ptr)) {
        auto offset = *mem_block_get_magic_from_header(ptr);
        p = mem_block_char_ptr(ptr) - offset;
    }

    return p;
}

Непосредственно резолвинг:

static void *mem_block_resolve_from_align(void *ptr)
{
    auto p = ptr;

    if (!mem_block_check_block(ptr)) {
        auto offset = *mem_block_get_magic_from_header(ptr);
        p = mem_block_char_ptr(ptr) - offset;
    }

    return p;
}

Такой подход повышает требования к безопасности. Нам нужно как-то убедиться что у нас правильный у нас блок. За это отвечает функция mem_block_check_block:

static inline bool mem_block_check_block(void *ptr)
{
    if (*mem_block_get_magic_from_header(ptr) == kMagicNumber) {
        if ((reinterpret_cast<size_t>(ptr) % kAlignment) == 0) {
            if (*mem_block_header(ptr) == *mem_block_footer(ptr)) {
                return true;
            }
            else {
                ALOGE("Bad block. Header and footer are not the same");
            }
        }
        return true;
    }
    else {
        ALOGE("Bad magic number");
    }

    return false;
}

Вот новые функции mem_free и mem_realloc:

void *mem_realloc(void *ptr, size_t new_sz)
{
    if (!ptr) {
        return mem_malloc(new_sz);
    }

    void *p = mem_block_resolve_from_align(ptr);
    auto block = mem_malloc(new_sz);

    if (block) {
        memmove(block, p, min(new_sz, mem_block_size(p)));
        mem_free(p);
    }

    return block;
}

void mem_free(void *ptr)
{
    if (ptr) {
        void *p = mem_block_resolve_from_align(ptr);

        if (mem_block_check_block(p)) { // проверяем что блок полностью коррктен
            if (mem_block_is_allocated(p)) {
                size_t size = mem_block_size(p);
                mem_block_init(p, size, kBlockFree);
                p = mem_block_erase_merge(p);
                bin_insert(mem_block_list_head(p));
            }
        }
        else {
            ALOGE("%s(): Invalid pointer (%p)\n", __func__, ptr);
        }
    }
}

Ну и вот и все, готово!

До новых встреч!

Комментарии (9)


  1. AnSa8
    14.06.2026 06:09

    У вас в коде на Github есть две ошибки (уверен что их больше, но это первое что бросилось в глаза) при работе с std::vector в файлах main.cpp и allocator_test.cpp. std::vector::reserve не изменяет размер вектора. Замените на resize или укажите размер в конструкторе.

    После этих исправлений allocator_test выдаёт ошибку: ERROR: memory: Bad magic number в виде 20988 строк на тестах: MallocTest.EvenNotEvenFreeAligned, CombinedTest.RepeatedAlignedAllocations и AlignedAllocation.VerifyAlignment. При этом тесты считаются пройденными.


    1. GNU_Dimarik Автор
      14.06.2026 06:09

      ERROR: memory: Bad magic number это избыточный лог который выводится функцией mem_block_check_block которая вызывается в коде следующего вида:
      static void mem_block_resolve_from_align(void ptr){ auto p = ptr; if (!mem_block_check_block(ptr)) { auto offset = *mem_block_get_magic_from_header(ptr); p = mem_block_char_ptr(ptr) - offset; } return p; }
      думаю этот лог нужно убрать и оставить лог только в функции проверки целостности
      сделаю и только что увидел что не помешало бы добавить проверку в mem_realloc как в mem_free


    1. GNU_Dimarik Автор
      14.06.2026 06:09

      Благодарю вас что навели на мысль: https://github.com/GNUDimarik/small_allocator/commit/b77123deb5f6c6d2e7f50f5358413d0a0318de18#diff-45b5c5bc4e257b0cd52937a7cfb5217b4d4f26711cc3300c5e7aef051b4ef9e9R558
      не стесняйтесь создавать issue на github если наши ошибку где-то.
      Буду признателен если ошибки будут не в main.cpp он тестовый и его функция лишь в визуализации дампа для статьи и дебага, просто пишите каменты сюда о нем


      1. AnSa8
        14.06.2026 06:09

        Да это я так, можно сказать случайно заметил. Изучать аллокаторы мне не особо интересно.

        А почему вы используете C++20 и при этом у вас от C++ практически ничего нет? Я имею в виду new, delete, шаблоны и прочее.


        1. GNU_Dimarik Автор
          14.06.2026 06:09

          Это будет уже в libcxxabi, часть я взял из libcxxrt. Идея состоит в том, что у меня будут С совместимые апи в духе vsnprintf (будет следующая статья, уже в черновиках есть), malloc и т.д. и на этой базе я реализую уже new/delete с itatium hooks но без исключений. С апи нужны что бы взять например деманглер из libcxxrt, в общем зависимости для стороеннего кода на С который я возможно захочу использовать


    1. Genius_Russian_Coders
      14.06.2026 06:09

      По делу. Важный нюанс: на ARM (Cortex-A) unaligned доступ к atomic вызывает не деградацию, а исключение. C++17 решает это через over-aligned new — интересно сравнить с вашим подходом.


      1. GNU_Dimarik Автор
        14.06.2026 06:09

        Спасибо, арм только буду изучать. Взял книгу по асму RISC-V


  1. AnSa8
    14.06.2026 06:09

    Тест CallocTest.OverflowDetection бесполезен, т.к. он не проверяет переполнение. Точнее, переполнение не проверяет функция mem_calloc. Честно говоря, я не понимаю как вы собирались "поймать" переполнение такой записью: size_t count = size * num;

    Вот вам для примера вызов функции: mem_calloc( 18’446’744, 1’000’000’004’000 ). На x86-64 внутри функции переменная count будет равна 77’424’384, и это уже нормальный размер который функция mem_malloc вполне может выделить. Можно подобрать и другие параметры при которых count будет ещё меньше.


    1. GNU_Dimarik Автор
      14.06.2026 06:09

      Исправил, благодарю вас за внимательность!