Редакция Highload разобралась, что такое язык ассемблера, разобрала его синтаксис и варианты использования. Ведь умение читать и писать код на низкоуровневом языке ассемблера – это весомый навык для любого системного программиста. Он позволяет создавать более оптимизированный код, использовать недоступные в Си возможности и выполнять реверс-инжиниринг скомпилированного кода.
Содержание:
1. Что такое язык ассемблера? Для чего он нужен?
2. Пример кода на языке ассемблера
3. Синтаксис языка ассемблера
4. Достоинства и недостатки
5. Применение языка ассемблера
6. Происхождение и критика языка сегодня
Язык ассемблера — это низкоуровневый язык программирования, который транслируется непосредственно в инструкции машинного языка. Трансляцию выполняет специальная программа — ассемблер (от англ. assembler — сборщик).
У каждого семейства процессоров есть собственный набор инструкций для выполнения различных операций, например для получения ввода с клавиатуры, вывода информации на экран и выполнения других действий. Эти наборы инструкций называются «инструкциями машинного языка».
Инструкции машинного языка представлены в виде последовательностей из нулей и единиц. Создавать программное обеспечение в виде таких последовательностей очень сложно для человека. Поэтому был разработан низкоуровневый язык сборки, в котором инструкции представлены в более понятной для человека форме.
Ассемблеры, как и их языки, предназначены для определенных семейств процессоров. Вместе с тем, они могут работать на разных платформах и в разных операционных системах. Также существуют кросс-ассемблеры, которые работают на одной архитектуре, а транслируют для другой.
Для Intel, например, самые популярные ассемблеры это MASM (входит в состав Visual Studio), FASM и TASM, которые достаточно современны и поддерживают Win32 API (хотя их развитие, нужно признать, уже пару лет как остановлено).
Для примера используем FASM (Flat Assembler). Редактор кода FEditor 2.0 предлагает следующий шаблон кода приложения типа «Hello World!» для DOS.
use16 org 100h mov ah, 9 mov dx, mes int 21h mov ah, 0Ah mov dx, key int 21h mov ax, 4C00h int 21h mes db 'Hello World!$' key db 2, 0, 0, 0
Это инструкции языка ассемблера как таковые. В этом приложении не используются макросы, которые широко применяются в современных приложениях на языке ассемблера. Рассмотрим вкратце, что делает эта программа.
AH
помещается код команды вывода строки, а в dx
— адрес этой строки, затем вызывается прерывание 21H
, которое выполняет эту команду (это зарезервированный сервис со стороны DOS, что-то типа современного Win32 API).ah
, адрес буфера для строки — в dx
.ax
и снова вызывается прерывание.Вывод в DOSBox:
Пример 64-разрядного консольного приложения типа «Hello World!
» на FASM, опять же из примеров FEditor.
format PE64 Console 5.0 entry Start include 'win64a.inc' section '.text' code readable executable Start: invoke GetStdHandle, [STD_OUTP_HNDL] mov [hStdOut], eax invoke WriteConsoleA, [hStdOut], mes, mesLen, chrsWritten, 0 invoke ExitProcess, 0 section '.data' data readable writeable mes db 'Hello World!', 0dh, 0ah, 0 mesLen = $-mes hStdOut dd 0 chrsWritten dd 0 STD_OUTP_HNDL dd -11 section '.idata' import data readable library kernel,'KERNEL32.DLL' import kernel,\ GetStdHandle, 'GetStdHandle',\ WriteConsoleA, 'WriteConsoleA',\ ExitProcess, 'ExitProcess'
EAX.
EAX
по адресу, закрепленному за идентификатором hStdOut
.hStdOut
), сообщения, расположенного в буфере, на который указывает указатель mes
. Сообщение имеет длину mesLen
символов. Фактическая длина выведенного сообщения отправляется по адресу, закрепленному за идентификатором chrsWritten
. Последний параметр зарезервирован, и в него передается значение 0.ExitProcess
для выхода из приложения с нулевым кодом (успешное завершение).db = define byte
) для строки, которая будет выведена. Ее завершает нулевой байт.mesLen
размещается длина сообщения. Она получена путем вычитания адреса первого байта строки из текущего адреса ($).
GetStdHandle, WriteConsoleA
и ExitProcess.
В результате компиляции получаем программу, которая выводит в консоли строку:
В FASM используются макросы, позволяющие сократить текст программы. Программа на ассемблере с макросами напоминает программу на высокоуровневом языке. Вот пример, поставляемый с FASM.
; example of simplified Windows programming using complex macro features include 'win32ax.inc' ; you can simply switch between win32ax, win32wx, win64ax and win64wx here .code start: invoke MessageBox,HWND_DESKTOP,"May I introduce myself?",invoke GetCommandLine,MB_YESNO .if eax = IDYES invoke MessageBox,HWND_DESKTOP,"Hi! I'm the example program!","Hello!",MB_OK .endif invoke ExitProcess,0 .end start
Он выводит диалоговое окно с сообщением, а затем, если будет нажата кнопка «Да», выводит еще одно диалоговое окно.
Синтаксис языка ассемблера зависит, в основном, от набора команд процессора. С другой стороны его определяют директивы определенного транслятора. Поэтому, например, для процессоров, совместимых с Intel, есть синтаксис Intel и синтаксис AT&T.
В машинном коде инструкции программы и данные располагаются в последовательно расположенных ячейках памяти. Команды выполняются в указанном порядке одна за другой, пока не будет вызвана команда перехода по иному указанному адресу, например для выполнения подпрограммы.
Адреса памяти, по которым будут осуществляться переходы, в ассемблере можно обозначать метками. Метки записываются с начала строки (с первой позиции) с двоеточием в конце. Команда перехода по метке может располагаться как до, так и после метки.
В приведенных выше примерах вы уже видели метки, задающие точку входа в программу:
… section '.text' code readable executable Start: invoke GetStdHandle, [STD_OUTP_HNDL] …
Процессоры «понимают» двоичный код. Ассемблеры позволяют использовать числа и в других системах счисления: десятичной, восьмеричной, шестнадцатеричной. Чтобы транслятор мог определить, в какой системе записано число, используется специальное представление чисел в разных системах счисления, причем эти представления для различных трансляторов могут отличаться друг от друга.
Десятичные* | Двоичные | Восьмеричные | Шестнадцатеричные | |
TASM: по последнему символу (не зависит от регистра) | D | B | O
| H если первая цифра – от |
MASM32: по последнему символу (не зависит от регистра) | D
| B
| O
| H если первая цифра – от |
FASM по префиксу или последнему символу (не зависит от регистра) | D | B | O | H если первая цифра – от Или же ставится префикс |
NASM по префиксу или последнему символу (не зависит от регистра) | D** | B**
| O**
| H если первая цифра – от Или же ставится префикс |
* В перечисленных ассемблерах по умолчанию используется десятичная система счисления, поэтому постфикс/префикс можно не указывать, когда используется основание 10.
** В NASM может быть как постфиксом, так и префиксом.
Ассемблер работает и с вещественными числами. Но это тема довольно сложная и обширная для обзорной статьи, оставим ее для более подробного знакомства с языком.
Команды ассемблера соответствуют командам семейства микропроцессоров, для которых они созданы. Это более удобная для человека запись команд — мнемокоды.
Рассмотрим инструкции Intel-синтаксиса. Он используется в ассемблерах, которые мы уже упоминали: Borland Turbo Assembler (TASM), Microsoft Macro Assembler (MASM), Flat Assembler (FASM), Netwide Assembler (NASM), и во многих других ассемблерах.
В нем используются такие команды:
mov, xchg, push, pop
…);add, sub, mul, div, inc, dec
…);and, or, xor, not
);sar, shr, sal, shl
…);bt, bts, btr, btc
…);jmp, jz, jnz, loop, ret, int
…);movs, cmps, scas, lods, stos
… и подобные команды для байтов, слов и двойных слов);stc, clc, cmc, cld, std
…);lds, les, lfs, lgs, lss
);lea, nop
…) и другие виды команд.Формат записи команды ассемблера таков:
[метка:] [ [префикс] мнемокод [операнд {, операнд}] ] [;комментарий]
Пример:
Output: mov ah, 9 mov dx, mes int 21h ; вызвать прерывание
Программа содержит не только команды, но и директивы. Они не транслируются в команды процессора, а управляют работой транслятора, поэтому зависят именно от транслятора, а не от процессора.
Директивы выполняют следующие функции:
В рассмотренном выше примере встречались, в частности, такие директивы:
format PE64 Console 5.0 entry Start include 'win64a.inc' section '.text' code readable executable mes db 'Hello World!', 0dh, 0ah, 0 mesLen = $-mes hStdOut dd 0
Преимущества этого языка позволяют использовать его в рассмотренных ниже сферах.
Уникальные характеристики языка ассемблера позволяют писать программы с такими возможностями, которые недоступны для других языков. Этот язык целесообразно использовать, когда необходимо обеспечить экономию памяти и быстродействие.
С помощью языка ассемблера создаются:
Благодаря возможностям дизассемблирования и реинжиниринга можно получить доступ к низкоуровневому коду программы. Это позволяет внедрить в нее собственный код: например вирус или обход защиты программы. В любом случае такие действия незаконны, если только вы не собираетесь обнаружить и устранить вирус или легально восстановить алгоритм работы программы, исходный код которой недоступен.
Так вот, давайте попробуем дизассембилровать приведенное здесь консольное приложение для Windows. Для этого используем отличный дизассемблер IDA (Interactive DisAssembler). Уже сразу при открытии IDA Pro выяснила, что использовался процессор AMD.
Но это еще цветочки. Она полностью раскрыла наши планы. Вот сегмент кода:
Видно, что куда передавали и какие функции вызывали. Переменные dword_402017
и т. п. — это ссылки на переменные в сегменте данных. Имена переменных из нашего кода в машинном коде не сохраняются: они не нужны процессору.
Это всего лишь условное обозначение адресов ячеек памяти. Дизассемблер дает им свои имена. А вот и наша переменная, и ссылка на ее использование в секции кода:
Для удобства чтения мы можем переименовать ее, как было в нашем коде:
Можем запустить процесс в отладчике и вычислить, как работает программа, наглядно увидеть переходы и так далее:
Можем работать с трассировкой чтения, трассировкой записи и трассировкой выполнения. Выполнять программу до определенного места в пошаговом режиме. Просматривать перекрестные ссылки, и вообще делать все, что доступно в отладчике. Возможно, даже больше, чем в некоторых.
Завершается наш код сегментом импорта:
Красиво, с перекрестными ссылками, удобными сигнатурами функций на Си.
На вкладке Hex View (шестнадцатеричное представление) можно просматривать шестнадцатеричное представление и редактировать код. Вызовем контекстное меню, щелкнув правой кнопкой мыши на w из слова world.
Выберем Edit, Прямо по тексту наберем «IDA!», а оставшиеся буквы заменим на нули слева в шестнадцатеричном коде.
Осталось применить изменения…
потом сохранить — и…
Дальнейшее освоение реинжиниринга остается на вашей совести 😉 Книга Криса Касперски «Искусство дизассемблирования» вам в помощь!
Часто язык ассемблера используется для написания фрагментов высокоуровневых программ. Когда фрагменты кода написаны на разных языках, их нужно связать между собой. Небольшие фрагменты кода можно вставлять непосредственно в код на другом языке.
Например, в C++ это выглядит так:
__asm { mov al, 2 mov dx, 0xD007 out dx, al }
В этом случае связывание производится на этапе компиляции.
Если же на языке ассемблера написан код с подпрограммами, данными и т. п., где реализуются возможности, не поддерживаемые в высокоуровневом языке, то связывание выполняется на этапе компоновки, а код на разных языках компилируется по отдельности.
Язык ассемблера получил свое название от англ. слова assembler — «сборщик». Сборщиком он назван потому, что позволил автоматически «собирать» программу, а не вводить машинные коды вручную, например в виде чисел в восьмеричной системе счисления. (Это еще упрощенное объяснение. О реальном опыте программирования в машинных кодах в 60–70-х годах прошлого века можно узнать из этой интересной статьи.)
Набор и отладка таких программ занимали много времени, разбираться в них было довольно сложно. Сложность росла и по мере роста объема этих программ, соответственно требовались большие расходы времени и денег. Возникла необходимость в упрощении работы для человека.
Первые ассемблеры появились в 1947-м и 1948-м году (акторы — Кэтлин Бут и Дэвид Уиллер). Они предназначались для машин ARC2 и EDSAC. Ассемблеры позволили ускорить и упростить разработку.
После ассемблеров появились и языки высокого уровня, компилируемые и интерпретируемые, со множеством библиотек. Некоторые из них очень приблизились к человеческому языку. Сейчас можно создавать игры, даже не зная языка программирования, перемещая блоки программы с помощью мыши.
В наше время для разработки низкоуровневого ПО (например, драйверов) чаще используется Си и библиотеки доступа к Win32 API. Благодаря оптимизации кода Си может работать с эффективностью ассемблера. На ассемблере пишут фрагменты кода, которые используют возможности, недоступные для языков высокого уровня: фрагменты ядра ОС и системных библиотек, вызовы каких-то недокументированных функций и т.п..
Ассемблер дает возможность понять, что происходит за кулисами, как работает процессор, как распределяется память, и разобраться, что к чему. А если вы разбираетесь, то будет проще разрабатывать эффективные программы на языках высокого уровня.
Язык ассемблера — это то, что дает программисту его могущество. Программисты используют ассемблер…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…