Основы ассемблера


Содержание

Ассемблер для чайников

Эта книга рассчитана на начинающих изучать язык ассемблера. Двольно часто можно увидеть книги и статьи с заголовками типа Ассемблер это просто. Как бы не так. Подобные лозунги ни что иное, как маркетинговый ход — надо же как то завлекать клиентов (читателей). Конечно, научиться писать простые программки и в самом деле легко (в этом вы убедитесь, прочитав первые главы представленной ниже книги). Но всё зависит от задач, которые вы перед собой ставите. Научиться водить автомобиль — это просто. Однако Шумахер только один.

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

Хочу всё знать: язык ассемблера

Краткая справка

Язык ассемблера – машинно-ориентированный код низкого уровня, первое упоминание о котором датировано 40-ми годами 20-го века в контексте взаимодействии с компьютером EDSAC. Несмотря на то, что он не всегда использует внутренние инструкции самих машин (всё-таки речь идёт об универсализации), это практически не вызывает потери в быстродействии, наоборот лишь предоставляя пользователю возможности для использования макрокоманд.

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

Исходя из этого выделим очевидные достоинства:

Быстродействие. Быстрее только использовать непосредственные инструкции процессора;

Безопасность. Низкоуровневость в данном случае практически исключает наличие белых пятен в коде;

Эффективность использования возможностей конкретной платформы. Ориентированность на используемую машину позволяет иметь серьезное преимущество по сравнению с высокоуровневыми языками;

Понимание исполняемого кода программистом. На таком уровне программирования код и действие имеют очевидно причинно-следственную связь.

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

Трудно выучить. Специализация на платформу, несколько диалектов — все это не способствует быстрому изучению и пониманию;

Тяжело читать. Большой листинг, простые однотипные операции;

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

Большинство используемых машин просто не нуждается в таком примитивном языке, как ассемблер;

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

Сильно ограниченное количество библиотек, сообществ, вспомогательных ресурсов по современным меркам.

Кому изучать?

Очевидно, что язык ассемблера был хорош и востребован во времена не слишком мощных компьютеров, но почему новый виток роста объявился сейчас? Всё просто: на мир огромной волной надвигается IoT. Использование многочисленных компьютеров, подчинённых одной цели, оправдывает себя только при общей максимально низкой себестоимости и высоком быстродействии, что вынуждает разработчиков обратится к старому-доброму языку ассемблера.

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

Книги

Zen of Assembly Language, Майкл Абраш – именно с этой книги стоит начать изучение, если уж без языка ассемблера вам не обойтись. Основная цель, которую пытается Абраш донести до читателя — это необходимость мыслить легко и широко (“иначе”) в решении сложных задач с помощью такого низкоуровневого инструмента;

Programming from the Ground Up, Джонатан Бартлетт –вторая книга по очереди для прочтения имеет более сухой язык изложения, зато изобилует полезными советами и техническими данными;

Introduction to 64 Bit Assembly Language, Programming for Linux and OS X, Рэй Сейфарт — в этой книге язык ассемблера рассматривается, как базис для всех систем и устройств. Новичку такая книга может показаться тяжелой для понимания, поэтому рекомендуется иметь за плечами хоть какие-то познания в программировании;

Assembly Language for x86 Processors, Уип Ирвинг — уже из названия вы можете понять, что это в большей степени справочная книга, рекомендуемая в учебных заведениях в качестве дополнительной литературы. Однако распространенность данных процессоров и практически неизбежность работы с ними, переносит эту книгу в раздел must-read.

Art of Assembly Language, Рэндэлл Хайд — еще одна прекрасная книга для новичков. Говорят, это одна из наиболее часто рекомендуемых книг в интернете в данной области;

PC Assembly Language, Пол Картер – обучающая языку ассемблера книга с огромным количеством примеров и конкретным их применением из реальной жизни;

Ассемблер и дизассемблирование, Пирогов Владислав – должна быть среди всего этого обучающего великолепия и книга на русском языке. Примеры кода, описание инструментов и ответы на актуальные вопросы новичков — всё прилагается;

Ассемблер? Это просто! Учимся программировать, Калашников Олег — книга второй ступени познания языка ассемблера. То лучше наложить информацию, описанную в ней, на ваши хотя бы минимальные уже полученные знания;

Ассемблер на примерах, Марек Рудольф — а вот эта книга, хоть и не российского производства (однако в оригинале вы её тоже вряд ли прочтёте), идеально подойдёт в качестве базиса. Всё доступно и понятно.

Как вы уже знаете, согласно последним мировым тенденциям, язык ассемблера снова входит в 10-ку самых популярных языков программирования. Как так получилось, что в мире, где большинство начинающих разработчиков с трудом представляет механизмы взаимодействия кода и машины, это первобытное создание вновь на виду? И главное: надо ли учить язык ассемблера? И как? Попробуем разобраться.

Краткая справка

Язык ассемблера – машинно-ориентированный код низкого уровня, первое упоминание о котором датировано 40-ми годами 20-го века в контексте взаимодействии с компьютером EDSAC. Несмотря на то, что он не всегда использует внутренние инструкции самих машин (всё-таки речь идёт об универсализации), это практически не вызывает потери в быстродействии, наоборот лишь предоставляя пользователю возможности для использования макрокоманд.

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

Исходя из этого выделим очевидные достоинства:

Быстродействие. Быстрее только использовать непосредственные инструкции процессора;

Безопасность. Низкоуровневость в данном случае практически исключает наличие белых пятен в коде;

Эффективность использования возможностей конкретной платформы. Ориентированность на используемую машину позволяет иметь серьезное преимущество по сравнению с высокоуровневыми языками;

Понимание исполняемого кода программистом. На таком уровне программирования код и действие имеют очевидно причинно-следственную связь.

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

Трудно выучить. Специализация на платформу, несколько диалектов — все это не способствует быстрому изучению и пониманию;

Тяжело читать. Большой листинг, простые однотипные операции;

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

Большинство используемых машин просто не нуждается в таком примитивном языке, как ассемблер;

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

Сильно ограниченное количество библиотек, сообществ, вспомогательных ресурсов по современным меркам.

Кому изучать?

Очевидно, что язык ассемблера был хорош и востребован во времена не слишком мощных компьютеров, но почему новый виток роста объявился сейчас? Всё просто: на мир огромной волной надвигается IoT. Использование многочисленных компьютеров, подчинённых одной цели, оправдывает себя только при общей максимально низкой себестоимости и высоком быстродействии, что вынуждает разработчиков обратится к старому-доброму языку ассемблера.

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

Книги

Zen of Assembly Language, Майкл Абраш – именно с этой книги стоит начать изучение, если уж без языка ассемблера вам не обойтись. Основная цель, которую пытается Абраш донести до читателя — это необходимость мыслить легко и широко (“иначе”) в решении сложных задач с помощью такого низкоуровневого инструмента;

Programming from the Ground Up, Джонатан Бартлетт –вторая книга по очереди для прочтения имеет более сухой язык изложения, зато изобилует полезными советами и техническими данными;

Introduction to 64 Bit Assembly Language, Programming for Linux and OS X, Рэй Сейфарт — в этой книге язык ассемблера рассматривается, как базис для всех систем и устройств. Новичку такая книга может показаться тяжелой для понимания, поэтому рекомендуется иметь за плечами хоть какие-то познания в программировании;

Assembly Language for x86 Processors, Уип Ирвинг — уже из названия вы можете понять, что это в большей степени справочная книга, рекомендуемая в учебных заведениях в качестве дополнительной литературы. Однако распространенность данных процессоров и практически неизбежность работы с ними, переносит эту книгу в раздел must-read.

Art of Assembly Language, Рэндэлл Хайд — еще одна прекрасная книга для новичков. Говорят, это одна из наиболее часто рекомендуемых книг в интернете в данной области;

PC Assembly Language, Пол Картер – обучающая языку ассемблера книга с огромным количеством примеров и конкретным их применением из реальной жизни;

Ассемблер и дизассемблирование, Пирогов Владислав – должна быть среди всего этого обучающего великолепия и книга на русском языке. Примеры кода, описание инструментов и ответы на актуальные вопросы новичков — всё прилагается;

Ассемблер? Это просто! Учимся программировать, Калашников Олег — книга второй ступени познания языка ассемблера. То лучше наложить информацию, описанную в ней, на ваши хотя бы минимальные уже полученные знания;

Ассемблер на примерах, Марек Рудольф — а вот эта книга, хоть и не российского производства (однако в оригинале вы её тоже вряд ли прочтёте), идеально подойдёт в качестве базиса. Всё доступно и понятно.

Основы Ассемблера

Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически «читаемый текст»- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.

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

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.

Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV.

Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

mov приемник, источник

Вы можете копировать значение из одного регистра в другой.

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми, например: эта команда — НЕ допустима:

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. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.

Посмотрите на смещение 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.

Можно также непосредственные значения:

Эта команда просто запишет в регистр 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 (первое размещение, последнее извлечение)

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

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение 00 00 00 00 00 00 00 00 00
ESP
(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

mov ax, 4560h
push ax

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение 00 00 60 45 00 00 00 00 00
ESP

mov cx, FFFFh
push cx

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение FF FF 60 45 00 00 00 00 00
ESP

pop edx

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение FF FF 60 45 00 00 00 00 00
ESP

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. Конечно, вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.

Вот и кончился очередной урок. На следующем уроке мы напишем первую программу на ассемблере.

Рубрика: Ассемблер для начинающих

Самоучитель по быстрому и эффективному изучению ассемблера.

Ассемблер с нуля. Часть 15. Процедуры (функции).

Процедуры в ассемблере.

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

Ассемблер с нуля. Часть 14. Turbo Debugger.

Отладчик ассемблера Turbo Debugger.

Применим усвоенные понятия на практике — прогоним нашу крохотную программу prg.com через отладчик ассемблера Turbo Debugger (TD). Именноо TD мы будем использовать в изучении 16 битного ассемблера. Для 32 и 64 битного программирования имеется свое програмное обеспечение. Вместе с тем, для своих целей Turbo Debugger очень хорош. Работать с программой просто и понятно — ничего лишнего — то, что нужно для начинающих.

Ассемблер с нуля. Часть 13. Команды ассемблера.

Команды ассемблера и команды процессора.

Стоит пояснить, что если к вопросу подойти формально строго, то команды процессора и команды ассемблера — это не одно и то же. Ассеммблер — хоть и низкоуровневый язык программирования, но иногда он без спроса программиста «корректирует код под себя». Причём у каждого ассемблера (masm, tasm, fasm) это может быть по-разному. Самый яркий пример — команда ret. В ассемблерном коде мы запишем ret, а реальный ассемблер ассемблирует её как retf или retn 8. Может также изменяться код, добавлением в качестве выравнивания кода команды процессора nop (об этом ниже в статье) и т.п. Чтобы не усложнять суть вопроса, под понятиями команды процессора и команды ассемблера мы будем подразумевать одно и то же.

Ассемблер с нуля. Часть 12. Указатель.

Понятие указателя.

Понятие указателя (pointer) — одно из самых важных в программировании. Для начинающего програмиста просто необходимо его понять и усвоить. Попробуем разобраться, что это такое и зачем он нужен.

Процесор работает с памятью. Минимально возможное значение памяти — 1 бит (bit). Память воспринимается машиной блочно. Один минимальный блок равен одному байту (byte), 1 байт = 8 бит (bit — минимальная адресуемая единица памяти).

Ассемблер с нуля. Часть 11. Регистры.

Регистры процессора.

Процессор работает с данными. Обрабатываемые данные содержаться в оперативной памяти, куда считываются с жёсткого диска. Для того, чтобы ими манипулировать, а также верно организовывать свою работу по их обработке — выполнению программного кода, у процессора имеется система, построенная на небольших, но очень быстрых блоках памяти — регистров. Регистры процессора — это входящие непосредственно в процессор блоки памяти.

Ассемблер с нуля. Часть 10. Данные.

Данные в ассемблере.

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

Ассемблер с нуля. Часть 9. Системы счисления.

Три системы счисления.

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

Ассемблер с нуля. Часть 8. Ключевые понятия.

Основные понятия.

Чтобы идти дальше, нам необходимо получить ключевую информацию об определённых понятиях, научиться использовать в работе отладчики и дизассемблеры — программы, которые помогут нам изучать и анализировать наш код.

  1. Шестнадцатеричная система счисления.
  2. Данные.
  3. Регистры.
  4. Указатель.
  5. Команды ассемблера.
  6. Процедура (подпрограмма, функция).
  7. Стек.

Ассемблер с нуля. Часть 7. Анализ кода.

Анализ программного кода «Hello World!» на ассемблере.

Проведем анализ программного кода нашей первой программы на ассемблере.

Ассемблер с нуля. Часть 6. Первая программа.

Наша первая программа на ассемблере.

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

Ещё мы знаем, что в указанном формате пишутся резидентные программы, драйверы и вирусы.

Лекция 7. Основы программирования на языке Ассемблера

7.1. Структура команды языка Ассемблера

Давайте рассмотрим команду «загрузить число 0x1234 в регистр АХ». На языке ассемблера она записывается очень просто:

Для процессора каждая команда представляется в виде двоичного числа (пункт 7 концепции фон Неймана). Ее числовое представление называется машинным кодом. Команда MOV АХ, 0x1234 на машинном языке может быть записана так:

0x1111: 0хВ8, 0x34, 0x12

0x1114: следующие команды

В рассматриваемом примере команда помещена по адресу 0x1111. Следующая команда начинается тремя байтами дальше, значит, под команду MOV с операндами отведено 3 байта. Второй и третий байты содержат операнды команды MOV. А что такое 0хВ8? После преобразования 0хВ8 в двоичную систему мы получим значение 10111000b. Первая часть – 1011 – и есть код команды MOV. Встретив код 1011, контроллер «понимает», что перед ним – именно MOV. Следующий разряд (1) означает, что операнды будут 16-разрядными. Три последние цифры определяют регистр назначения. Три нуля соответствуют регистру АХ (или AL, если предыдущий бит был равен 0, указывая таким образом, что операнды будут 8-разрядными).

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

Общий формат команды языка ассемблера такой:

имя_команды [подсказка] операнды

Необязательная подсказка указывает компилятору требуемый размер операнда. Ее значением может быть слово BYTE (8-битный операнд), WORD (16-битный) или DWORD (32-битный).

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

mov dword [0x12345678],0 ; записывает 4 нулевых байта,

; начиная с адреса 0x12 345678

mov word [0x12345678],0 ; записывает 2 нулевых байта,

; начиная с адреса 0x12345678

mov byte [0x12345678],0 ; записывает 1 нулевой байт

; по адресу 0x12345 678

7.2. Операнды команд языка Ассемблера

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

Одни команды вообще не имеют никаких операндов, другие имеют один или два операнда. В качестве операнда можно указать непосредственное значение (например, 0x123), имя регистра или ссылку на ячейку памяти (так называемые косвенные операнды). Что же касается разрядности, имеются 32-разрядные, 16-разрядные, и 8-разрядные операнды. Почти каждая команда требует, чтобы операнды были одинакового размера (разрядности). Команда

имеет два операнда: операнд регистра и непосредственное значение, и оба они 16-битные.

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

В документации по Ассемблеру различные форматы операндов представлены следующими аббревиатурами:

reg8-операнд – любой 8-разрядный регистр общего назначения;

regl6-onepaнд – любой 16-разрядный регистр общего назначения;

reg32-onepaнд – любой 32-разрядный регистр общего назначения;

m – операнд может находиться в памяти;

imm8 – непосредственное 8-разрядное значение;

imml6 – непосредственное 16-разрядное значение;

imm32 – непосредственное 32-разрядное значение;

segreg – операнд должен быть сегментным регистром.

Допускаются неоднозначные типы операндов, например: reg8/imm8-onepaнд может быть любым 8-битным регистром общего назначения или любым 8-битным непосредственным значением.

Иногда размер операнда определяется только по последнему типу, например, следующая запись аналогична предыдущей: R/imm8-onepaнд может быть любым регистром (имеется в виду 8-битный регистр) общего назначения или 8-разрядным значением.

7.3. Способы адресации памяти языка Ассемблера

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

В языке Ассемблера используются непосредственная, прямая и косвенная адресации. При использовании косвенного операнда адрес в памяти, по которому находится нужное значение, записывается в квадратных скобках: [адрес]. Если мы используем указатель, то есть символическое представление адреса, например, [ESI], то в листинге машинного кода мы увидим, что указатель был заменен реальным значением адреса. Можно также указать точный адрес памяти, например, [0x594F].

Чаще всего мы будем адресовать память по значению адреса, занесенному в регистр процессора. Чтобы записать такой косвенный операнд, нужно просто написать имя регистра в квадратных скобках. Например, если адрес загружен в регистр ESI, вы можете получить данные, расположенные по этому адресу, используя выражение [ESI].

Рассмотрим ситуацию, когда регистр ESI содержит адрес первого элемента (нумерация начинается с 0) в массиве байтов. Как получить доступ, например, ко второму элементу (элементу, адрес которого на 1 байт больше) массива?

Процессор поддерживает сложные способы адресации, которые очень нам пригодятся в дальнейшем. В нашем случае, чтобы получить доступ ко второму элементу массива, нужно записать косвенный операнд [ESI + 1].

Имеются даже более сложные типы адресации: [адрес + ЕВХ + 4]. В этом случае процессор складывает адрес, значение 4 и значение, содержащееся в регистре ЕВХ. Результат этого выражения называется эффективным адресом (ЕА, Effective Address) и используется в качестве адреса, по которому фактически находится операнд.

При вычислении эффективного адреса процессор 80386+ также позволяет умножать один член выражения на константу, являющуюся степенью двойки: [адрес + ЕВХ * 4]. Корректным считается даже следующее «сумасшедшее» выражение: [число — 6 + ЕВХ * 8 + ESI].

На практике чаще всего довольствуются только одним регистром [ESI] или суммой регистра и константы, например, [ESI + 4]. В зависимости от режима процессора, мы можем использовать любой 16-разрядный или 32-разрядный регистр общего назначения [ЕАХ], [ЕВХ]. [ЕВР].

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

7.4. Псевдокоманды языка Ассемблера

Некоторые из команд могут работать с операндом, расположенным в памяти. Классический пример – команда MOV AX, [number], загружающая в регистр АХ значение из области памяти, адрес которой представлен символическим обозначением «number». Но пока не ясно, как связать символическое обозначение и реальный адрес в памяти. Как раз для этого и служат псевдокоманды.

Предложения языка ассемблера делятся на команды и псевдокоманды (директивы). Команды ассемблера – это символические имена машинных команд, обработка их компилятором приводит к генерации машинного кода. Псевдокоманды же управляют работой самого компилятора. На одной и той же аппаратной архитектуре могут работать различные ассемблеры: их команды обязательно будут одинаковыми, но псевдокоманды могут быть разными.

Псевдокоманды DB, DW и DD

Чаще всего используется псевдокоманда DB (define byte), позволяющая определить числовые константы и строки. Рассмотрим несколько примеров:

db 0x55 ; один байт в шестнадцатеричном виде

db 0x55,0x56,0x57 ; три последовательных байта: 0x55, 0х56, 0x57

db ‘а’,0x55 ; можно записать символ в одинарных кавычках

; получится последовательность 0x61, 0x55

db ‘Hello’,13,10,’$’ ; можно записать целую строку

; получится 0x48, 0x65, 0х6С, 0х6С,

; 0x6F, 0xD, 0xA, 0x24

Для определения порции данных размера, кратного слову, служит директива DW (define word):

dw 0x1234 ; 0х34, 0x12

dw ‘a’ ; 0x61, 0x00: второй байт заполняется нулями

Директива DD (define double word) задает значение порции данных размера, кратного двойному слову:

dd 0x12345678 ; 0х78 0x56 0x34 0x12

dd 1.234567e20 ; так определяются числа с плавающей точкой

А вот так определяется переменная, тот самый «number»:

number dd 0x1 ; переменная number инициализирована

Переменная «number» теперь представляет адрес в памяти, по которому записано значение 0x00000001 длиной в двойное слово.

Эта директива определяет константу, известную во время компиляции. В качестве значения константы можно указывать также константное выражение. Директиве EQU должно предшествовать символическое имя:

four EQU 4 ; тривиальный пример

Псевдокоманды RESB, RESW и RESD

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

Илон Маск рекомендует:  Что такое аккаунт Как зарегистрировать аккаунт и зачем он нужен

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

Для резервирования памяти служат три директивы: RESB (резервирует байт), RESW (резервирует слово) и RESD (резервирует двойное слово). Аргументом этих псевдокоманд является количество резервируемых позиций:

resb 1 ; резервирует 1 байт

resb 2 ; резервирует 2 байта

resw 2 ; резервирует 4 байта (2 слова)

resd 1 ; резервирует 4 байта

number resd 1 ; резервирует 4 байта для переменной «number»

buffer resb 64 ; резервирует 64 байта для переменной «buffer»

Директива TIMES – это псевдокоманда префиксного типа, то есть она используется только в паре с другой командой. Она повторяет последующую псевдокоманду указанное количество раз, подобно директиве DUP из ассемблера Borland TASM.

Применяется эта директива в тех случаях, когда нужно «забить» некоторую область памяти повторяющимся образцом. Следующий код определяет строку, состоящую из 64 повторяющихся «Hello»:

many_hello: times 64 db ‘Hello’

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

buffer db «Hello» ; определяем строку

times 32-($-buffer) db ‘ ‘ ; определяем нужное кол-тво пробелов

Выражение 32-($-buffer) возвратит значение 27, потому что $-buffer равно текущей позиции минус позиция начала строки, то есть 5.

Вместе с TIMES можно использовать не только псевдокоманды, но и команды процессора:

times 5 inc eax ; 5 раз выполнить INC EAX

Не нашли то, что искали? Воспользуйтесь поиском:

Лучшие изречения: Студент — человек, постоянно откладывающий неизбежность. 10532 — | 7319 — или читать все.

188.64.174.135 © studopedia.ru Не является автором материалов, которые размещены. Но предоставляет возможность бесплатного использования. Есть нарушение авторского права? Напишите нам | Обратная связь.

Отключите adBlock!
и обновите страницу (F5)

очень нужно

HackWare.ru

Этичный хакинг и тестирование на проникновение, информационная безопасность

Введение в Ассемблер [черновик]

[В ПРОЦЕССЕ НАПОЛНЕНИЯ И РЕДАКТИРОВАНИЯ]

Оглавление

Руководство по программированию на Ассемблер

1. Введение в Ассемблер

1.1 Для кого эти уроки по ассемблеру

Что нужно для изучения Ассемблера

Что такое язык Ассемблер?

Преимущества языка Ассемблер

2. Системы счисления

Основные характеристики аппаратной составляющей ПК

Двоичная система счисления

Шестнадцатеричная система счисления

Отрицательные двоичные числа

Адресация данных в памяти

3. Настройка рабочего окружения для Ассемблер

Настройка локального рабочего окружения

4. Основы синтаксиса Ассемблера

Компиляция и связывание (Linking) программы на Ассемблере в NASM

5. Ассемблер: сегменты памяти

6. Ассемблер: регистры (Registers)

7. Ассемблер: Системные вызовы

Системные вызовы Linux

8. Ассемблер: Режимы адресации

Адресации на регистр

Адресация на память

Прямая адресация со смещением

Косвенная адресация на память

9. Ассемблер: Переменные

Выделение пространства хранения для инициализированных данных

Выделение дискового пространства для неинициализированных данных

10. Ассемблер: Константы

11. Ассемблер: Арифметические инструкции

Инструкции ADD и SUB

12. Ассемблер: Логические инструкции

Инструкция AND (И)

13. Ассемблер: Условия

Данный материал — это азы языка программирования Ассемблер для абсолютных новичков. Здесь говориться о том, как написать программу на Ассемблере, приводятся основные команды Ассемблера, имеются примеры программа на Ассемблер и подробно описано как скомпилировать первую программу.

В целом материал является переводом «Assembly — Introduction» — небольшого учебника, в котором рассматриваются основы языка Ассемблер, но также имеются дополнения — некоторые вопросы рассмотрены более подробно.

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

Руководство по программированию на Ассемблер

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

Для кого эти уроки по ассемблеру

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

Что нужно для изучения Ассемблера

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

Что такое язык Ассемблер?

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

Каждая семья процессоров имеет свой собственный набор инструкций для обработки различных операций, таких как получения ввода с клавиатуры, отображение информации на экране и выполнения различных других работ. Этот набор инструкций называется «инструкции машинного языка» (‘machine language instructions’).

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

Преимущества языка Ассемблер

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

  • Как программы взаимодействуют с ОС, процессором и BIOS;
  • Как данные представлены в памяти и других внешних устройствах;
  • Как процессор обращается к инструкции и выполняет её;
  • Как инструкции получают доступ и обрабатывают данные;
  • Как программа обращается к внешним устройствам.

Другие преимущества использования ассемблера:

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

Системы счисления

Основные характеристики аппаратной составляющей ПК

Каждый компьютер содержит процессор и оперативную память. Процессор содержит регистры — компоненты, которые содержат данные и адреса. Для выполнения программы, система копирует её с устройства постоянного хранения во внутреннюю память. Процессор выполняет инструкции программы.

Фундаментальной единицей компьютерного хранилища является бит. Он может быть в состоянии Включён (1) или Выключен (0). Группа из девяти связанных битов составляет байт, из которых восемь бит используются для данных, а последний используется для контроля чётности. Согласно правилу чётности, количество битов, которые Включены (1) в каждом байте, всегда должно быть нечётным.

Таким образом, бит чётности используется для того, чтобы сделать количество битов в байте нечётным. Если соотношение является чётным, система предполагает, то что произошла ошибка соотношения (хотя и редко), которая могла быть вызвана неисправностью оборудования или электрическими помехами.

Процессор поддерживает следующие размеры данных —

  • Word: 2-байтовый элемент данных
  • Doubleword: a 4-байтовый (32 бита) элемент данных
  • Quadword: 8-байтовый (64 бита) элемент данных
  • Paragraph: 16-байтовая (128 бита) область
  • Kilobyte: 1024 байт
  • Megabyte: 1,048,576 байт

Двоичная система счисления

В каждой системе счисления используются позиционные обозначения, то есть каждая позиция, в которой записана цифра, имеет различное позиционное значение. Каждая позиция — это степень базы, которая равна 2 для двоичной системы счисления, и эти степени начинаются с 0 и увеличиваются на 1.

В следующей таблице приведены позиционные значения для 8-битного двоичного числа, где все биты установлены в положение ON (Включено).

Значение бита 1 1 1 1 1 1 1 1
Значение позиции как степень основания 2 128 64 32 16 8 4 2 1
Номер бита 7 6 5 4 3 2 1

Значение двоичного числа, как и в десятичном, зависит от составляющих его цифр и расположения этих цифр. Но в двоичном числе используются только цифры 1 и 0, и расположение цифр имеет другое значение степени. Первая цифра, как и в десятичном числе, может означать 0 или 1. Вторая цифра (смотрим число справа на лево) может означать 2 (если этот бит установлен на 1) или 0 (если бит установлен на 0). Третья цифра (смотрим число справа на лево) может означать 4 (если этот бит установлен на 1) или 0 (если бит установлен на 0). И так далее. В десятичном числе значение каждого символа нужно умножить на 10 в степени порядкового номера этой цифры за минусом единицы.

То есть число 1337 это 1 * 10 3 + 3 * 10 2 + 3 * 10 1 + 7 * 10 0 = 1337

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

Допустим число 110101 и мы хотим узнать, сколько это будет в десятичной системе счисления, для этого достаточно выполнить следующее преобразование:

1 * 2 5 * + 1 * 2 4 + 0 * 2 3 + 1 * 2 2 + 0 * 2 1 + 1 * 2 0 = 1 * 32 + 1 * 16 + 0 * 8 + 1 * 4 + 0 * 2 + 1 * 1 = 53

Итак, значение бинарного числа основывается на наличии битов 1 и их позиционном значении. Поэтому значение числа 11111111 в двоичной системе является:

1 + 2 + 4 + 8 +16 + 32 + 64 + 128 = 255

Кстати, это то же самое, что и 2 8 — 1.

Шестнадцатеричная система счисления

Шестнадцатеричная система счисления использует основание 16. Цифры в этой системе варьируются от 0 до 15. По соглашению, буквы от A до F используются для представления шестнадцатеричных цифр, соответствующих десятичным значениям с 10 по 15.

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

Десятичное число Двоичный вид Шестнадцатеричный вид
1 1 1
2 10 2
3 11 3
4 100 4
5 101 5
6 110 6
7 111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

Чтобы преобразовать двоичное число в его шестнадцатеричный эквивалент, разбейте его на группы по 4 последовательные группы в каждой, начиная справа, и запишите эти группы в соответствующие цифры шестнадцатеричного числа.

Пример — двоичное число 1000 1100 1101 0001 эквивалентно шестнадцатеричному — 8CD1

Чтобы преобразовать шестнадцатеричное число в двоичное, просто запишите каждую шестнадцатеричную цифру в её 4-значный двоичный эквивалент.

Пример — шестнадцатеричное число FAD8 эквивалентно двоичному — 1111 1010 1101 1000

Отрицательные двоичные числа

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

Удобством для арифметических действий в процессоре обусловлено то, как записываются отрицательные двоичные числа. Вы должны помнить из курса информатики, что в одном байте содержится 8 бит. Но старший бит используется для установки знака. Чтобы правильно прочесть число, а также правильно поменять его знак, нужно выполнять следующие правила:

Во-первых, нужно помнить, что если старшие биты (крайние слева), равны нулю, то их иногда не записывают. Например, восьмибитное число 10 (в десятичной системе счисления оно равно 2), также можно записать как 0000 0010. Обе эти записи означают число 2.

Если старший бит равен нулю, то это положительное число. Например, возьмём число 110. В десятичной системе счисления это 6. Данное число является положительным или отрицательным? На самом деле, однозначно на этот вопрос можно ответить только зная разрядность числа. Если это восьмиразрядное число, то его полная запись будет такой: 0000 0110. Как можно увидеть, старший бит равен нулю, следовательно, это положительное число.

Для трёхбитовых чисел было бы справедливо следующее:

Двоичное значение трёхбитового числа со знаком
(в представлении Дополнительный код)

Десятичное значение
000
1 001
2 010
3 011
-4 100
-3 101
-2 110
-1 111

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

Отрицательные двоичные числа записываются без знака минус и для получения этого же числа со знаком минус (то есть для получения числа в Дополненном коде) нужно выполнить два действия:

  1. нужно переписать его полную форму с противоположным значением битов (то есть для единиц записываются нули, а для нулей записываются единицы)
  2. и затем добавить к этому числу 1.
Число 53 00110101
Замена битов на противоположные 11001010
Добавляем 1 0000000 1
Число -53 11001011

На русском языке такая форма записи называется Дополнительный код, в англоязычной литературе это называется Two’s complement.

Примеры восьмибитного двоичного числа в Дополнительном коде (старший бит указывает на знак):

Десятичное значение Двоичное значение трёхбитового числа со знаком
(в представлении Дополнительный код)
0000 0000
1 0000 0001
2 0000 0010
126 0111 1110
127 0111 1111
−128 1000 0000
−127 1000 0001
−126 1000 0010
−2 1111 1110
−1 1111 1111

Двоичное представление (8 бит)

(в виде Дополнительного кода)

Десятичное
представление
127 0111 1111
1 0000 0001
0000 0000
-0
-1 1111 1111
-2 1111 1110
-3 1111 1101
-4 1111 1100
-5 1111 1011
-6 1111 1010
-7 1111 1001
-8 1111 1000
-9 1111 0111
-10 1111 0110
-11 1111 0101
-127 1000 0001
-128 1000 0000

Числа в дополненном коде удобно применять для вычитания — это будет показано далее.

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

Рассмотрим пример с числом -5. Запись отрицательного восьмибитного числа:

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

Добавив к результату 1 получим положительное число 5 в прямом коде:

И проверим, сложив с дополнительным кодом

0000 0101 + 1111 1011 = 1 0000 0000, десятый разряд выбрасывается, то есть получается 0000 0000, то есть 0. Следовательно, преобразование выполнено правильно, так как 5 + (-5) = 0.

Двоичная арифметика

Следующая таблица иллюстрирует четыре простых правила для двоичного сложения:

(i) (ii) (iii) (iv)
1
1 1 1
+0 +0 +1 +1
=0 =1 =10 =11

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

Десятичные Двоичные
60 00111100
+42 00101010
102 01100110

Рассмотрим, как делается вычитание.

Для вычитания число, которое вычитается, записывается в форме Дополнительного кода, а затем эти два числа складываются.

Пример: Вычесть 42 из 53

Число 53 00110101
Число 42 00101010
Инвертируем биты 42 11010101
Добавляем 1 00000001
Число -42 11010110
Выполняем операцию: 53 — 42 = 11 00110101 + 11010110 = 100001011, то есть = 00001011

Бит который вызывает переполнение — крайней левый, девятый по счёту, просто отбрасывается.

Адресация данных в памяти

Процесс, посредством которого процессор управляет выполнением инструкций, называется циклом fetch-decode-execute (выборки-декодирования-выполнения) или циклом выполнения (execution cycle). Он состоит из трёх непрерывных шагов —

  • Извлечение инструкции из памяти
  • Расшифровка или идентификация инструкции
  • Выполнение инструкции

Процессор может одновременно обращаться к одному или нескольким байтам памяти. Давайте рассмотрим шестнадцатеричное число 0725H (буква H означает, что перед нами шестнадцатеричное число). Для этого числа потребуется два байта памяти. Байт старшего разряда или старший значащий байт — 07, а младший байт — 25.

Процессор хранит данные в последовательности обратного байта, то есть байт младшего разряда хранится в низком адресе памяти и байт старшего разряда в старшем адресе памяти. Таким образом, если процессор переносит значение 0725H из регистра в память, он сначала перенесёт 25 на нижний адрес памяти и 07 на следующий адрес памяти.

Когда процессор получает числовые данные из памяти для регистрации, он снова переворачивает байты. Есть два вида адресов памяти:

  • Абсолютный адрес — прямая ссылка на конкретное место.
  • Адрес сегмента (или смещение) — начальный адрес сегмента памяти со значением смещения.

Настройка рабочего окружения для Ассемблер

Настройка локального рабочего окружения

Язык ассемблера зависит от набора команд и архитектуры процессора. В этом руководстве мы сосредоточимся на процессорах Intel-32, таких как Pentium. Чтобы следовать этому уроку, вам понадобится:

  • ПК IBM или любой другой совместимый компьютер
  • Копия операционной системы Linux
  • Копия программы ассемблера NASM

Есть много хороших ассемблерных программ, таких как:

  • Microsoft Assembler (MASM)
  • Borland Turbo Assembler (TASM)
  • GNU ассемблер (GAS)

Мы будем использовать ассемблер NASM, так как он:

  • Бесплатный
  • Хорошо задокументированный — вы получите много информации в сети.
  • Может использоваться как в Linux, так и в Windows.

Установка NASM

Если вы выбираете «Инструменты разработки» при установке Linux, вы можете установить NASM вместе с операционной системой Linux, и вам не нужно загружать и устанавливать его отдельно. Чтобы проверить, установлен ли у вас NASM, сделайте следующее:

Откройте терминал Linux.

и нажмите клавишу ВВОД.

Если он уже установлен, появляется строка типа

В противном случае вы увидите просто

значит вам нужно установить NASM.

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

Например, для установки в Debian, Ubuntu, Linux Mint, Kali Linux и их производные выполните:

Для установки в Arch Linux, BlackArch и их производные выполните:

Чтобы установить NASM из исходного кода, сделайте следующее:

Проверьте веб-сайт ассемблера (NASM) на последнюю версию.

Загрузите исходный архив Linux nasm-X.XX.ta.gz, где X.XX — номер версии NASM в архиве.

Распакуйте архив в каталог, который создаст подкаталог nasm-X.XX.

Перейдите к nasm-X.XX

Этот скрипт оболочки найдёт лучший компилятор C для использования и сделает настройки в соответствии с Makefiles.

чтобы создать двоичные файлы nasm и ndisasm.

чтобы установить nasm и ndisasm в /usr/local/bin и установить справочные страницы (man).

Это должно установить NASM в вашей системе. Кроме того, вы можете использовать RPM-дистрибутив для Fedora Linux. Эта версия проще в установке, просто дважды щёлкните файл RPM.

Основы синтаксиса Ассемблера

Программу на языке Ассемблер можно разделить на три раздела:

  • Раздел data
  • Раздел bss
  • Раздел text

Раздел data

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

Синтаксис объявления раздела data:

Раздел BSS

Секция bss используется для объявления переменных. Синтаксис объявления раздела bss:

Раздел text

Раздел text используется для хранения самого кода. Этот раздел должен начинаться с объявления global _start, которое сообщает ядру, где начинается выполнение программы.

Синтаксис объявления раздела text:

Комментарии

Комментарий на ассемблере начинается с точки с запятой (;). Он может содержать любой печатный символ, включая пробел. Он может появиться в строке сам по себе, например:

или в той же строке вместе с инструкцией, например:

Операторы Ассемблера

Программы на ассемблере состоят из трёх типов операторов:

  • Исполняемые инструкции или инструкции,
  • Директивы ассемблера или псевдооперации (pseudo-ops), и
  • Макросы.


Исполняемые инструкции или просто инструкции говорят процессору, что делать. Каждая инструкция состоит из кода операции (opcode). Каждая исполняемая инструкция генерирует одну инструкцию на машинном языке.

Директивы ассемблера или псевдооперации говорят ассемблеру о различных аспектах процесса сборки. Они не являются исполняемыми и не генерируют инструкции машинного языка.

Макросы — это в основном механизм подстановки текста.

Синтаксис операторов ассемблера

Операторы языка ассемблера вводятся по одной инструкции в каждой строке. Каждое утверждение имеет следующий формат:

Поля в квадратных скобках являются необязательными. Основная инструкция состоит из двух частей: первая — это имя инструкции (или мнемоника), которая должна быть выполнена, а вторая — операнды или параметры команды.

Ниже приведены некоторые примеры типичных операторов языка ассемблера.

Программа Hello World на Ассамблее

Следующий код на ассемблере выводит на экран строку «Hello World»:

Когда приведённый выше код скомпилирован и выполнен, он даст следующий результат:

Компиляция и связывание (Linking) программы на Ассемблере в NASM

Убедитесь, что вы установили путь до исполнимых файлов nasm и ld в вашей переменной окружения PATH (в Linux это уже сделано). Теперь пройдите следующие шаги для компиляции и связывания приведённой выше программы:

  • Наберите приведённый выше код используя текстовый редактор и сохраните как hello.asm.
  • Убедитесь, что вы в той же самой директории, где вы сохранили hello.asm.
  • Для сборки вашей программы выполните:
  • Если в коде присутствуют какие-либо ошибки, то на этом этапе вам будет выведено сообщение о них. Если ошибок нет, то будет создан объектный файл вашей программы с именем hello.o.
  • Для связывания объектного файла и создания исполнимого файла с именем hello выполните:

Выполните программу набрав:

Если вы всё сделали правильно, то она отобразит на экране ‘Hello, world!’.

Возможно, вас интересует, что такое связывание (Linking) и зачем оно требуется после сборки программы. Если коротко, то на этом этапе объектные файлы (если их несколько) собираются в один исполнимый файл, также благодаря этому процессу исполнимый файл теперь может использовать библиотеки. Линкеру указывается (обычно) целевой исполнимый формат. Если совсем коротко — это просто нужно. Я не буду в этом базовом курсе по ассемблеру останавливаться на этом более подробно — если вас интересует эта тема, то вы всегда сможете найти по ней дополнительную информацию в Интернете.

Ассемблер: сегменты памяти

Мы уже рассмотрели три раздела программы на ассемблере. Эти разделы также представляют различные сегменты памяти.

Обратите внимание, что если вы замените ключевое слово section на слово segment, вы получите тот же самый результат. Попробуйте этот код:

После компиляции и выполнения вышеприведённого кода он даст следующий результат:

Сегменты памяти

Модель сегментированной памяти делит системную память на группы независимых сегментов, на которые ссылаются указатели, расположенные в регистрах сегментов. Каждый сегмент используется для хранения данных определённого типа. Один сегмент используется для хранения кодов команд, другой — для хранения элементов данных, а третий — для программного стека.

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

  • Сегмент Data. Он представлен разделом .data и .bss. Раздел .data используется для объявления области памяти, где хранятся элементы данных для программы. Этот раздел не может быть расширен после объявления элементов данных, и он остаётся статическим во всей программе.
    Раздел .bss также является разделом статической памяти, который содержит буферы для данных, которые будут объявлены позже в программе. Эта буферная память заполнена нулями.
  • Сегмент Code. Он представлен разделом .text. Он определяет область в памяти, в которой хранятся коды команд. Это также фиксированная зона.
  • Stack — этот сегмент содержит значения данных, передаваемые функциям и процедурам в программе.

Ассемблер: регистры (Registers)

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

Для ускорения работы процессора процессор включает в себя несколько мест хранения внутренней памяти, называемых регистрами (registers).

Регистры хранят элементы данных для обработки без необходимости доступа к памяти. Ограниченное количество регистров встроено в чип процессора.

Регистры процессора

В архитектуре IA-32 имеется десять 32-разрядных и шесть 16-разрядных процессорных регистров. Регистры сгруппированы в три категории:

  • Общие регистры,
  • Регистры управления и
  • Сегментные регистры.

Общие регистры далее делятся на следующие группы:

  • Регистры данных,
  • Регистры указателя и
  • Индексные регистры.

Регистры данных

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

  • Как полные 32-битные регистры данных: EAX, EBX, ECX, EDX.
  • Нижние половины 32-битных регистров могут использоваться как четыре 16-битных регистра данных: AX, BX, CX и DX.
  • Нижняя и верхняя половины вышеупомянутых четырёх 16-битных регистров могут использоваться как восемь 8-битных регистров данных: AH, AL, BH, BL, CH, CL, DH и DL.

Некоторые из этих регистров данных имеют конкретное применение в арифметических операциях.

AX — основной аккумулятор; он используется во вводе/выводе и большинстве арифметических инструкций. Например, в операции умножения один операнд сохраняется в регистре EAX или AX или AL в соответствии с размером операнда.

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

CX известен как регистр подсчёта, так как регистры ECX, CX хранят счётчик циклов в итерационных операциях.

DX известен как регистр данных. Он также используется в операциях ввода/вывода. Он также используется с регистром AX вместе с DX для операций умножения и деления с большими значениями.

Регистры указателя

Регистры указателя являются 32-разрядными регистрами EIP, ESP и EBP и соответствующими 16-разрядными правыми частями IP, SP и BP. Есть три категории регистров указателей:

  • Указатель инструкций (IP) — 16-битный регистр IP хранит адрес смещения следующей команды, которая должна быть выполнена. IP вместе с регистром CS (как CS:IP) даёт полный адрес текущей инструкции в сегменте кода.
  • Указатель стека (SP) — 16-разрядный регистр SP обеспечивает значение смещения в программном стеке. SP в сочетании с регистром SS (SS:SP) относится к текущей позиции данных или адреса в программном стеке.
  • Базовый указатель (BP) — 16-битный регистр BP в основном помогает ссылаться на переменные параметра, передаваемые подпрограмме. Адрес в регистре SS объединяется со смещением в BP, чтобы получить местоположение параметра. BP также можно комбинировать с DI и SI в качестве базового регистра для специальной адресации.

Индексные регистры

32-разрядные индексные регистры ESI и EDI и их 16-разрядные крайние правые части. SI и DI, используются для индексированной адресации и иногда используются для сложения и вычитания. Есть два набора указателей индекса:

  • Исходный индекс (SI) — используется в качестве исходного индекса для строковых операций.
  • Указатель назначения (DI) — используется как указатель назначения для строковых операций.

Регистры управления

Регистр указателя 32-битной инструкции и регистр 32-битных флагов рассматриваются как регистры управления.

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

Популярные биты флага:

  • Флаг переполнения (OF) — указывает на переполнение старшего бита (крайнего левого бита) данных после арифметической операции со знаком.
  • Флаг направления (DF) — определяет направление влево или вправо для перемещения или сравнения строковых данных. Когда значение DF равно 0, строковая операция принимает направление слева направо, а когда значение равно 1, строковая операция принимает направление справа налево.
  • Флаг прерывания (IF) — определяет, будут ли игнорироваться или обрабатываться внешние прерывания, такие как ввод с клавиатуры и т. д. Он отключает внешнее прерывание, когда значение равно 0, и разрешает прерывания, когда установлено значение 1.
  • Trap Flag (TF) — позволяет настроить работу процессора в одношаговом режиме. Программа DEBUG, которую мы использовали, устанавливает флаг прерывания, чтобы мы могли пошагово пройтись по инструкциям — по одной инструкции за раз.
  • Флаг знака (SF) — показывает знак результата арифметической операции. Этот флаг устанавливается в соответствии со знаком элемента данных после арифметической операции. Знак указывается старшим левым битом. Положительный результат очищает значение SF до 0, а отрицательный результат устанавливает его в 1.
  • Нулевой флаг (ZF) — указывает результат арифметической операции или операции сравнения. Ненулевой результат очищает нулевой флаг до 0, а нулевой результат устанавливает его в 1.
  • Вспомогательный флаг переноса (AF) — содержит перенос с бита 3 на бит 4 после арифметической операции; используется для специализированной арифметики. AF устанавливается, когда 1-байтовая арифметическая операция вызывает перенос из бита 3 в бит 4.
  • Флаг чётности (PF) — указывает общее количество 1-битов в результате, полученном в результате арифметической операции. Чётное число 1-бит очищает флаг чётности до 0, а нечётное число 1-битов устанавливает флаг чётности в 1.
  • Флаг переноса (CF) — содержит перенос 0 или 1 из старшего бита (крайнего слева) после арифметической операции. Он также хранит содержимое последнего бита операции shift или rotate.

В следующей таблице указано положение битов флага в 16-битном регистре флагов:

Флаг: O D I T S Z A P C
Номер бита: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1

Сегментные регистры

Сегменты — это специальные области, определённые в программе для хранения данных, кода и стека. Есть три основных сегмента:

  • Сегмент Code — содержит все инструкции, которые должны быть выполнены. 16-битный регистр сегмента кода или регистр CS хранит начальный адрес сегмента кода.
  • Сегмент Data — содержит данные, константы и рабочие области. 16-битный регистр сегмента данных или регистр DS хранит начальный адрес сегмента данных.
  • Сегмент Stack — содержит данные и адреса возврата процедур или подпрограмм. Он реализован в виде структуры данных стека. Регистр сегмента стека или регистр SS хранит начальный адрес стека.

Помимо регистров DS, CS и SS существуют и другие регистры дополнительных сегментов — ES (дополнительный сегмент), FS и GS, которые предоставляют дополнительные сегменты для хранения данных.

При программировании на ассемблере программе необходим доступ к ячейкам памяти. Все области памяти в сегменте относятся к начальному адресу сегмента. Сегмент начинается с адреса, равномерно делимого на 16 или в шестнадцатеричном виде числа 10. Таким образом, крайняя правая шестнадцатеричная цифра во всех таких адресах памяти равна 0, что обычно не сохраняется в регистрах сегментов.

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

Пример

Посмотрите на следующую простую программу, чтобы понять использование регистров в программировании на Ассемблере. Эта программа отображает 9 звёзд на экране вместе с простым сообщением.

После компиляции и выполнения эта программа выведет:

Ассемблер: Системные вызовы

Системные вызовы — это API для интерфейса между пространством пользователя и пространством ядра. Мы уже использовали системные вызовы sys_write и sys_exit для записи на экран и выхода из программы соответственно.

Системные вызовы Linux

Вы можете использовать системные вызовы Linux в ваших ассемблерных программах. Для использования системных вызовов Linux в вашей программе необходимо выполнить следующие шаги:

  • Поместите номер системного вызова в регистр EAX.
  • Сохраните аргументы системного вызова в регистрах EBX, ECX и т. д.
  • Вызовите соответствующее прерывание (80h).
  • Результат обычно возвращается в регистр EAX.

Существует шесть регистров, в которых хранятся аргументы используемого системного вызова. Это EBX, ECX, EDX, ESI, EDI и EBP. Эти регистры принимают последовательные аргументы, начиная с регистра EBX. Если существует более шести аргументов, ячейка памяти первого аргумента сохраняется в регистре EBX.

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

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

Все системные вызовы перечислены в /usr/include/asm/unistd.h вместе с их номерами (значение, которое нужно указать в EAX перед вызовом int 80h). Точнее говоря, сейчас это файлы /usr/include/asm/unistd_32.h и /usr/include/asm/unistd_64.h.

Чтобы посмотреть содержимое файла /usr/include/asm/unistd_32.h:

Начало этого файла:

Чтобы получить справку по системным вызовам:

Чтобы получить справку по конкретному вызову, укажите вначале man 2, а затем название вызова. Например, чтобы узнать о вызове read:

Чтобы узнать о вызове mkdir:

В следующей таблице приведены некоторые системные вызовы, используемые в этом руководстве:

%eax Имя %ebx %ecx %edx %esx %edi
1 sys_exit int (целое число)
2 sys_fork struct pt_regs
3 sys_read unsigned int (целое беззнаковое число) char * size_t
4 sys_write unsigned int (целое беззнаковое число) const char * size_t
5 sys_open const char * int (целое число) int (целое число)
6 sys_close unsigned int (целое беззнаковое число)

Пример

Следующий пример читает число с клавиатуры и отображает его на экране:

Скомпилированный и запущенный вышеприведённый код даёт следующий результат:

Ассемблер: Режимы адресации

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

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

Три основных режима адресации:

  • Адресации на регистр
  • Немедленная адресация
  • Адресация на память

Адресации на регистр

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

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

Немедленная адресация

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

Адресация на память

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

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

При прямой адресации в памяти один из операндов ссылается на ячейку памяти, а другой операнд ссылается на регистр.

Прямая адресация со смещением

Этот режим адресации использует арифметические операторы для изменения адреса. Например, посмотрите на следующие определения, которые определяют таблицы данных:

Следующие операции обращаются к данным из таблиц в памяти в регистрах:

Косвенная адресация на память

В этом режиме адресации используется способность компьютера Segment:Offset (Сегмент:Смещение). Обычно для этой цели используются базовые регистры EBX, EBP (или BX, BP) и регистры индекса (DI, SI), закодированные в квадратных скобках для ссылок на память.

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

В следующем фрагменте кода показано, как получить доступ к различным элементам переменной.

Инструкция MOV

Мы уже задействовали инструкцию MOV, которая используется для перемещения данных из одного пространства хранения в другое. Инструкция MOV принимает два операнда.

Синтаксис

Синтаксис инструкции MOV:

Инструкция MOV может иметь одну из следующих пяти форм:

Пожалуйста, обратите внимание, что:

  • Оба операнда в операции MOV должны быть одинакового размера
  • Значение исходного операнда остаётся неизменным

Инструкция MOV порой вызывает двусмысленность. Например, посмотрите на утверждения:

Не ясно, хотите ли вы переместить байтовый эквивалент или словесный эквивалент числа 110. В таких случаях целесообразно использовать спецификатор типа (type specifier).

В следующей таблице приведены некоторые общие спецификаторы типов:

Спецификатор типа Байты
BYTE 1
WORD 2
DWORD 4
QWORD 8
TBYTE 10

Пример

Следующая программа иллюстрирует некоторые из концепций, обсуждённых выше. Он сохраняет имя «Zara Ali» в разделе данных памяти, затем программно меняет его значение на другое имя «Nuha Ali» и отображает оба имени.

Когда приведённый выше код скомпилирован и выполнен, он даёт следующий результат:

Ассемблер: Переменные

NASM предоставляет различные директивы определения (define directives) для резервирования места для хранения переменных. Директива определения ассемблера используется для выделения пространства хранения. Его можно использовать для резервирования, а также для инициализации одного или нескольких байтов.

Выделение пространства хранения для инициализированных данных

Синтаксис для оператора распределения памяти для инициализированных данных:

Где имя-переменной — это идентификатор для каждого пространства хранения. Ассемблер связывает значение смещения для каждого имени переменной, определённого в сегменте данных.

Существует пять основных форм директивы определения:

Директива Цель Размер хранения
DB Определить Byte выделяет 1 байт
DW Определить Word выделяет 2 байта
DD Определить Doubleword выделяет 4 байта
DQ Определить Quadword выделяет 8 байта
DT Определить Ten Bytes выделяет 10 байта

Ниже приведены некоторые примеры использования директив определения.

Пожалуйста, обратите внимание, что:

  • Каждый байт символа хранится как его значение ASCII в шестнадцатеричном формате.
  • Каждое десятичное значение автоматически преобразуется в его 16-разрядный двоичный эквивалент и сохраняется в виде шестнадцатеричного числа.
  • Процессор использует little-endian порядок байтов.
  • Отрицательные числа преобразуются в его представление Дополнительный код (рассмотрен выше).
  • Короткие и длинные числа с плавающей запятой представлены с использованием 32 или 64 бит соответственно.

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

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

Выделение дискового пространства для неинициализированных данных

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

Существует пять основных форм директив резервирования:

Директива Цель
RESB Зарезервировать Byte
RESW Зарезервировать Word
RESD Зарезервировать Doubleword
RESQ Зарезервировать Quadword
REST Зарезервировать 10 байт

Множественность определений

Вы можете иметь несколько операторов определения данных в программе. Например:

Ассемблер выделяет непрерывную память для нескольких определений переменных.

Множественность инициализаций

Директива TIMES позволяет выполнить несколько инициализаций к одному и тому же значению. Например, массив с именем marks размера 9 может быть определён и инициализирован на начальное значение ноль с помощью следующего оператора:

Директива TIMES полезна при определении массивов и таблиц. Следующая программа отображает 9 звёздочек на экране:

Результат выполнения скомпилированной программы:

Ассемблер: Константы

NASM предоставляет несколько директив, определяющих константы. Мы уже использовали директиву EQU в предыдущих разделах. Особое внимание мы уделим трём директивам:

  • EQU
  • %assign
  • %define

Директива EQU

Директива EQU используется для определения констант. Синтаксис директивы EQU следующий:

Затем вы можете использовать это постоянное значение в вашем коде, например:

Операндом оператора EQU может быть выражение:

Приведённый фрагмент кода определит AREA как 200.

Пример

Следующий пример иллюстрирует использование директивы EQU:

Скомпилированный и выполненный код даст следующие результаты:

Кстати, в коде программы мы использовали 0xA,0xD в качестве части строк. Точнее говоря, в качестве окончания строк. Как можно догадаться, это шестнадцатеричные цифры. При выводе на экран эти шестнадцатеричные цифры трактуются как коды символов ASCII. То есть, чтобы понять их значение, нужно заглянуть в таблицу ASCII символов, например в статье «ASCII и шестнадцатеричное представление строк. Побитовые операции со строками».

Там мы можем найти, что 0xA (в той таблице он обозначен как 0A) и означает он перевод строки. Во многих языках программирования символ обозначается как «\n». Нажатие на клавишу ↵ Enter при выводе текста переводит строку.

Что касается 0xD (там в таблице он обозначен как 0D) и означает enter / carriage return — возврат каретки. Во многих языках программирования — символ «CR» обозначается как «\r».

Итак, если вы программируете на каком либо языке, то последовательность из двух шестнадцатеричных чисел 0xA,0xD, соответствует последовательности «\n\r», то есть, упрощённо говоря, это универсальный способ (чтобы срабатывал и в Linux, и в Windows) перейти на новую строку.

Директива %assign

По аналогии с директивой EQU, директива %assign может использоваться для определения числовых констант. Эта директива допускает переопределение. Например, вы можете определить постоянную TOTAL следующим образом:

Позже в коде вы можете переопределить её так:

Эта директива чувствительна к регистру.

Директива %define

Директива %define позволяет определять как числовые, так и строковые константы. Эта директива похожа на #define в C. Например, вы можете определить постоянную PTR так:

Приведённый выше код заменяет PTR на [EBP+4].

Эта директива также допускает переопределение и учитывает регистр.

Ассемблер: Арифметические инструкции

Инструкция INC

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

Инструкция INC имеет следующий синтаксис:

Операндом может быть 8-битный, 16-битный или 32-битный операнд.

Инструкция DEC

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

Инструкция DEC имеет следующий синтаксис:

Операндом может быть 8-битный, 16-битный или 32-битный операнд.

Инструкции ADD и SUB

Команды ADD и SUB используются для выполнения простого сложения/вычитания двоичных данных размером в byte, word и doubleword, т.е. для сложения или вычитания 8-битных, 16-битных или 32-битных операндов соответственно.

Инструкции ADD и SUB имеют следующий синтаксис:

Инструкция ADD/SUB может выполняться между:

  • Регистр к регистру
  • Память к регистру
  • Регистр к памяти
  • Регистр к константе
  • Память к константе

Однако, как и другие инструкции, операции с память-в-память невозможны с использованием инструкций ADD/SUB. Операция ADD или SUB устанавливает или очищает флаги переполнения (overflow) и переноса (carry).

Пример

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

Скомпилированный и выполненный код даст следующие результаты:

Код программы сильно упрощается, если прописать значения переменных для арифметических действий прямо в самом коде:

Результат выполнения этого кода:

Инструкции MUL/IMUL

Есть две инструкции для умножения двоичных данных. Инструкция MUL (Multiply) обрабатывает беззнаковые данные, а IMUL (Integer Multiply) обрабатывает данные со знаком. Обе инструкции влияют на флаг переноса и переполнения.

Синтаксис для инструкций MUL/IMUL следующий:

Множимое в обоих случаях будет в аккумуляторе, в зависимости от размера множимоего и умножителя, и результат умножения также сохраняется в двух регистрах в зависимости от размера операндов. Следующий раздел объясняет инструкции MUL в трёх разных случаях:

Когда перемножаются два байта

Множимое находится в регистре AL, а множитель — это байт в памяти или в другом регистре. Результат произведения находится в AX. Старшие 8 битов произведения хранятся в AH, а младшие 8 битов хранятся в AL.

Когда умножаются два значения word

Множимое должно быть в регистре AX, а множитель — это word в памяти или в другом регистре. Например, для такой инструкции, как MUL DX, вы должны сохранить множитель в DX и множимое в AX.

В результате получается двойное word, для которого понадобятся два регистра. Часть высшего порядка (крайняя слева) сохраняется в DX, а часть нижнего порядка (крайняя справа) сохраняется в AX.

Когда умножаются два значения doubleword

Когда умножаются два значения doubleword, множимое должно быть в EAX, а множитель — это значение doubleword, хранящееся в памяти или в другом регистре. Результат умножения сохраняется в регистрах EDX:EAX, то есть 32-разрядные старшие разряды сохраняются в регистре EDX, а 32-разрядные младшие разряды сохраняются в регистре EAX.

В следующем примере 3 умножается на 2 и отображается результат:

Результат выполнения программы:

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

Инструкция DIV (Divide) используется для данных без знака, а IDIV (Integer Divide) используется для данных со знаком.

Формат для инструкции DIV/IDIV:

Делимое находится в аккумуляторе. Обе инструкции могут работать с 8-битными, 16-битными или 32-битными операндами. Операция влияет на все шесть флагов состояния. Следующий раздел объясняет три случая деления с различным размером операнда:

Номер Сценарии
1

Когда делитель равен 1 байту

Предполагается, что делимое находится в регистре AX (16 бит). После деления частное переходит в регистр AL, а остаток — в регистр AH.

Когда делителем является 1 word

Предполагается, что делимое имеют длину 32 бита и оно размещено в регистрах DX:AX. Старшие 16 битов находятся в DX, а младшие 16 битов — в AX. После деления 16-битное частное попадает в регистр AX, а 16-битное значение попадает в регистр DX.

Когда делитель doubleword

Предполагается, что размер делимого составляет 64 бита и оно размещено в регистрах EDX:EAX. Старшие 32 бита находятся в EDX, а младшие 32 бита находятся в EAX. После деления 32-битное частное попадает в регистр EAX, а 32-битный остаток попадает в регистр EDX.

В следующем примере 8 делится на 2. Делимое 8 сохраняется в 16-битном регистре AX, а делитель 2 сохраняется в 8-битном регистре BL.

Результат выполнения приведённого выше кода:

Ассемблер: Логические инструкции

Набор команд процессора содержит инструкции логики AND, OR, XOR, TEST и NOT, которые проверяют, устанавливают и очищают биты в соответствии с потребностями программы.

Формат для этих инструкций:

Номер Сценарии
1
Номер Инструкция Формат
1 AND AND операнд1, операнд2
2 OR OR операнд1, операнд2
3 XOR XOR операнд1, операнд2
4 TEST TEST операнд1, операнд2
5 NOT NOT операнд1

Первый операнд во всех случаях может быть либо в регистре, либо в памяти. Второй операнд может быть либо в регистре/памяти, либо в непосредственном (постоянном) значении. Однако операции память-и-память невозможны. Эти инструкции сравнивают или сопоставляют биты операндов и устанавливают флаги CF, OF, PF, SF и ZF.

Инструкция AND (И)

Инструкция AND используется для поддержки логических выражений путём выполнения побитовой операции AND. Побитовая операция AND возвращает 1, если совпадающие биты обоих операндов равны 1, в противном случае она возвращает 0. Например:

Операция AND может использоваться для очистки одного или нескольких битов. Например, допустим, регистр BL содержит 0011 1010. Если вам нужно очистить старшие биты до нуля, то вы выполняете операцию AND этого регистра с 0FH.

Давайте рассмотрим другой пример. Если вы хотите проверить, является ли данное число нечётным или чётным, простой тест будет проверять младший значащий бит числа. Если это 1, число нечётное, иначе число чётное.

Предполагая, что номер находится в регистре AL, мы можем написать:

Следующая программа иллюстрирует это.

Результат выполнения кода:

Измените значение в регистре ax на нечётную цифру, к примеру:

Программа будет отображать:

Точно так же очистить весь регистр вы можете сделав AND с 00H.

Инструкция OR

Инструкция OR (ИЛИ) используется для выполнения логической побитовой операции OR. Побитовый оператор OR возвращает 1, если совпадающие биты одного или обоих операндов равны единице. Возвращает 0, если оба бита равны нулю.

Операция OR может использоваться для установки одного или нескольких битов. Например, предположим, что регистр AL содержит 0011 1010, вам нужно установить на единицы четыре младших бита, тогда вы можете сделать OR со значением 0000 1111, т.е.

В следующем примере демонстрируется инструкция OR. Давайте сохраним значения 5 и 3 в регистрах AL и BL, соответственно, затем

затем в регистре AL в результате выполнения операции OR получится 7

Результат работы программы:

Инструкция XOR

Инструкция XOR реализует побитовую операцию XOR. Операция XOR устанавливает результирующий бит в 1, если и только если биты из операндов отличаются. Если биты из операндов одинаковы (оба 0 или оба 1), результирующий бит сбрасывается в 0.

XOR операнд числа с самим собой меняет операнд на 0. Это используется для очистки регистра.

Инструкция TEST

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

Инструкция NOT

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

Ассемблер: Условия

Выполнение в зависимости от выполнения условия на ассемблере реализовано несколькими инструкциями зацикливания и ветвления. Эти инструкции могут изменить поток управления в программе. Условное исполнение рассматривается в двух сценариях:

Безусловный прыжок

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

Условный переход

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

Давайте обсудим инструкцию CMP, прежде чем обсуждать условные инструкции.

Инструкция CMP

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

CMP сравнивает два числовых поля данных. Операнд-адресат может быть либо в регистре, либо в памяти. Исходным операндом могут быть постоянные (непосредственные) данные, регистр или память.

CMP часто используется для сравнения того, достигло ли значение счётчика количества раз, которое цикл должен быть выполнен. Рассмотрим следующее типичное условие:

Безусловный переход

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

Инструкция JMP предоставляет имя метки, куда поток управления передаётся немедленно. Синтаксис инструкции JMP:

Следующий фрагмент кода иллюстрирует инструкцию JMP:

Условный переход

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

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

Инструкции условия
1
Инструкция Описание Тестируемые флаги
JE/JZ Jump Equal or Jump Zero (равно или ноль) ZF
JNE/JNZ Jump not Equal or Jump Not Zero (не равно или не ноль) ZF
JG/JNLE Jump Greater or Jump Not Less/Equal (больше или не меньше/равно) OF, SF, ZF
JGE/JNL Jump Greater/Equal or Jump Not Less (больше/равно или не меньше) OF, SF
JL/JNGE Jump Less or Jump Not Greater/Equal (меньше или не больше/равно) OF, SF
JLE/JNG Jump Less/Equal or Jump Not Greater (меньше/равно или не больше) OF, SF, ZF

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

Инструкция Описание Тестируемые флаги
JE/JZ Jump Equal или Jump Zero (равно или ноль) ZF
JNE/JNZ Jump not Equal или Jump Not Zero (не равно или не ноль) ZF
JA/JNBE Jump Above или Jump Not Below/Equal (больше или не меньше/равно) CF, ZF
JAE/JNB Jump Above/Equal или Jump Not Below (больше/равно или не меньше) CF
JB/JNAE Jump Below или Jump Not Above/Equal (меньше или не больше/равно) CF
JBE/JNA Jump Below/Equal или Jump Not Above (меньше/равно или не больше) AF, CF

Следующие инструкции условного перехода имеют специальное использование и проверяют значение флагов:

Инструкция Описание Тестируемый флаг
JXCZ Переход если CX равен нулю нет
JC Переход если Перенос CF
JNC Переход если нет Переноса CF
JO Переход если переполнение OF
JNO Переход если нет переполнения OF
JP/JPE Переход при наличии чётности PF
JNP/JPO Переход при отсутствии чётности PF
JS Переход при наличии знака (отрицательная величина) SF
JNS Переход при отсутствии знака (положительная величина) SF

Синтаксис для набора инструкций J :

Пример

Следующая программа отображает наибольшую из трёх переменных. Переменные являются двузначными переменными. Три переменные num1, num2 и num3 имеют значения 47, 22 и 31 соответственно:

Основы ассемблера. Оглавление. Общие сведения о языке ассемблера

    Алёна Раевская 2 лет назад Просмотров:

1 Основы ассемблера Оглавление Общие сведения о языке ассемблера. 1 Команды ассемблера. 2 Команда MOV. 2 Формат хранения данных в памяти. 3 Команды SUB и ADD. 4 Умножение и деление. 4 Логические операции. 5 Сдвиги. 6 Метки, данные, переходы. 7 Данные. 7 Метки. 8 Переходы Циклы Работа с флагами процессора Работа с памятью Работа со стеком Работа с битами Общие сведения о языке ассемблера Когда вы пишете программу на ассемблере, вы фактически пишете команды процессору. Команды процессору — это просто коды (или коды операций, или опкоды). Из-за этого ассемблер считается самым низкоуровневым языком программирования: все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня — ассемблер (программа) переводит команды, понимаемые человеком, непосредственно в шестнадцатеричные коды. В программах на ассемблере каждая команда должна находиться на одной строке, т. е. одна строка — одна команда. Комментарии в ваших программах оставляются после точки с запятой; все символы строки, которые находятся после точки с запятой, считаются комментарием. Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того чтобы показать, в какой системе использовано число, надо поставить после него букву. Для бинарной (двоичной) системы пишется буква b (пример: b, b), для десятеричной системы можно ничего не указывать после числа или добавить в конце букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо добавить в конце букву h, запись шестнадцатеричного числа начинается с нуля, за которым следуют буквы A F (примеры: правильно 0AC45h; неправильно F145Ch,

2 C123h). Также шестнадцатеричные числа можно записывать в форматах языков высокого уровня: с начальным знаком $, как в pascal, или сочетанием 0х, как в языке С. Символы значения могут быть заключены как в кавычки, так и в апострофы. Идентификаторы в программе не могут содержать символов +-*/=<>() [] <>:, &-#’. Команды ассемблера Команда MOV Самая первая команда — это команда пересылки данных MOV. Эта команда пересылает данные, а именно байт, слово, двойное слово, четверное слово из источника в назначение. Данная команда является эталоном для всех команд, принимающих два операнда: все остальные команды, работающие с двумя операндами, имеют такой же синтаксис. Команда MOV — самая распространённая, и в большинстве программ до 40% команд являются командами MOV. Синтаксис команды MOV: mov , . Фактически команда присваивает регистру или памяти некоторое значение, которое можно взять из памяти, регистра или непосредственного значения (просто цифра). Если назначением является регистр, то источником может быть память, регистр или непосредственно значение; если назначение — память, то источником может быть только регистр или непосредственное значение (перемещение из памяти в память недопустимо). Размерность источника и назначения должны быть равными. mov edx, ebx mov rax, r10 mov eax, r12d mov ah, dh mov cx, r14w mov ecx, h mov ax, 12h mov al, 890h ; неправильно mov dx, rbx ; неправильно Для работы с памятью надо указать адрес и заключить его в квадратные скобки. Адрес должен быть совместим с текущим режимом работы процессора: например, в режиме реальных адресов необходимо указывать 16-битный адрес, а в 64-битном режиме — 64-разрядный адрес. mov еах, dword [ Ah] mov rbx, qword [00A056F1h] mov dword [0F81562Dh], r9d mov qword [0F8156F6h], 0DF899564FFh Для того чтобы указать, какой размер данных необходимо пересылать, надо указать перед адресом оператор размера. Оператор размера Размер в байтах Byte 1 Word 2 Dword 4 Fword 6 Pword 6 Qword 8 Tbyte 10 Tword 10

3 dqword 16 Наиболее часто используемые операторы размера — byte, word, dword, qword. В защищённом режиме вместе с командой МОV не получится использовать 8-байтовый операнд, а в режиме реальных адресов не получится использовать 4-байтовый операнд (4-байтовые регистры использовать можно). Вместо квадратных скобок можно указать директиву PTR перед адресом. mov еах, dword ptr Ah mov rbx, qword ptr 00A056F1h mov qword ptr 0F81562Dh, r9d mov qword ptr 0F8156F6h, 09564FFh Перемещение из памяти в память недопустимо. Иногда размер можно не указывать: например, когда мы переносим значение из памяти в регистр, размер регистра уже показывает размерность операнда. Также можно использовать регистр для адресации памяти. mov eax, [eax] mov rbx, ptr ebx mov [rax], r9d mov ptr edx, 09564FFh Свободно работать можно только с регистрами общего назначения, а работа с остальными регистрами возможна только вкупе с регистрами общего назначения (отсюда и их название). Т. е. перемещать, присваивать и получать значения можно только через регистры общего назначения. Все остальные регистры (не считая регистров математического сопроцессора) будем называть привилегированными регистрами, потому в защищённом режиме доступ к ним можно получить только из привилегированного участка кода. Фактически с привилегированными регистрами может работать только эта команда. Есть также набор специфических для каждого типа машины регистров — регистров MSR. К ним нельзя обратиться с помощью команды MOV, для этого предназначены команды RDMSR (чтение), WRMSR (запись). Фактически команды перемещения из регистра в регистр и из регистра в память и т. д. — это разные команды, т. е. опкоды у них разные, но они выполняют идентичные действия и поэтому объединены под одним названием. Большинство команд ассемблера не обозначают только один опкод, а объединяют несколько идентичных. Формат хранения данных в памяти Рассмотрим пример использования команды MOV. Предположим, у нас есть область памяти: адрес A 27B 27C 27D 27E 27F данные 0A A 5E 72 EF 7D FF AD C7 Теперь, допустим, есть команда mov eax, dword [ Ah] После выполнения этой команды регистр ЕАХ будет содержать значение 725E7A25h. Возможно, вы заметили, что это инверсия того, что находится в памяти: 25 7А 5Е 72. Это происходит потому, что значения сохраняются в памяти, используя формат little endian. Суть в том, что самый младший байт сохраняется в наиболее значимом (т. е. с меньшим адресом) байте: указывается обратный порядок байтов. Числа в процессоре могут быть только целые: знаковые и беззнаковые. Вещественные числа всё равно представляются в двоичном виде как 32-, 64-, 80-битные значения. Оперировать вещественными

4 значениями как таковыми может только математический сопроцессор. Работа с вещественными числами требует отдельного рассмотрения. Беззнаковые числа представляются обычным преобразованием числа в двоичное; следовательно, диапазон значений беззнаковых чисел: 0. 2П — 1, где п — это разрядность числа. Старший бит знаковых чисел обозначает знак числа: если старший бит — единица, то число отрицательное. Все остальные биты — это модуль числа. Диапазон знакового числа: — 2 n n-1-1. Принципы сложения и вычитания целых, знаковых и беззнаковых чисел одинаковые; отличия возникают при умножении и делении чисел. Команды SUB и ADD Команда ADD, как уже понятно из названия, складывает оба операнда и сохраняет полученный результат в операнде назначения. Синтаксис команды ADD: add , . Формат операндов идентичен формату операндов команды MOV. Оба операнда складываются, и результат сохраняется в назначении. После операции сложения в зависимости от результата изменяются соответствующие биты в регистре флагов. Можно догадаться, что команда SUB производит вычитание — а если быть точнее, то из операнда назначения она вычитает операнд источника и сохраняет его в операнде назначения. Синтаксис команды SUB полностью идентичен команде ADD. Если после сложения (вычитания) двух операндов результат не помещается в операнде назначения (число слишком большое и отрицательное), происходит переполнение и флаг OF устанавливается в 1. Если произошёл перенос или заём из старшего бита (например, из бита 17 или 33), то устанавливается флаг переноса. Обычно эти два флага всегда сопутствуют друг другу, т. е. если устанавливается один, то устанавливается и второй. В случае вычитания может и не установиться флаг переполнения. Также есть ещё две команды увеличения и уменьшения операнда — это INC и DEC. Эти команды увеличивают и уменьшают операнд на единицу соответственно. Они принимают только один операнд, в качестве которого может выступать регистр или значение памяти. Размер операнда может быть любым (1, 2, 4, 8 байт). В качестве параметров всех вышеописанных команд могут выступать как знаковые, так и беззнаковые числа. Умножение и деление Команда MUL — беззнаковое умножение. Синтаксис команды: mul . Команда MUL умножает операнд с регистром EAX(AL, АХ, RAX) и сохраняет его в регистрах EDX:EAX (АХ, DX:AX, RDX:RAX). Операндом может быть регистр или значение в памяти. В табл. 1.7 представлены возможные результаты работы этой команды. Таблица 1.7. Зависимость результата работы команды MUL от размера операнда Размер операнда Результат 1 байт АХ = AL * 2 байта DX:AX = АХ * 4 байта EDX:EAX = ЕАХ * 8 байт RDX: RAX = RAX * Команда DIV — беззнаковое деление. Синтаксис команды: div . Команда DIV делит регистры EDX:EAX (АХ, DX:AX, RDX:RAX) на операнд и сохраняет результат деления в ЕАХ (AL, АХ, RAX), а также остаток от деления в EDX (АН, DX, RDX). В табл. 1.8 показаны возможные результаты работы этой команды.

5 Таблица 1.8. Зависимость результата работы команды DIV от размера операнда Размер операнда Источник Результат деления Остаток деления 1 байт АХ AL АН 2 байта DX:AX AX DX 4 байта EDX: EAX EAX EDX 8 байт RDX: RAX RAX RDX Команда IMUL — знаковое умножение. Команда может принимать до трёх операндов. У этой команды есть три формы записи в зависимости от того, сколько операндов указано. 1. Первая форма — только один операнд. Команда работает точно так же, как и MUL, только умножение происходит с учётом знака. 2. Вторая форма — два операнда. Синтаксис: imul , . В качестве первого операнда может выступать только регистр общего назначения, а вторым операндом может быть регистр, значение памяти или непосредственно значение. Операнды могут быть любого размера, но если второй операнд — непосредственно значение, то оно не может быть размером 8 байт. Первый операнд умножается с учётом знака на второй, и результат сохраняется в первом операнде. 3. Третья форма — это три операнда. Формат: imul , , . В качестве первого операнда может выступать только регистр общего назначения. В качестве второго операнда может выступать регистр или значение памяти. Первый и второй операнды должны быть одинакового размера. Третий операнд может быть только непосредственным значением и не может быть размером 8 байт. При использовании этой формы команда умножает с учётом знака второй операнд на третий и результат сохраняет в первом. Команда IDIV — знаковое деление. Команда IDIV по результату работы полностью идентична команде DIV. Отличие команды IDIV от DIV заключается в том, что если операнды имеют разные знаковые биты, то результат (частное и остаток) будет отрицательным; если же у операндов знаковые биты будут равны, то результат (частное и остаток) будет положительным. Команды DIV и IDIV не влияют на регистр флагов. Команда МUL устанавливает флаги OF и CF в ноль, если старшая часть результата (АН, DX, EDX, RDX) равна нулю; в любом другом случае эти биты устанавливаются в единицу. При использовании команды IMUL в первой форме флаги OF и CF устанавливаются в единицу, если какиелибо значащие биты переносятся в старшую часть результата (АН, DX, EDX, RDX), и в ноль, если в старшую часть результата переносов не было. Таким образом, если флаги OF и CF сброшены, то результат может быть считан только из младшей части, и он будет верным. При использовании команды IMUL во второй и третьей форме флаги OF и CF устанавливаются в единицу, если размерность результата больше, чем размер операнда, указанного в качестве результата. Таким образом, если после операции умножения выставлены флаги OF и CF, значит, произведение в результирующем операнде неверное (усечённое). Логические операции Есть несколько базовых логических операций: операция «или» (команда OR), логическое «и» (команда AND), логическое отрицание (команда NOT), «исключающее или» (команда XOR).

6 Команда OR — операция битового логического ИЛИ. Синтаксис команды: or , . Эта команда производит побитовое логическое сложение между переданными операндами и сохраняет результат в назначении. Операнды могут быть размером 1,2,4, 8 байт. При логическом сложении результат является единицей (истиной) при истинности любого из операндов. Команда AND — операция битового логического И. Синтаксис команды: and , . Эта команда производит побитовое логическое умножение переданных операндов и сохраняет результат в назначении. Операнды могут быть размером 1, 2, 4, 8 байт. При логическом умножении результат является единицей (истиной) при истинности обоих операндов. Команда NOT — операция битового логического отрицания. Синтаксис команды: not . Эта команда производит побитовое логическое отрицание операнда. При логическом отрицании результат является отрицанием операнда, т. е. если 1, то получается 0, если 0 — то получается 1. Операнд может быть размером 1,2,4,8 байт. Команда XOR — операция битового исключающего ИЛИ. Синтаксис команды: хоr , . Команда XOR производит операцию побитового исключающего «или» над переданными операндами и сохраняет результат в назначении. Операнды могут быть размером 1, 2,4,8 байт. Если операнды одинаковые, то результат 0, если разные — то 1. Таким образом, если указать в качестве обоих операндов один и тот же регистр, то регистр будет обнулён. Этот приём бывает очень полезным при оптимизации кода. Все четыре вышеперечисленные логические операции в зависимости от результата операции изменяют соответствующие биты в регистре флагов. Сдвиги Сдвиг — это побитовый сдвиг операнда вправо или влево. Например, сдвиг числа на 3 в результате даст Все команды сдвига имеют одинаковый синтаксис: команда , . Сдвиги бывают разных видов: циклические, арифметические и логические. Начнём с логических. SHL — логический сдвиг влево. Команда производит сдвиг операнда влево на указанное количество бит. В освободившиеся справа биты заносятся нули. Значение CF совпадает со значением бита, который последним был вытеснен за левый край операнда. Если количество битов не равно 1, то признак переполнения OF не определен. Если же количество битов равно 1, тогда OF = 0, при том что 2 старших бита исходного значения операнда назначения совпадали; иначе OF = 1. SHR — логический сдвиг вправо. Команда производит сдвиг операнда вправо на указанное количество битов. В освободившиеся справа биты заносятся нули. Значение CF совпадает со значением бита, который последним был вытеснен за правый край операнда. Если бит знака сохраняет свое значение, то признак переполнения OF = 0, иначе OF = 1. Значение CF совпадает со значением бита, который последним был вытеснен за правый край операнда. Если количество битов не равно 1, то признак переполнения OF не определен. Если же количество равно 1, то флаг OF равен значению старшего бита исходного операнда. SAL — арифметический сдвиг влево. Команда полностью идентична команде SHL. Это одна и та же команда, только имена разные. SAR — арифметический сдвиг вправо. Команда идентична команде SHR, за исключением того, что каждый вновь вставленный слева бит равен самому старшему биту изначального операнда. Таким образом, при арифметическом сдвиге вправо операнд не изменит свой знак. Например, сдвиг числа на 3 бита вправо в результате даст

7 ROL — циклический сдвиг влево. Команда сдвигает операнд влево на указанное количество битов. Бит, который выходит за левый предел, вставляется справа. Значение CF совпадает со значением бита, который последним был вытеснен за левый край операнда. Если количество битов не равно 1, то признак переполнения OF не определен. Если же количество битов равно 1, то во флаг OF заносится результат выполнения операции исключающего «или», примененной к 2 старшим битам исходного значения операнда. ROR — циклический сдвиг вправо. Команда сдвигает операнд вправо на указанное количество битов. Бит, который выходит за левый предел, вставляется слева. Значение CF совпадает со значением бита, который последним был вытеснен за правый край операнда. Если количество битов не равно 1, то признак переполнения OF не определен. Если же количество битов равно 1, то в OF заносится результат выполнения операции исключающего «или», примененной к 2 старшим битам результата. RCL — циклический сдвиг влево через флаг CF. Команда сдвигает операнд влево на указанное количество битов. Бит, который выходит за левый край, заносится во флаг CF, а старое значение CF заносится в освободившийся правый бит. Если количество битов не равно 1, то признак переполнения OF не определен. Если же количество битов равно 1, то в OF заносится результат выполнения операции исключающего «или», применённой к 2 старшим битам результата. RCR — циклический сдвиг вправо через флаг CF. Команда сдвигает операнд вправо на указанное количество битов. Бит, который выходит за правый край, заносится во флаг CF, а старое значение CF заносится в освободившийся левый бит. Если количество битов не равно 1, то признак переполнения OF не определен. Если же количество битов равно 1, то в OF заносится результат выполнения операции исключающего «или», применённой к 2 старшим битам результата. У всех команд операнд назначения может быть размером 1,2,4,8 байт. Метки, данные, переходы Условные и безусловные переходы являются важнейшей частью любой программы; они позволяют писать программы с ветвящимися и циклическими алгоритмами. Данные Данные в программах на ассемблере объявляются (или резервируются) с помощью директив данных. Директивы определения данных и их размеры. Размер (байты) Определение данных 1 DB 2 DW, DU 4 DD 6 DF, DP 8 DQ 10 DT За директивой описания данных должно следовать одно или несколько числовых значений, разделенных запятыми. Эти выражения определяют значения для простейших элементов данных, размер которых зависит от того, какая директива используется. Вместо числового значения может стоять символ; впоследствии он будет интерпретирован как числовой код символа (символов). db 67h db 5dh, 0f6h db «z» ; то же самое, что и db 7ah db «w», «k», «y» dw 8a34h, 0c51h, 8bh

8 du 9e3ah, 07deh dw «WE» ; то же самое, что и db 57h, 45h dd 01F243D5Eh dd «WEGa» ; то же самое, что и db 57h, 45h, 47h, 61h dq h За директивами db и du могут следовать строки неопределённого размера; в результате каждый символ будет интерпретирован как его числовое значение. Отличие директивы du от db заключается в том, что каждый символ интерпретируется двумя байтами, старший из которых заполняется нулём, например: db «assembler» то же самое, что и db 61h, 73h, 73h, 65h, 6Dh, 62h, 6Ch, 65h, 72h du «assembler» то же самое, что и db 0, 61h,0, 73h,0, 73h,0, 65h,0, 6Dh,0, 62h,0, 6Ch,0, 65h,0, 72h Для описания большей последовательности одинаковых данных предназначена директива dup. Она используется после директивы определения данных. Перед ней должно стоять число повторений, а после неё (в скобках) — значение или цепь значений для повторения. db 7 dup (1Ah) db 6 dup (45h, 0A3h, 90h) dd 13 dup (0A713E445h) dd 9 dup (0A713E445h, 0F8D3E412h) dw 5 dup (?) Если после директивы определения данных идёт вопросительный знак, то начальное значение этих данных будет не определено. Данные, помеченные вопросительным знаком, называются неинициализированными. Неинициализированные данные не включаются в исполняемый файл и будут доступны лишь после загрузки программы в память (разумеется, если это поддерживается форматом исполняемых файлов, в который будет компилироваться программа). Неинициализированные данные позволяют сократить размер исполняемого файла, т. к. в большинстве случаев не важно какое начальное значение имеет переменная. Метки Метка — идентификатор, который используется в программе и возвращает адрес памяти, по которому она находится. Метка — это главная форма взаимодействия с данными в памяти. Если метка используется в программе, то фактически на месте метки будет стоять адрес, на который она указывает. Существуют разные способы определения меток. Простейший из них — двоеточие после названия метки. За этой директивой на той же строке может следовать инструкция или директива. Она определяет метку, значение которой равно смещению, т. е. адресу точки, в которой она определена. Вернее, смещению следующей директивы или инструкции. Этот метод обычно используется, чтобы пометить места в коде, но ничто не мешает вам пометить этим методом данные. Данные обычно помечаются другим методом — это написание названия метки перед директивой объявления данных. Метка возвращает адрес данных, перед которыми она объявлена. Компилятор запоминает размер данных, на которые указывает эта метка, и при использовании с ней операндов,

9 несовместимых по размеру, уведомляет об этом. При использовании этого метода объявления меток отпадает необходимость использования операторов размера (word, dword, qword и т. д.). Третий метод объявления меток — самый гибкий: это использование директивы label. После этой директивы должно следовать имя метки, потом (опционально) размер оператора, далее (тоже опционально) оператор at и числовое выражение, определяющее адрес, на который данная метка должна ссылаться. В качестве адреса может использоваться другая метка, ведь метка это то же самое что и адрес, на который она указывает. metka1: dd. data1 db. dw. mov al, [data1] metka2: mov ebx, eax mov ebx, dword [metka1] mov [data1], 4567AADDh mov cx, word [metka2] Последняя инструкция — это пример того, что в ассемблере нет разницы между кодом и данными. Назначение последней инструкции: в регистр сх помещается опкод инструкции mov ebx, eax, в регистре сх будет обычное число (код), с помощью которого кодируется инструкция mov ebx, eax. Метка, имя которой начинается с точки, обрабатывается как локальная, и её имя прикрепляется к имени последней глобальной метки (с названием, начинающемся с чего угодно, кроме точки) для создания полного имени этой метки. Так, вы можете использовать короткое имя (начинающееся с точки) где угодно перед следующей глобальной меткой, а в других местах вам придется пользоваться полным именем. Метки, начинающиеся с двух точек, — исключения. Они имеют свойства глобальных, но не создают новый префикс для локальных меток. globmetka1:.locmetka1 dd. locmetka2: mov [.locmetka1], eax mov word [.locmetka2], cx mov word [.locmetka3], cx ; ошибка! mov word [globmetka2.locmetka2], globmetka2:. locmetka3. mov [..globmetka3.locmetka4], cx ; ошибка! mov [globmetka3.locmetka4], cx ; ошибка. globmetka3:. locmetka4: dw. mov [.locmetka4], cx Метка $ обозначает текущее смещение или смещение текущей команды; таким образом, чтобы бесконечно зациклить выполнение программы, достаточно написать в программе jmp $, т. е. это будет ее безусловный «прыжок на саму себя». Метки бывают ближние и дальние. Дальность или близость метки определяется только тем, в каком месте она используется. Одна и та же метка, используемая в двух разных командах, в одной команде может быть ближней, а в другой — дальней. Ближняя метка та, которая определена на далее чем 127 байт после и не далее чем на 128 байт до команды, где она используется. Все метки, которые находятся за этими

10 пределами, являются дальними. Дальность или близость меток имеет значение только с командами передачи управления. Переходы Переход — это передача управления другой команде. Фактически переход осуществляется после выполнения каждой команды. В регистре EIP (IP, RIP) находится адрес команды, которая выполнится следующей. После выполнения команды процессор выполняет команду, находящуюся в памяти, на которую указывает EIP (IP, RIP). Этот регистр доступен только для чтения и изменить его нельзя — он изменяется самим процессором. Но иногда надо выполнить не следующую команду, а команду, которая находится, скажем, через 20 команд. Для этого есть команды переходов. Переходы бывают безусловные и условные. Есть три основные команды безусловной передачи управления: JMP, CALL и RET. Фактически команда JM P изменяет регистр EIP (IP, RIP) на значение, которое было указано в качестве операнда. Операндом может быть непосредственно значение в памяти, регистр, содержащий адрес, или непосредственно значение адреса. Если привести эквивалент команды jmp , то он будет такой: mov eip, Команда CALL производит переход с сохранением в стеке адреса следующей команды, для того чтобы функция (или процедура), на которую производится переход, могла вернуться назад для дальнейшего выполнения вызвавшего её кода. У этой команды формат такой же, что и у команды JMР. Эквивалент этой команды CALL: push eip jmp Команда RET берёт из верхушки стека адрес возврата и переходит по нему. Она не принимает никаких параметров. Эквивалент команды RET: pop eip. Также существует команда RETN, которая принимает один операнд. Операнд задаёт количество байтов, которое необходимо «вытолкнуть» из стека перед возвращением из процедуры. Эквивалент команды RETN n следующий: pop temp sub esp, n mov eip, temp Для дальнего возврата, т. е. возврата, когда произошла межсегментная передача управления, предназначена команда RETF. Инструкция RETF также может принимать параметр, подобно инструкции RETN. Эквивалент инструкции RETF n следующий: pop temp_eip pop temp_cs sub esp, n mov cs, temp_cs mov eip, temp_eip Если передача (возврат) управления осуществляется на другой уровень привилегий, то происходит переключение стека.

11 Эквиваленты команд, которые были приведены выше, никогда не сработают, потому что регистр изменить нельзя; примеры были приведены нами только для лучшего понимания предмета. EIP Команды JMP и CALL могут принимать в качестве операнда как значение памяти, так и непосредственный адрес. Чаще всего в качестве операнда выступает непосредственно адрес (т. е. метка). В таком случае компилятор анализирует близость адреса и, если он близкий, то генерирует короткий опкод инструкции, в противном случае — длинный опкод инструкции. Т. е. короткий вариант инструкции может генерироваться только тогда, когда операнд не заключён в скобки (или не указано ptr) и не является регистром. Короткий вариант инструкции используется, когда происходит переход не далее чем на 127 байт вперёд и 128 байт назад. Команды условного перехода передают управление, только если выполнено условие. Таких команд много, и каждая передаёт управление в зависимости от значения некоторого флага в регистре флагов. Например, команда JZ передаёт управление другому адресу, только если выставлен флаг ZF. Команды условного перехода принимают в качестве параметра ближнюю метку, т. е. могут передать управление не далее чем на 127 байт вперёд и 128 байт назад. Команды условного перехода чаще всего используются вместе с инструкциями сравнения. Чаще всего используемая инструкция сравнения — это команда СМР. Она сравнивает операнды и изменяет регистр флагов. Формат команды: cmp , . В качестве первого операнда может выступать регистр или значение памяти любого размера (1, 2,4,8 байт). В качестве второго операнда может выступать регистр, значение памяти или непосредственное значение. Значение не может быть 64-битным. Одновременно двух значений памяти быть не может. Если 32-битное значение сравнивается с 64-битным, то оно расширяется нулями. В таблице приведены команды условных переходов (условия указаны после выполнения команды cmp x, y). Команда Условие Условие JA X > Y CF = 0 & ZF = 0 JAE X >= Y CF = 0 JB X Y CF = 0 & SF = 0 JGE X >= Y SF = OF JL X = Y CF = 0 JNBE X > Y CF = 1 & ZF = 0 JNC CF = 0 JNE X!= Y ZF = 0 JNG X = Y SF = OF JNLE X > Y ZF = 0 & SF = OF JNO OF = 0 JNP PF = 0 JNS SF = 0

12 JNZ X!= Y ZF = 0 JO OF = 1 JP PF = 1 JPE PF = 1 JPO PF = 0 JS SF = 1 JZ Z = 1 Иногда имеет смысл использовать команду TEST. Формат этой команды почти такой же, как и у команды СМР, но только в качестве второго операнда не может выступать значение памяти. Эта команда осуществляет операцию «логического И» и изменяет только флаги SF, ZF, PF. Команда TEST полезна для проверки соответствия значения операнда некоторой битовой маске. Циклы Иногда одно и тоже действие нужно выполнить несколько раз; для этого счётчик повторений можно поместить в некоторый регистр и при каждом повторении уменьшать на единицу этот регистр. Если он равен нулю, то повторять действие не нужно. mov есх, metkal: стело цикла> dec есх jnz metkal Последние две строки — это код, который организует цикл. Команда DEC уменьшает значение регистра ЕСХ, и если оно станет равно нулю, то во флаг ZF будет помещена единица; в итоге, если регистр не будет равен нулю, то произойдёт переход на метку. Но есть более простой метод организации цикла. Команда LOOP принимает в качестве единственного операнда ближнюю метку. Она уменьшает регистр ЕСХ (СХ, RCX) на единицу и, если этот регистр не равен нулю, то происходит передача управления метке. Единственное преимущество этой команды состоит лишь в том, что она занимает меньше места. Также есть две команды организации циклов: LOOPE (LOOPZ) и LOOPNE (LOOPNZ). Команда LOOPE перед передачей управления проверяет флаг ZF и, если он не выставлен, то передачи управления не происходит. Обратное действие производит команда LOOPNE: если флаг ZF выставлен, то передачи управления не происходит. На современных процессорах организация цикла с помощью команд DEC/ JNZ/JZ предпочтительна с точки зрения производительности; в сумме они будут работать в два раза быстрее, нежели одна инструкция LOOP. Работа с флагами процессора Состояние некоторых наиболее часто используемых битов в регистре флагов можно изменить, используя специально отведённые для этого команды. Ниже перечислены команды, работающие с флагами процессора: 1. CLC — сброс флага переноса (CF=0). 2. CLD — сброс флага направления (DF=0). 3. СLI — сброс флага разрешения прерываний (IF=0). 4. LAHF — сохранение в регистре АН содержимого первого байта регистра флагов (флаги SF, ZF, AF, PF, CF)

13 5. SAHF — сохранение регистра АН в первый байт регистра флагов (флаги S F, ZF, AF, PF, CF); биты с зарезервированными значениями игнорируются. 6. STC — установка флага переноса в единицу (CF=1). 7. STD — установка флага направления в единицу (DF=1). 8. STI — разрешения прерываний (IF= 1). Все команды, кроме CLI и STI, являются непривилегированными: вы не всегда можете свободно использовать их в своих программах. Работа с памятью Команда XCHG — обмен значений операндов. Синтаксис команды: xchg , . В качестве операндов может выступать регистр или значение в памяти. Разумеется, одновременно два значения памяти менять нельзя. Операнды могут быть любого размера. Команда ADC — сложение с учётом флага переноса. Синтаксис команды полностью идентичен синтаксису команды ADD. Отличие команды ADC от команды ADD заключается только в том, что команда ADC после выполнения сложения прибавляет к результату значение флага CF, т. е. если этот флаг выставлен, то происходит инкремент результата. Команда LEA — загрузка эффективного адреса. Синтаксис команды: lea , . Команда загружает адрес переменной в регистр общего назначения. В целом без этой команды можно обойтись, но в некоторых случаях она бывает очень полезна. С помощью этой команды можно произвести некоторые вычисления, которые обычно делаются в два этапа; например, после выполнения этой команды lea еах, [ebx + ebx * 4] в ЕАХ мы получим значение из ЕВХ, умноженное на 5. Наиболее полезной команда LEA будет в следующем случае: lea есх, [edx + 4 ] ; после выполнения этой команды в регистр ЕСХ будет помещено значение EDX, увеличенное на 4 — таким образом, мы заменяем одной командой LEA пару команд MOV/ADD. Работа со стеком На верхушку стека указывает регистр ESP (SP, RSP). Есть две команды, которые работают со стеком: PUSH и POP. Команда PUSH «заталкивает» в стек операнд; если операнд меньше размера элемента стека, то он дополняется нулями. Синтаксис команд РUSН/РОР: push/pop . Операнд может иметь размер 2,4,8 байт в зависимости от режима; также он может быть сегментным регистром. У команды POP операнд не может быть непосредственно значением. Команда PUSH присваивает памяти, на которую указывает ESP (SP, RSP), значение операнда и уменьшает значение этого регистра на 2,4 или 8 в зависимости от режима. Команда POP работает аналогично, только наоборот. Для пояснения принципа работы команд PUSH/POP, приведем их эквиваленты. Эквивалент команды PUSH: sub esp, 4 mov [esp], Эквивалент команды POP: mov 14 Команда PUSНF — сохранение регистра флагов в стеке. Эта команда сохраняет первые 2 байта регистра флагов в стеке. В защищённом режиме эта команда сохраняет 4 байта регистра флагов в стеке, и она уже называется PUSНFD, но опкод такой же. Соответственно команда PUSHFQ сохраняет 8 байт регистра флагов в стеке. Команда POPF — «выталкивание» из стека в регистр флагов. Эта команда «выталкивает» из стека значение и заносит его в регистр флагов. Соответственно работают команды POPFD и POPFQ. Последние две команды созданы для изменения флагов процессора. Зарезервированные флаги изменить нельзя; если программа выполняется на непривилегированном уровне защиты, то привилегированные флаги изменить ей не удастся. Команда PUSHА — сохранение в стеке регистров общего назначения. Эта команда сохраняет 16-битные регистры общего назначения в стеке. В 64-разрядном режиме команда PUSНА не поддерживается. В защищённом режиме сохраняются 32-битные регистры общего назначения. В защищённом режиме эта команда называется PUSHAD (опкод тот же). Порядок сохранения регистров: ЕАХ, ЕСХ, EDX, EBX, ESP, EBP, ESI, EDI (в режиме реальных адресов сохраняются их младшие части). Регистр ESP (SP) сохраняется в том состоянии, в котором он был до выполнения команды, а не в том состоянии, в котором он был после помещения в стек регистра ЕВХ. Команда РОРА — забор из стека в регистров общего назначения. Эта команда забирает из стека регистры общего назначения; 4 позиция в стеке игнорируется, т. к. там находится регистр ESP (SP). В защищённом режиме эта команда называется POPAD (опкод тот же). Команда NOP — пустая: она не принимает параметров и абсолютно ничего не делает. Она только занимает пространство и время. Используется для резервирования места в сегменте кода или организации программной задержки Работа с битами Иногда в программе возникает ситуация, когда нужно проверить содержимое некоторого бита либо изменить его состояние. Для этих целей существуют четыре команды работы с битами: ВТ, BTC, BTR, BTS. Все четыре команды имеют следующий формат: bt* , . В качестве операнда может выступать регистр либо переменная в памяти любого размера. Номер бита можно указать как непосредственным значением, так и любым регистром общего назначения. Номера битов нумеруются с нуля. Команда ВТ заносит значение указанного бита во флаг CF в регистре флагов. Команда ВТС заносит значение указанного бита во флаг CF и производит его инвертирование в операнде. Команда BTR заносит значение указанного бита во флаг CF и обнуляет его в операнде. Команда BTS заносит значение указанного бита во флаг CF и заносит в указанный бит в операнде единицу. Как видно, все указанные команды заносят значение указанного бита во флаг CF, после чего можно использовать команды условного перехода JC и JNC. mov ах, 4 ;ах = 100Ь bt ах, 2 ; проверка третьего бита в регистре ах jc metka ; будет произведён прыжок на метку metka: Использование команд ВТ, ВТС, BTR, BTS поможет при оптимизации кода программы; они могут заменить команду TEST для проверки значения конкретного бита.

Хочу всё знать: язык ассемблера

Краткая справка

Язык ассемблера – машинно-ориентированный код низкого уровня, первое упоминание о котором датировано 40-ми годами 20-го века в контексте взаимодействии с компьютером EDSAC. Несмотря на то, что он не всегда использует внутренние инструкции самих машин (всё-таки речь идёт об универсализации), это практически не вызывает потери в быстродействии, наоборот лишь предоставляя пользователю возможности для использования макрокоманд.

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

Исходя из этого выделим очевидные достоинства:

Быстродействие. Быстрее только использовать непосредственные инструкции процессора;

Безопасность. Низкоуровневость в данном случае практически исключает наличие белых пятен в коде;

Эффективность использования возможностей конкретной платформы. Ориентированность на используемую машину позволяет иметь серьезное преимущество по сравнению с высокоуровневыми языками;

Понимание исполняемого кода программистом. На таком уровне программирования код и действие имеют очевидно причинно-следственную связь.

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

Трудно выучить. Специализация на платформу, несколько диалектов — все это не способствует быстрому изучению и пониманию;

Тяжело читать. Большой листинг, простые однотипные операции;

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

Большинство используемых машин просто не нуждается в таком примитивном языке, как ассемблер;

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

Сильно ограниченное количество библиотек, сообществ, вспомогательных ресурсов по современным меркам.

Кому изучать?

Очевидно, что язык ассемблера был хорош и востребован во времена не слишком мощных компьютеров, но почему новый виток роста объявился сейчас? Всё просто: на мир огромной волной надвигается IoT. Использование многочисленных компьютеров, подчинённых одной цели, оправдывает себя только при общей максимально низкой себестоимости и высоком быстродействии, что вынуждает разработчиков обратится к старому-доброму языку ассемблера.

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

Книги

Zen of Assembly Language, Майкл Абраш – именно с этой книги стоит начать изучение, если уж без языка ассемблера вам не обойтись. Основная цель, которую пытается Абраш донести до читателя — это необходимость мыслить легко и широко (“иначе”) в решении сложных задач с помощью такого низкоуровневого инструмента;

Programming from the Ground Up, Джонатан Бартлетт –вторая книга по очереди для прочтения имеет более сухой язык изложения, зато изобилует полезными советами и техническими данными;

Introduction to 64 Bit Assembly Language, Programming for Linux and OS X, Рэй Сейфарт — в этой книге язык ассемблера рассматривается, как базис для всех систем и устройств. Новичку такая книга может показаться тяжелой для понимания, поэтому рекомендуется иметь за плечами хоть какие-то познания в программировании;

Assembly Language for x86 Processors, Уип Ирвинг — уже из названия вы можете понять, что это в большей степени справочная книга, рекомендуемая в учебных заведениях в качестве дополнительной литературы. Однако распространенность данных процессоров и практически неизбежность работы с ними, переносит эту книгу в раздел must-read.

Art of Assembly Language, Рэндэлл Хайд — еще одна прекрасная книга для новичков. Говорят, это одна из наиболее часто рекомендуемых книг в интернете в данной области;

PC Assembly Language, Пол Картер – обучающая языку ассемблера книга с огромным количеством примеров и конкретным их применением из реальной жизни;

Ассемблер и дизассемблирование, Пирогов Владислав – должна быть среди всего этого обучающего великолепия и книга на русском языке. Примеры кода, описание инструментов и ответы на актуальные вопросы новичков — всё прилагается;

Ассемблер? Это просто! Учимся программировать, Калашников Олег — книга второй ступени познания языка ассемблера. То лучше наложить информацию, описанную в ней, на ваши хотя бы минимальные уже полученные знания;

Ассемблер на примерах, Марек Рудольф — а вот эта книга, хоть и не российского производства (однако в оригинале вы её тоже вряд ли прочтёте), идеально подойдёт в качестве базиса. Всё доступно и понятно.

Как вы уже знаете, согласно последним мировым тенденциям, язык ассемблера снова входит в 10-ку самых популярных языков программирования. Как так получилось, что в мире, где большинство начинающих разработчиков с трудом представляет механизмы взаимодействия кода и машины, это первобытное создание вновь на виду? И главное: надо ли учить язык ассемблера? И как? Попробуем разобраться.

Краткая справка

Язык ассемблера – машинно-ориентированный код низкого уровня, первое упоминание о котором датировано 40-ми годами 20-го века в контексте взаимодействии с компьютером EDSAC. Несмотря на то, что он не всегда использует внутренние инструкции самих машин (всё-таки речь идёт об универсализации), это практически не вызывает потери в быстродействии, наоборот лишь предоставляя пользователю возможности для использования макрокоманд.

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

Исходя из этого выделим очевидные достоинства:

Быстродействие. Быстрее только использовать непосредственные инструкции процессора;

Безопасность. Низкоуровневость в данном случае практически исключает наличие белых пятен в коде;

Эффективность использования возможностей конкретной платформы. Ориентированность на используемую машину позволяет иметь серьезное преимущество по сравнению с высокоуровневыми языками;

Понимание исполняемого кода программистом. На таком уровне программирования код и действие имеют очевидно причинно-следственную связь.

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

Трудно выучить. Специализация на платформу, несколько диалектов — все это не способствует быстрому изучению и пониманию;

Тяжело читать. Большой листинг, простые однотипные операции;

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

Большинство используемых машин просто не нуждается в таком примитивном языке, как ассемблер;

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

Сильно ограниченное количество библиотек, сообществ, вспомогательных ресурсов по современным меркам.

Кому изучать?

Очевидно, что язык ассемблера был хорош и востребован во времена не слишком мощных компьютеров, но почему новый виток роста объявился сейчас? Всё просто: на мир огромной волной надвигается IoT. Использование многочисленных компьютеров, подчинённых одной цели, оправдывает себя только при общей максимально низкой себестоимости и высоком быстродействии, что вынуждает разработчиков обратится к старому-доброму языку ассемблера.

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

Книги

Zen of Assembly Language, Майкл Абраш – именно с этой книги стоит начать изучение, если уж без языка ассемблера вам не обойтись. Основная цель, которую пытается Абраш донести до читателя — это необходимость мыслить легко и широко (“иначе”) в решении сложных задач с помощью такого низкоуровневого инструмента;

Programming from the Ground Up, Джонатан Бартлетт –вторая книга по очереди для прочтения имеет более сухой язык изложения, зато изобилует полезными советами и техническими данными;

Introduction to 64 Bit Assembly Language, Programming for Linux and OS X, Рэй Сейфарт — в этой книге язык ассемблера рассматривается, как базис для всех систем и устройств. Новичку такая книга может показаться тяжелой для понимания, поэтому рекомендуется иметь за плечами хоть какие-то познания в программировании;

Assembly Language for x86 Processors, Уип Ирвинг — уже из названия вы можете понять, что это в большей степени справочная книга, рекомендуемая в учебных заведениях в качестве дополнительной литературы. Однако распространенность данных процессоров и практически неизбежность работы с ними, переносит эту книгу в раздел must-read.

Art of Assembly Language, Рэндэлл Хайд — еще одна прекрасная книга для новичков. Говорят, это одна из наиболее часто рекомендуемых книг в интернете в данной области;

PC Assembly Language, Пол Картер – обучающая языку ассемблера книга с огромным количеством примеров и конкретным их применением из реальной жизни;

Ассемблер и дизассемблирование, Пирогов Владислав – должна быть среди всего этого обучающего великолепия и книга на русском языке. Примеры кода, описание инструментов и ответы на актуальные вопросы новичков — всё прилагается;

Ассемблер? Это просто! Учимся программировать, Калашников Олег — книга второй ступени познания языка ассемблера. То лучше наложить информацию, описанную в ней, на ваши хотя бы минимальные уже полученные знания;

Ассемблер на примерах, Марек Рудольф — а вот эта книга, хоть и не российского производства (однако в оригинале вы её тоже вряд ли прочтёте), идеально подойдёт в качестве базиса. Всё доступно и понятно.

Computer Science

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

Что такое ассемблер

Язык ассемблера — язык программирования низкого уровня. Он имеет доступ почти ко всем регистрам компьютера на уровне ISA, кроме 5 служебных регистров процессора. Подготовкой кода для процессора занимается транслятор — ассемблер. Программы на языке ассемблера пишутся до сих пор по двум причинам: производительность и прямой доступ к регистрам процессора.

Регистры ассемблера

У ассемблера есть четыре специальных командных регистра для результатов команд: EAX, EBX, ECX и EDX. В них можно записывать любые данные, но все регистры кроме EBX перезаписываются командами.

EAX — аккумулятор. Перезаписывается командами умножения и деления.
EBX — хранилище адресов. Он не перезаписывается никакой командой.
ECX — счётчик цикла. Он перезаписывается командой LOOP.
EDX — остаток от деления. Перезаписывается командой деления.

Основные команды

В языке ассемблера есть пять команд для простых операций.

MOV — перемещение данных в память или регистр.
ADD — сложение.
SUB — вычитание.
MUL — умножение.
DIV — деление.

Стековые и строковые регистры

Стековые регистры — ESP и EBP.

ESP — указатель на вершину стека. Изменяется командой push и pop.
EBP — указатель для навигации в стеке. Через этот регистр процессор находит локальные переменные функций.

Строковые регистры — ESI и EDI.

ESI — указатель на начало строки. Используется для обработки строк.
EDI — промежуточный указатель. Помогает команде mov перемещать данные между оперативкой и регистрами.

Секции кода

Код на языке ассемблера разделяется на три секции: известные переменные, неизвестные переменные и код. Мы изучали NASM — один из диалектов языка ассемблера. В NASMе секции обозначаются так.

section .data — известные переменные. В этой секции объявляются переменные и их значения.
section .bss — неизвестные переменные. В этой секции объявляются имена переменных и резервируется память для их будущего значения.
section .text — код. В этой секции мы объявляем глобальную метку, чтобы ассемблер понял откуда начинается код и после метки пишем код.

Переходы

В языке ассемблера есть команда сравнения и два вида переходов: условный и безусловный.

CMP — команда сравнения. Сравнивает данные и записывает результат сравнения во флаговый регистр.
JG, JL, JZ — основные команды условного перехода. Они выполняют переход только при определённом результате выполнения CMP.
JMP — безусловный переход. Выполняет переход независимо от результата CMP

Циклы

Управлением циклом занимается команда LOOP. Она повторяет кусок кода от метки до себя. После каждой итерации LOOP проверяет ECX на ноль: больше нуля — отнимает единицу и повторяет цикл, меньше или равен нулю — завершает цикл.

Вывод сообщений

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

В EAX записываем номер системного вызова, у нас это вывод сообщения — 4.
В EBX записываем номер потока, у нас это поток вывода — 1.
В ECX записываем сообщение для вывода.
В EDX записываем длину сообщения для вывода.

Макросы и функции

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

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

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

Ассемблирование

Подготовка кода для процессора называется «ассемблирование», оно происходит
в два прохода.

При первом проходе транслятор формирует три таблицы: символических имён, кодов операций и директив ассемблера. Таблицы записываются во временный файл.

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

Компоновка

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

Компоновка проходит по двум сценариям: статическая компоновка или динамическая компоновка.

Файл с двоичным кодом называется «исполняемый». После этого программу можно запустить.

Assembler

Русский

Ассемблер и программирование для IBM PC. Питер Абель

Книга наиболее широко и полно охватывает все наиболее важные вопросы, .

Ассемблер. Самоучитель. Александр Крупник

Книга знакомит читателя с ассемблером — универсальным языком «низкого уровня», .

Ассемблер — это просто. Учимся программировать . О. А. Калашников

Подробно и доходчиво объясняются все основные вопросы программирования на ассемблере. .

Assembler. Учебный курс. Пирогов

Язык ассемблера фактически представляет собой машинный язык (язык процессора), где .

Программирование на языке ассемблера для IBM PC. Д.Бредли

Цель этой книги — научить писать программы на языке ассемблера .

Ассемблер на примерах. Базовый курс. Рудольф Марек

Эта книга представляет собой великолепное практическое руководство по основам программирования .

Ассемблер для чайников. Поляков А.В.

Данная книга предназначена для тех, кто уже имеет навыки программирования .

Assembler. Учебник для вузов. 2-е изд. В. И. Юров

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

Программирование на Ассемблере для PIC. Носов

Этот самоучитель был написан в 2007-2008 годах.
Самоучитель прошел .

Программирование на языке ассемблера IBM PC. В. Н. Пильщиков

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

Программирование на языке Ассемблера М.Ю. Смоленцев

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

English

Dick Grune — Modern Compiler Design

Twelve years have passed since the first edition of Modern .

AMD x86-64 Architecture Programmer’s Manual Volume 2: System Programming

This book is part of a multivolume work entitled the .

Assembly Language for x86 Processors. Kip R. Irvine

Assembly Language for x86 Processors, Seventh Edition, teaches assembly language .

Professional Assembly Language. Blum

Assembly language is one of the most misunderstood programming languages .

Илон Маск рекомендует:  Достойный бюджетный плеер Cowon iAudio 10. Полный обзор.
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL