Функции

Определение и вызов функций

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

Функции в ассемблере позволяют разбить программу на подпрограммы, где каждая подпрограмма выполняет какой-то определенный набор действий.

Фактически функция - это набор инструкций, которые проектируются на некоторую метку. И эта метка выступает в качестве имени функции. Набор инструкций функции должен завершаться инструкцией ret, которая передает управление в вызывающий код. Например:

sum:
    mov rdi, 7
    mov rsi, 5
    add rdi, rsi
    ret

В данном случае функция называется sum (имя метки). Для теста функция просто складывает два числа из регистров rdi и rsi и помещает результат в регистр rdi.

Чтобы вызвать функцию применяется инструкция call, после которой идет метка вызываемой функции:

call название_функции

Инструкция call помещает в стек 64-битный адрес инструкции, которая идет сразу после вызова. Значение, которое вызов помещает в стек, называется адресом возврата. Когда процедура завершает выполнение, для возвращения к вызывающему коду она выполняет инструкцию ret. Команда ret извлекает 64-битный адрес возврата из стека и косвенно передает управление на этот адрес.

Например, выполним вышеопределенную функцию sum в программе на Linux:

global _start

section .text
_start:
    call sum    ; вызываем функцию sum

    mov rax, 60
    syscall 

; определяем функцию sum
sum:
    mov rdi, 7
    mov rsi, 5
    add rdi, rsi
    ret

Аналогичный код на Windows:

global _start

section .text
_start:
    call sum    ; вызываем функцию sum
    ret    

; определяем функцию sum
sum:
    mov rax, 7
    mov rsi, 5
    add rax, rsi
    ret

Вызываемые функции могут, в свою оцередь вызывать другие функции. Например:

global _start

section .text
_start:
    call sum    ; вызываем функцию sum

    mov rax, 60
    syscall 

; определяем функцию sum
sum:
    call set_rdi    ; вызывем функцию set_rdi
    mov rsi, 5
    add rdi, rsi
    ret

set_rdi:
    mov rdi, 3
    ret

Здесь вызывемая функция sum сама, в свою очередь, вызывает другую функцию - set_rdi.

Стек и функции

При работе со стеком в процедурах следует учитывать, что вызов функции с помощью инструкции call помещает в стек адрес возврата. При завершении функции инструкция ret извлечет этот адрес возврата из стека и перейдет по этому адресу. Таким образом, выполнение вернется в код, где была вызвана процедура. Поэтому при вызове инструкции ret (при завершении функции) адрес возврата должен быть в верхушке стека.

Но при невнимательности это требование может быть нарушено. Например:

global _start

section .text
_start:
    mov rdi, 3
    mov rsi, 9
    call sum 

    mov rax, 60
    syscall 

; определяем функцию sum
sum:
    push rsi          ; сохраняем регистр RSI в стек
    add rdi, rsi      ; сложение чисел - в RDI результат
    ret

Здесь вызывается процедура sum, в которой в стек сохраняется регистр RSI. Однако в конце функции значение указателя стека RSP не восстанавливается. Поэтому в качестве адреса возврата будет рассматриваться значение регистра RSI, которое при вызовае инструкции ret будет находится в верхушке стека. В итоге поведение программы неопределено. На Linux такая программа скорее всего она завершится ошибкой "Segmentation fault":

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm# ld hello.o -o hello
root@Eugene:~/asm# ./hello
Segmentation fault
root@Eugene:~/asm#

Другой пример - извлечение адреса возврата до завершения функции:

global _start

section .text
_start:
    mov rdi, 3
    mov rsi, 9
    call sum 

    mov rax, 60
    syscall 

; определяем функцию sum
sum:
    pop rsi           ; извлекаем из стека в регистр RSI адрес возврата
    add rdi, rsi      ; сложение чисел - в RDI результат
    ret

Здесь в регистр RSI извлекаются данные из стека - по сути в него извлекается адрес возврата. В результате опять же поведение программы опять же неопределено и опять же скорее всего завершиться ошибкой.

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

global _start

section .text
_start:
    mov rdi, 5
    mov rsi, 20
    call sum 

    add rdi, 10      ; RDI = 15
    mov rax, 60
    syscall 

; определяем функцию sum
sum:
    jmp [rsp]        ; переходим по адресу, который храниться в RSP
    add rdi, rsi        ; эта строка НЕ выполняется
    ret

Здесь инструкция jmp [rsp] выполняет переход по адресу, который хранится в верхушке стека и соответственно в регистре RSP. А это адрес следующей инструкции после вызова функции, то есть адрес инструкции

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