Ассемблер
Предпосылки: Что такое программирование (программа, процессор, память, ввод/вывод).
← Что такое программирование | Переменные и типы →
Процессор понимает только числа. Инструкция «сложи два числа» для него — последовательность чисел вроде 0010 0001. Первые программисты записывали именно такие коды, сверяясь с таблицами: «код 0010 означает сложение, 0001 — номер регистра». На программе из десяти строк это терпимо. На программе из ста — мучительно: одна перепутанная цифра, и программа делает не то, а найти ошибку в стене чисел почти невозможно.
Люди решили: пусть каждая числовая инструкция получит короткое читаемое имя. 0010 стало ADD, 0001 стало MOV, переход к другой инструкции — JMP (от английского jump — прыжок). Набор таких имён и правил называется языком ассемблера, а ассемблер (assembler) переводит этот текст обратно в числа для процессора. На следующем уровне абстракции этот подход развилось в идею переменных: дать имя не машинной команде, а хранилищу для данных.
Название assembler приходит от assemble — «собирать»: этот инструмент собирает понятный человеку текст в машинный код.
Как устроена машина на уровне ассемблера
В предыдущей заметке мы описали компьютер тремя частями: процессор, память, ввод/вывод. Для ассемблера нужны две детали, которые мы тогда опустили.
Адреса. Память — не просто «место для данных», а пронумерованный ряд ячеек. У каждой ячейки есть номер — адрес. Чтобы прочитать или записать данные, процессор указывает адрес: «возьми число из ячейки 200» или «положи результат в ячейку 208». В ассемблере программист работает с этими адресами напрямую.
В этой заметке для удобства примем: одно целое значение занимает 4 байта. Тогда между соседними элементами массива будет разница 4 (200 → 204).
Почему память считают байтами и почему размер числа в других случаях может отличаться, см. двоичная система и байты и целые числа.
Регистры. Процессор не может складывать числа прямо в памяти — он сначала копирует их к себе, во внутреннее хранилище. Это хранилище состоит из нескольких ячеек, которые называются регистрами. Регистров мало (в простых процессорах — от 4 до 16), зато доступ к ним мгновенный. Типичные имена: EAX, EBX, ECX, EDX (E — Extended, расширение до 32 бит; A — Accumulator, B — Base, C — Counter, D — Data — исторические роли, закрепившиеся в названиях).
Память Процессор
(тысячи ячеек) (несколько регистров)
[200] = 1500 EAX = ?
[204] = 300 EBX = ?
[208] = ? ECX = ?
... EDX = ?Программа на ассемблере — это последовательность команд, каждая из которых говорит процессору: откуда взять данные, что с ними сделать, куда положить результат.
Пять команд, из которых строится всё
Ассемблер конкретного процессора может содержать сотни команд, но почти любая программа строится из нескольких базовых:
MOV (move — переместить) — копирует число из одного места в другое. Из памяти в регистр, из регистра в память, из регистра в регистр:
MOV EAX, [200] ; скопировать число из ячейки 200 в регистр EAX
MOV [208], EAX ; скопировать число из EAX в ячейку 208ADD (add — сложить) — прибавляет число к содержимому регистра:
ADD EAX, EBX ; EAX = EAX + EBXSUB (subtract — вычесть) — обратная операция: вычитает число из регистра:
SUB ECX, 1 ; ECX = ECX - 1CMP (compare — сравнить) — сравнивает два числа и запоминает результат (больше, меньше, равно). Само по себе ничего не делает — только готовит информацию для следующей команды.
JMP (jump — прыгнуть) — перейти к другой инструкции вместо следующей по порядку. Процессор обычно выполняет команды одну за другой, сверху вниз. JMP ломает этот порядок: он говорит «перейди вот туда». Куда именно — указывает метка: имя с двоеточием, которое программист ставит перед нужной строкой. Например, start: — это метка, а JMP start — команда «прыгни к строке с меткой start».
Существуют условные варианты: JNE (jump if not equal — прыгнуть, если не равно), JL (jump if less — если меньше), JE (jump if equal — если равно). Они прыгают только когда выполнено условие, заданное предыдущей командой CMP. Именно через них программа принимает решения и повторяет действия: CMP сравнивает два числа, а условный прыжок решает, что делать дальше.
Программа: сложить пять чисел
Задача: в памяти лежат пять чисел (по адресам 200, 204, 208, 212, 216). Нужно сложить их и записать результат в ячейку 220.
1 MOV EAX, 0 ; EAX = 0 (здесь будем накапливать сумму)
2 MOV EBX, 200 ; EBX = 200 (адрес первого числа)
3 MOV ECX, 5 ; ECX = 5 (сколько чисел осталось)
4
5 loop:
6 ADD EAX, [EBX] ; прибавить число по адресу EBX к сумме
7 ADD EBX, 4 ; сдвинуть адрес на 4 байта (в этой модели размер числа)
8 SUB ECX, 1 ; уменьшить счётчик на 1
9 CMP ECX, 0 ; счётчик дошёл до нуля?
10 JNE loop ; если нет — вернуться к строке 5
11
12 MOV [220], EAX ; записать сумму в ячейку 220Двенадцать строк. Задача тривиальная — сложить пять чисел. Но программист должен:
- помнить, что
EAX— сумма,EBX— текущий адрес,ECX— счётчик; - вручную считать адреса (200, 204, 208…);
- следить, что
JNEпрыгает именно наloop, а не на другую строку;- знать, что в этой простой модели одно целое значение занимает 4 байта, поэтому адрес сдвигается на 4.
Что будет, если заменить ADD EBX, 4 на ADD EBX, 1?
Программа прочитает не следующее число, а часть текущего. Число
1500занимает ячейки 200-203. Сдвиг на 1 прочитает ячейки 201-204 — набор байтов, не имеющий отношения к исходным данным. Результат будет бессмысленным, но никакой ошибки программа не покажет — она не знает, что данные интерпретируются неправильно.
Почему это больно
Ассемблер решил одну проблему: вместо чисел программист пишет читаемые имена. MOV EAX, [200] понятнее, чем 0010 0001 0000 1100 1000. Но все остальные проблемы остались.
Программист — бухгалтер адресов. Каждое число, каждая инструкция занимают несколько ячеек памяти. Добавил данные в середину — все последующие адреса сдвинулись, и каждую ссылку нужно пересчитать вручную.
Регистров мало, а задач много. Четыре регистра при сложении пяти чисел — терпимо. Но при расчёте зарплаты (оклад, налог, бонус, стаж, надбавка) регистров не хватает, приходится постоянно сохранять значения в память и загружать обратно. Программист должен помнить, что в какой ячейке лежит.
Ошибки невидимы. Процессор не знает, что ячейка 200 хранит зарплату, а ячейка 204 — налоговую ставку. Если программист перепутает адреса — программа молча посчитает мусор. Никакого сообщения об ошибке не будет.
При ста строках ассемблера эти проблемы утомительны. При тысяче — невыносимы. При сотнях тысяч строк, из которых состоят реальные программы, — невозможны: ни один человек не удержит в голове десятки тысяч адресов. Программист тратит больше сил на отслеживание адресов и регистров, чем на решение задачи.
Люди задались вопросом: что, если машина сама будет следить за адресами, а программист будет работать с именами? Вместо MOV EAX, [200] написать salary = 1500 — и пусть специальная программа сама решит, в какую ячейку положить число и какой регистр использовать. Это привело к идее переменных.
Sources
Реальные наборы команд процессоров (ARM, x86) и почему у разных архитектур (наборов инструкций конкретного процессора) разный ассемблер — в ISA.
- Petzold, C., 2000, Code: The Hidden Language of Computer Hardware and Software. Microsoft Press.
- Hyde, R., 2010, The Art of Assembly Language. No Starch Press.