Допустимо ли писать ниже ESP? - PullRequest
       104

Допустимо ли писать ниже ESP?

0 голосов
/ 10 сентября 2018

Для 32-разрядных приложений Windows допустимо ли использовать стековую память ниже ESP для временного пространства подкачки без явного уменьшения ESP?

Рассмотрим функцию, которая возвращает значение с плавающей запятой в ST(0). Если наше значение в настоящее время в EAX, мы бы, например,

PUSH   EAX
FLD    [ESP]
ADD    ESP,4  // or POP EAX, etc
// return...

Или, не изменяя регистр ESP, мы могли бы просто:

MOV    [ESP-4], EAX
FLD    [ESP-4]
// return...

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

Несмотря на реальную необходимость сохранения этого значения в стеке (проблемы с повторным входом, вызовы функций между PUSH чтением и возвратом значения и т. Д.), Существует какая-либо фундаментальная причина, по которой запись в стек ниже ESP, подобная этой, была бы недопустимой ?

Ответы [ 4 ]

0 голосов
/ 11 сентября 2018

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 ) действительно определяет для код пользовательского пространства (только 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.)

0 голосов
/ 10 сентября 2018

в общем случае ( x86 / x64 платформа) - прерывание может быть выполнено в любое время, которое перезаписывает указатель стека ниже памяти (если он выполнен в текущем стеке). потому что это, даже временное сохранение чего-то ниже указателя стека, недопустимое в режиме ядра - прерывание будет использовать текущий стек ядра. но в пользовательском режиме ситуация другая - Windows создает таблицу прерываний (IDT) таким образом, чтобы при возникновении прерывания она всегда выполнялась в режиме ядра и в стеке ядра. в результате стек режима пользователя (ниже указателя стека) не будет затронут. и возможно временно использовать некоторое пространство стека под указателем, пока вы не выполните никаких вызовов функций. если исключение будет (скажем, по недействительному адресу доступа) - также будет перезаписан указатель стека ниже, - исключение процессора, конечно, начнется в режиме ядра и в стеке ядра, но затем ядро ​​выполнит обратный вызов в пространстве пользователя через ntdll.KiDispatchExecption уже в текущем стеке пространство. в общем, это действует в пользовательском режиме Windows (в текущей реализации), но вам нужно хорошо понимать, что вы делаете. Однако это очень редко, я думаю, используется


конечно, насколько правильно отмечено в комментариях, что мы можем в windows пользовательском режиме писать ниже указателя стека - это просто текущее поведение реализации. это не задокументировано или не гарантировано.

но это очень фундаментально - вряд ли будет изменено: прерывания всегда будут выполняться только в привилегированном режиме ядра. и режим ядра будет использовать только стек режима ядра. контекст пользовательского режима вообще не является доверенным. что будет, если программа пользовательского режима установит неверный указатель стека? скажем mov rsp,1 или mov esp,1? и сразу после этой инструкции прерывание будет возбуждено. что будет, если он начнет выполняться на таких недопустимых esp / rsp? вся операционная система просто рухнула. именно потому, что это прерывание будет выполняться только в стеке ядра. и не перезаписывать пространство стека пользователя.

также необходимо учитывать, что стек ограничен (даже в пользовательском режиме), доступ к нему ниже 1 страницы (4 КБ) уже является ошибкой (необходимо выполнить проверку стека постранично, для перемещения защитной страницы вниз).

и, наконец, действительно нет необходимости обычно обращаться к [ESP-4], EAX - в чем проблема декремента ESP сначала? даже если нам нужно пространство стека доступа в цикле, огромное количество времени - указатель стека декремента требуется только один раз - 1 дополнительная инструкция (не в цикле) ничего не меняет ни в производительности, ни в размере кода.

, поэтому, несмотря на то, что формально это будет работать корректно в режиме пользователя Windows, лучше (и не нужно) использовать это


конечно в официальной документации сказано:

Использование стека

Вся память за пределами текущего адреса RSP считается энергозависимой

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


возможно в будущих окнах и добавить «прямой» apc или некоторые «прямые» сигналы - некоторый код будет выполнен с помощью обратного вызова сразу после входа потока в ядро ​​(во время обычного аппаратного прерывания). после этого все ниже esp будет неопределенным. но пока этого не существует. пока этот код не будет работать всегда (в текущих сборках) правильно.

0 голосов
/ 10 сентября 2018

Когда создается поток, Windows резервирует непрерывную область виртуальной памяти настраиваемого размера (по умолчанию 1 МБ) для стека потока. Первоначально стек выглядит следующим образом (стек растет вниз):

--------------
|  committed |
--------------
| guard page |
--------------
|     .      |
| reserved   |
|     .      |
|     .      |
|            |
--------------

ESP будет указывать где-то внутри зафиксированной страницы. Защитная страница используется для поддержки автоматического роста стека. Область зарезервированных страниц обеспечивает доступность запрошенного размера стека в виртуальной памяти.

Рассмотрим две инструкции из вопроса:

MOV    [ESP-4], EAX
FLD    [ESP-4]

Есть три варианта:

  • Первая инструкция успешно выполнена. Ничто не использует стек пользовательского режима, который может выполняться между двумя инструкциями. Таким образом, вторая инструкция будет использовать правильное значение (@RbMm указал это в комментариях под своим ответом, и я согласен).
  • Первая инструкция вызывает исключение, а обработчик исключений не возвращает EXCEPTION_CONTINUE_EXECUTION. Пока вторая инструкция находится сразу после первой (она не находится в обработчике исключений или не помещена после него), вторая инструкция не будет выполняться. Так что ты все еще в безопасности. Выполнение продолжается со стекового фрейма, где существует обработчик исключений.
  • Первая инструкция вызывает исключение, а обработчик исключений возвращает EXCEPTION_CONTINUE_EXECUTION. Выполнение продолжается с той же инструкции, которая вызвала исключение (возможно, с контекстом, измененным обработчиком). В этом конкретном примере первый будет выполнен повторно, чтобы записать значение ниже ESP. Нет проблем. Если вторая инструкция вызвала исключение или существует более двух инструкций, то исключение может возникнуть после того, как значение будет записано ниже ESP. Когда вызывается обработчик исключения, он может перезаписать значение и затем вернуть EXCEPTION_CONTINUE_EXECUTION. Но когда выполнение возобновляется, записанное значение, как предполагается, все еще там, но это больше не так. Это ситуация, когда писать ниже ESP небезопасно. Это применимо, даже если все инструкции размещены последовательно. Спасибо @RaymondChen за указание на это.

Как правило, если две инструкции не помещаются вплотную, если вы пишете в места за пределами ESP, нет гарантии, что записанные значения не будут повреждены или перезаписаны. Один случай, который я могу придумать, где это может произойти, - это структурированная обработка исключений (SEH). Если возникает аппаратное исключение (например, деление на ноль), обработчик исключений ядра будет вызван (KiUserExceptionDispatcher) в режиме ядра, который вызовет сторону обработчика в пользовательском режиме (RtlDispatchException). При переключении из режима пользователя в режим ядра, а затем обратно в режим пользователя, любое значение, которое было в ESP, будет сохранено и восстановлено. Однако сам обработчик пользовательского режима использует стек пользовательского режима и будет перебирать зарегистрированный список обработчиков исключений, каждый из которых использует стек пользовательского режима. Эти функции изменят ESP по мере необходимости. Это может привести к потере значений, которые вы написали за пределами ESP. Аналогичная ситуация возникает при использовании программно-определяемых исключений (throw в VC ++).

Я думаю, что вы можете справиться с этим, зарегистрировав свой собственный обработчик исключений перед любыми другими обработчиками исключений (чтобы он вызывался первым). Когда ваш обработчик вызывается, вы можете сохранить ваши данные за пределами ESP в другом месте. Позже, во время раскручивания, вы получаете возможность cleanup восстановить ваши данные в том же месте (или любом другом месте) в стеке.

Необходимо также следить за асинхронными вызовами процедур (APC) и обратными вызовами.

0 голосов
/ 10 сентября 2018

В целом (не относится конкретно к какой-либо ОС);писать ниже ESP небезопасно, если:

  • Возможно прерывание кода, и обработчик прерываний будет работать с тем же уровнем привилегий.Примечание: Это обычно очень маловероятно для кода «пользовательского пространства», но чрезвычайно вероятно для кода ядра.

  • Вы вызываете любой другой код (где используется либо call, либо стеквызываемая подпрограмма может уничтожить данные, которые вы сохранили ниже ESP)

  • Что-то еще зависит от "нормального" использования стека.Это может включать в себя обработку сигналов, (на основе языка) разматывание исключений, отладчики, «защиту от разрушения стека»

Можно писать ниже ESP, если это не «небезопасно».

Обратите внимание, что для 64-битного кода запись ниже RSP встроена в ABI x86-64 («красная зона»);и становится безопасным благодаря поддержке в цепочках инструментов / компиляторах и во всем остальном.

...