Nasm


Структура программы и память

Интересный учебник

Еще интересный ресурс

Типы памяти

Регистровая память Самый быстрый способ хранения данных. Процессоры имеют набор регистров, которые могут использоваться для хранения данных. 

Оперативная память (RAM) Это основное место хранения данных программы. В отличие от регистров, доступ к памяти менее быстрый, но объём значительно больше. Оперативная память делится на несколько сегментов:

Данные (Data)
Стек (Stack)
Код (Code)
В Assembler можно использовать сегментирование для организации памяти.

Стек
Стек — структура данных, в которой операции записи и чтения выполняются по принципу “последним пришёл — первым ушёл” (LIFO). Стек используется для хранения локальных переменных, адресов возврата при вызове функций, а также для управления процессом вызова процедур.

Куча
Куча (heap) — это область памяти, в которой динамически выделяются блоки памяти во время выполнения программы. Работа с кучей требует явного управления памятью (например, с помощью системных вызовов операционной системы).

Структура программы

section .data
	msg db 	"hello, world", 0
section .bss
section .text
	global main
main:
	mov	rax, 1
	mov	rdi, 1
	mov rsi, msg
	mov	rdx, 12
	syscall
	mov	rax, 60
	mov	rdi, 0
	syscall

Страницы:

.data

Переменные: <varname> <type> <value>
Константы: <constant_name> equ <value>

.bss

Неинициализированные данные. resb - байт, resw - слово, resd - двойное слово, resq - двойное длинное слово

.txt Программа. 

Карта памяти:

Адреса в памяти:

section .data
    code_msg    db "Code (.text):   0x%lx", 10, 0
    data_msg    db "Data (.data):   0x%lx", 10, 0  
    heap_msg    db "Heap (break):   0x%lx", 10, 0
    stack_msg   db "Stack (rsp):    0x%lx", 10, 0
    diff_msg    db "Stack - Heap:   %ld bytes", 10, 0

section .text
    global main
    extern printf

main:
    push rbp
    mov rbp, rsp
    
    ; Адрес кода (самой функции main)
    lea rax, [rel main]
    mov rdi, code_msg
    mov rsi, rax
    call printf
    
    ; Адрес данных
    mov rdi, data_msg
    mov rsi, code_msg        ; любая метка из .data
    call printf
    
    ; Адрес кучи (break)
    mov rax, 12              ; sys_brk
    mov rdi, 0
    syscall
    mov rdi, heap_msg
    mov rsi, rax
    call printf
    
    ; Адрес стека (rsp)
    mov rdi, stack_msg
    mov rsi, rsp
    call printf
    
    ; Разница между стеком и кучей
    mov rax, 12              ; снова получаем break
    mov rdi, 0
    syscall
    mov rbx, rax             ; heap в rbx
    mov rax, rsp             ; stack в rax
    sub rax, rbx             ; stack - heap
    
    mov rdi, diff_msg
    mov rsi, rax
    call printf
    
    pop rbp
    mov rax, 0
    ret

Примерный результат выполнения:

Code (.text):   0x400500
Data (.data):   0x600800  
Heap (break):   0x1ae20000
Stack (rsp):    0x7fffffffe010
Stack - Heap:   2147358720 bytes  (примерно 2GB)

Выделение и освобождение стека: 

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    ; Текущий rsp
    mov r12, rsp

    ; Выделяем 1KB в стеке
    sub rsp, 1024
    ; rsp УМЕНЬШИЛСЯ!
    ; r12 (старый rsp) > rsp (новый rsp)
    ; Восстанавливаем
    mov rsp, rbp
    pop rbp
    ret

Выделение и освобождение кучи: 

section .text
    global main

main:
    ; Текущий break
    mov rax, 12
    mov rdi, 0
    syscall
    mov r12, rax             ; сохраняем старый break
    
    ; Увеличиваем кучу на 1KB
    mov rax, 12
    mov rdi, r12
    add rdi, 1024
    syscall
    mov r13, rax             ; новый break
    
    ; r13 > r12 - куча ВЫРОСЛА!
    
    ret

Если куча и стек встречаются, то SEGFAULT или ENOMEM при попытке выделить память. Предотвращение столкновений:

# Посмотреть текущие лимиты
ulimit -a

# Ограничить стек для теста
ulimit -s 1024  # 1MB стек
./program

# Ограничить кучу
ulimit -d 65536  # 64MB куча

# Просмотр карты памяти для процесса
cat /proc/.../maps

Проверка в коде: 

safe_stack_allocation:
    mov rax, rsp
    sub rax, desired_size
    cmp rax, [heap_upper_bound]
    jl .stack_heap_collision
    ; Иначе безопасно
    sub rsp, desired_size
    ret

.stack_heap_collision:
    ; Обработка нехватки памяти
    mov rax, -1
    ret

Динамическое выделение памяти

Выделяется через системные вызовы или стандартные библиотеки (например, malloc из libc). При старте программы выделяется куча только под текущие потребности приложения. Затем при наличии динамических элементов размер увеличивается.

Выделить дополнительную память можно через увеличение кучи и через выделение сегмента.

Работа с кучей.

Получение текущего адреса кучи (heap): 

Для x32:
mov eax, 45
mov ebx, 0
int 0x80        ; В eax вернётся текущий адрес конца кучи 

Для x64:
mov rax, 12
mov rdi, 0
syscall         ; В rax вернётся текущий адрес конца кучи 

Кучу можно использовать через sys_brk Базовое использование sys_brk: 

section .data
    success_msg db "Heap: allocated %d bytes at address 0x%lx", 10, 0
    error_msg db "Heap: allocation failed!", 10, 0

section .bss
    initial_break resq 1
    current_break resq 1

section .text
    global main
    extern printf

main:
    push rbp
    mov rbp, rsp
    
    ; 1. Получаем текущую границу кучи
    mov rax, 12                 ; sys_brk
    mov rdi, 0
    syscall
    mov [initial_break], rax
    mov [current_break], rax
    
    ; 2. Выделяем 16KB памяти
    mov rdi, rax
    add rdi, 16384              ; 16 * 1024 = 16384 байт
    mov rax, 12                 ; sys_brk
    syscall
    
    ; 3. Проверяем успешность
    cmp rax, [current_break]
    jle .error
    
    mov [current_break], rax    ; сохраняем новую границу
    
    ; 4. Используем выделенную память
    mov rdi, [initial_break]    ; начало выделенной области
    
    ; Записываем некоторые данные
    mov byte [rdi], 'H'
    mov byte [rdi + 1], 'e'
    mov byte [rdi + 2], 'l'
    mov byte [rdi + 3], 'l'
    mov byte [rdi + 4], 'o'
    mov byte [rdi + 5], 0
    
    ; Выводим информацию
    mov rdi, success_msg
    mov rsi, 16384              ; размер
    mov rdx, [initial_break]    ; адрес
    mov rax, 0
    call printf
    
    jmp .exit

.error:
    mov rdi, error_msg
    mov rax, 0
    call printf

.exit:
    pop rbp
    mov rax, 0
    ret

Ключевые принципы

Работа с сегментами памяти.

section .data
    msg db "Hello from allocated memory in x64!", 10
    msg_len equ $ - msg

section .bss
    allocated_ptr resq 1  ; 64-битный указатель

section .text
global _start

_start:
    ; Выделяем 2 страницы памяти с помощью mmap
    mov rax, 9          ; sys_mmap
    xor rdi, rdi        ; адрес = NULL (ядро выбирает)
    mov rsi, 8192       ; размер: 2 страницы (8192 байта)
    mov rdx, 0x7        ; PROT_READ | PROT_WRITE | PROT_EXEC
    mov r10, 0x22       ; MAP_PRIVATE | MAP_ANONYMOUS
    mov r8, -1          ; без файла
    xor r9, r9          ; смещение = 0
    syscall
    
    test rax, rax
    js error            ; если ошибка (отрицательное значение)
    
    mov [allocated_ptr], rax
    
    ; Копируем строку в выделенную память
    mov rdi, rax        ; назначение
    mov rsi, msg        ; источник
    mov rcx, msg_len    ; длина
    rep movsb           ; копируем байты
    
    ; Выводим строку из выделенной памяти
    mov rax, 1          ; sys_write
    mov rdi, 1          ; stdout
    mov rsi, [allocated_ptr]
    mov rdx, msg_len
    syscall
    
    ; Освобождаем память
    mov rax, 11         ; sys_munmap
    mov rdi, [allocated_ptr]
    mov rsi, 8192
    syscall
    
    ; Выход
    mov rax, 60         ; sys_exit
    xor rdi, rdi        ; код 0
    syscall

error:
    ; Обработка ошибки
    mov rax, 60         ; sys_exit
    mov rdi, 1          ; код ошибки 1
    syscall

Работа с сегментами считается сложнее, но эффективнее в контексте выделения и освобождения.

Выполнение кода из кучи. Это называется JIT-компиляция (Just-In-Time) или динамическое генерирование кода.

section .data
    success_msg db "Executing code from heap!", 10, 0
    after_exec_msg db "Back from JIT code! Return value: %d", 10, 0

section .bss
    code_buffer resb 4096       ; буфер для кода

section .text
    global main
    extern printf, mprotect

main:
    push rbp
    mov rbp, rsp
    
    ; 1. Выделяем память под код (уже есть code_buffer)
    
    ; 2. Записываем машинный код в буфер
    mov rdi, code_buffer
    
    ; Генерируем простую функцию: mov rax, 42; ret
    mov byte [rdi], 0x48        ; mov rax, 42
    mov byte [rdi + 1], 0xC7
    mov byte [rdi + 2], 0xC0
    mov byte [rdi + 3], 0x2A
    mov byte [rdi + 4], 0x00
    mov byte [rdi + 5], 0x00
    mov byte [rdi + 6], 0x00
    mov byte [rdi + 7], 0xC3    ; ret
    
    ; 3. Делаем память исполняемой
    mov rax, 10                 ; sys_mprotect
    mov rdi, code_buffer        ; адрес
    and rdi, ~0xFFF             ; выравниваем до границы страницы
    mov rsi, 4096               ; размер
    mov rdx, 7                  ; PROT_READ|PROT_WRITE|PROT_EXEC
    syscall
    test rax, rax
    jnz .error
    
    ; 4. Вызываем код из кучи!
    mov rdi, success_msg
    call printf
    
    mov rax, code_buffer        ; получаем указатель на функцию
    call rax                    ; ВЫЗЫВАЕМ КОД ИЗ КУЧИ!
    
    ; 5. rax содержит возвращаемое значение (42)
    mov rsi, rax
    mov rdi, after_exec_msg
    call printf
    
    jmp .exit

.error:
    ; Обработка ошибки
.exit:
    pop rbp
    mov rax, 0
    ret

Важные моменты безопасности

1. mprotect обязателен, без PROT_EXEC флага будет SEGFAULT: 

; Без этого - SEGFAULT!
mov rax, 10                 ; sys_mprotect
mov rdi, code_addr
mov rsi, size
mov rdx, 7                  ; PROT_READ|PROT_WRITE|PROT_EXEC
syscall

2. W^X политика

В современных системах часто включена политика W^X (Write XOR Execute):

section .data
    code_bytes db 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3  ; mov eax,1; ret

section .text
global _start

_start:
    ; Выделяем память с правами READ|WRITE
    mov rax, 9          ; mmap
    xor rdi, rdi        ; адрес
    mov rsi, 4096       ; размер
    mov rdx, 0x3        ; PROT_READ | PROT_WRITE (но НЕ PROT_EXEC!)
    mov r10, 0x22       ; MAP_PRIVATE | MAP_ANONYMOUS
    mov r8, -1
    xor r9, r9
    syscall
    mov rbx, rax        ; сохраняем адрес
    
    ; Копируем код в выделенную память
    mov rdi, rax
    mov rsi, code_bytes
    mov rcx, 6
    rep movsb
    
    ; Пытаемся выполнить код - ЭТО ВЫЗОВЕТ SEGFAULT!
    ; call rbx  ; ← segmentation fault!
    
    ; Сначала меняем права на READ|EXECUTE
    mov rax, 10         ; mprotect
    mov rdi, rbx        ; адрес
    mov rsi, 4096       ; размер
    mov rdx, 0x5        ; PROT_READ | PROT_EXECUTE
    syscall
    
    ; Теперь можно выполнять
    call rbx            ; работает!

Отладчики и дизассемблеры

objdump

Простейший дизассемблер, есть по умолчанию в linux. 

-d только секцию кода, -D все секции. -M intel в формате intel. 

objdump -d -M intel strswap

В секции .data пытается разобрать данные на команды.

GDB

Консольный отладчик, не полноценный компилятор. Не работает без дополнительных файлов при компиляции. 

Загрузка программы для отладки: gdb <program> 

Консоль gdb:

Просмотр



list


Вывести 10 строк кода, повтор команды выводит следующие строки.


<число> Вывод конкретной строки.
x/s <адрес памяти> адрес, по которому размещена строка
x/c <адрес памяти> вывод одного символа
x/13с <адрес памяти> x/13c 0x000000 выведет 13 символов в виде строк
x/13d <адрес памяти> вывод 13 символов в виде чисел
x/13x <адрес памяти> 13 шестнадцатеричных символов
x/s  <имя ссылки> x/s &msg
Запуск

run
Выполнение загруженного приложения
disassemble <link> Дизассемблирование метки
break <link> Точка останова
info registers вывод значений регистров
step
следующий шаг 
continue
продолжение выполнения
print p <registr>

Вывод значения регистра 

print $rax
Дополнительно

quit
Выход
set <парам>

Настройка параметра. 

 set disassembly-flavor intel Установка формата отображения Intel

Компоновка и линковка

При стандартной компиляции проекта создается полноценный ELF файл, происходит выравнивание по границам страниц памяти. При использовании указателя global main подключается стартовый код стандартной библиотеки C.

Точки входа.

Точка входа определяется значением global <что-то> Варианты точек входа.

Тип программы Рекомендуемая точка входа Компиляция
Самостоятельная Linux _start ld
С использованием libc main gcc
GUI Windows WinMain Visual Studio
DLL Windows DllMain Visual Studio
Ядро ОС kmain специальный линкер
Пользовательская любое имя ld -e имя

Варианты компиляции

Команды консоли

nasm -f elf64 hello.asm -o hello.o
ld -o hello hello.o

В случае использования точки входа _start

make файл

hello: hello.o
	gcc -o hello hello.o -no-pie
hello.o: hello.asm
	nasm -f elf64 -g -F dwarf hello.asm -l hello.lst

Компилирование происходит командой make,

Процедура поиска библиотек

Директивой extern printf говорится компилятору: "я знаю где эта функция, делай все остальное". 

nm -D /lib/x86_64-linux-gnu/libc.so.6 # просмотр списка функций в библиотеке
# Какие библиотеки использует программа
ldd program

# Посмотреть неразрешенные символы в объектном файле
nm -u program.o

# Посмотреть символы в исполняемом файле
nm program | grep printf

Объединение нескольких файлов.

Вариант 1 - директива include. Она фактически вставляет текст одного файла в другой файл. Например есть файл sum.asm который мы будем включать в файл hello.asm. 

section .text
; Функция возвращает сумму чисел
; Принимает два параметра:
; rdi - первое число 
; rsi - второе число
; Результат функции возвращается через регистр rax
sum:
    mov rax, rdi      ; результат в rax
    add rax, rsi
    ret

Тогда hello.asm 

global _start
 
section .text
%INCLUDE "sum.asm"
_start:
    mov rdi, 33
    mov rsi, 44
     
    call sum
    mov rdi, rax         ; для проверки результата помещаем сумму из RAX в RDI
    mov rax, 60
    syscall

Вариант 2. Раздельная компиляция. В этом случае доступные извне метки (функции, данные) объявляются с помощью директивы global. Файл sum.asm: 

global sum  ; делаем функцию sum доступной извне
 
section .text
; Функция возвращает сумму чисел
; Принимает два параметра:
; rdi - первое число 
; rsi - второе число
; Результат функции возвращается через регистр rax
sum:
    mov rax, rdi      ; результат в rax
    add rax, rsi
    ret

Файл hello.asm 

global _start
 
extern sum      ; функция sum расположена где-то во вне
 
section .text
_start:
    mov rdi, 33
    mov rsi, 44
     
    call sum
    mov rdi, rax         ; для проверки результата помещаем сумму из RAX в RDI
    mov rax, 60
    syscall

Сначала скомпилируем файл sum.asm: 

nasm -f elf64 sum.asm -o sum.o

Затем скомпилируем файл hello.asm: 

nasm -f elf64 hello.asm -o hello.o

В итоге у нас получится два разных объектных файла - sum.o и hello.o. Скомпонуем их в один исполняемый файл: 

ld hello.o sum.o -o hello


Инструкции

Арифметика и логика

Mov копирование значений.

mov destination, source

destination: регистр или память. Source: регистр, память, число. Одновременно не может из памяти в память. Должны совпадать по размерам. Для расширения нулями меньших регистров (только регистр - регистр):

movsxd dest, source     ; если dest - 64-разрядный операнд и source - 32-разрядный
movsx dest, source      ; для всех остальных комбинаций операндов

Однако с знаком будут проблемы. Для беззнакового расширения нулями movzx: 

mov al, 5
movzx rdi, cx 

Есть относительная адресация, то есть 

mov eax, [ebx + 8]      ; EAX = значение по адресу EBX + 8 Надо разобраться
mov [esi + ecx*4], edx  ; Записать EDX по адресу ESI + ECX*4

Однако разность [esi - 4] может не работать.

Значение регистра AL помещается в самый младший байт регистра RDI. Остальные байты (7 байт) регистра RDI заполняются нулями. Если не сделать заполнение нулями, то в старший из 8 байт попадет байт аргумента, дальше - некое непотребство. 

movzx rsi, byte [cura] ; если cura это байт 

lea загрузка/вычисление адреса. Есть адресная арифметика.

Add / sub сложение и вычитание 

add operand1, operand2  ; operand1 = operand1 + operand2
inc rdi    ; rdi = rdi + 1
sub rdi, rsi    ; rdi = rdi - rsi
dec rdi    ; rdi = rdi - 1

mul и imul умножает два целых числа. imul умножает числа со знаком, а mul - беззнаковые числа. Обе инструкции принимают один операнд - регистр или адрес в памяти, который умножается на значение в регистре RAX. Результат помещается в регистры RAX/RDX 

mul operand8   ; если операнд 8-разрядный, результат в AX
mul operand16  ; если операнд 16-разрядный, результат в DX:AX
mul operand32  ; если операнд 32-разрядный, результат в EDX:EAX
mul operand64  ; если операнд 64-разрядный, результат в RDX:RAX

В AX/EAX/RAX помещается младшая часть результата, а в DX/EDX/RDX - старшая. 

global _start
 
section .text
_start:
    mov rdi, 2
    mov rax, 4
    mul rdi         ; RAX = RAX * RDI
    mov rdi, rax    ; RDI = RAX = 8
    mov rax, 60
    syscall

div и idiv. idiv делит два числа со знаком, а div - беззнаковые числа. 

global _start
 
section .text
_start:
    mov rax, 0  ; обнуляем регистр
    mov ax, 22  ; 16-разрядный регистр
    mov bl, 5   ; 8-разрядный регистр
    div bl      ; AX/BL = AL =4 (результат), AH = 2 (остаток)
    movzx rdi, al   ; RDI = 4
    mov rax, 60
    syscall

При этом в x86-64 нельзя разделить два числа одинаковой разрядности, например, одно 8-разрядное на другое 8-разрядное. Если знаменатель представляет собой 8-битное значение, числитель должен быть 16-битным значением. Если же нужно разделить одно 8-битное значение без знака на другое, то необходимо дополнить числитель нулями до 16 бит, загрузив числитель в регистр AL, а затем переместив 0 в регистр AH. Отсутствие расширения AL до нуля перед выполнением div может привести к тому, что x86-64 выдаст некорректный результат.

Логические операции.

and, or, xor, not, neg, 

and reg1 reg2 reg1 = reg1 and reg2

Есть сдвиг и вращение

Переходы.

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

JMP - безусловный переход. 

jmp метка
jmp регистр
jmp адрес_в_памяти

Переход по метке: 

global _start
section .text
_start:
    mov rdi, 11         ; RDI = 11
    jmp exit            ; переходим к метке exit
    mov rdi, 22         ; не выполняется
exit:                   ; метка exit
    mov rax, 60         ; 60 - номер системного вызова exit
    syscall             

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

global _start
section .text
_start:
    mov rbx, exit       ; в регистр RBX помещаем адрес метки exit
    mov rdi, 22         ; RDI = 22
    jmp rbx             ; переходим к адресу из регистра RBX
    mov rdi, 33         ; не выполняется
exit:                   ; метка exit
    mov rax, 60         ; 60 - номер системного вызова exit
    syscall

Переход к адресу в памяти. Переменная должна быть qword, четверичное слово, которое занимает 64 бит. 

global _start
section .text
_start:
    mov rdi, 23         ; RDI = 23
    jmp [exitPtr]       ; переходим к адресу из exitPtr
    mov rdi, 33         ; не выполняется
exit:                   ; метка exit
    mov rax, 60         ; 60 - номер системного вызова exit
    syscall             ; выполняем системный вызов exit  
exitPtr: dq exit      ; переменная exitPtr хранит адрес метки exit  

Условный переход

В регистре eflags 4 бита используются для проверки состояния исполнения предыдущей команды и перехода. Инструкции,  выполняющие математические или логические операции (add, sub, and, or, xor и not) влияют на установку флагов, а инструкции загрузки данных типа mov или lea не влияют.

Флаг Описание Команда Описание
CF Флаг переноса. Беззнаковое переполнение (сумма с переносом или вычитании с заимствованием). jc переход к метке, если флаг переноса установлен
    jnc переход, если флаг переноса НЕ установлен


clc сброс флага переноса


setc установка флага переноса
OF Флаг переполнения. Переполнение со знаком jo переход к метке, если флаг переполнения установлен


jno переход к метке, если флаг переполнения не установлен
SF Флаг знака. Если старший бит результата установлен. То есть флаг знака отражает состояние старшего бита результата. js переход к метке, если флаг знака установлен


jns переход к метке, если флаг знака не установлен
ZF Флаг нуля. Если результат вычисления дает 0 jz переход к метке, если флаг нуля установлен


jnz переход к метке, если флаг нуля не установлен

Сохранение/восстановление состояния


Порядок битов для обоих операций:

    1. Флаг переноса (CF)
    2. Всегда равен 1

    3. Флаг паритетности (PF)

    4. Всегда равен 0

    5. Дополнительный флаг переноса (AF)

    6. Всегда равен 0

    7. Флаг нуля (ZF)

    8. Флаг знака (SF)

Биты 1, 3, и 5 не используются.

lahf копирует флаги состояния из регистра eflags в регистр ah


sahf сохраняет флаги состояния из регистра ah в регистр eflags

Пример перехода 

global _start
section .text
_start:
    mov al, 255
    add al, 3       ; AL = AL + 3
    jc carry_set    ; если флаг переноса установлен, переход к метке carry_set
    mov rdi, 2      ; если флаг переноса не установлен, RDI = 2
    jmp exit
carry_set:          ; если флаг переноса установлен
    mov rdi, 4      ; RDI = 4
exit:               ; метка exit
    mov rax, 60
    syscall

Сравнение cmp (от compare) сравнивает значения и устанавливает флаги. Результат сравнения используется для условного перехода. 

cmp left_operand, right_operand

Могут участвовать регистры, переменные, непосредственные операнды. Если сравнивается непосредственный операнд, то он указывается вторым. Оба операнда целые числа, числа с плавающей точкой НЕ сравниваются.

Cmp вычитает второй из первого и устанавливает флаги кода условия на основе результата вычитания. Cmp не сохраняет результат вычитания. Аналогичные команды для получения результатов сравнения:

Команда Описание
je / jz проверяет ZF == 1 выполняет переход, если оба операнда равны.
jne / jnz проверяет ZF == 0 выполняет переход, если оба операнда не равны.
ja / jnbe проверяет одновременно СF == 0 и ZF == 0. Переход, если первый операнд больше второго. Оба операнда беззнаковые.
jae / jnb проверяет СF == 0 Переход, если первый операнд больше или равен второму. Оба операнда беззнаковые. Аналогичен инструкции jnc
jb / jnae проверяет условие СF == 1 и выполняет переход, если первый операнд меньше второго. Оба операнда беззнаковые. Аналогичен инструкции jc.
jbe / jna проверяет одновременно два условия СF == 1 и ZF == 1 (достаточно, чтобы выполнялось хотя бы одно из этих условий). Выполняет переход, если первый операнд меньше или равен второму. Оба операнда беззнаковые.
jg / jnle проверяет одновременно два условия SF == OF и ZF == 0 (оба условия должны быть истинными). Выполняет переход, если первый операнд больше второго. Оба операнда со знаком.
jge / jnl проверяет условие SF == OF и выполняет переход, если первый операнд больше или равен второму. Оба операнда со знаком.
jl / jnge проверяет условие SF != OF (флаги SF и OF не должны быть равны) и выполняет переход, если первый операнд меньше второго. Оба операнда со знаком.
jle / jng проверяет одновременно два условия SF != OF и ZF == 1 (достаточно, чтобы выполнялось хотя бы одно из этих условий). Выполняет переход, если первый операнд меньше или равен второму. Оба операнда со знаком.

Через / - одинаковые операторы, и машинный код одинаковый.

Условное копирование. В зависимости от сравнения загрузить в регистр некоторое значение. 

Команда Описание
cmovc / cmovb / cmovnae копирует значение, если флаг переноса CF = 1
cmovnc / cmovnb / cmovae копирует значение, если флаг переноса CF = 0
cmovz / cmove копирует значение, если флаг нуля ZF = 1
cmovnz / cmovne копирует значение, если флаг нуля ZF = 0
cmovs копирует значение, если флаг знака SF = 1
cmovns копирует значение, если флаг знака SF = 0
cmovo копирует значение, если флаг переполнения OF = 1
cmovno копирует значение, если флаг переполнения OF = 0

Инструкции для сравнения с копированием. Здесь есть инструкции для сравнения беззнаковых чисел: 

Команда Описание
cmova копирует значение, если первый операнд больше второго (CF=0, ZF=0)
cmovnbe копирует значение, если первый операнд не меньше и не равен второму (CF=0, ZF=0)
cmovae / cmovnc / cmovnb копирует значение, если первый операнд больше или равен второму (CF=0)
cmovnb / cmovnc / cmovae копирует значение, если первый операнд не меньше второго (CF=0)
cmovb / cmovc / cmovnae копирует значение, если первый операнд меньше второго (CF=1)
cmovnae / cmovc / cmovb копирует значение, если первый операнд не больше и не равен второму (CF=1)
cmovbe копирует значение, если первый операнд меньше или равен второму (CF=1 или ZF=1)
cmovna копирует значение, если первый операнд не больше второго (CF=1 или ZF=1)

Инструкции сравнения чисел со знаком:

Команда Описание
cmovg копирует значение, если первый операнд больше второго (SF=OF или ZF=0)
cmovnle копирует значение, если первый операнд не меньше и не равен второму (SF=OF или ZF=0)
cmovge копирует значение, если первый операнд больше или равен второму (SF=OF)
cmovnl копирует значение, если первый операнд не меньше второго (SF=OF)
cmovl копирует значение, если первый операнд меньше второго (SF != OF)
cmovnge копирует значение, если первый операнд не больше и не равен второму (SF != OF)
cmovle копирует значение, если первый операнд меньше или равен второму (SF != OF или ZF=1)
cmovng копирует значение, если первый операнд не больше второго (SF != OF или ZF=1)

И две общие инструкции как для чисел со знаком, так и для беззнаковых чисел:

cmove: копирует значение, если первый операнд равен второму (ZF=1). Аналогичен инструкции cmovz

cmovne: копирует значение, если первый операнд не равен второму (ZF=0). Аналогичен инструкции cmovnz

Первый параметр этих инструкций (куда копируем) представляет либо регистр, либо переменную (16, 32 или 64-битные). Второй параметр (что копируем) - регистр общего назначения(также 16, 32 или 64-битные). 

global _start
 
section .text
_start:
    mov al, 255
    mov bl, 3
    add al, bl          ; складываем AL и BL
     
    mov rcx, 2          ; вариант, если флаг переноса сброшен (CF = 0)
    mov rdx, 4          ; вариант, если флаг переноса установлен (CF = 1)
 
    cmovnc rdi, rcx     ; Если CF = 0
    cmovc rdi, rdx      ; Если CF = 1
    mov rax, 60
    syscall

Стек

Команда Описание
push Кладёт значение в стек push eax
pop Извлекает значение из стека pop ebx
enter Создаёт стековый фрейм enter 16, 0 
leave Удаляет стековый фрейм

Циклы

Простой цикл:

global _start
 
section .text
_start:
    mov rcx, 5
    mov rdi, 0
loop:
    add rdi, 2      ; RDI = RDI + 2 
    dec rcx         ; RCX = RCX - 1 
    jnz loop        ; если флаг нуля НЕ установлен, переход обратно к метке loop         
    mov rax, 60
    syscall

Встроенный цикл:

Команда Описание
loop уменьшает на 1 число в регистре RCX и переходит к определенной метке, если RCX не равен нулю. 
loope продолжает цикл, если установлен флаг нуля
loopne повторяет цикл, если флаг нуля не установлен
jrcxz проверяет значение RCX, и если оно рано 0, то переходит к определенной метке.

Пример использования на Linux: 

global _start
 
section .text
_start:
    mov rcx, 5      ; регистр-счетчик
    mov rdi, 0
mainloop:           ; цикл
    add rdi, 2      ; некоторые действия цикла
    loop mainloop   ; уменьшаем rcx на 1, переходим к mainloop, если rcx не содержит 0
 
    mov rax, 60
    syscall

Пример для jrcxz 

global _start
  
section .text
_start:
    mov rcx, 5      ; регистр-счетчик
    mov rdi, 1
mainloop:           ; цикл
    jrcxz exit      ; если rcx = 0, то переход к метке exit
    add rdi, 2      ; некоторые действия цикла
    loop mainloop  ; уменьшаем значение в rcx на 1, переходим к метке mainloop, если rcx не содержит 0
 
exit:
    mov rax, 60
    syscall

Вложенные циклы

Для вложенных циклов нужно сохранять значение внешнего счётчика (например, в стеке). Пример: Таблица умножения (5x5)

section .text  
    global _start  

_start:  
    mov ebx, 1       ; Внешний счётчик (строки)  

outer_loop:  
    mov ecx, 1       ; Внутренний счётчик (столбцы)  

inner_loop:  
    ; Вычисляем произведение EBX * ECX  
    mov eax, ebx  
    mul ecx          ; EAX = EBX * ECX  

    ; Здесь можно вывести EAX (пропущено для краткости)  

    ; Увеличиваем внутренний счётчик  
    inc ecx  
    cmp ecx, 5  
    jle inner_loop  

    ; Увеличиваем внешний счётчик  
    inc ebx  
    cmp ebx, 5  
    jle outer_loop  

    ; sys_exit(0)  
    mov eax, 1  
    xor ebx, ebx  
    int 0x80  

Оптимизация циклов в ассемблере

1. Разворот цикла (Loop Unrolling) Уменьшение числа итераций за счёт повторения тела цикла внутри одной итерации.

Пример: Сумма элементов массива (4 элемента за итерацию)

section .data  
    arr dd 1, 2, 3, 4, 5, 6, 7, 8  
    len equ ($ - arr) / 4  ; 8 элементов  

section .text  
    global _start  

_start:  
    mov esi, arr        ; Указатель на массив  
    mov ecx, len / 4    ; Количество итераций (8 / 4 = 2)  
    xor eax, eax        ; Сумма = 0  

sum_loop:  
    add eax, [esi]      ; Элемент 1  
    add eax, [esi + 4]  ; Элемент 2  
    add eax, [esi + 8]  ; Элемент 3  
    add eax, [esi + 12] ; Элемент 4  
    add esi, 16         ; Сдвиг на 4 элемента (4 * 4 байта)  
    loop sum_loop  

    ; Проверка остатка (если len не кратен 4)  
    mov ecx, len % 4  
    jz done  

remainder_loop:  
    add eax, [esi]  
    add esi, 4  
    loop remainder_loop  

done:  
    ; EAX = сумма

Преимущества Недостатки
  • Уменьшение накладных расходов на проверку условия.

  • Лучшее использование конвейера процессора.

  • Увеличение размера кода.

  • Сложность обработки остатков.

2. Замена loop на dec + jnz

Инструкция loop медленнее, чем связка dec + jnz т к процессоры лучше оптимизируют dec + jnz.  Пример:

mov ecx, 100  

; Медленнее:  
; loop_label:  
;     ...  
;     loop loop_label  

; Быстрее:  
loop_label:  
    ...  
    dec ecx  
    jnz loop_label

3. Вынос инвариантов из цикла. Вычисление константных выражений до цикла. Пример:

; Плохо:  
mov ecx, 100  
loop_start:  
    mov eax, [esi]  
    add eax, 10       ; 10 — инвариант  
    mov [edi], eax  
    add esi, 4  
    add edi, 4  
    loop loop_start  

; Лучше:  
mov ecx, 100  
mov ebx, 10           ; Вынесли инвариант  
loop_start:  
    mov eax, [esi]  
    add eax, ebx  
    mov [edi], eax  
    add esi, 4  
    add edi, 4  
    loop loop_start

4. Использование регистров вместо памяти. Минимизация обращений к памяти внутри цикла. Пример:

; Плохо:  
mov ecx, 100  
loop_start:  
    mov eax, [esi]  
    add eax, [edi]    ; Чтение из памяти  
    mov [esi], eax  
    add esi, 4  
    add edi, 4  
    loop loop_start  

; Лучше:  
mov ecx, 100  
loop_start:  
    mov eax, [esi]  
    mov ebx, [edi]    ; Загрузили в регистр  
    add eax, ebx  
    mov [esi], eax  
    add esi, 4  
    add edi, 4  
    loop loop_start

5. Устранение зависимостей данных. Параллельное выполнение независимых операций. Пример:

; Плохо (зависимость по EAX):  
mov ecx, 100  
loop_start:  
    add eax, [esi]  
    add eax, [edi]    ; Ждёт завершения предыдущего ADD  
    mov [esi], eax  
    add esi, 4  
    add edi, 4  
    loop loop_start  

; Лучше:  
mov ecx, 100  
loop_start:  
    mov ebx, [esi]  
    add ebx, [edi]    ; Независимая операция  
    mov [esi], ebx  
    add esi, 4  
    add edi, 4  
    loop loop_start

6. Инструкции SIMD (SSE/AVX) Обработка нескольких данных одной командой. Пример

section .data  
    arr1 dd 1.0, 2.0, 3.0, 4.0  
    arr2 dd 5.0, 6.0, 7.0, 8.0  

section .text  
    global _start  

_start:  
    mov ecx, 4  
    mov esi, arr1  
    mov edi, arr2  

loop_start:  
    movaps xmm0, [esi]  ; Загрузка 4 float  
    movaps xmm1, [edi]  
    addps xmm0, xmm1    ; Параллельное сложение  
    movaps [esi], xmm0  
    add esi, 16  
    add edi, 16  
    sub ecx, 1  
    jnz loop_start

Разное

Представление чисел По умолчанию десятичное. Другие форматы:

Префикс

Постфикс Описание
0b b Двоичное
0x h Шестнадцатеричное

 

в

 

Регистры

Запись в часть 64-битного регистра, например в регистр AL, влияет только на биты этой части. В случае AL загрузка 8-битного значения изменяет младшие 8 битов RAX, оставляя остальные 48 бит без изменений.

Виды регистров

Название Разряд Тип Назначение

RAX

EAX

AX

AH, AL

64

32

16

8

Универсальный (Accumulator): для арифметических операций

RBX

EBX

BX

BH, BL

64

32

16

8

Универсальный (Base pointer): указатель на базу стека внутри функции

RCX

ECX

CX

CH, CL

64

32

16

8

Универсальный (Counter): для хранения счетчика цикла

RDX

EDX

DX

DH, DL

64

32

16

8

Универсальный (Data): для арифметических операций и операций ввода-вывода

R8-R15

R8D-R15D

R8W-R15W

R8B-R15B

64

32

16

8

Универсальный 8 универсальных регистров

RSP

ESP

SP

64

32

16

Указатели (Stack pointer): указатель на верхушку стека

RBP

EBP

BP

64

32

16

Указатели (Base pointer): указатель на базу стека внутри функции

RSI

ESI

SI

64

32

16

Индексы (Source index): указатель на источник при операциях с массивом

RDI

EDI

DI

64

32

16

Индексы (Destination index): указатель на место назначения в операциях с массивами

RFLAGS


Флаги Биты состояния процессора после предыдущей операции

RIP

EIP

64

32

Специальный Счетчик команд

ST0 - ST7

80

Специальные Регистры для работы с числами с плавающей точкой

YMM0-YMM15

XMM0 - XMM15

256

 

128



 

Каждый регистр можно настроить как четыре 32-битных регистра с плавающей точкой; два 64-битных регистра двойной точности с плавающей точкой; или шестнадцать 8-битных, восемь 16-битных, четыре 32-битных, два 64-битных или один 128-битный целочисленный регистр.

Регистр флагов RFLAGS:

Бит

Имя

назначение

0

CF

Флаг переноса (Carry flag):казывает, был ли при сложении перенос или заимствование при вычитании. Используется в качестве входных данных для инструкций сложения и вычитания.

2

PF

Флаг четности: устанавливается, если младшие 8 битов результата содержат четное число единиц.

4

AF

Флаг настройки: указывает, был ли при сложении перенос или заимствование при вычитании младших 4 битов.

6

ZF

Флаг нуля (Zero flag): устанавливается, если результат операции равен нулю

7

SF

Флаг знака (Sign flag): устанавливается, если результат операции отрицательный.

8

TF

Флаг прерывания выполнения (Trap flag): используется при одношаговой отладке.

9

IF

Флаг разрешения прерывания: установка этого бита разрешает аппаратные прерывания.

10

DF

Флаг направления: контролирует направление обработки. Если не установлен, то порядок от самого младшего до самого старшего адреса. Если установлен, то порядок обратный - от самого старшего до самого младшего адреса.

11

OF

Флаг переполнения (Overflow flag): если устанавлен, то операция привела к переполнению со знаком.

12-13

IOPL

Уровень привилегий ввода-вывода (I/O privilege level): уровень привилегий текущего выполняемого потока. IOPL 0 — это режим ядра, а 3 — пользовательский режим.

14

NT

Флаг вложенной задачи (Nested task flag): управляет цепочкой прерываний.

16

RF

Флаг возобновления (Resume flag): используется для обработки исключений во время отладки.

17

VM

Флаг режима виртуальной машины 8086: если установлен, режим совместимости с 8086 активен. Этот режим позволяет запускать некоторые приложения MS-DOS в контексте операционной системы в защищенном режиме.

18

AC

Флаг проверки выравнивания (Alignment check flag): если установлен, проверка выравнивания памяти активна. Например, если установлен флаг AC, сохранение 16-битного значения по нечетному адресу вызывает исключение проверки выравнивания. Процессоры x86 могут выполнять невыровненный доступ к памяти, когда этот флаг не установлен, но количество требуемых командных циклов может увеличиться.

19

VIF

Флаг виртуального прерывания (Virtual interrupt flag): виртуальная версия флага IF в виртуальном режиме 8086..

20

VIP

Флаг ожидания виртуального прерывания: Устанавливается, когда прерывание находится в состоянии ожидания в виртуальном режиме 8086.

21

ID

Флаг ID: если этот бит установлен, то поддерживается инструкция cpuid. Эта инструкция возвращает идентификатор процессора и информацию о его функциях.

Задачи

Задача 1.

Вычислить выражение: (a + b) * c - d, где a=5, b=3, c=4, d=8. Результат вывести в консоль.
global main
extern printf
section .data
    cura dd 5
    curb dd 3
    curc dd 4
    curd dd 8
    mymsg db "%d",10,0
section .text
main: 
    mov eax, 0 
    add eax, [cura] 
    add eax, [curb] 
    mov edx, [curc] 
    mul dword [curc]; in ax (a+b)*c 
    sub eax, [curd] 
    mov rsi, rax ; print results 
    mov rax, 0 
    mov rdi, mymsg 
    call printf 
    ret

Проверяет: Работу с регистрами, базовые арифметические операции.

Задача 2.

Перевернуть строку "Hello!" и вывести. Не использовать внешние функции кроме системных вызовов.
global _start
section .data
msg db "abcdefg",10,0
msg_full_len equ $-msg
msg_half_len equ (msg_full_len - 2)/2
section .text
_start:
    ; mirroring string
    mov rcx, msg_half_len
    mov r8, msg_full_len - 3
    mov r9, 0
    mov rsi, msg
loop:
    mov bl, [rsi + r8]
    mov dl, [rsi + r9]
    mov [rsi + r9], bl
    mov [rsi + r8], dl
    dec r8
    inc r9
    dec rcx
    jnz loop
    ; printing string
    mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, msg_full_len-1
    syscall
    mov rax, 60
    mov rdi, 0
    syscall

Проверяет: Работу с памятью, циклами, обработку строк.

Задача 3.

В массиве чисел [7, 2, 9, 1, 5] найти максимальный элемент и вывести его.

global main
extern printf

section .data
    nums db 7, 2, 9, 1, 5
    nums_len equ $-nums
    infostr db "Max number: %d",10,0

section .text
main:
    mov rcx, nums_len
    mov rax, 0
mainloop:
    cmp byte [nums + rcx-1], al
    ja new_max
    dec rcx
    jnz mainloop
    jmp progend
new_max:
    mov al, [nums + rcx -1]
    dec rcx
    jnz mainloop
progend:
    mov rsi, rax
    mov rax, 0 
    mov rdi, infostr 
    call printf 
    mov rax, 60
    syscall

Проверяет: Работу с массивами, условные переходы.

Задача 4. 

Реализуйте рекурсивную функцию вычисления факториала для n=5

extern printf

section .data
    msg db "Factorial: %d",10,0
    fact equ 5

section .text
    global main

main:
    mov rcx, fact
    mov rax, 1
    call factorial

    mov rsi, rax
    mov rax, 0 
    mov rdi, msg 
    call printf 
    mov rax, 60
    xor rdi, rdi
    syscall

factorial:
    mul rcx
    dec rcx
    cmp rcx, 1
    jnz factorial
    ret

Проверяет: Понимание стека, рекурсии, соглашений о вызовах.

Задача 5.

Строки и вывод данных

Завершение программы 

    mov rax, 60
    mov rdi, 0
    syscall

При использовании gcc можно 

    ret

Код возврата

Linux

Без отладчика можно смотреть состояние одного регистра за счет копирования его в регистр rdi (Linux) при завершении программы.

global _start           ; делаем метку метку _start видимой извне
 
section .text           ; объявление секции кода
_start:                 ; объявление метки _start - точки входа в программу 
    mov rdi, 23         ; помещаем в регистр rdi код возврата - 23 
    mov rax, 60         ; 60 - номер системного вызова exit
    syscall             ; выполняем системный вызов exit

Затем выполняется приложение, команда $? выводит код завершения предыдущей команды 

root@Eugene:~/asm# ./hello
root@Eugene:~/asm# echo $?
23

Windows: 

global _start       ; делаем метку метку _start видимой извне
 
section .text       ; объявление секции кода
_start:             ; метка _start - точка входа в программу
    mov rax, 23     ; помещаем в регистр rax код возврата - 23 
    ret             ; выход из программы

Получение кода возврата

echo %ERRORLEVEL%

Строки

Статичное определение строки и длины (для последующего вывода) 

section .data
	msg db "Hello!",10,0
    msg_len equ $ - msg  ; $ - текущая позиция ассемблера
    msg_half_len equ (msg_full_len - 2)/2 ; возможен такой расчет

Использование системного вызова 

global main
section .data
	msg db "Hello",10,0
    msg_len equ $ - msg
section .text
main: 
	mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, [msg_len] - 1
    syscall
    ret

Использование функции C 

global main
extern printf
section .data
  fmtint db "Result: %d",10,0
  fmtstr db "Outstring: %s",10,0
  msg db "for textout",0
...
section .text
main: 
...
; вывод числа
	mov rsi, <переменная> ; в rsi то что нужно вывести
    mov rax, 0
	mov rdi, fmtint 
	call printf 
; вывод строки
	mov rsi, msg
    mov rax, 0
	mov rdi, fmtstr 
	call printf 

Данные

Типы данных

db байт
dw слово
dd двойное слово
dq двойное длинное слово

Для строк в конце добавляется завершающий 0 (NULL). 

Массив: 

nums dq 11, 12, 13, 14, 15, 16, 17 ; семь 8-байтных чисел

Как и всегда, хранится адрес первого элемента. 

times определяет массив одинаковых элементов. 

numb:  times 10 db 2 ; десять чисел, каждое из которых равно 2, 1-байтные

Упрощенный вариант выделения памяти для массива (начальные 0): 

resb выделяет некоторое количество байт
resw выделяет некоторое количество слов (2-х байтовых чисел)
resd выделяет некоторое количество двойных слов (4-х байтовых чисел)
resq выделяет некоторое количество четверных слов (8-х байтовых чисел)

Пример: 

buffer resb 10

Значение переменной получаем при [], по умолчанию адрес переменной. В Windows лексика сложнее.

Структура:

Простое определение  - через метку и смещение

section .data
; условная структура
person:
    db "Alice",10    ; имя
    dq 34           ; возраст
 
; смещение компонентов в структуре
NAME_OFFSET equ 0
AGE_OFFSET equ 6
section .text
_start:
     
    mov rsi, person     ; в RSI - адрес строки
    mov rdi, 1          ; в RDI - дексриптор вывода в стандартный поток (консоль)
    mov rdx, AGE_OFFSET    ; в RDX - длина строки
    mov rax, 1        ; в RAX - номер функции для вывода в поток 
    syscall              ; вызываем функцию Linux
 
    mov rdi, [rsi + AGE_OFFSET]  ; в RDI - возраст

Другой способ: 

struc имя_структуры 
 
    поле_1:      тип_поля_1    размер_поля_1
    поле_2:      тип_поля_2    размер_поля_2
    ........................................
    поле_N:      тип_поля_N   размер_поля_N
 
endstruc

Пример: 

struc person
    .id:        resd 1  ; 4 байта (d=double word)
    .name:      resb 20 ; 20 байт (b=byte)
    .age:       resw 1  ; 2 байта (w=word)
endstruc

person.id = 0
person.name = 4 (потому что .id занял 4 байта)
person.age = 24 (потому что .name занял 20 байт после .id)
person_size = 26 (общий размер: 4 + 20 + 2)

Т е при использовании struc не нужно самому высчитывать адреса меток.

Создание экземпляра (выделение памяти)
Для хранения данных обычно применяются две секции - .bss (для неинициализированных данных) и .data, то соответственно мы можем создавать инициализированные и неинициализированные экземпляры структуры.

Неинициализированный экземпляр (в секции .bss) Это самый простой способ, использующий метку _size: 

section .bss
    person1: resb person_size  ; Выделить 26 байт под один экземпляр
    person2: resb person_size  ; Выделить еще 26 байт

Инициализированный экземпляр (в секции .data)
Для создания экземпляра с начальными значениями используются макросы ISTRUC, AT и IEND. 

section .data
    tom:
        istruc person      ; Начало экземпляра структуры person
            at person.id,   dd 101           ; в поле .id число 101 
            at person.name, db "Tom", 0  ; в .name строка "Tom", 0
            ; (Оставшиеся байты .name будут неявно заполнены нулями)
            at person.age, dw 2            ; в поле .age число 2
        iend                ; Завершение экземпляра структуры person

Стоит отметить, что поля структуры должны быть объявлены в том же порядке, в котором они были указаны в определении структуры.

Доступ к полям структуры осуществляется путем сложения базового адреса экземпляра структуры с меткой-смещением нужного поля:

Пример доступа к полям экземпляра tom (из .data): 

; Поместить ID в EAX
mov eax, [tom + person.id]   ; EAX = 101
 
; Поместить возраст в BX
mov bx, [tom + person.age]  ; BX = 2
 
; Получить адрес имени (например, для вызова функции)
lea esi, [tom + person.name]
; Теперь ESI указывает на строку "Tom"

 

 

Преобразование разрядности.

При несоответствии разрядности регистра и памяти желательно точно определять, что делать. 

...
section .data
nums db 1, 2, 0, 0, 0, 0, 0, 0
...
movzx rax, byte [nums]

Точка определения данных:

section .text Должны определяться либо до первой инструкции, либо после последней инструкции. Только константы.
section .data Наиболее логичная точка размещения. Но все данные в этой секции размещаются в бинарнике и затем копируются в ОП.
section .rodata Раздел только для чтения. Отличие от констант в том, что занимают память. Константы подставляются во время компиляции. Нельзя сделать массив констант.
section .bss Логичнее размещать здесь неизвестные сначала данные, resb/... Не занимается память в бинарнике,

Косвенная адресация.

Обращение по некоторому адресу: [base + (index * scale) + offset] Компоненты:

base базовый регистр, который содержит некоторый адрес. Это может быть 64-разрядный или 32-разрядный регистр общего назначения или регистр RSP
index индексный регистр, который содержит некоторый индекс относительно адреса в базовом регистре. В качестве индексного регистра также могут выступать 64-разрядный или 32-разрядный регистр общего назначения или регистр RSP
scale множитель, на который умножается значение индексного регистра. Может принимать значения 1, 2, 4 или 8
offset может представлять 32-разрядное значение в виде числа или имени переменной. Это может быть 64-разрядный регистр общего назначения или регистр RSP

Стек

LIFO. Управляется через регистр RSP. Когда программа начинает выполняться, ОС инициализирует регистр RSP адресом последней ячейки памяти в сегменте стека. Размер стека зависит от системы. На Linux х86-64 стек ограничен 2 мегабайтами.

Стек растет от больших адресов к меньшим. При начале стек выровнен по 16-байтовой границе.

Использование стека

push добавляет данные в стек. Возможно добавить 16- и 64-разрядный регистр, адрес в памяти 16- и 64-разрядного числа или значение 16- и 32-разрядной константы (32-битная констранта расширяется до 64 бит). 

При выполнении инструкции push от значения регистра RSP вычитается размер операнда. А по адресу, который хранится в стеке, помещается значение операнда.

pop получает из стека значение, адрес которого в регистре RSP. Можно сохранить в 16- и 64-разрядный регистр или адрес в памяти 16- и 64-разрядного числа. 

global _start
 
section .text
_start:
    mov rdx, 15
    push rdx            ; в стек помещаем содержимое регистра RDX
    pop rdi             ; значение из вершины стека помещаем в регистр RDI
    mov rax, 60
    syscall

Сохранение регистров в стек 

global _start
 
section .text
_start:
... 
    push rdi
    push rdx
...
    pop rdx
    pop rdi

Сохранение флагов состояния

pushfq и popfq (без аргументов) сохраняет/восстанавливает регистр RFLAGS

Восстановление стека без извлечения данных

При завершении программы нужно восстановить адрес в RSP. Можно через pop. Можно прибавть нужное значение к RSP 

global _start
 
section .text
_start:
    mov rdi, 11
    mov rdx, 33
 
    push rdi
    push rdx
 
    add rsp, 16     ; прибавляем к адресу в RSP 16 байт 
 
    mov rax, 60
    syscall

Резервирование пространства в стеке: 

global _start
 
section .text
_start:
    sub rsp, 16  ; резервируем в стеке 16 байт
     
    ; некоторая работа со стеком
 
    add rsp, 16     ; восстанавливаем значение стека
    mov rax, 60
    syscall

Косвенная адресация в стеке

Как и в случае с любым другим регистром, в отношении регистра стека RSP можно использовать косвенную адресацию и обращаться к данным в стеке без смещения указателя RSP.  

global _start
 
section .text
_start:
    push 12
    push 13
    push 14
    push 15
 
    mov rdi, [rsp+16]      ; [rsp+16] - адрес значения 13
 
    add rsp, 32     ; восстанавливаем значение стека
 
    mov rax, 60
    syscall

Функции и прерывания

Функция - набор инструкций под некоторой меткой (имя функции). Функции завершается 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. 

Соглашения о вызовах (Calling Conventions) Определяют, как передавать аргументы и кто очищает стек.

Стандартные соглашения (x86)

Соглашение Передача аргументов Очистка стека Используемые регистры
cdecl Через стек (справа налево) Вызывающий EAXECXEDX — не сохраняются
stdcall Через стек (справа налево) Вызываемая EAXECXEDX — не сохраняются
fastcall Первые 2 — ECXEDX, остальные — стек Вызываемая EAXECXEDX — не сохраняются

В случае большого объекта можно вместо значения возвратить его адрес (который занимает 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


Инструкции enter и leave

Поскольку данная схема работа с регистром %rbp довольно распространена, то для упрощения ассемблер NASM предоставляет две дополнительные инструкции. Так, вместо кода: 

push rbp
mov rbp, rsp
sub rsp, N_байтов 

Можно применять следующую инструкцию: 

enter N_байтов, 0

Инструкции enter передается выделяемое в стеке количество байт, а второй параметр - число 0. При выполнении эта инструкция сама сохранит старое значение %rbp в стек, скопирует значение rsp в rbp и выделит в стеке N_байтов.

А вместо кода 

mov rsp, rbp
pop rbp 

Можно применить 

leave

 

 

 

Системные и внешние вызовы

Syscall

Инструкция процессора, мост между ядром и непривилегированными программами. Для вызова заполняются регистры в соответствии с соглашениями ABI (Application Binary Interface). Есть обновляемая таблица системных вызовов Номер функции размещается в регистре rax, Аргументы функции последовательно в регистрах rdi, rsi, rdx, r10, r8, r9.

syscall изменяет регистры rcx и r11. В регистр RCX сохраняется предыдущее значение регистра RIP - адрес следующей инструкции, которую будут выполнять приложение после завершения системного вызова, а в RIP помещается адрес обработчика системного вызова. Также syscall изменяет регистр флагов RFLAGS в соответствии с системным вызовом, а старое значение RFLAGS сохраняется в регистр r11. Поэтому, если программа использует регистры rcx и r11, то перед выполнением системного вызова эти регистры следует сохранить, например, в стек, чтобы не потерять их содержимое.

Кроме того, системный вызов может возвращать некоторый результат, который помещается в регистр rax.