Assembler&Win32. Курс молодого бойца.

Учебник по ассемблеру

Уроки для изучения ассемблера

Assembler и Win32. Введение

Assembler и Win32. Урок 1

Assembler и Win32. Урок 2

Assembler и Win32. Урок 3

Assembler и Win32. Урок 4

Assembler и Win32. Урок 5

Assembler и Win32. Урок 6

Assembler и Win32. Урок 7

Assembler и Win32. Урок 8

Assembler и Win32. Урок 9

Assembler и Win32. Урок 10

Assembler и Win32. Урок 11

Assembler и Win32. Урок 12

Assembler и Win32. Урок 13

Assembler и Win32. Урок 14

Assembler и Win32. Урок 15

Автор и составитель: АБЛЯЗОВ Руслан

г. Кузнецк Пензенская область

E-mail: gruzino-abhaz@rambler.ru

Материалы взяты с сайта http://www.programmersClub.ru/

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

Copyright © 2006 Клуб программистов


Assembler&Win32. Курс молодого бойца

Assembler&Win32. Курс молодого бойца. Введение.

Что это такое? Наверное, каждый из нас думает, что ассемблер это что-то сложное, очень трудное для изучения и вообще недосягаемое. Поверьте мне, я тоже так, когда-то думал. Невозможное возможно. Этот туториал рассчитан для тех, кто даже не знает никакого языка программирования, я вообще выступаю, за то, что надо сначала изучать ассемблер, а потом только языки высокого уровня. Если вы перечитаете весь туториал от этих строк до самого конца то вам будет намного проще изучать другие языки программирования, и вы уже никогда не будете спорить о том какой язык лучше С++ или Delphi.

В этом туториале я расскажу вам, что такое ассемблер и с чем его едят. Чем же примечателен этот туториал. Этот туториал примечателен тем, что я расскажу вам про ассемблер под Win32 системы, конечно, есть много учебников и книг про ассемблер, но и все писались под DOS, а ДОС это 16 разрядный отстой. Программирование под ДОС, очень сложно, но чтобы программировать под Win32 можно вообще ничего не знать про DOS. Короче забудьте про ДОС. По ходу этого туториала, я буду немного возвращаться к нему, но только для того чтобы пояснить, чем Win32 лучше DOS.

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

MOV EAX, EBX

В машинных кодах это будет 8BC3 (шестнадцатеричный код, теперь всё будет шестнадцатеричным, ПРИВЫКАЙТЕ). Процессор смотрит на эти циферки и делает то, что они обозначают. Любая программа, написанная на языке высокого уровня (исключая .NET), преобразовывается в ассемблерный код. А почему же ассемблер лучше, чем С++ и др.. Дело в том, что программу, написанную на языке высокого уровня надо перевести на ассемблер, а это очень трудно, потом надо его оптимизировать, современные компиляторы очень плохо с этим справляются. Я провел эксперимент, написал программу (обычное окно с одной кнопкой в центре, которая закрывает его) на разных языках высокого уровня и после компиляции получил вот такие размеры этой самой программы:

C++ Builder 4 - 22 kb
Delphi 5 - 291 kb
Delphi 5 + библиотека KOL - 26 kb
Ассемблер MASM - около 3 kb

Так, что смотрите и делайте выводы, что лучше использовать.
Есть другое различие с некоторыми языками высокого уровня, это использование runtime DLL библиотек для их функций. Visual Basic даже не преобразовывает свой язык в ассемблер (хотя версия 5 и выше делают это частично, но не полностью), это сильно зависит от msvbvm50.dll, виртуальной машины Visual Basic. Exe файл, который создан VB, состоит из простых частей кода и многих вызовов этой DLL. Вот почему VB очень медленный. Ассемблер это и есть самый быстрый язык. Он использует только системные DLL: kernel32.dll, user32.dll, и т.д.
Теперь приступим к тому, где именно мы будем писать наши программы на ассмеблере. Компиляция состоит из 2 стадий:
  1. Проверка синтаксиса, создание объектного файла и файла листинга
  2. Линковка, т.е. компоновка.
Вы, наверное, все привыкли к тому, что запускаете Microsoft Visual C++, пишете программу, нажимаете на F7 и всё готово. На ассемблере вам придётся всё делать всё вручную. У ассемблера нет такой среды разработки. Вам придётся писать программу в блокноте, сохранять его в *.asm файл. И из командной строки вызывать ассемблер и линковщик. Потом я вам всё объясню.

Теперь смотрим инструменты.

Связки ассемблер-линковщик:
MASM32 v9.0 (ml.exe, link.exe)
TASM32 v5.0 (tasm32.exe, tlink32.exe)
Также можно FASM, но он только для спецов, примечателен тем, что у него есть своя оболочка не надо никаких объектных файлов т.д., нажимаешь F9 и всё готово.

Текстовые редакторы
EmEditor Professional Version 4.13 (вне конкуренции)
UltraEdit v*.*.*.* (тоже хороший не я им не пользуюсь)

Блокнот : (классика).
Также пригодится справочник по API функциям. Желательно из комплекта MS SDK. Полная версия весит 24 MB (почти все апи), есть урезанные версии весом 12 МВ. Русские версии весом ~400 KB даже не читайте.

Думаю, хватит для введения. Ну, как? Ещё не отбило желания изучить ассемблер.


Это наиболее простая схема процессора.

Assembler&Win32. Курс молодого бойца. Урок 1.

Что такое процессор

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

Простая схема процессора

Самое главное в процессоре это регистры. Регистры состоят из триггеров. Триггер может иметь 2 значения 0 или 1. Регистры бывают 8, 16, 32 (,64) разрядные, понятно, что если 8 разрядов, то в регистре 8 триггеров. Регистр используется для промежуточного хранения информации, некоторые регистры хранят только определённую информацию. Также есть порты ввода вывода. Доступ к внешним устройствам происходит через порты ввода вывода, с помощью контроллера ввода-вывода. Не путайте порты ввода-вывода с портами LPT, COM и т.д.

Процессоры x86

Процессоры x86 бывают 8086, 80186, 80286, 80386, 80486, 80586 и т.д. дальше до предела человеческого прогресса. Процессоры 8086-80286 16-ти разрядные, дальше идут 32 разрядные. Я вам буду рассказывать о процессоре 80386, остальные не слишком сильно отличаются друг от друга.
Регистры общего назначения - EAX, EBX, ECX, EDX 32 битные регистры младшая часть которых называется AX, BX, CX, DX они 16 битные, каждый регистр делится на 2 части: младшие AL, BL, CL, DL, старшие AH, BH, CH, DH Регистры общего назначения используются для операций с данными, такими, как сравнение, математические операции или запись данных в память. Регистр СХ(ECX) чаще всего используется как счетчик в циклах.
Регистры сегментов - это CS, DS, ES, FS, GS, SS. Эти регистры 16-ти битные, и содержат в себе первую половину адреса "оффсет:сегмент". Адреса я вам поясню в следующем уроке. Регистры оффсета - EIP, ESP, EBP, ESI, EDI. Эти регистры 32-х битные, нижняя половина которых доступна как регистры IP, SP, BP, SI, DI. EIP - указатель команд, и содержит указатель на линию кода, которая будет исполняться следующей. То есть полный адрес на следующую исполняемую линию кода будет CS:IP (CS:EIP).

Регистр ESP указывает на адрес вершины стека. Регистр ЕВР содержит адрес, начиная с которого в стек вносится или забирается информация (или "глубина" стека). Параметры функций имеют положительный сдвиг относительно ЕВР, локальные переменные - отрицательный сдвиг, а полный адрес этого участка памяти будет SS:EBP. Регистр ESI - адрес источника, и содержит адрес начала блока информации для операции "переместить блок" (полный адрес DS:SI), а регистр EDI- адрес назначения в этой операции (полный адрес ES:EDI). А кое-что забыл, я не объяснил вам, что такое стек. Представьте, что одна подпрограмма вызывает другую, вызываемая подпрограмма завершила свое выполнение, теперь ей надо передать управление подпрограмме, которая её вызвала, как раз адрес следующей команды после вызова подпрограммы находится на верхушке стека.

Стек как магазин автомата засунули адрес, засунули второй, засунули третий, и достают адреса в порядке обратном засовыванию, сначала третий, второй, первый. Следовательно, подпрограмма, может вызвать ещё одну подпрограмму, а она в свою очередь ещё одну и так, сколько хочешь, всё ограничивается только размером стека. Со стеком разобрались.
Процессор 80386+ (плюс обозначает, что и все остальные процессоры) может работать в 2х режимах: обычный и защищённый. В защищённом режиме доступны 32 разрядные инструкции и регистры. Win32 работает на процессорах 80386+ в защищённом режиме. DOS работает в обычном режиме. В ДОС нет 32 битных регистров. Вот ещё одно преимущество Win32.
Для того чтобы узнать результат предыдущей операции, существует регистр флагов. Он 16 битный. Каждый бит в нём обозначает некоторый флаг. В регистре флагов есть флаги переполнения, отрицательного результата, положительного результата, запрета прерываний, чётности, переноса и т.д.
Также процессоры 386+, существует поддержка многозадачности, реализована система колец защиты: их 4. Самое привилегированное 0 кольцо в нем, что хочешь, то и делаешь. 3 кольцо защиты наиболее ограниченное. В win32 существует только 0 и 3 кольцо защиты. В 3 кольце нельзя выполнять привилегированные команды процессора нельзя обращаться к портам ввода вывода и запускать прерывания, но об этом позже.
Вот и подошёл к концу первый урок.


память операционной системы

Assembler&Win32. Курс молодого бойца. Урок 2.

Память системы

Прежде чем рассказывать, о языке ассемблера я расскажу вам, как устроена память на Win32 системах. Программировать на ассемблере не зная, как устроена память системы бессмысленно. Самое главное преимущество ассемблера состоит во взаимодействии с памятью.
Ладно, приступим. Сначала я объясню, что такое смещение. Смещение это положение относительно чего-то. Смещение может быть в файле, в памяти и вообще во всём. Если говорить по-другому, то смещение это положение, адрес, местонахождение всегда измеряется в байтах. У каждой переменной есть смещение, у каждой команды есть смещение.

Немного истории.
Теперь о памяти. В ДОС и в windows 3.Х память была сегментной. Вся память делилась на сегменты по 64 КВ., и адрес (именно адрес, а не смещение) задавалась, форматом сегмент: офсет. Сегмент задаёт порядковый номер сегмента, а офсет задаёт смещение чего-либо в этом сегменте.

Память

Сегмент 1
(64kb)

Сегмент 2
(64kb)

Сегмент 3
(64kb)

Сегмент 4
(64kb)

и так далее

Учтите, что это объяснение для 16-разрядных программ, о 32-разрядных я расскажу позже, (но не пропускайте эту часть, важно понять, что такое 32-разрядность).
Таблица выше иллюстрирует общую память, разделенную на сегменты по 64kb. Здесь максимум 65536 сегментов. Теперь возьмите один из сегментов:

Сегмент 1(64kb)

Смещение 1

Смещение 2

Смещение 3

Смещение 4

Смещение 5

и так далее

Вы спросите, зачем нужна эта сегментность. Всему есть разумное объяснение. Дело в том в том, что ДОС 16 битный. И регистры 16 битные. С помощью 16 бит можно адресовать только 65536 байт памяти и 65536 сегментов.

Например:
0030:4012 (всё шестнадцатиричное)
Это означает: сегмент 30, смещение 4012. Чтобы узнать, что находится в том адресе, вы сначала переходите на сегмент 30, а затем в сегменте смещаетесь на 4012. В предыдущих уроках мы узнали о сегментных и указательных регистрах. Например, сегментные регистры:
CS - Сегмент кода
DS - Сегмент данных
SS - Сегмент стека
ES - Дополнительный сегмент
FS - Универсальный сегмент
GS - Универсальный сегмент
Названия говорят о их функциях: сегмент кода (CS) содержит номер секции, где вылнен текущий код. Сегмент данных для получения данных из текущего сегмента. На стек указывает сегмент стека (SS) (подробнее о стеке я раскажу позже), ES, FS, GS - универсальные регистры и могут использоваться для любого сегмента (не в win32).

Теперь о Win32

В 16-разрядном программировании, сегменты необходимы. К счастью, эта проблема решена в 32-разрядном Windows (95 и выше). Вы все еще имеете сегменты, но вам не нужно заботиться о них, потому, что они уже не 64kb (как в 16-разрядном), а 4 Гб. Windows вероятно даже "повиснет", если вы попытаетесь изменить один из сегментных регистров. Это называеся плоской моделью памяти (flat). Здесь есть только смещения и они теперь 32-разрядные (в диапазоне от 0 до 4,294,967,295). Каждая ячейка в памяти указывается смещением. Это действительно одно из лучших преимуществ 32-разрядного программирования над 16-разрядным. Так что теперь вы можете забыть о сегментных регистрах и сосредоточиться на других регистрах.

Общая схема памяти в Win32
общая схема памяти в Win32

Но 4ГБ может и не быть на машине, поэтому эта память называется виртуальной. Мало того, каждый процесс выполняется в своём виртуальном адресном пространстве. А это значит, что никакой другой процесс не сможет получить доступ к вашей памяти. Для этого существуют специальные API функции, но для их использования нужно иметь специальные привилегии в системе. Память 0-0000FFFF не используется и служит для выявления нулевых указателей, значит, если вы укажете адрес 0000С567, то он будет считаться нулевым, удобно, не правда ли? Любая попытка обратится к этой памяти приводит к ошибке. Память выше 80000000 одна для всех процессов. В этой памяти находится код нулевого кольца, структуры ядра, код планировщика задач, код драйверов, диспетчер ввода вывода, таблица прерываний и т.д. Любая попытка обратиться к памяти ядра приводит к ошибке и к немедленному завершению приложения. Память в диапазоне 00001000-7FFFFFFF доступна для 3 кольца, т.е. для вашего приложения. С ней вы, что хотите то и делаете, в неё также грузятся Дллки.

Выделение памяти.
Если вам даётся 2ГБ памяти, то это не означает, что вы можете обратиться к любому участку памяти. Для того чтобы получить доступ к некоторому участку памяти надо сначала её зарезервировать. Грубо говоря, перед резервированием памяти, резервируемого участка памяти просто не существует (я не оговорился её просто не существует), вы как бы его создаёте и задаёте ему атрибуты доступа (полный доступ, только чтение, только запись, нет доступа). Минимальный размер выделяемой памяти - страница, равна 1000 байтам (для тех, кто не привык к шестнадцатеричной записи, это 4096 байт - 4КБ). Даже если вы захотите выделить 5 байт, то всё равно выделится 4КБ. Такой метод распределения памяти называется гранулярность. Думаю, вы не столкнётесь с тем, что вам надо будет выделять память, лично мне это ни разу не пригодилось. Это нужно при работе с файлами для того, что бы в эту память читать файл.
Не расслабляемся, читаем следующий, урок.


mov - команда ассемблера

Assembler&Win32. Курс молодого бойца. Урок 3.

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

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

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

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

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

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

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

mov edx, ecx

Вышеприведенная команда копирует содержание 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.

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

mov edx, 5006h

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

mov eax, 403045h ; пишет в eax значение 403045
mov cx, [eax] ; помещает в регистр CX значение (размера word) из памяти
; указанной в EAX (403045)

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Стековые операции - PUSH, POP. Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении - увеличивается. Рассмотрим пример:

(1) mov ecx, 100
(2) mov eax, 200
(3) push ecx ; сохранение ecx
(4) push eax
(5) xor ecx, eax
(6) add ecx, 400
(7) mov edx, ecx
(8) pop ebx
(9) pop ecx

Анализ:
1: поместить 100 в ecx
2: поместить 200 в eax
3: разместить значение из ecx (=100) в стеке (размещается первым)
4: разместить значение из eax (=200) в стеке (размещается последним)
5/6/7: выполнение операций над ecx, значение в ecx изменяется
8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)

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


Пример простейшей программы на ассемблере

Assembler&Win32. Курс молодого бойца. Урок 4.

Первая программа

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

Общая структура программы.

.386p ;тип процессора
.model flat, stdcall ; модель памяти и вызова подпрограмм
;объявление включаемых (заголовочных) файлов, макросов, макроопределений,
; также внешних определений
.data
; Инициализированные данные
.data?
; неинициализированные данные
.const
; константы
.code
; исполняемый код
End <метка точки входа>

Процессоры могут быть 386, 486, 586, обычно всегда стоит 386p, но ничто вам не мешает поставить 486p или 586. Модель памяти всегда flat и никакой другой не может быть. Вызов подпрограмм обычно всегда stdcall, стандарт вызова почти всех API функций. Секция .data секция с инициализированными данными она включается в исполняемый файл. Секция .data? секция с неинициализированными данными, она не включается в исполняемый файл и появляется только тогда, когда программа загружается в память. Секция .const секция констант. Секция .code содержит исполняемый код программы. В конце программы всегда должно стоять слово end, которая задаёт точку входа программы, т.е. место с которого начнётся выполняться программа. Секции .data, .data? имеют полный доступ. Разумеется, секции .const и .code имеют атрибут доступа - только чтение. Секция .const наиболее редко встречается в программах, так как константы можно задавать с помощью макроопределений.
При программировании под Win32 мы не имеем доступ к портам ввода вывода и не можем вызывать прерывания (самые главные команды в DOS), у нас есть только WIN API, которые экспортируют системные библиотеки. Вызывая API функцию, вы просто передаёте управление точке входа функции (точка входа функции это первая инструкция функции), сама функция находится где-то в памяти вашего процесса внутри некоторой библиотеки. API функции находятся в библиотеках kernel32.dll, user32.dll, gdi32.dll (классика) advapi32.dll и т.д., для того чтобы использовать некоторую функцию этих библиотек надо сначала загрузить нужную библиотеку в свою память, либо включить используемые функции в таблицу импорта и тогда загрузчик загрузит библиотеки за вас. О kernel32.dll можно не волноваться, потому что эта библиотека есть в каждом процессе в любом случае, тем более она там существует ещё до запуска главной нити процесса, она даже проецируется в вашу память всегда по одинаковым адресам. Как я уже говорил все функции, используют модель вызова stdcall, а значит, параметры подпрограмме передаются "задом-на-перёд" и вызываемый очищает стек. Общий вид вызова API функции будет такой:

Push параметр 3
Push параметр 2
Push параметр 1
Call функция ; если быть точнее то здесь должен стоятб ажрес функции
; т.е. адрес её точки входа

Каждая программа в Win32 в конце своего выполнения ОБЯЗАТЕЛЬНО должна вызвать функцию ExitProcess. Сами подумайте, после выполнения последней инструкции программы, дальше идёт пустота, даже если там что-то осмысленное, все равно у выделенной памяти есть конец, а после конца идёт невыделенная (т.е. несуществующая) память. Процессор начнёт выполнять непоймёшь чё, пойдут глюки, и ваша программа даст в конце хорошо всем знакомый звук - "ДУНГ". А это уже бессмыслица. Вот определение ExitProcess из MS SDK:

VOID ExitProcess(
UINT uExitCode // exit code for all threads
);
uExitCode - это значение никогда не используется и всегда равно 0.

Первая программа.
Теперь я приведу вам программу, которая выводит легендарное сообщение "Hello, World!".

.386
.model flat

extrn ExitProcess:PROC
extrn MessageBoxA:PROC

.data

Ttl db "First ASSEMBLER program",0h
Msg db 'Hello, World!!!!',0h

.code

start:
push 0h
push offset Msg
push offset Ttl
push 0h
call MessageBoxA
push 0h
call ExitProcess
end start

Мы вызываем функцию MessageBoxA, для вывода окна сообщения потом выходим из процесса. Теперь поподробнее.

Определение функции MessageBox из MS SDK в оригинале:
int MessageBox(
HWND hWnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType // style of message box
);

Мы указали только модель памяти и модель вызов подпрограмм не указали, так как это просто не к чему. В качестве первого параметра мы передаём ноль, поэтому у окна сообщения не будет родительского окна. В качестве параметров lpText и lpCaption мы передаём адреса соответствующих строк, заметьте, что мы передаём адрес строки (т.е. её первого символа) у которой в конце стоит символ #0, это нужно, для того чтобы функция смогла найти конец строки. Последний параметр определяет стиль окна сообщения, в данном случае мы передали 0, что обозначает, что там будет только одна кнопка OK, иначе этот стиль обозначается MB_OK. Функцию ExitProcess вы уже знаете. После директивы end мы указали метку, с которой начнётся выполнение ваша программа.

Зачем нужна буква A в конце функции?
В Win32 есть 2 типа функции заканчивающиеся на A и на W. Всё отличие в строках, которые принимают функции. А-функции принимают ANSI строки. W-функции принимают Unicode строки. У каждой функции, которая принимает строки, есть обе версии. Обычно все пользуются ANSI версиями функций. Но в любом случае при вызове ANSI функции все строки сначала преобразуются в Unicode строки, и будет вызвана Unicode функция.

Компиляция.
Чуть-чуть не забыл рассказать, как надо компилировать программы. Для этого вам надо написать текст программы на ассемблере и сохранить в файл с расширением *.asm. Потом надо ассемблировать его. Для этого вам понадобится программа tasm32.exe она нахидтся в папке %tasmdir%\bin asm32.exe, формат её вызова такой: TASM [options] source [,object] [,listing] [,xref] (доп сведения смотрите в tasm32 /?) для большинства программ ассемблирование проходит так:
%tasmdir%\BIN asm32 /m3 /ml asmfile,,;
Где asmfile - имя вашего файла БЕЗ расширения. Потом будет создан объектный файл, файл листинга и т.д. Теперь вам надо всё это слинковать. Для Линковки вам понадобится программа tlink32.exe. Вот формат её вызова:
TLINK objfiles, exefile, mapfile, libfiles, deffile, resfiles
Для всех программ в этом туториале линковка будет такой:
%tasmdir%\BIN link32 /Tpe /aa asmfile,asmfile,,%tasmdir% \Lib\import32.lib
Где asmfile - имя вашего файла БЕЗ расширения.
Статическая библиотека %tasmdir% \Lib\import32.lib содержит в себе все функции библиотек kernel32.dll, user32.dll, gdi32.dll и может быть других стандартных библиотек. Желательно чтобы asmfile находился в папке %tasmdir%.
Для бысрой компиляции я вам советую создать *.bat файл, который всё это делает за вас. Вот текст моего файла.
Compile.bat
cls
echo off
d: asm_5\BIN asm32 /m3 /ml %1,,;
d: asm_5\BIN link32 /Tpe /aa %1,%1,,d:\TASM_5\Lib\import32.lib
echo "Succesful"
Теперь после вызова
Compile sample
У вас готовый к выполнению экзешник.
Подошёл к концу четвёртый урок. На этом уроке мы написали первую программу на ассемблере. Но я вам почти не объяснил, как надо пользоваться меткам и данными. Именно, для этого и предназначены 2 следующий урока


Работа с данными в ассемблере.

Assembler&Win32. Курс молодого бойца. Урок 5.

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

Секции .data, .data? и .const нужны для определения данных программы. Место под данные резервируется с помощью директив db, dw, dd, dq, dt.

db - 1 байт
dw - 2 байта - 1 слово
dd - 4 байта - 2 слова
dq - 8 байт - 4 слова
dt - 10 байт

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

.data
Perem dd 0000FF11h
X1 dw 01235h
Binary db 00111010b
dd 11225599h
decimal dw 28d
large dq 01123456789ABCDEFh
dw 1011100101010111b
.data?
Perem1 dd ?
Perem2 dq ?
Perem5 dw ?
Dd ?
Dw ?
Perem4 db ?
..const
Const1 dd 012345678h
dw 768d

Строки.
В ассемблере можно задавать только ANSI строки, Unicode строки сложнее задавать и для их обработки существует целый ряд API функций. В ассемблере также можно вместо присваивания однобайтовой переменной некоторого числа можно присвоить переменно букву. Но, в конечном счете, эта переменная будет равна коду буквы в кодировке ANSI. При инициализации символа можно использовать и кавычки и апострофы - без разницы.

.data
Char1 db 'W'
Char2 db 'Й'
db "r"
Всё выше написанное тоже самое что и:
.data
Char1 db 57h
Char2 db 0C9h
db 72h
При объявлении строк можно просто написать стоку после директивы db. Это воспринимается как последовательность символов
String db "ASM"
Тоже самое:
String db 'A'
Db "S"
Db "M"
Тоже самое:
String db 41h
Db 53h
Db 4Dh
Тоже самое:
String db 41h, 53h, 4Dh
При передаче строк функциям в качестве параметров надо чтобы в конце строки был 0, для того чтобы функция смогла найти конец строки.
String db "ASM",0
Msg db "First ASSEMBLER program",0
Ttl db 'Hello, World!!!!',0

Заполнение данными.
Иногда нужно описать много одинаковых переменных примерно штук 30. Вы будете делать так
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 30 раз
Это неудобно и некрасиво, тем более можно обсчитаться. Для сделана директива DUP.
db 30 DUP (0) ; тот же результат
в скобках указываем, чем надо заполнять, можно использовать символы в кавычках, обязательно чтобы размер в скобках совпадал с директивой.

Примеры:
Dd 10 DUP ("в")
Dw 45h DUP (0DF23h)
Dd 100b DUP (12345678h)

С данными всё понятно. Читаем следующий урок. На 6 уроке мы будем говорить о метках и их использовании.


Символьные метки, метки данных.

Assembler&Win32. Курс молодого бойца. Урок 6.

Символьные метки

Одно из преимуществ ассемблера заключается в использовании символьных меток и меток данных (переменных). По сути оба типа меток - это символьные метки. На этом уроке я вам объясню, как надо пользоваться символьными метками и метками данных.
Начну с более сложного - меток данных. В секции с данными с помощью директив db, dw, dd, dq вы резервируете память под данные соответствующего размера. Но надо неким образом получить доступ к этим данным. Для этого существуют символьные метки переменных. По существу метка возвращает смещение, на котором она находится. Метка виртуальна - в шестнадцатеричных кодах у неё нет эквивалента, она создана только для удобства программиста. При использовании метки под ней подразумевается некоторое смещение, на которую она указывает.
Пример:
.data
PARAM dd 11223344h

.code
mov eax, [PARAM]
mov [PARAM], ebx
По определению это правильный метод использования меток. После данных операций регистр eax будет равен 11223344. Но парадокс у операций:

mov eax, PARAM
mov PARAM, ebx

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

.data
PARAM dw 1122h
PARAM1 db 12h
PARAM2 dd 12345678h

.code

mov eax,0
mov ebx, 0
mov ecx,0
mov edx, 0
mov al, byte ptr [PARAM]
mov bl, [PARAM1]
mov ecx,[PARAM2]
mov dx, word ptr [PARAM2]
mov bh, byte ptr [PARAM2]

После всех манипуляций состояние регистров будет таким:
AL = 22
BL = 12
ECX = 12345678
DX = 5678
BH = 78

Ещё один пример:

.data

PARAM1 dd 12345678h

.code
mov ax, 8888h
mov word ptr [PARAM1], ax
mov ebx, [PARAM1]
mov ecx, 0
mov edx, 0
mov cx, word ptr [PARAM1+2]
mov [PARAM1], 089ABCDEFh
mov dx, word ptr [PARAM1]

После всех манипуляций состояние регистров будет таким:
EBX = 12348888
CX = 1234
DX = CDEF

Думаю здесь всё понятно. Размер копируемых данных по умолчанию равен размеру директивы, которая стоит после метки.
Символьные метки отличаются от меток данных тем, что их можно описывать в любом месте программы. После символьной метки надо ставить двоеточие (как в других языках высокого уровня). Они также возвращают смещение, на котором стоят. Они используются при прыжках (о них расскажу позже), или с командой call. Команда call передаёт управление команде, которая находится на смещении, на которое указывает данная метка. Их также можно использовать вместо меток данных. Но при получении значения или его изменении надо обязательно указывать размер данных.
Пример:
.data
metka:
PARAM1 dd 12345678h

.code
mov ecx, 0
mov ebx, 0
mov ax, 0BEDAh
mov word ptr [metka],ax
mov bx, word ptr [metka]
mov cx, word ptr [metka+2]
После этих манипуляций состояние регистров будет такое:
CX = 1234
BX = BEDA

Получение смещения метки.
Для получения смещения метки есть специальный префикс offset. Вы, наверное, видели использование этого префикса в нашей первой программе. Вот пример использования префикса offset:
.data
PARAM1 dd 12345678h

.code
mov eax, offset PARAM1
После этого в регистре eax будет находиться смещение метки PARAM1. При использовании префикса назначением также может быть ячейка памяти, у следующих команд будет тот же результат что и в первом случае:
mov [PARAM1], offset PARAM1
mov eax, [PARAM1]
Альтернативой префиксу offset является команда lea. Пример:
.data
metka:
PARAM1 dd 12345678h

.code
lea eax, metka
После этого в регистре eax будет находиться смещение метки metka. В команде lea можно получать смещение метки данных. Следующий пример будет иметь тот же результат что и предыдущий:
.data
metka:
PARAM1 dd 12345678h

.code
lea eax, PARAM1

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


Основы программирования на ассемблере

Assembler&Win32. Курс молодого бойца. Урок 7.

Переходы (прыжки)

Для корректной обработки данных в ассемблере нужна разветвленность программы. Это достигается использованием условных переходов. Условный переход это такая команда процессору, при которой в зависимости от состояния регистра флагов производится передача управления по некоторому адресу иначе говоря прыжок. Этот адрес может быть ближним или дальним. Прыжок считается ближним, если адрес, на который делается прыжок, находится не дальше чем 128 байт назад и 127 байт вперёд от следующей команды. Дальний прыжок это прыжок дальше, чем на [-128,127] байт. Прыгать просто так в любом месте программы бессмысленно. Каждая команда изменяет регистр флагов в зависимости от результата своей операции. Обычно перед командой прыжка идёт команда сравнения, которая изменяет регистр флагов в зависимости от результата. Команд сравнения две: cmp и test. Наиболее универсальная cmp. О команде test я вам расскажу позже. Команда Cmp производит сравнение двух операндов. Она сравнивает два значения (регистр, память, непосредственное значение) и устанавливает флаг нуля Z (zeroflag) если они равны.

Cmp eax, ebx
Cmp edx, 045567890h
Cmp [ebx], esi
Cmp byte ptr [VALUE1], 045h

Регистр флагов.
Регистр флагов имеет набор флагов, которые устанавливаются или сбрасываются в зависимости от вычислений или других событий. Я не буду расказывать о всех, а только о некоторых важных:
ZF (Zero flag) Флаг нуля
Этот флаг устанавливается, когда результат вычисления нулевой (чтобы сравнить - фактически substraction без того, чтобы сохранить(экономить) результаты, но устанавливать флажки только). This flag is set when the result of a calculation is zero (сравнение это фактически вычитание без сохранения результата, и установка соответствующих флагов).
SF (Sign flag) Флаг знака
Если установлен, значит результат вычисления - отрицательное число.
CF (Carry flag) Флаг переноса
Флаг переноса, содержит крайний левый (старший) бит после вычислений.
OF (Overflow flag) Флаг переполнения
Указывает переполнение результата вычисления, т.е. результат больше, приемник.
Есть еще большое колличество флагов (флаг паритета (pf), вспомогательный флаг переноса (af), флаг трассировки (tf), флаг прерывания (if), флаг управления (df), флаг уровня привилегий ввода/вывода (iopl) , флаг вложенности задачи (nt), флаг возобновления (rf), флаг виртуального режима (vm)) но так как мы не будем их использовать, я не буду о них рассказывать.

Прыжки.
Приведу пример самой распространённой команды условного перехода jz:

Cmp eax, 42h
Jz metka2

Если регистр eax равен 42, то управление передастся первой команде после метки metka2.
Также есть команда безусловного перехода - jmp. Она передаёт управление в любом случае. Вы скажете: "Для этого нужна команда call". Я отвечу: "Команда call пихает в стек адрес возврата, а jmp не трогает стек". Условные переходы могут делать только ближний прыжок, а команда jmp может делать и короткий и дальний прыжки. Вот таблица всех условных переходов.

cmp x, y
Опкод | Значение(переход,если...) | Условие
JA | Jump if above (X > Y) | CF=0 & ZF=0
JAE | Jump if above or equal (X >= Y) | CF=0
JB | Jump if below (X < Y) | CF=1
JBE | Jump if below or equal (X < Y) | CF=1 or ZF=1
JC | Jump if carry (cf=1) | CF=1
JCXZ | Jump if CX=0 | регистр CX=0
JE (то же, что и JZ) | Jump if equal (X = Y) | ZF=1
JG | Jump if greater (signed) (X > Y) | ZF=0 & SF=OF
JGE | Jump if greater or equal (signed) (X >= Y) | SF=OF
JL | Jump if less (signed) (X < Y) | SF != OF
JLE | Jump if less or equal (signed) (X <= Y) | ZF=1 or SF!=OF
JMP | Безусловный переход | -
JNA | Jump if not above (X <= Y) | CF=1 or ZF=1
JNAE | Jump if not above or equal (X < Y) | CF=1
JNB | Jump if not below (X >= Y) | CF=0
JNBE | Jump if not below or equal (X > Y) | CF=1 & ZF=0
JNC | Jump if not carry (cf=0) | CF=0
JNE | Jump if not equal (X != Y) | ZF=0
JNG | Jump if not greater (signed) (X <= Y) | ZF=1 or SF!=OF
JNGE | Jump if not greater or equal (signed) (X < Y) | SF!=OF
JNL | Jump if not less (signed) (X >= Y) | SF=OF
JNLE | Jump if not less or equal (signed) (X > Y) | ZF=0 & SF=OF
JNO | Jump if not overflow (signed) (of=0) | OF=0
JNP | Jump if no parity (pf=0) | PF=0
JNS | Jump if not signed (signed) (sf=0) | SF=0
JNZ | Jump if not zero (X != Y) | ZF=0
JO | Jump if overflow (signed) (of=1) | OF=1
JP | Jump if parity (pf=1) | PF=1
JP | Jump if parity (pf=1) | PF=1
JPE | Jump if parity even | PF=1
JPO | Jump if parity odd | PF=0
JS | Jump if signed (signed) | SF=1
JZ | Jump if zero (X = Y) | ZF=1
Все команды перехода имеют один операнд: смещение для перехода.

Организация циклов.
Иногда надо организовать цикл, т.е. совершить повторение количество раз, которое указано в регистре ECX. Следовательно, надо у теле цикла уменьшать значение ECX. Именно для этого предназначена команда loop. Он проверяет, равен ли регистр ECX нулю, если он не равен нулю, то значение регистра ECX уменьшается на 1 и совершается ближний прыжок на смещение указанное в операнде.

Mov ecx, 023h
repeat:
……..; обязательно ближнее расстояние
Loop repeat
Тело цикла выполнится 23h раза.

Команда loope делает то же самое, но перед прыжком проверяет, установлен ли флаг ZF, если он установлен, то прыжок совершается. Точно тоже самое делает команда loopz. Команды loopne и loopnz делают то же сомое что и loope, но прыгают, если флаг ZF сброшен.

С переходами разобрались. Разобрались не только с переходами, но и с циклами. Продолжаем изучение ассемблера, читаем следующий урок.


Assembler&Win32. Курс молодого бойца. Урок 8.

Команды ассемблера

Арифметические операции - ADD, SUB, MUL, DIV. Многие опкоды делают вычисления. Вы можете узнать многие из них по их названиям: add (addition - добавление), sub (substraction - вычитание), mul (multiply - умножение), div (divide - деление).

Опкод add имеет следующий синтаксис:

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

Выполняет вычисление: приемник = приемник + источник.

Имеются также другие формы:
приемник | источник | пример
регистр | регистр | add ecx, edx
регистр | память | add ecx, dword ptr [104h] / add ecx, [edx]
регистр | значение | add eax, 102
память | значение | add dword ptr [401231h], 80
память | регистр | add dword ptr [401231h], edx

Эта команда очень проста. Она добавляет значение источника к значение приемника и помещает результат в приемник. Другие математические команды:

sub приемник, источник (приемник = приемник - источник)
mul множимое, множитель (множимое = множимое * множитель)
div делитель (eax = eax / делитель, edx = остаток)

Поскольку регистры могут содержать только целочисленные значения (то есть числа, не, с плавающей запятой), результат деления разбит на частное и остаток. Теперь, в зависимости от размера источника, частное сохраняется в eax, а остаток в edx:
размер источника | деление | частное в... | остаток в...
BYTE (8-bits) | ax / делитель | AL | AH
WORD (16-bits) | dx:ax* / делитель | AX | DX
DWORD (32-bits) | edx:eax* / делитель | EAX | EDX

* = Например: если dx = 2030h, а ax = 0040h, dx: ax = 20300040h. Dx:ax - значение dword, где dx представляет старшее word, а ax - младшее. Edx:eax - значение quadword (64 бита), где старшее dword в edx и младшее в eax.

Источник операции деления может быть:
  1. 8-бит регистр (al, ah, cl,...)
  2. 16-бит регистр (ax, dx, ...)
  3. 32-бит регистр (eax, edx, ecx...)
  4. 8-бит значение из памяти (byte ptr [xxxx])
  5. 16-бит значение из памяти (word ptr [xxxx])
  6. 6a 32-бит значение памяти (dword ptr [xxxx])

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

Логические операции с битами - OR, XOR, AND, NOT.Эти команды работают с приемником и источником, исключение команда 'NOT'. Каждый бит в приемнике сравнивается с тем же самым битом в источнике, и в зависимости от команды, 0 или 1 помещается в бит приемника:
команда | AND |  |  |  | OR |  |  |  | XOR |  |  |  | NOT
Бит источника | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1
Бит приемника | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | X | X
Бит результата | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 0

AND (логическое И) устанавливает бит результата в 1, если оба бита, бит источника и бит приемника установлены в 1.
OR (логическое ИЛИ) устанавливает бит результата в 1, если один из битов, бит источника или бит приемника установлен в 1.
XOR (НЕ ИЛИ) устанавливает бит результата в 1, если бит источника отличается от бита приемника.
NOT инвертирует бит источника.

Пример:

mov ax, 3406d
mov dx, 13EAh
xor ax, dx

ax = 3406 (десятичное), в двоичном - 0000110101001110.

dx = 13EA (шестнадцатиричное), в двоичном - 0001001111101010.

Выполнение операции XOR на этими битами:

Источник = 0001001111101010 (dx)

Приемник = 0000110101001110 (ax)

Результат = 0001111010100101 (новое значение в ax)

Новое значение в ax, после выполнения команды - 0001111010100101 (7845 - в десятичном, 1EA5 - в шестнадцатиричном).

Другой пример:

mov ecx, FFFF0000h
not ecx

FFFF0000 в двоичном это - 11111111111111110000000000000000
Если вы выполните инверсию каждого бита, то получите:
00000000000000001111111111111111 , в шестнадцатиричном это 0000FFFF
Значит после операции NOT, ecx будет содержать 0000FFFFh.

Увеличение/Уменьшение - INC/DEC. Есть 2 очень простые команды, DEC и INC. Эти команды увеличивают или уменьшают содержимое памяти или регистра на единицу. Просто поместите:

inc регистр ; регистр = регистр + 1
dec регистр ; регистр = регистр - 1
inc dword ptr [103405] ; значение в [103405] будет увеличено на 1.
dec dword ptr [103405] ; значение в [103405] будет уменьшено на 1.

Ещё одна команда сравнения - test. Команда Test выполняет операцию AND (логическое И) с двумя операндами и в зависимости от результата устанавливает или сбрасывает соответствующие флаги. Результат не сохраняется. Test используется для проверки бит, например в регистре:

test eax, 100b
jnz смещение

Команда jnz выполнит переход, если в регистре eax третий бит справа - установлен. Очень часто комманду test используют для проверки, равен ли регистр нулю:

test ecx, ecx
jz смещение

Команда jz выполнит переход, если ecx = 0.

Ничего не делающая команда - nop. Эта команда не делает абсолютно ничего (пустая команда). Она только занимает пространство и время. Используется для резервирования места в сегменте кода или организации программной задержки.

Обмен значениями - XCHG. Команда XCHG также весьма проста. Назначение: обмен двух значений между регистрами или между регистрами и памятью:

mov eax , 237h
mov ecx, 978h
xchg eax, ecx
в результате:
eax = 978h
ecx = 237h

Вот и конец урока. Надеюсь, этот не был скучным. Следующий урок расскажет вам про подпрограммы.


Assembler&Win32. Курс молодого бойца. Урок 9.

Подпрограммы и структуры

На этом уроке я вам расскажу про подпрограммы и структуры. Подпрограмма по-простому это процедура (или функция: без разницы, язык Pascal дал этим терминам разные определения), а структура это запись.

Подпрограммы.
Подпрограммы удобны, когда некоторое действие повторяется несколько раз в программе. Процедуры делают программу более читабельной и надёжнее, в такой программе легче устранять ошибки и улучшать.
Процедуры задаются директивами proc и endp. Proc обозначает начало процедуры, а endp конец процедуры. Вот пример объявления процедуры:

SameProc proc
……more code……
Ret ; обязательно
SameProc endp
Вызов процедуры
…
Call SameProc
…

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

SameProc proc
Push ebx
Push edi
….more code ….
Mov esi, edx
…изменяем сохранённые регисты….
Pop edi
Pop ebx
Ret
SameProc endp

Специально для этого создана директива uses.

SameProc proc uses ebx,esi
……more code……
Mov esi, edx
…изменяем сохранённые регисты….
Ret ; обязательно
SameProc endp

И всё теперь вместо всяких "пушев" и "попов" ставите ret, TASM сделает всё за вас: в начале поставит "пуши" вместо ret поставит "попы" (извините за выражение) и ret.

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

Структуры.
Структура это набор переменных (данных). Структура задаётся с помощью директивы struct и ends.

SOMESTRUCTURE STRUCT
dword1 dd ?
dword2 dd ?
some_word dw ?
abyte db ?
anotherbyte db ?
SOMESTRUCTURE ENDS
(имя структуры не должно содержать прописных букв).

Вы также можете объявить ваши переменные как в секции с инициализированными данными, так и в секции с неинициализированными данными, со знаком вопроса.

MYSTRUCT struc
dword1 dd ?
dword2 dd ?
some_word dw ?
abyte db ?
anotherbyte db ?
MYSTRUCT ends
.data
msg MYSTRUCT >

или

MYSTRUCT struc
dword1 dd ?
dword2 dd ?
some_word dw ?
abyte db ?
anotherbyte db ?
MYSTRUCT ends
.data?
msg MYSTRUCT >
одинаковый результат, но во втором случае размер файла будет меньше.

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

mov [msg.dword1], 45h
xor eax,eax
mov eax, [msg.dword1] ; eax = 45
при этом запись msg.dword1 считается обычной меткой данных: берётся смещение метки msg плюс смещение поля dword1 в структуре, размер данных по умолчанию равен размеру директивы указанной после метки поля. Также можно пользоваться обращением к полю при обращении к записи через регистр:

mov [msg.dword2], 45h
xor eax,eax
lea ebx, msg
mov eax, [ebx].dword2 ; eax = 45
в данном случае к смещению, которое указано в ebx прибавляется смещение dword2 в своей структуре. Так как имя поля не гарантирует уникальности то лучше использовать такой тип использования записи так:

mov [msg.dword2], 45h
xor eax,eax
lea ebx, msg
mov eax, [ebx].MYSTRUCT.dword2 ; eax = 45
это безопаснее. Для того, что бы окончательно разобраться с записями приведу ещё один пример, в котором используется доступ к полю записи в лоб:

mov [msg.abyte], 45h
xor eax,eax
lea ebx, msg
mov al, [ebx+10d] ; al = 45
к ebx я прибавил 10d, потому что смещение поля abyte в структуре равно 10d.

Вот и конец девятого урока.


Assembler&Win32. Курс молодого бойца. Урок 10.

Упрощаем программирование

Альтернативы секции .const
Главной альтернативой секции .const является простое объявление символьной константы. Всё гениальное просто (слишком сильно сказано):

CONST_VALUE = 678h
.data
Dd CONST_VALUE
.code
…..more code…
Mov edi, CONST_VALUE
Xor eax, CONST_VALUE

Вторая не менее важная альтернатива это определение "макросимвола" (я это сам придумал, а по научному абсолютный символ или "прозвище"). Он задаётся через директиву equ.

примеры

CONST1 equ 0123h
CONST2 equ 14d*15h
CONST3 equ "slovo"
CONST4 equ 56-45
CONST5 equ (offset metka1)
CONST6 equ (offset metka2+offset metka3)
CONST7 equ (CONST2+10b)
CONST8 equ CONST7/2
CONST9 equ (offset metka1-offse metka5)
CONST10 equ (offset metka4+CONST4)
CONST11 equ add edx,edi
…. и так далее до посинения ……. :

Директива equ используется в там же, где определяются символьные константы. Это ещё не все возможности этой директивы. О ней можно говорить очень много. Я вам объяснил, как надо объявлять численные константы, но также можно дать прозвище некоторой команде, например:

Command1 equ mov eax, esi.

После этого при каждом упоминании command1 будет подразумеваться команда move eax, esi.

Пример
Mov eax, CONST1
Add edi, CONST2
Xor ebp, CONST7
CONST11
Sub edx, CONST8

Объявлять equ надо в начале файла там же где объявляются структуры и константы.

Макросы.
Макрос - это набор команд. С помощью equ мы могли создавать "прозвище" только для одной команды, а с помощью макросов можно создавать "прозвище" для нескольких команд. Для создания макроса надо использовать директивы macro и соответственно endm.

Firstmacro macro
Sub ebp, esp
Mov eax, ebp
Endm

Теперь если компилятор встречает слово Firstmacro, то он автоматически заменяет его на тело макроса. Также макросу можно передавать параметры.

Secondmacro macro param1, param2
Add edi, param1
Sub esi, param2
endm
.code ; использование
………………………
Secondmacro 55h,edx

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

Включаемые файлы.
Иногда надоедает (особенно при создании оконных приложений) объявлять в каждой программе одни и те же структуры и константы. Хотелось бы один раз их задать, а потом их использовать в каждой своей программе как в Си или Паскале. Для этого предназначены включаемые файлы *.inc. В них можно задать структуры, константы, макросы. Для включения такого файла надо использовать директиву include. После этой директивы надо указать путь к включаемому файлу абсолютный или относительный. При написании такого файла просто думайте, что вы находитесь после директивы .model и до .data.

Пример:

Файл sample.asm
;=======[CUT HERE]========
.386
.model flat, stdcall

include sample.inc

extrn MessageBoxA:PROC

.data

Msg db "First ASSEMBLER program",0h
Ttl db 'Hello, World!!!!',0h
RCTNGL RECT ?
.code

start:
call MessageBoxA,0,offset Msg,offset Ttl,MB_OKCANCEL
call ExitProcess, 0
end start
;=====[CUT HERE]==========
Файл sample.inc
;=======[CUT HERE]========
extrn ExitProcess:PROC

UINT EQU <DD>; 32 bits for WIN32

RECT struc
rcLeft UINT ?
rcTop UINT ?
rcRight UINT ?
rcBottom UINT ?
RECT ends

MB_OK = 0000H
MB_OKCANCEL = 0001H
MB_ABORTRETRYIGNORE = 0002H
MB_YESNOCANCEL = 0003H
MB_YESNO = 0004H
MB_RETRYCANCEL = 0005H
;=====[CUT HERE]===========

Думаю что с включаемыми файлами всё понятно. Директива include заменяется на всё содержимое включаемого файла.

Упрощённый вызов API функций в TASM.
Если мы укажем в директиве .model укажем модель вызова функций. То вызывать API будет намного проще

Call <функция>, <параметр1>,<параметр2>,<параметр3>

Теперь наша первая программа будет иметь такой вид:
.386
.model flat, stdcall

extrn MessageBoxA:PROC
extrn ExitProcess:PROC
.data

Msg db "First ASSEMBLER program",0h
Ttl db 'Hello, World!!!!',0h

.code

start:
call MessageBoxA,0,offset Msg,offset Ttl,0
call ExitProcess, 0
end start

Команда

call func, param1,pram2, paramn

при компиляции автоматически преобразуется в

Push paramn
Push param2
Push param1
Call func

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

Реализация конструкции if.
Если у вас есть опыт в языках программирования, возможно, вы видели что-то вроде if/else конструкций:

.IF eax==1
;eax равен 1
.ELSEIF eax=3
; eax равен 3
.ELSE
; eax не равен 1 и 3
.ENDIF

Эта конструкция очень полезна. Вам не нужно вставлять сравнения и переходы, а только вставте директиву .IF (не забудьте точку перед .IF и .ELSE и т.д.). Директива .endif нужна для определения ещё одного сравнения, если предыдущие сравнения были ложгыми. Инструкции после директивы .else выполняются только в том случае, если все сравнения были ложными. Вложенности if позволяются:

.IF eax==1
.IF ecx!=2
; eax= 1 и ecx не равно 2
.ENDIF
.ENDIF

Это может быть сделано проще:

.IF (eax==1 && ecx!=2)
; eax = 1 и ecx не равно 2
.ENDIF

А вот и операторы, которые вы можете использовать:
== | равно
!= | не равно
> | больше
< | меньше
>= | больше или равно
<= | меньше или равно
& | проверка бита
! | инверсия ( NOT )
&& | логическое 'И' ( AND )
|| | логическое 'ИЛИ' ( OR )
CARRY? | флаг переноса (cf) установле
OVERFLOW? | флаг переполнения (of) установлен
PARITY | флаг паритета (pf) установлен
SIGN? | флаг знака (sf) установлен
ZERO? | флаг нуля (zf) установлен

Такая конструкция есть и в TASM и в MASM.

Вот и кончился очередной урок. На следующем уроке я объясню ещё несколько команд ассемблера.


Assembler&Win32. Курс молодого бойца. Урок 11.

Ещё о командах ассемблера.

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

Функции сдвига (сдвиг логический операнда влево/вправо)

SHL операнд, количество_сдвигов
SHR операнд, количество_сдвигов
SHL и SHR сдвигают биты операнда (регистр/память) влево или вправо соответственно на один разряд.

Указанное выше действие повторяется количество раз, равное значению второго операнда.

Пример:

; al = 01011011 (двоичное)
shr al, 3

Это означает: сдвиг всех битов регистра al на 3 разряда вправо. Так что al станет 00001011. Биты слева заполняются нулями, а биты справа выдвигаются. Последний выдвинутый бит, становится значением флага переноса cf.
Бит переноса это бит флагового регистра процессора. Этот регистр не такой как eax или ecx, к которому вы можете непосредственно обращаться (хотя есть опкоды, которые это делают), его содержание, зависит от результатов многих команд. Об этом я вам расскажу позже, единственное, что вы должны сейчас запомнить, это то, что флаг переноса это бит во флаговом регистре и что он может быть установлен (т.е. равен 1) или сброшен (равен 0).
Команда shl такая же, как и shr, но сдвигает влево.

; bl = 11100101 (двоичное)
shl bl, 2

После выполнения команды регистр bl будет равен 10010100 (двоичное). Два последних бита заполнились нулями, флаг переноса установлен, потому, что последний выдвинутый слева бит был равен 1

Здесь есть еще два других опкода: (сдвиг арифметический операнда влево/вправо)

SAL операнд, количество_сдвигов
SAR операнд, количество_сдвигов

Команда SAL такая же, как SHL, а вот SAR не совсем такая, как SHR. Команда SAR также, как и SHR сдвигает все биты операнда вправо на один разряд, при этом выдвигаемый справа бит становится значением флага переноса cf. Обратите внимание: одновременно слева в операнд вдвигается не нулевой бит, как в SHR, а значение старшего бита операнда. Пример:

al = 10100110
sar al, 3
al = 11110100
sar al, 2
al = 11111101

bl = 00100110
sar bl, 3
bl = 00000010

Циклический сдвиг

rol операнд, количество_сдвигов ; циклический сдвиг операнда влево
ror операнд, количество_сдвигов ; циклический сдвиг операнда вправо
rcl операнд, количество_сдвигов ; циклический сдвиг операнда влево через флаг переноса
rcr операнд, количество_сдвигов ; циклический сдвиг операнда вправо через флаг переноса

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

Пример: команды ror (циклический сдвиг вправо)
 | бит 7 | бит 6 | бит 5 | бит 4 | бит 3 | бит 2 | бит 1 | бит 0 | выдвигается
Операнд | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
ror операнд,3 |  |  |  | 1 | 0 | 0 | 1 | 1 | 011
Результат | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 |

Как видно из рисунка выше, биты вращаются, то есть каждый бит, который выталкивается снова вставляется с другой стороны. Флаг переноса cf содержит значение последнего выдвинутого бита.
RCL и RCR сдвигают все биты операнда влево (для RCL) или вправо (для RCR) на один разряд, при этом старший(для RCL) или младший(для RCR) бит становится значением флага переноса cf; одновременно старое значение флага переноса cf вдвигается в операнд справа(для RCL) или слева(для RCR) и становится значением младшего(для RCL) или старшего(для RCR) бита операнда. Указанные действия повторяются количество раз, равное значению второго операнда.
ROL и ROR сдвигают все биты операнда влево(для ROL) или вправо(для ROR) на один разряд, при этом старший(для ROL) или младший(для ROR) бит операнда вдвигается в операнд справа(для ROL) или слева(для ROR) и становится значением младшего(для ROL) или старшего(для ROR) бита операнда; одновременно выдвигаемый бит становится значением флага переноса cf. Указанные действия повторяются количество раз, равное значению второго операнда.

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

Push eax
Push ebx
Push ecx
[также все остальные регистры]
……
[вытаскиваем оттуда остальные регистры]
Pop ecx
Pop ebx
Pop eax

Такой подход очень громоздок именно для этого и создана команда pushad. Она сохраняет все расширенные регистры в стеке. Соответственно команда popad нужна для извлечения из стека всех регистров.

Pushad
……..
Popad

Также есть команды для сохранения младших регистров pushaw и popaw. Они равносильны командам PUSH AX, CX, DX, BX, SP, BP, SI, DI и POP DI,SI,BP,BX,DX,CX,AX.
Иногда надо сохранять значения флагов после выполнения некоторой операции. Для этого нужны команды pushf и popf. Эти команды сохраняют в стеке значение регистра флагов или восстанавливают его оттуда.
Изменение флагов процессора.
CLC установка CF=0
CLD установка DF=0
CLI установка IF=0
LAHF загрузка регистра AH из регистра флагов: устанавливает биты 7, 6, 4, 2 и 0 по значениям флагов SF ZF AF PF CF.
SAHF запись регистра AH в регистр флагов: устанавливает флаги SF ZF AF PF CF по битам 7, 6, 4, 2 и 0 регистра AH.
STC установка CF=1
STD установка DF=1
STI установка IF=1

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


Assembler&Win32. Курс молодого бойца. Урок 12.

Обработка блоков данных

Загрузка и выгрузка данных.
Команды lodsb, lodsw, lodsd нужны, для того чтобы загрузить байт (слово, двойное слово) из памяти, на которую указывает регистр ESI в регистр al,ax или eax. После присваивания значения регистру значение регистра ESI увеличивается на 1,2 или 4. Вот эквиваленты этих команд

Lodsb Mov al, byte ptr [esi]
Inc esi /dec esi
Lodsw Mov ax, word ptr [esi]
Add esi, 2h /sub esi, 2h
Lodsd Mov eax, dword ptr [esi]
Add esi, 4h / sub esi, 4h

Следовательно, эти команды облегчают нам обработку данных, сразу увеличивая значение регистра esi для следующей порции данных. Регистр может также уменьшаться в зависимости от флага направления DF. Если он выставлен то регистр будет уменьшаться, а если он сброшен, то регистр esi будет увеличиваться.
Абсолютно, противоположное делают команды stosb, stows и stosd. Они загружают регистр al, ax, eax в память, на которую указывает регистр edi. Изменение регистра edi зависит от того же самого флага направления DF.

movsb Mov al, byte ptr [esi]
Mov byte ptr [edi], al
Inc edi /dec edi
Inc esi /dec esi
stosw Mov ax, word ptr [esi]
Mov word ptr [edi], ax
Add edi, 2h /sub edi, 2h
Add esi, 2h /sub esi, 2h
stosd Mov eax, dword ptr [esi]
Mov dword ptr [edi], eax
Add edi, 4h / sub edi, 4h
Add esi, 4h / sub esi, 4h

Сравнение данных.
Команды scasb, scasw и scasd сравнивают содержимое регистра al, ax, eax с содержимым памяти на которую указывает регистр edi. Разумеется с увеличением регистра edi. Более продвинутые команды cmpsb, cmpsw, cmpsd сравнивают значения памяти, на которые указывают регистры esi и edi соответственно. С соответствующим изменением регистра флагов.

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

Mov ecx, 045h
Mov edi, 0422310h
Mov al, 088h
Rep stosb

После этого память в диапазоне 0422310h-0422355h будет заполнена значениями 88h. Для команд cmps* и scas* префикс повторяет команду только до тех пор, пока флаг ZF установлен. Префиксы repe, repz это тоже самое. Если после очередного выполнения команды флаг ZF сбросился, то повторы прекращаются.
Не менее полезен префикс repne, он будет повторять команду пока флаг ZF сброшен. Если после очередного выполнения команды флаг ZF установился, то повторы прекращаются. То же самое делает префикс repnz.

Str1 db 'qwerty'
Str2 db '111111'
……
Mov ecx, 06h
Mov esi, offset str1
Mov edi, offset str2
Rep movsb

После всех этих манипуляций данные будут такими:

Str1 db 'qwerty'
Str2 db 'qwerty'

Ещё пример:

Str1 db 'qwerty'
Str2 db 'qwe111'
……
Mov ecx, 06h
Mov esi, offset str1
Mov edi, offset str2
Rep cmpsb

Это повторение закончится на 4 шаге при сравнении 'r' и '1' т.е. при сравнении 'r' и '1' флаг ZF сбросится и повторение прекратится. Тем самым можно сравнивать две строки а после Rep cmpsb проверять равен ли регистр ECX нулю, если он равен нулю, то строки равны.

Str1 db 'qwerty'
Str2 db '1111t1'
……
Mov ecx, 06h
Mov esi, offset str1
Mov edi, offset str2
Repne cmpsb

Это повторение прекратится на 5 шаге при сравнении 't' и 't' т.е. при их сравнении флаг ZF установится и повторение прекратится.

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


Assembler&Win32. Курс молодого бойца. Урок 13.

Компилятор MASM

На всех предыдущих уроках я рассказывал о компиляторе TASM, потому что он стандартный и ненавороченный. Но более популярный компилятор это MASM, он создан специально для написания программ на ассемблере для Win32. В нём есть макросы и специальные директивы для упрощения программирования.

Функции.
Основное преимущество MASM это макрос invoke, он позволяет вызывать API функции по-обычному с проверкой количества и типа параметров. Это почти тот же call, как в TASM, но этот макрос проверяет количество параметров и их типы. Вот так вызывается функция:
Invoke <функция>, <параметр1>, <параметр2>, <параметр3>

Чтобы использовать invoke для вызова процедуры, вы должны определить ее прототип:

PROTO STDCALL testproc:DWORD, :DWORD, :DWORD

Эта директива объявляет процедуру, названную testproc, которая берет 3 параметра размером DWORD.
Теперь, если вы сделаете это...

invoke testproc, 1, 2, 3, 4

...masm выдаст вам ошибку потому, что процедура testproc берет 3 параметра, а не 4. Masm также имеет контроль соответствия типов, т.е. проверяет, имеют ли параметры правильный тип (размер).

В invoke вы можете использовать ADDR вместо OFFSET. Это сделает адрес в правильной форме, когда код будет собран.

testproc PROTO STDCALL :DWORD, :DWORD, :DWORD
.code
testproc proc param1:DWORD, param2:DWORD, param3:DWORD
……….
ret
testproc endp

Это создает процедуру, названную testproc, с тремя параметрами. Прототип используется, invoke. Все параметры можно использовать в коде процедуры, они автоматически извлекутся из стека. Также в процедурах можно использовать локальные переменные.

testproc proc param1:DWORD, param2:DWORD, param3:DWORD
LOCAL var1:DWORD
LOCAL var2:BYTE
mov ecx, param1
mov var2, cl
mov edx, param2
mov eax, param3
mov var1, eax
add edx, eax
mul eax, ecx
mov ebx, var1
.IF bl==var2
xor eax, eax
.ENDIF
ret
testproc endp

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

Конструкции сравнения и повтора.

If
Об этой конструкции я рассказывал на 10 уроке. Она имеет тот же самый синтаксис, что и в TASM.

Repeat
Эта конструкция выполняет блок, пока условие не истинно:

.REPEAT
; код здесь
.UNTIL eax==1

Эта конструкция повторяет код между repeat и until, пока eax не станет равным 1.

While
Конструкция while это инверсия конструкции repeat. Она выполняет блок, пока условие истинно:

.WHILE eax==1
; код здесь
.ENDW

Вы можете использовать директиву .BREAK, чтобы прервать цикл и выйти.

.WHILE edx==1
inc eax
.IF eax==7
.BREAK
.ENDIF
.ENDW
Если eax=7, цикл while будет прерван.
Директива continue осуществляет переход на код проверяющий условие цикла в конструкциях repeat и while.Теперь наша первая программа видоизменяется следующим образом:

.486
.model flat, stdcall
option casemap :none

includelib .\masm32\lib\kernel32.lib
includelib .\masm32\lib\user32.lib
MessageBoxA PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD
ExitProcess PROTO STDCALL :DWORD

.data
ttl db '11111',0
.code
start:
invoke MessageBoxA,0,offset ttl,offset ttl,0
invoke ExitProcess,0
end start

При компиляции в TASM пути к статическим библиотекам мы указывали при компиляции, в MASM пути к статическим библиотекам мы указываем в тексте программы точно так же как и пути к включаемым файлам с помощью директивы includelib.
Прототипы каждой из библиотек есть в одноимённых включаемых файлах в папке include. Теперь не надо писать прототипы функций самому, они уже есть:
includelib .\masm32\lib\kernel32.lib
includelib .\masm32\lib\user32.lib
include .\masm32\include\kernel32.inc
include .\masm32\include\user32.inc
В этих включаемых файлах определены функции без букв A или W в конце. Теперь не надо указывать эти буквы в конце.
Директива option нужна для задания некоторых настроек компиляции. Опция casemap задаёт чувствительность к регистру символов. Мы указали none, тем самым установили чувствительность к регистру символов. Это надо, для того чтобы избежать конфликтов включаемых файлов от разных авторов.

Компиляция.

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

ML [ /опции ] filelist [ /link linkoptions ]
/c | ассемблирование без линковки В основном вы будете использовать эту опцию, так как вы будете использовать внешний линкер (например link.exe), для компоновки ваших файлов.
/coff | генерировать объектный файл в COFF формате Это генерирует формат файла для компоновщика microsoft.
/Fo<file> | имя объектного файла может использоваться, если вы хотите, чтобы выходной файл был с другим именем, не таким как исходный файл.
/G<c|d|z> | Использует вызовы Pascal, C, или Stdcall выберите тип вызовов для ваших процедур.
/Zi | Добавить символьную отладочную информацию Установите эту опцию, если хотите использовать отладчик.
/I<name> | Установить include путь Определяет ваш include путь

Линковка:

LINK [опции] [файлы] [@commandfile]
/DEBUG | Отладка Это создаст информацию для отладки. Используйте эту опцию, когда вы хотите использовать отладчик.
DEBUGTYPE:CV|COFF | Тип отладки: codeview / coff Выбирает выходной формат отладочной информации. Это зависит от вашего отладчика. Softice и visual c++ отладчики оба могут обрабатывать CV (codeview)
/DEF:имя_файла | DEF файл Указывает файл определения (.def). Используется с dll, для экспортируемых функций.
/DLL | DLL Выходной файл DLL, а не EXE.
/LIBPATH:path | Путь к библиотекам Указывает путь к файлам библиотек (*.lib).
/I<имя> | Устанавливает путь для inc-файлов Указывает путь для inc-файлов, по умолчанию.
/OUT:имя_файла | Out:имя_файла Может изменить имя выходного файла.
/SUBSYSTEM:{...} | Подсистема Выбирает ОС на которой должна выполнятся программа: NATIVE|WINDOWS|CONSOLE|WINDOWSCE|POSIX

Я установил masm в папку d:\masm\masm32, в папке d:\masm находятся все тексты наших программ *.asm. А для компиляции я создал bat файл, который компилирует файл current.asm:
;======[CUT HERE]=====
d:\masm\masm32\bin\ml.exe /c /coff current.asm
d:\masm\masm32\bin\link.exe /subsystem:windows current.obj
pause
;=======[CUT HERE]========
В результате после запуска это файл у нас в папке d:\masm готовая скомпилированная программа current.exe.

Вот и конец этого урока. В этом уроке я рассказал про макрософтский компилятор MASM. На следующем уроке мы напишем первое оконное приложение на ассемблере.


Assembler&Win32. Курс молодого бойца. Урок 14.

Оконное приложение

На этом уроке мы напишем оконное приложение на ассемблере. Я буду писать его на MASM. Это приложение я постарался сделать наиболее минимальным и понятным.

.386p
.model flat, stdcall
option casemap:none

includelib .\masm32\lib\kernel32.lib
includelib .\masm32\lib\user32.lib

include .\masm32\include\windows.inc
include .\masm32\include\kernel32.inc
include .\masm32\include\user32.inc

.data

newhwnd dd 0
hInst dd 00000000h
szTitleName db 'Window Application',0
szClassName db 'ASMCLASS32',0
msg MONMSGSTRUCT >; структура сообщения
wc WNDCLASS >; структура класса

.code
start:
Invoke GetModuleHandle,0 ; получаем hInstanse
Mov [hInst], eax

Mov [wc.style], CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
; устанавливаем стиль окна
Mov [wc.lpfnWndProc], offset WndProc ;
Mov [wc.cbClsExtra], 0
Mov [wc.cbWndExtra], 0
Mov eax, [hInst]
Mov [wc.hInstance], eax
Invoke LoadIcon,0,IDI_APPLICATION ; получаем значок приложения по
; умолчанию
Mov [wc.hIcon], eax

Invoke LoadCursorA,0,IDC_ARROW ; получаем курсор по умолчанию
Mov [wc.hCursor], eax
Mov [wc.hbrBackground], COLOR_BACKGROUND+1
Mov dword ptr [wc.lpszMenuName], 0
Mov dword ptr [wc.lpszClassName], offset szClassName ; задаём имя класса
;окна
Invoke RegisterClassA,offset wc ; регистрируем класс окна

Push 0
Push [hInst] ; дескриптор
Push 0
Push 0
Push CW_USEDEFAULT ; высота
Push CW_USEDEFAULT ; ширина
Push CW_USEDEFAULT ; y
Push CW_USEDEFAULT ; x
Push WS_OVERLAPPEDWINDOW ; стиль
Push offset szTitleName ; заголовок окна
Push offset szClassName ; имя класса
Push 0 ; дополнительный стиль
Call CreateWindowEx ; создаём окно
mov [newhwnd], eax ; сохраняем его дескриптор
invoke ShowWindow,[newhwnd],SW_SHOWNORMAL; показываем окно
invoke UpdateWindow, [newhwnd]; обновляем его
msg_loop: ; запускаем цикл обработки сообщений
invoke GetMessage,offset msg, 0,0,0
cmp ax, 0
je end_loop
invoke TranslateMessage, offset msg
invoke DispatchMessage, offset msg
jmp msg_loop
end_loop:
invoke ExitProcess, 0

WndProc proc uses ebx edi esi, hwnd:DWORD, wmsg:DWORD, wparam:DWORD, lparam:DWORD

Cmp [wmsg], WM_DESTROY
Je wmdestroy
Cmp [wmsg], WM_KEYDOWN
Je wmkeydown
Invoke DefWindowProcA,[hwnd],[wmsg],[wparam],[lparam]
; вызываем стандартный обработчик сообщений Jmp finish
wmkeydown:
cmp [wparam], VK_ESCAPE
je wmdestroy; если нажата клавиша Escape то выход
jmp finish
wmdestroy:
invoke PostQuitMessage, 0
invoke ExitProcess, 0 ; выход
finish:
ret
WndProc endp
end start

В самой первой строке мы получаем hInstanse т.е хендл приложения, фактически это база образа приложения, т.е. то место в памяти в которого начинается наша программа. Не пойму, зачем именно это нужно, но это зачем-то нужно. Функция GetModuleHandle нужна для получения хендла библиотеки (фактически хендл библиотеки это и есть её база образа), параметром этой функции надо передавать имя библиотеки, но если мы укажем 0, то получим хендл нашего приложения. Потом мы заполняем структуру класса окна, т.е. задаём атрибуты класса окна.

Mov [wc.lpfnWndProc], offset WndProc

Этой строкой мы указываем, какая функция будет обрабатывать сообщения окна. Функция, которая обрабатывает сообщения окна, называется оконной функцией. Оконная функция вызывается всякий раз, когда окну было прислано какое-либо сообщение.
Потом мы получаем иконку окна с помощью функции LoadIcon, в качестве первого параметра этой функции надо передавать хендл приложения, но мы указали 0 поэтому мы получим стандартный значок окна. Во втором параметре мы указываем указатель строку, которая содержит имя иконки, которую мы хотим получить, но если мы укажем определённую константу, то сможем получить предопределённую иконку. Точно также мы получаем курсор, т.е. его хендл.
Потом мы вызываем RegisterClass, единственным параметром которой является указатель на структуру, которая описывает новый класс окна.
Потом мы создаём окно с помощью функции CreateWindowEx.
HWND CreateWindowEx(
DWORD dwExStyle, // расширенный стиль окна
LPCTSTR lpClassName, // имя класса
LPCTSTR lpWindowName, // заголовок окна
DWORD dwStyle, // стиль окна
int x, // позиция х
int y, // позиция у
int nWidth, // ширина
int nHeight, // высота
HWND hWndParent, // хендл окна родителя
HMENU hMenu, // хендл меню
HINSTANCE hInstance, // хендл приложения
LPVOID lpParam //указатель на данные (обычно они не нужны)
);
После создания окна мы показываем его и обновляем. Потом запускаем бесконечный цикл обработки системных сообщений.
В оконной функции мы получаем сообщение и обрабатываем его. Если тип сообщения WM_DESTROY, то мы прыгаем на соответствующую метку, после которой вы выходим из нашей программы. Если была нажата клавиша, то мы продолжаем обработку и смотрим, нажата ли клавиша escape, если да, то выходим. Если сообщение, какое либо другое, то вызываем стандартный обработчик сообщений окна. Этот обработчик превращает наше окно в настоящее "живое" окно, которое может двигаться, расширятся и свёртываться. Можно и не вызывать эту функцию, но для того чтобы окно стало живым нам придётся вручную обрабатывать все сообщения окна, а это просто нецелесообразно.

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


Assembler&Win32. Курс молодого бойца. Урок 15.

Некоторые функции Win32.

На прошлом уроке мы написали первое оконное приложение на ассемблере. На прошлом уроке я вам рассказал структуру ЛЮБОГО оконного приложения. Сегодня я вам расскажу (т.е. опишу) несколько функций API.

Как я вам уже рассказывал, в Win32 есть только API функции. ЛЮБОЕ приложение в ЛЮБОМ случае должно использовать АПИ функции (по крайней мере, для того чтобы оно могло нормально завершиться). Эти функции находятся в библиотеках kernel32.dll, user32.dll, gdi32.dll (3 классические библиотеки) и др. Функции библиотеки kernel32.dll самые главные, они отвечают за работу с файлами памятью, от них никуда не денешься, user32.dll отвечает за окна и интерфейс пользователя, а gdi32.dll за "рисование" и мультимедиа. Для того, что бы использовать функции этих библиотек надо сначала их загрузить (kernel32.dll загружать не надо).

Каждое приложение проецируется в своё собственное виртуальное адресное пространство размером в 4 ГБ, верхние 2 из которых недоступны для него. Это пространство (вернее нижние 2 ГБ) подготавливается для него: в него загружаются 2 библиотеки kernel32.dll и ntdll.dll (для Win NT) всегда по одинаковым адресам, подготавливается стек для главной нити процесса и ещё много чего. В эту память загружаются код и данные приложения. Для того, что бы операционная система знала по каким адресам надо загружать данные и код приложения она смотрит дополнительную информацию о секциях приложения в "экзе" файле. Если она этого не сделает, то она просто не будет знать, куда спроецировать код и данные, следовательно, все обращения к памяти будут неверными. (Мы, конечно, помним что под записью mov eax, [value] понимается помещение в регистр eax содержимого некого адреса памяти, которую подразумевает метка value, эта метка подразумевает некий фиксированный адрес, все адреса рассчитываются при компиляции программы и вам об этом задумываться не надо).

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

Call VirtualAalloc

Как мы уже знаем слово VirtualAlloc будет заменено на некоторый адрес. Этот адрес будет соответствовать адресу точки входа функции VirtualAlloc. Потом эта функция передаёт управление функциям Native API которые находятся в библиотеке ntdll.dll. Функции Native API подготавливают процесс к переходу в режим ядра и командой sysenter переводят процесс в режим ядра и т.д. Дальше я не буду рассказывать, потому что это слишком много для одного урока.

Теперь я опишу несколько API функций для работы с памятью.

Функция VirtualAlloc позволяет зарезервировать некоторый участок памяти заданного размера, при этом эта память полностью обнуляется.
Вот описание этой функции из MS SDK в оригинале
LPVOID VirtualAlloc(
LPVOID lpAddress, // address of region to reserve or commit
DWORD dwSize, // size of region
DWORD flAllocationType, // type of allocation
DWORD flProtect // type of access protection
);
lpAddress -начальный адрес региона
dwSize - размер региона flAllocationType - тип резервирования может быть указана одна из 3х констант :
MEM_COMMIT эта память может быть сброшена в файл подкачки
MEM_RESERVE этот участок памяти не может быть сброшен в файл подкачки он будет присутствовать в физической памяти всегда, пока эту память процесс не ввысвободит.
MEM_TOP_DOWN не пойму для чего нужна эта константа, но она есть, она позволяет зарезервировать виртуальную память, которая будет соответствовать максимально возможному физическому адресу.
flProtect тип доступа к памяти. Могут быть использованы эти константы или их комбинации.
PAGE_READONLY
PAGE_READWRITE
PAGE_EXECUTE позволяет передавать управление этому региону попытка чтения или записи приводит к нарушению прав доступа.
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
PAGE_NOACCESS
Мне кажется всё и так понятно, кому не понятно читайте MS SDK.
Функция VirtualFree обратна функции VirtualAlloc высвобождает заданный диапазон памяти.
BOOL VirtualFree(
LPVOID lpAddress, // начальный адрес освобождаемого региона
DWORD dwSize, // размер региона
DWORD dwFreeType // type of free operation
);
dwFreeType метод высвобождения:
если укажете MEM_RELEASE то размер указывать не надо, если MEM_DECOMMIT, то размер надо указывать остальных различий, если указано MEM_RELEASE, то эта память высвобождается полностью.
Если функция завершилась нормально, то результат отличен от нуля.
Функция VirtualProtect позволяет изменить тип доступа к зарезервированной памяти, но не так всё просто надо ещё указать старый тип доступа.
BOOL VirtualProtect(
LPVOID lpAddress, // начальный адрес
DWORD dwSize, // размер
DWORD flNewProtect, // новый тип доступа
PDWORD lpflOldProtect // старый тип доступа
);
Вроде всё понятно.
Функция VirtualLock блокирует участок памяти, при этом доступ к нему невозможен и одновременно не приводит к ошибке.
BOOL VirtualLock(
LPVOID lpAddress, // начальный адрес
DWORD dwSize // размер
);
При удачном исходе результат не равен нулю.
Функция VirtualUnlock обратная предыдущей функции.
BOOL VirtualUnlock(
LPVOID lpAddress,
DWORD dwSize
);
При удачном исходе результат не равен нулю.
Все указанные выше функции могут работать только с памятью текущего процесса.
На сегодня хватит. Для этих целей существует MS SDK. Я объяснил только несколько функций.

Вот и конец 15 урока моего туториала.