Процедуры или функции в ассемблере позволяют разбить программу на подпрограммы, где каждая подпрограмма выполняет какой-то определенный набор действий.
Для определения процедуры применяется выражение
proc_name proc
где proc_name
- имя процедуры.
Завершается определение процедуры выражением
proc_name endp
где proc_name
- также имя процедуры.
Между proc_name proc
и proc_name endp
идет произвольный набор инструкций. Перед завершением процедуры помещается инструкция
ret, которая передает управление в вызывающий код. Например:
.code setReg proc ; начало процедуры setReg mov rax, 10 ret setReg endp ; конец процедуры setReg main proc ret main endp end
Здесь определены две процедуры - главная функция main и процедура setReg. В процедуре setReg устанавливается значение регистра rax. При компиляции подобной программы (допустим, она находится в файле hello.asm) с помощью команды
ml64 hello.asm /link /entry:main
благодаря флагу /entry:main
будет создавать файл, при запуске которого выполняется процедура с именем main.
Но в нашем случае эта процедура пока ничего не делает, а процедура setReg автоматически не выполняется. Чтобы выполнить одну процедуру/функцию в другой, необходимо использовать
инструкцию call, после которой указывается имя выполняемой процедуры:
call proc_name
Инструкция call помещает в стек 64-битный адрес инструкции, которая идет сразу после вызова. Значение, которое вызов помещает в стек, называется адресом возврата. Когда процедура завершает выполнение, для возвращения к вызывающему коду она выполняет инструкцию ret. Команда ret извлекает 64-битный адрес возврата из стека и косвенно передает управление на этот адрес.
Например, выполним процедуру setReg в функции main:
.code setReg proc ; начало процедуры setReg mov rax, 10 ret setReg endp ; конец процедуры setReg main proc call setReg ; вызов процедуры setReg ret main endp end
Вызываемые процедуры могут, в свою оцередь вызывать другие процедуры. Например:
.code inner proc add rax, 1 ret inner endp outer proc call inner add rax, 1 ret outer endp main proc mov rax, 1 call outer ret main endp end
Здесь функция main вызывает процедуру outer, а та вызывает процедуру inner. В итоге к завершению программы в регистре RAX будет число 3.
При работе со стеком в процедурах следует учитывать, что вызов процедуры с помощью инструкции call помещает в стек адрес возврата. При завершении процедуры инструкция ret извлечет этот адрес возврата из стека и перейдет по этому адресу. Таким образом, выполнение вернется в код, где была вызвана процедура. Поэтому при вызове инструкции ret (при завершении процедуры) адрес возврата должен быть в верхушке стека.
Но при невнимательности это требование может быть нарушено. Например:
.code sum proc push rbx ; сохраняем регистр RBX в стек add rax, rbx ret ; регистр RBX НЕ восстанавливаем sum endp main proc mov rax, 1 mov rbx, 2 call sum ret main endp end
Здесь вызывается процедура sum, в которой в стек сохраняется регистр RBX. Однако в конце процедуры регистр RBX не восстанавливается. Поэтому в качестве адреса вохврата будет рассматриваться значение регистра RBX, которое при вызовае инструкции ret будет находится в верхушке стека. В итоге поведение программы неопределено, и скорее всего она завершится ошибкой.
Другой пример - извлечение адреса возврата до завершения процедуры:
.code sum proc pop rbx ; извлекаем данные из стека в регистр RBX add rax, rbx ret ; адрес возврата неопределен sum endp main proc mov rax, 1 mov rbx, 2 call sum ret main endp end
Здесь в регистр RBX извлекаются данные из стека - по сути в него извлекается адрес возврата. В результате опять же поведение программы неопределено.
Поэтому процедура должна извлекать из стека все ранее сохраненные в ней данные и извлекать ровно столько, сколько было сохранено, чтобы адрес возврата сохранялся в стеке и к концу программы оказался в верхушке стека.