Функции

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

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

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

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

sum:
    movq $7, %rdi
    movq $5, %rsi
    addq %rsi, %rdi
    ret

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

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

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

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

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

.globl _start

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

    movq $60, %rax
    syscall
# определяем функцию sum
sum:
    movq $7, %rdi
    movq $5, %rsi
    addq %rsi, %rdi
    ret

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

.globl _start

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

    movq $60, %rax
    syscall
# определяем функцию sum
sum:
    call set_rdi    # вызывем функцию set_rdi
    movq $5, %rsi
    addq %rsi, %rdi
    ret

set_rdi:
    movq $3, %rdi 
    ret

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

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

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

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

.globl _start 

.text
_start:
    movq $3, %rdi
    movq $9, %rsi
    call sum 

    movq $60, %rax
    syscall

sum:
    pushq %rsi          # сохраняем регистр RSI в стек
    addq %rsi, %rdi     # сложение чисел - в RDI результат
    ret

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

root@Eugene:~/asm# as hello.s -o hello.o
root@Eugene:~/asm# ld hello.o -o hello
root@Eugene:~/asm# ./hello
Segmentation fault
root@Eugene:~/asm#

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

.globl _start 

.text
_start:
    movq $3, %rdi
    movq $9, %rsi
    call sum 

    movq $60, %rax
    syscall

sum:
    popq %rsi           # извлекаем из стека в регистр RSI адрес возврата
    addq %rsi, %rdi     # сложение чисел - в RDI результат
    ret

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

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

.globl _start 

.text
_start:
    movq $5, %rdi
    movq $20, %rsi
    call sum 

    addq $10, %rdi      # RDI = 15
    movq $60, %rax
    syscall

sum:
    jmp *(%rsp)         # переходим по адресу, который храниться в RSP
    addq %rsi, %rdi     # эта строка НЕ выполняется
    ret

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

addq $10, %rdi

ABI

При вызове функций в Linux применяется ряд условностей, которые еще называют ABI (Application Binary Interface или бинарный интерфейс приложения). В Linux применяемый интерфейс ABI еще называется "System V ABI". Согласно этим условностям вызываемая функция должна сохранять содержимое регистров %rbp, %rbx и %r12, %r13, %r14, %r15 (например, сохранять их в стек). Остальные регистры могут быть сохраняться по мере необходимости. Это также означает, что если вы вызываете функцию, то любой регистр, кроме указанных выше, может быть изменен во время вызова функции.

Параметры передаются в функцию через следующие регистры (в порядке следования параметров):

  1. %rdi

  2. %rsi

  3. %rdx

  4. %rcx

  5. %r8

  6. %r9

Итак, если есть только один параметр, он передается в %rdi. Если их два, первый параметр передается в %rdi, а второй — в %rsi. Если параметров более шести, то все дополнительные параметры помещаются в стек в виде чисел .quad (например, с помощью pushq). Последний параметр первым помещается в стек.

Возвращаемые значения возвращаются в %rax. Спецификация ABI также позволяет использовать %rdx, если есть второе возвращаемое значение. Обычно, если требуется больше возвращаемых значений, либо %rax будет содержать указатель на набор значений, либо входные параметры будут включать указатели на участки памяти, где должны храниться эти дополнительные возвращаемые значения.

Также согласно System V ABI, стек должен быть выровнен по размеру, кратному 16 байтам, непосредственно перед каждым вызовом функции. "Выравнивание по 16 байтам" означает, что адрес указателя стека (%rsp) должен быть кратен 16. Это не всегда обязательно, но вызовы некоторых функций приведут к сбою, если это выравнивание не будет соблюдаться. Поскольку все вызовы функций будут включать в себя сохранение адреса возврата, соответственно при вызове функции стек может быть выровнен по 8 байтам. Нередко в стек дополнительно сохраняют предыдущее значение регистра %RBP (базового указателя фрейма стека). В итоге получается 16 байт. Это упрощает управление стеком, поскольку внутри функции достаточно следует выделять в стеке размер, кратный 16 байтам. Хотя опять же это не является строго необходимым, если функция не вызывает другие функции.

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