вторник, 1 июля 2008 г.

Обработка исключений и корректность программ на С++.

Я снова хочу затронуть тему обрабртки исключений. На этот раз речь пойдет о структурной обработке исключений операционной системы windows — SEH. Стандарт описывает только модель обработки исключений, не зависящую от платформы, под которую программа компилируется. Такое исключение может быть выброшено с помощью ключевого слова throw, функцией стандартной библиотеки, или оператором new... Исключение имеет тип, который определяет то, какой обработчик будет вызван, а так-же то, какую информацию исключение передаст в свой обработчик. Помимо этого существуют еще структурные исключения, они не зависят от языка программирования, а специфичны для операционной системы. Такое исключение может быть выброшено при попытке поделить на ноль, или ошибке доступа к памяти и так далее. Компилятор Visual Studio имеет такую опцию как /Eha, которая позволяет программе использовать SEH. Использовать одновременно исключения обоих типов, в программе на С++ проблематично, так как прийдется их обрабатывать по отдельности. Что-бы этого избежать SEH исключение нужно транслировать в обычное исключение. Делается это с помощью функции _set_se_translator стандартной библиотеки, сама эта функция стандартной не является. Она получает указатель на функцию транслятор, которая получает структуру описывающую исключение и в ответ, должна бросить типизированное исключение, вот как-то так:
#include <exception>
#include <iostream>
#include <eh.h>
#include <windows.h>
void trans_func( unsigned int u, EXCEPTION_POINTERS* p )
{
    std::cout << "trans_func called" << std::endl;
    if (p->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
        throw std::runtime_error("access violation");
    throw std::runtime_error("some other error");
}
class Foo 
{
public:
    virtual void bar() { std::cout << ":)" << std::endl; }
};
int main(int argc, char* argv[])
{
    _set_se_translator(&trans_func);
    Foo *foo = NULL;
    try 
    {
        foo->bar();
    }
    catch(std::exception& e) 
    {
        std::cout << "error handled: " << e.what() << std::endl;
    }
    system("pause");
    return 0;
}
Этот код выведет сообщение об ошибке. Теперь о моральных аспектах проблеммы. Во первых, использование опции /Eha снижает общую производительность, так как компилятор «не знает» о том, где может быть выброшено исключение, а где его в принципе быть не может. Во вторых выбрасывание исключения при разименовании указателя или при порче стэка — не стандартное поведение, к примру разименование нулевого указателя, это undefined behavior. Поэтому программа, которую я привел в качестве примера, не является корректной с точки зрения стандарта, но зато работает :).

7 комментариев:

Artem комментирует...

кстати говоря, следует обратить внимание что если уж юзать SEH (например, для краш репортинга), то нужно ещё хотя бы set_terminate(), set_unexpected(), _set_purecall_handler() и _set_invalid_parameter_handler().
А иначе такая простая конструкция как, скажем, std::vector<int>v;v.front(); повалит программу несмотря на любую обработку исключений.
А всё потому что http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=101337

Ещё интересным нюансом является то, что плохо обрабатывать SEH в контексте того исполняемого файла, где произошло исключение. Дело в том что SEH-исключение запросто может быть вызвано нарушением кучи, вследсвтие чего обработчик точно так же упадёт из-за тех же нарушений в куче -- при малейшем динамическом управлении памятью. Чтобы этого не произошло, обработчик SEH ложат в отдельную DLL... но в общем случае он уже мало что может сделать для спасения главной проги -- разве что красиво попросить прощения :)

Lazin комментирует...

Вообще в таких случаях не очень понятно как следует поступать. Вот, допустим программа перехватила Access Violation, и что, продолжить работать дальше? Тут остается только запись что-нибудь в лог, создать crash report и подохнуть :)
К тому-же это решение не будет кроссплатформенным...

Artem комментирует...

да, единственное что правильно сделать в обработчике SEH -- это красиво и с песнями умереть :)

о кроссплатформенности -- пусть это и не переносимо на уровне кода, но концепция работает и в линухе -- там тоже можно обрабатывать AccessViolation и иже с ними, через catch(...) + signal()

прикольная фича которую доводилось реализовать -- в SEH обработчике сервера запомнить последний запрос, перезапуститься и таки его обработать :) Некрасиво конечно, но спасло на здоровенном чужом проекте, когда дедлайн прижал а оно изредка падало. Кажется, та система до сих пор крутится на серверах заказчика, и никто даже не подозревает что сервер падает каждых полчаса :)))

Lazin комментирует...

Мне все-же кажется, что лучше SEH не обрабатывать вообще, производительность чуть выше, крэшдамп можно и без этого сформировать, а для чего еще их перехватывать кроме диагностики я не представляю...

Lazin комментирует...

ну конечно кроме таких экзотических случаев :))

Анонимный комментирует...

SEH может пригодится тому, кто решится под Windows эмулировать какую-нибудь другую среду (например Linux). Так как позволяет отловить вызовы прерываний, обращений по несуществуещему адресу и т.д.

Lazin комментирует...

Вообще в таких случаях не очень понятно как следует поступать. Вот, допустим программа перехватила Access Violation, и что, продолжить работать дальше? Тут остается только запись что-нибудь в лог, создать crash report и подохнуть :)
К тому-же это решение не будет кроссплатформенным...