Параметры функции

Последнее обновление: 15.10.2023

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

Если параметров немного, распростренным способом является передача значений в функцию через регистры. Напимер, определим в программе для 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 байтам. Тогда придется выделять много пространства, большая часть которого может быть не заполнена и может расходоваться впустую.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850