Показаны сообщения с ярлыком stack. Показать все сообщения
Показаны сообщения с ярлыком stack. Показать все сообщения

пятница, 29 мая 2009 г.

Опять играем со стеком

По ходу экспериментов над нашим мутантом, пришла в голову мысль создать еще один штамм мутантика. Идея состояла в следующем: изменить непосредственно в стеке адрес возврата из подпрограммы, так чтобы подпрограмма вернула управление не на следующую команду после вызвавшей ее команды call, а на команду, указанную нами. Для этого, конечно, пришлось немного забежать вперед по тексту учебника, чтобы найти подходящие для реализации этого финта ушами команды (я так подозреваю, что не самые оптимальные, но замысел удался! :) Вот наш новый мутантик:

Теперь смотрим листинг:

Погружаемся в медитацию...
Команда call subp теперь имеет код E8FFE4. Складываем FFE4h+1Ch получаем 0 (проверьте в калькуляторе Windows предварительно выставив длину 2 байта). Команда jmp finish имеет вид EBF7, где собственно F7 отрицательное (в данном случае) смещение равное -9. То есть эта команда вычитает 9 из следующего после нее адреса (вернее она складывает шестнадцатеричные значения). Проверяем 25h+F7=1C (проверьте в калькуляторе Windows, предварительно выставив длину 1 байт). Почему выставляем длину, то один, то два байта – это тема для медитации :)
Теперь посмотрим прогу в hiew...

Внимательно медитируем на стрелочки :) И далее погружаемся в глубокую медитацию на прогу в отладчике...

Скриншот сделан после выполнения команды call subp (call 0000). Обращаем внимание, что адрес возврата из подпрограммы помещен в стек и равен 001C, как и должно быть. Дебагим нашу прогу дальше до команды ret (ее не выполняем):

Как видим, наш код поменял непосредственно в стеке адрес возврата из подпрограммы, поэтому команда ret извлечет из стека уже не адрес 001С, а адрес 0021, передав таким образом управление уже на этот адрес.

Ну и приведем вывод нашей программы:

C:\ASM\HELLO\TASM>hello10
-=* Hello World *=--=* Hello World *=--=* Hello World *=-
C:\ASM\HELLO\TASM>

четверг, 28 мая 2009 г.

Игры со стеком

Еще немного поиграем со стеком и подпрограммами, чтобы лучше усвоить как работает стек и передается управление на подпрограммы. Для этого выведем новый штамм нашей мега программы:

Как видно из скрина, мы переместили подпрограмму в начало сегмента кода. Посмотрим теперь на листинг:

Как видим, машинный код команды call subp изменился, теперь он имеет вид - E8FFEE, где FFEE является смещением на начало подпрограммы. Складываем адрес следующей команды, после команды call с этим смещением 12h+FFEEh=0. Можете проверить это в калькуляторе Windows, только не забудьте выставить длину в ДВА БАЙТА (Почему именно в два байта? Ну помедитируйте на это :) ). Здесь мы опять встретилсь с явлением циклического возврата или оборачивания адреса. Помедитируем на это немного и дальше в путь... (Подсказка для медитирующих: 12h=18d и это как раз количество команд отделяющих адрсе 12h до начала нашей подпрограммы. Вобщем медитируйте...)
Далее на нашем пути hiew...

Hiew, надо отметить, преполезнейшая вещь.
Ну и теперь отладчик :)

Этот скрин сделан еще до выполнения первой команды. Обращаем внимание на точку входа в программу и на состояние регистров SS и SP. Следующий скрин сделан уже после входа в подпрограмму, но еще до выполнения первой ее команды. Смотрим, медитируем, просветляемся...

среда, 27 мая 2009 г.

Стек и подпрограммы

В своем изучении ассемблера, я немного отклонился от линии начертанной великим Питерем Аблем, но я думаю он на меня не обидется :) Еще немного поэкспериментируем со стеком и подпрограммами. Немного практики ни когда не повредит, а потом вернемся к генеральной линии партии... ммм.... вернее Питера.... Абеля....

Итак, наша програ продолжает мутировать:

Оператор CALL вызывает подпрограмму subp, которую мы определили с помощью соответствующих директив PROC и ENDP. Оператор RET осуществляет возврат из подпрограммы в головную программу.
Взглянем на листинг:

Внимательно медитируем на листинг...
Команда call subp имеет машинный код E80005, где собственно E8 – это сама команда, а 0005 – это смещение, указывающее на первую команду нашей подпрограммы. Команда call уменьшает значение SP на два, помещает в стек адрес, следующей за ней команды (в нашем случае адрес команды mov ax,4c00h равный 000F). Затем вычисляет адрес команды подпрограммы, на которую надо передать управление, прибавляя к адресу следующей за ней команды смещение (положительное или отрицательное) равное количеству байт программы, до первой команды вызываемой подпрограммы – в нашем случае 5 байт (000F+5=0014) и помещает это число (в нашем случае 0014) в регистр IP, передавая таким образом управление на подпрограмму. Подпрограмма выполняется до команды RET, которая извлекает из стека адрес возврата из подпрограммы (в нашем случае 000F) и помещает его в регистр IP (передавая, таким образом, управление вызвавшей ее программе) и уменьшает значение SP на два.
И, традиционно, помедитируем на код сгенерированный ассемблером:

Посмотреть на программу в hiew, так же не повредит:

Ну и, святое дело, дебагер:

Скриншот сделан еще до выполнения первой команды. Внимательно медитируем и жмем пять раз F8, то есть останавливаемся на команде call (ее не выполняем).

Из скриншота видно, что отладчик не плохо поорудовал в стеке, но при этом значение регистра SP осталось прежним, и он по прежнему указывает на дно стека (вернее на первое слово под ним). Делаем выводы и далее жмем один раз F7, чтобы войти в подпрограмму.

Как, уже говорилось, команда call уменьшила содержимое регистра SP на два (0040-2=003E), поместила в стек адрес следующей за ней команды (000F), прибавила к адресу следующей за ней команды смещение до первой исполняемой команды подпрограммы (000F+5=14h) и поместила это значение (14h) в регистр IP.
Далее жмем два раза F8.

Команда ret, извлекла из стека адрес возврата из подпрограммы (000F) и поместила его в регистр IP, а затем увеличила значение регистра SP на два, вернув указатель стека, опять на его дно.
Ну и покажем вывод нашей мега программы. Она два раза выводит знаменитую фразочку, но без перевода каретки на новую строку (так как мы это не запрограммировали.

C:\ASM\HELLO\TASM>hello8
-=* Hello World *=--=* Hello World *=-
C:\ASM\HELLO\TASM>

понедельник, 25 мая 2009 г.

Стек

Стеком называют область программы для временного хранения произвольных данных. Разумеется, данные можно сохранять и в сегменте данных, однако в этом случае для каждого сохраняемого на время данного надо заводить отдельную именованную ячейку памяти, что увеличивает размер программы и количество используемых имен. Удобство стека заключается в том, что его область используется многократно, причем сохранение в стеке данных и выборка их оттуда выполняется с помощью эффективных команд push и pop без указания каких-либо имен.
Стек традиционно используется, например, для сохранения содержимого регистров, используемых программой, перед вызовом подпрограммы, которая, в свою очередь, будет использовать регистры процессора "в своих личных целях". Исходное содержимое регистров извлекается из стека после возврата из подпрограммы. Другой распространенный прием - передача подпрограмме требуемых ею параметров через стек. Подпрограмма, зная, в каком порядке помещены в стек параметры, может забрать их оттуда и использовать при своем выполнении.
Отличительной особенностью стека является своеобразный порядок выборки содержащихся в нем данных: в любой момент времени в стеке доступен только верхний элемент, т.е. элемент, загруженный в стек последним. Выгрузка из стека верхнего элемента делает доступным следующий элемент.
Элементы стека располагаются в области памяти, отведенной под стек, начиная со дна стека (т.е. с его максимального адреса) по последовательно уменьшающимся адресам. Адрес верхнего, доступного элемента хранится в регистре-указателе стека SP. Как и любая другая область памяти программы, стек должен входить в какой-то сегмент или образовывать отдельный сегмент. В любом случае сегментный адрес этого сегмента помещается в сегментный регистр стека SS. Таким образом, пара регистров SS:SP описывают адрес доступной ячейки стека: в SS хранится сегментный адрес стека, а в SP - смещение последнего сохраненного в стеке данного (смотрим рисунок). Обратите внимание на то, что в исходном состоянии указатель стека SP указывает на ячейку, лежащую под дном стека и не входящую в него.

а - исходное состояние, б - после загрузки одного элемента (в данном примере - содержимого регистра АХ), в - после загрузки второго элемента (содержимого регистра DS), г - после выгрузки одного элемента, д - после выгрузки двух элементов и возврата в исходное состояние.

Загрузка в стек осуществляется специальной командой работы со стеком push (протолкнуть). Эта команда сначала уменьшает на 2 содержимое указателя стека (SP), а затем помещает операнд по адресу в SP. Если, например, мы хотим временно сохранить в стеке содержимое регистра АХ, следует выполнить команду

push АХ

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

push DS

переведет стек в состояние, показанное на рис. в. В стеке будут теперь храниться два элемента, причем доступным будет только верхний, на который указывает указатель стека SP. Если спустя какое-то время нам понадобилось восстановить исходное содержимое сохраненных в стеке регистров, мы должны выполнить команды выгрузки из стека pop (вытолкнуть):

pop DS
pop AX

Состояние стека после выполнения первой команды показано на рис. г, а после второй - на рис. д. Для правильного восстановления содержимого регистров выгрузка из стека должна выполняться в порядке, строго противоположном загрузке - сначала выгружается элемент, загруженный последним, затем предыдущий элемент и т.д.
Совсем не обязательно при восстановлении данных помещать их туда, где они были перед сохранением. Например, можно поместить в стек содержимое DS, а извлечь его оттуда в другой сегментный регистр - ES;

push DS
pop ES ; Теперь ES=DS, а стек пуст

Это распространенный прием для перенесения содержимого одного регистра в другой, особенно, если второй регистр - сегментный.
Обратите внимание (см. рисунок) на то, что после выгрузки сохраненных в стеке данных они физически не стерлись, а остались в области стека на своих местах. Правда, при "стандартной" работе со стеком они оказываются недоступными. Действительно, поскольку указатель стека SP указывает под дно стека, стек считается пустым; очередная команда push поместит новое данное на место сохраненного ранее содержимого АХ, затерев его. Однако пока стек физически не затерт, сохраненными и уже выбранными из него данными можно пользоваться, если помнить, в каком порядке они расположены в стеке. Этот прием часто используется при работе с подпрограммами.
Какого размера должен быть стек? Это зависит от того, насколько интенсивно он используется в программе. Если, например, планируется хранить в стеке массив объемом 10 000 байт, то стек должен быть не меньше этого размера. При этом надо иметь в виду, что в ряде случаев стек автоматически используется системой, в частности, при выполнении команды прерывания int 21h. По этой команде сначала процессор помещает в стек адрес возврата, а затем DOS отправляет туда же содержимое регистров и другую информацию, относящуюся к прерванной программе. Поэтому, даже если программа совсем не использует стек, он все же должен присутствовать в программе и иметь размер не менее нескольких десятков слов.
Что произойдет, если программист по ошибке или умышленно не опишет стек в своей программе? Изменим нашу мега программу, удалив из нее описание стека.

В регистр DX помещен код двух знаков @. Это сделано для наглядности примера.
Запустим программу под отладчиком.

Видим, что в SS находится тот же адрес, что и в CS; от сюда можно сделать вывод, что сегменты команд и стека совпадают. Однако содержимое SP равно нулю. Первая же команда push уменьшит содержимое SP на 2, то есть поместит в SP число -2, значит ли это, что стек будет расти как ему и положено, ввех, но не внутри сегмента команд, а над ним, по адресам -2, -4, -6 и т.д. относительно верхней границы сегмента команд. Оказывается это не так.
Если взять 16-разрядный двоичный счетчик, в котором записн 0, и послать в него два вычитающих импульса, то после первого в нем окажется число FFFFh, а после второго – FFFEh. При желании мы можем рассматривать число FFFEh как -2 (что имеет место при работе со знаковыми числами, о которых поговорим чуть позже), однако процессор при вычислении адресов рассматривает содержимое регистров как целые числа без знака и число FFFEh оказывается эквивалентным не -2, а 65534. В результате первая же команда занесения данного в стек (в нашем случае push dx) поместит это данное не над сегментом команд, а в самый его конец, в последнее слово по адресу CS:FFFEh (что, в данном примере, эквивалентно SS:FFFEh). При дальнейшем использовании стека его указатель будет смещаться в сторону меньших адресов, проходя значения FFFCh, FFFAh и т.д.
Таким образом, если в программе отсутствует явное определение стека, система сама создает стек по умолчанию в конце сегмента команд или, точнее, по адресу FFFEh относительно начала сегмента команд (см. рисунок).

Выполним программу дальше и убедимся в этом. По ходу выполнения программы с первой команды до седьмой, мы сможем видеть, что в стек помещаются какие-то значения, но при этом регистр SP не изменяется (предположительно, все это самодеятельность отладчика), и только после выполнения седьмой команды – push dx, SP принимает значение FFFEh, и на дне стека мы видим два символа @.

Рассмотренное явление, когда при уменьшении адреса после адреса 0 у нас получается адрес FFFEh, т.е. от начала сегмента мы прыгнули сразу в его конец, носит название циклического возврата или оборачивания адреса. С этим явлением приходится сталкиваться довольно часто.
Расположение стека в конце сегмента команд не приводит к каким-либо неприятностям, пока размер программы далек от граничной величины 64Кбайт. В этом случае начало сегмента команд занимают коды команд, конец – стек, а между ними располагаются данные (если сегмент данных описан в программе после сегмента команд). Если, однако, размер сегментов команд или данных приближается к 64Кбайт, то фактически стек будет наложен на тот или иной сегмент программы и возникнет опасность затирания данных или кода. Очевидно, что этого нельзя допускать. В тоже время, система не проверяет, что происходит со стеком и ни как не реагирует на затирание команд или данных. Таким образом, оценка размеров собственно программы, данных и стека является важным этапом разработки программы.