При вызове процедура имеет доступ ко всему пространству стека, выделенному в программе. Тем не менее каждая процедура имеет свою какую-то специфичную информацию, включая адрес возврата, параметры и локальные переменные. И чтобы разграничить данные каждой отдельной процедуры в рамках стека для процедуры выделяется область стека, которая еще называется фреймом стека (stack frame).
В качестве указателя на базовый адрес фрейма стека в архитектуре x86-64 предназначен регистр RBP (BP - base pointer или базовый указатель).
Создание фрейма стека начинается в коде, который вызывает процедуру. Вызывающий код может передавать через стек значения для параметров. Вне зависимости, передаются ли параметры, инструкция call помещает адрес возврата в стек.
Далее создание фрейма стека продолжается внутри самой процедуры. Что мы можем добавить в этот фрейм в самой процедуре? Во многих языках программирования высокого уровня есть локальные переменные,
которые доступны только в рамках какой-то определенной функции или блока кода. В ассемблере нет локальных переменных, все переменные, которые определяются в секции .data
и других секциях, являются глобальными,
доступны из любой части программы на ассемблере. Но использование стека позволяет определить данные, которые будут применяться только в данной процедуре. То есть такие данные будут локальны
по отношению к процедуре. При завершении процедуры в ней очищается данная область стека, и соответственно данные удаляются и недоступны для других процедур. Конечно,
для каких-то локальных данных мы могли бы использовать и регистры, но однако данных может быть очень много, для всего регистров может не хватить. Кроме того, немногочисленные регистры лучше использовать для вычислений.
Рассмотрим простейший пример:
.code sum proc ; добавляем в стек число 5 - условная безымянная локальная переменная push 5 ; RSP указывает на адрес числа 5 mov rax, rcx ; в RAX значение параметра из RCX add rax, [rsp] ; RAX = RAX + 5 add rsp, 8 ; особождаем стек ret sum endp main proc mov rcx, 11 ; устанавливаем параметр для процедуры sum call sum ; после вызова в RAX - результат сложения ret main endp end
Здесь процедура sum выполняет сложение двух чисел. Первое число передается через параметр - регистр RCX
Второе число мы сохраняем в стек в самой процедуре:
push 5
Пусть это будет число 5. Поскольку добавляем с помощью инструкции push, то для числа в стеке будет выделено 8 байт. После этого указатель стека RSP будет указвать на адрес, где хранится это число. Соответственно адрес возврата в стеке будет находиться по адресу [RSP+8].
В самой процедуре sum через адрес [rsp]
складываем со значением регистра-параметра RCX, а результат помещается в регистр RAX. При этом я отмечу, что в данном случае
идет речь не просто о константном числе 5, а именно о локальной переменной - некоторые данные, которые существуют во время работы процедуры и которые могут изменяться. Например, мы можем
увеличить эту переменную:
.code sum proc push 5 ; По адресу [rsp] локальная переменная, которая равна 5 add byte ptr [rsp], 2 ; увеличиваем переменную на 2 - она равна 7 mov rax, rcx ; в RAX значение параметра из RCX add rax, [rsp] ; RAX = RAX + 7 add rsp, 8 ; особождаем стек ret sum endp main proc mov rcx, 11 call sum ; RAX = 18 ret main endp end
Здесь увеличиваем условную локальную переменную на 2:
add byte ptr [rsp], 2
В данном случае рассматриваем ее как значение типа byte, так как число 5 вполне укладывается в диапазон типа byte.
Нередко значения параметров, которые передаются через регистры, также помещаются в локальные переменные. Благодаря этому мы сможем высвободить регистры для вычислений:
.code sum proc sub rsp, 8 ; выделяем для двух переменных в стеке 8 байт mov dword ptr [rsp], 5 ; По адресу [rsp] первая локальная переменная, которая равна 5 mov dword ptr [rsp+4], ecx ; По адресу [rsp+4] вторая локальная переменная, которая равна ECX mov eax, [rsp] ; в EAX значение первой переменной add eax, [rsp+4] ; EAX = EAX + вторая переменная add rsp, 8 ; особождаем стек ret sum endp main proc mov rcx, 11 call sum ; RAX = 16 ret main endp end
Для наглядности вместо инструкции push
данные добавляются посредством смещений, что позволит выделить меньшую область стека для данных. В частности, предполагаем, что
обе переменных будут представлять 4-байтные числа (dword), соответственно нам хватит 8 байт. И сначала выделяем эту область
sub rsp, 8
Потом помещаем в нее данные с помощью смещений. Учитываем, что поскольку мы не применяем инструкцию push, по умолчанию RSP указывает на адрес возврата. Чтобы обратиться к определенной области, прибавляем к адресу в RSP 0 и 4 (так как наши числа занимают 4 байта):
mov dword ptr [rsp], 5 mov dword ptr [rsp+4], ecx
Затем при операциях с переменными можно использовать смещения относительно RSP:
mov eax, [rsp] add eax, [rsp+4]
Выше обе наших локальных переменных были безымянными. Для нас фактически они существуют лишь как смещения относительно указателя стека RSP. Однако манипулировать смещения не очень удобно, в процессе написания программы мы можем перепутать спещения. Но с помощью констант мы можем им назначить переменным определенные имена:
_a equ [rsp] _b equ [rsp+4] .code sum proc sub rsp, 8 mov dword ptr _a, 6 ; переменная по адресу [rsp] mov dword ptr _b, ecx ; переменная по адресу [rsp+4] mov eax, _a add eax, _b add rsp, 8 ret sum endp main proc mov rcx, 11 call sum ; RAX = 17 ret main endp end
Здесь определены константы для смещений относительно стека под названием _a и _b:
_a equ [rsp] _b equ [rsp+4]
Названия переменных произвольные (главное, чтобы они не были зарезервированными словами в ассемблере). В итоге везде, где в программе встретятся эти константы, фактически
будут применяться адреса [rsp]
и [rsp+4]
:
mov dword ptr _a, 6 .................... mov eax, _a
Для управления доступом к различным частям фрейма стека Intel предоставляет специальный регистр - RBP (Base Pointer). А для доступа к объектам во фрейме стека можно использовать смещение до нужного объекта относительно адреса из регистра RBP.
Вызывающий процедуру отвечает за выделение памяти для параметров в стеке и перемещение данных параметра в соответствующее место. Инструкция call помещает адрес возврата в стек. Процедура несет ответственность за создание остальной части фрейма, в частности, за добавление локальных переменных. Для этого при вызове процедуры значение RBP помещается в стек (поскольку при вызове процедуры в RBP значение вызывающего кода, и это значение надо сохранить), а значение указателя стека RSP копируется в RBP. Затем в стеке освобождается место для локальных переменных.
push rbp ; срхраняем старое значение регистра RBP mov rbp, rsp ; помещаем указатель стека RSP в регистр RBP sub rsp, пространство_для_локальных переменны
Локальные переменные не должны быть кратны 8 байтам, однако весь блок локальных переменных должен иметь размер, кратный 8 байтам, чтобы RSP оставался выровненным по 8-байтовой границе (при взаимодействии с функциями C/C++ в Windows согласно Microsoft ABI блок локальных переменных должен быть кратен 16 байтам). Локальные переменные занимают пространство, равное их собственному размеру (символы - 1 байт, слова word - 2 байта и т.д.).
Прежде чем процедура возвратит управление в вызывающий код, ей необходимо очистить фрейм стека. Если процедура не имеет параметров, последовательность выхода проста. Для этого требуется всего три инструкции:
mov rsp, rbp ; удаляем локальные переменные и очищаем стек pop rbp ; восстанавливаем в RBP значение для вызывающего кода ret ; возвращаемся в вызывающий код
Место для параметров обычно очищается вызывающим кодом. Однако в принципе возможна также очистка памяти параметров и в вызванной процедуре. Для этого оператору ret передается количество байтов стека, которые надо освободить:
mov rsp, rbp ; удаляем локальные переменные и очищаем стек pop rbp ; восстанавливаем в RBP значение для вызывающего кода ret N_байтов ; возвращаемся в вызывающий код и очищаем N байтов в стеке, где лежат параметры
Также Intel представляет специальную инструкцию - leave, которая копирует значение RBP в RSP и извлекает ранее сохраненное значение регистра RBP.
leave ret N_байтов ; возвращаемся в вызывающий код и очищаем N байтов в стеке, где лежат параметры
Для доступа к объектам во фрейме стека необходимо использовать смещение до нужного объекта относительно адреса из регистра RBP. Для обращения к параметрам применяется положительное смещение относительно значения регистра RBP; для доступа к локальным переменным - отрицательное смещение. Следует с осторожностью использовать регистр RBP для общих расчетов, потому что если вы произвольно измените значение в регистре RBP, вы можете потерять доступ к параметрам текущей процедуры и локальным переменным.
Рассмотрим простейший пример:
sum proc push rbp mov rbp, rsp sub rsp, 8 ; выделяем место для двух переменных mov dword ptr[rbp-4], 7 ; По адресу [rsp-4] первая локальная переменная, равная 7 mov [rbp-8], ecx ; По адресу [rsp-8] вторая локальная переменная, равная ECX mov eax, [rbp-4] ; в EAX значение переменной из [rbp-4] add eax, [rbp-8] ; EAX = EAX + [rbp-8] mov rsp, rbp ; освобождаем стек pop rbp ; восстанавливем RBP ret sum endp main proc mov rcx, 12 call sum ; RAX = 19 ret main endp end
Опять же у нас функция sum использует две локальных переменных. Пусть обе переменных будут представлять тип dword, соответственно для них нужно в совокупности 8 байт:
push rbp mov rbp, rsp sub rsp, 8
Затем определяем значения локальных переменных в стеке, используя смещение относительно RBP:
mov dword ptr [rbp-4], 7 mov [rbp-8], ecx
То есть первая локальная переменная, которая равна 7, будет располагаться в стеке по адресу [rbp-4], а вторая переменная, которая получает значение из ECX - по адресу [rbp-8].
Далее для обращения к этим переменным применяется эти же адреса:
mov eax, [rbp-4] add eax, [rbp-8]
В данном случае мы опять же могли бы использовать константы для именования переменных:
_a equ [rbp-4] _b equ [rbp-8] .code sum proc push rbp mov rbp, rsp sub rsp, 8 ; выделяем место для двух переменных mov dword ptr _a, 7 ; локальная переменная _a равна 7 mov _b, ecx ; локальная переменная _b равна ECX mov eax, _a ; в EAX значение переменной _a add eax, _b ; EAX = EAX + _b mov rsp, rbp ; освобождаем стек pop rbp ; восстанавливем RBP ret sum endp main proc mov rcx, 12 call sum ; RAX = 19 ret main endp end
Для упрощения создания локальных переменных MASM предоставляет специальную директиву local, которая имеет следующий синтаксис:
local переменная1: тип, переменная2: тип, ... переменнаяN: тип
После директивы local
указываются определения переменных, где для каждой переменной указывается имя и через двоеточие ее тип.
Директива local
должна идти в процедуре сразу же после объявления процедуры до остальных инструкций.
Процедура может иметь более одной директивы local
. ; если имеется более одной локальной директивы, все они должны появиться вместе после объявления proc.
Пример использования директивы локальных переменных:
.code sum proc local _a:word, _b:dword ; определяем две переменных push rbp mov rbp, rsp sub rsp, 8 ; выделяем место для двух переменных mov _a, 5 mov _b, ecx movzx eax, _a add eax, _b mov rsp, rbp ; освобождаем стек pop rbp ; восстанавливем RBP ret sum endp main proc mov rcx, 22 call sum ; RAX = 27 ret main endp end
Здесь определяются две переменных - _a и _b, которые представляют типы word и dword. MASM автоматически связывает соответствующие смещения с каждой переменной из директивы local
.
MASM назначает смещения переменным, вычитая размер переменной из текущего смещения (начиная с нуля), а затем округляя до смещения, кратного размеру объекта.
При этом все равно надо выделять в стеке место для хранения локальных переменных.
Поскольку установка значения для регистра RBP, выделение места в стеке, а затем освобождение места и восстановление RBP представляют рутинную работу, которая может повторяться из процедуры в процедуру, то для упрощения управления стеком MASM позволяет сократить код, используя директиву option:
option prologue:PrologueDef option epilogue:EpilogueDef
Данные выражения ставится перед процедурой. В итоге MASM будет автоматически выполнять все действия по выделению и освобождению места в стеке и сохранению/восстановлению регистра RBP:
.code option prologue:PrologueDef option epilogue:EpilogueDef ; вручную выделять память больше не надо sum proc local _a:word, _b:dword mov _a, 6 mov _b, ecx movzx eax, _a add eax, _b ret sum endp main proc mov rcx, 22 call sum ; RAX = 28 ret main endp end