Это была очень простая задачка. Видимо, сложность её решения заключается исключительно в не самой частой ситуации на практике.
Итак, давайте посмотрим: в задачке участвует две простые процедуры, одна из которых вызывает вторую. Тут нет опосредованного вызова, стоит простой прямой call.
Что такое call? Грубо говоря, это - две инструкции:
asm // CALL DoSomething : PUSH X // где X - адрес (число) инструкции после JMP JMP DoSomething end;
Если вы посмотрите на условия задачи, то увидите, что они явно говорят, что проблема не в
JMP
. Остаётся тогда простой PUSH
.Т.е. задача сводится к вопросу, когда помещение значения в стек может приводить к Access Violation? Разумеется, в обычных условиях, не предполагая "внешнего вредительства". Добавьте к этому подсказку, что ответ связан с предыдущей задачей.
Подумайте, это ваш последний шанс. Вы можете освежить матчасть.
Ну, хорошо, если вы ещё не догадались, то вот ответ. Как вы должны знать из матчасти, стек потока ограничен сверху и снизу двумя страницами с атрибутом PAGE_NOACCEESS - т.е. это две зарезервированные страницы, которые защищают вас от выхода за границы стека. Любое обращение к ним приводит к Access Violation. Именно этим задачка связана с предыдущей: дело всё в атрибутах защиты страниц.
Но это ещё не полный ответ. С чего бы вызов функции приводил к обращению за границы стека? Как вы снова должны знать из матчасти - стек изначально пуст (занимает только одну страницу памяти, не считая ограничителей), а сверху него (снизу, если смотреть по карте памяти) расположена страница с PAGE_GUARD, называемая "сторожевой". Когда вы используете стек и исчерпаете первую страницу, вы обратитесь к сторожевой странице. При этом возбуждается системное исключение, которое обрабатывается системой путём смещения сторожевой страницы далее и выделения памяти по месту её старого размещения. Теперь, когда сторожевая страница памяти доходит до конца стека (по умолчанию - 1 Мб), то на последнем шаге система убирает сторожевую страницу и возбуждает исключение Stack Overflow. И если программа продолжит потреблять память и после этого, то она дойдёт до конца стека и попытается обратиться к странице с PAGE_NOACCESS, т.к. сторожевой страницы уже нет. Учтите ещё, что стек программы умеет только расти, но не уменьшаться.
Вот вам и ответ на задачку - второе переполнение стека после первого переполнения стека. Вот простой пример для иллюстрации:
procedure P; begin P; end; procedure TForm1.Button1Click(Sender: TObject); begin P; end;Нажмите на кнопку. Вы получите сообщение о Stack overflow. Закройте сообщение и нажмите кнопку второй раз - вы получите Access Violation, о котором и говорилось в задачке.
Примечание: вы не сможете увидеть сообщение о Access Violation. Показ сообщения подразумевает вызов функций (обработчика исключения, MessageBox и т.п.), что невозможно, т.к. стека у нас уже нет. Поэтому програма аварийно завершается. Код исключения (access violation) можно увидеть в системном логе. Ну а если программа запущена под отладчиком, то вы можете увидеть уведомление отладчика (но не сообщение от программы). Отладчик скажет вам:
--------------------------- Debugger Fault Notification --------------------------- Project Project1.exe faulted with message: 'access violation at 0x004ae4b4: write of address 0x00090ffc'. Process Stopped. Use Step or Run to continue. --------------------------- OK ---------------------------Ключевое слово здесь "faulted". Т.е. это фатальное исключение, приводящее к закрытию программы, оно не обрабатывается программой (сравните это с "Project ... raised exception".
Александр, Браво!
ОтветитьУдалитьБыла догадка в вышеописанную сторону с атрибутом PAGE_NOACCEESS, но меня смутила ссылка на связь с пред. задачей, где имеет место быть четкое указание на JMP, + опосредственное знание asm.
Старею, наверное... :)
Классика срыва стека на переполнении - модифицируем вниз (правда обычно сразу выставляют нужный регистр и делают вызов с возвратом в снятую с протекта область).
ОтветитьУдалитьА вот так делается выход через убиение SEH фреймов (модифицируем вверх):
procedure Test;
var
D: Pointer;
begin
FillChar(D, MaxInt, 0);
end;
begin
Test;
end.
тут когда сработало AV уже поздно пить боржоми :)
Кажется об особенностях выделения памяти под стек я читал именно тут :) Только, засомневался: неужели на такую обыденную операцию отладчик будет реагировать? А вот про ситуацию окончания стека как то не подумал.
ОтветитьУдалитьP.S. Я Torbins, а Blogger - глюк.
Однако, давненько не было свеженьких постов. Задачку, что ли, подкинули бы, развлечения ума ради)
ОтветитьУдалитьК сожалению, крайне сейчас занят.
ОтветитьУдалить