Работа с массивами в ассемблере: основы и примеры обработки данных

Содержание

  1. 1.Что такое массив в ассемблере
  2. 2.Как устроена память под массив
  3. 3.Массивы в ассемблере: объявление и типы
  4. 4.Как создать массив в ассемблере
    1. 4.1.Статический массив
    2. 4.2.Динамический массив
  5. 5.Обход и обработка элементов
    1. 5.1.Цикл с индексом
  6. 6.Поиск и сравнение элементов в массиве
  7. 7.Передача массива в подпрограмму
    1. 7.1.Соглашения о передаче аргументов
    2. 7.2.Сохранение регистров при вызове
    3. 7.3.Возврат результата из подпрограммы
  8. 8.Типичные ошибки при работе с массивами
  9. 9.Подытожим: что важно помнить
Хотите стать фрилансером и начать зарабатывать удаленно?
Регистрируйтесь на Ворк24!
Хотите заказать настройку и доработку сайта?
Эксперты Ворк24 помогут!

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

Информация, которая содержится в этой статье, пригодится тем, кто изучает архитектуру процессора, пишет низкоуровневый код или хочет разобраться, что происходит «под капотом» любой программы.

По данным Stack Overflow Developer Survey, ассемблер остаётся в топ-20 языков, которые используют разработчики встроенных систем и системного программного обеспечения — то есть там, где работа с железом ведётся напрямую.

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

Массив — это несколько ячеек памяти, расположенных подряд. Каждая ячейка хранит один элемент: байт, слово (2 байта), двойное слово (4 байта) или четвёрное (8 байт). Разрыва между элементами нет — они идут один за другим без промежутков, если только вы сами их не задали.

Ключевая особенность — никакого встроенного типа «массив» не существует. Процессор видит просто участок памяти. Интерпретация этого участка — задача программиста: он решает, где начинается структура, как велик каждый элемент и сколько их всего.

📍 Важный момент!

В ассемблере нет автоматической проверки границ. Вышли за пределы объявленного участка — читаете или пишете «чужую» память. Это источник большинства трудноуловимых багов.

Как устроена память под массив

Представьте ленту адресов: 1000, 1001, 1002… Если вы объявили массив из пяти байт начиная с адреса 1000, то элемент с индексом 0 лежит по адресу 1000, с индексом 1 — по 1001, и так далее. Для слов (2 байта) шаг будет уже 2: элемент 0 → адрес 1000, элемент 1 → адрес 1002.

Адрес любого элемента вычисляется по формуле:

адрес = базовый_адрес + индекс × размер_элемента

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

Массивы в ассемблере: объявление и типы

Объявление происходит в секции данных. Синтаксис зависит от архитектуры (x86, x86-64, ARM) и ассемблера (NASM, MASM, GAS). Логика везде одна: директива типа + начальные значения или зарезервированное место.

Основные директивы в NASM для x86-64:

  • DB (define byte) — массив байт (1 байт на элемент).
  • DW (define word) — массив слов (2 байта на элемент).
  • DD (define dword) — массив двойных слов (4 байта).
  • DQ (define qword) — массив четверных слов (8 байт).
  • RESB / RESW / RESD / RESQ — резервирование без инициализации.

Пример объявления в секции .data:

пример объявления в секции дата.png

Для неинициализированных данных используют секцию .bss:

вторая мелкая.png
💡 Обратите внимание!

Если нужно хранить строку символов — это тоже байтовый массив. Строки в ассемблере заканчиваются нулевым байтом (0) по соглашению Си или явно задаются с длиной.

Код объявления полностью определяет поведение: одна и та же область памяти может быть прочитана как массив байт или как массив слов — всё зависит от того, каким регистром и с каким шагом вы её обходите.

Как создать массив в ассемблере

Создание массива — это три шага:

  1. Объявить данные.
  2. Загрузить базовый адрес в регистр.
  3. Работать с элементами через смещение или индекс.

Разберём оба варианта.

Статический массив

Задаётся при компиляции. Размер фиксирован и менять его в процессе выполнения программы нельзя. Это самый распространённый вариант для простых задач.

Полный пример на NASM x86-64 — объявление и чтение первого элемента:

статический массив.png

Шаг равен размеру элемента: для dd это 4 байта. Для dw — 2, для db — 1.

Динамический массив

Если размер заранее неизвестен, память выделяют через системный вызов (Linux: sys_brk или mmap) или получают через стек. В стеке всё проще: смещаете указатель стека (RSP) на нужный размер и получаете область под данные.

Пример: массив из 10 двойных слов на стеке:

динамический массив.png

После работы с таким массивом обязательно восстановите RSP: add rsp, 40. Иначе при возврате из функции процессор прочитает «мусор» вместо адреса возврата.

❗ Заметьте!

Стековый массив живёт только пока активен текущий кадр вызова. Возвращать указатель на него из функции нельзя — по завершении функции эта область будет перезаписана.

Обход и обработка элементов

Перебирать элементы вручную — плохая идея при любом массиве длиннее трёх позиций. Для этого есть цикл. В ассемблере цикл реализуется через счётчик и условный или безусловный переход.

Цикл с индексом

Стандартная схема: в один регистр помещают счётчик, в другой — базовый адрес. На каждом шаге читают или записывают элемент и смещают указатель на размер элемента.

Пример: сумма всех элементов массива dword в NASM x86-64:

цикл с индексом.png

Строка [rsi + ecx*4] — индексная адресация: процессор сам умножает индекс на масштаб (1, 2, 4 или 8) и добавляет к базовому адресу. Это эффективнее, чем считать смещение вручную.

Аналогичным образом строится цикл для копирования, поиска максимума, сортировки. Логика всегда одна: управляете счётчиком, читаете или изменяете нужный элемент, переходите к следующей итерации.

✅ Лайфхак!

Используйте команду LEA вместо MOV для загрузки адреса — она не обращается к памяти, а просто вычисляет адрес. Быстрее и не трогает флаги.

Поиск и сравнение элементов в массиве

Найти нужное значение в массиве — одна из самых частых задач при написании низкоуровневого кода. В ассемблере линейный поиск строится по той же схеме, что и обычный обход: загружаете базовый адрес, запускаете цикл, на каждом шаге сравниваете текущий элемент с эталоном командой CMP. Как только условие выполнилось — выходите из цикла через условный переход: JE, JNE, JL или JG в зависимости от задачи.

Команда CMP не меняет операнды — она только устанавливает флаги процессора. Дальше работает переход: JE сработает, если значения равны; JL — если первый операнд меньше второго для знаковых чисел. Это принципиально важно, когда сравниваете беззнаковые величины: для них нужны JB и JA, а не JL и JG. Путаница здесь не даёт сообщения об ошибке, но поиск будет находить совсем не то, что ожидается.

Нужно найти максимум или минимум? Логика немного меняется. Перед циклом кладёте в регистр первый элемент как текущий «рекордсмен». На каждой итерации сравниваете его с очередным элементом и при необходимости обновляете. После последней итерации в регистре остаётся нужное значение. Такой подход не требует дополнительной памяти — всё хранится в регистрах на протяжении всей работы цикла.

Отдельный случай — поиск в заранее отсортированном массиве. Там оправдан бинарный поиск: делите диапазон индексов пополам, сравниваете средний элемент с целевым и сужаете область поиска. В ассемблере это потребует нескольких дополнительных регистров под левую и правую границу, но выигрыш по скорости при больших наборах данных существенный. Для массива из тысячи элементов линейный поиск сделает до 1000 сравнений, бинарный — не более десяти.

Передача массива в подпрограмму

Когда программа растёт, повторяющийся код выносят в подпрограммы. С массивами в ассемблере это требует аккуратности: сам массив в стек не кладут — передают адрес его начала и количество элементов. Подпрограмма работает с переданным указателем, не зная и не заботясь о том, где именно в памяти расположены данные.

Соглашения о передаче аргументов

В 64-битных системах Linux (System V AMD64 ABI) первые шесть целочисленных аргументов передаются через регистры: RDI, RSI, RDX, RCX, R8, R9. Если вызываете свою подпрограмму и хотите передать ей массив, кладёте базовый адрес в RDI, длину — в RSI.

Внутри подпрограммы работаете с RDI как с базой, с RSI как со счётчиком границы. В Windows x64 первые четыре аргумента передаются через RCX, RDX, R8, R9 — соглашение другое, и при портировании кода это первое место, где возникают трудноуловимые ошибки.

Сохранение регистров при вызове

Не все регистры сохраняются автоматически при вызове функции. По System V ABI регистры RBX, RBP, R12–R15 обязан сохранить вызываемый, если собирается их использовать. Регистры RAX, RCX, RDX, RSI, RDI, R8–R11 считаются «мусорными» — их может испортить любая вызванная подпрограмма.

Если в вашем цикле обхода задействованы именно эти регистры, перед вызовом вложенной функции сохраняйте их на стек через PUSH и восстанавливайте через POP после возврата. Пропустите этот шаг — базовый адрес или счётчик окажется перезаписан, и следующий элемент прочитается из случайного места в памяти.

Возврат результата из подпрограммы

Подпрограмма возвращает одно значение через RAX (или EAX для 32-битных операций). Если результат — одиночное число: сумма, максимум, найденный элемент — просто пишете его в RAX перед RET.

Нужно вернуть несколько значений или изменённый массив? Работайте через переданный указатель: подпрограмма пишет результаты прямо в память по адресу, который ей передали в RDI. Это эффективнее, чем копировать данные — массив в памяти остаётся на месте, меняется только его содержимое.

Типичные ошибки при работе с массивами

Большинство ошибок при обработке массивов в ассемблере повторяются из проекта в проект. Знание типичных проблем экономит часы отладки.

1. Выход за границы. Счётчик дошёл до len, но вы проверяете jge после add — последний раз обращаетесь к несуществующему элементу. Всегда проверяйте условие перед телом цикла.

2. Неверный шаг. Работаете с массивом слов (dw), а шаг задаёте 4 вместо 2. Результат — читаете каждый второй реальный элемент плюс половину соседнего.

3. Загрязнение регистра. Вызвали подпрограмму внутри цикла, которая испортила регистр-счётчик или базовый адрес. Сохраняйте нужные регистры на стек перед вызовом.

4. Забытый нулевой байт. При работе со строками как байтовыми массивами не добавили завершающий ноль. Функции, ожидающие C-строку, выйдут за пределы.

5. Несовпадение размера директивы и шага. Объявили массив через dd, а обходите с шагом 2. Компилятор не предупредит — код просто работает неправильно.

6. Переполнение счётчика. Используете 8-битный регистр (AL) как счётчик для массива длиннее 255 элементов. Счётчик переполняется и цикл начинается заново.

📌 Закрепите эту информацию!

Для отладки используйте GDB или SASM (Simple ASM IDE). Установите точку останова на начало цикла и пошагово проверяйте значения регистров и адреса памяти.

Быстрая сводка по директивам объявления и правильным шагам:

Директива Размер элемента Шаг адресации Примеры регистров
DB 1 байт ×1 [rsi + rcx*1]
DW 2 байта ×2 [rsi + rcx*2]
DD 4 байта ×4 [rsi + rcx*4]
DQ 8 байт ×8 [rsi + rcx*8]

Подытожим: что важно помнить

Массив в ассемблере — это участок смежных ячеек памяти, с которым вы работаете напрямую. Нет абстракций, нет автоматических проверок. Вы сами выбираете директиву, задаёте размер элемента и следите за тем, чтобы индекс не вышел за пределы.

Объявление — через директивы DB/DW/DD/DQ для статики или через манипуляции со стеком для временных структур. Обработка данных сводится к циклу: базовый адрес в регистре, счётчик в другом, шаг равен размеру элемента. Всё остальное — вариации этой схемы.

Ошибки здесь обходятся дорого: неверный шаг или выход за границы не дают ни ошибки компилятора, ни предупреждения. Код просто работает неправильно или падает. Поэтому стоит заранее зафиксировать в комментариях: размер массива, тип элементов и максимальный допустимый индекс.

Понимание того, как строятся данные в памяти на уровне ассемблера, напрямую помогает писать более предсказуемый и быстрый код на C, C++ и Rust: именно там компилятор генерирует похожие конструкции автоматически.

Вам нужна биржа фриланса для новичков или требуются разработчики сайтов?

Комментарии

Нет комментариев
Не можешь разобраться в этой теме?
Обратись за помощью к фрилансерам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 1 дня
Безопасная сделка
Прямой эфир