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
-
Всегда учитывайте Cookie в аллокаторах — даже если вы не используете массивы с нетривиальными деструкторами, это защитит от будущих ошибок.
-
Используйте
alignof(std::max_align_t)для выравнивания Cookie. В Itanium ABI Cookie выровнен поalignof(size_t), но лучше перестраховаться. -
Проверяйте размер выделения — если ваш аллокатор не может выделить
sizeof(array_cookie) + n * sizeof(T), программа упадёт. -
Не забывайте про
delete[]— реализация должна корректно обрабатывать адрес с учётом Cookie. -
Тестируйте на реальном железе — эмуляторы могут скрывать некоторые баги.
Реальный случай из моей практики
Когда я писал загрузчик для собственной ОС на 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.
Комментарии