Почему 64-битная Windows не может отменить исключения пользователя-ядра-пользователя?

Почему 64-битная Windows не может разматывать стек во время исключения, если стек пересекает границу ядра - когда 32-битная Windows может?

Контекст всего этого вопроса исходит из:

Случай исчезновения исключения OnLoad - исключения обратного вызова пользовательского режима в x64

Фон

В 32-битной Windows, если я сгенерирую исключение в моем коде режима пользователя, которое было вызвано из кода режима ядра, которое было вызвано из моего кода режима пользователя, например:

User mode                     Kernel Mode
------------------            -------------------
CreateWindow(...);   ------>  NtCreateWindow(...)
                                   |
WindowProc   <---------------------+                                   

Структурная обработка исключений (SEH) в Windows может разматывать стек, разматывать обратно через режим ядра обратно в мой пользовательский код, где я могу обработать исключение и вижу действительную трассировку стека.

Но не в 64-битной Windows

64-разрядные версии Windows не могут сделать это:

По сложным причинам мы не можем распространять исключение обратно в 64-битных операционных системах (amd64 и IA64). Это имело место со времени первого 64-разрядного выпуска Server 2003. На x86 это не так - исключение распространяется через границу ядра и в конечном итоге возвращает кадры назад.

И поскольку в этом случае нет способа вернуться к надежной трассировке стека, пришлось принять решение: позволить вам увидеть несмысленное исключение или спрятать его полностью:

Архитекторы ядра в то время решили придерживаться консервативного подхода к AppCompat - скрыть исключение и надеяться на лучшее.

Далее в статье рассказывается, как все 64-битные операционные системы Windows вели себя так:

  • Windows XP 64-битная
  • Windows Server 2003 64-разрядная версия
  • Windows Vista 64-разрядная
  • Windows Server 2008 64-разрядная версия

Но начиная с Windows 7 (и Windows Server 2008), архитекторы изменили свое мнение - вроде как. Только для 64-битных приложений (не 32-битных) они (по умолчанию) перестали бы подавлять эти исключения user-kernel-user. Итак, по умолчанию, на:

  • Windows 7 64-битная
  • Windows Server 2008

все 64-битные приложения будут видеть эти исключения, где они никогда не видели их.

В Windows 7, когда происходит сбой родного приложения x64, помощник по совместимости программ получает уведомление. Если приложение не имеет манифеста Windows 7, мы покажем диалоговое окно, сообщающее, что PCA применила прокладку совместимости приложений. Что это значит? Это означает, что при следующем запуске приложения Windows будет эмулировать поведение Server 2003 и исключать исключение. Имейте в виду, что PCA не существует на Server 2008 R2, поэтому этот совет не применяется.

Итак, вопрос

Вопрос в том, почему 64-разрядная Windows не может размотать стек обратно при переходе к ядру, а 32-разрядные версии Windows могут?

Единственный совет:

По сложным причинам мы не можем распространять исключение обратно в 64-битных операционных системах (amd64 и IA64).

Намек это сложно.

Возможно, я не понимаю объяснения, так как я не разработчик операционной системы, но мне хотелось бы узнать, почему.


Обновление: исправление для прекращения подавления 32-разрядных приложений

Microsoft выпустила исправление, позволяющее 32-разрядным приложениям также больше не подавлять исключения:

KB976038: Исключения, которые выбрасываются из приложения, работающего в 64-разрядной версии Windows, игнорируются

  • Исключение, которое выдается в процедуре обратного вызова, выполняется в пользовательском режиме.

В этом случае это исключение не вызывает сбой приложения. Вместо этого приложение переходит в противоречивое состояние. Затем приложение выдает другое исключение и вылетает.

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

Затем исправление позволяет вам запретить Windows использовать глобальные исключения:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
DisableUserModeCallbackFilter: DWORD = 1

или для каждого приложения:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\Notepad.exe
DisableUserModeCallbackFilter: DWORD = 1

Поведение было также задокументировано на XP и Server 2003 в KB973460:


Намек

я обнаружил еще одну подсказку при исследовании использования xperf для захвата следов стека в 64-битной Windows:

Stack Walking в Xperf

Отключить Paging Executive

Чтобы трассировка работала в 64-битной Windows, вам необходимо установить раздел реестра Disable Paging Executive. Это заставляет операционную систему не публиковать драйверы режима ядра и системный код на диске, что является необходимым условием для получения стеков 64-битных вызовов с использованием xperf, поскольку обход 64-битного стека зависит от метаданных в исполняемых образах, а в некоторых ситуациях Коду обхода стека xperf не разрешается касаться страниц. Выполнение следующей команды из командной строки с повышенными привилегиями установит этот раздел реестра для вас.

 REG ADD "HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management" -v 
 DisablePagingExecutive -d 0x1 -t REG_DWORD -f

После установки этого ключа реестра вам нужно будет перезагрузить систему, прежде чем вы сможете записывать стеки вызовов. Установка этого флага означает, что ядро ​​Windows блокирует больше страниц в ОЗУ, поэтому, вероятно, это потребует около 10 МБ дополнительной физической памяти.

Это создает впечатление, что в 64-битной Windows (и только в 64-битной Windows) вам не разрешается проходить стеки ядра, поскольку на диске могут быть страницы.

2 ответа

Решение

Я разработчик, который написал это исправление довольно давно, а также пост в блоге. Основная причина в том, что полный файл реестра не всегда захватывается при переходе в пространство ядра по соображениям производительности.

Если вы выполняете обычный системный вызов, двоичный интерфейс приложения x64 (ABI) требует только сохранения энергонезависимых регистров (аналогично обычному вызову функции). Тем не менее, для правильной отмены исключения требуется наличие всех регистров, поэтому это невозможно. По сути, это был выбор между перфомансом в критическом сценарии (то есть сценарии, который потенциально может происходить тысячи раз в секунду) против 100% правильной обработки патологического сценария (сбой).

Бонус Чтение

Очень хороший вопрос

Я могу дать подсказку, почему "распространение" исключения через границу ядра-пользователя несколько проблематично.

Цитата из вашего вопроса:

Почему 64-битная Windows не может разматывать стек во время исключения, если стек пересекает границу ядра - когда 32-битная Windows может?

Причина очень проста: не существует такой вещи, как "стек пересекает границу ядра". Вызов функции режима ядра ни в коем случае не сравним со стандартным вызовом функции. На самом деле это не имеет никакого отношения к стеку вызовов. Как вы, наверное, знаете, память в режиме ядра просто недоступна из пользовательского режима.

Вызов функции режима ядра (он же syscall) реализуется путем запуска программного прерывания (или аналогичного механизма). Код пользовательского режима помещает некоторые значения в регистры (которые идентифицируют необходимую службу режима ядра) и вызывает инструкцию CPU (такую ​​как sysenter), который переводит процессор в режим ядра и передает управление ОС.

Затем есть код режима ядра, который обрабатывает запрошенный системный вызов. Он работает в отдельном стеке режима ядра (это не имеет никакого отношения к стеку режима пользователя). После обработки запроса - элемент управления возвращается к коду пользовательского режима. В зависимости от конкретного системного вызова адрес возврата в пользовательском режиме может быть тем, который вызвал транзакцию в режиме ядра, а также может быть другим адресом.

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

Теперь, если код режима пользователя, "вызванный из режима ядра", вызывает исключение - вот что должно произойти:

  1. Код пользовательского режима оболочки обрабатывает исключение SEH (то есть останавливает его распространение, но пока не выполняет разматывание стека).
  2. Передает управление в режим ядра (OS), как в обычном случае выполнения программы.
  3. Код режима Kenrel отвечает соответствующим образом. Завершает запрошенную услугу. В зависимости от того, было ли исключение пользовательского режима - обработка может отличаться.
  4. После возврата в режим пользователя - код режима ядра может указывать, было ли вложенное исключение. В случае исключения стек не восстанавливается в исходное состояние (поскольку раскрутки еще не было).
  5. Код пользовательского режима проверяет, не было ли такого исключения. Если это так - стек вызовов подделывается, чтобы включить вложенный вызов пользовательского режима, и исключение распространяется.

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

Другие вопросы по тегам