
Приветствую!
Код тут
В этом цикле статьей я рассмотрю весь путь реализации минимального Itanium ABI для своей ОС. Исключения не поддерживаются. Вопросы разработки юнванйдера в этих статьях рассмотрены не будут. Этапы разработки osdev-libstdc подробно рассмотрены в следующих статьях:
malloc/free/realloc/alloc_aligned
В статье про аллокаторы есть ссылка на цикл статей с пояснениям алгоритма и полной его реализацией.
Все функции о которых пойдет речь должны быть объявлены в пространстве имен __cxxabiv1 и должны быть extern "C".
Array Operator new Cookies
Сразу скажу что в итоге оказалось что современные компиляторы не используют эти АПИ, по крайней мере свежий GCC точно (на момент написания статьи последний стабильный релиз GCC 16.1) но тем не менее раз уж Itanium требует наличия этих функций в ABI я реализовал их. An ABI-compliant system shall provide several runtime routines for use in array construction and destruction что означает, что совместимый с Itanium рантайм обязан предоставить реализацию этих функций. Не смотря на это требование компилятор может вызывать или не вызывать их по своему усмотрению. Современные компиляторы способны генерировать эквивалентный и скорее всего более быстрый код самостоятельно.
Поговорим о функциях конструирования массивов. Их реализация довольно простая, особенно если понимать что происходит. Рассмотрим каждую по отдельности на примере их реализации в библиотеке osdev-libcppabi.
Эти функции можно условно разделить на три группы:
создание массива __cxa_vec_new*
работа с элементами массива __cxa_vec_ctor, __cxa_vec_cctor, __cxa_vec_dtor
уничтожение массива __cxa_vec_delete*
__cxa_vec_ctor
Не распределяет память. Просто вызывает конструкторы для объектов в массиве в порядке их следования от 0 до __element_count. Конструктор и размер массива само собой передаются компилятором. Деструктор передается на тот случай если один из конструкторов бросил исключение. В этом случае уже сконструированные объекты должны быть удалены. osdev-libcppabi не поддерживает исключения, поэтому параметр __destructor просто игнорируется
__cxa_vec_ctor_return_type __cxa_vec_ctor( void *__array_address, size_t __element_count, size_t __element_size, __cxa_cdtor_type __constructor, __cxa_cdtor_type __destructor [[maybe_unused]]) { if (__constructor) { auto __c = static_cast<char *> (__array_address); // конструкторы вызываем сначала массива к его концу // самый обычный цикл for (size_t __i = 0; __i < __element_count; ++__i) { __constructor(__c); __c += __element_size; } } }
__cxa_vec_cctor
Эта функция может быть вызвана когда нужно проинициализировать массив конструктором копирования. В остальном полностью аналогична функции выше.
__cxa_vec_ctor_return_type __cxa_vec_cctor( void *__dest_array, void *__src_array, size_t __element_count, size_t __element_size, __cxa_cdtor_return_type (*__copy_constructor)(void *, void *), __cxa_cdtor_type __destructor [[maybe_unused]]) { if (__copy_constructor && __src_array && __dest_array) { auto __s = static_cast<char *> (__src_array); auto __d = static_cast<char *> (__dest_array); for (size_t __i = 0; __i < __element_count; ++__i) { __copy_constructor(__d, __s); __s += __element_size; __d += __element_size; } } }
__cxa_vec_dtor
Эта функция может быть вызвана в случае когда нужно уничтожить объекты находящиеся в массиве путем вызова деструктора. Обратите внимание, что деструкторы вызываются в порядке обратном вызову конструкторов, т.е. от __element_count до 1, т.е. от конца к началу массива.
void __cxa_vec_dtor( void *__array_address, size_t __element_count, size_t __element_size, __cxa_cdtor_type __destructor) { if (__destructor) { auto __c = static_cast<char *> (__array_address) + __element_count * __element_size; // Вызываем дострукторы в обратном порядке for (size_t __i = __element_count; __i > 0; --__i) { __c -= __element_size; __destructor(__c); } } }
__cxa_vec_new
.Itanium:
Equivalent to __cxa_vec_new2(element_count, element_size, padding_size, constructor, destructor, &::operator new[], &::operator delete[])
Я скопипастил :-)
void *__cxa_vec_new( size_t __element_count, size_t __element_size, size_t __padding_size, __cxa_cdtor_type __constructor, __cxa_cdtor_type __destructor) { // No exceptions anyway return __cxa_vec_new2(__element_count, __element_size, __padding_size, __constructor, __destructor, &::operator new[], &::operator delete[]); }
__cxa_vec_new2
Выделяет память под массив и вызывает конструкторы для его объектов. Если __padding_size больше нуля значит нужно положить размер массива перед нулевым элементом. Это и есть cookie. Именно благодаря этому delete [] знает количество элементов массива. Объекты конструируются при помощи рассмотренной выше __cxa_vec_ctor
void *__cxa_vec_new2( size_t __element_count, size_t __element_size, size_t __padding_size, __cxa_cdtor_type __constructor, __cxa_cdtor_type __destructor, void *(*__alloc)(size_t), void (*__dealloc)(void *)) { char *__base = nullptr; if (__alloc) { size_t __sz = __element_count * __element_size + __padding_size; __base = static_cast<char *>(__alloc(__sz)); if (__padding_size > 0) { __base += __padding_size; reinterpret_cast<size_t *>(__base)[-1] = __element_count; } __cxa_vec_ctor(__base, __element_count, __element_size, __constructor, __destructor); } return __base; }
__cxa_vec_new3
Itanium:
Same as __cxa_vec_new2 except that the deallocation function takes both the object address and its size.
__cxa_vec_new2 поддержкой sized deallocation через параметр __dealloc вида void(*)(void*, size_t). У меня нет исключений и я просто использую __cxa_vec_new2 забив на __dealloc. С поддержкой исключений эта функция в случае если один из конструкторов бросает исключение должна была бы вызвать деструкторы для уже созданных объектов и очистить память через __dealloc.
void *__cxa_vec_new3( size_t __element_count, size_t __element_size, size_t __padding_size, __cxa_cdtor_type __constructor, __cxa_cdtor_type __destructor, void *(*__alloc)(size_t), void (*__dealloc)(void *, size_t)) { // No exceptions anyway return __cxa_vec_new2(__element_count, __element_size, __padding_size, __constructor, __destructor, __alloc, nullptr); }
__cxa_vec_delete
Эта функция может быть вызвана в случае когда нужно освободить память и уничтожить объекты в массиве.
Itanium говорит следующее:
If the array_address is NULL, return immediately. Otherwise, given the (data) address of an array, the non-negative size of prefix padding for the cookie, and the size of its elements, call the given destructor on each element, using the cookie to determine the number of elements, and then delete the space by calling ::operator delete[](void *)
Т.е. если массив != nullptr извлечь из cookie размер массива и таким образом определить количество элементов в массиве, затем вызвать деструкторы и освободить память при помощи ::operator delete[](void*). У меня реализована через __cxa_vec_delete2 с явной передачей деаллокатора &::operator delete[]
void __cxa_vec_delete( void *__array_address, size_t __element_size, size_t __padding_size, __cxa_cdtor_type __destructor) { __cxa_vec_delete2(__array_address, __element_size, __padding_size, __destructor, &::operator delete[]); }
__cxa_vec_delete2
Itanium пишет о ней только это:
Same as __cxa_vec_delete, except that the given function is used for deallocation instead of the default delete function. If dealloc throws an exception, the result is undefined. The dealloc pointer may not be NULL.
Эта функция может быть вызвана в случае когда удаление должно осуществляться аллокатором вида void (*dealloc)(void*). Видимо предназначена для внутреннего использования реализацией ABI например так же как сделано выше :-).
void __cxa_vec_delete2( void *__array_address, size_t __element_size, size_t __padding_size, __cxa_cdtor_type __destructor, void (*__dealloc)(void *)) { if (__array_address && __dealloc) { char *__base = static_cast<char *>(__array_address); if (__padding_size > 0) { size_t __element_count = reinterpret_cast<size_t *>(__base)[-1]; __base -= __padding_size; __cxa_vec_dtor(__array_address, __element_count, __element_size, __destructor); } __dealloc(__base); } }
Как можно видеть уничтожение объектов массива осуществляется при помощи рассмотренной выше __cxa_vec_dtor.
__cxa_vec_delete3
Same as __cxa_vec_delete, except that the given function is used for deallocation instead of the default delete function. The deallocation function takes both the object address and its size .
В общем то же самое что и __cxa_vec_delete только освобождение осуществляется через переданный dealloc(void*, size_t) с целью поддержки так называемого sized delete. О нем я кратко расскажу ниже.
void __cxa_vec_delete3( void *__array_address, size_t __element_size, size_t __padding_size, __cxa_cdtor_type __destructor, void (*__dealloc)(void *, size_t)) { if (__array_address && __dealloc) { char *__base = static_cast<char *>(__array_address); size_t __element_count = 0; if (__padding_size > 0) { __element_count = reinterpret_cast<size_t *>(__base)[-1]; __base -= __padding_size; __cxa_vec_dtor(__array_address, __element_count, __element_size, __destructor); } size_t __sz = __element_count * __element_size + __padding_size; __dealloc(__base, __sz); } }
SIzed allocation
Начиная с С++14 стандарт разрешает компилятору вызовы в духе
void operator delete(void ptr, size_t size);
void operator delete[](void ptr, size_t size);
что позволяет реализации этих операторов не искать размер блока где то у себя в метаданных, а получить его от компилятора. Для массивов размер блока памяти берется из cookie. Поэтому Itanium ABI содержит отдельные функции __cxa_vec_new3 и __cxa_vec_delete3, предназначенные для работы с deallocator'ами, принимающими аргумент size_t.
Моя реализация перегрузок operator new/operator delete игнорирует этот параметр потому что размер блока лежит в заголовке.
Реализация operator new/operator delete
void *operator new(size_t size) { return __OSDEV_STD_SYMBOL(malloc)(size); } void *operator new[](size_t size) { return __OSDEV_STD_SYMBOL(malloc)(size); } void operator delete(void *ptr) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void operator delete[](void *ptr) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void operator delete(void *ptr, size_t size [[maybe_unused]]) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void operator delete[](void *ptr, size_t size [[maybe_unused]]) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void *operator new(size_t size, __STD_NAMESPACE::align_val_t alignment) { return __OSDEV_STD_SYMBOL(aligned_alloc)(static_cast<size_t>(alignment), size); } void *operator new[](size_t size, __STD_NAMESPACE::align_val_t alignment) { return __OSDEV_STD_SYMBOL(aligned_alloc)(static_cast<size_t>(alignment), size); } void operator delete(void *ptr, __STD_NAMESPACE::align_val_t alignment [[maybe_unused]]) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void operator delete[](void *ptr, __STD_NAMESPACE::align_val_t alignment [[maybe_unused]]) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void operator delete(void *ptr, size_t size, __STD_NAMESPACE::align_val_t alignment [[maybe_unused]]) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); } void operator delete[](void *ptr, size_t size, __STD_NAMESPACE::align_val_t alignment [[maybe_unused]]) noexcept { __OSDEV_STD_SYMBOL(free)(ptr); }
Макрос __OSDEV_STD_SYMBOL предназначен для борьбы с GCC что бы можно было гонять тесты на linux.
До новых встреч!
