пятница, 24 июля 2009 г.

Исключительная ситуация

Нет, у меня все в порядке, дела идут и жизнь легка. Просто лето и отпуск, и я совершенно забросил свой блог. Но делать так ни в коем случае нельзя. У меня уже был подобный опыт по забрасыванию рассылки comp.soft.prog.asmos на Subscribe.ru, которая находится в дауне до сих пор. Чем больше времени проходит с момента последнего поста, тем проще ничего не писать.

А темой сегодняшнего поста я избрал исключения. Но я не стану расскзывать тривиальные вещи, про то, что исключения из деструктора не бросатся, это и так все знают. Мне вдруг стало интересно как это работает изнутри. Но пока здесь одни вопросы. Разбираться надо.

gcc очень наглый компилятор. Он может спокойно инлайнить функции, даже если используется -O0. Он может нормальные казалось бы вызовы стандартных функций заменять ассемблерными мнемониками. Но с исключениями все обстоит еще сложнее. Стандартный, казалось бы, С++ код превращается в нагромождения непонятных вызовов c которыми я и хотел бы разобраться, чтобы прикрутить исключения к своему ядру.

Правда, тут все зависит от цены, так сказать. Патч для ядра linux, позволяющий использовать C++ с исключениями занимает 600к, в то время как вся моя кроссплатформенная часть (для которой собственно рунтайм и тащится) занимает 200к. Неравноценный получается обмен.

В самом gcc все реализовано максимально переносимо, из за чего, вероятно, трудно все это понять. Большой уровень косвенности. Мне же переносимость в данном случае не нужна, нужна минимальная реализация для конкретной платформы.

Так как же работают исключения? gcc, не смортя на всю свою наглость, конечно молодец. Для корректной отработки деструкторов выделенных в стеке объектов в нем используются так называемые "landing pad", область приземления? или как это правильно перевести. Область приземления располагается в конце каждого программного блока, в котором требуется деинициализация. Причем gcc очеь грамотно задвигает все, что не требуется деинициализировать.

Координаты этих областей перечисленны в "exception handling tables". Эти таблицы формируются компилятором для каждой функции. Хотя может и не для каждой, точно сказать пока не могу.

При возникновении исключения управление передается в функцию __cxa_throw, и дальше начинается сплошной грязный хак. Управление обратно уже не возвращается. Происходит разворот стека, для каждой функции производится поиск таблиц исключений, и в зависимости от адреса, где вызывалась функция __cxa_throw, выбирается ближайший landing pad и управление передается на него.

Кстати, именно на функцию __cxa_throw надо ставить брекпоинт для программы, собранной с помощью gcc, если есть желание отловить момент возникновения исключения в отладчике.

Чтобы механизм разворота стека заработал дальше, в случае отсутствия обработчиков исключений, в конце функции ставится вызов _Unwind_Resume, в котором может быть инициирована дальнейшая раскрутка стека, если исключение имеет место быть. Но ставится он не в каждой функции. Иногда даже перед вызовами деструкторов. По какому принципу он расставляется - я пока не осознал.

Стандарт оговаривает и структуры, которые вращаются внутри __cxa_throw и других функций, связанных с исключениями, но для минимальной реализации на это можно забить. Главное - это понять логику разворота стека и связывания функций с соответствующими таблицами прерываний.

Так же для меня является загадкой, почему разработчики gcc так перемешали интерфейсы __cxa* и _Unwind*. Но в недрах реализации они переплетены столь плотно, что разорвать их практически нереально.

Если вдруг кто-то уже разбирался с обработкой исключений в gcc - отзовитесь, у меня много вопросов. :) Хотя постепенно осознание придет, если долго разбираться.