Как работать со стеком в ассемблере

от admin

Устройство Стека для Intel386

Стек (от англ. Stack) — специально отведённое место в памяти для хранения временных данных. Он подчиняется следующим правилам:

LIFO (Last In First Out), который подразумевает, что элемент, который был помещён на стек последним, будет первым оттуда удалён.

Стек растёт в сторону начала адресного пространства (ещё говорят, что стек растёт вниз).

Минимальная единица, которая может быть удалена\помещена со\на стек(а) — 16 бит (2 байта).

Максимальная единица, которая может быть удалена\помещена со\на стек(а) — 32 бита (4 байта).

Устройство Стека

Рисунок 1. Устройство стека на Intel386

Как видно из Рисунка 1, при помещении чего-либо на стек он увеличивается вниз. Указатель стека (ESP — Stack Pointer) содержит адрес последнего элемента стека, следующий за последним элементом. Эту часть стека также называют вершиной стека (от англ. TOS — Top Of Stack).

Когда что-то помещается на стеке, процессор декрементирует значение в регистре ESP и записывает помещаемое значение на вершину стека. В случае, когда необходимо что-то удалить со стека, процессор сначала копирует удаляемое значение с вершины стека, а затем уже инкрементирует значение в регистре ESP.

Чтобы процессор смог понять, что ему нужно сохранить на стек, используется инструкция push в ассемблерном коде, в случае удаления «вытаскивания» значения со стека — pop.

Инструкция push

Её синтаксис может разниться от одного языка ассемблера к другому, однако суть её остаётся неизменной — помещение какого-либо значения на стек.

Префикс r/m (от англ. register/memory) означает, что значение, которое необходимо поместить на стек находится в памяти, которая в свою очередь находится в регистре, например, в регистре лежит значение 0x87654321 — адрес памяти, по которому хранится значение 0x11223344 , соответственно на стек будет помещено значение 0x11223344.

Префикс r (от англ. register) означает, что значение, которое необходимо поместить на стек, находится в регистре.

Префикс imm (от англ. immediate), т.е. значение, которое непосредственно передаётся инструкции в качестве параметра.

Постфиксы 8, 16, 32 означают сколько бит содержит, передаваемое инструкции, значение, которое в свою очередь, обычно называют операндом.

На данный момент может возникнуть вопрос от том, что, как писалось выше, минимальная единица, которая может быть помещена на стек — 16 бит, но в синтаксисе инструкции push есть imm8 , говорящее о том, что операндом инструкции может быть 8-битное значение. На самом деле, 8-битные значения дополняются до 16-битных, т.к. стек выровнен по 16 бит, однако это имеет значение и для знаковых типов, которые используют 2-ое дополнение дополнение до двух.

Инструкция pop

Синтаксис инструкции pop аналогичен тому, который используется в инструкции push , за исключением того, что значение удаляется со стека и помещается в операнд инструкции.

А также, по очевидным причинам, операндом инструкции pop не может быть immediate value, т.к. мы не можем сохранить что-либо туда.

Регистры для управления стеком

Уже был упомянут один из регистров, который позволяет управлять стеком — ESP (Stack Pointer), пожалуй, это самый важный регистр, который хранит указатель на вершину стека, однако есть ещё несколько регистров, связанных со стеком:

SS (Stack Segment) — регистр, указывающий на определённый сегмент памяти, в котором и находится сам стек, только одним сегментом стека можно манипулировать в конкретный момент времени, этот регистр задействуется процессором для всех операций, связанных со стеком.

EBP (Base Pointer) — регистр, указывающий на текущий фрейм, т.е. на начало стека для конкретной процедуры\функции, обычно используется для адресации локальных переменных и аргументов процедуры\функции.

Соглашение о вызовах функций в System V Intel386

В System V Intel386 (далее System V) существует несколько правил для вызова функций и соответственно передачи им аргументов, эти правила распространяются исключительно на глобальные функции, локальные функции могут не следовать этим правилам (однако это считается не лучшим выбором):

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

Регистры EBP, EBX, EDI, ESI и ESP могут изменяться вызываемой функцией, соответственно, если вызывающая функция хранит какое-либо значение в одном из этих регистров — она сначала должна поместить значения этих регистров на стеке, а после восстановить их. Исключение составляет регистр EBP, который не изменяется при вызове функции и продолжает указывать на предыдущий фрейм (на фрейм вызывающей функции), поэтому он помещается на стек вызываемой функцией в начале её выполнения и восстанавливается по завершении, то же касается и регистра ESP.

При осуществлении вызова функции используется инструкция call в ассемблере, которая сохраняет на стеке адрес следующей за ней [call] инструкции, который обычно называют return address.

Если функция возвращает какое-либо значение — она должна поместить его в регистр EAX, в ином случае она не должна ничего сохранять в какой-либо регистр.

Следовательно, после вызова функции (после выполнения инструкции call ), стек будет выглядеть следующим образом:

Рисунок 2. Стек после вызова функции

Рисунок 2. Стек после вызова функции

Как показано на Рисунке 2, первое, что есть на текущем фрейме — адрес следующей после call инструкции, далее идёт сохранённое значение регистра EBP, указывающее на предыдущий фрейм, а затем идут локальные переменные, относящиеся к конкретной функции.

Всё, что выше return address, относится к предыдущему фрейму, включая аргументы, переданные вызываемой функции. Таким образом, первый аргумент будет в EBP + 8, второй в EBP + 12, и т.д., за исключением случаев, когда аргументом является 16-битное значение.

Пример работы со стеком на GNU Assembler x86

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

В примере будет использоваться GNU Assembler (GAS), который использует синтаксис AT&T, для сборки используется GCC.

Начнём с того, как будет выглядеть стек для функции main :

В качестве аргументов были переданы argc, argv и envp и находятся они, соответственно, в EBP + 8, EBP + 12 и EBP + 16, return address — как уже упоминалось — адрес, следующий за инструкцией call , local argv index — локальная переменная для красивого вывода (ну почти) массива argv.

Для начала в секции .rodata (Read-Only Data) создадим 3 переменные, которые будут форматированными строками, для вывода argc, argv и envp:

Затем объявим в секции .text функцию main, где собственно, и будет код программы, и пометим её, как глобальную:

В самом начале функции помещаем регистр EBP на стек и делаем его локальным указателем на текущий фрейм:

Помещаем argc и указатель на массив argv в EDI и ESI соответственно, а также аллоцируем на стеке 4 байта для локальной переменной и инициализируем её значением ноль:

Теперь можно заняться выводом argc, для этого необходимо в обратном порядке поместить все аргументы функции (в данном случае используется printf ), а после вызова очистить стек, т.к. в System V очисткой стека занимается вызывающая сторона.

Аргументами функции будут форматированная строка и значение argc, которое хранится в регистре EDI:

Стоит заметить, что на стек мы помещаем не значение, которое хранится в argc_str , а её адрес, т.к. printf ожидает именно указатель на форматированную строку.

Следующим шагом будет вывод массива argv, который будет осуществляться в цикле, однако перед этим необходимо понимать, что argv содержит адрес памяти, по которому лежит массив символов (необходимая нам строка), поэтому первым аргументом будет адрес, лежащий в argv, вторым аргументом будет адрес, лежащий в argv + 4, и т.д., здесь мы прибавляем по 4 байта, т.к. адрес — это 32-битное (4-байтовое) число:

Первым делом, в цикле проверяется значение индекса и argc, в случае если они равны (напоминаю, что индексация массива в данном случае идёт с нуля, поэтому последний элемент в массиве argv будет argv + (argc — 1) ), то мы просто выходим из цикла, иначе же вызываем функцию printf (регистр ESI содержит адрес argv), чистим стек, увеличиваем индекс на единицу, перемещаем указатель на следующий элемент в массиве argv и возвращаемся в начало цикла.

После чего, чтобы отделить масив argv от массива envp, сделаем перенос строки (символ переноса строки — \n , который в десятичном представлении имеет значение 10, а в шестнадцатеричном, соответственно, 0xA), для этого используем функцию putchar:

Далее мы будем импользовать тот же регистр ESI для хранения указателей на переменные окружения в массиве envp, как и в случае с argv:

Теперь, т.к. у нас нет индекса для массива envp, а также мы не знаем заранее, сколько там будет элементов, необходимо вспомнить, что массив envp — это нуль-терминированный массив, поэтому, чтобы узнать, что больше элементов нет — достаточно проверить равен ли нулю элемент (он будет следовать за последним):

Цикл вывода переменных окружения не сильно отличается от того, что использовался для вывода массива аргументов командной строки, за исключением проверки выхода из цикла и отсутствием индекса.

Ну и в самом конце необходимо установить возвращаемое значение функции в ноль, а также произвести очистку стека:

Очистка стека производится путём восстановления регистра EBP и ESP в их исходные значения, следовательно всё, что было в этой функции может быть перезаписано и использовано другими функциями\процедурами, инструкция ret устанавливает в EIP (Instruction Pointer) значение return address, таким образом управление возрвращается вызывающей стороне.

Важно упомянуть, что существует более удобная инструкция для очистки стека — leave , она выполняет именно эти две вещи — восстановление регистров EBP и ESP, соответственно последняя часть кода может быть переписана следующим образом:

Однако у этой инструкции есть менее привлекательный компаньон — инструкция enter , у неё два операнда, первый отвечает за количество байт, которое необходимо аллоцировать на стеке, а второй за уровень вложенности, из-за чего реализация подобной инструкции достаточно сложна и не ограничивается этими тремя инструкциями:

Поэтому выполняется в разы медленнее, из-за чего большинство компиляторов стараются её избегать, однако, для демонстрации, те три строчки можно заменить одной:

Теперь подошло время проверить работу программы, для этого используем gcc , чтобы скомпилировать и слинковать ассемблерный код:

Или, в случае, если хостом является x64:

И, наконец, можно запустить программу:

Заключение

Работа со стеком — важная часть для любого языка программирования, особенно низкоуровнего, однако полезно знать, как она устроена и для высокоуровневых языков, хотя для них она может значительно отличаться из-за самих концепций языка.

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

Существуют ещё 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

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

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

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

Читать:
Как создать базу данных на хостинге reg ru

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». Напишите код для переворачивания строки с использованием стека (в цикле поместите каждый символ в стек, а затем извлеките в обратном порядке). Выведите полученную строку на экран. Свои результаты пишите в комментариях ��

7.7. Работа со стеком

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

Стеком называется сегмент памяти, на начало которого указывает сегментный регистр SS. При работе программы в регистр SS можно последовательно загружать адреса начал нескольких сегментов, поэтому иногда говорят, что в программе несколько стеков. Однако в каждый момент стек только один – тот, на который сейчас указывает регистр SS. Именно этот стек мы и будем иметь в виду.

Кроме начала, у стека есть текущая позиция – вершина стека, её смещение от начала сегмента стека записано в регистре SP (stack pointer). Следовательно, как мы уже знаем, физический адрес вершины стека можно получить по формуле Афиз = (SS*16 + SP)mod 2 20 .

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

В соответствие с определением понятия стек последнее записанное в него слово будет читаться из стека первым. Это так называемое правило «последний пришёл – первый вышел» (английское сокращение LIFO). 1 Обычно стек принято изображать «растущим» снизу-вверх. Как следствие получается, что конец стека фиксирован и расположен снизу, а вершина двигается вверх (при записи в стек) и вниз (при чтении из стека).

В каждый момент времени регистр SP указывает на последнее слово, записанное в стек. Обычно стек изображают, как показано на рис. 7.1.

Начало стека SS 

Вершина стека SP 

SP для пустого стека 

Рис. 7.1. Так мы будем изображать стек.

На нашем рисунке, как обычно, стек растёт снизу-вверх, занятая часть стека закрашена. В начале работы программы, когда стек пустой, регистр SP указывает на первое слово за концом стека. Особым является случай, когда стек имеет максимальный размер 2 16 байт, в этом случае значение регистра SP для пустого стека равно нулю, т.е. совпадает со значением этого регистра и для полного стека, поэтому стеки максимального размера использовать не рекомендуется, так как будет затруднён контроль пустоты и переполнения стека.

Обычно для резервирования памяти под стек на языке Ассемблера описывается специальный сегмент стека. В наших предыдущих программах мы делали это таким образом:

stack segment stack

dw 64 dup (?)

stack ends

Имя сегмента стека и способ резервирования памяти может быть любым, например, можно описать такой стек:

st_1 segment stack

db 128 dup (?)

То, что этот сегмент будет при выполнении программы использоваться именно как сегмент стека, указывается параметром stack директивы segment. Этот параметр является служебным словом языка Ассемблера и, вообще говоря, не должен употребляться ни в каком другом смысле. 2

В нашем последнем примере размер сегмента стека установлен в 64 слова, поэтому в начале работы регистр SP будет иметь значение 128, т.е., как мы и говорили ранее, указывает на первое слово за концом стека. Области памяти в стеке обычно не имеют имён, так как доступ к ним, как правило, производится только с использованием регистров.

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

Рассмотрим сначала те команды работы со стеком, которые не являются командами перехода. Команда

где op1 может иметь форматы r16, m16, CS,DS,SS,ES, записывает в стек слово, определяемое своим операндом. Это команда выполняется по правилу:

Здесь запись <SS,SP> обозначает адрес в стеке, вычисляемый по формуле

Особым случаем является команда

В младших моделях нашего семейства она выполняется, как описано выше, а в старших – по схеме

Следовательно, если мы хотим, чтобы наша программа правильно работала на всех моделях семейства, надо с осторожностью использовать в программе команду push SP .

где op1 может иметь форматы r16, m16, SS, DS, ES, читает из стека слово и записывает его в место памяти, определяемое своим операндом. Это команда выполняется по правилу:

op1 := <SS,SP>; SP := (SP + 2)mod 2 16

записывает в стек регистр флагов FLAGS, а команда

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

В старших моделях нашего семейства появились две новые удобные команды работы со стеком. Команда

последовательно записывает в стек регистры AX,CX,DX,BX,SP (этот регистр записывается до его изменения), BP,SI и DI. Команда

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

Команды записи в стек не проверяют того, что стек уже полон, для надёжного программирования это должен делать сам программист. Например, для проверки того, что стек уже полон, и писать в него нельзя, можно использовать команду сравнения

cmp SP,0; стек уже полон ?

и выполнить условный переход, если регистр SP равен нулю. Особым случаем здесь будет стек максимального размера 2 16 байт, для него значение регистра SP=0 как для полного, так и для пустого стека (обязательно понять это!), поэтому не рекомендуется использовать стек максимального размера.

Аналогично для проверки того, что стек уже пуст, и читать из него нельзя, следует использовать команду сравнения

cmp SP,K; стек пуст ?

где K – чётное число – размер стека в байтах. Если размер стека в байтах нечётный, то стек полон при SP=1, т.е. в общем случае необходима проверка SP<2. Обычно избегают задавать стеки нечёт­ной длины, для них труднее проверить и пустоту стека.

В качестве примера использования стека рассмотрим программу для решения следующей задачи. Необходимо вводить целые беззнаковые числа до тех пор, пока не будет введено число ноль (признак конца ввода). Затем следует вывести в обратном порядке то из введённых чисел, которые принадлежат диапазону [2..100] (сделаем спецификацию, что таких чисел может быть не более 300). Ниже приведено возможное решение этой задачи.

include io.asm

st segment stack

db 128 dup (?); это для системных нужд

dw 300 dup (?); это для хранения наших чисел

code segment

assume cs:code,ds:code,ss:st

T1 db ′Вводите числа до нуля$′

T2 db ′Числа в обратном порядке:′,10,13,′$′

T3 db ′Ошибка – много чисел!′,10,13,′$′

mov ax,code

mov dx, offset T1; Приглашение к вводу

sub cx,cx; хороший способ для cx:=0

L: inint ax

cmp ax,0; проверка конца ввода

je Pech; на вывод результата

ja L; проверка диапазона

cmp cx,300; в стеке уже 300 чисел ?

push ax; запись числа в стек

inc cx; счетчик количества чисел в стеке

Pech: jcxz Kon; нет чисел в стеке

mov dx, offset T2

L1: pop ax

outword ax,10; ширина поля вывода=10

Kon: finish

Err: mov dx,T3

end program_start

Заметим, что в нашей программе нет собственно переменных, а только строковые константы, поэтому мы не описали отдельный сегмент данных, а разместили эти строковые константы в кодовом сегменте. Можно считать, что сегменты данных и кода в нашей программе совмещены. Мы разместили строковые константы в начале сегмента кода, перед входной точкой программы, но с таким же успехом можно разместить эти строки и в конце кодового сегмента после последней макрокоманды finish.

Обратите внимание, как мы выбрали размер стека: 128 байт мы зарезервировали для системных нужд (как уже упоминалось, стеком будут пользоваться и другие программы, подробнее об этом будет рассказано далее) и 300 слов мы отвели для хранения введённых нами чисел. При реализации этой программы может возникнуть желание определять, что введено слишком много чисел, анализируя переполнение стека. Другими словами, вместо проверки

cmp cx,300; в стеке уже 300 чисел ?

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

cmp SP,2; стек уже полон ?

Это, однако, может повлечь за собой тяжёлую ошибку. Дело в том, что в стеке может остаться совсем мало места, а, как мы знаем, стек использует не только наша, но и другие программы, которые в этом случае будут работать неправильно.

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

Функции

Функции позволяют организовать код в отдельные независимые единицы, которые при необходимости можно многократно вызывать в течение работы программы. Но прежде чем переходить к функциям надо иметь понимание того, что такое стек и как он работает.

Стек представляет структуру данных, которая позволяет управлять регистрами между вызовами функций, а также управлять памятью в процессе вызова функции. Стек работает по принципу LIFO (last in first out — последний вошел, первый вышел). Это значит, что первым извлекается из стека тот элемент, который был добавлен последним. Стек подобен стопке тарелок — каждую новую тарелку кладут поверх предыдущей и, наоборот, сначала берут самую верхнюю тарелку.

Когда Linux запускает программу, то этой программе выделяется стек размером 8 мегабайт. Для работы со стеком в программе на ассемблере применяется регистр Х31 . Этот регистр имеет специальное назначение: он может выступать в качестве нулевого регистра и в качестве указателя стека (SP) . Если инструкция не работает со стеком, то она рассматривает этот регистр как нулевой регистр.

Ассемблер ARM предоставляет ряд инструкций для управления стеком:

  • STR и STP сохраняют данные регистров в стек
  • LDR и LDP загружают данные из стека в регистры

ARM-процессор требует, чтобы регистр SP всегда имел выравнивание в 16 байт. Это значит, что мы можем добавлять или вычитать из SP только те данные, которые кратны 16. В ином случае мы столкнемся с ошибкой.

Сохранение данных из регистров в стек

Для копирования данных из регистра Xn в стек применяется инструкция:

Выражение [SP, #-N]! указывает, что данные Xn копируются в память по адресу SP – N . N представляет смещение относительно верхушки стека. Причем это смещение должно быть кратным 16 для выравнивания стека. После чего в регистр SP помещается этот новый адрес SP – N .

Здесь сначала в регистр X1 загружается адрес метки bytes (адрес числа 13) и данные по этому адресу сохраняем в X4. Затем данные из X4 сохраняем в стек по адресу SP — 16 , при этом в регистр SP сохраняется новый адрес. Визуально это могло бы выглядеть так (адреса условны):

Сохранение в стек в ассемблере ARM 64

Загрузка данных из стека в регистры

Для загрузки данных из стека в регистр X0 в стек применяется инструкция:

Эта инструкция помещает данные по адресу, который хранится в регистре SP , из стека в регистр Xn и затем добавляет N байт (выравнивае) к адресу в SP .

Здесь загружаем из стека данные, адрес которых хранится в SP , в регистр X5. Затем к адресу в SP прибавляется 16.

Визуально это могло бы выглядеть так (адреса условны):

Загрузка данных из стека в регистр в ассемблере ARM 64

Загрузка и сохранение в два регистра

Инструкции STP/LDP позволяют сохранить/загрузить данные сразу в два регистра:

Здесь загружаем в регистры X2 и X3 первое и второе число из набора bytes . Затем эти значения загружаем в стек:

Похожие статьи