что такое раскрутка стека

Что же там такого тяжелого в обработке исключений C++?

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека
Исключения и связанная с ними раскрутка стека – одна из самых приятных методик в C++. Обработка исключений интуитивно понятно согласуется с блочной структурой программы. Внешне, обработка исключений представляется очень логичной и естественной.

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

Тем не менее, в C++, исключения традиционно рассматриваются буквально как исключительные ситуации, связанные с восстановлением после ошибок. Трудно сказать, является ли это причиной или следствием того, что реализация обработки исключений компиляторами чрезвычайно дорога. Попробуем разобраться почему.

Продолжение здесь.
Как обстоят дела.

Существует предубеждение, что данный метод является весьма дорогостоящим. Уже в силу того, что на каждый try-блок вызывается setjmp, который недёшев. В самом деле, нужно полностью сохранить состояние процессора, где могут быть десятки регистров. Тогда как на момент возникновения исключения, содержимое большей части этих регистров уже бесполезно. В действительности же, компилятор поступает весьма рационально. Он разворачивает setjmp, причем, сохраняет только полезные регистры (уж эта информация у него есть). Автор сомневается, что издержки на setjmp так уж высоки.

Концептуально, на каждый адрес кода программы хранится информация о том, как попасть в вышестоящий фрейм вызова. На практике ввиду объемности этой информации, она сжимается, фактически, вычисляется с помощью интерпретации байт-кода. Этот байт-код исполняется при возникновении исключения. Расположено всё это в секциях «.eh_frame» и «.eh_frame_hdr».
Да, помимо всего прочего, DWARF интерпретатор представляет собой отличный backdoor, с помощью которого, подменив байт-код, можно перехватить исключение и отправить его на обработку куда душе угодно.
GCC/DW2 использует практически такую же секцию LSDA, что и GCC/SJLJ.

PS: Отдельное спасибо Александру Артюшину за содержательное обсуждение.

Источник

11.3.2. Раскрутка стека

11.3.2. Раскрутка стека

Поиск catch-обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw находится в try-блоке, все ассоциированные с ним предложения catch исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее предложение catch найдено, то исключение обрабатывается. В противном случае поиск продолжается в вызывающей функции. Предположим, что вызов функции, выполнение которой прекратилось в результате исключения, погружен в try-блок; в такой ситуации исследуются все предложения catch, ассоциированные с этим блоком. Если один из них может обработать исключение, то процесс заканчивается. В противном случае переходим к следующей по порядку вызывающей функции. Этот поиск последовательно проводится во всей цепочке вложенных вызовов. Как только будет найдено подходящее предложение, управление передается в соответствующий обработчик.

В нашем примере первая функция, для которой нужен catch-обработчик, – это функция-член pop() класса iStack. Поскольку выражение throw внутри pop() не находится в try-блоке, то программа покидает pop(), не обработав исключение. Следующей рассматривается функция, вызвавшая pop(), то есть main(). Вызов pop() внутри main() находится в try-блоке, и далее исследуется, может ли хотя бы одно ассоциированное с ним предложение catch обработать исключение. Поскольку обработчик исключения popOnEmpty имеется, то управление попадает в него.

Процесс, в результате которого программа последовательно покидает составные инструкции и определения функций в поисках предложения catch, способного обработать возникшее исключение, называется раскруткой стека. По мере раскрутки прекращают существование локальные объекты, объявленные в составных инструкциях и определениях функций, из которых произошел выход. C++ гарантирует, что во время описанного процесса вызываются деструкторы локальных объектов классов, хотя они исчезают из-за возбужденного исключения. (Подробнее мы поговорим об этом в главе 19.)

Если в программе нет предложения catch, способного обработать исключение, оно остается необработанным. Но исключение – это настолько серьезная ошибка, что программа не может продолжать выполнение. Поэтому, если обработчик не найден, вызывается функция terminate() из стандартной библиотеки C++. По умолчанию terminate() активизирует функцию abort(), которая аномально завершает программу. (В большинстве ситуаций вызов abort() оказывается вполне приемлемым решением. Однако иногда необходимо переопределить действия, выполняемые функцией terminate(). Как это сделать, рассказывается в книге [STROUSTRUP97].)

Вы уже, наверное, заметили, что обработка исключений и вызов функции во многом похожи. Выражение throw ведет себя аналогично вызову, а предложение catch чем-то напоминает определение функции. Основная разница между этими двумя механизмами заключается в том, что информация, необходимая для вызова функции, доступна во время компиляции, а для обработки исключений – нет. Обработка исключений в C++ требует языковой поддержки во время выполнения. Например, для обычного вызова функции компилятору в точке активизации уже известно, какая из перегруженных функций будет вызвана. При обработке же исключения компилятор не знает, в какой функции находится catch-обработчик и откуда возобновится выполнение программы. Функция terminate() предоставляет механизм времени выполнения, который извещает пользователя о том, что подходящего обработчика не нашлось.

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

Просмотр стека вызовов

Просмотр стека вызовов В отладчике можно вывести окно Call Stack со списком всех активных процедур и функций сценария. Для этого нужно выполнить команду View|Call Stack. Например, если вызвать это окно, находясь внутри функции MyFunc() в сценарии ForDebug.js, то в списке мы увидим название

Раскрутка

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

Раскрутка за деньги

Раскрутка за деньги Если у вас нет времени, но есть деньги, то раскрутку можно поручить пользователям, занимающимся этим профессионально. Существует множество сайтов, предлагающих услуги по увеличению количества посетителей интернет-ресурсов. Найти их несложно:

1.7.5. Многоуровневая архитектура стека TCP/IP

1.7.5. Многоуровневая архитектура стека TCP/IP Этот пункт книги является необязательным: если вы считаете, что у вас уже достаточно знаний о протоколе TCP/IP, то можете перейти к следующим разделам, а к этому вернуться позже. Здесь будет описана многоуровневая архитектура

27.1.1. Многоуровневая архитектура стека TCP/IP

27.1.1. Многоуровневая архитектура стека TCP/IP Протокол TCP/IP был создан в конце 60-х — начале 70-х годов агентством DARPA Министерства Обороны США (U.S. Department of Defense Advanced Research Projects Agency). Основные этапы развития этого протокола отмечены в таблице 27.1.Этапы развития протокола TCP/IP Таблица

Глава 4 РАСКРУТКА БЛОГА

Глава 4 РАСКРУТКА БЛОГА Как и зачем раскручивать блог Цель раскрутки или продвижения блога очень простая — если ваш блог не раскручен, то вы ничего не заработаете.Представьте популярного эстрадного исполнителя: что было бы, если бы он пел не на сцене, а дома в душе? Стал

Бесплатная раскрутка сайта

Бесплатная раскрутка сайта Раскрутка сайта дело довольно муторное, отнимающее много времени. Но результат, как правило, положительный. Но, прежде чем вообще что-то раскручивать, необходимо привести сайт в порядок, провести правильную его внутреннюю оптимизацию.

12. Запуск и раскрутка

12. Запуск и раскрутка Этап запуска и раскрутки инфопродукта мы будем обсуждать в других интеллект-картах.Таким образом мы реализуем каждую нашу идею. Самые главные – первые пункты, когда вы определяете дату и сколько кусочков должны записывать в день, чтобы дойти до даты

5. Базовая раскрутка партнерки

5. Базовая раскрутка партнерки Что делать для привлечения партнеров?? Рассказывайте о партнерской программе везде, на всех ваших сайтах – продающих и не продающих.? У профессионалов вы наверняка видели в конце каждого продающего текста или мини-сайта упоминание о

2. Раскрутка и продвижение (60 % времени)

2. Раскрутка и продвижение (60 % времени) В инфобизнесе на продвижение тратится 60 % времени. Это настройка источников трафика, таких как Яндекс. Директ, или контроль этого процесса, если он у вас на аутсорсинге. Партнерская программа, продающие тексты, их улучшение,

19.2.5. Раскрутка стека и вызов деструкторов

19.2.5. Раскрутка стека и вызов деструкторов Когда возбуждается исключение, поиск его catch-обработчика – раскрутка стека – начинается с функции, возбудившей исключение, и продолжается вверх по цепочке вложенных вызовов (см. раздел 11.3).Во время раскрутки поочередно

Источник

19.2.5. Раскрутка стека и вызов деструкторов

19.2.5. Раскрутка стека и вызов деструкторов

Когда возбуждается исключение, поиск его catch-обработчика – раскрутка стека – начинается с функции, возбудившей исключение, и продолжается вверх по цепочке вложенных вызовов (см. раздел 11.3).

Во время раскрутки поочередно происходят аномальные выходы из просмотренных функций. Если функция захватила некоторый ресурс (например, открыла файл или выделила из хипа память), он в таком случае не освобождается.

Существует прием, позволяющий решить эту проблему. Всякий раз, когда во время поиска обработчика происходит выход из составной инструкции или блока, где определен некоторый локальный объект, для этого объекта автоматически вызывается деструктор. (Локальные объекты рассматривались в разделе 8.1.)

Например, следующий класс инкапсулирует выделение памяти для массива целых в конструкторе и ее освобождение в деструкторе:

Локальный объект такого типа создается в функции manip() перед вызовом mathFunc():

void manip( int parm ) <

mathFunc( parm ); // возбуждает исключение divideByZero

Если mathFunc() возбуждает исключение типа divideByZero, то начинается раскрутка стека. В процессе поиска подходящего catch-обработчика проверяется и функция manip(). Поскольку вызов mathFunc() не заключен в try-блок, то manip() нужного обработчика не содержит. Поэтому стек раскручивается дальше по цепочке вызовов. Но перед выходом из manip() с необработанным исключением процесс раскрутки уничтожает все объекты типа классов, которые локальны в ней и были созданы до вызова mathFunc(). Таким образом, локальный объект localPtr уничтожается до того, как поиск пойдет дальше, а следовательно, память, на которую он указывает, будет освобождена и утечки не произойдет.

Поэтому говорят, что процесс обработки исключений в C++ поддерживает технику программирования, основной принцип которой можно сформулировать так: “захват ресурса – это инициализация; освобождение ресурса – это уничтожение”. Если ресурс реализован в виде класса и, значит, действия по его захвату сосредоточены в конструкторе, а действия по освобождению – в деструкторе (как, например, в классе PTR выше), то локальный для функции объект такого класса автоматически уничтожается при выходе из функции в результате необработанного исключения. Действия, которые должны быть выполнены для освобождения ресурса, не будут пропущены при раскрутке стека, если они инкапсулированы в деструкторы, вызываемые для локальных объектов.

Класс auto_ptr, определенный в стандартной библиотеке (см. раздел 8.4), ведет себя почти так же, как наш класс PTR. Это средство для инкапсуляции выделения памяти в конструкторе и ее освобождения в деструкторе. Если для выделения одиночного объекта из хипа используется auto_ptr, то гарантируется, что при выходе из составной инструкции или функции из-за необработанного исключения память будет освобождена.

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

Просмотр стека вызовов

Просмотр стека вызовов В отладчике можно вывести окно Call Stack со списком всех активных процедур и функций сценария. Для этого нужно выполнить команду View|Call Stack. Например, если вызвать это окно, находясь внутри функции MyFunc() в сценарии ForDebug.js, то в списке мы увидим название

Раскрутка

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

Раскрутка за деньги

Раскрутка за деньги Если у вас нет времени, но есть деньги, то раскрутку можно поручить пользователям, занимающимся этим профессионально. Существует множество сайтов, предлагающих услуги по увеличению количества посетителей интернет-ресурсов. Найти их несложно:

1.7.5. Многоуровневая архитектура стека TCP/IP

1.7.5. Многоуровневая архитектура стека TCP/IP Этот пункт книги является необязательным: если вы считаете, что у вас уже достаточно знаний о протоколе TCP/IP, то можете перейти к следующим разделам, а к этому вернуться позже. Здесь будет описана многоуровневая архитектура

27.1.1. Многоуровневая архитектура стека TCP/IP

27.1.1. Многоуровневая архитектура стека TCP/IP Протокол TCP/IP был создан в конце 60-х — начале 70-х годов агентством DARPA Министерства Обороны США (U.S. Department of Defense Advanced Research Projects Agency). Основные этапы развития этого протокола отмечены в таблице 27.1.Этапы развития протокола TCP/IP Таблица

Глава 4 РАСКРУТКА БЛОГА

Глава 4 РАСКРУТКА БЛОГА Как и зачем раскручивать блог Цель раскрутки или продвижения блога очень простая — если ваш блог не раскручен, то вы ничего не заработаете.Представьте популярного эстрадного исполнителя: что было бы, если бы он пел не на сцене, а дома в душе? Стал

Бесплатная раскрутка сайта

Бесплатная раскрутка сайта Раскрутка сайта дело довольно муторное, отнимающее много времени. Но результат, как правило, положительный. Но, прежде чем вообще что-то раскручивать, необходимо привести сайт в порядок, провести правильную его внутреннюю оптимизацию.

12. Запуск и раскрутка

12. Запуск и раскрутка Этап запуска и раскрутки инфопродукта мы будем обсуждать в других интеллект-картах.Таким образом мы реализуем каждую нашу идею. Самые главные – первые пункты, когда вы определяете дату и сколько кусочков должны записывать в день, чтобы дойти до даты

5. Базовая раскрутка партнерки

5. Базовая раскрутка партнерки Что делать для привлечения партнеров?? Рассказывайте о партнерской программе везде, на всех ваших сайтах – продающих и не продающих.? У профессионалов вы наверняка видели в конце каждого продающего текста или мини-сайта упоминание о

2. Раскрутка и продвижение (60 % времени)

2. Раскрутка и продвижение (60 % времени) В инфобизнесе на продвижение тратится 60 % времени. Это настройка источников трафика, таких как Яндекс. Директ, или контроль этого процесса, если он у вас на аутсорсинге. Партнерская программа, продающие тексты, их улучшение,

8.3. Использование конструкторов и деструкторов для управления ресурсами (RAII)

8.3. Использование конструкторов и деструкторов для управления ресурсами (RAII) ПроблемаДля класса, представляющего некоторый ресурс, требуется использовать конструктор для получения этого ресурса и деструктор для его освобождения. Эта методика часто называется

11.3.2. Раскрутка стека

11.3.2. Раскрутка стека Поиск catch-обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw находится в try-блоке, все ассоциированные с ним предложения catch исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее

18.5.3. Порядок вызова конструкторов и деструкторов

18.5.3. Порядок вызова конструкторов и деструкторов Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear (плюшевый мишка) есть два виртуальных

Источник

Исключения в Windows x64. Как это работает. Часть 4

Опираясь на материал, описанный в первой, второй и третьей частях данной статьи, мы продолжим обсуждение темы обработки исключений в Windows x64.

Описываемый материал требует знания базовых понятий, таких, как пролог, эпилог, кадр функции и понимания базовых процессов, таких, как действия пролога и эпилога, передача параметров функции и возврат результата функции. Если читатель не знаком с вышеперечисленным, то перед прочтением рекомендуется ознакомиться с материалом из первой части данной статьи. Если читатель не знаком со структурами PE образа, которые задействуются в процессе обработки исключения, тогда перед прочтением рекомендуется ознакомиться с материалом из второй части данной статьи. Также, если читатель не знаком с процессом поиска и вызова обработчиков исключений, рекомендуется ознакомиться с третьей частью данной статьи.

Приводимое описание относится к реализации в Windows, и, следовательно, не следует полагать, что прилагаемая к статье реализация данного механизма будет в точности совпадать с ней, хотя концептуально различий нет. Детали прилагаемой реализации в статье рассматриваться не будут, если об этом не будет сказано явно. Поэтому предполагается, что эти детали, по необходимости, следует изучить самостоятельно.

К статье прилагается реализация механизма, которая находится в папке exceptions хранилища git по этому адресу.

1. Раскрутка стека

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

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

На примере выше стек состоит из кадров четырех функций, где функция Main вызвала Func1, Func1 вызвала Func2, а Func2 вызвала Func3. Поэтому, например, если функции Func3 требуется вернуть управление функции Main, тогда она воспользуется функцией RtlUnwind/RtlUnwindEx, которая экспортируется модулем ntdll.dll в пользовательском пространстве и модулем ntoskrnl.exe в пространстве ядра. Прототип функции RtlUnwindEx изображен ниже, на рисунке 2.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Параметр TargetFrame принимает адрес кадра той функции, до которой следует раскрутить стек. Параметр TargetIp принимает адрес инструкции, с которой продолжится выполнение после раскрутки. Параметр ExceptionRecord принимает указатель на EXCEPTION_RECORD структуру, которая будет передаваться обработчикам при раскрутке. Параметр ReturnValue записывается в RAX регистр процессора, т.е. сразу после передачи управления соответствующей функции регистр RAX будет содержать значение этого параметра. Параметр ContextRecord содержит указатель на CONTEXT структуру, которая используется функцией RtlUnwindEx при раскрутке функций и определении целевого состояния процессора после раскрутки. Параметр HistoryTable принимает указатель на структуру, которая используется для кэширования поиска. Формат этой структуры вы сможете найти в winnt.h.

Параметр TargetFrame является необязательным. Если его значение равно NULL, тогда функция RtlUnwindEx выполняет так называемую раскрутку при выходе (exit unwind), где раскручиваются кадры всех функций стека. В этом случае параметр TargetIp игнорируется. Параметр ExceptionRecord является необязательным, и если он равен NULL, тогда функция RtlUnwindEx инициализирует свою структуру EXCEPTION_RECORD, где поле ExceptionCode будет содержать STATUS_UNWIND значение, поле ExceptionRecord будет содержать NULL, поле ExceptionAddress будет содержать указатель на инструкцию функции RtlUnwindEx, а поле NumberParameters будет содержать 0. Параметр HistoryTable является необязательным.

Прототип функции RtlUnwind отличается лишь тем, что он не принимает два последних параметра.

Ниже, на рисунке 3, изображен пример работы функции RtlUnwind.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

На рисунке выше изображен пример программы, состоящей из четырех функций: _tmain, Func1, Func2, Func3. Функция _tmain вызывает функцию Func1, функция Func1 вызывает Func2, а функция Func2 вызывает Func3. Функции Func1, Func2, Func3 возвращают булево значение. Функция Func3 выполняет виртуальную раскрутку трех предыдущих функций с целью: найти адрес кадра функции _tmain; найти адрес инструкции, с которой будет продолжено выполнение, и в данном примере адрес будет указывать на инструкцию сразу после инструкции вызова функции Func1. Справа от исходного кода изображен ассемблерный код _tmain и Func3 функций, адреса инструкций которых являются абсолютными. Справа от ассемблерного кода изображены состояния процессора и стеки вызовов для трех случаев: сверху изображено состояние процессора и стек вызовов сразу перед вызовом функции Func1; посередине изображено состояние процессора и стек вызовов сразу перед вызовом функции RtlUnwind; внизу изображено состояние процессора после выполнения функции RtlUnwind. Указатели инструкций этих состояний сопоставляются с ассемблерными инструкциями посредством уникальных номеров. Следует обратить внимание на последний случай, где RAX регистр принял значение параметра ReturnValue, а стек вызов сократился до одной функции, т.е. кадры функций Func1, Func2 и Func3 более не существуют в стеке. Поскольку значение RAX после раскрутки не нулевое, функция _tmain выведет сообщение на экран. В обычном случае, т.е. если бы раскрутка не выполнялась, это сообщение не будет выведено, т.к. функция Func3 возвращает false. Также следует обратить внимание на то, что цикл поиска указателя кадра функции _tmain выполняет четыре итерации, когда раскручиваемых функций всего три. Это связано с ранее обсуждаемыми особенностями функции RtlVirtualUnwind. Дело в том, что после вызова функции RtlVirtualUnwind параметры HandlerData и EstablisherFrame примут соответствующие значения для той функции, для которой выполнялась виртуальная раскрутка, когда параметр ContextRecord будет отражать состояние процессора сразу после вызова раскрученной функции. Следовательно, на третьей итерации цикла функция RtlVirtualUnwind вернет в параметр EstablisherFrame указатель кадра для функции Func1, когда параметр ContextRecord будет отражать состояние процессора сразу после вызова функции Func1. Поэтому требуется выполнить дополнительную итерацию, чтобы определить указатель кадра функции _tmain.

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

Ниже на рисунке 4 изображена блок-схема функции RtlUnwindEx.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

В начале своей работы функция получает нижний и верхний лимиты стека. Далее функция захватывает текущее состояние процессора посредством вызова функции RtlCaptureContext. Таким образом, структура CONTEXT будет отражать состояние процессора сразу после вызова функции RtlCaptureContext. Эта же структура используется в качестве первоначального состояния процессора, с которого начинается виртуальная раскрутка функций. Функция RtlUnwindEx в процессе своей работы использует две структуры CONTEXT: одна отражает состояние процессора в момент выполнения функции, для которой выполняется вызов обработчика (здесь и далее — текущий контекст); другая отражает состояние процессора сразу после возврата из этой функции (здесь и далее — предыдущий контекст). Это необходимо из-за ранее обсуждаемых особенностей функции RtlVirtualUnwind. Также функция RtlUnwindEx, как это уже ранее обозначалось, инициализирует структуру EXCEPTION_RECORD для последующей передачи обработчикам раскрутки, если соответствующий параметр не был передан при вызове функции.

Далее функция формирует первоначальное значение поля ExceptionFlags для структуры EXCEPTION_RECORD. Это значение хранится в локальной переменной и изначально не хранится в поле самой структуры. Функция устанавливает флаг EXCEPTION_UNWINDING, и если адрес кадра целевой функции не был передан функции, тогда функция также устанавливает флаг EXCEPTION_EXIT_UNWIND. Таким образом, флаг EXCEPTION_UNWINDING для обработчиков означает, что выполняется раскрутка, а флаг EXCEPTION_EXIT_UNWIND означает, что раскручиваются кадры всех функций.

Далее функция посредством функции RtlLookupFunctionEntry получает адрес PE образа и указатель на RUNTIME_FUNCTION структуру функции этого образа, обработчик которой необходимо вызвать (здесь и далее — текущая функция). Адрес одной из инструкций этой функции извлекается из текущего контекста. На первой итерации это будет адрес инструкции самой функции RtlUnwindEx. Если функция RtlLookupFunctionEntry не вернула указатель, тогда считается, что текущая функция, для которой выполнялась попытка вызова её обработчика, — простая, и, следовательно, функция не имеет кадра. Т.к. простые функции не выделяют память в стеке, значение их RSP будет указывать на адрес возврата, следовательно, для таких функций функция RtlUnwindEx извлекает этот адрес, копирует его значение в текущий контекст и увеличивает значение поля Rsp текущего контекста на 8. Теперь текущий контекст отражает состояние процессора в момент выполнения следующей по стеку выше функции. Затем функция продолжит свою работу, начиная с получения адреса PE образа и указателя на RUNTIME_FUNCTION структуру, для адреса новой инструкции, уже для следующей по стеку выше функции.

Если функция RtlVirtualUnwind вернула адрес обработчика для текущей функции, тогда ее обработчик необходимо вызвать. Перед его вызовом функция RtlUnwindEx установит флаг EXCEPTION_TARGET_UNWIND в том случае, если текущая функция является целевой. Таким образом, обработчик этой функции сможет определить, что его соответствующая функция является функцией, управление которой передается. Затем функция RtlUnwindEx обновит содержимое поля ExceptionFlags структуры EXCEPTION_RECORD из своей локальной копии. Обработчик исключения впервые обсуждался в разделе 3 второй части данной статьи, а его прототип изображен на рисунке 5. Перед вызовом обработчика функция, как и функция RtlDispatchException, обсуждаемая в разделе 2.2 третьей части данной статьи, подготавливает структуру DISPATCHER_CONTEXT, которая активно используется в случаях вложенных исключений (nested exception) и активной раскрутки (collided unwind). Определение самой структуры также изображено на рисунке 17 в разделе 2.2 третьей части данной статьи. Поля этой структуры инициализируются так же, как и в случае с функцией RtlDispatchException, с тем исключением, что поле TargetIp будет содержать значение соответствующего параметра переданного функции RtlUnwindEx, т.е. адрес инструкции, с которого будет возобновлено выполнение после раскрутки; поле ContextRecord будет содержать указатель на структуру CONTEXT, которая описывает состояние процессора в момент выполнения текущей функции, а не следующей по стеку выше; поле ScopeIndex содержит текущее значение локальной переменной и будет более подробно рассмотрено при обсуждении конструкций try/except и try/finally.

Обработчик, как и в случае с функцией RtlDispatchException, не вызывается напрямую, и вместо этого используется вспомогательная функция RtlpExecuteHandlerForUnwind, которая принимает такие же параметры, как и сам обработчик, а также возвращает такое же значение. Данная функция фактически является оберткой над функцией обработчика раскрутки и используется для того, чтобы перехватывать исключения, возникшие в процессе выполнения самого обработчика. Ассемблерное представление функции представлено ниже на рисунке 5.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Как отражено на рисунке, сначала функция выделяет память в стеке для регистровых переменных и одной переменой, сохраняет указатель на переданную DISPATCHER_CONTEXT структуру в этой переменной и вызывает обработчик исключения, адрес которого хранится в поле LanguageHandler структуры DISPATCHER_CONTEXT. Также обратите внимание на присутствие заполнителя тела функции. Его роль такая же, как и для функции RtlpExecuteHandlerForException. Ассемблерное представление функции обработчика исключения представлено ниже на рисунке 6.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Как отражено на рисунке, обработчик копирует контекст предыдущего процесса раскрутки в структуру DISPATCHER_CONTEXT текущего процесса поиска обработчика или раскрутки. Это позволяет продолжить поиск обработчика с того места, где была ранее прервана раскрутка, или продолжить ранее прерванную раскрутку. Также это позволяет пропустить вызов обработчиков тех функций, для которых такой вызов уже был выполнен во время предыдущей раскрутки. Следует также отметить, что вызов обработчиков возобновляется с той функции, на которой был прерван процесс раскрутки. Т.е. для таких функций обработчик будет вызван повторно. Более подробное пояснение этому будет дано во время обсуждения конструкций try/except и try/finally.

После того, как структура DISPATCHER_CONTEXT была подготовлена, функция RtlUnwindEx вызывает соответствующий обработчик. Сразу после вызова обработчика функция сбрасывает флаги EXCEPTION_COLLIDED_UNWIND и EXCEPTION_TARGET_UNWIND.

Если обработчик вернул ExceptionContinueSearch, то функция поменяет местами текущий и предыдущий контексты, если текущая функция не являлась целевой. Таким образом, следующая по стеку выше функция станет текущей. Далее функция продолжит свою работу, начиная с получения адреса PE образа и указателя на RUNTIME_FUNCTION структуру, для адреса новой инструкции, уже для следующей по стеку выше функции.

Если обработчик вернул ExceptionCollidedUnwind, то это означает, что в процессе раскрутки была обнаружена другая активная раскрутка, в контексте которой возникло исключение. В этом случае структура DISPATCHER_CONTEXT функции RtlUnwindEx будет содержать контекст прерванной раскрутки, т.к. он был скопирован обработчиком функции RtlpExecuteHandlerForUnwind. Следовательно, функция обновит текущий контекст из поля ContextRecord структуры DISPATCHER_CONTEXT, посредством функции RtlVirtualUnwind получит предыдущий, установит флаг EXCEPTION_COLLIDED_UNWIND и вызовет обработчик, в контексте которого ранее возникло исключение, и в зависимости от его возвращаемого результата выполнит ранее описанные действия.

Во всех остальных случаях функция RtlUnwindEx сгенерирует исключение STATUS_INVALID_DISPOSITION.

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

2. Конструкции try/except и try/finally

С точки зрения операционной системы, как это уже было рассмотрено при описании обработки исключений и раскрутки стека, сама обработка всегда является обычным вызовом соответствующей функции. Конструкции try/except и try/finally являют собой механизм, который позволяет во время разработки размещать код обработки исключений прямо в теле функции. Следовательно, поскольку код обработки размещается непосредственно в теле, эти части кода не могут быть вызваны операционной системой напрямую. Чтобы обеспечить корректное функционирование этих конструкций, компилятор генерирует вспомогательную информацию, которой пользуются вызываемые операционной системой обработчики исключений. Ранее упоминалось, что всю обработку исключений условно можно поделить на две фазы. Фаза поиска и передача управления обработчикам исключений обсуждаемых конструкций и является второй фазой. Такое разделение необходимо, поскольку разные языки программирования по-разному обрабатывают исключения; таким образом, сама операционная система абстрагирована от понимания разнообразия механизмов разных языков программирования.

Компилятор C/C++ резервирует функцию __C_specific_handler. Именно эта функция отвечает за поиск и передачу управления соответствующей конструкции. Сама функция должна быть реализована программистом. Такой подход позволяет абстрагировать компилятор от понимания работы самой операционной системы и адаптировать исполняемый образ к любой среде исполнения, например, к подсистеме Win32, к среде исполнения ядра Windows или к любой другой среде. Также реализация этой функции экспортируется модулем ntdll.dll в пользовательском пространстве и модулем ntoskrnl.exe в пространстве ядра. Поставляемые Windows SDK и WDK содержат библиотеки, которые импортируют эту функцию из соответствующего модуля. Поле ExceptionHandlerAddress структуры EXCEPTION_HANDLER будет содержать указатель на эту функцию, когда поле LanguageSpecificData этой же структуры будет содержать структуру SCOPE_TABLE, которая описывает расположение всех конструкций в теле функции. Прототип функции изображен на рисунке 5 в разделе 3 второй части данной статьи. Определение структуры SCOPE_TABLE представлено ниже, на рисунке 7.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Поле Count содержит количество конструкций в теле функции и, следовательно, количество элементов ScopeRecord в структуре. Компилятор генерирует для каждой конструкции соответствующий ScopeRecord элемент структуры, который в свою очередь описывает расположение соответствующей конструкции в теле функции, а также расположение его обработчиков. Элементы ScopeRecord сортируются в следующем порядке: невложенные конструкции следуют друг за другом в порядке их появления в коде, когда вложенные конструкции всегда следуют перед конструкцией, в которую они вложены. Поле BeginAddress элемента ScopeRecord содержит адрес начала try блока. Поле EndAddress содержит адрес инструкции, следующей за последней инструкцией, заключенной в try блок. Поле JumpTarget, если не равно нулю, содержит адрес первой инструкции кода, заключенной в except блок. Код except блока следует сразу после кода, заключенного в try блок. Поле HandlerAddress содержит адрес функции фильтра except блока. Несмотря на то, что фильтр исключения заключается в скобках после except выражения, код фильтра генерируется компилятором в виде отдельной функции, прототип которой изображен ниже, на рисунке 8.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Функция принимает два параметра. Первый параметр содержит указатель на структуру, определение которой приведено ниже, на рисунке 9. Второй параметр содержит указатель кадра функции, в которой располагается соответствующая конструкция. Этот указатель используется фильтром в том случае, если во время фильтрации необходимо получить доступ к локальным переменным функции, в которой располагается соответствующая конструкция.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Как это отражено на рисунке выше, структура содержит два указателя. Первый указывает на структуру, описывающую причину исключения, второй — на структуру, описывающую состояние процессора в момент возникновения исключения.

Функция фильтра возвращает следующие значения: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_CONTINUE_EXECUTION. Первое значение означает, что требуется передать управление обработчику исключения, для которого была вызвана функция фильтра. Также это значение может быть закодировано непосредственно в поле HandlerAddress. В таком случае конструкция не имеет фильтра, и передача управления обработчику исключения этой конструкции выполняется всегда. Второе значение указывает на то, что следует продолжить поиск обработчика исключения. Третье значение означает, что следует прервать поиск и возобновить выполнение прерванного потока.

Если поле JumpTarget равно нулю, тогда данная конструкция является finally конструкцией, и код, заключенный в finally блоке, следует сразу после кода, заключенного в try блок. В этом случае поле HandlerAddress содержит адрес функции, которая по своему содержимому повторяет код, заключенный в finally блок. Прототип этой функции изображен на рисунке 10.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Поскольку код, заключенный в finally блок, выполняется независимо от того, возникало исключение или нет, то в случае, если исключение имело место, этот код не может быть вызван напрямую, т.к. он располагается в теле функции. И поскольку во время раскрутки вызывать этот код необходимо, компилятор дублирует код, заключенный в finally блок, в виде отдельной функции. Первый параметр является булевым значением, означающим, что код finally блока выполняется из-за ненормального завершения кода, заключенного в try блок (т.е. в процессе его выполнения возникло исключение). Второй параметр содержит указатель кадра функции, в которой располагается соответствующая конструкция. Этот указатель используется функцией так же, как и функцией фильтра исключения – доступ к локальным переменным функции, в которой располагается соответствующая конструкция. Функция не возвращает никаких значений.

В тех случаях, когда в процессе выполнения кода, заключенного в try блоке, не возникло исключения, выполняется код finally блока, который следует сразу после кода try блока.

Все адреса в структуре SCOPE_TABLE являются адресами относительно начала образа.

Ниже, на рисунке 11, изображен пример структуры SCOPE_TABLE, которую сгенерирует компилятор.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

На рисунке выше изображен пример программы, _tmain функция которой включает в себя try/except и try/finally конструкции. Слева от исходного кода изображено ассемблерное представление функций: _tmain, функции фильтра нижней try/except конструкции и функции, дублирующей код, заключенный в finally блок. Функции перечислены снизу вверх. Адреса ассемблерных инструкций являются абсолютными. Зелеными маркерами сопоставляется код, заключенный в блоки, с его ассемблерными эквивалентами. Следует обратить внимание на то, что блок кода с маркером 2 в ассемблерном представлении встречается дважды: в теле функции _tmain и в самой верхней функции. Последнее является дубликатом кода, заключенного в блок finally. Также следует обратить внимание на присутствие инструкции nop после инструкции вызова функции FailExecution в блоке кода с маркером 1. Данная инструкция также является заполнителем, как и в случаях с функциями шлюзов, функцией RtlpExecuteHandlerForException и функцией RtlpExecuteHandlerForUnwind. Если заполнитель будет отсутствовать, то при проверке инструкции на принадлежность к той или иной конструкции может быть сделано ошибочное предположение об ее принадлежности. В данном случае будет сделано ошибочное предположение о том, что инструкция вызова функции FailExecution не принадлежит блоку кода с маркером 1, т.к. функция RtlVirtualUnwind после раскрутки вернет адрес не на инструкцию вызова функции FailExecution, а на инструкцию сразу после нее. По этой причине компилятор добавляет заполнитель после инструкции вызова функции, если та в свою очередь является последней инструкцией в блоке. Если инструкция вызова является не последней инструкцией в блоке, тогда такого заполнителя не будет.

В левой части рисунка изображены структуры, которые сгенерирует компилятор. Вверху изображен элемент таблицы функций, ниже него изображена структура UNWIND_INFO, на которую ссылается этот элемент. Несмотря на то, что структура EXCEPTION_HANDLER не является частью структуры UNWIND_INFO, на рисунке она представлена как часть этой структуры, т.к. если она присутствует, то следует сразу после структуры UNWIND_INFO. Ниже структуры UNWIND_INFO изображено более подробное представление структуры EXCEPTION_HANDLER, ниже него изображено более подробное представление поля LanguageSpecificData этой структуры, в котором размещается структура SCOPE_TABLE. В самом низу последовательно изображены элементы ScopeRecord массива этой структуры. Все адреса в сгенерированных структурах являются относительными. Также эти адреса сопоставляются с адресами ассемблерного кода посредством уникальных номеров.

Более подробно стоит остановиться на элементах ScopeRecord массива. Элемент 0 описывает расположение блока с маркером 1. Поле HandlerAddress этого элемента содержит адрес функции, дублирующей код finally блока с маркером 2. Поле JumpAddress содержит 0, т.к. это finally блок. Элемент 1 описывает расположение блока с маркером 3. Поле HandlerAddress этого элемента содержит значение 1, что в свою очередь означает, что конструкция не имеет фильтра, и при возникновении исключения следует всегда передавать управление коду блока с маркером 4. Поле JumpAddress содержит адрес начала блока с маркером 4. Элемент 2 описывает расположение блока с маркером 5. Поле HandlerAddress этого элемента содержит адрес функции фильтра, код которого заключен в скобки после ключевого слова except. Ассемблерное представление функции фильтра располагается посередине, между функцией _tmain и функцией, дублирующей finally блок. Как изображено на рисунке, функция фильтра вызывает функцию ExceptionFilter, которая принимает указатель на структуру, описывающую контекст исключения. Поле JumpAddress содержит адрес начала блока с маркером 6.

Несмотря на то, что функция __C_specific_handler не представлена на рисунке, поле ExceptionHandlerAddress структуры EXCEPTION_HANDLER, содержит адрес этой функции. Эта функция будет вызвана операционной системой во время поиска обработчика исключения или во время раскрутки стека. Следовательно, реализация этой функции отвечает за интерпретацию структуры SCOPE_TABLE, вызов фильтров, вызов finally блоков и передачу управления except блокам.

Блок-схема функции __C_specific_handler изображена ниже, на рисунке 12.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

В начале своей работы функция получает: адрес начала PE образа; относительный адрес инструкции, принадлежащий телу функции, для которой обработчик был вызван; указатель на структуру SCOPE_TABLE. В зависимости от выполняемой операции (поиск обработчика или раскрутка) работа функции варьируется.

Если выполняется поиск обработчика, то функция подготавливает структуру EXCEPTION_POINTERS, указатель на которую передается фильтрам соответствующих конструкций. Затем функция последовательно сканирует ScopeRecord элементы структуры SCOPE_TABLE и проверяет, принадлежит ли ранее полученный адрес инструкции какой-либо конструкции. Если принадлежит, тогда также проверяется, является ли конкретная конструкция try/except конструкцией, и, если нет, то она просто игнорируется, и проверяется следующий элемент. В противном случае вызывается фильтр этой конструкции. Если фильтр вернул EXCEPTION_CONTINUE_SEARCH, то данная конструкция игнорируется, и проверяется следующий элемент. Если фильтр вернул EXCEPTION_CONTINUE_EXECUTION, то функция завершает свою работу и возвращает ExceptionContinueExecution, чтобы указать операционной системе прекратить поиск обработчика и возобновить выполнение прерванного потока. Если фильтр вернул EXCEPTION_EXECUTE_HANDLER, то функция вызывает функцию RtlUnwind, которой в качестве кадра целевой функции указывается кадр функции, обработчик которой был вызван; в качестве адреса инструкции, с которой будет продолжено выполнение, передается адрес первой инструкции except блока; а также передается код исключения, который будет содержаться в RAX регистре сразу после передачи управления целевой функции. Функция RtlUnwind перед передачей управления последовательно вызовет обработчики всех промежуточных функций.

Если выполняется раскрутка, тогда функция ведет себя иначе. Сначала функция получает относительный адрес инструкции, с которой будет возобновлено выполнение. Затем функция последовательно сканирует ScopeRecord элементы структуры SCOPE_TABLE и проверяет, принадлежит ли ранее полученный адрес инструкции какой-либо конструкции. Если принадлежит, тогда также проверяется, является ли конкретная конструкция try/finally конструкцией. Если является, тогда вызывается ее обработчик, который по сути является дубликатом кода, заключенного в finally блок. Перед вызовом функция увеличит значение поля ScopeIndex структуры DISPATCHER_CONTEXT на единицу. Значение параметра AbnormalTermination при вызове этого обработчика всегда является TRUE. Следовательно, макрос AbnormalTermination всегда будет возвращать TRUE для этих блоков, вызванных таким образом. Для кода finally блока, располагающегося в теле самой функции, этот же макрос всегда будет возвращать FALSE. В этих случаях компилятор явно подставляет это значение. Иначе говоря, макрос AbnormalTermination возвращает TRUE только тогда, когда выполняется раскрутка. Практически, вследствие исключения. Если конструкция не является try/finally, тогда проверяется, не является ли адрес начала except блока адресом, с которого будет продолжено выполнение. И, если является, тогда работа функции завершается. Такая проверка необходима потому, что конструкция try/except может быть вложена в другую конструкцию try/finally, как это изображено ниже, на рисунке 13.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Как видно из рисунка, если такую проверку не выполнить, то во время раскрутки будет вызван finally блок внешней конструкции. А это недопустимо.

Если функция выполняет раскрутку для целевой функции, т.е. флаг EXCEPTION_TARGET_UNWIND установлен в поле ExceptionFlags структуры EXCEPTION_RECORD, тогда она выполняет дополнительную проверку перед вызовом обработчика. Суть проверки заключается в том, чтобы определить, не принадлежит ли адрес, с которого будет продолжено выполнение, самой конструкции, а не ее обработчику. И если принадлежит, тогда работа функции завершается. Подобная ситуация может быть только в случае использования в пределах finally блоков операторов goto, которые указывают за пределы этих блоков. Данная ситуация изображена ниже, на рисунке 14.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Как видно из рисунка, если такую проверку не выполнить, то во время раскрутки будет вызван finally блок внешней конструкции. А это недопустимо. Также, если функция не является целевой, тогда данная проверка не нужна.

Следует отметить, что в обоих случаях (и в случае поиска обработчика, и в случае выполнения раскрутки) сканирование элементов начинается не с начала, а с элемента, номер которого хранится в поле ScopeIndex структуры DISPATCHER_CONTEXT. Как это уже было отмечено, функция перед вызовом обработчика try/finally конструкции увеличивает значение поля ScopeIndex структуры DISPATCHER_CONTEXT на единицу. Ранее упоминалось, что если в процессе поиска обработчика или выполнения раскрутки будет обнаружен незавершенный процесс раскрутки, то продолжение поиска обработчика или выполнения раскрутки будет возобновлено с прерванного места. При этом обработчик функции, который породил исключение, будет вызван повторно, когда обработчики остальных функций вызваны не будут. В такой ситуации недопустимо, чтобы обработчики конструкций, которые уже были вызваны, оказались вызваны повторно. Эта ситуация изображена ниже, на рисунке 15.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

На рисунке выше изображен стек вызова функций, слева от которого изображена стрелка направления его роста, а справа изображена часть кода функции Func1. Функции RtlDispatchException и RtlUnwindEx хоть и вызывают обработчики функций посредством функций RtlpExecuteHandlerForException и RtlpExecuteHandlerForUnwind, но в стеке вызовов эти функции для краткости не присутствуют. Функция Func1 вызвала функцию Func2, которая в свою очередь вызвала функцию Func3, которая породила исключение. Как только функция RtlDispatchException получала управление, она последовательно вызвала обработчики для функций: сначала Func3, затем Func2 и в конечном счете Func1. Обработчик функции Func1 нашел конструкцию, которая может обработать исключение, и вызвал функцию RtlUnwind для передачи управления обработчику этой конструкции. Функция RtlUnwind в свою очередь вызвала RtlUnwindEx, которая последовательно вызывала обработчики для функций сначала Func3, затем Func2 и в конечном счете Func1. Обработчик функции Func1 вызвал обработчик самого вложенного finally блока, который в свою очередь породил новое исключение. Как только функция RtlDispatchException получила управление, она последовательно вызывала обработчики предыдущих функций. Один из этих обработчиков окажется обработчиком функции RtlpExecuteHandlerForUnwind, которую вызывает функция RtlUnwindEx при передаче управления обработчику раскручиваемой функции. Обработчик функции RtlpExecuteHandlerForUnwind скопирует контекст раскрутки из RtlUnwindEx функции в функцию RtlDispatchException и, после возврата управления ей, поиск обработчика продолжится с того места, где была прервана раскрутка. Поскольку функция RtlUnwindEx ранее раскрутила функции Func3 и Func2, их обработчики вызываться не будут. Но поскольку функция Func1 породила исключение, она не была раскручена функцией, и, следовательно, ее обработчик будет вызван. Поскольку функция __C_specific_handler во время раскрутки увеличивает значение поля ScopeIndex структуры DISPATCHER_CONTEXT на единицу перед вызовом обработчика конструкции, то в скопированном контексте это поле будет равно 1. Следовательно, когда функция __C_specific_handler будет вызвана для функции Func1 вновь, поиск конструкции начнется с конструкции с индексом 1. Таким образом конструкция, породившая исключение, будет пропущена. Возобновление раскрутки выполняется аналогичным образом. Несмотря на то, что операционная система и компилятор абстрагированы друг от друга, наличие поля ScopeIndex в структуре DISPATCHER_CONTEXT является нарушением этой абстракции.

В завершение обсуждения try/except и try/finally конструкций стоит описать принцип работы макроса GetExceptionCode. Использование этого макроса возможно только в except блоках. Этот макрос читает содержимое регистра RAX, а при описании функции __C_specific_handler упоминалось, что передача управления конкретному except блоку выполняется посредством функции RtlUnwind, которая принимает параметр, значение которого будет записано в RAX регистре после передачи управления. Через этот параметр передается код возникшего исключения.

Также стоит описать принцип работы макроса GetExceptionInformation. Использование этого макроса возможно только в выражениях, заключенных в скобках после ключевого слова except. Поскольку в действительности выражение, заключенное в скобки (иначе говоря, фильтр), является отдельной функцией, которая принимает два параметра, данный макрос получает значение первого параметра. При описании функции __C_specific_handler упоминалось, что функция фильтра принимает два параметра, где первый параметр является указателем на структуру, описывающую исключение и состояние процессора в момент возникновения исключения.

3. Недостатки реализации механизма

Одним из недостатков данного механизма является использование в finally блоках операторов goto, которые указывают за пределы этих блоков. В этом случае компилятор C/C++, вместо прямой передачи управления, использует зарезервированную функцию _local_unwind, прототип которой изображен ниже, на рисунке 16.

что такое раскрутка стека. Смотреть фото что такое раскрутка стека. Смотреть картинку что такое раскрутка стека. Картинка про что такое раскрутка стека. Фото что такое раскрутка стека

Первый параметр функции принимает адрес кадра функции, до которой следует раскрутить стек, когда второй принимает адрес инструкции, которой следует передать управление после раскрутки. Сама функция должна быть реализована программистом. Также реализация этой функции экспортируется модулем ntdll.dll в пользовательском пространстве и модулем ntoskrnl.exe в пространстве ядра. Поставляемые Windows SDK и WDK содержат библиотеки, которые импортируют эту функцию из соответствующего модуля. Реализация самой функции очень простая, она вызывает функцию RtlUnwind, которой передает два своих параметра. Остальные параметры функции RtlUnwind при вызове обнулены.

Использование компилятором функции _local_unwind вместо прямой передачи управления в первую очередь связано с невозможностью передать управление в произвольное место функции в том случае, если finally блок был вызван в результате раскрутки. В таком случае передать управление в нужное место функции возможно только посредством нового процесса раскрутки. Такой подход имеет побочные эффекты. Оператор goto, в своей основе, передает прямое управление, когда раскрутка приводит к вызову finally блоков. Следовательно, до фактической передачи управления будут вызваны finally блоки, которые могут изменить контекст самой функции. Microsoft не рекомендует использовать оператор goto таким образом, а компилятор выдаст соответствующее предупреждение.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *