Использование стека в программах на ассемблере

Эта группа представляет собой набор специализированных команд, ориентированных на организацию гибкой и эффективной работы со стеком.

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

Для работы со стеком предназначены три регистра:

  • ss — сегментный регистр стека;
  • sp/esp — регистр указателя стека;
  • bp/ebp — регистр указателя базы кадра стека.

Размер стека зависит от режима работы микропроцессора и ограничивается 64 Кбайт (или 4 Гбайт в защищенном режиме). В каждый момент времени доступен только один стек, адрес сегмента которого содержится в регистре ss. Этот стек называется текущим. Для того чтобы обратиться к другому стеку (“переключить стек”), необходимо загрузить в регистр ss другой адрес. Регистр ss автоматически используется процессором для выполнения всех команд, работающих со стеком. Перечислим еще некоторые особенности работы со стеком:

  • запись и чтение данных в стеке осуществляется в соответствии с принципом LIFO (Last In First Out — “последним пришел, первым ушел”);
  • по мере записи данных в стек последний растет в сторону младших адресов. Эта особенность заложена в алгоритм команд работы со стеком;
  • при использовании регистров esp/sp и ebp/bp для адресации памяти ассемблер автоматически считает, что содержащиеся в нем значения представляют собой смещения относительно сегментного регистра ss.

В общем случае стек организован так, как показано на рис. 2. Рис. 2.Концептуальная схема организации стека Для работы со стеком предназначены регистры ss, esp/sp и ebp/bp. Эти регистры используются комплексно, и каждый из них имеет свое функциональное назначение. Регистр esp/sp всегда указывает на вершину стека, то есть содержит смещение, по которому в стек был занесен последний элемент. Команды работы со стеком неявно изменяют этот регистр так, чтобы он указывал всегда на последний записанный в стек элемент. Если стек пуст, то значение esp равно адресу последнего байта сегмента, выделенного под стек. При занесении элемента в стек процессор уменьшает значение регистра esp, а затем записывает элемент по адресу новой вершины. При извлечении данных из стека процессор копирует элемент, расположенный по адресу вершины, а затем увеличивает значение регистра указателя стека esp. Таким образом, получается, что стек растет вниз, в сторону уменьшения адресов. Что делать, если нам необходимо получить доступ к элементам не на вершине, а внутри стека? Для этого применяют регистр ebp. Регистр ebp — регистр указателя базы кадра стека. Например, типичным приемом при входе в подпрограмму является передача нужных параметров путем записи их в стек. Если подпрограмма тоже активно работает со стеком, то доступ к этим параметрам становится проблематичным. Выход в том, чтобы после записи нужных данных в стек сохранить адрес вершины стека в указателе кадра (базы) стека — регистре ebp. Значение в ebp в дальнейшем можно использовать для доступа к переданным параметрам. Начало стека расположено в старших адресах памяти. На рис. 2 этот адрес обозначен парой ss:ffff. Смещение ffff приведено здесь условно. Реально это значение определяется величиной, которую программист задает при описании сегмента стека в своей программе. К примеру, для программы в листинге 2 началу стека будет соответствовать пара ss:0100h. Адресная пара ss:ffff — это максимальное для реального режима значение адреса начала стека, так как размер сегмента в нем ограничен величиной 64 Кбайт (0ffffh). Для организации работы со стеком существуют специальные команды записи и чтения. pushисточник — запись значения источник в вершину стека. Интерес представляет алгоритм работы этой команды, который включает следующие действия (рис. 3):

  • (sp) = (sp) – 2; значение sp уменьшается на 2;
  • значение из источника записывается по адресу, указываемому парой ss:sp.

Рис. 3.Принцип работы команды pushpopназначение — запись значения из вершины стека по месту, указанному операндом назначение. Значение при этом “снимается” с вершины стека. Алгоритм работы команды pop обратен алгоритму команды push (рис. 4):

  • запись содержимого вершины стека по месту, указанному операндом назначение;
  • (sp) = (sp) + 2; увеличение значения sp.

Рис. 4.Принцип работы команды poppusha — команда групповой записи в стек. По этой команде в стек последовательно записываются регистры ax, cx, dx, bx, sp, bp, si, di. Заметим, что записывается оригинальное содержимое sp, то есть то, которое было до выдачи команды pusha (рис. 5).

Рис. 5.Принцип работы команды pushapushaw — почти синоним команды pusha. В чем разница? На уроке 5 мы обсуждали один из атрибутов сегмента — атрибут разрядности. Он может принимать значение use16 или use32. Рассмотрим работу команд pusha и pushaw при каждом из этих атрибутов:

  • use16 — алгоритм работы pushaw аналогичен алгоритму pusha.
  • use32 — pushaw не изменяется (то есть она нечувствительна к разрядности сегмента и всегда работает с регистрами размером в слово — ax, cx, dx, bx, sp, bp, si, di). Команда pusha чувствительна к установленной разрядности сегмента и при указании 32-разрядного сегмента работает с соответствующими 32-разрядными регистрами, то есть eax, ecx, edx, ebx, esp, ebp, esi, edi.

pushad — выполняется аналогично команде pusha, но есть некоторые особенности, которые вы можете узнать из “Справочника команд”. Следующие три команды выполняют действия, обратные вышеописанным командам: popa; popaw; popad. Группа команд, описанная ниже, позволяет сохранить в стеке регистр флагов и записать слово или двойное слово в стеке. Отметим, что перечисленные ниже команды — единственные в системе команд микропроцессора, которые позволяют получить доступ (и которые нуждаются в этом доступе) ко всему содержимому регистра флагов. pushf — сохраняет регистр флагов в стеке. Работа этой команды зависит от атрибута размера сегмента:

  • use16 — в стек записывается регистр flags размером 2 байта;
  • use32 — в стек записывается регистр eflags размером 4 байта.
Читайте также:
Для какой цели можно использовать указанные кнопки в программе excel

pushfw — сохранение в стеке регистра флагов размером в слово. Всегда работает как pushf с атрибутом use16. pushfd — сохранение в стеке регистра флагов flags или eflags в зависимости от атрибута разрядности сегмента (то есть то же, что и pushf). Аналогично, следующие три команды выполняют действия, обратные рассмотренным выше операциям: popfpopfwpopfd И в заключение отметим основные виды операции, когда использование стека практически неизбежно:

  • вызов подпрограмм;
  • временное сохранение значений регистров;
  • определение локальных переменных.

Ассемблер (работа со стеком)

Урок 19. Язык Ассемблера. Работа со стеком

Источник: studfile.net

Использование стека в программах на ассемблере

Стек — это динамическая структура данных, которая хранит важную информация о программе, включая локальные переменные, информацию о подпрограммах и временные данные. В архитектуре x86-64 стек реализуется с помощью сегмента стека. Процессор x86-64 управляет стеком через регистр RSP (указатель стека). Когда программа начинает выполняться, операционная система инициализирует регистр RSP адресом последней ячейки памяти в сегменте стека. В процессе работы программы данные записываются в сегмент стека или, наоборот, извлекаются из стека.

Для добавления данных в стек применяется инструкция push , которая имеет следующий синтаксис:

push reg16 push reg64 push mem16 push mem64 pushw constant16 push constant32 ; расширяется до 64 бит

Итак, мы можем добавить в стек значения 16- и 64-разрядного регистра, 16- и 64-разрядной переменной и 16- и 32-разрядной константы (32-битная констранта расширяется до 64 бит).

При выполнении инструкции push от значения регистра RSP вычитается размер операнда. А по адресу, который хранится в стеке, помещается значение операнда.

RSP = RSP — размер операнда [RSP] = значение операнда

Инструкция pop позволяет, наоборот, взять из стека значение, адрес которого хранится в текущий момент в регистре RSP . Эта инструкция имеет следующий синтаксис

pop reg16 pop reg64 pop memory16 pop memory64

Инструкция в качестве операнда получает место, куда надо сохранить данные из стека. Это может быть или 16- и 64-разрядный регистр, или 16- и 64-разрядная переменная. При выполнении этой инструкции в операнд помещается значение, которое хранится в адресе из RSP. А само значение RSP увеличивается на размер операнда:

operand = [RSP] RSP = RSP + размер операнда

Например, возьмем следующую программу:

.code main proc mov rdx, 15 push rdx ; в конец стека помещаем содержимое регистра RDX pop rax ; значение из конца стека помещаем в регистр RAX ret main endp end

Допустим, регистр RSP содержит изначально адрес 00FF_FFFFh.

Стек в ассемблере

Пусть в регистре RDX хранится некоторое значение, которое с помощи инструкции push заталкивается в стек:

push rdx

В результате в последующие 8 байт начиная с адреса, который хранится в RSP, помещается значение из регистра RDX (в данном случае число 15). А в регистр RSP будет помещен адрес RSP-8, то есть условно 00FF_FFF7h и сохранит текущее значение RAX в ячейке памяти начиная с 00FF_FFFEh по 00FF_FFF7h.

Стек в ассемблере MASM и push

Затем извлекаем из стека значение по адресу, который хранится в RSP, в регистр RAX:

pop rax

В результате в RAX помещается число из стека (в данном случае число 15). А в регистр RSP будет помещен адрес RSP+8, то есть условно 00FF_FFFFh и сохранит текущее значение RAX в ячейке памяти начиная с 00FF_FFFУh по 00FF_FFF7h.

Стек в ассемблере MASM и pop

Принцип LIFO и сохранение регистров в стек

Наиболее распространенное использование команд push и pop — это сохранение значений регистров во время промежуточных вычислений. Поскольку регистры — лучшее место для хранения временных значений, и регистры также могут потребоваться для других операций, поэтому в процессе программы легко исчерпать регистры. Инструкции push и pop позволяют сохранить начальные значения регистров при старте программы, а при завершении программы восстановить эти значения.

Следует учитывать, что стек представляет структуру LIFO (Last In, First Out или Последний вошел, первый вышел), что значит, что получение данных из стека происходит в порядке, обратном их добавлению. Рассмотрим следующую программу:

.code main proc mov rax, 11 mov rdx, 33 push rax push rdx pop rax pop rdx ret main endp end

Допустим, в самом начале программы до добавления данных стек регистр RSP хранит адрес 00FF_FFF0.

LIFO стек в ассемблере

Затем добавляем в стек значение регистра RAX:

push rax

Адрес в RSP смещается на 8 байтов и указывает на адрес значения из регистра RAX

LIFO стек в ассемблере push

Далее добавляем в стек значение регистра RDX:

push rdx

Адрес в RSP смещается на 8 байтов и указывает на адрес значения из регистра RDX

LIFO стек в ассемблере push регистр

После добавления мы последовательно извлекаем данные. Первая инструкция извлекает данные, на которые указывает регистр RSP, в регистр RAX:

pop rax

LIFO стек в ассемблере pop регистр

Однако поскольку RSP перед операцией извлечения указывал на адрес последнего добавленного значения — значения регистра RDX, то регистр RAX получит значение регистра RDX. Соответственно при последующей инструкции pop:

pop rdx

Регистр RDX получить значение регистра RAX, которое было в регистре RAX до добавления в стек.

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

push rax push rdx pop rdx ; Последним добавлено значение RDX, поэтому сначала извлекаем в RDX pop rax

В любом случае стоит помнить, что количество инструкций push и pop должно быть равно, сколько раз мы добавили данные в стек, столько раз мы должны получить данные из стека.

Сохранение флагов состояния

Ассемблер предоставляет дополнительную пару инструкций pushfq и popfq для сохранения и восстановления соответственно регистра RFLAGS (и всех флагов состояния). Например:

.code main proc pushfq ; сохраняем значения флагов mov al, 255 add al, 2 ; 255 + 2 = 257 — флаг CF будет установлен popfq ; восстанавливаем значения флагов jc set ; если флаг CF установлен, переход к метке set mov rax, 0 ret set: mov rax, 1 ret main endp end

Читайте также:
Программа как вернуть бывшего

Здесь инструкцией pushfq сначала сохраняем флаги. По умолчанию флаг переноса CF будет равен 0.

Затем выполняем сложение 255 + 2, что даст 257 и что очевидно за пределы разрядности регистра AL, соответственно будет установлен флаг переноса CF. Далее с помощью инструкции jc set переходим к метке set, если флаг CF установлен. Однако перед этой инструкцией мы восстанавливаем флаги — popfq . То есть флаг CF получит свое значение 0, и никакого перехода к метке set не произойдет.

Восстановление стека без извлечения данных

При завершении программы следует восстановить адрес в RSP. Как выше было показано, для этого мы можем использовать инструкцию pop . Однако может сложиться ситуация, что данные не требуется извлекать из стека. Например, в зависимости от некоторых условий данные могут понадобиться, а могут не понадобиться. Если данные не нужны, извлекать каждые 8 байт отдельно с помощью инструкции pop не имеет смысла, особенно если надо извлечь много данных из стека. И в этом случае мы можем восстановить адрес в RSP, просто прибавив нужное значение — смещение относительно начального адреса. Например:

.code main proc mov rax, 2 mov rdx, 3 push rax push rdx add rsp, 16 ; прибавляем к адресу в RSP 16 байт ret main endp end

Здесь в стек помещаем значения двух регистров — RAX и RDX, то есть адрес в RSP уменьшится на 16 байт (совокупный размер двух регистров). И чтобы быстро восстановить стек, прибавляем к адресу в RSP 16 байт:

add rsp, 16

Подобным образом можно вычитать из адреса в RSP определенное число, тем самым резервируя в стеке некоторое пространство:

.code main proc sub rsp, 16 ; смещаем адрес в RSP на 16 байт mov rax, 2 add rsp, 16 ; восстанавливаем адрес в RSP ret main endp end

Вычитание определенного количество байтов из стека может потребоваться при взаимодействии с некоторыми внешними функциями, например, на языках C/С++ для резервирования места для параметров функций или для выравнивания стека. Так, до вызова функций в Windows в соответствии с Microsoft ABI стек должен быть выровнен по 16-байтовой границе. При использовании инструкции push для сохранения значения регистра перед вызовом внешней функции надо убедиться, адрес RSP кратен 16 байтам.

Косвенная адресация в стеке

Как и в случае с любым другим регистром, в отношении регистра стека RSP можно использовать косвенную адресацию и обращаться к данным в стеке без смещения указателя RSP. Например:

.code main proc mov rdx, 11 push rdx ; в конец стека помещаем содержимое регистра RDX mov rax, [rsp] ; в RAX помещаем значение по адресу из RSP — число 11 pop rcx ; значение из конца стека помещаем в регистр RCX ret main endp end

В данном случае в стек помещаем число из регитсра RDX — число 11. Далее в регистр RAX помещаем значение, которое располагается по адресу из RSP. Фактически это тот адрес, где располагается число 11.

Аналогично можно применять смещения и масштабирование. Например:

.code main proc push 12 push 13 push 14 mov rax, [rsp + 8] ; [rsp + 8] — адрес значения 13 ; извлекаем сохраненные значения в r8, r9, r10 pop r8 pop r9 pop r10 ret main endp end

Здесь в стек последовательно помещаются три числа 12, 13, 14. Каждое число будет занимать 8 байт. После добавления адрес в RSP будет указывать на адрес последнего добавленного числа — 14. И чтобы, например, получить предыдущее число — 13, нам надо к адресу в RSP прибавить 8. И в данном случае получаем это число в регистр RAX.

mov rax, [rsp + 8]

Соотвественно чтобы получить из стека первое число — 12, надо к адресу в RSP прибавить 16:

mov rax, [rsp + 16]

Источник: metanit.com

FasmWorld Программирование на ассемблере FASM для начинающих и не только

Стеком называется структура данных, организованная по принципу LIFO («Last In — First Out» или «последним пришёл — первым ушёл»). Стек является неотъемлемой частью архитектуры процессора и поддерживается на аппаратном уровне: в процессоре есть специальные регистры (SS, BP, SP) и команды для работы со стеком.

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

Схема организации стека в процессоре 8086 показана на рисунке:

Стек располагается в оперативной памяти в сегменте стека, и поэтому адресуется относительно сегментного регистра SS. Шириной стека называется размер элементов, которые можно помещать в него или извлекать. В нашем случае ширина стека равна двум байтам или 16 битам. Регистр SP (указатель стека) содержит адрес последнего добавленного элемента. Этот адрес также называется вершиной стека. Противоположный конец стека называется дном

Дно стека находится в верхних адресах памяти. При добавлении новых элементов в стек значение регистра SP уменьшается, то есть стек растёт в сторону младших адресов. Как вы помните, для COM-программ данные, код и стек находятся в одном и том же сегменте, поэтому если постараться, стек может разрастись и затереть часть данных и кода (надеюсь, с вами такой беды не случится :)).

Для стека существуют всего две основные операции:

  • добавление элемента на вершину стека (PUSH);
  • извлечение элемента с вершины стека (POP);

Добавление элемента в стек

Выполняется командой PUSH. У этой команды один операнд, который может быть непосредственным значением, 16-битным регистром (в том числе сегментым) или 16-битной переменной в памяти. Команда работает следующим образом:

  1. значение в регистре SP уменьшается на 2 (так как ширина стека — 16 бит или 2 байта);
  2. операнд помещается в память по адресу в SP.
Читайте также:
Виды анимационных программ для детей

push -5 ;Поместить -5 в стек push ax ;Поместить AX в стек push ds ;Поместить DS в стек push [x] ;Поместить x в стек (x объявлен как слово) push word [bx] ;Поместить в стек слово по адресу в BX

push -5 ;Поместить -5 в стек push ax ;Поместить AX в стек push ds ;Поместить DS в стек push [x] ;Поместить x в стек (x объявлен как слово) push word [bx] ;Поместить в стек слово по адресу в BX

Существуют ещё 2 команды для добавления в стек. Команда PUSHF помещает в стек содержимое регистра флагов. Команда PUSHA помещает в стек содержимое всех регистров общего назначения в следующем порядке: АХ, СХ, DX, ВХ, SP, BP, SI, DI (значение DI будет на вершине стека). Значение SP помещается то, которое было до выполнения команды. Обе эти команды не имеют операндов.

Извлечение элемента из стека

Выполняется командой POP. У этой команды также один операнд, который может быть 16-битным регистром (в том числе сегментым, но кроме CS) или 16-битной переменной в памяти. Команда работает следующим образом:

  1. операнд читается из памяти по адресу в SP;
  2. значение в регистре SP увеличивается на 2.

Обратите внимание, что извлеченный из стека элемент не обнуляется и не затирается в памяти, а просто остаётся как мусор. Он будет перезаписан при помещении нового значения в стек.

pop cx ;Поместить значение из стека в CX pop es ;Поместить значение из стека в ES pop [x] ;Поместить значение из стека в переменную x pop word [di] ;Поместить значение из стека в слово по адресу в DI

pop cx ;Поместить значение из стека в CX pop es ;Поместить значение из стека в ES pop [x] ;Поместить значение из стека в переменную x pop word [di] ;Поместить значение из стека в слово по адресу в DI

Соответственно, есть ещё 2 команды. POPF помещает значение с вершины стека в регистр флагов. POPA восстанавливает из стека все регистры общего назначения (но при этом значение для SP игнорируется).

Пример программы

Имеется двумерный массив — таблица 16-битных значений со знаком размером n строк на m столбцов. Программа вычисляет сумму элементов каждой строки и сохраняет результат в массиве sum. Первый элемент массива будет содержать сумму элементов первой строки, второй элемент — сумму элементов второй строки и так далее.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
use16 ;Генерировать 16-битный код org 100h ;Программа начинается с адреса 100h jmp start ;Переход к метке start ;———————————————————————- ; Данные n db 4 ;Количество строк m db 5 ;Количество столбцов ;Двумерный массив — таблица c данными table: dw 12,45, 0,82,34 dw 46,-5,87,11,56 dw 35,21,77,90,-9 dw 44,13,-1,99,32 sum rw 4 ;Массив для сумм каждой строки ;———————————————————————- start: movzx cx,[n] ;Счётчик строк mov bx,table ;BX = адрес таблицы mov di,sum ;DI = адрес массива для сумм xor si,si ;SI = смещение элемента от начала таблицы rows: xor ax,ax ;Обнуление AX. В AX будет считаться сумма push cx ;Сохранение значения CX movzx cx,[m] ;Инициализация CX для цикла по строке calc_sum: add ax,[bx+si] ;Прибавление элемента строки add si,2 ;SI = смещение следующего элемента loop calc_sum ;Цикл суммирования строки pop cx ;Восстановление значения CX mov [di],ax ;Сохранение суммы строки add di,2 ;DI = адрес следующей ячейки для суммы строки loop rows ;Цикл по всем строкам таблицы mov ax,4C00h ; int 21h ;/ Завершение программы

use16 ;Генерировать 16-битный код org 100h ;Программа начинается с адреса 100h jmp start ;Переход к метке start ;———————————————————————- ; Данные n db 4 ;Количество строк m db 5 ;Количество столбцов ;Двумерный массив — таблица c данными table: dw 12,45, 0,82,34 dw 46,-5,87,11,56 dw 35,21,77,90,-9 dw 44,13,-1,99,32 sum rw 4 ;Массив для сумм каждой строки ;———————————————————————- start: movzx cx,[n] ;Счётчик строк mov bx,table ;BX = адрес таблицы mov di,sum ;DI = адрес массива для сумм xor si,si ;SI = смещение элемента от начала таблицы rows: xor ax,ax ;Обнуление AX. В AX будет считаться сумма push cx ;Сохранение значения CX movzx cx,[m] ;Инициализация CX для цикла по строке calc_sum: add ax,[bx+si] ;Прибавление элемента строки add si,2 ;SI = смещение следующего элемента loop calc_sum ;Цикл суммирования строки pop cx ;Восстановление значения CX mov [di],ax ;Сохранение суммы строки add di,2 ;DI = адрес следующей ячейки для суммы строки loop rows ;Цикл по всем строкам таблицы mov ax,4C00h ; int 21h ;/ Завершение программы

Как видите, в программе два вложенных цикла: внешний и внутренний. Внешний цикл — это цикл по строкам таблицы. Внутренний цикл вычисляет сумму элементов строки. Стек здесь используется для временного хранения счётчика внешнего цикла. Перед началом внутреннего цикла CX сохраняется в стеке, а после завершения восстанавливается.

Такой приём можно использовать для программирования и большего количества вложенных циклов.

Turbo Debugger

В отладчике Turbo Debugger стек отображается в нижней правой области окна CPU. Левый столбец чисел — адреса, правый — данные. Треугольник указывает на вершину стека, то есть на тот адрес, который содержится в регистре SP. Если запустить программу в отладчике, то можно увидеть, как работают команды «push cx» и «pop cx».

Упражнение

Объявите в программе строку «$!olleH». Напишите код для переворачивания строки с использованием стека (в цикле поместите каждый символ в стек, а затем извлеките в обратном порядке). Выведите полученную строку на экран. Свои результаты пишите в комментариях

Источник: fasmworld.ru

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru