До этой части учебного курса все переменные в наших программах были только глобальными — они создавались и инициализировались при запуске программы и к ним можно было обратиться из любой её части.
Локальные переменные используются для хранения промежуточных результатов во время выполнения процедуры. В отличие от глобальных, эти переменные являются временными и создаются при запуске процедуры. Для локальных переменных существует понятие области видимости — так называется область программы, в которой доступна переменная. Обычно в ассемблере область видимости ограничена процедурой, создавшей локальную переменную. Хотя возможны и более сложные варианты 😉
Создание локальных переменных
Чтобы создать локальные переменные в процедуре, необходимо выделить для них память. Эта память выделяется в стеке. Сделать это очень просто — достаточно вычесть из регистра SP значение, равное суммарному размеру всех локальных переменных в процедуре. Так как ширина стека равна 16 бит, то это значение должно быть кратно 2 байтам. При выходе из процедуры нужно восстановить указатель стека. Обычно это выполняется командой mov sp,bp (В bp сохраняется значение sp при входе в процедуру, как в случае с параметрами, передаваемыми через стек). Код процедуры с локальными переменными будет выглядеть следующим образом:
;Процедура с локальными переменными
myproc:
push bp ;Сохранение BP
mov bp,sp ;Копирование указателя стека в BP
sub sp,locals_size ;Выделение памяти для локальных переменных
...
mov sp,bp ;Восстановление указателя стека
pop bp ;Восстановление BP
ret ;Возврат из процедуры
Код, выполняемый при входе в процедуру, называют также кодом пролога, а код, выполняемый при выходе, — кодом эпилога. После выполнения кода пролога стек будет иметь такой вид:

Область стека, включающая в себя параметры процедуры, адрес возврата, локальные переменные и сохранённые регистры, называется кадром или фреймом стека. Из рисунка понятно, что для обращения к локальным переменным внутри процедуры нужно использовать отрицательные смещения относительно регистра BP. Лучше всего показать это на примере 🙂
Пример процедуры с локальными переменными
Пример очень простой. Внутри процедуры создаются 2 локальных переменных: одна размером слово и вторая размером байт. Чтобы не нарушить стек, нужно выделить 4 байта (один байт не будет использоваться). Процедуре будет передаваться через стек один параметр — x (слово без знака). Она будет вычислять выражение 1234+x/10 и возвращать его в регистре AX. Числа 1234 и 10 будут нашими локальными переменными. Конечно, в данном случае можно обойтись и без локальных переменных, они здесь только для примера.
;Пример процедуры с двумя локальными переменными
;вход: параметр x в стеке
;выход: AX = вычисленное значение
simpleproc:
push bp ;Сохранение BP
mov bp,sp ;Копирование указателя стека в BP
sub sp,4 ;Выделение 4 байт для локальных переменных
mov word[bp-4],1234 ;Инициализация первой локальной переменной
mov byte[bp-2],10 ;Инициализация второй локальной переменной
mov ax,[bp+4] ;AX = x
div byte[bp-2] ;AL = x/10
xor ah,ah ;AX = x/10
add ax,[bp-4] ;AX = 1234+x/10
mov sp,bp ;Восстановление указателя стека
pop bp ;Восстановление BP
ret 2 ;Возврат из процедуры
После выполнения кода пролога первая локальная переменная будет находиться по адресу bp-4, а вторая по адресу bp-2. Обратите внимание, что стековые переменные должны быть явно инициализированы. Так как память выделяется в стеке, то изначально в них будет всякий мусор (а вовсе не нули). Структура стека при выполнении процедуры показана на рисунке:

Команды ENTER и LEAVE
В системе команд процессоров x86 существуют также специальные команды для работы с кадром стека процедуры: ENTER [1] и LEAVE [2]. Команда ENTER [1] обычно размещается в начале процедуры. У неё два непосредственных операнда: первый операнд — размер памяти, выделяемой под локальные переменные, второй операнд — уровень вложенности. в нашем случае второй операнд будет равен 0. Тогда по действию команда будет аналогична трём следующим командам:
push bp ;\
mov bp,sp ; > или enter locals_size,0
sub sp,locals_size ;/
Второй операнд этой команды позволяет организовывать вложенные области видимости, как в некоторых языках высокого уровня. В ассемблере эти возможности используются редко.
Команда LEAVE [2] не имеет операндов и аналогична по действию двум командам:
mov sp,bp ;\
pop bp ;/ или leave
А так будет выглядеть наша процедура, если использовать команды ENTER [1] и LEAVE [2]:
;Пример процедуры с двумя локальными переменными
;вход: параметр x в стеке
;выход: AX = вычисленное значение
simpleproc2:
enter 4,0 ;Создание кадра стека
mov word[bp-4],1234 ;Инициализация первой локальной переменной
mov byte[bp-2],10 ;Инициализация второй локальной переменной
mov ax,[bp+4] ;AX = x
div byte[bp-2] ;AL = x/10
xor ah,ah ;AX = x/10
add ax,[bp-4] ;AX = 1234+x/10
leave ;Освобождение памяти, восстановление BP
ret 2 ;Возврат из процедуры
Вроде кажется, что проще, но компиляторы такой вариант практически не используют. Почему? Дело в том, что команда ENTER [1] на современных процессорах выполняется гораздо медленнее, чем пролог из 3-х команд. Самый быстрый вариант такой:
;Процедура с локальными переменными
myproc:
push bp ;Сохранение BP
mov bp,sp ;Копирование указателя стека в BP
sub sp,locals_size ;Выделение памяти для локальных переменных
...
leave ;Освобождение памяти, восстановление BP
ret ;Возврат из процедуры
Упражнение
Напишите улучшенную процедуру для ввода десятичного числа (от 0 до 255) с консоли. Буфер для ввода строки должен быть локальным, то есть выделяться в стеке при вызове процедуры. Процедура должна вызываться без параметров и возвращать введённое число в регистре AL. Ваши результаты, а также вопросы можете писать в комментариях.