LEARN X · ЗА 20 МИН

Assembly (x86-64)

Ассемблер x86-64 (NASM, Linux) за 20 минут: регистры, секции, mov, арифметика, стек, переходы, циклы, syscall и вызов функций на примерах.

Ассемблер x86-64 (синтаксис NASM, Linux) — это язык, в котором каждая инструкция почти напрямую соответствует машинной команде процессора. Вы работаете с регистрами, памятью и системными вызовами вручную. Здесь нет циклов for, объектов и сборщика мусора — только данные, адреса и переходы. Весь язык на одной странице, всё объяснено в комментариях кода. Собрать и запустить: nasm -f elf64 prog.asm && ld prog.o -o prog && ./prog.

Что такое ассемблер и регистры

Регистры — это сверхбыстрые ячейки внутри процессора. В x86-64 они 64-битные.

; Это однострочный комментарий: всё после ';' игнорируется ассемблером.
; Ассемблер NASM нечувствителен к регистру в мнемониках, но метки чувствительны.

; --- Регистры общего назначения (64 бита каждый) ---
; rax  ; аккумулятор: результаты арифметики, номер syscall, возврат функции
; rbx  ; база: обычно "сохраняемый" регистр (callee-saved)
; rcx  ; счётчик: используется инструкцией loop и сдвигами
; rdx  ; данные: старшая часть при умножении/делении, 3-й аргумент
; rsi  ; source index: источник при строковых операциях, 2-й аргумент
; rdi  ; destination index: приёмник, 1-й аргумент функции
; rbp  ; base pointer: указатель на базу кадра стека
; rsp  ; stack pointer: указатель вершины стека (НЕ трогать вручную без нужды)
; r8..r15 ; дополнительные регистры общего назначения
; rip  ; instruction pointer: адрес следующей инструкции (меняется переходами)

; --- Доступ к частям регистра rax ---
; rax (64 бита) -> eax (младшие 32) -> ax (16) -> al (младшие 8) / ah (биты 8-15)
; Запись в eax обнуляет старшие 32 бита rax. Запись в al — нет.

Секции программы

Программа делится на секции: инициализированные данные, неинициализированные данные и код.

section .data            ; инициализированные данные (есть начальное значение)
    message db "Привет", 0xA   ; db = define byte; 0xA — символ перевода строки
    msg_len equ $ - message    ; equ — константа времени сборки;
                               ; '$' — текущий адрес, '$ - message' = длина строки
    number  dq 42              ; dq = define quadword (8 байт), число 42
    pi      dd 3               ; dd = define doubleword (4 байта)

section .bss             ; неинициализированные данные (только резервируем место)
    buffer  resb 64       ; resb = reserve bytes: 64 байта под буфер
    counter resq 1        ; resq = reserve quadword: одно 8-байтовое число

section .text            ; секция с исполняемым кодом
    global _start         ; делаем метку _start видимой для компоновщика (ld)

_start:                  ; точка входа в программу
    ; ... код будет здесь ...
    nop                  ; nop = no operation, ничего не делает

Пересылка данных: mov и lea

mov копирует значение, lea вычисляет адрес.

section .text
    global _start
_start:
    mov rax, 10          ; положить число 10 в rax (немедленное значение)
    mov rbx, rax         ; скопировать значение из rax в rbx (теперь rbx = 10)
    mov rcx, [number]    ; загрузить из памяти по метке number в rcx (значение)
    mov [counter], rax   ; сохранить значение rax в память по адресу counter

    ; lea — "load effective address": кладёт АДРЕС, а не значение по адресу
    lea rsi, [message]   ; rsi = адрес начала строки message
    lea rdx, [rax + rbx*2 + 4] ; rdx = rax + rbx*2 + 4 (быстрый расчёт адреса)

    ; mov НЕ умеет копировать память -> память за одну инструкцию:
    ; mov [counter], [number]  ; ОШИБКА! Нужно через регистр-посредник:
    mov rax, [number]
    mov [counter], rax

Арифметика

section .text
    global _start
_start:
    mov rax, 7
    add rax, 3           ; rax = rax + 3  -> 10
    sub rax, 4           ; rax = rax - 4  -> 6
    inc rax              ; rax = rax + 1  -> 7  (инкремент)
    dec rax              ; rax = rax - 1  -> 6  (декремент)
    neg rax              ; rax = -rax     -> -6 (смена знака)

    ; Умножение: imul (со знаком). Двухоперандная форма удобнее:
    mov rax, 6
    imul rax, 7          ; rax = rax * 7 -> 42

    ; Беззнаковое mul: результат в паре rdx:rax (старшие:младшие 64 бита)
    mov rax, 1000000
    mov rbx, 1000000
    mul rbx              ; rdx:rax = rax * rbx; rax — младшая часть

    ; Деление: делим rdx:rax на операнд. Частное -> rax, остаток -> rdx.
    mov rax, 17
    xor rdx, rdx         ; ОБЯЗАТЕЛЬНО обнулить rdx перед делением!
    mov rbx, 5
    div rbx              ; rax = 17/5 = 3, rdx = 17 mod 5 = 2

Стек: push и pop

Стек растёт вниз (в сторону меньших адресов). rsp всегда указывает на вершину.

section .text
    global _start
_start:
    mov rax, 100
    mov rbx, 200

    push rax             ; положить rax на стек: rsp -= 8, [rsp] = rax
    push rbx             ; положить rbx: rsp -= 8, [rsp] = rbx

    ; ... здесь rax и rbx можно свободно портить ...
    mov rax, 0
    mov rbx, 0

    pop rbx              ; снять с вершины в rbx: rbx = [rsp], rsp += 8 -> 200
    pop rax              ; снять следующее в rax -> 100 (порядок зеркальный!)

    ; Важно: что положили последним — то снимаем первым (LIFO).
    ; Резерв места под локальные переменные:
    sub rsp, 16          ; выделить 16 байт на стеке
    mov qword [rsp], 5   ; записать туда число
    add rsp, 16          ; освободить место обратно

Сравнения и переходы, флаги

cmp вычитает операнды и выставляет флаги, но результат не сохраняет.

section .text
    global _start
_start:
    mov rax, 5
    mov rbx, 8

    cmp rax, rbx         ; сравнить rax и rbx (внутри: rax - rbx), флаги обновятся

    ; Условные переходы читают флаги после cmp:
    je  equal            ; jump if equal      (rax == rbx)
    jne not_equal        ; jump if not equal  (rax != rbx)
    jg  greater          ; jump if greater    (rax > rbx, со знаком)
    jl  less             ; jump if less       (rax < rbx, со знаком)
    jge ge               ; jump if greater or equal (>=)
    jle le               ; jump if less or equal    (<=)
    ; Для беззнаковых: ja (above), jb (below), jae, jbe

    jmp done             ; jmp — безусловный переход (всегда)

equal:      nop
not_equal:  nop
greater:    nop
less:       nop
ge:         nop
le:         nop
done:
    ; test — побитовое И без сохранения, удобно проверять на ноль:
    test rax, rax        ; выставит флаг нуля (ZF), если rax == 0
    jz   is_zero         ; jz == je: переход, если результат был нулём
is_zero:
    nop

Циклы

Цикл строится из cmp + переход назад, либо инструкцией loop через rcx.

section .text
    global _start
_start:
    ; --- Цикл вручную: посчитать сумму 1..5 ---
    mov rax, 0           ; rax — накопитель суммы
    mov rcx, 1           ; rcx — счётчик i = 1
.loop_start:
    cmp rcx, 5           ; сравнить i с 5
    jg  .loop_end        ; если i > 5 — выйти
    add rax, rcx         ; сумма += i
    inc rcx              ; i++
    jmp .loop_start      ; вернуться к проверке
.loop_end:
    ; теперь rax = 1+2+3+4+5 = 15
    ; Метки с точкой '.loop_start' — локальные (привязаны к ближайшей обычной метке)

    ; --- Цикл через инструкцию loop (rcx — счётчик повторений) ---
    mov rcx, 3           ; повторить 3 раза
.repeat:
    ; ... тело цикла ...
    loop .repeat         ; rcx--; если rcx != 0 — прыжок на .repeat

Системные вызовы (syscall)

Обращение к ядру Linux: номер вызова в rax, аргументы в rdi, rsi, rdx.

section .data
    text db "Hello", 0xA   ; строка + перевод строки
    len  equ $ - text      ; длина строки

section .text
    global _start
_start:
    ; write(fd=1, buf=text, count=len): номер syscall = 1
    mov rax, 1            ; номер системного вызова write
    mov rdi, 1            ; 1-й аргумент: файловый дескриптор 1 = stdout
    mov rsi, text         ; 2-й аргумент: адрес буфера со строкой
    mov rdx, len          ; 3-й аргумент: сколько байт вывести
    syscall              ; передать управление ядру

    ; exit(code=0): номер syscall = 60
    mov rax, 60          ; номер системного вызова exit
    mov rdi, 0           ; код возврата 0 (успех)
    syscall              ; программа завершается ЗДЕСЬ (иначе segfault)

    ; Соглашение syscall (Linux x86-64):
    ; rax=номер, аргументы: rdi, rsi, rdx, r10, r8, r9; результат -> rax

Работа с памятью и адресация

Квадратные скобки [...] означают "взять значение по адресу".

section .data
    arr dq 10, 20, 30, 40   ; массив из четырёх 8-байтовых чисел

section .text
    global _start
_start:
    lea rbx, [arr]          ; rbx = адрес начала массива

    mov rax, [rbx]          ; rax = arr[0] = 10 (значение по адресу rbx)
    mov rax, [rbx + 8]      ; rax = arr[1] = 20 (смещение на 8 байт = 1 элемент)
    mov rax, [rbx + 16]     ; rax = arr[2] = 30

    ; Масштабируемая адресация: [база + индекс*размер + смещение]
    mov rcx, 3              ; хотим элемент с индексом 3
    mov rax, [rbx + rcx*8]  ; rax = arr[3] = 40 (rcx*8 — каждый элемент 8 байт)

    ; Размер операнда при записи в "безымянную" память указывают явно:
    mov qword [rbx], 99     ; записать 8-байтовое 99 в arr[0]
    mov byte  [rbx + 8], 7  ; записать 1 байт
    ; Размеры: byte (1), word (2), dword (4), qword (8)

Вызов функций: call, ret и соглашение о вызовах

Аргументы передаются в rdi, rsi, rdx, rcx, r8, r9, результат — в rax.

section .text
    global _start
_start:
    mov rdi, 7           ; 1-й аргумент функции
    mov rsi, 5           ; 2-й аргумент функции
    call add_two         ; вызвать: push адрес возврата в стек, прыжок в add_two
    ; после возврата результат в rax (= 12)

    mov rdi, rax         ; передать результат как код выхода
    mov rax, 60
    syscall              ; exit(12)

; Функция: складывает rdi и rsi, результат в rax
add_two:
    push rbp             ; пролог: сохранить старый базовый указатель
    mov  rbp, rsp        ; завести новый кадр стека

    mov rax, rdi         ; rax = первый аргумент
    add rax, rsi         ; rax += второй аргумент

    mov rsp, rbp         ; эпилог: восстановить вершину стека
    pop rbp              ; вернуть старый rbp
    ret                  ; снять адрес возврата со стека и прыгнуть туда

    ; Соглашение System V (Linux):
    ; callee-saved (функция обязана сохранить): rbx, rbp, r12-r15
    ; caller-saved (может затереться): rax, rcx, rdx, rsi, rdi, r8-r11

Полный пример: вывод строки и сумма чисел

Программа считает сумму 1..10, выводит строку "Hello" и завершается с кодом, равным сумме.

; Сборка и запуск:
;   nasm -f elf64 prog.asm -o prog.o
;   ld prog.o -o prog
;   ./prog ; echo "Код возврата: $?"

section .data
    hello db "Hello, Assembly!", 0xA   ; строка для вывода + \n
    hello_len equ $ - hello            ; её длина

section .text
    global _start
_start:
    ; --- 1) Вывести строку в stdout через write ---
    mov rax, 1           ; syscall write
    mov rdi, 1           ; stdout
    mov rsi, hello       ; адрес строки
    mov rdx, hello_len   ; длина
    syscall

    ; --- 2) Посчитать сумму чисел от 1 до 10 ---
    xor rax, rax         ; rax = 0 (накопитель), xor с самим собой = обнуление
    mov rcx, 1           ; счётчик i = 1
.sum_loop:
    cmp rcx, 10          ; пока i <= 10
    jg  .sum_done
    add rax, rcx         ; сумма += i
    inc rcx              ; i++
    jmp .sum_loop
.sum_done:
    ; rax = 55

    ; --- 3) Завершиться с кодом возврата = сумма ---
    mov rdi, rax         ; код выхода = 55
    mov rax, 60          ; syscall exit
    syscall              ; проверить потом: echo $?  -> 55
Поддержать проект