Словарь основных терминов
| Термин | Значение | Пример |
|---|---|---|
| Массив | Последовательность элементов одного типа в памяти | [10, 20, 30, 40] |
| Метка | Имя для адреса в памяти | numbers: |
| Директива | Команда ассемблеру (не процессору) | db, dw, dd |
| Адресация | Способ обращения к элементу | [numbers + 4] |
| Индекс | Номер элемента в массиве (начинается с 0) | В [10, 20, 30] индекс 1 = 20 |
| Смещение | Расстояние в байтах от начала массива | Для 2-го элемента dw: смещение = 2 байта |
| Буфер | Временная область памяти для хранения данных | buffer resb 1024 |
⏱️ 1. Первый массив за 2 минуты
Начнём с самого простого примера — массив из пяти чисел и вывод первого элемента:
section .data
numbers db 10, 20, 30, 40, 50 ; Массив из 5 байтов
section .text
global _start
_start:
mov al, [numbers] ; Загружаем первый элемент (10)
; al теперь содержит 10
mov rax, 60 ; Системный вызов exit
xor rdi, rdi
syscall
Что здесь происходит:
db(define byte) объявляет массив байтовnumbers— это метка, указывающая на начало массива[numbers]— обращение к первому элементу через квадратные скобки
Запустите этот код — он успешно скомпилируется и выполнится. Теперь разберёмся, как это работает детально.
🤔 2. Зачем нужны массивы в ассемблере?
В отличие от языков высокого уровня, в ассемблере вы управляете каждым байтом памяти вручную. Массивы здесь — это не просто удобный тип данных, а единственный способ организовать память так, чтобы код оставался читаемым и масштабируемым.
Взгляните на разницу между хранением отдельных переменных и использованием массива:
ПРОБЛЕМЫ
Представьте, что нужно сохранить 100 чисел:
section .data
num1 db 10
num2 db 20
num3 db 30
; ...ещё 97 переменных
ПОСЛЕДСТВИЯ
- Невозможно обрабатывать в цикле
- Огромный объём повторяющегося кода
- Нельзя динамически выбрать элемент
- Кошмар при масштабировании
РЕШЕНИЕ
Один массив вмещает всё:
section .data
numbers db 10, 20, 30, ..., 100
ПРЕИМУЩЕСТВА
- Компактность: Одна строка вместо сотен
- Цикличность: Легко обходить
for(i=0; i<100; i++) - Индексация: Доступ к любому элементу через индекс
- Эффективность: Непрерывный блок в памяти
Реальные применения
Обработка строк
Строки — это массивы символов (байтов):
section .data
message db "Hello, World!", 0 ; Массив символов с нуль-терминаторомТаблицы данных
Предвычисленные значения для быстрого доступа:
section .data
; Таблица квадратов чисел 0-9
squares db 0, 1, 4, 9, 16, 25, 36, 49, 64, 81Буферы ввода-вывода
Временное хранение данных:
section .bss
buffer resb 1024 ; Резервируем 1024 байта под буфер📋 3. Способы объявления массивов
В NASM есть директивы для разных размеров элементов. Выбор зависит от диапазона значений и требований к памяти.
Основные директивы
| Директива | Размер элемента | Диапазон значений | Применение |
|---|---|---|---|
db |
1 байт (8 бит) | 0 до 255 (или -128 до 127) | Символы, малые числа |
dw |
2 байта (16 бит) | 0 до 65535 (или -32768 до 32767) | Короткие числа, Unicode |
dd |
4 байта (32 бит) | 0 до 4,294,967,295 | Целые числа, адреса (32-bit) |
dq |
8 байтов (64 бит) | 0 до 18,446,744,073,709,551,615 | Большие числа, указатели |
Практические примеры
section .data
; ASCII-коды символов
chars db 'A', 'B', 'C', 0
; Малые числа
ages db 25, 30, 18, 45
; Смешанное (осторожно!)
mixed db 100, 'X', 200section .data
; Большие целые числа
populations dd 1000000, 5000000
; Числа с плавающей точкой (32-bit float)
temperatures dd 3.14, 2.71
; Можно смешивать форматы
data dd 42, 0x2A, 0b101010 ; Всё равно 42Специальные возможности
Повторяющиеся значения:
section .data
zeros db 100 dup(0) ; 100 нулевых байтов
pattern dw 10 dup(0xFFFF) ; 10 слов со значением 0xFFFFНеинициализированные массивы:
section .bss
buffer resb 512 ; Резервируем 512 байт (не инициализированы)
matrix resw 100 ; Резервируем 100 слов (200 байт)
bigdata resd 1000 ; Резервируем 1000 двойных слов (4000 байт)⚠️ Важно: Разница между .data и .bss
.data— инициализированные данные, занимают место в исполняемом файле.bss— неинициализированные данные, в файле только метаданные о размере- Используйте
.bssдля больших буферов, чтобы не раздувать размер программы. Эта и другие подобные ошибки разобраны в статье «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты».
Профессиональный приём: Выравнивание (align)
Представьте, что память — это тетрадь в клеточку, где процессор «читает» по 8 клеточек за раз.
Если ваше число случайно начинается с середины “строки” и перелезает на следующую, процессору приходится делать два чтения вместо одного и склеивать половинки. Это не только медленно, но и опасно: некоторые современные инструкции (SSE/AVX) в такой ситуации просто аварийно завершают программу.
Директива align заставляет ассемблер вставить пустые байты, чтобы данные ложились «красиво».
section .data
flag db 1 ; Адрес: 0x...00
; Следующий адрес: 0x...01 (нечетный!)
value dq 100 ; Адрес: 0x...01
; Занимает байты с 01 по 08.
; Пересекает границу 8 байт!section .data
flag db 1 ; Адрес: 0x...00
align 8 ; Вставляет 7 пустых байт (NOP)
; Следующий адрес станет 0x...08
value dq 100 ; Адрес: 0x...08
; Идеально попадает в "сетку" памяти🤔 А разве компилятор не делает это сам?
В языках высокого уровня (C, C++, Rust) компилятор автоматически добавляет отступы (padding) между переменными. Но NASM — это не компилятор. Он создаёт структуру памяти с точностью до байта именно так, как вы написали. Если вы не напишете
align, данные будут лежать вплотную, даже если это вредит производительности. В ассемблере вся ответственность за память лежит на вас.
Золотое правило выравнивания
Ставьте align X перед данными размером X байт:
| Тип данных | Размер | Директива |
|---|---|---|
dw (word) |
2 байта | align 2 |
dd (dword) |
4 байта | align 4 |
dq (qword) |
8 байт | align 8 |
| SSE / AVX | 16/32 байта | align 16 / align 32 (Обязательно!) |
🗺️ 4. Как работает адресация массивов
Концепция: Массив как улица с домами
Представьте массив как улицу с пронумерованными домами:
🏠 Дом №0 🏠 Дом №1 🏠 Дом №2 🏠 Дом №3
[10] [20] [30] [40]
Адрес: 100 Адрес: 101 Адрес: 102 Адрес: 103- Метка массива (
numbers) — это адрес первого дома (улица Пушкина, дом №0) - Каждый дом занимает определённую площадь:
db: 1 байт — маленький домdw: 2 байта — средний домdd: 4 байта — большой дом
Чтобы дойти до дома №3:
- Если дома по 1 байту: идём на
3 × 1 = 3байта вперёд - Если дома по 2 байта: идём на
3 × 2 = 6байтов вперёд - Если дома по 4 байта: идём на
3 × 4 = 12байтов вперёд
Формула адресации
Теперь та же идея в виде формулы:
$$Address = Base + (Index \times ElementSize)$$
Конкретный числовой пример:
section .data
arr dw 10, 20, 30, 40 ; Массив начинается с адреса 0x1000Если arr находится по адресу 0x1000 (база), то:
| Элемент | Индекс | Вычисление | Адрес | Значение |
|---|---|---|---|---|
arr[0] |
0 | 0x1000 + (0 × 2) |
0x1000 |
10 |
arr[1] |
1 | 0x1000 + (1 × 2) |
0x1002 |
20 |
arr[2] |
2 | 0x1000 + (2 × 2) |
0x1004 |
30 |
arr[3] |
3 | 0x1000 + (3 × 2) |
0x1006 |
40 |
Размер элемента dw = 2 байта, поэтому умножаем индекс на 2.
💡 Как узнать реальный адрес массива?
В коде вы никогда не пишете 0x1000 вручную. Адрес определяет операционная система при запуске. Чтобы получить его в регистр, используйте инструкцию LEA:
lea rsi, [arr] ; Теперь RSI содержит реальный адрес (например, 0x402010)Как увидеть адрес при отладке (GDB):
(gdb) print &arr
$1 = (<data variable, no debug info> *) 0x402010ФОРМУЛА АДРЕСАЦИИ
Для массива dw (2-байтовые элементы):
element[3] → base_address + (3 × 2)
ПОЧЕМУ ТАК?
- Индекс 0:
base + 0×2 = base - Индекс 1:
base + 1×2 = base+2 - Индекс 3:
base + 3×2 = base+6
Каждый элемент занимает 2 байта, поэтому умножаем индекс на 2.
РЕАЛИЗАЦИЯ
section .data
arr dw 10, 20, 30, 40 ; 2-байтовые элементы
section .text
mov rsi, arr ; rsi = база массива
mov rcx, 3 ; индекс элемента
; Вычисляем адрес: база + индекс×2
mov ax, [rsi + rcx*2] ; ax = arr[3] = 40
АВТОМАТИЧЕСКИЙ МАСШТАБ
NASM сам умножает rcx на 2, если указать *2.
Режимы адресации
NASM поддерживает несколько способов доступа к элементам:
section .data
arr db 10, 20, 30
section .text
; Доступ через метку напрямую
mov al, [arr] ; arr[0]
mov al, [arr+1] ; arr[1]
mov al, [arr+2] ; arr[2]section .data
arr db 10, 20, 30
section .text
; Доступ через регистр
mov rsi, arr ; rsi указывает на массив
mov al, [rsi] ; arr[0]
mov al, [rsi+1] ; arr[1]
; Индексация через регистр
mov rcx, 2
mov al, [rsi+rcx] ; arr[rcx] = arr[2]Масштабирование индекса
Для массивов с элементами больше 1 байта используйте множители *1, *2, *4, *8:
section .data
bytes db 10, 20, 30, 40 ; 1 байт/элемент
words dw 10, 20, 30, 40 ; 2 байта/элемент
dwords dd 10, 20, 30, 40 ; 4 байта/элемент
qwords dq 10, 20, 30, 40 ; 8 байтов/элемент
section .text
mov rcx, 2 ; Индекс = 2
mov al, [bytes + rcx*1] ; bytes[2] = 30
mov ax, [words + rcx*2] ; words[2] = 30
mov eax, [dwords + rcx*4] ; dwords[2] = 30
mov rax, [qwords + rcx*8] ; qwords[2] = 30💡 Совет: Как запомнить множитель
Множитель = Размер элемента в байтах:
db→*1(1 байт)dw→*2(2 байта)dd→*4(4 байта)dq→*8(8 байтов)
⚙️ 5. Типичные операции с массивами
Чтение элемента
section .data
numbers dd 100, 200, 300, 400
section .text
; Прямой доступ
mov eax, [numbers] ; eax = numbers[0] = 100
mov eax, [numbers+4] ; eax = numbers[1] = 200
mov eax, [numbers+8] ; eax = numbers[2] = 300section .data
numbers dd 100, 200, 300, 400
section .text
; Через регистр-индекс
mov rcx, 2 ; Индекс = 2
mov eax, [numbers + rcx*4] ; eax = numbers[2] = 300
; Или через указатель
lea rsi, [numbers]
mov eax, [rsi + rcx*4]Запись элемента
section .data
arr dd 10, 20, 30, 40
section .text
; Изменяем arr[1] на 999
mov dword [arr+4], 999
; Через регистр
mov rcx, 3
mov dword [arr + rcx*4], 777 ; arr[3] = 777Обход массива в цикле
Задача: Просуммировать все элементы массива.
Метод 1: Счётчик индекса
section .data
arr dd 1, 2, 3, 4, 5
len equ ($ - arr) / 4 ; Длина массива
section .text
xor eax, eax ; sum = 0
xor rcx, rcx ; i = 0
.loop:
cmp rcx, len
jge .done ; if (i >= len) goto done
add eax, [arr + rcx*4] ; sum += arr[i]
inc rcx ; i++
jmp .loop
.done:
; eax содержит сумму (15)💡 Альтернативный подход: использование указателя
Этот метод двигает указатель по массиву вместо индекса. Более эффективен, но менее интуитивен для новичков.
section .data
arr dd 1, 2, 3, 4, 5
len equ ($ - arr) / 4
section .text
xor eax, eax ; sum = 0
lea rsi, [arr] ; rsi = &arr[0]
mov rcx, len ; counter = len
.loop:
add eax, [rsi] ; sum += *rsi
add rsi, 4 ; rsi++ (двигаем на 4 байта)
dec rcx ; counter--
jnz .loop ; if (counter != 0) goto loop
; eax содержит сумму (15)Когда использовать этот метод:
- Когда индекс элемента не важен
- Для последовательной обработки всех элементов
- Когда нужна максимальная скорость
📖 Объяснение: Вычисление длины массива
len equ ($ - arr) / 4$— текущий адрес при ассемблировании (конец массива)arr— адрес начала массива$ - arr— размер массива в байтах/ 4— делим на размер элемента, получаем количество элементов
Для dd используем /4, для dw — /2, для db — /1.
Поиск элемента
Задача: Найти индекс первого вхождения числа 30 в массиве.
section .data
arr dd 10, 20, 30, 40, 30, 50
len equ ($ - arr) / 4
target dd 30
section .text
xor rcx, rcx ; i = 0
mov r8d, -1 ; found_index = -1 (не найдено)
.search_loop:
cmp rcx, len
jge .not_found
mov eax, [arr + rcx*4] ; eax = arr[i]
cmp eax, [target]
je .found ; if (eax == target) goto found
inc rcx
jmp .search_loop
.found:
mov r8, rcx ; r8 = индекс найденного элемента
jmp .end
.not_found:
; r8 уже содержит -1
.end:
; r8 содержит индекс (2) или -1Копирование массива
section .data
source dd 10, 20, 30, 40
len equ ($ - source) / 4
section .bss
dest resd 4 ; Место для копии
section .text
lea rsi, [source] ; rsi = указатель на источник
lea rdi, [dest] ; rdi = указатель на назначение
mov rcx, len ; rcx = счётчик элементов
.copy_loop:
mov eax, [rsi] ; Читаем из source
mov [rdi], eax ; Пишем в dest
add rsi, 4 ; Переходим к следующему элементу
add rdi, 4
dec rcx
jnz .copy_loop
; Альтернатива: использовать rep movsd
; mov rcx, len
; rep movsd ; Копирует rcx двойных слов из [rsi] в [rdi]🚀 Оптимизация: Инструкция REP MOVSD
lea rsi, [source]
lea rdi, [dest]
mov rcx, len
rep movsd ; Копирует rcx×4 байта из [rsi] в [rdi]rep movsd — это специальная инструкция процессора, которая повторяет movsd (move double word) rcx раз. Она быстрее ручного цикла на современных процессорах.
📝 6. Практические примеры
Простой пример: Поиск максимума
Начнём с компактного примера без вывода на экран:
section .data
numbers dd 45, 12, 89, 23, 67, 34, 91, 56
len equ ($ - numbers) / 4
section .text
global _start
_start:
; Находим максимум
mov eax, [numbers] ; max = numbers[0]
mov rcx, 1 ; i = 1
.loop:
cmp rcx, len
jge .done
mov edx, [numbers + rcx*4] ; edx = numbers[i]
cmp edx, eax
jle .skip ; if (edx <= max) skip
mov eax, edx ; max = edx
.skip:
inc rcx
jmp .loop
.done:
; eax содержит максимум (91)
; Выход с кодом = максимум (для демонстрации)
mov rdi, rax ; exit code
mov rax, 60 ; sys_exit
syscall
Что происходит:
- Загружаем первый элемент как начальный максимум
- Проходим по остальным элементам
- Если текущий элемент больше максимума — обновляем максимум
- После цикла в
eaxлежит максимальное значение
Проверка работы:
nasm -f elf64 max.asm -o max.o
ld max.o -o max
./max
echo $? # Выведет 91 — код возврата программы
Усложнённый пример: Вывод максимума на экран
Теперь добавим форматированный вывод результата:
Основная логика находится в функции
find_max(строка 30).
Функцияprint_number— дополнительная, можно пропустить при первом чтении.
section .data
numbers dd 45, 12, 89, 23, 67, 34, 91, 56
numbers_end:
len equ (numbers_end - numbers) / 4
msg_prefix db "Максимум: "
msg_prefix_len equ $ - msg_prefix
newline db 10
section .bss
digit_buffer resb 20
section .text
global _start
_start:
; Находим максимум
call find_max ; Результат в eax
; СОХРАНЯЕМ результат, так как syscall его испортит
push rax
; Выводим префикс "Максимум: "
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg_prefix
mov rdx, msg_prefix_len
syscall
; ВОССТАНАВЛИВАЕМ результат после syscall
pop rax
; Преобразуем число в строку и выводим
mov edi, eax ; Копируем eax в edi
call print_number
; Выводим перенос строки
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
; Выход
mov rax, 60
xor rdi, rdi
syscall
; Функция: находит максимум в массиве numbers
; Возвращает: eax = максимальное значение
find_max:
mov eax, [numbers] ; max = numbers[0]
mov ecx, 1 ; i = 1
.loop:
cmp ecx, len ; Сравниваем текущий индекс с длиной массива
jge .done ; if (i >= len) goto done
mov edx, [numbers + rcx*4] ; edx = numbers[i]
cmp edx, eax
jle .skip ; if (edx <= max) skip
mov eax, edx ; max = edx
.skip:
inc ecx
jmp .loop
.done:
ret
; Функция: выводит число на экран
; Параметры: rdi = число для вывода (беззнаковое)
print_number:
push rbp
mov rbp, rsp
push rbx
; Преобразуем число в строку (в обратном порядке)
lea rsi, [digit_buffer + 19]
mov byte [rsi], 0
dec rsi
mov rax, rdi
mov rbx, 10
.convert_loop:
xor rdx, rdx
div rbx
add dl, '0'
mov [rsi], dl
dec rsi
test rax, rax
jnz .convert_loop
; Вывод строки
inc rsi
lea rdx, [digit_buffer + 19]
sub rdx, rsi
mov rax, 1
mov rdi, 1
syscall
pop rbx
pop rbp
ret
Компиляция и запуск:
nasm -f elf64 max_print.asm -o max_print.o
ld max_print.o -o max_print
./max_print
Вывод:
Максимум: 91
🔍 Разбор функции print_number
Эта функция преобразует число в строку, разбивая его на цифры:
- Деление на 10:
91 ÷ 10 = 9остаток1 - Сохраняем ‘1’: преобразуем
1в ASCII символ'1'(код 49) - Повторяем:
9 ÷ 10 = 0остаток9→ сохраняем'9' - Результат: строка
"91"
Мы записываем цифры справа налево, поэтому начинаем с конца буфера.
❌ 7. Частые ошибки и их решения
При работе с массивами легко допустить ошибки, связанные с неправильной адресацией или несоответствием типов. Ниже рассмотрены самые типичные из них. Более подробный разбор и другие примеры вы найдёте в руководстве «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты», а техники отладки описаны в статье «Отладка ASM в VS Code: Настройка GDB и визуальный интерфейс».
1. НЕПРАВИЛЬНЫЙ РАЗМЕР ЭЛЕМЕНТА
arr dw 10, 20, 30
mov ax, [arr + 2] ; ❌ Получим arr[1], а не arr[2]!
ПРОБЛЕМА
Забыли умножить на размер элемента (2 байта для dw).
2. ВЫХОД ЗА ГРАНИЦЫ
arr db 1, 2, 3
mov al, [arr + 5] ; ❌ Читаем случайные данные!
ПРОБЛЕМА
Массив из 3 элементов, обращаемся к 6-му байту.
3. НЕВЕРНЫЙ РЕГИСТР
arr dw 1000
mov al, [arr] ; ❌ al — 1 байт, потеряем данные!
ПРОБЛЕМА
Используем 8-битный регистр для 16-битного значения.
1. ИСПОЛЬЗУЕМ МАСШТАБИРОВАНИЕ
arr dw 10, 20, 30
mov ax, [arr + 2*2] ; ✅ arr[2] = 30
; или
mov rcx, 2
mov ax, [arr + rcx*2]
2. ПРОВЕРЯЕМ ГРАНИЦЫ
arr db 1, 2, 3
len equ $ - arr
mov rcx, 2 ; Индекс
cmp rcx, len
jge .out_of_bounds ; ✅ Проверка перед доступом
mov al, [arr + rcx]
3. СООТВЕТСТВИЕ ТИПОВ
arr dw 1000
mov ax, [arr] ; ✅ 16-битный регистр для dw
| Директива | Регистры |
|---|---|
db |
al, bl, cl, dl |
dw |
ax, bx, cx, dx |
dd |
eax, ebx, ecx, edx |
dq |
rax, rbx, rcx, rdx |
Дополнительные подводные камни
4. Забыли про знаковость
section .data
arr db -1, -2, -3 ; Интерпретируется как 255, 254, 253
section .text
mov al, [arr]
cmp al, 0
jl .negative ; ❌ Не сработает! al = 255 (беззнаковое)Решение: Используйте знаковые сравнения или явное расширение знака.
⚠️ Отладка: Если вы случайно вышли за границы массива, программа упадёт с ошибкой
Segmentation fault. Как найти точное место ошибки с помощью GDB, описано в статье «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты».
5. Перепутали lea и mov
mov rsi, [arr] ; Загружает ЗНАЧЕНИЕ первого элемента
lea rsi, [arr] ; Загружает АДРЕС массиваПравило: lea для получения адреса, mov для получения значения.
✅ Заключение
Что вы освоили:
✅ Объявление массивов с помощью db, dw, dd, dq
✅ Адресацию через прямой доступ и индексные регистры
✅ Масштабирование индексов для элементов разных размеров
✅ Базовые операции: чтение, запись, обход, поиск, копирование
✅ Практическое применение в полноценных программах
Шпаргалка по массивам
| Операция | Код | Примечание |
|---|---|---|
| Объявить массив байтов | arr db 1, 2, 3 |
1 байт/элемент |
| Объявить массив слов | arr dw 100, 200 |
2 байта/элемент |
| Объявить массив двойных слов | arr dd 1000, 2000 |
4 байта/элемент |
| Резервировать буфер | buf resb 1024 |
В секции .bss |
| Длина массива | len equ ($ - arr) / 4 |
Для dd делим на 4 |
| Прочитать arr[0] | mov eax, [arr] |
Первый элемент |
| Прочитать arr[i] (байты) | mov al, [arr + rcx] |
rcx = индекс |
| Прочитать arr[i] (слова) | mov ax, [arr + rcx*2] |
Масштаб ×2 |
| Прочитать arr[i] (dword) | mov eax, [arr + rcx*4] |
Масштаб ×4 |
| Записать в arr[i] | mov [arr + rcx*4], eax |
Для dd |
| Загрузить адрес массива | lea rsi, [arr] |
rsi = &arr[0] |
| Цикл по массиву | cmp rcx, len → jge .done |
Проверка границ |