За что я люблю ассемблер?

Этой статье уже почти 3 года. Однако сегодня я решил подредактировать её, дополнить и выложить, наконец, на Хабр.

image-loader.svg

Оговорочки

Хочу сразу оговориться, что правильно говорить не «ассемблер» (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-х десятков лет, и сейчас экономия памяти (особенно дисковой) в подавляющем большинстве случаев уже не так актуальна, да и скорости современных процессоров для выполнения повседневных задач вполне хватает (популярность языков сверхвысокого уровня подтверждает это, хотя закон Вирта никто не отменял). А современные компиляторы зачастую могут оптимизировать код по скорости даже лучше человека. Что же может привлекать программиста в ассемблере, ведь исходники на нём гораздо более объёмные и сложные, а на разработку требуется больше времени и внимания (в т.ч. на отладку)?

Вон оно что!

Приведу свои доводы относительно того, чем так хорош ассемблер.

  1. Ассемблер даёт полный контроль над кодом и обладает большей гибкостью, чем любой другой язык программирования (даже C/C++). На асме мы можем конструировать нашу программу, размещая блоки кода и данных как нам вздумается. Каждый генерируемый байт будет таким, каким мы хотим его видеть. Без лишнего runtime-кода стандартных библиотек. Правда, справедливости ради отмечу, что необходимость в этом может возникнуть лишь в весьма специфических случаях. Однако существуют аппаратные платформы с ограниченными ресурсами, где оптимизация кода важна и актуальна и в наши дни.

  2. На ассемблере можно написать ВСЁ, он всемогущ! Вряд ли у вас получится создать MBR-загрузчик полностью на C или на чём-то ещё. Для работы с железом на низком уровне, программирования чипсетов зачастую может потребоваться ассемблер. Для внедрения кода в другие процессы (injection, не только с вредоносными целями), создания различных антиотладочных приёмов тоже необходим ассемблер. Или, скажем, для проделывания чего-то вроде этого. Для C/C++ имеются интринсики — функции для генерации отдельных инструкций процессора (есть ли что-то подобное для других языков программирования — не знаю, не встречал). Но их частое использование загромождает код (не проще ли тогда писать на чистом ассемблере?) А их отсутствие не позволяет нам контролировать генерируемый компилятором код (при этом, к слову говоря, Visual C/C++, GNU C/C++ и Clang будут генерировать разный код; и даже один и тот же компилятор с разными настройками выдаст различный результат).

  3. Получаемый код даже самого умного и навороченного компилятора, как правило, можно оптимизировать (как по скорости, так и по размеру). К примеру, автоматическую векторизацию кода (приведение обычных скалярных вычислений к параллельным вычислениям с использованием SIMD: SSE, AVX и т.п.), компиляторы делают весьма посредственно. А ещё можно изощриться и использовать неочевидные комбинации, сделав код короче и быстрее. Этим можно, конечно, заняться и на других языках, но на ассемблере больше простора для творчества. К тому же, это особый кайф, азарт, челлендж в некотором роде! Разве не прикольно написать программу, выполняющую полезные функции, весом менее 10 Кб? :) Для сравнения: VCL-программа на Delphi 10.2 Tokyo, создающая пустое окно без какого-либо полезного функционала, весит в release-версии целых 2 Мб (а в debug-версии… кхм, 11 Мб). На C++Builder 10.2 Tokyo release-версия такой же программы, не требующая внешних библиотек, получится размером ≈ 2.7 Мб. Аналогичная программа на fasm будет занимать всего пару килобайт.

  4. Обычно одна строка кода на ЯВУ разворачивается в несколько (или даже десяток) инструкций процессора. А знаете ли вы о том, что некоторые инструкции процессора Intel требуют несколько строк для реализации на ЯВУ (на том же C/C++, если не использовать интринсики)? Если не знаете, просто поверьте на слово, а я, возможно, напишу об этом в одной из следующих статей. Приведу лишь один простой пример: аналоги инструкций rol, ror (существующих ещё в самых ранних процессорах i8086 с конца 70-х годов) появились только в стандарте C++20 в библиотеке bit (как функции std: rotl, std: rotr), а в большинстве других языков они вообще отсутствуют.

  5. Есть такое направление компьютерного искусства, под названием демосцена. Написать intro, уместив исполняемый файл в 256 байт [1, 2, 3, 4] (а то и 128, 64, 32 или даже ещё меньше) на чём-то отличном от ассемблера (ну или по крайней мере, без использования ассемблера для финальной корректировки кода) вы вряд ли сможете.

  6. Ещё одна интересная область применения ассемблера — создание файлов данных с помощью макросов и директив генерации данных. К примеру, fasm позволяет создавать виртуальные данные и генерировать отдельные файлы (директива virtual), а также читать и изменять ранее сгенерированный код (директивы load, store). Есть даже примеры AES-шифрования файлов.

  7. Без ассемблера не обойтись при исследовании (reverse engineering), а зачастую и при отладке программ.

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

На том же C/C++ можно написать практически всё, что можно написать и на ассемблере, причём сразу под десяток платформ и ОС, включая и выключая отдельными опциями компилятора использование различных наборов инструкций, векторизацию, оптимизацию и пр.

Но иногда использование ассемблера действительно оправдано (пример). Часто ассемблер хорошо использовать в виде вставок в код на ЯВУ (посмотрите RTL-модули Delphi, там этого добра в изобилии). Да и использование интринсиков, как правило, не имеет смысла (или даже опасно) без знания ассемблера.

Подытожим…

Итак, приведу неполный перечень того, в каких случаях используется ассемблер.

  1. Создание загрузчиков, прошивок устройств (комплектующих ПК, встраиваемых систем), элементов ядра ОС.

  2. Низкоуровневая работа с железом, в т.ч. с процессором, памятью.

  3. Внедрение кода в процессы (injection), как с вредоносной целью, так и с целью защиты или добавления функционала. Системный софт.

  4. Блоки распаковки, защиты кода и прочего функционала (с целью изменения поведения программы, добавления новых функций, взлома лицензий), встраиваемые в исполняемые файлы (см. UPX, ASProtect и пр).

  5. Оптимизация кода по скорости, в т.ч. векторизация (SSE, AVX, FMA), математические вычисления, обработка мультимедиа, копирование памяти.

  6. Оптимизация кода по размеру, где нужно контролировать каждый байт. Например, в демосцене.

  7. Вставки в языки высокого уровня, которые не позволяют выполнять необходимую задачу, либо позволяют делать это неоптимальным образом.

  8. При создании компиляторов и трансляторов исходного кода с какого-либо языка на язык ассемблера (например, многие компиляторы C/C++ позволяют выполнять такую трансляцию). При создании отладчиков, дизассемблеров.

  9. Собственно, отладка, дизассемблирование, исследование программ (reverse engineering).

  10. Создание файлов данных с помощью макросов и директив генерации данных.

  11. Вы не поверите, но ассемблер можно использовать и для написания обычного прикладного ПО (консольного или с графическим интерфейсом — GUI), игр, драйверов и библиотек:)

Быть или не быть?

Так, нужно ли изучать ассемблер современному программисту? Если вы уже не новичок в программировании, и у вас серьёзные амбиции, то изучение ассемблера, внутреннего устройства операционных систем и функционирования железа (особенно процессоров, памяти), а также использование различных инструментов для дизассемблирования, отладки и анализа кода полезно тем, кто хочет писать действительно эффективные программы. Иначе будет сложно в полной мере понять, что происходит «под капотом» любимого компилятора (хотя бы в общих чертах), как оптимизировать программы на любом языке программирования и какой приём стоит предпочесть. Необязательно погружаться слишком глубоко в эту тему, если вы пишете на Python или JavaScript. А вот если ваш язык — C или C++, хорошенько изучить ассемблер будет полезно.

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

Это будет полезно

image-loader.svg

Если вы решили изучить ассемблер и окунуться в низкоуровневое программирование, вам будет полезна следующая литература:

  • Зубков С.В. 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.

Успехов вам!

© Habrahabr.ru