Функции
Функция - набор инструкций под некоторой меткой (имя функции). Функции завершается ret. Вызываемые функции могут вызывать другие функции.
sum:
mov rdi, 7
mov rsi, 5
add rdi, rsi
ret
Вызов функции:
call название_функции
Call помещает в стек 64-битный адрес инструкции, которая идет сразу после вызова. Это называется адресом возврата. Когда процедура завершает выполнение, для возвращения к вызывающему коду она выполняет инструкцию ret. Команда ret извлекает 64-битный адрес возврата из стека и косвенно передает управление на этот адрес.
global _start
section .text
_start:
call sum
mov rax, 60
syscall
sum:
mov rdi, 7
mov rsi, 5
add rdi, rsi
ret
Стек и функции
При вызове ret на верхушке стека должен быть адрес возврата. Иначе скорее всего будет ошибка "Segmentation fault":
Поэтому процедура должна извлекать из стека все ранее сохраненные в ней данные и извлекать ровно столько, сколько было сохранено, чтобы адрес возврата сохранялся в стеке и к концу программы оказался в верхушке стека.
Можно использовать этот адрес для выхода из функции:
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
Функции могут использовать регистры. Поэтому нужно сохранять нужные регистры перед вызовом функций.
Передача и возврат параметров
Для передачи параметров применяются регистры, стек или через глобальные переменные. Если параметров немного, то через регистры. Наиболее удобным местом для возврата результатов функции в архитектуре x86-64 являются регистры.
Как правило, результат в регистр RAX, хотя можно любой регистр общего назначения. В RAX большинство языков высокого уровня помещают результат функции. А согласноСогласно интерфейсам System ABI и Microsoft Windows ABI прицелочисленныйцелочисленный результат помещается в регистр RAX.
В случае большого объекта можно вместо значения возвратить его адрес (который занимает 8 байт).
При вызове функции доступен весь стек, выделенный в программе. Но функция может иметь свои локальные переменные. Для этого определяется фрейм стека (stack frame) - некоторая область в стеке, которая предназначена для текущей функции, включая адрес возврата, параметры и локальные переменные. Для доступа к фрейму стека предназначен регистр RBP (BP - base pointer или базовый указатель), который представляет указатель на базовый адрес фрейма стека.
global _start
section .data
nums dq 10, 20, 30, 15, 15
count equ ($-nums)/numSize ; количество элементов
numSize equ 8 ; размер каждого элемента
section .text
_start:
mov rdi, 11 ; в RDI параметр для функции sum
call sum ; после вызова в RAX - результат сложения
mov rdi, rax ; помещаем результат в RDI
mov rax, 60
syscall
sum:
; добавляем в стек число 5 - условная безымянная локальная переменная
push 5 ; RSP указывает на адрес числа 5
mov rax, rdi ; в RAX значение параметра из RDI
add rax, [rsp] ; rax = rax + [rsp] = rax + 5
add rsp, 8 ; особождаем стек
ret
Нередко значения параметров, которые передаются через регистры, также помещаются в локальные переменные. Благодаря этому мы сможем высвободить регистры для вычислений.
global _start
section .text
_start:
mov rdi, 11 ; в RDI параметр для функции sum
call sum ; после вызова в RAX - результат сложения
mov rdi, rax ; помещаем результат в RDI
mov rax, 60
syscall
sum:
sub rsp, 8 ; резервируем для двух переменных в стеке 8 байт
mov dword [rsp+4], 5 ; По адресу [rsp+4] первая локальная переменная, которая равна 5
mov dword [rsp], edi ; По адресу [rsp] вторая локальная переменная, которая равна EDI
mov eax, [rsp+4] ; в EAX значение первой переменной (5)
add eax, [rsp] ; EAX = EAX + вторая переменная (edi)
add rsp, 8 ; особождаем стек
ret
Установка имен переменных
Выше обе наших локальных переменных были безымянными. Для нас фактически они существуют лишь как смещения относительно указателя стека RSP. Однако манипулировать смещения не очень удобно, в процессе написания программы мы можем перепутать спещения. Но с помощью констант мы можем им назначить переменным определенные имена.
global _start
_a equ 4 ; смещение переменной _a относительно rsp
_b equ 0 ; смещение переменной _b относительно rsp
section .text
_start:
mov rdi, 12 ; в RDI параметр для функции sum
call sum ; после вызова в RAX - результат сложения
mov rdi, rax ; помещаем результат в RDI
mov rax, 60
syscall
sum:
sub rsp, 8 ; резервируем для двух переменных в стеке 8 байт
mov dword [rsp+_a], 5 ; По адресу (rsp+4) первая локальная переменная, которая равна 5
mov dword [rsp + _b], edi ; По адресу (rsp) вторая локальная переменная, которая равна EDI
mov eax, [rsp+_a] ; в EAX значение первой переменной
add eax , [rsp + _b] ; EAX = EAX + вторая переменная
add rsp, 8 ; особождаем стек
ret
Регистр RBP
Для управления доступом к различным частям фрейма стека Intel предоставляет специальный регистр - RBP (Base Pointer). А для доступа к объектам во фрейме стека можно использовать смещение до нужного объекта относительно адреса из регистра RBP.
Вызывающий функцию код отвечает за выделение памяти для параметров в стеке и перемещение данных параметра в соответствующее место. Инструкция call помещает адрес возврата в стек. Функция несет ответственность за создание остальной части фрейма, в частности, за добавление локальных переменных. Для этого при вызове функции значение RBP помещается в стек (поскольку при вызове функции в RBP значение вызывающего кода, и это значение надо сохранить), а значение указателя стека RSP копируется в RBP. Затем в стеке освобождается место для локальных переменных.
Для доступа к объектам во фрейме стека необходимо использовать смещение до нужного объекта относительно адреса из регистра RBP. Для обращения к параметрам, которые передаются через стек, применяется положительное смещение относительно значения регистра RBP, а для доступа к локальным переменным - отрицательное смещение. Следует с осторожностью использовать регистр RBP для общих расчетов, потому что если вы произвольно измените значение в регистре RBP, вы можете потерять доступ к параметрам текущей функции и локальным переменным.
global _start
section .text
_start:
mov rdi, 11 ; в RDI параметр для функции sum
call sum ; после вызова в RAX - результат сложения
mov rdi, rax ; помещаем результат в RDI
mov rax, 60
syscall
sum:
push rbp ; сохраняем старое значение RBP в стек
mov rbp, rsp ; копируем текущий адрес из RSP в RBP
sub rsp, 16 ; выделяем место для двух переменных по 8 байт
mov qword[rbp-8] , 7 ; По адресу [rbp-8] первая локальная переменная, равная 7
mov qword [rbp-16], rdi ; По адресу [rbp-16] вторая локальная переменная, равная RDI
mov rax, [rbp-8] ; в RAX значение из [rbp-8] - первая локальная переменная
add rax, [rbp-16] ; RAX = RAX + [rbp-16] - вторая локальная переменная
mov rsp, rbp ; восстанавливаем ранее сохраненное значение RSP
pop rbp ; восстанавливем RBP
ret
В функции sum первым делом сохраняем старое значение rbp и копируем в регистр rbp текущее значение указателя стека rsp - оно же старое значение rbp
1
2
push rbp ; сохраняем старое значение RBP в стек
mov rbp, rsp ; копируем текущий адрес из RSP в RBP
После этого регистр rbp указывает на текущее значение rsp.
Опять же у нас функция sum использует две локальных переменных. Пусть обе переменных будут представлять тип qword, то есть 64-разрядные числа, соответственно для них нужно в совокупности 16 байт:
1
sub rsp, 16
После этой инструкции rbp по прежнему указывает на старое значение rsp (оно же старое значение rbp), а адрес в rsp уменьшился на 16 байт.
Затем определяем значения локальных переменных в стеке, используя смещение относительно регистра rbp:
1
2
mov qword [rbp-8] , 7 ; По адресу [rbp-8] первая локальная переменная, равная 7
mov qword [rbp-16], rdi ; По адресу [rbp-16] вторая локальная переменная, равная RDI
То есть первая локальная переменная, которая равна 7, будет располагаться в стеке по адресу [rbp-8], а вторая переменная, которая получает значение из rdi - по адресу [rbp-16].
Далее для обращения к этим переменным применяется эти же адреса:
1
2
mov rax, [rbp-8] ; в RAX значение из [rbp-8] - первая локальная переменная
add rax, [rbp-16] ; RAX = RAX + [rbp-16] - вторая локальная переменная
Визуально это можно представить следующим образом:
------------------------------------------------------------
rsp -> | 2-я локальная переменная: [rbp-16] | 0x00E8
------------------------------------------------------------------
| 1-я локальная переменная: [rbp-8] | 0x00E8
------------------------------------------------------------------
rbp -> | Предыдущее значение rbp | 0x00F0
------------------------------------------------------------------
| Адрес возврата | 0x00F8
------------------------------------------------------------------
В данном случае мы опять же могли бы использовать константы для именования переменных:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
global _start
_a equ -8
_b equ -16
section .text
_start:
mov rdi, 11 ; в RDI параметр для функции sum
call sum ; после вызова в RAX - результат сложения
mov rdi, rax ; помещаем результат в RDI
mov rax, 60
syscall
sum:
push rbp ; сохраняем старое значение RBP в стек
mov rbp, rsp ; копируем текущий адрес из RSP в RBP
sub rsp, 16 ; выделяем место для двух переменных по 8 байт
mov qword[rbp+_a] , 7 ; По адресу [rbp-8] первая локальная переменная, равная 7
mov qword [rbp+_b], rdi ; По адресу [rbp-16] вторая локальная переменная, равная RDI
mov rax, [rbp+_a] ; в RAX значение из [rbp-8] - первая локальная переменная
add rax, [rbp+_b] ; RAX = RAX + [rbp-16] - вторая локальная переменная
mov rsp, rbp ; восстанавливаем ранее сохраненное значение RSP
pop rbp ; восстанавливем RBP
ret
Аналогичная программа на Windows:
1234567891011121314151617181920212223242526global _start _a equ -8_b equ -16 section .text_start: mov rcx, 11 ; в RCX параметр для функции sum call sum ; после вызова в RAX - результат сложения ret sum: push rbp ; сохраняем старое значение RBP в стек mov rbp, rsp ; копируем текущий адрес из RSP в RBP sub rsp, 16 ; выделяем место для двух переменных по 8 байт mov qword[rbp+_a] , 7 ; По адресу [rbp-8] первая локальная переменная, равная 7 mov qword [rbp+_b], rcx ; По адресу [rbp-16] вторая локальная переменная, равная RCX mov rax, [rbp+_a] ; в RAX значение из [rbp-8] - первая локальная переменная add rax, [rbp+_b] ; RAX = RAX + [rbp-16] - вторая локальная переменная mov rsp, rbp ; восстанавливаем ранее сохраненное значение RSP pop rbp ; восстанавливем RBP ret
Инструкции enter и leave
Поскольку данная схема работа с регистром %rbp довольно распространена, то для упрощения ассемблер NASM предоставляет две дополнительные инструкции. Так, вместо кода:
123
push rbp
mov rbp, rsp
sub rsp, N_байтов
Можно применять следующую инструкцию:
1
enter N_байтов, 0
Инструкции enter передается выделяемое в стеке количество байт, а второй параметр - число 0. При выполнении эта инструкция сама сохранит старое значение %rbp в стек, скопирует значение rsp в rbp и выделит в стеке N_байтов.
А вместо кода
12
mov rsp, rbp
pop rbp
Можно использоватьприменить специальную инструкцию - leave, которая копирует значение RBP в RSP и извлекает ранее сохраненное значение регистра RBP.
1
leave
Так, перепишем предыдущий пример, использовав эти инструкции:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
global _start
_a equ -8
_b equ -16
section .text
_start:
mov rdi, 12 ; в RDI параметр для функции sum
call sum ; после вызова в RAX - результат сложения
mov rdi, rax ; помещаем результат в RDI
mov rax, 60
syscall
sum:
enter 16, 0 ; сохраняем значения RSP и RBP и выделяем в стеке 16 байт
mov qword[rbp+_a] , 7 ; По адресу [rbp-8] первая локальная переменная, равная 7
mov qword [rbp+_b], rdi ; По адресу [rbp-16] вторая локальная переменная, равная RDI
mov rax, [rbp+_a] ; в RAX значение из [rbp-8] - первая локальная переменная
add rax, [rbp+_b] ; RAX = RAX + [rbp-16] - вторая локальная переменная
leave ; восстанавливаем ранее сохраненное значение RSP и RBP
ret
На практике считается, что инструкция enter работает медленнее, чем заменяемый ею код. Поэтому ее использование можно встречить не часто. А вот инструкция leave работает быстрее, чем заменяемый ею код, поэтому она достаточно распространена.