Переход по адресу в регистре
.intel_syntax noprefix
.global main
jmp label // relative jump
main:
lea rax, [rip + label]
jmp rax // absolute jump
jmp label // relative jump
nop
label:
xor eax, eax
ret
Или так:
.intel_syntax noprefix
.global main
.data
functable:
.quad func1
.quad func2
.text
main:
jmp [rip + functable + 8]
func1:
xor eax, eax
func2:
xor ecx, ecx
ret
Выравнивание
Как правило, лучше, чтобы многобайтовые обращения к памяти были выровнены (aligned).
// начало секции лежит по максимально выровненному адресу (кратному 16)
.byte 1
.int 1 // эти 4 байта лежат по адресу, не кратному 4
.balign 4 // byte align: здесь добавит 3 байта нулей
.int 1 // эти 4 байта лежат по адресу, кратному 4
.balign 2 // не добавит ничего
.short 1
Подпрограммы
Мы хотим переиспользовать код — вызывать одну и ту же последовательность инструкций из разных точек программы.
double_eax:
sal eax, 1
jmp ... // куда?
...
jmp double_eax
// хотим продолжить исполнение здесь
...
jmp double_eax
// или здесь
Некоторые архитектуры решают это с помощью специального регистра для адреса возврата. Если бы такой был в x86, подпрограммы могли бы выглядеть так:
double_eax:
sal eax, 1
jmp return_address_register
...
lea return_address_register, [rip + 1f]
jmp double_eax
1:
...
lea return_address_register, [rip + 1f]
jmp double_eax
1:
Но в x86 принято адрес возврата класть на стек:
double_eax:
sal eax, 1
pop rdx // достаём из стека адрес возврата
jmp rdx // и переходим по нему
...
lea rax, [rip + ret1]
push rax // кладём в стек адрес возврата
// (адрес следующей инструкции после jmp)
jmp double_eax // и переходим на начало подпрограммы
ret1:
...
lea rax, [rip + ret2]
push rax
jmp double_eax
ret2:
Для этих операций (вход в подпрограмму и возвращение из неё)
есть специальные инструкции call
и ret
:
double_eax:
sal eax, 1
ret // достаём из стека адрес возврата
// и переходим по нему
...
call double_eax // кладём в стек адрес возврата
// (адрес следующей инструкции после call)
// и переходим на начало подпрограммы
...
call double_eax
В подпрограмме важно соблюдать баланс инструкций
push
и pop
, чтобы не промахнуться мимо адреса возврата.
Соглашения о вызовах
Чтобы разные люди (и компиляторы) могли совместно разрабатывать подпрограммы, им нужно договориться, как передавать в подпрограмму параметры, как возвращать результат и какие регистры подпрограмма не будет портить. Такие договорённости называются соглашениями о вызовах (calling conventions).
Стандартное соглашение на нашей платформе (Linux/x86) называется cdecl:
- первые 6 параметров передаются в регистрах rdi, rsi, rdx, rcx, r8, r9;
- прочие параметры передаются в стеке, причём лежат в памяти «по порядку» (адрес увеличивается вместе с номером аргумента);
- параметры удаляет из стека тот, кто их туда положил (то есть вызывающая функция);
- возвращаемое значение в регистре rax
(а 128-битное — в паре
rdx:rax
); - callee-saved регистры: rbx, rsp, rbp, r12–r15;
- caller-saved регистры: все остальные;
- перед инструкцией call указатель стека выровнен на 16 байт.
Вооружённые этим знанием, мы теперь можем вызывать функции на Си и быть ими вызваны:
// int foobar(int a, int b)
mov edi, a
mov esi, b
call foobar
// возвращённое значение лежит в %eax
// возможная реализация функции foobar (return a + b)
.global foobar
foobar:
// сейчас стек выглядит так: return_address ...
mov eax, edi
add eax, esi
ret
Локальные переменные
Под них мы выделяем место на стеке:
baz:
sub rsp, 8 // выделили себе 8 байт, в которых неизвестно что
push 0 // выделили себе ещё 8 байт, в которых 0
// сейчас стек выглядит так: 0 ? return_addr ...
Чтобы обращаться к локальным переменным через rsp, придётся помнить, на сколько мы этот rsp сместили:
mov [rsp + 8], 42
Стековый кадр
Принято при входе в функцию сохранять rsp в регистре rbp (base pointer), а сам rbp перед этим класть на стек:
quux:
push rbp
mov rbp, rsp
/*
stack layout: oldrbp return_addr ...
↑ rbp
local var 1: [rbp - 8]
local var 2: [rbp - 16]
*/
...
mov rsp, rbp
pop rbp
ret
Стековый кадр (stack frame):
│ ... │
├───────────────┤
│ saved rbp │ ◄─┐
│ │ │
│ │ │
│ │ │
│ return addr │ │
├───────────────┤ │
rbp→│ saved rbp │ ──┘
│ local1 │
rsp→│ local2 │
│ │
Текст (не тот, который .text
, а настоящий)
Кодировка ASCII.
greeting:
.byte 'H' // то же, что .byte 0x48
.byte 'i' // то же, что .byte 0x69
.byte ' '
.ascii "guy"
.asciz "s" // то же, что .ascii "s\0"
.asciz "Hi guys" // ещё раз та же последовательность байт