Рубріки: Теория

Язык ассемблера: предельно просто про синтаксис и кодирование

Андрій Денисенко

Редакция Highload разобралась, что такое язык ассемблера, разобрала его синтаксис и варианты использования. Ведь умение читать и писать код на низкоуровневом языке ассемблера – это весомый навык для любого системного программиста. Он позволяет создавать более оптимизированный код, использовать недоступные в Си возможности и выполнять реверс-инжиниринг скомпилированного кода.


Содержание:
1. Что такое язык ассемблера? Для чего он нужен?
2. Пример кода на языке ассемблера
3. Синтаксис языка ассемблера
4. Достоинства и недостатки
5. Применение языка ассемблера
6. Происхождение и критика языка сегодня

1. Что такое язык ассемблера? Для чего он нужен?

Язык ассемблера — это низкоуровневый язык программирования, который транслируется непосредственно в инструкции машинного языка. Трансляцию выполняет специальная программа — ассемблер (от англ. assembler — сборщик).

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

Инструкции машинного языка представлены в виде последовательностей из нулей и единиц. Создавать программное обеспечение в виде таких последовательностей очень сложно для человека. Поэтому был разработан низкоуровневый язык сборки, в котором инструкции представлены в более понятной для человека форме.

Ассемблеры, как и их языки, предназначены для определенных семейств процессоров. Вместе с тем, они могут работать на разных платформах и в разных операционных системах. Также существуют кросс-ассемблеры, которые работают на одной архитектуре, а транслируют для другой.

Для Intel, например, самые популярные ассемблеры это MASM (входит в состав Visual Studio), FASM и TASM, которые достаточно современны и поддерживают Win32 API (хотя их развитие, нужно признать, уже пару лет как остановлено).

2. Пример кода на языке ассемблера

Для DOS

Для примера используем 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

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

  • Используется 16-разрядная система, создается приложение .com.
  • В таких приложениях исполняемый код должен начинаться со смещения 100H. Первые адреса (до 0FFH) используются для префикса сегмента программы (Program Segment Prefix, PSP), в котором хранятся разные важные данные, которые крайне нежелательно перезаписывать.
  • В регистр AH помещается код команды вывода строки, а в dx — адрес этой строки, затем вызывается прерывание 21H, которое выполняет эту команду (это зарезервированный сервис со стороны DOS, что-то типа современного Win32 API).
  • Таким же образом готовится ожидание нажатия клавиши после вывода строки. Код команды — в ah, адрес буфера для строки — в dx.
  • После нажатия клавиши готовится команда завершения работа приложения. Ее код передается в регистр ax и снова вызывается прерывание.
  • Далее расположены определения данных: буфер для строки, которая выводится на экран, и для строки, которую представляет нажатая клавиша.

Вывод в DOSBox:

Windows — консольное

Пример 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'
  • Объявляется формат выходного файла.
  • Объявляется точка входа в программу.
  • В программу включается файл заголовков для 64-разрядных систем Windows и кодировки ANSI для текущей кодовой страницы.
  • Объявляется секция кода.
  • Указывается метка, которая объявлена точкой входа в программу. Выполнение кода начинается со строки, следующей за этой меткой.
  • Вызывается функция Windows для получения описателя стандартного потока вывода (для вывода в консоль). В результате ее выполнения этот описатель будет помещен в регистр EAX.
  • Этот описатель передается из регистра EAX по адресу, закрепленному за идентификатором hStdOut.
  • Вызывается API-функция Windows WriteConsole для вывода в консоль (по адресу, находящемуся в hStdOut), сообщения, расположенного в буфере, на который указывает указатель mes. Сообщение имеет длину mesLen символов. Фактическая длина выведенного сообщения отправляется по адресу, закрепленному за идентификатором chrsWritten. Последний параметр зарезервирован, и в него передается значение 0.
  • Вызывается функция Windows ExitProcess для выхода из приложения с нулевым кодом (успешное завершение).
  • Объявляется секция данных с доступом для чтения и записи. В нашем приложении она будет разбита на участки из байтов и двойных слов.
  • Объявляется буфер из байтов (db = define byte) для строки, которая будет выведена. Ее завершает нулевой байт.
  • По адресу с идентификатором mesLen размещается длина сообщения. Она получена путем вычитания адреса первого байта строки из текущего адреса ($).
  • По следующим двум адресам будут записаны указатель на поток стандартного вывода и количество выведенных символов (в виде двойных слов).
  • Указатель на константу для получения описателя потока стандартного вывода.
  • Объявляется секция импорта данных.
  • Объявляется псевдоним для библиотеки KERNEL32.DLL, из которой затем импортируются функции GetStdHandle, WriteConsoleA и ExitProcess.

В результате компиляции получаем программу, которая выводит в консоли строку:

Windows— GUI

В 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

Он выводит диалоговое окно с сообщением, а затем, если будет нажата кнопка «Да», выводит еще одно диалоговое окно.

3. Синтаксис языка ассемблера

Синтаксис языка ассемблера зависит, в основном, от набора команд процессора. С другой стороны его определяют директивы определенного транслятора. Поэтому, например, для процессоров, совместимых с Intel, есть синтаксис Intel и синтаксис AT&T.

Метки

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

Адреса памяти, по которым будут осуществляться переходы, в ассемблере можно обозначать метками. Метки записываются с начала строки (с первой позиции) с двоеточием в конце. Команда перехода по метке может располагаться как до, так и после метки.

В приведенных выше примерах вы уже видели метки, задающие точку входа в программу:

…
section '.text' code readable executable

Start:
  invoke GetStdHandle, [STD_OUTP_HNDL]
…

Числовые константы

Процессоры «понимают» двоичный код. Ассемблеры позволяют использовать числа и в других системах счисления: десятичной, восьмеричной, шестнадцатеричной. Чтобы транслятор мог определить, в какой системе записано число, используется специальное представление чисел в разных системах счисления, причем эти представления для различных трансляторов могут отличаться друг от друга.

Десятичные* Двоичные Восьмеричные Шестнадцатеричные
TASM:

по последнему символу (не зависит от регистра)

D B O

Q

H

если первая цифра – от A до F, то перед ней ставится 0

MASM32:

по последнему символу (не зависит от регистра)

D

T

B

Y

O

Q

H

если первая цифра – от A до F, то перед ней ставится 0

FASM

по префиксу или последнему символу (не зависит от регистра)

D B O H

если первая цифра – от A до F, то перед ней ставится 0

Или же ставится префикс 0x или $.

NASM

по префиксу или последнему символу (не зависит от регистра)

D** B**

Y**

O**

Q**

H

если первая цифра – от A до F, то перед ней ставится 0

Или же ставится префикс 0x, 0h или $.

* В перечисленных ассемблерах по умолчанию используется десятичная система счисления, поэтому постфикс/префикс можно не указывать, когда используется основание 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

  • определить формат выходного файла;
  • задать точку входа;
  • включить файл заголовков;
  • определить сегмент кода;
  • выделить пространство под данные.

4. Достоинства и недостатки

Достоинства языка ассемблера

  • Язык ассемблера предоставляет доступ ко всем возможностям архитектуры, в том числе к структурам памяти, типам данных, программным моделям процессоров, портам ввода-вывода и так далее
  • Язык ассемблера предоставляет доступ к 16-, 32- и 64-разрядным регистрам.
  • Язык ассемблера позволяет проводить реинжиниринг. Преимущество здесь в том, что с его помощью можно найти вредоносные внедренные фрагменты кода.
  • Компактность скомпилированного кода.
  • Высокая скорость выполнения программ.
  • Код на ассемблере можно использовать в других языках для оптимизации некоторых функций.

Недостатки языка ассемблера

  • Для разных архитектур используются разные ассемблеры и их языки, нет возможности непосредственно перенести программу на другую архитектуру.
  • Язык ассемблера позволяет проводить реинжиниринг. Это дает возможность заражать программы вирусами, но с помощью того же реинжиниринга можно эти вирусы обнаруживать.
  • Низкая скорость разработки, особенно больших приложений.
  • Мало библиотек по сравнению с языками высокого уровня.
  • Высокая вероятность ошибок в коде, и вследствие этого — сложная отладка.

Преимущества этого языка позволяют использовать его в рассмотренных ниже сферах.

5. Применение языка ассемблера

Уникальные характеристики языка ассемблера позволяют писать программы с такими возможностями, которые недоступны для других языков. Этот язык целесообразно использовать, когда необходимо обеспечить экономию памяти и быстродействие.

Сферы применения

С помощью языка ассемблера создаются:

  • драйверы устройств;
  • игры для компьютеров и игровых приставок;
  • фрагменты кода, от которых требуется высокая скорость выполнения;
  • встроенное ПО;
  • загрузочные секторы;
  • BIOS;
  • программы для микроконтроллеров;
  • вирусы;
  • антивирусы;
  • библиотеки для трансляторов языков программирования и так далее.

Нелегальная сфера деятельности

Благодаря возможностям дизассемблирования и реинжиниринга можно получить доступ к низкоуровневому коду программы. Это позволяет внедрить в нее собственный код: например вирус или обход защиты программы. В любом случае такие действия незаконны, если только вы не собираетесь обнаружить и устранить вирус или легально восстановить алгоритм работы программы, исходный код которой недоступен.

Так вот, давайте попробуем дизассембилровать приведенное здесь консольное приложение для 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
}

В этом случае связывание производится на этапе компиляции.

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

6. Происхождение и критика языка сегодня

Язык ассемблера получил свое название от англ. слова assembler — «сборщик». Сборщиком он назван потому, что позволил автоматически «собирать» программу, а не вводить машинные коды вручную, например в виде чисел в восьмеричной системе счисления. (Это еще упрощенное объяснение. О реальном опыте программирования в машинных кодах в 60–70-х годах прошлого века можно узнать из этой интересной статьи.)

Набор и отладка таких программ занимали много времени, разбираться в них было довольно сложно. Сложность росла и по мере роста объема этих программ, соответственно требовались большие расходы времени и денег. Возникла необходимость в упрощении работы для человека.

Первые ассемблеры появились в 1947-м и 1948-м году (акторы — Кэтлин Бут и Дэвид Уиллер). Они предназначались для машин ARC2 и EDSAC. Ассемблеры позволили ускорить и упростить разработку.

После ассемблеров появились и языки высокого уровня, компилируемые и интерпретируемые, со множеством библиотек. Некоторые из них очень приблизились к человеческому языку. Сейчас можно создавать игры, даже не зная языка программирования, перемещая блоки программы с помощью мыши.

В наше время для разработки низкоуровневого ПО (например, драйверов) чаще используется Си и библиотеки доступа к Win32 API. Благодаря оптимизации кода Си может работать с эффективностью ассемблера. На ассемблере пишут фрагменты кода, которые используют возможности, недоступные для языков высокого уровня:  фрагменты ядра ОС и системных библиотек, вызовы каких-то недокументированных функций и т.п..

Ассемблер дает возможность понять, что происходит за кулисами, как работает процессор, как распределяется память, и разобраться, что к чему. А если вы разбираетесь, то будет проще разрабатывать эффективные программы на языках высокого уровня.

Язык ассемблера — это то, что дает программисту его могущество. Программисты используют ассемблер…

Останні статті

Что такое прокси-сервер: пояснение простыми словами, зачем нужны прокси

Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…

21.11.2024

Что такое PWA приложение? Зачем необходимо прогрессивное веб-приложение

Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…

19.11.2024

Как создать игру на телефоне: программирование с помощью конструктора

Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…

17.11.2024

Google Bard: эффективный аналог ChatGPT

В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…

14.11.2024

Скрипт и программирование: что это такое простыми словами

Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…

12.11.2024

Дедлайн в разработке: что это такое простыми словами

Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…

11.11.2024