TL: DR: нет, есть некоторые угловые случаи SEH, которые могут сделать его небезопасным на практике, а также документироваться как небезопасный. @ Раймонд Чен недавно написал в блоге сообщение , которое вы, вероятно, должны прочитать вместо этого ответа.
Его пример ошибки ввода-вывода при сбое кода, который можно «исправить», предложив пользователю вставить компакт-диск и повторить попытку, также является моим выводом об единственной практически восстанавливаемой ошибке, если ее нет. t любые другие возможные неисправные инструкции между хранением и перезагрузкой ниже ESP / RSP.
Или, если вы попросите отладчик вызвать функцию в отлаживаемой программе, он также будет использовать стек целевого процесса.
В этом ответе есть список некоторых вещей, которые, как вы думаете, могут повлиять на память ниже ESP, но на самом деле этого не делают, что может быть интересно. Кажется, что только SEH и отладчики могут быть проблемой на практике.
Прежде всего, если вы заботитесь об эффективности, не можете ли вы избежать x87 в вашем соглашении о вызовах? movd xmm0, eax
- более эффективный способ вернуть float
, который был в целочисленном регистре. (И вы часто можете избежать перемещения значений FP в целочисленные регистры, во-первых, используя целочисленные инструкции SSE2, чтобы выделить экспоненту / мантиссу для log(x)
, или добавить целое число 1 для nextafter(x)
.) Но если вам нужно поддерживать очень старое оборудование, тогда вам нужна 32-битная версия x87 вашей программы, а также эффективная 64-битная версия.
Но есть и другие варианты использования для небольшого количества пустого места в стеке, где было бы неплохо сохранить пару инструкций, компенсирующих ESP / RSP.
Попытка собрать объединенную мудрость других ответов и обсуждения в комментариях под ними (и по этому ответу):
Он явно задокументирован как не безопасный от Microsoft: (для 64-битного кода я не нашел эквивалентного утверждения для 32-битного кода, но я уверен, что есть)
Использование стека (для x64)
Вся память за пределами текущего адреса RSP считается энергозависимой : ОС или отладчик могут перезаписать эту память во время сеанса отладки пользователем или обработчика прерываний.
Так что это документация, но указанная причина прерывания не имеет смысла для стека пространства пользователя, только для стека ядра. Важной частью является то, что они документируют это как не гарантировано безопасно, а не причины, приведенные.
Аппаратные прерывания не могут использовать пользовательский стек; это позволило бы пользовательскому пространству аварийно завершить работу ядра с mov esp, 0
, или, что еще хуже, захватить ядро, заставив другой поток в пользовательском пространстве изменять адреса возврата во время работы обработчика прерываний. Вот почему ядра всегда конфигурируют вещи, поэтому контекст прерывания помещается в стек ядра.
Современные отладчики работают в отдельном процессе и не являются «навязчивыми». В 16-разрядные дни DOS без многозадачной ОС с защищенной памятью, которая давала бы каждой задаче свое собственное адресное пространство, отладчики использовали бы тот же стек, что и отлаживаемая программа, между любыми двумя инструкциями при пошаговом выполнении.
@ RossRidge указывает, что отладчик может захотеть позволить вам вызвать функцию в контексте текущего потока , например с SetThreadContext
. Это будет работать с ESP / RSP чуть ниже текущего значения. Это, очевидно, может иметь побочные эффекты для отлаживаемого процесса (преднамеренно со стороны пользователя, запускающего отладчик), но блокирование локальных переменных текущей функции ниже ESP / RSP будет нежелательным и неожиданным побочным эффектом. (Таким образом, компиляторы не могут поместить их туда.)
(В соглашении о вызовах с красной зоной ниже ESP / RSP отладчик может учитывать эту красную зону, уменьшая ESP / RSP перед вызовом функции.)
Существуют программы, которые намеренно ломаются, когда их вообще отлаживают, и считают это функцией (для защиты от попыток обратного инжиниринга их).
Связанные : x86-64 System V ABI (Linux, OS X, все другие системы не Windows ) действительно определяет red-zone для код пользовательского пространства (только 64-разрядный): 128 байт ниже RSP, который гарантированно не будет асинхронно захвачен. Обработчики сигналов Unix могут работать асинхронно между любыми двумя инструкциями пользовательского пространства, но ядро учитывает красную зону, оставляя 128-байтовый пробел ниже старого RSP пользовательского пространства, если он использовался. При отсутствии установленных обработчиков сигналов вы получаете практически неограниченную красную зону даже в 32-битном режиме (где ABI не гарантирует красную зону). Сгенерированный компилятором код или код библиотеки, конечно, не может предполагать, что ничто другое во всей программе (или в библиотеке, вызываемой программой) не установило обработчик сигнала.
Таким образом, возникает вопрос: есть ли в Windows что-нибудь, что может асинхронно выполнять код, используя стек пользовательского пространства между двумя произвольными инструкциями? (т. е. любой эквивалент обработчика сигнала Unix.)
Насколько мы можем судить, SEH (Структурная обработка исключений) является единственным реальным препятствием для того, что вы предлагаете для кода пользовательского пространства на текущих 32 и 64 -бит Windows. (Но в будущем Windows может включать новую функцию.)
И я думаю, отладка, если вы случитесь, попросите ваш отладчик вызвать функцию в целевом процессе / потоке, как упомянуто выше.
В этом конкретном случае, не касаясь какой-либо другой памяти, кроме стека, или делать что-либо еще, что может вызвать ошибку, это, вероятно, безопасно даже из SEH.
SEH (Структурная обработка исключений) позволяет программному обеспечению пользовательского пространства иметь аппаратные исключения, такие как деление на ноль, доставленные несколько аналогично исключениям C ++. Они не являются действительно асинхронными: они для исключений, запускаемых по инструкциям, которые вы выполнили, а не для событий, которые произошли после некоторой случайной инструкции.
Но, в отличие от обычных исключений, обработчик SEH может возобновить с того места, где произошло исключение. (@RossRidge прокомментировал: обработчики SEH изначально вызываются в контексте развернутого стека и могут выбрать игнорирование исключения и продолжить выполнение в точке, где произошло исключение.)
Так что это проблема, даже если в текущей функции нет предложения catch()
.
Обычно исключения HW могут быть вызваны только синхронно. например с помощью инструкции div
или доступа к памяти, который может привести к ошибке с STATUS_ACCESS_VIOLATION
(Windows-эквивалент ошибки сегментации SIGSEGV в Linux). Вы контролируете, какие инструкции вы используете, поэтому вы можете избежать инструкций, которые могут неисправны.
Если вы ограничиваете свой код только доступом к памяти стека между хранилищем и перезагрузкой, и вы уважаете защитную страницу роста стека, ваша программа не будет ошибаться при доступе к [esp-4]
. (Если вы не достигли максимального размера стека (переполнение стека), в этом случае может произойти сбой и push eax
, и вы не сможете по-настоящему оправиться от этой ситуации, потому что нет места в стеке для использования SEH.)
Так что мы можем исключить STATUS_ACCESS_VIOLATION
как проблему, потому что, если мы получим это при доступе к памяти стека, мы все равно будем скрываться.
Обработчик SEH для STATUS_IN_PAGE_ERROR
может быть запущен перед любой инструкцией загрузки . Windows может вывести на экран любую страницу, которую хочет, и прозрачно вернуть ее обратно, если она понадобится снова ( подкачка виртуальной памяти). Но если есть ошибка ввода-вывода, ваша Windows пытается позволить вашему процессу обработать ошибку, предоставив STATUS_IN_PAGE_ERROR
Опять же, если это произойдет с текущим стеком, мы попадаем.
Но выборка кода может вызвать STATUS_IN_PAGE_ERROR
, и вы можете правдоподобно восстановиться после этого. Но не путем возобновления выполнения в том месте, где произошло исключение (если мы не можем каким-то образом переназначить эту страницу в другую копию в очень отказоустойчивой системе ??), поэтому мы все еще можем быть в порядке здесь.
Пейджинг ошибок ввода / вывода в коде, который хочет прочитать то, что мы сохранили ниже ESP, исключает любую возможность его прочтения. Если вы все равно не планируете этого делать, у вас все в порядке. Универсальный обработчик SEH, который не знает об этом конкретном фрагменте кода, в любом случае не будет пытаться это сделать. Я думаю, что обычно STATUS_IN_PAGE_ERROR
в большинстве случаев пытался бы напечатать сообщение об ошибке или, возможно, что-то записать в журнал, а не пытаться продолжить какие-либо вычисления.
Доступ к другой памяти между хранилищем и перезагрузка в память ниже ESP может вызвать STATUS_IN_PAGE_ERROR
для этой памяти. В библиотечном коде вы, вероятно, не можете предположить, что какой-то другой указатель, который вы передали, не будет странным, и вызывающая сторона ожидает обработки STATUS_ACCESS_VIOLATION
или PAGE_ERROR для него.
Текущие компиляторы не используют пространство ниже ESP / RSP в Windows, даже если они делают , используют красную зону в x86-64 System V (в конечных функциях, которые должны быть разлиты) / перезагрузите что-то, точно так же, как вы делаете для int -> x87.) Это потому, что MS говорит, что это небезопасно, и они не знают, существуют ли обработчики SEH, которые могут попытаться возобновить работу после SEH.
Вещи, которые, по вашему мнению, могут быть проблемой в текущей Windows, и почему они не:
Материал защитной страницы под ESP: если вы не заходите слишком далеко под текущим ESP, вы будете касаться защитной страницы и инициировать выделение большего количества стекового пространства вместо сбоя. Это нормально, если ядро не проверяет ESP пространства пользователя и не обнаруживает, что вы касаетесь пространства стека, не «зарезервировав» его сначала.
Восстановление ядром страниц ниже ESP / RSP: по-видимому, в настоящее время Windows этого не делает. Так что использование большого количества стекового пространства когда-либо сохранит эти страницы выделенными на весь оставшийся срок жизни вашего процесса, , если вы вручную VirtualAlloc(MEM_RESET)
их . (Ядро должно было бы разрешить , чтобы сделать это, хотя, поскольку в документах говорится, что память ниже RSP является энергозависимой. Ядро может эффективно обнулять ее асинхронно, если оно хочет, копируя при записи, отображая ее в ноль страница вместо записи в файл под давлением памяти.)
APC ( Асинхронные вызовы процедур ): они могут быть доставлены только тогда, когда процесс находится в «состоянии тревоги», что означает, что только внутри call
функция, подобная SleepEx(0,1)
. call
Функция уже использует неизвестное количество пространства ниже E / RSP, поэтому вы уже должны предполагать, что каждый call
забивает все, что находится ниже указателя стека. Таким образом, эти «асинхронные» обратные вызовы не являются действительно асинхронными по отношению к нормальному выполнению, как это делают обработчики сигналов Unix. (забавный факт: POSIX async io использует обработчики сигналов для выполнения обратных вызовов).
Обратные вызовы консольного приложения для ctrl-C и других событий (SetConsoleCtrlHandler
). Это выглядит точно как регистрация обработчика сигнала Unix, но в Windows обработчик работает в отдельном потоке со своим собственным стеком. ( См. Комментарий RbMm )
SetThreadContext
: другой поток может асинхронно изменять наш EIP / RIP, пока этот поток приостановлен, но вся программа должна быть написана специально для этого, чтобы иметь какой-либо смысл. Если это не отладчик, использующий его. Корректность, как правило, не требуется, когда какой-то другой поток возится с вашим EIP, если только обстоятельства не очень контролируемы.
И, очевидно, нет других способов, которыми другой процесс (или что-то, зарегистрированное в этом потоке) может инициировать выполнение чего-либо асинхронно относительно выполнения кода пользовательского пространства в Windows.
Если нет обработчиков SEH, которые могли бы попытаться возобновить работу, Windows более или менее имеет красную зону на 4096 байт ниже ESP (или, может быть, больше, если вы прикасаетесь к ней постепенно), но RbMm говорит, что никто не использует это на практике , Это неудивительно, потому что MS говорит, что нет, и вы не всегда можете знать, могли ли ваши абоненты что-то сделать с SEH.
Очевидно, что следует избегать всего, что могло бы синхронно его затереть (например, call
), также как и при использовании красной зоны в соглашении о вызовах System V. в x86-64. (Подробнее см. https://stackoverflow.com/tags/red-zone/info.)