Параметры позволяют передавать в функцию из вне некоторые значения. Обычно для передачи параметров применяются регистры. Но поскольку количество регистров ограничено, и они могут использоваться для других целей, также можно передавать значения параметров через стек или через глобальные переменные. Можно комбинировать различные подходы.
Если параметров немного, распростренным способом является передача значений в функцию через регистры. Напимер, определим в программе для Linux простейшую функцию, которая получает получает извне два числа и складывает иx:
global _start section .text _start: mov rcx, 3 ; первый параметр для функции sum mov rdx, 4 ; второй параметр для функции sum call sum mov rax, 60 syscall ; определяем функцию sum sum: mov rdi, rcx ; в RDI копируем число из RCX add rdi, rdx ; складываем с числом из RDX ret
Здесь мы предполагаем, что два числа в функцию sum будут передаваться через регистры RCX и RDX. В функции sum помещаем в RDI первое число из RCX и складываем его с числом из RDX. В итоге после выполнения этой программы в регистре RDI будет число 7.
Аналогичная программа на Windows:
global _start section .text _start: mov rcx, 3 ; первый параметр для функции sum mov rdx, 4 ; второй параметр для функции sum call sum ret ; определяем функцию sum sum: mov rax, rcx ; в RAX копируем число из RCX add rax, rdx ; складываем с числом из RDX ret
Если мы сами пишем все функции программы на ассемблере, то мы можем установить себе свои собственные правила, каким образом, через какие именно регистры будут передаваться параметры в функцию. Однако если мы задействуем какие-то внешний функционал, например, взаимодействуем с функциями С/С++, то нам придется также применять те условности, которые накладывают эти функции и конкретные ОС. В частности, в при вызове функций C/C++ на Linux первые 6 параметров передаются последовательно через регистры RDI, RSI, RDX, RCX, R8 и R9. При вызове функций C/C++ на Windows первые 4 параметра передаются последовательно через регистры RCX, RDX, R8 и R9. Остальные возможные параметры передаются через стек. Хотя эти правила касаются передачи параметров в функции на других языках, в частности, на C/C++, но нередко они также применяются и к ассемблеру, особенно когда необходимо вызывать функции ассемблера из других языков программирования.
Еще один способ передачи параметров представляет передача через стек. Возьмем следующую программу для Linux:
global _start section .text _start: push 1 ; [rsp + 24] - 1 push 2 ; [rsp + 16] - 2 push 3 ; [rsp + 8] - 3 call sum ; [rsp] - адрес возврата add rsp, 24 ; восстанавливаем стек mov rax, 60 syscall ; определяем функцию sum sum: mov rdi, [rsp + 24] ; RDI = 1 add rdi, [rsp + 16] ; RDI = RDI + 2 = 3 add rdi, [rsp + 8] ; RDI = RDI + 3 = 6 ret
В основной части программы в функцию sum через стек передаются три параметра с помощью инструкции push
push 1 ; [rsp + 24] - 1 push 2 ; [rsp + 16] - 2 push 3 ; [rsp + 8] - 3
Поскольку инструкция push помещает в стек 8-байтные значения, то в данном случае три добавленных числа займут в стеке пространство в 24 байта.
После вызова функции sum это пространство очищается
add rsp, 24
В функции sum для получения параметров используем косвеную адресацию и смещение относительно указателя RSP. При вызовае функции sum стек условно будет выглядеть следующим образом:
RSP ----------------------0x00A0 адрес возврата --------------------------0x00A8 3 --------------------------0x00B0 2 --------------------------0x00B8 1 --------------------------0x00C0
Регистр RSP будет указывать на ячейку с адресом возврата. Соответственно, чтобы получить тот или иной параметр, нам надо использовать смещение 8, 16 или 24:
mov rdi, [rsp + 24] ; RDI = 1
В итоге после выполнения программы в регистре rdi будет число 6.
Аналогичная программа для Windows:
global _start section .text _start: push 1 ; [rsp + 24] - 1 push 2 ; [rsp + 16] - 2 push 3 ; [rsp + 8] - 3 call sum ; [rsp] - адрес возврата add rsp, 24 ; восстанавливаем стек ret ; определяем функцию sum sum: mov rax, [rsp + 24] ; RAX = 1 add rax, [rsp + 16] ; RAX = RAX + 2 = 3 add rax, [rsp + 8] ; RAX = RAX + 3 = 6 ret
Однако добавление в стек с помощью инструкции push может быть не оптимальным способом. В частности, выделенная память для стека может быть избыточно. В нашем случае для наших трех чисел необязательно выделять 24 байта, можно обойтись гораздо меньшим объемом. В этом случае вызывающий код может вручную помещать значения в нужные области стека, используя регистр RSP и смещения:
global _start section .text _start: sub rsp, 3 ; резервируем для параметров 3 байта ; помещаем в стек три числа по 1 байту mov byte [rsp + 2], 3 mov byte [rsp + 1], 4 mov byte [rsp], 5 call sum add rsp, 3 ; восстанавливаем стек mov rdi, rax ; для проверки состояния rax перемещаем значение в rdi mov rax, 60 syscall ; определяем функцию sum sum: xor rax, rax ; обнуляем регистр mov al, [rsp + 10] ; RAX = 2 add al, [rsp + 9] ; RAX = RAX + 4 = 6 add al, [rsp + 8] ; RAX = RAX + 6 = 12 ret
Здесь также в стек помещаются три числа, но теперь они представлют 1-байтные числа. Соостветственно нам нужно всего лишь 3 байта:
mov byte [rsp + 2], 3 mov byte [rsp + 1], 4 mov byte [rsp], 5
При получении данных в функции sum при установке смещения по прежнему учитываем 8 байтов адреса возврата:
mov al, [rsp + 10] ; RAX = 2 add al, [rsp + 9] ; RAX = RAX + 4 = 6 add al, [rsp + 8] ; RAX = RAX + 6 = 12
Аналогичная программа для Windows:
global _start section .text _start: sub rsp, 3 ; резервируем для параметров 3 байта ; помещаем в стек три числа по 1 байту mov byte [rsp + 2], 3 mov byte [rsp + 1], 4 mov byte [rsp], 5 call sum add rsp, 3 ; восстанавливаем стек ret ; определяем функцию sum sum: xor rax, rax ; обнуляем регистр mov al, [rsp + 10] ; RAX = 2 add al, [rsp + 9] ; RAX = RAX + 4 = 6 add al, [rsp + 8] ; RAX = RAX + 6 = 12 ret
Стоит отметить, что при выполнении других инструкций или при взаимодействии с функциями со сторонними API, например, с функциями C/C++ и API операционных систем есть свои ограничения на работу со стеком. Например, некоторые инструкции требуют, чтобы данные располагались в стеке по четным адресам, тогда естественно размещать в стеке по 1 байту будет проблематично. Кроме того, функции могут потребовать выравнивания стека по 16 байтам. Тогда придется выделять много пространства, большая часть которого может быть не заполнена и может расходоваться впустую.