lea

Load effective address — не обращается к памяти, а загружает в регистр вычисленный адрес:

    lea 4(%esi, %edi, 8), %eax  // теперь eax = esi + 8*edi + 4

    lea (%eax, %eax, 8), %eax   // умножили eax на 9 🤡

Переход по адресу в регистре

    jmp *%eax

Например:

somelabel:
    ...

    ...
    mov $somelabel, %eax
    jmp *%eax

Или так:

func1:
    ...
func2:
    ...

    .section .rodata
functable:
    .int func1
    .int func2

    .text
    ...
    mov functable + 4, %eax
    jmp *%eax

Выравнивание

Как правило, лучше, чтобы многобайтовые обращения к памяти были выровнены (aligned).

    // начало секции
    .byte 1
    .int 1    // эти 4 байта лежат по адресу, не кратному 4

    .balign 4 // byte align: здесь добавит 3 байта нулей
    .int 1    // эти 4 байта лежат по адресу, кратному 4

    .balign 2 // не добавит ничего
    .short 1

Подпрограммы

Мы хотим переиспользовать код — вызывать одну и ту же последовательность инструкций из разных точек программы.

double_eax:
    sal $1, %eax
    jmp ...  // куда?
    

    ...
    jmp double_eax
    // хотим продолжить исполнение здесь

    ...
    jmp double_eax
    // или здесь

Некоторые архитектуры решают это с помощью специального регистра для адреса возврата. Если бы такой был в x86, подпрограммы могли бы выглядеть так:

double_eax:
    sal $1, %eax
    jmp *%return_address
    

    ...
    mov $1f, %return_address
    jmp double_eax
1:

    ...
    mov $1f, %return_address
    jmp double_eax
1:

Но в x86 принято адрес возврата класть на стек:

double_eax:
    sal $1, %eax
    pop %edx        // достаём из стека адрес возврата
    jmp *%edx       // и переходим по нему
    

    ...
    push $1f        // кладём в стек адрес возврата
                    // (адрес следующей инструкции после jmp)
    jmp double_eax  // и переходим на начало подпрограммы
1:

    ...
    push $1f
    jmp double_eax
1:

Для этих операций (вход в подпрограмму и возвращение из неё) есть специальные инструкции call и ret:

double_eax:
    sal $1, %eax
    ret             // достаём из стека адрес возврата
                    // и переходим по нему
    

    ...
    call double_eax // кладём в стек адрес возврата
                    // (адрес следующей инструкции после call)
                    // и переходим на начало подпрограммы

    ...
    call double_eax

В подпрограмме важно соблюдать баланс инструкций push и pop, чтобы не промахнуться мимо адреса возврата.

Соглашения о вызовах

Чтобы разные люди (и компиляторы) могли совместно разрабатывать подпрограммы, им нужно договориться, как передавать в подпрограмму параметры, как возвращать результат и какие регистры подпрограмма не будет портить. Такие договорённости называются соглашениями о вызовах (calling conventions).

Стандартное соглашение на нашей платформе (Linux/x86) называется cdecl:

  • параметры передаются в стеке, причём лежат в памяти «по порядку» (адрес увеличивается вместе с номером аргумента);
  • параметры удаляет из стека тот, кто их туда положил (то есть вызывающая функция);
  • возвращаемое значение в регистре eax (а 64-битное — в паре eax:edx);
  • caller-saved регистры: eax, ecx и edx;
  • callee-saved регистры: все остальные.

Вооружённые этим знанием, мы теперь можем вызывать функции на Си и быть ими вызваны:

    // int foobar(int a, int b)

    pushl b
    pushl a
    call foobar
    add $8, %esp

    // возвращённое значение лежит в %eax
    // возможная реализация функции foobar
    .global foobar
foobar:
    // сейчас стек выглядит так: ra  a  b
    mov 4(%esp), %eax
    add 8(%esp), %eax
    ret

Локальные переменные

Под них мы выделяем место на стеке:

baz:
    sub $8, %esp   // выделили себе 8 байт, в которых неизвестно что
    push $0        // выделили себе 4 байта, в которых 0
    // сейчас стек выглядит так: 0  ?  ?  ra  arg1  arg2...

Чтобы обращаться к аргументами функции через esp, придётся помнить, на сколько мы этот esp сместили:

    mov 16(%esp), %eax  // достали первый аргумент

Стековый кадр

Принято при входе в функцию сохранять esp в регистре ebp (base pointer), а сам ebp соответственно в стеке:

quux:
    push %ebp
    mov %esp, %ebp

    /*
    stack layout:  oldebp  ra  arg1  arg2...
                   ↑ ebp
    
    arg1: 8(%ebp)
    arg2: 12(%ebp)...

    local var 1: -4(%ebp)
    local var 2: -8(%ebp)...
    */

    ...

    mov %ebp, %esp
    pop %ebp
    ret

Стековый кадр (stack frame):

    │   ...         │
    ├───────────────┤
    │  saved ebp    │ ◄─┐
    │               │   │
    │               │   │
    │               │   │
    │  arg2         │   │
    │  arg1         │   │
    │  return addr  │   │
    ├───────────────┤   │
ebp→│  saved ebp    │ ──┘
    │  local1       │
esp→│  local2       │
    │               │

Текст (не тот, который .text, а настоящий)

Кодировка ASCII.

greeting:
    .byte 'H'  // то же, что .byte 0x48
    .byte 'i'  // то же, что .byte 0x69
    .byte ' '
    .ascii "guy"
    .asciz "s"  // то же, что .ascii "s\0"

    .asciz "Hi guys"  // ещё раз та же последовательность байт