Exception handling, unwinding и все все все

Тема в разделе "WASM.X64", создана пользователем Joes, 15 апр 2008.

  1. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    Разбирался с exception handling под х64...

    Есть некий stub, на асме. Он дергает функцию.
    Надо что бы стаб ловил exception'ы которые происходят в функции и "ниже".

    Попытка 1.
    Для стаба не генерится инфа о unwind, но потом в рантайме добавляется через RtlAddFunctionTable.
    Стаб:
    Код (Text):
    1. ExecuteGuard    PROC
    2.         push rbp
    3.         call rcx           
    4.         pop     rbp
    5.         ret
    6. ExecuteGuard    ENDP
    Exception handler никогда не вызывается, как бы я не добавлял нужные мне структурки. RtlAddFunctionTable ошибок не возвращает.
    На только что добавленную область RtlLookupFunctionEntry выдает NULL - структурки нету.
    Параметры перепроверялись н-цать раз.
    Из мыслей - если для процесса с его imagebase уже есть табличка, оно не добавит еще один элемент. Т.е. работает RtlAddFunctionTable только и только для случаев, когда действительно динамический код и он не принадлежит процессу/dll.
    Убился день.

    Попытка 2.
    Сгенерил для стаба unwind info через макросы.
    Код (Text):
    1. ExecuteGuard    PROC FRAME : ExceptionGuardHandler
    2.         push    rbp
    3.         .pushreg    rbp
    4.         .endprolog
    5.  
    6.         xor eax,eax
    7.         div eax
    8.                
    9.         pop     rbp
    10.         ret
    11. ExecuteGuard    ENDP
    Ура, exception handler вызывается!
    Меняю код на:

    Код (Text):
    1. ExecuteGuard    PROC FRAME : ExceptionGuardHandler
    2.         push    rbp
    3.         .pushreg    rbp
    4.         .endprolog
    5.  
    6.         call ecx
    7.                
    8.         pop     rbp
    9.         ret
    10. ExecuteGuard    ENDP
    Вызывается функция которая передана в первом параметре, оная кидает exception (тоже division by zero). Мой exception handler не вызывается. Как ни крутил параметры unwind - не вызывается и все тут.
    Взял windbg, словил exception, проверил exception handler list - мой хендлер первый в списке.
    Начал дебагать ntdll.dll от KiUserExceptionDispatch.
    И вот что нашел...
    RtlVirtualUnwind, который "отматывает" стек делает это таким образом:
    1. По rip ищет структурку которая отвечает за функцию
    2. Ищет "эпилог" функции. А именно выход. А именно 0xC2/0xC3. Прямо хардкодом. Паралельно пропускает (и эмулирует) опкоды работы со стеком.
    Так вот, мне влом было вникать, но оказалось, что если rip при выходе из функции (в моем случае это на pop rbp) указывает на эпилог (тоже pop rbp), то exception handler вызван не будет.

    Исправилось таким образом:
    Код (Text):
    1. ExecuteGuard    PROC FRAME : ExceptionGuardHandler
    2.         push    rbp
    3.         .pushreg    rbp
    4.         .endprolog
    5.  
    6.         call ecx
    7.  
    8.         nop
    9.                
    10.         pop     rbp
    11.         ret
    12. ExecuteGuard    ENDP
    Второй день был потерян.

    Выводы:
    х64 exception handling, может быть, и хорошая идея. Но реализация - ни в какие ворота.
    Начиная с того, что документации почти ноль - постоянные отсылы к производителям процов, к calling convention, etc. Calling convention в свою очередь ничего про exception'ны не говорит. И заканчивая тем, что нигде никаких ошибок не происходит, что реально усложняет дебаг и просто напросто сбивает с толку. Да и поиск конца функции по опкодам это еще та мега-технология..

    P.S. Если у кого получилось заставить работать RtlAddFunctionTable для функции внутри модуля, но без структурки в .pdata - скажите как...
     
  2. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    Joes
    Там жоп@ полная. Я так и не понял чем их не устраивал вариант с FS:0.
     
  3. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    P.S. Можно глянуть код макросов ".pushreg rbp" и ".endprolog" ?
     
  4. IceStudent

    IceStudent Active Member

    Публикаций:
    0
    Регистрация:
    2 окт 2003
    Сообщения:
    4.300
    Адрес:
    Ukraine
    dermatolog
    Они на уровне ассемблера, если не ошибаюсь (как и .FPO).
     
  5. green

    green New Member

    Публикаций:
    0
    Регистрация:
    15 июл 2003
    Сообщения:
    1.217
    Адрес:
    Ukraine
    dermatolog
    От раскрутки по стеку решили отказаться, во-первых, по соображениям надёжности и безопасности: слишком легко его запортить, в т.ч. и сознательно.
    Во-вторых, новая модель практически полностью устраняет накладные расходы на __try/__except в коде ф-ции (теперь exception frame не строится при каждом входе в ф-цию). Правда, стоимость раскрутки(unwind) значительно повышается, но это не имеет значения при правильном использовании исключений.
     
  6. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    green
    Со всем согласен, только есть одно НО - помимо автоматической генерации кода МС-ными компиляторами есть еще необходимость в "ручной" установке своих обработчиков. Простого и удобного механизма сейчас просто нет - приходится все делать на уровне Execption Table (причем если это делается не средствами компилятора, то это полный пипец :))).
     
  7. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    Идея, действительно хорошая.
    Они целиком и полностью избавились от runtime расходов на установку handler'ов. В результате, если раньше для игр (например), делательно было выключать exception handling, т.к. чуть быстрее работало, то теперь не имеет значения.

    Они часть макро ассмеблера. В каждом PE-32+ есть новая секция, звется .pdata. В ней лежат структурки которые были сгенерены компилятором и которые "покрывают" код. Собственно директива компилятора FRAME:

    Код (Text):
    1. MyFunc PROC FRAME
    2. MyFunc ENDP
    и генерит нужные структурки в этой самой секции, при линковке. А вот эти макросы нужны что бы система (а именно RtlVirtualUnwind) смогла "отмотать" стек, в случае exception.

    В целом:
    + Нет накладных расходов на установку обработчиков во время исполнения
    + Более "четкая" система unwind'а стека при exception, чем сильно упростили процесс stack walking'a.
    - Мало внятной документации
    - Баги реализации (или фичи, только никто об этом не знает)?
    - Вообще весь код должен быть "покрыт" такими структурками. Если exception происходит в коде, который не "покрыт", приложение тихо мирно упадет (в случае если не стоит UnhandledExceptionFilter). Скажем для кода на плюсах, размер структурки около 2-3% размера файла.
     
  8. green

    green New Member

    Публикаций:
    0
    Регистрация:
    15 июл 2003
    Сообщения:
    1.217
    Адрес:
    Ukraine
    Joes
    Вот здесь немного есть.

    Насчёт RtlAddFunctionTable не знаю, но
    вполне документированная фича.
     
  9. IceStudent

    IceStudent Active Member

    Публикаций:
    0
    Регистрация:
    2 окт 2003
    Сообщения:
    4.300
    Адрес:
    Ukraine
    Joes
    В смысле, тихо? Даже без стандартного сообщения? Под рукой нет х64, чтоб проверить..
     
  10. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    Joes
    Если честно, то я не понял эту фразу. В UNWIND_INFO описан размер пролога (зеркальная копия эпилога). На кой еще что-то парсить до RET? И сразу вопрос - а если у меня концом функции является JMP - то всё, приехали?
     
  11. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    В смысле с сообщением :)
    Обработчики в цепочке не вызовутся, только UnhandledExceptionHandler или VectoredExceptionHandler

    Пример:
    1. Хацкерская DLL у которой некая функция ее API "непокрыта" структуркой.
    2. Дергаем функцию, там exception, приложение валится и ничего не поможет кроме UnhandledExceptionHandler.

    P.S. Это в случае если функция не "leaf". Leaf функции не могут модифицировать стек и не могут вызывать другие функции. В смысле - у них первое значение в стеке - адрес возврата.
     
  12. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    Я тоже думал. Не придумал :)
    Возможно связано с тем, что в эпилоге не всегда делается так:
    Код (Text):
    1. lea rsp, [rbp]
    или аналогично, а стек восстанавливать надо.

    Кстати, вот ссылка.
    http://msdn2.microsoft.com/en-us/library/tawsa7cb.aspx

    И чего они пишут:
    В моем случае два варианта:
    1. Я не сделал "правильный" пролог (не было add RSP, xx), но почему оно проигнорировало exception handler?
    2. Таки баг с адресом выхода (дебаг, вроде бы, подтвердил)
    Забыл добавить сразу: вышестоящий SEH handler вызывался. Т.е. такой вот код:

    Код (Text):
    1. void test()
    2. {
    3.  __try
    4.  {
    5.    ExecuteGuard(someTest);
    6.  } __except(EXCEPTION_EXECUTE_HANDLER)
    7.  {
    8.    printf("Caught exception.\n");
    9.  }
    10. }
    Печатал "Caught exception.". Мой exception handler не вызывался вообще. Пока не добавил nop.

    Если без REX префикса 0x48 - считает локальным прыжком. Т.е. не выходом из функции и скипает.
    Если с префиксом - выходом.
     
  13. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    Joes
    В "Calling convention" написано, что вызывающая функция обязана зарезервировать в стеке 4*8 байтов. Поэтому "sub rsp, 20" ИМХО является минимальным набором для пролога. Опять же раскрутить пролог в обратном направлении никаких проблем не вызывает - если мы сделали SUB RSP, то в эпилоге ВСЕГДА делаем ADD:
    Код (Text):
    1. 00000000004011C0 53                      push rbx
    2. 00000000004011C1 4883EC20                sub rsp, 20
    3. ...
    4. 000000000040124A 4883C420                add rsp, 20
    5. 000000000040124E 5B                      pop rbx
    6. 000000000040124F C3                      ret
    Не - это точно муть какая-то :)) Каво там скипать если в RUNTIME_FUNCTION жестко заданы границы функции.
     
  14. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    Дык, пофиг: выделял, не выделял - побоку. Если нет nop, то даже в твоем примере exception handler не вызовется.
    В смысле:
    Код (Text):
    1. ExecuteGuard    PROC FRAME : ExceptionGuardHandler
    2.         push    rbp
    3.         .pushreg    rbp
    4.         sub rsp, 20
    5.         .allocstack 20 
    6.         .endprolog
    7.  
    8.         call ecx
    9.                
    10.         add     rsp, 20
    11.         pop     rbp
    12.         ret
    13. ExecuteGuard    ENDP
    Не работает :)

    А если несколько точек выхода?

    Код (Text):
    1. push rbx
    2. sub rsp, 20
    3.  
    4. ; Тут закончился пролог и идет код функции уже
    5. push rcx
    6.  
    7. call someFunc
    8. jmp quit
    9.  
    10. add rsp, 20
    11. pop rbx
    12. ret
    13.  
    14. quit:
    15. pop rcx
    16. add rsp, 20
    17. pop rbx
    18. ret
    Разве в данном случае jmp является эпилогом? Если его не обработать, оно не выкинет rcx из стека и в результате оно все поломается..
    Вобщем, еще та технология-ниппель.
     
  15. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    Joes
    Код (Text):
    1. push rcx
    Такого не будет 100%!!! UNWIND_INFO однозначно определяет глубину стека для ВСЕЙ функции, т.к. при возникновении ошибки в someFunc ты потом хрен раскрутишь стек до функции, которая вызвала твой код. Поэтому все манипуляции со стеком делаются ТОЛЬКО в прологе и эпилоге.
     
  16. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    Увы, может быть и будет. Тебе ж надо дергать функции, а они кушают параметры, а не все параметры передаются через регистры :)
    Да и в MSDN написано что может.
     
  17. Joes

    Joes New Member

    Публикаций:
    0
    Регистрация:
    5 янв 2008
    Сообщения:
    98
    Это ж где она документирована? Я не нашел, но может плохо искал :-\
    Кроме того, почему ж оно не должно вызваться, если я точно знаю что мне надо обработать exception... Причем я проверил, у меня стоит и EHANDLER и UHANDLER (exception и unwind соответственно).
     
  18. asmfan

    asmfan New Member

    Публикаций:
    0
    Регистрация:
    10 июл 2006
    Сообщения:
    1.004
    Адрес:
    Abaddon
  19. green

    green New Member

    Публикаций:
    0
    Регистрация:
    15 июл 2003
    Сообщения:
    1.217
    Адрес:
    Ukraine
    Joes
    Видимо, причина в том, что x64 EH заточен под ЯВУ, в которых пролог/эпилог на уровне языка не контроллируется.
     
  20. green

    green New Member

    Публикаций:
    0
    Регистрация:
    15 июл 2003
    Сообщения:
    1.217
    Адрес:
    Ukraine
    Интересно, как это реализовано в других ОС...