Ты решил освоить ассемблер, но не знаешь, с чего начать и какие инструменты для этого нужны? Сейчас расскажу и покажу — на примере программы «Hello, world!». А попутно объясню, что процессор твоего компьютера делает после того, как ты запускаешь программу.
- Основы ассемблера
- Если наборы инструкций у процессоров разные, то на каком учить ассемблер лучше всего?
- Что и как процессор делает после запуска программы
- Регистры процессора: зачем они нужны, как ими пользоваться
- Подготовка рабочего места
- Написание, компиляция и запуск программы «Hello, world!»
- Инструкции, директивы
- Метки, условные и безусловные переходы
- Комментарии, алгоритм, выбор регистров
- Взаимодействие с пользователем: получение данных с клавиатуры
- Полезные мелочи: просмотр машинного кода, автоматизация компиляции
- Выводы
Основы ассемблера
Я буду исходить из того, что ты уже знаком с программированием — знаешь какой-нибудь из языков высокого уровня (С, PHP, Java, JavaScript и тому подобные), тебе доводилось в них работать с шестнадцатеричными числами, плюс ты умеешь пользоваться командной строкой под Windows, Linux или macOS.
Если наборы инструкций у процессоров разные, то на каком учить ассемблер лучше всего?
Знаешь, что такое 8088? Это дедушка всех компьютерных процессоров! Причем живой дедушка. Я бы даже сказал — бессмертный и бессменный. Если с твоего процессора, будь то Ryzen, Core i9 или еще какой-то, отколупать все примочки, налепленные туда под влиянием технологического прогресса, то останется старый добрый 8088.
SGX-анклавы, MMX, 512-битные SIMD-регистры и другие новшества приходят и уходят. Но дедушка 8088 остается неизменным. Подружись сначала с ним. После этого ты легко разберешься с любой примочкой своего процессора.
РЕКОМЕНДУЕМ:
Лучшие игры для программистов и технарей
Больше того, когда ты начинаешь с начала — то есть сперва выучиваешь классический набор инструкций 8088 и только потом постепенно знакомишься с современными фичами, — ты в какой-то миг начинаешь видеть нестандартные способы применения этих самых фич.
Что и как процессор делает после запуска программы
После того как ты запустил софтину и ОС загрузила ее в оперативную память, процессор нацеливается на первый байт твоей программы. Вычленяет оттуда инструкцию и выполняет ее, а выполнив, переходит к следующей. И так до конца программы.
Некоторые инструкции занимают один байт памяти, другие два, три или больше. Они выглядят как-то так:
1 2 3 4 |
90 B0 77 B8 AA 77 C7 06 66 55 AA 77 |
Вернее, даже так:
1 |
90 B0 77 B8 AA 77 C7 06 66 55 AA 77 |
Хотя погоди! Только машина может понять такое. Поэтому много лет назад программисты придумали более гуманный способ общения с компьютером: создали ассемблер.
Благодаря ассемблеру ты теперь вместо того, чтобы танцевать с бубном вокруг шестнадцатеричных чисел, можешь те же самые инструкции писать в мнемонике:
1 2 3 4 |
nop mov al, 0x77 mov ax, 0x77AA mov word [0x5566], 0x77AA |
Согласись, такое читать куда легче. Хотя, с другой стороны, если ты видишь ассемблерный код впервые, такая мнемоника для тебя, скорее всего, тоже непонятна. Но мы сейчас это исправим.
Регистры процессора: зачем они нужны, как ими пользоваться
Что делает инструкция mov? Присваивает число, которое указано справа, переменной, которая указана слева.
Переменная — это либо один из регистров процессора, либо ячейка в оперативной памяти. С регистрами процессор работает быстрее, чем с памятью, потому что регистры расположены у него внутри. Но регистров у процессора мало, так что в любом случае что-то приходится хранить в памяти.
Когда программируешь на ассемблере, ты сам решаешь, какие переменные хранить в памяти, а какие в регистрах. В языках высокого уровня эту задачу выполняет компилятор.
У процессора 8088 регистры 16-битные, их восемь штук (в скобках указаны типичные способы применения регистра):
- AX — общего назначения (аккумулятор);
- BX — общего назначения (адрес);
- CX — общего назначения (счетчик);
- DX — общего назначения (расширяет AX до 32 бит);
- SI — общего назначения (адрес источника);
- DI — общего назначения (адрес приемника);
- BP — указатель базы (обычно адресует переменные, хранимые на стеке);
- SP — указатель стека.
Несмотря на то что у каждого регистра есть типичный способ применения, ты можешь использовать их как заблагорассудится. Четыре первых регистра — AX, BX, CX и DX — при желании можно использовать не полностью, а половинками по 8 бит (старшая H и младшая L): AH, BH, CH, DH и AL, BL, CL, DL. Например, если запишешь в AX число 0x77AA ( mov ax, 0x77AA), то в AH попадет 0x77, в AL — 0xAA.
С теорией пока закончили. Давай теперь подготовим рабочее место и напишем программу «Hello, world!», чтобы понять, как эта теория работает вживую.
Подготовка рабочего места
- Скачай компилятор NASM с www.nasm.us. Обрати внимание, он работает на всех современных ОС: Windows 10, Linux, macOS. Распакуй NASM в какую-нибудь папку. Чем ближе папка к корню, тем удобней. У меня это c:\nasm (я работаю в Windows). Если у тебя Linux или macOS, можешь создать папку nasm в своей домашней директории.
- Тебе надо как-то редактировать исходный код. Ты можешь пользоваться любым текстовым редактором, который тебе по душе: Emacs, Vim, Notepad, Notepad++ — сойдет любой. Лично мне нравится редактор, встроенный в Far Manager, с плагином Colorer.
- Чтобы в современных ОС запускать программы, написанные для 8088, и проверять, как они работают, тебе понадобится DOSBox или VirtualBox.
Написание, компиляция и запуск программы «Hello, world!»
Сейчас ты напишешь свою первую программу на ассемблере. Назови ее как хочешь (например, first.asm) и скопируй в папку, где установлен nasm.
Если тебе непонятно, что тут написано, — не переживай. Пока просто постарайся привыкнуть к ассемблерному коду, пощупать его пальцами. Чуть ниже я все объясню. Плюс студенческая мудрость гласит: «Тебе что-то непонятно? Перечитай и перепиши несколько раз. Сначала непонятное станет привычным, а затем привычное — понятным».
Теперь запусти командную строку, в Windows это cmd.exe. Потом зайди в папку nasm и скомпилируй программу, используя вот такую команду:
1 |
nasm -f bin first.asm -o first.com |
Если ты все сделал правильно, программа должна скомпилироваться без ошибок и в командной строке не появится никаких сообщений. NASM просто создаст файл first.com и завершится.
Чтобы запустить этот файл в современной ОС, открой DOSBox и введи туда вот такие три команды:
1 2 3 |
mount c c:\nasm c: first |
Само собой, вместо c:\nasm тебе надо написать ту папку, куда ты скопировал компилятор. Если ты все сделал правильно, в консоли появится сообщение «Hello, world!».
Инструкции, директивы
В нашей с тобой программе есть только три вещи: инструкции, директивы и метки.
Инструкции. С инструкциями ты уже знаком (мы их разбирали чуть выше) и знаешь, что они представляют собой мнемонику, которую компилятор переводит в машинный код.
Директивы (в нашей программе их две: org и db) — это распоряжения, которые ты даешь компилятору. Каждая отдельно взятая директива говорит компилятору, что на этапе ассемблирования нужно сделать такое-то действие. В машинный код директива не переводится, но она влияет на то, каким образом будет сгенерирован машинный код.
Директива org говорит компилятору, что все инструкции, которые последуют дальше, надо размещать не в начале сегмента кода, а отступить от начала столько-то байтов (в нашем случае 0x0100).
Директива db сообщает компилятору, что в коде нужно разместить цепочку байтов. Здесь мы перечисляем через запятую, что туда вставить. Это может быть либо строка (в кавычках), либо символ (в апострофах), либо просто число.
В нашем случае: db "Hello, world", '!', 0.
Обрати внимание, символ восклицательного знака я отрезал от остальной строки только для того, чтобы показать, что в директиве db можно оперировать отдельными символами. А вообще писать лучше так:
1 |
db "Hello, world!", 0 |
Метки, условные и безусловные переходы
Метки используются для двух целей: задавать имена переменных, которые хранятся в памяти (такая метка в нашей программе только одна: string), и помечать участки в коде, куда можно прыгать из других мест программы (таких меток в нашей программе три штуки — те, которые начинаются с двух символов собаки).
Что значит «прыгать из других мест программы»? В норме процессор выполняет инструкции последовательно, одну за другой. Но если тебе надо организовать ветвление (условие или цикл), ты можешь задействовать инструкцию перехода. Прыгать можно как вперед от текущей инструкции, так и назад.
РЕКОМЕНДУЕМ:
Язык программирования Ада
У тебя в распоряжении есть одна инструкция безусловного перехода ( jmp) и штук двадцать инструкций условного перехода.
В нашей программе задействованы две инструкции перехода: je и jmp. Первая выполняет условный переход (Jump if Equal — прыгнуть, если равно), вторая (Jump) — безусловный. С их помощью мы организовали цикл.
Обрати внимание: метки начинаются либо с буквы, либо со знака подчеркивания, либо со знака собаки. Цифры вставлять тоже можно, но только не в начало. В конце метки обязательно ставится двоеточие.
Комментарии, алгоритм, выбор регистров
Итак, в нашей программе есть только три вещи: инструкции, директивы и метки. Но там могла бы быть и еще одна важная вещь: комментарии. С ними читать исходный код намного проще.
Как добавлять комментарии? Просто поставь точку с запятой, и все, что напишешь после нее (до конца строки), будет комментарием. Давай добавим комментарии в нашу программу.
Теперь, когда ты разобрался во всех частях программы по отдельности, попробуй вникнуть, как все части служат алгоритму, по которому работает наша программа.
- Поместить в BX адрес строки.
- Поместить в AL очередную букву из строки.
- Если вместо буквы там 0, выходим из программы — переходим на 6-й шаг.
- Выводим букву на экран.
- Повторяем со второго шага.
- Конец.
Обрати внимание, мы не можем использовать AX для хранения адреса, потому что нет таких инструкций, которые бы считывали память, используя AX в качестве регистра-источника.
Взаимодействие с пользователем: получение данных с клавиатуры
От программ, которые не могут взаимодействовать с пользователем, толку мало. Так что смотри, как можно считывать данные с клавиатуры. Сохрани вот этот код как second.asm.
Потом иди в командную строку и скомпилируй его в NASM:
1 |
nasm -f bin second.asm -o second.com |
Затем запусти скомпилированную программу в DOSBox:
1 |
second |
Как работает программа? Две строки после метки @@start вызывают функцию BIOS, которая считывает символы с клавиатуры. Она ждет, когда пользователь нажмет какую-нибудь клавишу, и затем кладет ASCII-код полученного значения в регистр AL. Например, если нажмешь заглавную A, в AL попадет 0x41, а если строчную A — 0x61.
Дальше смотрим: если нажата клавиша с кодом 0x1B (клавиша ESC), то выходим из программы. Если же нажата не ESC, вызываем ту же функцию, что и в предыдущей программе, чтобы показать символ на экране. После того как покажем — прыгаем в начало ( jmp): start.
Обрати внимание, инструкция cmp (от слова compare — сравнить) выполняет сравнение, инструкция je (Jump if Equal) — прыжок в конец программы.
Полезные мелочи: просмотр машинного кода, автоматизация компиляции
Если тебе интересно, в какой машинный код преобразуются инструкции программы, скомпилируй исходник вот таким вот образом (добавь опцию -l):
1 |
nasm -f bin second.asm -l second.lst -o second.com |
Тогда NASM создаст не только исполняемый файл, но еще и листинг: second.lst. Листинг будет выглядеть как-то так.
Еще тебе наверняка уже надоело при каждом компилировании вколачивать в командную строку длинную последовательность одних и тех же букв. Если ты используешь Windows, можешь создать батник (например, m.bat) и вставить в него вот такой текст.
Теперь ты можешь компилировать свою программу вот так:
1 |
m first |
Само собой, вместо first ты можешь подставить любое имя файла.
Выводы
Итак, ты теперь знаешь, как написать простейшую программу на ассемблере, как ее скомпилировать, какие инструменты для этого нужны. Конечно, прочитав одну статью, ты не станешь опытным программистом на ассемблере. Чтобы придумать и написать на нем что-то стоящее — вроде Floppy Bird и «МикроБ», которые написал я, — тебе предстоит еще много пройти. Но первый шаг в эту сторону ты уже сделал.