Продолжение вчерашнего разговора:
Архитектура x86 традиционно использует регистр EBP для установки стекового фрейма. Типичный пролог функции выглядит вот так:
push ebp ; сохранить старый ebp mov ebp, esp ; установить новый ebp sub esp, nn*4 ; выделить место под локальные переменные push ebx ; надо сохранить для вызывающего push esi ; надо сохранить для вызывающего push edi ; надо сохранить для вызывающегоЭтот код создаёт новый стековый фрейм, которой для stdcall-функции с двумя параметрами выглядит вот так:
.. другие данные .. параметр_2 параметр_1 адрес_возврата сохранённый EBP <- EBP локальная_переменная_1 локальная_переменная_2 ... локальная_переменная_nn сохранённый EBX сохранённый ESI сохранённый EDI <- ESPК параметрам функции можно получить доступ, используя положительные смещения от EBP; например, параметр_1 лежит по адресу [ebp+8]. Локальные переменные имеют отрицательное смещение от EBP; например, локальная_переменная_2 - это [ebp-8].
Теперь предположим, что произошла путаница соглашений вызова, так что в стеке остаётся лишний мусор:
.. другие данные .. параметр_2 параметр_1 адрес_возврата сохранённый EBP <- EBP локальная_переменная_1 локальная_переменная_2 ... локальная_переменная_nn сохранённый EBX сохранённый ESI сохранённый EDI мусор мусор <- ESPСама функция не ощущает никаких изменений. Параметры всё так же доступны по тем же положительным смещениям от EBP, а локальные переменные всё ещё доступны по тем же отрицательным смещениям.
Настоящие проблемы начнут возникать, когда придёт время очистки и выхода. Посмотрим на эпилог функции:
pop edi ; восстановим для вызывающего pop esi ; восстановим для вызывающего pop ebx ; восстановим для вызывающего mov esp, ebp ; удаляем локальные переменные pop ebp ; восстановим для вызывающего retd 8 ; возврат из функции с очисткой стекаВ нормальном стеке три инструкции "pop" соответствуют данным, размещённым в стеке тремя последними инструкциями "push" и никто при этом не пострадает. Но сейчас у нас в стеке лежит мусор, поэтому "pop edi" на самом деле загрузит в регистр EDI какой-то мусор, так же, как и "pop esi". А инструкция "pop ebx" (которая считает, что она восстанавливает значение EBX) на самом деле загрузит исходное значение, которое было в EDI в регистр EBX. Но потом инструкция "mov esp, ebp" исправит стек, поэтому команды "pop ebp" и "retd" выполнятся нормально - так же, как и при корректном стеке.
Что сейчас у нас произошло? Кажется, что все вещи вернулись на свои места. Ну за исключением того, что регистры ESI, EDI и EBX оказались испорченными. Если вам повезло (прим. пер.: или не повезло - это ещё как посмотреть), то значения в ESI, EDI и EBX не были важны вызывающему, и выполнение продолжится как ни в чём не бывало. Или вызывающему было важно только, не ноль ли значение регистра, а вы заменили одно не нулевое значение на другое. В любом случае, порча этих трёх регистров не становится видной сразу, и вы никогда не осознаете, что же вы сделали не так.
Может быть, повреждение регистров окажет побочный эффект (к примеру, вы изменили значение с нуля на не ноль, что привело к тому, что вызывающий пошёл не по тому пути выполнения), но настолько смазанный, что вы его не заметите, поэтому вы выбрасываете программу на рынок, закатываете вечеринку и переходите к следующему проекту.
А потом выходит новый компилятор, например такой, который поддерживает оптимизацию FPO.
FPO расшифровывается как "frame pointer omission" (пропуск фреймового указателя) - функция перестаёт использовать регистр EBP как указатель фрейма и вместо этого использует его, как любой другой регистр. На x86, который имеет относительно мало регистров, дополнительный арифметический регистр будет большим бонусом.
С FPO пролог функции теперь выглядит вот так:
sub esp, nn*4 ; локальные переменные push ebp ; надо сохранить для вызывающего push ebx ; надо сохранить для вызывающего push esi ; надо сохранить для вызывающего push edi ; надо сохранить для вызывающегоИтоговый стек выглядит вот так:
.. другие данные .. параметр_2 параметр_1 адрес_возврата локальная_переменная_1 локальная_переменная_2 ... локальная_переменная_nn сохранённый EBP сохранённый EBX сохранённый ESI сохранённый EDI <- ESPТеперь все данные (параметры и локальные переменные) доступны по смещениям относительно регистра ESP. Например, локальная_переменная_nn это [esp+$10].
В таких условиях мусор в стеке становится гораздо более фатальным. Эпилог функции будет таким:
pop edi ; восстановим для вызывающего pop esi ; восстановим для вызывающего pop ebx ; восстановим для вызывающего pop ebp ; восстановим для вызывающего add esp, nn*4 ; удаляем локальные переменные retd 8 ; возврат из функции с очисткой стекаЕсли в стеке есть мусор, то четыре инструкции "pop", как и ранее, восстановят неверные значения, но в этот раз очистка локальных переменных ничего не исправит. Команда "add esp, nn*4" подправит стек на значение, которое функция считает правильным, но поскольку в стеке лежит мусор, то в итоге указатель стека будет неверен:
.. другие данные .. параметр_2 параметр_1 адрес_возврата локальная_переменная_1 локальная_переменная_2 <- ESP (упс!)Инструкция "retd 8" теперь попробует передать управление вызывающему, но вместо этого она перейдёт по адресу, который записан в локальная_переменная_2, который, вероятно, не будет являться указателем на корректный код.
Итак, вот вам и пример, когда оптимизация вашего кода (вызываемой функции) выявляет ошибки других людей (неверная сигнатура у вызывающего).
В следующий раз я дам более тонкий пример того, что может пойти не так, если вы используете неверную сигнатуру callback-функции.
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.