Inna twórczość > C, C++
Ciekawostka c++
(1/1)
Wonski:
Jak kompilator alokuje pamięć pod obiekt wyjątku std::bad_alloc?
Jak kompilator radzi sobie w przypadku kiedy zabraknie pamięci pod alokację obiektu wyjątku?
Np. prostą do wyobrażenia jest sytuacja, gdzy program rzuca wyjątek bad_alloc.
Na pierwszy rzut oka, nic nas to nie obchodzi. Ale biorąc to pod lupę, możemy stwierdzić, że jest to sprzeczna sytuacja i niezdefiniowane zachowanie.
I w rzeczywistości tak jest. Jak zaalokować pamięć pod wyjątek, gdy program chce nam zakomunikować, że zasoby zostały wyczerpane?
Na logikę powinno to wyglądać tak, że rzucenie bad_alloc powoduje, że znowu obrywamy bad_allockiem i tak dalej... skoro nie ma miejsca na alokację bad_alloca to powinno znowu rzucić bad_alloca. Proste.
Ale mimo wszystko obserwujemy co innego. Gdy wyjątek zostaje rzucony, programista może go przechwycić w bloku catch. Nie obserwujemy żadnej magii, a kompilator alokuje pamięć pod wyjątek, mimo, że dostając bad_alloca, dostaliśmy również komunikat, że coś z pamięcią jest nie do końca halo.
To nie zawsze musi być problem braku pamięci. Pamięć może być z jakiegoś powodu niedostępna. Po prostu alokacja pamięci pod wyjątek na strecie może się nie udać z różnych przyczyn.
Tak więc uogólnijmy problem. Niech to nie będzie brak pamięci, ale po prostu błąd alokacji.
Przeszukałem sieć w poszukiwaniu ciekawych materiałów na ten temat i znalazłem pewne wytłumaczenie.
Gdy alokacja pamięci pod wyjątek nie jest możliwa to standard takie zachowanie opisuje jako niezdefiniowane i w tej sytuacji daje wolną rękę kompilatorom, co z tym fantem zrobić.
Jednak jest to poważny brak w standardzie języka, ponieważ jeżeli piszemy kod z użyciem wyjątków, który jest w 100% zgodny z standardem, to w ekstreamlnych przypadkach nie mamy gwarancji, że wykona się on w 100% bezpiecznie. W sumie ciężko oczekiwać, by program napisany w jakimkolwiek języku wykonał sie poprawnie w takich warunkach. Bariery w postaci fizycznych zasobów nie da się przeskoczyć. Bezpieczeństwo w takich sytuacjach powinno zostać zapewnione przez system operacyjny, albo jakikolwiek resource manager (i tak pewnie jest w reczywistości).
Wracając do tematu.
Kompilatory różnie sobie radzą z tym problemem.
Znalazłem informację, że GCC i Clang korzystają z ABI C++ Itanium (ABI - application binary interface)
Znalazłem informację, że Intel również rozwija to ABI, więc można sądzić, że ICC również korzysta z tego rozwiązania, ale to tylko moje domysły, nie chciało mi się grzebać w dokumantacji intelowskiej :D
Kompilator Microsoftu definiuje własne ABI, więc raczej nie jest kompatybilne na poziomie binarnym z kompilatorami ICC, GCC i Clang... choc tego nie wiem, może jest jakiś RCP czy inne protokoły serializacyjne, udostępniające choć podstawową kompatybilność binarną.. również nie chciało mi się grzebać w dokumentacji od microsoftu :D
Tak więc MSVC radzi sobie trochę inaczej w tej sytuacji, ale to opiszę później..
Dobra, ale przejdźmy do samego problemu rzucania wyjątku.
Opiszę to trochę szerzej.
Słowo kluczowe throw w c++ odpala całą maszynerię związaną z obsługą wyjątków. Podczas wykonywania instrucji throw następuje alokacja pamięci pod obiekt wyjątku. I tu jest kłopot. Jeżeli podczas tego procesu wystąpi błąd to wyjątek nie powędruje do klazuli catch, nawet nie zacznie się proces odwijania stosu. Nie mamy żadnej możliwości by na to w jakikolwiek sposób zareagować. Nie opakujemy instrukcji throw blokiem try, bezsens xd. Teraz widać jakie to niebezpieczne.
Myślę, że warto wspomnieć o tym, że model rzucania wyjątków w c++ jest zupełnie inny niż w innych językach.
Metody obługi wyjątków możemy podzielić na Resumptive i Non-Resumptive. W C++ mamy model non-resumptive. W modelu Resumptive, po tym jak wyjątek zostaje obsłużony, program kontynuuje swoją pracę tam gdzie wyjątek został rzucony, czyli następna instrukcja po throw. W c++ jak wiemy, tak nie jest... program kontunuuje swoją pracę na następnej instrukcji po pasującej klauzuli catch.
Dobra. Wróćmy do instrukcji throw. Jak została ona rozwiązana w Itanium.
Mamy kod:
--- Kod: ---throw Exception();
--- Koniec kodu ---
Podczas napotkania instrucji throw, jest wołana funkcja __cxa_allocate_exception by utowrzyć obiekt wyjątku. Funkcja ta przyjmuje sizeof wyjątku. Następnie jest wkonywane wyrażenie Exception() a jego wynik jest zapisywane w buforze zwracanym przez __cxa_allocate_exception.
Następnie jest odpalana __cxa_throw, która przyjmuje bufor zrrócony przez __cxa_allocate_exception i jest odpalana cała maszyneria związana z odwijaniem stosu...
Warto zaznaczyć, że Itanium nie definiuje kolejności dwóch pierwszych kroków tego procesu. Tzn. Najpierw może zostać wykonane wyrażenie Exception(), następnie zostać zaalokowany bufor, a wartość wyrażenia Exception() skopiowana do tego buforu. To jest zależne od twórców kompilatorów implementujących ABI Itanium.
Kluczowa w tym procesie jest funkcja __cxa_allocate_exception, która alokuje pamięć dla obiektu wyjątku.
Normalnie obiekty wyjątków są alokowane na stercie.
Jednak gdy normalna alokacja na stercie zawodzi (czyli __cxa_allocate_exception failuje), to kompilator implementujący Itanium może próbować zaalokować "emergency buffers", którego minimalny rozmiar to 4KB, a maksymalny to 64KB. Bufory te są używane wyłącznie gdy dynamiczna alokacja zawodzi i tylko pod tymi warunkami:
- obiekt wyjątku nie może przekraczać 1KB, ponieważ w takich kawałkach jest alokowany bufor ratunkowy
- obecny wątek, chcący otrzymać kolejny bufor, nie może trzymać więcej niż 4 bufory
Jeżeli oba warunki nie zostaną spełnione, wołana jest funkcja terminate(), czyli kończymy program bez żadnej użytecznej informacji o błędzie. To najczarniejszy scenariusz :(
Jeżeli jest więcej niż 16 wątków trzymających bufory, to następny wątek chcący otrzymać bufor jest blokowany do momentu, gdy jakiś wątek nie zwolni buforu bezpieczeństwa.
Bufory bezpieczeństwa są implementowane przez kompilator, i używane tylko w sytuacjach związanych z wyjątkami.
Tak z tą systuacją radzą sobie kompilatory implementujące ABI Itanium.
MSVC w takim przypadku alokuje wyjątek na stosie co też jest cholernie niebezpieczne, jeżeli obiekt wyjątku jest duży (może prowadzić do przepełnienia stosu, a działanie w przypadku przepełnienia stosu również nie jest zdefiniowane przez standard)
Moim zdaniem takie rozwiązanie to delikatnie rzecz biorąc: protetyka.
Jak już wspomniałem świadczy to o tym, że NIGDY nie mamy gwarancji, że pisany kod nawet w 100% poprawny wykona się poprawnie.
Dziękuję, mam nadzieję, że miło się czytało.
Jeżeli ktoś chce poczytać więcej o tym jak są implemenotwane wyjątki i w ogóle mnóstwo innych mechanizmów (jak chocby polmorfizm i vtables) w c++ to polecam zapoznać się z Itanium C++ ABI:
http://refspecs.linuxbase.org/cxxabi-1.83.html
Adanos:
Bardzo ciekawy i pouczający wątek. To pokazuje jak wiele zależy od kompilatora czy od standardu języka, którego definiuje (tutaj to o braku jakiegoś standardu w bardzo ekstremalnej i groźnej sytuacji). Zgadzam się, że obecne rozwiązania dotyczące alokacji pamięci dla wyjątku std::bad_alloc nie są dobre i brakuje solidnego rozwiązania.
Nawigacja
Idź do wersji pełnej