Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически «читаемый текст»- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.
В этом уроке мы обсудим несколько опкодов, которые имеют отношение к вычислению, поразрядным операциям, и т.д. Другие опкоды: команды перехода, сравнения и т.д, будут обсуждены позже.
Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.
Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).
ЯЗЫК АССЕМБЛЕРА за 3 МИНУТЫ
Самая первая команда будет хорошо всем известная MOV. Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:
mov приемник, источник
Вы можете копировать значение из одного регистра в другой.
mov edx, ecx
например: эта команда — НЕ допустима:
mov al, ecx ; не правильно
Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).
А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:
mov al, bl mov cl, dl mov cx, dx mov ecx, ebx
Вы также можете получить значение из памяти и поместить эго в регистр. Для примера возьмем следующую схему памяти:
смещение | 34 | 35 | 36 | 37 | 38 | 39 | 3A | 3B | 3C | 3D | 3E | 3F | 40 | 41 | 42 |
данные | 0D | 0A | 50 | 32 | 44 | 57 | 25 | 7A | 5E | 72 | EF | 7D | FF | AD | C7 |
(Каждый блок представляет байт)
Значение смещения обозначено здесь как байт, но на самом деле это это — 32-разрядное значение. Возьмем для примера 3A, это также — 32-разрядное значение: 0000003Ah. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.
Hello World на Ассемблере (x86)
Посмотрите на смещение 3A в таблице выше. Данные на этом смещении — 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, вы также используете команду mov:
mov eax, dword ptr [0000003Ah]
Означает: поместить значение с размером DWORD (32-бита) из памяти со смещением 3Ah в регистр eax. После выполнения этой команды, eax будет содержать значение 725E7A25h. Возможно вы заметили, что это — инверсия того что находится в памяти: 25 7A 5E 72. Это потому, что значения сохраняются в памяти, используя формат little endian . Это означает, что самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед. Я думаю, что эти примеры покажут это:
- dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10
- word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40
Вернемся к примеру выше. Вы также можете это делать и с другими размерами:
mov cl, byte ptr [34h] ; cl получит значение 0Dh mov dx, word ptr [3Eh] ; dx получит значение 7DEFh
Вы, наверное, уже поняли, что префикс ptr обозначает, что надо брать из памяти некоторый размер. А префикс перед ptr обозначает размер данных:
Byte — 1 байт Word — 2 байта Dword — 4 байта
Иногда размер можно не указывать:
mov eax, [00403045h]
Так как eax — 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.
Можно также непосредственные значения:
mov edx, 5006h
Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.
Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):
mov eax, 403045h ; пишет в eax значение 403045 mov cx, [eax] ; помещает в регистр CX значение (размера word) из памяти ; указанной в EAX (403045)
В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.
Стековые операции — PUSH, POP. Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP.
Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении — увеличивается. Рассмотрим пример:
(1) mov ecx, 100 (2) mov eax, 200 (3) push ecx ; сохранение ecx (4) push eax (5) xor ecx, eax (6) add ecx, 400 (7) mov edx, ecx (8) pop ebx (9) pop ecx
- 1: поместить 100 в ecx
- 2: поместить 200 в eax
- 3: разместить значение из ecx (=100) в стеке (размещается первым)
- 4: разместить значение из eax (=200) в стеке (размещается последним)
- 5/6/7: выполнение операций над ecx, значение в ecx изменяется
- 8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
- 9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)
Чтобы узнать, что происходит в памяти, при размещении и извлечении значений в стеке, см. на рисунок ниже:
(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)
mov ax, 4560h push ax
mov cx, FFFFh push cx
pop edx
edx теперь 4560FFFFh.
Вызов подпрограмм возврат из них — CALL, RET. Команда call передает управление ближней или дальней процедуре с запоминанием в стеке адреса точки возврата.
Команда ret возвращает управление из процедуры вызывающей программе, адрес возврата получает из стека. Пример:
..code.. call 0455659 ..more code.. Код с адреса 455659: add eax, 500 mul eax, edx ret
Когда выполняется команда call, процессор передает управление на код с адреса 455659, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой. Вы можете поместить код, который вы часто используете в процедуру и каждый раз когда он вам нужен вызывать его командой call.
Подробнее: команда call помещает регистр EIP (указатель на следующюю команду, которая должна быть выполнена) в стек, а команда ret извлекает его и передаёт управление этому адресу. Вы также можете определить аргументы, для вызываемой программы (процедуры). Это можно сделать через стек:
push значение_1 push значение_2 call procedure
Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.
Одно важное замечание: регистр eax почти всегда используется для хранения результата процедуры.
Это также применимо к функциям windows. Конечно, вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.
Вот и кончился очередной урок. На следующем уроке мы напишем первую программу на ассемблере.
Источник: codenet.ru
Программирование на языке ассемблера. Основы
Программа представляет собой последовательность команд, выполнение которых приводит к решению задачи.
Команда определяет операцию, которую выполняет МП над данными. Она содержит в явной или неявной форме информацию о том, где будет помещен результат операции, а также об адресе следующей команды. Код команды состоит из нескольких частей, которые называются полями. Состав, назначение и расположение полей называется форматом команды. Формат команды в общем случае содержит операционную и адресную часть. Адресная часть может состоять из нескольких полей и содержать информацию об адресах операндов, результате операции и о следующей команде. Поле «признак адресации» определяет способ адресации операнда. Биты полей «Признак адресации» и «Адрес операндов» в совокупности определяют ячейки памяти, в которых хранятся операнды. Различают группы команд:
- команды передачи данных;
- команды ввода/вывода;
- команды обработки информации (арифметические, логические, сдвиг, сравнение операндов, десятичная коррекция);
- команды управления порядком выполнения программы (переход, вызов подпрограммы, возврат из подпрограммы, прерывания);
- команды задания режимов работы МП.
Общее количество бит в коде команды называют длиной формата. Длина формата определяет скорость выполнения команды и зависит от способа адресации операндов.
Существуют следующие способы адресации:
- Прямая адресация. Адрес операнда указан непосредственно в команде
- Косвенная адресация. В формате команды указывается регистр, в котором хранится адрес ячейки памяти, содержащей операнд. Для хранения 16-разрядного адреса в 8-разрядном процессоре 8-разрядные регистры объединяются в регистровые пары. В первом регистре регистровой пары хранится старший байт адреса, а во втором – младший байт.
- Непосредственная адресация. В первом байте команды размещается код операции. Значения операндов находятся во втором или втором и третьем байтах. Это, как правило, константы.
- Автоинкрементная (автодекрементная) адресация. Адрес операнда вычисляется так же, как и при косвенной адресации, после чего осуществляется увеличение (уменьшение) содержимого регистра : на один – для обращения к следующему байту, на два – для обращения к следующему слову.
- Страничная адресация. Память делится на ряд страниц одинаковой длины. Адресация страниц осуществляется по содержимому программного счетчика или отдельного регистра страниц. Адресация памяти внутри страниц осуществляется адресом, который содержится в команде.
- Индексная адресация. Для преобразования адреса операнда к значению адресного поля команды прибавляется содержимое индексного регистра, которое называется индексом.
- Относительная адресация. Адрес операнда определяется сложением содержимого программного счетчика или другого регистра (базового адреса) с указанным в команде числом. Для хранения базовых адресов в МП могут быть предусмотрены базовые регистры или специально выделенные ячейки памяти. Тогда в адресном поле регистра указывается номер базового регистра.
Ассемблером называется и язык программирования в мнемокодах команд, и специальная программа-транслятор, которая переводит мнемокоды в машинные коды, считываемые процессором из памяти программ, дешифрируемые и выполняемые. Процесс перевода в машинные коды называется ассемблированием.
Программа на языке ассемблера содержит 2 типа выражений:
- Команды, которые транслируются в машинные коды
- Директивы, которые управляют процессом трансляции.
Выражение имеет вид:
: (мнемокод) < ,> < (операнд)> < ; комментарий >
В фигурных скобках приведены элементы выражения, которых может не быть в некоторых командах. Метка, мнемокод и операнды отделяются хотя бы одним пробелом или табуляцией. Максимальная длина строки составляет 132 символа, однако наиболее часто используются строки из 80 символов, что соответствует длине экрана.
Примеры команд на языке ассемблера:
MOV AX, 0 ; команда, два операнда M1: ADD AX, BX ; метка, команда, два операнда DELAY: MOV CX, 1234 ; метка, команда, два операнда
Метка на языке ассемблера является символическим адресом команды. Метками обозначаются не все команды, а лишь те, к которым надо выполнять переход с помощью команд переходов или вызовов подпрограмм. После метки ставится двоеточие.
Максимальная длина метки – 31 символ. Нельзя использовать в качестве меток зарегистрированные ассемблером слова, к которым принадлежат коды команд, директивы, имена регистров. Все метки в программе должны быть уникальными, т.е. не может быть нескольких меток с одинаковыми именами.
Мнемокод идентифицирует команду ассемблера. Для мнемокодов используют сокращенные или полные английские слова, которые передают значения основной функции команды.
Операнды отделяются запятыми. Если даны два операнда, то первый из них всегда является источником, а второй – приемником информации.
Комментарии игнорируются в процессе трансляции и используются для документирования и лучшего понимания содержания программ. Комментарии всегда начинается с символа «;» и может содержать любые символы.
Директивы предназначены для управления процессом ассемблирования и формирования листинга. Язык ассемблера содержит такие основные директивы:
- Начала и конца сегмента SEGMENT и ENDS
- Начала и конца процедуры PROC и ENDP
- Назначения сегментов ASSUME
- Начала ORG
- Распределения и планирования памяти DB, DW, DD
- Завершения программы END
- Метки LABEL
Примеры написания простых программ на ассемблере
Простые программы целесообразно оформлять в виде командных файлов. Первой директивой таких программ является директива ORG 100H, последней – END.
Пример 1. Написать программу сложения содержимого двух 8-разрядных ячеек памяти, которые находятся в сегменте данных со смещениями 1000Н и 1001Н соответственно. Результат разместить в ячейке памяти с адресом 1002H.
В этом примере для простоты не будем учитывать возможность возникновения переносов. Программа имеет вид:
ORG 100H ; Начало программы MOV A, 1000H ; A — 1000H ; переслать в 8-разрядный регистр A содержимое ячейки ; памяти 1000 в шестнадцатеричном коде (Н) ADD A, 1001H ; A — A + 1001H ; прибавить к содержимому A содержимое ячейки памяти ; с адресом 1001 в шестнадцатеричном коде MOV 1002H, A ; 1002H- A ; переслать содержимое A в ячейку 1002H END ; завершение программы
Пример 2. Написать программу деления содержимого А на содержимое В. Результат поместить в 8-разрядную ячейку памяти с адресом 1000Н. остатком от деления пренебречь. Если содержимое В=0, то деление не выполнять, а на место результата поместить число FFFFH.
ORG 100H ; Начало программы CMP B,0 ; сравнить содержимое В с нулем (команда ; влияет на установку флага нуля Z) JZ M1 ; если Z=1 (В=0), то переход на метку М1 DIV B ; иначе выполнить деление А – А:В JMP M2 ; безусловный переход на метку М» М1: MOV A, FFFFH ; занести число FFFFH в А М2: MOV 1000H, A ; запомнить А в ячейке с адресом 1000Н END ; завершение программы
Регистр признаков микропроцессора КР580ИК80А (I8080)
- S – признак знака; принимает значение старшего разряда результата
- Z – признак нуля; если результат равен 0, то Z =1, иначе Z =0
- AC – признак вспомогательного переноса; если есть перенос между тетрадами байта, то АС=1, иначе АС=0
- P – признак четности; если число единиц в байте результата четно, то Р=1, иначе Р=0
- CY – признак переноса (займа); если при выполнении команды возник перенос из старшего разряда или заем в старший разряд, то CY =1, иначе CY=0
Источник: pro-prof.com
Начало работы с ассемблером
Если что-то не так, то доредактируем файл. После этого dosbox будет сконфигурирован.
Сегментация программы
По знаменитой формуле Никлауса Вирта (создателя языка Pascal, http://ru.wikipedia.org/wiki/%D0%92%D0%B8%D1%80%D1%82,_%D0%9D%D0%B8%D0%BA%D0%BB%D0%B0%D1%83%D1%81) программа = алгоритм + данные. Этот принцип в программировании реализуется повсеместно. Процессор 8086 на аппаратном уровне производит разделение программы на код и данные.
Для этого вводится понятие сегмента: это участок памяти объёмом в 64 KB, в котором процессор подразумевает нахождение либо данных, либо кода. Адрес начала сегмента хранят сегментные (системные) регистры: cs, ds, ss, es. При выполнении программы процессор обращается по адресу cs:ip и выполняет команду, которая содержится в этой ячейке памяти. Регистр ds указывает на начало сегмента данных, многие команды процессора, которые связаны с работой с памятью, вычисляют физический адрес данных, начиная от ds. Однако мы рассмотрели не все системные регистры: осталось ещё два. es — это дополнительный сегмент данных, он функционирует практически так же, как и ds. А вот ss — сегмнт стека (stack segment), он по-сложнее…
Stack (стек)
Стек (англ. stack — стопка) — структура данных с методом доступа к элементам LIFO (англ. Last In — First Out, «последним пришел — первым вышел»). Чаще всего принцип работы стека сравнивают со стопкой тарелок: чтобы взять вторую сверху, нужно снять верхнюю. (или со стопкой подносов:))
Добавление элемента, называемое также проталкиванием (push), возможно только в вершину стека (добавленный элемент становится первым сверху). Удаление элемента, называемое также выталкивание (pop), возможно также только из вершины стека, при этом, второй сверху элемент становится верхним.
Итак, стек — это тот же сегмент данных. На вершину стека указывает адрес ss:sp. В 8086 процессоре есть несколько команд, которые меняют содержимое стека, мы их рассмотрим позднее.
Написание программ на ассемблере
Первое, с чем мы столкнёмся: для ассемблера нет оболочки. Исторически сложилось, что программу вводят в текстовом редакторе, а затем запускают сам ассемблер и получают исполняемый файл — файл, который можно (а может и нельзя) запустить на компьютере.
Итак, чтобы написать программу мы должны создать файл в папке asm, отредактировать его с помощью gedit (особо одарённые могут использовать vi или emacs) и скомпилировать его в dosbox. Компиляция проходит в два этапа. Первый: собственно ассемблирование (команда tasm имя_файла). В результате получится файл имя_файла.obj. Этот файл запустить в dosbox просто так не удастся.
Вы можете подключить к этому файлу другие с аналогичным расширением. Это действие называется линковкой. В нашем случае почти всегда мы будем линковать только один файл командой tlink имя_файла.obj. Если программа была написана без ошибок, то на выходе мы получим имя_файла.exe или имя_файла.com.
ВНИМАНИЕ.
Имя файла в MS-DOS и в DOSBox не может превышать восьми символов.
Программы типа *.com
Главная особенность com-программ: cs, ds, es и ss указывают на один и тот же сегмент, тем самым смешивая всё в одну кучу! При компиляции tasm тщательно следит за тем, чтобы вы не меняли содержимого системных регистров. Так как на всю программу выделен всего один сегмент, то максимальный размер программы: 64 КБайт. Обычно этот тип файлов используется в качестве драйверов для MS-DOS.
Программы типа *.exe
В exe-программе под код данные и стек может отводится несколько сегментов. Компилятор никак не отслеживает изменение системных регистров, в этом варианте вы можете делать с компьютером всё, что хотите (или можете).
Программа на ассемблере
Для того, чтобы приступить к работе, мы должны познакомится с синтаксисом языка ассемблер.
Команды
[метка:] опкод [операнды] [;комментарий]
где опкод (код операции) — непосредственно мнемоника инструкции процессору. К ней могут быть добавлены префиксы (повторения, изменения типа адресации и пр.). Метка (если имеется), команда и операнд (если имеется) pазделяются по крайней мере одним пробелом или символом табуляции. Максимальная длина строки — 132 символа, однако, большинство предпочитают работать со строками в 80 символов (соответственно ширине экрана). Примеры кодирования:
Метка Команда Операнд COUNT DB 1 ;Имя, команда, один операнд MOV AX,0 ;Команда, два операнда
Метки
Метка в языке ассемблера может содержать следующие симво лы:
Первым символом в метке должна быть буква или спецсимвол. Ассемблер не делает различия между заглавными и строчными буквами. Максимальная длина метки — 31 символ. Примеры меток: COUNT, PAGE25, $E10. Рекомендуется использовать описательные и смысловые метки.
Имена регистров, например, AX, DI или AL являются зарезервированными и используются только для указания соответствующих регистров. Например, в команде
ADD AX,BX
ассемблер «знает», что AX и BX относится к регистрам. Однако, в команде
MOV REGSAVE,AX
ассемблер воспримет имя REGSAVE только в том случае, если оно будет определено в сегменте данных. В приложении 3 приведен cписок всех зарезервированных слов ассемблера.
Команда
Мнемоническая команда указывает ассемблеру какое действие должен выполнить данный оператор. В сегменте данных команда (или директива) определяет поле, рабочую oбласть или константу. В сегменте кода команда определяет действие, например, пересылка (MOV) или сложение (ADD).
Операнд
Если команда специфирует выполняемое действие, то операнд определяет а) начальное значение данных или б) элементы, над которыми выполняется действие по команде. В следующем примере байт COUNTER определен в сегменте данных и имеет нулевое значение:
Метка Команда Операнд COUNTER DB 0 ;Определить байт (DB) ; с нулевым значением
Команда может иметь один или два операнда, или вообще быть без операндов. Рассмотрим следующие три примера:
Команда Операнд Комментарий Нет операндов RET ;Вернуться Один операнд INC CX ;Увеличить CX Два операнда ADD AX,12 ;Прибавить 12 к AX
Метка, команда и операнд не обязательно должны начинаться с какой-либо определенной позиции в строке. Однако, рекомен дуется записывать их в колонку для большей yдобочитаемости программы. Для этого, например, редактор DOS EDLIN обеспечи вает табуляцию чепез каждые восемь позиций.
Программа на языке ассемблера может содержать директивы: инструкции, не переводящиеся непосредственно в машинные команды, а управляющие работой компилятора. Набор и синтаксис их значительно разнятся и зависят не от аппаратной платформы, а от используемого транслятора (порождая диалекты языков в пределах одного семейства архитектур). В качестве «джентельменского набора» директив можно выделить следующие:
определение данных (констант и переменных),
управление организацией программы в памяти и параметрами выходного файла,
задание режима работы компилятора,
всевозможные абстракции (то есть элементы языков высокого уровня) — от оформления процедур и функций (для упрощения реализации парадигмы процедурного программирования) до условных конструкций и циклов (для парадигмы структурного программирования),
Директивы управления листингом: PAGE и TITLE
Ассемблер содержит ряд директив, управляющих форматом печати (или листинга). Обе директивы PAGE и TITLE можно использовать в любой программе.
Директива PAGE
В начале программы можно указать количест во строк, распечатываемых на одной странице, и максимальное количество символов на одной строке. Для этой цели cлужит директива PAGE. Следующей директивой устанавливается 60 строк на страницу и 132 символа в строке:
PAGE 60,132
Количество строк на странице межет быть в пределах от 10 до 255, а символов в строке — от 60 до 132. По умолчанию в ассемблере установлено PAGE 66,80. Предположим, что счетчик строк установлен на 60. В этом случае ассемблер, распечатав 60 строк, выполняет прогон листа на начало следующей страницы и увеличивает номер страницы на eдиницу.
Кроме того можно заставить ассемблер сделать прогон листа на конкретной строке, например, в конце сегмента. Для этого необходимо записать директиву PAGE без операндов. Ассемблер автоматически делает прогон листа при обработке диpективы PAGE.
Директива TITLE
Для того, чтобы вверху каждой страницы листинга печатался заголовок (титул) программы, используется диpектива TITLE в следующем формате:
TITLE текст
Рекомендуется в качестве текста использовать имя програм мы, под которым она находится в каталоге на диске. Например, если программа называется ASMSORT, то можно использовать это имя и описательный комментарий общей длиной до 60 символов:
TITLE ASMSORT — Ассемблерная программа сортировки имен
В ассемблере также имеется директива подзаголовка SUBTTL, которая может оказаться полезной для очень больших программ, содержащих много подпрограмм.
Директива SEGMENT
Любые ассемблерные программы содержат по крайней мере один сегмент — сегмент кода. В некоторых программах используется сегмент для стековой памяти и сегмент данных для определения данных. Асcемблерная директива для описания сегмента SEGMENT имеет следующий формат:
Имя Директива Операнд имя SEGMENT [параметры] . . . имя ENDS
Имя сегмента должно обязательно присутствовать, быть уникальным и соответствовать соглашениям для имен в ассемблере. Директива ENDS обозначает конец сегмента. Обе директивы SEGMENT и ENDS должны иметь одинаковые имена. Директива SEGMENT может содержать три типа параметров, определяющих выравнивание, объединение и класс.