Сегодня для вас у меня только хорошие новости.
Первая - я создал сайт своей команды!!! Адрес пока не даю, т.к. многое ещё "в разработке".
Вторая
новость - у моей команды новый Форум .
Ну и третья, которая на прямую относится к вам, я решил
спросить вашего совета относительно того как будут проходить наши дальнейшие уроки. Стоит ли
мне продолжать объяснять вам программирование на примерах, работу которых мы можем заметить только в отладчике,
либо вы хотите писать уже сейчас программы, которые проявляют себя как минимум простым окном-сообщением.
Пишите обязательно, мне очень важно ваше мнение. Ну а теперь за учебу!!!
Сперва я расскажу, что такое стек, и для чего он нужен.
Стек - это область памяти для временного хранения
произвольных данных. Но, ведь, мы используем для хранения данных переменные? Для чего же нам стек?
Давайте представим, что нам нужно сохранять большое количество переменных на очень мало время, на несколько
команд, и что для этого заводить переменную? Нет, конечно, ведь, тогда размер нашей программы будет
неоправданно большим. Удобство стека заключается в том, что его область используется многократно, причем
сохранение в стеке данных и выборка их отуда выполняется с помощью эффективных команд push и pop без
указания каких-либо имен. Вот вам и новые команды ;-). Способ их использования мы рассмотрим чуть ниже.
Основное назначение стека - это сохранение содержимого регистров,
для этого он использовался всегда,
используемых программой, перед вызовом подпрограммы, которая, в свою очередь, будет использовать регистры
процессора "в своих личных целях". Поясню, что это значит, представьте, у нас есть программа, которая
записывает в регистры какие-то значения, которые будут в последствие использоваться, но в данный момент
программа должна исполнить подпрограмму (относительно независимую часть программы), которая использует
эти же регистры, и естественно, записывает в эти регистры другие значения. И в итоге получится, что
программа будет продолжать свою работу, после возвращения из подпрограммы, с другими, "неправильными"
значениями. Чтобы этого избежать, необходимо значения, которые были в регистрах до вызова подпрограммы, сохранить в стеке.
Время шло, появилась ОС Windows, и у стека появилось
еще одно предназначение. Основой в программирование
под Windows, является использование использования специальных функций - API-функции. API - Application
Programming Interface ( прикладной программный интерфейс). Так вот, не вдаваясь в подробности, можно
сказать, данные, которые передаются этим функциям должны находиться в стеке. То есть,
API-функции, которые мы вызываем из своей программы, берут данные из стека для своего использования,
и по выполнению этих функций, будут происходить различные события (вывод окна с сообщением с различными
иконками и кнопками и надписями и т.д.). Конечно все особенности, ну или почти все ;-), мы рассмотрим позже.
Теперь перейдем к особенностям использования, и устройства стека.
Так как стек является областью памяти,
то у него должен быть адрес, по которому он начинается. Адрес начала стека находится в сегментном регистре
SS (Stak Segment register). В прошлом выпуске вы познакомились с сегментным регистром данных (DS),
аналогично ему SS является 16-битным не делимым регистром.
Особенностью стека является то, что у него есть дно и вершина.
Дно - это максимальный адрес стека.Вершина, соответственно, - наименьший адрес. Работа со стеком осуществляется по принципу LIFO
(Last In First Out) - последним пришел, первым вышел. Что это за принцип? Загрузка стека, а именно
так называется "помещение" чего-либо в стек, и выгрузка, "выемка", значений осуществляется следующим
образом. Представьте себе, у на есть глубокая коробка, которою мы постепенно наполняем чем-либо
(скажем, тарелками). Последняя положенная тарелка - "вершина стека", чтобы достать самую нижнюю
тарелку нам необходимо вынуть из коробки все тарелки, расположенные выше нее. Таким образом, получается,
первым пришел в стек (положили в коробку), последним вышел, и наоборот. Этот принцип необходимо запомнить
раз и навсегда, чтобы не было путаницы и проблем в последствии.
Возникает вопрос, как можно узнать адрес вершины стека,
т.е. адрес последнего положенного значения. Полный
адрес содержится в двух регистрах SS и ESP, когда хотят указать, что значение находится в паре регистров,
пишут так - SS:ESP. Что это за регистр ESP? Этот регистр относится к группе регистров общего назначения.
Вот его полное название ESP (Stack Pointer register) - регистр указатель стека. В этом регистре содержится
указатель вершины стека, проще говоря, смещение от начала стека.
Существует еще один регистр для работы со стеком - EBP (Base Pointer register) - регистр указателя базы кадра
стека (какое страшное название =) ). Он предназначен для произвольного доступа к данным внутри стека. Но
сегодня о нем мы говорить не будем, и так впереди еще уйма дел ;-).
Теперь плавно переходим от "жуткой" теории к "мистической" практике =).
Для загрузки какого-либо значения в
стек используется команда PUSH (Поместить, загрузить). Для выгрузки значения - POP (выгрузить). С помощью
команды PUSH в стек можно загрузить ячейку памяти, либо значение регистра, либо "нормальное" число .
Примерчик:
push X ; загрузить в стек значение переменной X
push eax ; загрузить в стек значение регистра eax
push 123456; загрузить в стек 123456
Приступим к написанию. программы.
Все как обычно, запускаем текстовой редактор, пишем в нем нижеприведенный код, сохраняем с расширением *.asm,
запускаем батник, вуаля, готовый исполняемый файл ;-).
.386
.model flat, stdcall
option casemap:none
.data
A02 dw 0ABEFh
A03 dd 012536789h
.code
start:
xor eax,eax
xor ebx,ebx ; Очищаем регистры
PUSH A02 ; Загружаем в стек значение переменной A02
PUSH A03 ; Загружаем в стек значение переменной A03
pop eax ; Выгружаем значение в регистр EAX
pop bx ; Выгружаем значение в регистр BX
ret
end start
Дальше тоже все как обычно, запускаем отладчик, загружаем в него нашу свежеиспеченную программу и… Посмотрите
на правый нижний угол, там вы увидите примерно следующее:
Если вы увидели эту табличку, значит, вы смотрите на окно стека =).
Самая верхняя строка этой таблички - вершина стека.
Посмотрите в окно регистров, находящееся прямо над окном стека, найдите в нем регистр ESP и посмотрите чему равно
его значение. Посмотрели? Значение регистра ESP равно - 0063FE3C. Смотрим адрес верхней строки в табличке и
убеждаемся, что эта строка действительно вершина стека.
Теперь совершаем уже хорошо знакомое вам действо - трассировку. Как обычно трассируем F8. После того как
мы сделали первый шаг, очистился регистр EAX, после второго шага - EBX. А теперь внимание, после третьего
шага посмотрите на окно стека, там вы увидите, что в стек загрузилось число B560ABEF. В принципе,
это и есть наше число (ABEF), но откуда же там еще и это - B560? Скажу честно, не знаю. Если кто-то из
подписчиков знает, прошу написать мне об этом, буду благодарен. Но для нас это не важно, т.к. не смотря
на то, что на вершине стека находится кроме нашего числа еще одно, работает все как и должно.
После еще одного шага в стек загружается значение переменной
A02, тут все в порядке ;-), на вершине стека мы
видим число 12536789. Теперь, у нас после следующего шага в регистр eax должно загрузить число с вершины
стека, то есть, то которое было загружено последним. Делаем шаг и видим, что значение регистра EAX стало
12536789. После последнего шага в регистр bx должно загрузиться число с вершины стека, а на вершине стека
у нас теперь число, которое мы положили первым - ABEF. Смотрим, действительно значение регистра ebx теперь
0000ABEF.
А теперь об одном "подводном камне", мы для выхода из программы
использовали ассемблерную команду ret. Эта
команда выгружает число с вершины стека и осуществляет переход на адрес, равный выгруженному числу из стека.
Когда мы не работали со стеком, то на его вершине всегда находился адрес функции выхода из программы. Но когда
мы загружаем числа в стек, должны помнить, если мы не выгрузим все загруженные нами числа из стека, то команда
ret осуществит переход не на адрес функции выхода, а чем это может закончится, представьте сами 8-) . Как
минимум Окошки зависнут ;-).
На самом деле, есть специальная API- функция выхода из программы
- ExitProcess .
Но мы её пока не используем, так как мы не переходили к изучению API-функций, да и размер программ стал бы больше
;-), а для ассемблерщика каждый байт на счету. Когда мы будем писать настоящие программы с пользовательским
интерфейсом для завершения программы мы будем использовать ExitProcess. Конечно, можно использовать и ret,
для того чтобы сэкономить несколько байт, но тогда мы не сможем с уверенность сказать, что наша программа
на этапе тестирования корректно завершиться ;-). Хотя если программу оптимизируют, то часто ExitProcess заменяют ret.
После небольшого лирического отступления давайте перейдем к написанию еще одной программы.
Как я уже писал, стек используется при вызове API-функций и
подпрограмм. Но первое и второе мы отложим до
"лучших времен", а сейчас напишем программу для закрепления полученных знаний по работе со стеком. Программа
элементарная, но очень показательная. Мы загрузим пять чисел в стек, а потом их будем выгружать в регистр eax.
Ну, что же, приступим.
.386
.model flat, stdcall
option casemap:none
.code
start:
xor eax,eax ; Очищаем регистр eax
PUSH 1 ; Загружаем в стек 1
PUSH 2 ; Загружаем в стек 2
PUSH 3 ; Загружаем в стек 3
PUSH 4 ; Загружаем в стек 4
PUSH 5 ; Загружаем в стек 5
pop eax ; Выгружаем 5 в регистр EAX
pop eax ; Выгружаем 4 в регистр EAX
pop eax ; Выгружаем 3 в регистр EAX
pop eax ; Выгружаем 2 в регистр EAX
pop eax ; Выгружаем 1 в регистр EAX
ret
end start
В принципе, все должно быть понятно. В этом примере хорошо демонстрируется принцип LIFO. Порядок получения программы,
я не буду повторять, вы уже его хорошо запомнили, надеюсь на это =). Исполняемый модуль (он же программа, он же exe'шник)
загружаем в отладчик и начинаем трассировать. До выполнения первой команды pop смотрим в окно стека, а после в окно
регистров. Итак, после первой команды push в стек загружено число 1 (я не указал в какой с.с. это число, т.к. единица
она и в двоичной системе единица =), смотрим на вершину стека и видим там такую "картину"
0063FE38 00000001
А что это значит? А значит это, что на вершине стека находится число 1. Прежде чем продолжить рассмотрение дальнейшей
работы программы, хотел отметить одну, вещь.
Если мы при написании в исходном тексте программы после чисел не ставим "h" или "b", то числа считаются десятичными.
Продолжим.
После второй команды push на вершине стека оказывается число 2. Все как мы и предполагали ;-). Остановив трассировку
на первой команде pop, мы обращаем внимание на окно регистров, а именно на регистр eax. По идее, сейчас в регистр eax
должно загрузиться число расположенное на вершине стека - 00000005. Жмем F8, смотрим на регистр eax, и убеждаемся в
правоте своей догадки. Делаем еще шаг, в регистре eax число 4. После последней команды pop в регистре eax находится
1. Ну что же, все как и предполагали - работает принцип LIFO.
Для работы со стеком существует больше ассемблерных команд, с ними мы познакомимся позже, гораздо позже, т.к. они нам
нескоро пригодятся.
Если вы хотите что-то спросить по ассемблеру, крэку, или просто поболтать прошу на Форум моей команды ,
там вы сможете получить ответы от меня, и моих товарищей Mafia32, formatC
Вы можете отправить письмо на мой почтовый ящик , только в том случае если вопрос имеет отношение к рассылке.
Обязательно заполняйте поле "Тема", письма без темы, я не буду читать. Для вашего удобства я разместил в рассылке e-mail
форму, вы можете прямо из нее отправлять свое письмо, но для этого должна быть настроена ваша почтовая программа.
Копирайты
Вся информация, содержащаяся в рассылках, является интеллектуальной собственностью своих законных авторов.
Перепечатка и распространение материалов рассылки только с разрешения автора.