OSDEV: Itanium ABI и Array Operator new Cookies — что должен знать каждый разработчик ОС

OSDEV: Itanium ABI часть 1: Array Operator new Cookies

Когда я впервые столкнулся с разработкой собственной операционной системы, одной из самых неочевидных ловушек стало управление памятью для массивов объектов C++. Казалось бы, что может пойти не так с new[] и delete[]? Оказывается, многое. Сегодня разберём, что такое Array Cookies в контексте Itanium C++ ABI, и почему это критически важно для OSDEV.

Что такое Itanium C++ ABI?

Itanium C++ ABI — это стандарт бинарного интерфейса приложений для C++, первоначально разработанный для процессоров Itanium, но ставший де-факто стандартом для GCC, Clang и других компиляторов на большинстве платформ (Linux, *BSD, macOS). Он определяет, как компилятор организует вызовы функций, обработку исключений, работу с виртуальными таблицами и, конечно, управление памятью.

Для разработчика ОС понимание ABI — не роскошь, а необходимость. Если ваша реализация аллокатора не соответствует ожиданиям компилятора, программа упадёт с segmentation fault в самый неподходящий момент. Особенно это касается работы с массивами.

Что такое Array Cookies?

Термин «Cookie» в контексте C++ ABI — это служебные данные, которые аллокатор размещает перед реальным массивом объектов. Вся соль в том, что когда вы пишете:

MyClass* arr = new MyClass[10];
delete[] arr;

Компилятор должен знать, сколько объектов было создано, чтобы вызвать деструктор для каждого из них. Это количество и хранится в Cookie — небольшом блоке памяти перед указателем, который возвращает operator new[].

Структура Cookie в Itanium ABI

Согласно спецификации Itanium C++ ABI (раздел 2.7), Cookie имеет следующий формат:

struct array_cookie {
    size_t element_count;  // количество элементов массива
    // возможно, дополнительное выравнивание
};

Размер Cookie — ровно sizeof(size_t) байт. Когда компилятор видит new T[n], он:
1. Вызывает operator new[](sizeof(array_cookie) + n * sizeof(T))
2. Сохраняет n в Cookie (по адресу, который вернул аллокатор)
3. Возвращает пользователю указатель на первый элемент массива (смещённый на sizeof(size_t))

А для delete[]:
1. Читает Cookie по смещению -sizeof(size_t) от переданного указателя
2. Вызывает деструктор для каждого из element_count объектов
3. Вызывает operator delete[] с исходным адресом (который включает Cookie)

Почему это важно для OSDEV?

Допустим, вы пишете собственный аллокатор для ядра ОС. Если ваша реализация operator new[] не учитывает Cookie, произойдёт следующее:

Проблема 1: Неправильный размер выделения
Компилятор ожидает, что вы выделите память с учётом Cookie. Если вы просто выделите n * sizeof(T), то при записи Cookie произойдёт выход за границы буфера.

Проблема 2: Нарушение выравнивания
Cookie должен быть выровнен так же, как size_t. Если ваш аллокатор возвращает блоки с выравниванием меньше, чем alignof(size_t), чтение Cookie может привести к неопределённому поведению.

Проблема 3: Неправильный адрес при освобождении
operator delete[] должен получить тот же адрес, который вернул operator new[], а не указатель на первый элемент. Если вы передадите неверный адрес — произойдёт повреждение кучи.

Практический пример: реализация аллокатора для ядра

Вот как может выглядеть корректная реализация для Itanium ABI:

#include <cstddef>
#include <cstdlib>

void* operator new[](size_t size) {
    // size уже включает sizeof(array_cookie) + n * sizeof(T)
    // компилятор сам вычислил этот размер
    void* ptr = malloc(size);
    if (!ptr) throw std::bad_alloc();
    return ptr;
}

void operator delete[](void* ptr) noexcept {
    free(ptr);
}

Кажется простым? Но дьявол в деталях. Рассмотрим случай, когда вы хотите переопределить new[] для конкретного класса:

class MyClass {
public:
    static void* operator new[](size_t size) {
        // size уже включает Cookie!
        return KernelHeap::allocate(size);
    }

    static void operator delete[](void* ptr) {
        // ptr — это адрес, который вернул new[], а не arr
        KernelHeap::free(ptr);
    }
};

Когда Cookie не нужны?

Согласно Itanium ABI, Cookie не требуются, если:
- Тип элемента имеет тривиальный деструктор (или деструктор отсутствует)
- Тип элемента не имеет пользовательского operator delete[]

В этом случае компилятор может опустить Cookie и вызвать operator new[] с размером n * sizeof(T) без дополнительных накладных расходов. Это важная оптимизация, которую стоит учитывать при разработке.

Например, для int или std::pair<int, int> Cookie не выделяются:

int* arr = new int[100];  // Cookie НЕ выделяется
delete[] arr;             // деструктор не вызывается

А для std::string или любого класса с нетривиальным деструктором — выделяются:

std::string* arr = new std::string[10];  // Cookie выделяется
delete[] arr;                            // деструктор вызовется 10 раз

Как это проверить на практике?

Можно написать небольшой тест, который покажет, выделяется ли Cookie:

#include <iostream>
#include <cstddef>

class WithDestructor {
public:
    ~WithDestructor() {}
};

class WithoutDestructor {
public:
    // тривиальный деструктор (не определён явно)
};

int main() {
    // Заменим глобальный new[] для отслеживания
    auto old_new = [](size_t size) -> void* {
        std::cout << "allocated " << size << " bytes\n";
        return malloc(size);
    };

    // Нельзя просто так заменить new[] в C++, 
    // но для понимания концепции — сойдёт

    std::cout << "int[10]: ";
    int* a = new int[10];
    delete[] a;

    std::cout << "WithoutDestructor[10]: ";
    WithoutDestructor* b = new WithoutDestructor[10];
    delete[] b;

    std::cout << "WithDestructor[10]: ";
    WithDestructor* c = new WithDestructor[10];
    delete[] c;
}

Вывод будет примерно таким (зависит от компилятора и платформы):

int[10]: allocated 40 bytes
WithoutDestructor[10]: allocated 40 bytes
WithDestructor[10]: allocated 48 bytes  // 40 + 8 (Cookie)

Практические советы для OSDEV

  1. Всегда учитывайте Cookie в аллокаторах — даже если вы не используете массивы с нетривиальными деструкторами, это защитит от будущих ошибок.

  2. Используйте alignof(std::max_align_t) для выравнивания Cookie. В Itanium ABI Cookie выровнен по alignof(size_t), но лучше перестраховаться.

  3. Проверяйте размер выделения — если ваш аллокатор не может выделить sizeof(array_cookie) + n * sizeof(T), программа упадёт.

  4. Не забывайте про delete[] — реализация должна корректно обрабатывать адрес с учётом Cookie.

  5. Тестируйте на реальном железе — эмуляторы могут скрывать некоторые баги.

Реальный случай из моей практики

Когда я писал загрузчик для собственной ОС на Raspberry Pi, я столкнулся с багом, который проявлялся только при использовании std::vector. После долгих отладок выяснилось, что мой аллокатор возвращал блоки с выравниванием 4 байта, а size_t на ARM требует 8-байтового выравнивания. Cookie писался по невыровненному адресу, что приводило к случайным падениям. Решение заняло 2 строки кода — добавить выравнивание в аллокатор.

Заключение

Array Cookies в Itanium ABI — это не просто деталь реализации, а важный механизм, обеспечивающий корректную работу с динамическими массивами в C++. Для разработчика ОС понимание этого механизма критично: ошибка в реализации аллокатора может привести к трудноуловимым багам, которые проявятся только в рантайме.

В следующих частях мы разберём другие аспекты Itanium ABI, включая виртуальные таблицы, RTTI и обработку исключений. А пока — проверьте свои аллокаторы на предмет корректной работы с Cookie. Уверен, вы найдёте что-то интересное.

Источник


Если вы разрабатываете собственную ОС и хотите глубже разобраться в низкоуровневых аспектах C++ ABI, рекомендую изучить оригинальную спецификацию Itanium C++ ABI. ASI Biont поддерживает интеграцию с различными инструментами разработки — подробнее на asibiont.com.

← Все статьи

Комментарии

Читайте также