Nikita Mandrykin

Практикуюсь в программировании. C • Python • Assembly • Linux

Назад

Массивы в NASM: Адресация памяти, Циклы и Эффективность

Словарь основных терминов

Термин Значение Пример
Массив Последовательность элементов одного типа в памяти [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

ПРЕИМУЩЕСТВА

  1. Компактность: Одна строка вместо сотен
  2. Цикличность: Легко обходить for(i=0; i<100; i++)
  3. Индексация: Доступ к любому элементу через индекс
  4. Эффективность: Непрерывный блок в памяти

Реальные применения

Строки — это массивы символов (байтов):

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 Большие числа, указатели

Практические примеры

Массив байтов (db)
section .data
    ; ASCII-коды символов
    chars db 'A', 'B', 'C', 0
    
    ; Малые числа
    ages db 25, 30, 18, 45
    
    ; Смешанное (осторожно!)
    mixed db 100, 'X', 200
Массив двойных слов (dd)
section .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 байт!
🐇 Быстро (С align)
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] = 300
По переменному индексу
section .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

Задача: Просуммировать все элементы массива.

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

Что происходит:

  1. Загружаем первый элемент как начальный максимум
  2. Проходим по остальным элементам
  3. Если текущий элемент больше максимума — обновляем максимум
  4. После цикла в 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

Эта функция преобразует число в строку, разбивая его на цифры:

  1. Деление на 10: 91 ÷ 10 = 9 остаток 1
  2. Сохраняем ‘1’: преобразуем 1 в ASCII символ '1' (код 49)
  3. Повторяем: 9 ÷ 10 = 0 остаток 9 → сохраняем '9'
  4. Результат: строка "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

⚠️ Отладка: Если вы случайно вышли за границы массива, программа упадёт с ошибкой Segmentation fault. Как найти точное место ошибки с помощью GDB, описано в статье «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты».

Дополнительные подводные камни

4. Забыли про знаковость
section .data
    arr db -1, -2, -3    ; Интерпретируется как 255, 254, 253

section .text
    mov al, [arr]
    cmp al, 0
    jl .negative         ; ❌ Не сработает! al = 255 (беззнаковое)

Решение: Используйте знаковые сравнения или явное расширение знака.

5. Перепутали lea и mov
mov rsi, [arr]       ; Загружает ЗНАЧЕНИЕ первого элемента
lea rsi, [arr]       ; Загружает АДРЕС массива

Правило: lea для получения адреса, mov для получения значения.

💡 Шпаргалка по массивам

Операция Код Примечание
Объявить массив байтов 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, lenjge .done Проверка границ
💜

Полезный материал?

Если статья помогла разобраться в теме, вы можете поддержать автора любой комфортной суммой.

Поддержать через CloudTips