За что я люблю ассемблер?
Этой статье уже почти 3 года. Однако сегодня я решил подредактировать её, дополнить и выложить, наконец, на Хабр.
Оговорочки
Хочу сразу оговориться, что правильно говорить не «ассемблер» (assembler), а «язык ассемблера» (assembly language), потому как ассемблер — это транслятор кода на языке ассемблера (т.е. по сути, программа MASM, TASM, fasm, NASM, UASM, GAS и пр., которая компилирует исходный текст на языке ассемблера в объектный или исполняемый файл). Тем не менее, из соображения краткости многие, говоря «ассемблер» (асм, asm), подразумевают именно «язык ассемблера».
Синтаксис директив, стандартных макросов и пр. структурных элементов различных диалектов (к примеру, MASM, fasm, NASM, GAS), могут отличаться довольно существенно. Мнемоники (имена) инструкций (команд) и регистров, а также синтаксис их написания для одного и того же процессора одинаковы почти во всех диалектах (заметным исключением среди популярных ассемблеров является разве что GAS (GNU Assembler) в режиме синтаксиса AT&T, где к именам инструкций могут добавляться суффиксы, обозначающие размер обрабатываемых ими данных, что бывает довольно удобно, но там есть и другие нюансы, сбивающие с толку программиста, привыкшего к классическому ассемблеру, к примеру, иной порядок указания операндов, хотя всё это лечится специальной директивной переключения в режим синтаксиса Intel).
Поскольку ассемблер — самый низкоуровневый язык программирования, довольно проблематично написать код, который корректно компилировался бы для разных архитектур процессоров (например, x86 и ARM), для разных режимов одного и того же процессора (16-битный реальный режим, 32-битный защищённый режим, 64-битный long mode;, а ещё код может быть написан как с использованием различных технологий вроде SSE, AVX, FMA, BMI и AES-NI, так и без них) и для разных операционных систем (Windows, Linux, MS-DOS). Хоть иногда и можно встретить «универсальный» код (например, отдельные библиотеки), скажем, для 32- и 64-битного кода ОС Windows (или даже для Windows и Linux), но это бывает нечасто. Ведь каждая строка кода на ассемблере (не считая управляющих директив, макросов и тому подобного) — это отдельная инструкция, которая пишется для конкретного процессора и ОС, и сделать кроссплатформенный вариант можно только с помощью макросов и условных директив препроцессора, получая в итоге порой весьма нетривиальные конструкции, сложные для понимания.
Откуда растут ноги?
Ассемблером я увлёкся лет в 12–13, и он меня изрядно «затянул». Почему?
Во-первых, экономия памяти (дисковой и оперативной) и погоня за скоростью в те DOS-овские времена далёких 90-х годов были куда более актуальными темами, чем сейчас.
Во-вторых (и это более существенно), на ассемблере можно было делать много того, что сделать на языках высокого уровня (ЯВУ, не путайте с Java) нельзя, затруднительно или не так эффективно. К примеру, мне очень нравилось писать резидентные программы.
Но с тех пор прошло уже более 2-х десятков лет, и сейчас экономия памяти (особенно дисковой) в подавляющем большинстве случаев уже не так актуальна, да и скорости современных процессоров для выполнения повседневных задач вполне хватает (популярность языков сверхвысокого уровня подтверждает это, хотя закон Вирта никто не отменял). А современные компиляторы зачастую могут оптимизировать код по скорости даже лучше человека. Что же может привлекать программиста в ассемблере, ведь исходники на нём гораздо более объёмные и сложные, а на разработку требуется больше времени и внимания (в т.ч. на отладку)?
Вон оно что!
Приведу свои доводы относительно того, чем так хорош ассемблер.
Ассемблер даёт полный контроль над кодом и обладает большей гибкостью, чем любой другой язык программирования (даже C/C++). На асме мы можем конструировать нашу программу, размещая блоки кода и данных как нам вздумается. Каждый генерируемый байт будет таким, каким мы хотим его видеть. Без лишнего runtime-кода стандартных библиотек. Правда, справедливости ради отмечу, что необходимость в этом может возникнуть лишь в весьма специфических случаях. Однако существуют аппаратные платформы с ограниченными ресурсами, где оптимизация кода важна и актуальна и в наши дни.
На ассемблере можно написать ВСЁ, он всемогущ! Вряд ли у вас получится создать MBR-загрузчик полностью на C или на чём-то ещё. Для работы с железом на низком уровне, программирования чипсетов зачастую может потребоваться ассемблер. Для внедрения кода в другие процессы (injection, не только с вредоносными целями), создания различных антиотладочных приёмов тоже необходим ассемблер. Или, скажем, для проделывания чего-то вроде этого. Для C/C++ имеются интринсики — функции для генерации отдельных инструкций процессора (есть ли что-то подобное для других языков программирования — не знаю, не встречал). Но их частое использование загромождает код (не проще ли тогда писать на чистом ассемблере?) А их отсутствие не позволяет нам контролировать генерируемый компилятором код (при этом, к слову говоря, Visual C/C++, GNU C/C++ и Clang будут генерировать разный код; и даже один и тот же компилятор с разными настройками выдаст различный результат).
Получаемый код даже самого умного и навороченного компилятора, как правило, можно оптимизировать (как по скорости, так и по размеру). К примеру, автоматическую векторизацию кода (приведение обычных скалярных вычислений к параллельным вычислениям с использованием SIMD: SSE, AVX и т.п.), компиляторы делают весьма посредственно. А ещё можно изощриться и использовать неочевидные комбинации, сделав код короче и быстрее. Этим можно, конечно, заняться и на других языках, но на ассемблере больше простора для творчества. К тому же, это особый кайф, азарт, челлендж в некотором роде! Разве не прикольно написать программу, выполняющую полезные функции, весом менее 10 Кб? :) Для сравнения: VCL-программа на Delphi 10.2 Tokyo, создающая пустое окно без какого-либо полезного функционала, весит в release-версии целых 2 Мб (а в debug-версии… кхм, 11 Мб). На C++Builder 10.2 Tokyo release-версия такой же программы, не требующая внешних библиотек, получится размером ≈ 2.7 Мб. Аналогичная программа на fasm будет занимать всего пару килобайт.
Обычно одна строка кода на ЯВУ разворачивается в несколько (или даже десяток) инструкций процессора. А знаете ли вы о том, что некоторые инструкции процессора Intel требуют несколько строк для реализации на ЯВУ (на том же C/C++, если не использовать интринсики)? Если не знаете, просто поверьте на слово, а я, возможно, напишу об этом в одной из следующих статей. Приведу лишь один простой пример: аналоги инструкций rol, ror (существующих ещё в самых ранних процессорах i8086 с конца 70-х годов) появились только в стандарте C++20 в библиотеке bit (как функции std: rotl, std: rotr), а в большинстве других языков они вообще отсутствуют.
Есть такое направление компьютерного искусства, под названием демосцена. Написать intro, уместив исполняемый файл в 256 байт [1, 2, 3, 4] (а то и 128, 64, 32 или даже ещё меньше) на чём-то отличном от ассемблера (ну или по крайней мере, без использования ассемблера для финальной корректировки кода) вы вряд ли сможете.
Ещё одна интересная область применения ассемблера — создание файлов данных с помощью макросов и директив генерации данных. К примеру, fasm позволяет создавать виртуальные данные и генерировать отдельные файлы (директива virtual), а также читать и изменять ранее сгенерированный код (директивы load, store). Есть даже примеры AES-шифрования файлов.
Без ассемблера не обойтись при исследовании (reverse engineering), а зачастую и при отладке программ.
В ассемблере есть особая магия и притягательность! Но справедливости ради скажу, что писать всегда на ассемблере — занятие не очень разумное с точки зрения времени, усилий, вероятности допустить ошибку и кроссплатформенности (я сам реже пишу на ассемблере, нежели на других языках). Не так часто нам требуется полный контроль над кодом и столь уж жёсткая оптимизация, когда экономия пары тактов процессора имеет критически решающее значение.
На том же C/C++ можно написать практически всё, что можно написать и на ассемблере, причём сразу под десяток платформ и ОС, включая и выключая отдельными опциями компилятора использование различных наборов инструкций, векторизацию, оптимизацию и пр.
Но иногда использование ассемблера действительно оправдано (пример). Часто ассемблер хорошо использовать в виде вставок в код на ЯВУ (посмотрите RTL-модули Delphi, там этого добра в изобилии). Да и использование интринсиков, как правило, не имеет смысла (или даже опасно) без знания ассемблера.
Подытожим…
Итак, приведу неполный перечень того, в каких случаях используется ассемблер.
Создание загрузчиков, прошивок устройств (комплектующих ПК, встраиваемых систем), элементов ядра ОС.
Низкоуровневая работа с железом, в т.ч. с процессором, памятью.
Внедрение кода в процессы (injection), как с вредоносной целью, так и с целью защиты или добавления функционала. Системный софт.
Блоки распаковки, защиты кода и прочего функционала (с целью изменения поведения программы, добавления новых функций, взлома лицензий), встраиваемые в исполняемые файлы (см. UPX, ASProtect и пр).
Оптимизация кода по скорости, в т.ч. векторизация (SSE, AVX, FMA), математические вычисления, обработка мультимедиа, копирование памяти.
Оптимизация кода по размеру, где нужно контролировать каждый байт. Например, в демосцене.
Вставки в языки высокого уровня, которые не позволяют выполнять необходимую задачу, либо позволяют делать это неоптимальным образом.
При создании компиляторов и трансляторов исходного кода с какого-либо языка на язык ассемблера (например, многие компиляторы C/C++ позволяют выполнять такую трансляцию). При создании отладчиков, дизассемблеров.
Собственно, отладка, дизассемблирование, исследование программ (reverse engineering).
Создание файлов данных с помощью макросов и директив генерации данных.
Вы не поверите, но ассемблер можно использовать и для написания обычного прикладного ПО (консольного или с графическим интерфейсом — GUI), игр, драйверов и библиотек:)
Быть или не быть?
Так, нужно ли изучать ассемблер современному программисту? Если вы уже не новичок в программировании, и у вас серьёзные амбиции, то изучение ассемблера, внутреннего устройства операционных систем и функционирования железа (особенно процессоров, памяти), а также использование различных инструментов для дизассемблирования, отладки и анализа кода полезно тем, кто хочет писать действительно эффективные программы. Иначе будет сложно в полной мере понять, что происходит «под капотом» любимого компилятора (хотя бы в общих чертах), как оптимизировать программы на любом языке программирования и какой приём стоит предпочесть. Необязательно погружаться слишком глубоко в эту тему, если вы пишете на Python или JavaScript. А вот если ваш язык — C или C++, хорошенько изучить ассемблер будет полезно.
Вместе с тем, необходимо помнить не только о «тактике», но и о «стратегии» написания кода, поэтому не менее важно изучать и алгоритмы (правильный выбор которых зачастую более важен для создания эффективных программ, нежели низкоуровневая оптимизация), шаблоны проектирования и многие другие технологии, без которых программист не может считать себя современным.
Это будет полезно
Если вы решили изучить ассемблер и окунуться в низкоуровневое программирование, вам будет полезна следующая литература:
Зубков С.В. Assembler для DOS, Windows и Unix. — ДМК Пресс, 2017. — 638 c., ISBN 978–5–97060–535–6.
Руслан Аблязов. Программирование на ассемблере на платформе x86–64. — ДМК Пресс, 2016. — 302 с., ISBN 978–5–97060–364–2.
Все статьи старого WASM«а — кладезь обучающего материала на самые разные низкоуровневые темы (крайне рекомендую!)
Новый WASM (форум по низкоуровневому программированию и сборник статей).Книги и статьи Криса Касперски (много).
Официальная документация Intel (4 тома) [всё английском, PDF]:
Официальная документация AMD (множество документов) [всё на английском, PDF]
Архитектура и система команд микропроцессоров x86 (староватая документация на русском языке: в части инструкций присутствует базовый набор + x86, а также расширения MMX, 3DNow! и SSE (1))
Марк Руссинович, Дэвид Соломон, Алекс Ионеску. Внутреннее устройство Microsoft Windows. — 6-е изд., часть 1. –, а Питер, 2013. — 800 с., ISBN 978–5–496–00434–3, 978–5–459–01730–4 (англ.: 978–0735648739).
Марк Руссинович, Дэвид Соломон, Алекс Ионеску. Внутреннее устройство Microsoft Windows. Основные подсистемы ОС. — 6-е изд., часть 2. — Питер, 2014. — 672 с., ISBN 978–5–496–00791–7 (англ.: 978–0735665873).
Джеффри Рихтер. Windows для профессионалов. Создание эффективных Win32-приложений с учётом специфики 64-разрядной версии Windows. — 4-е изд. — Питер, Русская редакция, 2001. — 752 с. (есть вариант книги 2008 г. на 720 с., но она тоже 4-го издания, с переводом 2000 года… в чём разница?), 5–272–00384–5, 978–5–7502–0360–4 (англ.: 1–57231–996–8).
Документация по оптимизации от Agner«а Fog«а (5 томов в одном архиве) [всё на английском].
Михаил Гук. Аппаратные средства IBM PC. Энциклопедия. — 3-е изд. — Питер, 2008. — 1072 с., ISBN 978–5–46901–182–8 (2001 г. — 816 с., ISBN 5–88782–290–2).
Владимир Кулаков. Программирование на аппаратном уровне. Специальный справочник (+ дискета). — 2-е изд. — Питер, 2003. — 848 с., ISBN 5–94723–487–4.
Всеволод Несвижский. Программирование аппаратных средств в Windows (+ CD-ROM). — 2-е изд. — БХВ-Петербург, 2008. — 528 с., ISBN 978–5–9775–0263–4.
Электронная библиотека книг Александра Фролова и Григория Фролова.
Компиляторы и инструменты:
MASM32 (Macro Assembler) — наверное, самый популярный пакет самого популярного ассемблера.
MASM64 includes and libs — заголовки и библиотеки для 64-битной версии MASM (информация); файлы ml64.exe, link.exe и прочие потроха можно взять из Visual Studio (путь к папке с нужными файлами примерно такой: C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Tools\MSVC\14.12.25827\bin\Hostx64\x64\).fasm (flat assembler) — современный и удобный компилятор под DOS, Wndows, Linux с очень развитой системой макросов и полным набором инструкций Intel/AMD. Рекомендую в качестве основного!
Там же можно скачать и fasmg (flat assembler g) — универсальный ассемблер под любую платформу (имеются include-модули для создания кода под AVR, i8051, x86/x64, генерации байт-кода JVM, аналогично можно создать свои модули).NASM (Netwide Assembler) — ещё один современный кроссплатформенный компилятор с хорошей макросистемой и полным набором инструкций Intel/AMD, популярен в зарубежных проектах и при программировании под Linux/BSD.
NASMX — пакет макросов, include«ов, примеров и утилит для NASM под Windows, Linux, BSD, Xbox; включает макрос invoke, символы для работы с OpenGL и пр.UASM (он же HJWasm) — современный MASM-совместимый мультиплатформенный ассемблер с полным набором инструкций Intel/AMD.
TASM 5.x (Turbo Assembler) — старый, но всё ещё популярный ассемблер, в основном используется для создания программ под DOS.
ALINK, GoLink — компоновщики для программ под DOS и Windows.
objconv — преобразователь форматов объектных файлов (COFF/OMF/ELF/Mach-O).
ResEd — бесплатный редактор ресурсов.
GoRC — компилятор ресурсов (rc → res) [в вышеупомянутом NASMX есть и GoLink, и objconv, и GoRC].
Windows 10 Software Development Kit (SDK) — заголовочные файлы, библиотеки, инструменты (в том числе отладчик WinDbg) для разработчиков Windows.
Windows Driver Kit (WDK) — инструменты для разработчика драйверов (документация).
OllyDbg — популярный 32-битный отладчик (готовится 64-битная версия, но пока ещё не вышла).
x64dbg — хороший отладчик для 32- и 64-битного кода.
IDA Pro — мощный интерактивный дизассемблер (shareware).
VMware Workstation Player — мощный виртуализатор, позволяющий создавать и запускать виртуальные машины (бесплатный для персонального использования).
Oracle VirtualBox — альтернативный бесплатный виртуализатор.
Bochs — эмулятор компьютера IBM PC.
QEMU — эмулятор аппаратного обеспечения различных платформ (QEMU Manager).
Intel Software Development Emulator (SDE) — эмулятор расширений (инструкций) процессоров Intel.
DOSBox — очень популярный эмулятор компьютера для запуска программ под DOS (имеет встроенный замедлитель скорости).
Hiew — редактор двоичных файлов со встроенным дизассемблером, просмотром и редактированием заголовков исполняемых файлов (shareware).
PE Explorer — редактор секций, ресурсов PE, дизассемблер (shareware).
Windows Sysinternals — набор системных утилит для Windows (работа с процессами, мониторы и прочее).
ReactOS — бесплатная Windows-совместимая операционная система с открытым исходным кодом.
KolibriOS — миниатюрная ОС, умещающаяся на дискету 1.44 Mb, с исходниками на fasm.
Все эти ссылки вы можете найти, кликнув сюда (здесь их будет даже больше, т.к. раздел обновляется).
Также хочу пригласить вас в наш уютный «ламповый» раздел Assembler Форума на Исходниках.Ру ;)
Кто интересуется демосценой и сайзкодингом, welcome here.
Успехов вам!