Ассемблер
Win32Asm Tutorial |
назад | 1 - Ассемблер | вперед |
1.0 - Ассемблер
Ассемблер создан как замена двоичному коду, который понимает процессор. Давным-давно, когда еще не было никаких языков программирования высокого уровня, программы создавались на ассемблере. Коды ассемблера представляют непосредственно команды процессора, которые он может выполнять. Для примера:
add eax, edx
Эта инструкция, add, складывает вместе два значения. Eax и edx называются регистрами, они могут содержать значения, которые хранятся внутри процессора. Этот код будет преобразован в 66 03 C2 (шестнадцатиричный код). Процессор читает эти коды, и выполняет операцию, которую они представляют. Языки высокого уровня, например С, при компиляции преобразовывается в ассемблер, а ассемблер в двоичный код:
код C | >>Компилятор>> | Ассемблер | >>Ассемблер>> | выходной код (шестнадцатиричный) | ||
a = a + b; | add eax, edx | 66 03 C2 |
(Учтите, что ассемблерный код упрощен, выходной код будет зависеть от контекста С кода)
1.1 - Почему?
Зачем нам использовать ассемблер, а не C и что-нибудь другое, если писать на ассемблере сложнее. Программы написаные на ассемблере быстрые и имеют очень маленькие размеры. В языках программирования высокого уровня компилятору сложнее производить выходной код. Компилятор должен выяснить метод оптимизации (по скорости или по размеру), для производства ассемблерного кода, и несмотря на то, что компиляторы постоянно улучшаются, они программируют код самостоятельно (с опцией оптимизации кода) стараясь производить как можно меньший и более быстрый код (но у них это не очень хорошо получается).
Примечание от переводчика:
Я провел эксперимент, написал программу (обычное окно с одной кнопкой в центре, которая закрывает его) на разных языках высокого уровня и после компиляции получил вот такие размеры этой самой программы:
C++ Builder 4 - 22 kb
Delphi 5 - 291 kb
Delphi 5 + библиотека KOL - 26 kb
Ассемблер MASM - около 3 kb
Так, что смотрите и делайте выводы, что лучше использовать.
Есть другое различие с некоторыми языками высокого уровня, это использование runtime DLL библиотек для их функций. Например, Visual C++ имеет библиотеку msvcrt.dll которая содержит стандартные С функции. В большинстве случаев, это работает хорошо, но иногда возникают проблемы с версиями dll. У пользователя всегда должны быть установленны эти DLL-библиотеки (что не всегда бывает). Для Visual C это не проблема, они устанавливаются вместе с windows.
Visual Basic даже не преобразовывает свой язык в ассемблер (хотя версия 5 и выше делают это частично, но не полностью), это сильно зависит от msvbvm50.dll, виртуальной машины Visual Basic. Exe файл, который создан VB состоит из простых частей кода и многих вызовов этой DLL. Вот почему VB очень медленный. Ассемблер это и есть самый быстрый язык. Он использует только системные DLL: kernel32.dll, user32.dll, и т.д.
Другое непонимание в том, что многие люди думают, что на ассемблере невозможно программировать. Безусловно, это сложно, но очень даже возможно. Создавать большие проекты на ассемблере, это довольно сложно, я использую его для создания маленьких программ, DLL библиотек и частей кода, где требуется скорость. Также, есть большое различие между DOS и Windows программами. DOS программы мспользуют прерывание как 'функции'. Подобно int 10 для видео, int 13 для доступа к файлам и т.д. В win, есть API, приложения программирующие интерфейс. Этот интерфейс состоит из функций, которые вы можете использовать в своих программах. В DOS программах, прерывания имеют номер прерывания и номер функции. В Win, API функции имеют только имена (например: MessageBox, CreateWindowEx). Вы можете импортировать библиотеки (DLL) и использовать функции внутри них. Это облегчает программирование на ассемблере. Подробнее об этом в следующих уроках.
[наверх]
Начало
Win32Asm Tutorial |
назад | 2 - Начало | вперед |
2.0 - Начало
Достаточно введения, давайте начнем. Чтобы программировать на ассемблере, вам будут нужны некоторые инструменты. Ниже вы увидете, какие инструменты я буду использовать в этом учебнике. Я советую вам установить те же самые инструменты, чтобы вы могли следовать учебнику и пробовать примеры. Я также дал некоторые вырианты, для большинства инструментальных средств, вы можете выбрать вариант для этого учебника, но предупреждаю, что между ассемблерами (masm, tasm и nasm) имееются большие различия. В этом учебнике будет использоваться MASM, благодаря его полезным функциям (таким как invoke и т.д.), которые намного упрощают программирование. Конечно вы свободно можете использовать любой ассемблер, который предпочитаете, но будет тяжелее следовать этому учебнику и вам нужно будет преобразовывать примеры, чтобы они работали с вашим ассемблером.
Ассемблер | |
Использованный: Masm (из пакета win32asm ) Расположение: Win32asm.cjb.net Описание: Конвертирует исходный текст ассемблера (опкоды) в объектный файл для процессора. Описание: Masm, макроассемблер, является ассемблером с несколькими полезными особенностями, подобно 'invoke', которые облегчают вызовы функций API и проверяет типы данных, вы поимете это позже в этом учебнике. Если вы прочитали текст выше, то знаете, что в этом учебнике используют MASM. | |
Выбор: Tasm [dl], nasm [dl] |
Линкер | |
Использованный: Microsoft Incremental Linker (link.exe) Расположение: Win32asm.cjb.net (in the win32asm package) Описание: компоновщик 'связывает' все объектные файлы и библиотеки (для импорта DLL) вместе, чтобы произвести конечную выполнимую программу. Описание: я буду использовать link.exe, который является доступным в пакете win32asm на странице Iczelion'а, но можно использовать и другие. | |
Выбор: Tasm linker [dl] |
Редактор ресурсов | |
Использованный: Borland Resource Workshop Расположение: www.crackstore.com Описание: редактор ресурсов используется для создания ресурсов (изображения, диалоги, точечные рисунки, меню). Описание: Подойдет большинство редакторов, лично я предпочитаю Borland Resource Workshop, но вы можете использовать то, что хотите. Учтите: файлы ресурса, созданные с Resource Workshop иногда вызывают проблемы с компилированием ресурса, если Вы хотите использовать этот редактор, Вы также должны загрузить tasm, который содержит brc32.exe для компилирования ресурсов стиля Borland. | |
Выбор: Symantec Resource Editor, Resource Builder, и многие другие |
Текстовый редактор |
Используемый: Ultraedit Расположение: www.ultraedit.com Описание: Текстовый редактор нуждается в описании? О: Выбор текстового редактора это кому как, лично мне очень нравится Ultraedit. Вы можете скачать мой wordfile для ultraedit, тогда вы получите подсветку синтаксиса ассемблера. По крайней мере выберите текстовый редактор с подсветкой синтаксиса (ключевые слова будут автоматически окрашены), это очень полезная функция, делает ваш код более легким для чтения и записи. Ultraedit также имеет список функций, для быстрой вставки в ваш код. [download wordfile here] |
Выбор: Один из миллионов текстовых редакторов [здесь] |
References |
Используемый:Справочник програмиста "Win32 Programmer's reference" Расположение: www.crackstore.com (или поищите в интернете) Описание: Вам нужны будут некоторые справки по функциям Win API. Наиболее нужный справочник программиста "win32 programmer's reference" (win32.hlp). Это большой файл, около 24 mb (некоторые версии всего 12 mb но не полные). В этом файле описаны все функции системных DLL (kernel, user, gdi, shell и т.д.). По крайней мере вы будете нуждаться в этом файле, другие справочники (sock2.hlp, mmedia.hlp, ole.hlp и т.д.) также полезны, но не необходимы. От переводчика: Эти файлы включаются в стандартные пакеты типа Borland C++ Builder, Delphi и т.д. |
Выбор: нет |
2.1 - Установка инструментальных средств
Теперь у вас есть эти инструменты, устанавливаете их где-нибудь. Вот несколько важных замечаний:
Установите пакет masm на том же диске, на котором хотите писать ваши исходные файлы ассемблера. Это гарантирует то, что пути к файлам вложения и библиотекам будут правильными.
Добавьте каталог bin masm'а (или tasm'а) к вашему пути в autoexec.bat и перезагрузитесь.
Если у вас есть Ultraedit, используте wordfile который можете скачать выше и включите просмотр списка функций.
2.2 - Папка для ваших исходников
Создайте папку "win32" (или с другим именем) где-нибудь (на том же диске, что и masm), и создайте подпапку для каждого проекта, который вы делаете.
[наверх]
Основы ассемблера
Win32Asm Tutorial |
назад | 3 - Основы ассемблера | вперед |
3.0 - Основы ассемблера
Эти туториалы обучат вас основам ассемблера.
3.1 - Коды операции (далее опкоды)
Программы Ассемблера созданы с помощью опкодов. Опкоды это команды, которые понимает процессор. Например:
ADD
Инструкция add складывает вместе два числа. Большинство опкодов имеют операнды:
ADD eax, edx
ADD имеет 2 операнда. В случае, источника и приемника. Она добавляет значение источника к значению приемника и сохраняет результат в приемнике. Операнды могут быть разных типов: регистры, ячейки памяти, непосредственные значения (см. ниже).
3.2 - Регистры
Есть несколько размеров регистров: 8 бит, 16 бит, 32 бит (и больше на MMX процессорах). В 16-разрядных программах, вы можете использовать только 8 и 16 битные регистры. В 32-разрядных программах вы также можете использовать 32 битные регистры.
Некоторые регистры являются частью других; например, если EAX содержит значение EA7823BBh, вот то, что содержат другие регистры.
EAX | EA | 78 | 23 | BB |
AX | EA | 78 | 23 | BB |
AH | EA | 78 | 23 | BB |
AL | EA | 78 | 23 | BB |
регистры ax, ah, al - части регистра eax. Eax это 32-битный регистр (доступный только начиная с 386+), ax содержит младшие 16 бит (2 байта) регистра eax, ah содержит старший байт регистра ax, и al содержит младший байт регистра ax. Регистр ax - 16 битный, al и ah - 8 битные. Так из примера выше, вот значения регистров:
eax = EA7823BB (32-бит)
ax = 23BB (16-бит)
ah = 23 (8-бит)
al = BB (8-бит)
Пример использования регистров (не заботьтесь об опкодах, просто смотрите на регистры и описание):
mov eax, 12345678h | mov загружает значение в регистр (Обратите внимание: 12345678h это шестнадцатиричное число, т.к. в конце стоит суффикс 'h') |
mov cl, ah | копирует старший байт регистра ax (67h) в регистр cl |
sub cl, 10 | вычесть 10 (десятичное) из значения в регистре cl |
mov al, cl | и сохранить его в младшем байте регистра eax. |
Давайте исследуем вышеприведенный код:
Mov команда может перемещать значение от регистра, памяти или непосредственного значения к другому регистру. В примере выше, eax содержит 12345678h. Затем значение регистра ах (3-ий байт слева в регистре eax) копируется в регистр cl (самый младший байт регистра ecx). Далее, из регистра cl вычитается 10, и затем значение из регистра cl копируется в регистр al(самый младший байт регистра eax).
Есть различные типы регистров:
Универсальные
Эти 32-битные (и 16/8 для их компонентов) регистры могут использоваться для чего угодно:
eax (ax/ah/al) | Аккумулятор |
ebx (bx/bh/bl) | Базовый регистр |
ecx (cx/ch/cl) | регистр-счетчик |
edx (dx/dh/dl) | регистр данных |
Хотя они и имеют названия, вы можете использовать их как угодно.
Сегментные регистры
Сегментные регистры определяют сегмент памяти, которая используется. Вероятнее всего, они вам не понадобятся в win32asm, потому, что windows использует плоскую систему памяти. В dos, память разделена на сегменты по 64kb, так, что если вы хотите определить адрес памяти, вы определяете сегмент и смещение (например 0172:0500 (сегмент:смещение)). В windows, сегменты имеют размер 4Gb, так, что в сегментации в win. Сегментные регистры всегда - 16-битные.
CS | Сегмент кода |
DS | Сегмент данных |
SS | Сегмент стека |
ES | Дополнительный сегмент |
FS (только с 286+) | Универсальный сегмент |
GS (только с 386+) | Универсальный сегмент |
Регистры указателя
Фактически, вы можете использовать регистры указателя, как универсальные регистры (eip - исключение), пока вы сохраняете их первоначальные значения. Регистры указателя называются так потому, что их часто используют для сохранения адресов памяти. Некоторые опкоды (movb,scasb, и т.д.) также их используют.
esi (si) | Индекс источника |
edi (di) | Индекс приемника |
eip (ip) | Указатель команды |
Регистр EIP (или IP в 16-разрядных программах) содержит указатель на команду, которую собирается выполнить процессор. Так что вы не можете использовать регистр eip как универсальный.
Регистры указателя стека
Есть 2 стековых регистра: esp & ebp. Esp содержит текущую позицию стека в памяти (подробнее об этом в следующих уроках). Ebp используется в функциях как указатель на локальные переменные.
esp (sp) | Указатель стека |
ebp (bp) | Указатель базы кадра стека |
[наверх]
Память
Win32Asm Tutorial |
назад | 4 - Память | вперед |
4.0 - Память
Этот раздел объяснит, как устроена память в windows.
4.1 - DOS & win 3.xx
В 16-разрядных программах, как и в DOS, и в windows 3, память была разделена на сегменты. Эти сегменты имели размер 64kb. Для доступа к памяти были необходимы: указатель на сегмент и указатель смещения. Указатель сегмента указывал, какой сегмент (секцию 64kb) использовать, указатель смещения указывал непосредственно на место в сегменте. Смотрите рисунок:
Память | ||||
Сегмент 1 (64kb) | Сегмент 2 (64kb) | Сегмент 3 (64kb) | Сегмент 4(64kb) | и так далее |
Учтите, что это объяснение для 16-разрядных программ, о 32-разрядных я расскажу позже, (но не пропускайте эту часть, важно понять, что такое 32-разрядность).
Таблица выше иллюстрирует общую память, разделенную на сегменты по 64kb. Здесь максимум 65536 сегментов. Теперь возмите один из сегментов:
Сегмент 1(64kb) | |||||
Смещение 1 | Смещение 2 | Смещение 3 | Смещение 4 | Смещение 5 | и так далее |
Чтобы указать на местоположение в сегменте используется смещение. Смещение - это местоположение внутри сегмента. Здесь в сегменте максимум 65536 смещений. Запись адреса в памяти:
СЕГМЕНТ:СМЕЩЕНИЕ (SEGMENT:OFFSET)
Например:
0030:4012
(все числа шестнадцатиричные)
Это означает: сегмент 30, смещение 4012. Чтобы узнать, что находится в том адресе, вы сначала переходите на сегмент 30, а затем в сегменте смещаетесь на 4012. В предыдущих уроках мы узнали о сегментных и указательных регистрах. Например, сегментные регистры:
CS - Сегмент кода
DS - Сегмент данных
SS - Сегмент стека
ES - Дополнительный сегмент
FS - Универсальный сегмент
GS - Универсальный сегмент
Названия говорят о их функциях: сегмент кода (CS) содержит номер секции, где вылнен текущий код. Сегмент данных для получения данных из текущего сегмента. На стек указывает сегмент стека (SS) (подробнее о стеке я раскажу позже), ES, FS, GS - универсальные регистры и могут использоваться для любого сегмента (не в win32).
Регистры указателя большую часть времени содержат смещение, но и универсальные регистры (ax, bx, cx, dx и т.д.) также могут использоваться для этого. IP указывает смещение (в регистре CS (сегмент кода)) команды, которая в настоящее время выполнена. SP содержит смещение (в регистре SS (сегмент стека)) текущей позиции стека.
4.2 - 32-разрядный Windows
В 16-разрядном программировании, сегменты необходимы. К счастью, эта проблема решена в 32-разрядном Windows (95 и выше). Вы все еще имеете сегменты, но вам не нужно заботиться о них, потому, что они уже не 64kb (как в 16-разрядном), а 4 Гб. Windows вероятно даже "повиснет", если вы попытаетесь изменить один из сегментных регистров. Это называеся плоской моделью памяти (flat). Здесь есть только смещения и они теперь 32-разрядные (в диапазоне от 0 до 4,294,967,295). Каждая ячейка в памяти указывается смещением. Это действительно одно из лучших преимуществ 32-разрядного программирования над 16-разрядным. Так что теперь вы можете забыть о сегментных регистрах и сосредоточиться на других регистрах.
[наверх]
Коды операции
Win32Asm Tutorial |
назад | 5- Коды рперации | вперед |
5.0 - Коды операции (опкоды).
Опкоды это команды для процессора. Опкоды - фактически "читаемый текст"- версии шестнадцатиричных кодов. Из-за этого, ассемблер считается саммым низко-уровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатиричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.
В этом уроке мы обсудим несколько опкодов, которые имеют отношение к вычислению, поразрядным операциям, и т.д. Другие опкоды, команды перехода, сравнения и т.д, будут обсуждены позже.
5.1 - Несколько основных опкодов вычисления
MOV |
Эта команда используется для перемещения (или фактически, копирования) значения из одного места в другое. Это 'место' может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды mov:
mov приемник, источник
Вы можете перемещать значение из одного регистра в другой (Учтите, что команда копирует значение в приемник, несмотря на ее название 'move', что в переводе на русский - перемещать).
mov edx, ecx
Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми,
например: эта команда - НЕ допустима:
mov al, ecx ; не правильно
Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).
А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:
mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx
Смещение указывает на ячейки памяти (в win32, подробнее об этом см. на предыдущей странице). Вы также можете получить значение из памяти и поместить эго в регистр. Для примера возьмем следующую таблицу:
смещение | 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]
(суффикс 'h' означает шестнадцатеричное значение)
Команда 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 (см. таблицу выше, помните о порядке байт - задом на перед)
Иногда размер можно не указывать:
mov eax, [00403045h]
Так как eax - 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.
Можно также непосредственные значения:
mov edx, 5006
Эта команда просто запишет в регистр 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.
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 = остаток)
Поскольку регистры могут содержать только целочисленные значения (то есть числа, не, с плавающей запятой), результат деления разбит на частное и остаток. Например:
28 /6 --> частное = 4, остаток = 4
30 /9 --> частное = 3, остаток = 3
97 / 10 --> частное = 9, остаток = 7
18 /6 --> частное = 3, остаток = 0
Теперь, в зависимости от размера источника, частное сохраняется в 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.
Источник операции деления (div) может быть:
8-бит регистр (al, ah, cl,...)
16-бит регистр (ax, dx, ...)
32-бит регистр (eax, edx, ecx...)
8-бит значение из памяти (byte ptr [xxxx])
16-бит значение из памяти (word ptr [xxxx])
a 32-бит значение памяти (dword ptr [xxxx])
Источник не может быть непосредственным значением, потому что тогда процессор не сможет определить размер исходного операнда.
ОПЕРАЦИИ С БИТАМИ |
Эти команды работают с приемником и источником, исключение команда '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, 3406
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.
УВЕЛИЧЕНИЕ/УМЕНЬШЕНИЕ |
Есть 2 очень простые команды, DEC и INC. Эти команды увеличивают или уменьшают содержимое памяти или регистра на единицу. Просто поместите:
inc регистр -> регистр = регистр + 1
dec регистр -> регистр = регистр - 1
inc dword ptr [103405] -> значение в [103405] будет увеличено на 1.
dec dword ptr [103405] -> значение в [103405] будет уменьшено на 1.
NOP |
Эта команда не делает абсолютно ничего (пустая команда). Она только занимает пространство и время. Используется для резервирования места в сегменте кода или организации программной задержки
Биты, сдвиг логический, арифметический и циклический |
Обратите внимание: Большинство примеров ниже использует 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 операнд, количество_сдвигов (Shift Arithmetic Left)
SAR операнд, количество_сдвигов (Shift Arithmetic Right)
Команда 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 |
0 1 1 | ||||
Результат | 0 |
1 |
1 |
1 |
0 |
0 |
1 |
1 |
Как видно из рисунка выше, биты вращаются, то есть каждый бит, который выталкивается снова вставляется с другой стороны. Флаг переноса cf содержит значение последнего выдвинутого бита.
RCL и RCR фактически такие же как ROL и ROR. Их названия предлагают, что они используют флаг переноса cf, для указания последнего выдвигаемого бита, но поскольку ROL и ROR делают тот же самое, они не отличаются от них.
Примечание от переводчика: Вот тут мне кажется автор допустил ошибку относительно команд RCR и RCL, эти команды отличаются от ROR и ROL. Здесь я опишу алгоритм работы тех и других:
RCL и RCR сдвигают все биты операнда влево (для RCL) или вправо (для RCR) на один разряд, при этом старший(для RCL) или младший(для RCR) бит становится значением флага переноса cf; одновременно старое значение флага переноса cf вдвигается в операнд справа(для RCL) или слева(для RCR) и становится значением младшего(для RCL) или старшего(для RCR) бита операнда. Указанные действия повторяются количество раз, равное значению второго операнда.
ROL и ROR сдвигают все биты операнда влево(для ROL) или вправо(для ROR) на один разряд, при этом старший(для ROL) или младший(для ROR) бит операнда вдвигается в операнд справа(для ROL) или слева(для ROR) и становится значением младшего(для ROL) или старшего(для ROR) бита операнда; одновременно выдвигаемый бит становится значением флага переноса cf. Указанные действия повторяются количество раз, равное значению второго операнда.
Обмен |
Команда XCHG также весьма проста. Назначение: обмен двух значений между регистрами или между регистрами и памятью:
eax = 237h
ecx = 978h
xchg eax, ecx
eax = 978h
ecx = 237h
[наверх]
Структура файла
Win32Asm Tutorial |
назад | 6- Структура файла | вперед |
6.0 - Структура файла
Исходные файлы разделены на секции, такие как код, данные, неинициализированные данные, константы, ресурс и смещение. Секция ресурса создается из файла ресурсов, подробнее об этом я расскажу позже. Секция смещений не важна для нас (она содержит информацию, PE-загрузчика, чтобы он мог загрузить программу в любое место в памяти). Важные секции это код, данные, неинициализированные данные и константы. Секция кода содержит, рабочий код программы. Секция данных содержит данные, доступные для чтения и записи. Вся секция данных включается в exe файл и может быть инициализирована данными.
Неинициализированные данные не имеют никакого содержания при запуске, и даже не включены в exe файл непосредственно, это только часть памяти, зарезервированной windows. Эта секция доступна для чтения и записи. Секция констант такае же как секция данных, но доступна только для чтения. Хотя эта секция может использоваться для констант, проще и быстрее объявлять константы в файлах для включения, и использовать их как непосредственные значения.
6.1 Идентификаторы секций
В ваших исходных файлах (*.asm), вы объявляете секции:
.code ; начало секции кода
.data ; начало секции данных
.data? ; начало секции неинициализированных данных
.const ; начало секции констант
Исполняемые файлы (*.exe, *.dll и другие) (в win32) это файлы в PE формате. Я не буду подробно объяснять о нем, а раскажу только о некоторых важных вещах. Разделы определены в PE-заголовке с некоторыми характеристиками:
Название секции, RVA, offset, raw size, virtual size и флаги.
RVA (relative virtual address) - PE-загрузчик использует значение в этом поле, когда загружает секцию в память. Если значение в этом поле равняется 1000h, а файл файл загружен в 400000h, секция будет загружена в 401000h.
Offset это файловое смещение на начало секции. PE-загрузчик использует это значение для того, чтобы найти ее начало.
Raw size это размер секции, выравненный согласно установкам в PE-заголовке. PE-загрузчик проверяет это значение, чтобы знать, сколько байт он должен загрузить в память.
Virtual size это размер, занимаемый в памяти.
Флаги определяют характеристики (только чтение/запись/исполняемый и т.д.).
6.2 Пример программы
Имеется пример программы:
.data
Number1 dd 12033h
Number2 dw 100h,200h,300h,400h
Number3 db "blabla",0
.data?
Value dd ?
.code
mov eax, Number1
mov ecx, offset Number2
add ax, word ptr [ecx+4]
mov Value, eax
Эта программа не будет транслирована, но это не важно.
В секции данных (.data) есть 3 метки: Number1, Number2, Number3. Эти метки используются как указатели на то место в программе, где они расположены.
Директивы db, dw и dd используются для определения и инициализации основных единиц памяти, байт (db), слово (dw) и двойное слово (dd), на которую будет указывать метка стоящая перед этой директивой. Метка заменяет численное значение адреса распределенных единиц памяти символическим именем.
С директивой db, вы также можете определить строку, потому, что строка это фактически порядок байт, соответствующих символам в строке.
Секция данных (из примера выше) в памяти будет выглядеть вот-так: 33,20,01,00,00,01,00,02,00,03,00,04,62,6c,61,62,6c,61,00
(все числа шестнадцатиричные)
Я выделил цветами некоторые числа. Метка Number1 указывает на место в памяти, где синий байт 33, метка Number2 указывает на место в памяти, где красный 00, Number3 на зеленый 62.
Теперь, если вы мспользуете это в своей программе:
mov ecx, Number1
Это фактически будет означать:
mov ecx, dword ptr [место в памяти, где расположена dword 12033h]
А это:
mov ecx, offset Number1
означает:
mov ecx, место в памяти, где расположена dword 12033h
В первом примере, ecx получит значение, которое находится в памяти указанной меткой Number1. Во втором, ecx станет указателем на то место в памяти, на которое указывает метка Number1.
Эти два примера ниже, имеют тот же самый эффект:
(1)
mov ecx, Number1
(2)
mov ecx, offset Number1
mov ecx, dword ptr [ecx] ( или mov ecx, [ecx])
Давайте вернемся к примеру:
.data
Number1 dd 12033h
Number2 dw 100h,200h,300h,400h
Number3 db "blabla",0
.data?
Value dd ?
.code
mov eax, Number1
mov ecx, offset Number2
add ax, word ptr [ecx+4]
mov Value, eax
Метки Value может использоваться точно так же как Number1, Number2 и Number3, но при запуске она будет содержать 0, потому что она находится в секции неинициализированных данных. Преимущество этого в том, что все, что вы определяете в секции .data? не будет включено в исполнимый файл, а будет только в памяти.
.data?
ManyBytes1 db 5000 dup (?)
.data
ManyBytes2 db 5000 dup (0)
(5000 dup означает: 5000 копий. Value db 4,4,4,4,4,4,4 это тоже самое, что Value db 7 dup (4).)
ManyBytes1 не будет находится непосредственно в исполнимом файле, а только зарезервирует 5000 байт в памяти. А ManyBytes2 будет полностью вложена в испонимый файл, делая его на 5000 байт больше. Таким образом ваш файл будет содержать 5000 нулей, а это не рационально.
Секция кода (.code) будет ассемблирована (преобразованна в коды) и помещена в исполнимый файл.
[наверх]
Условные переходы
Win32Asm Tutorial |
назад | 7- Условные переходы | вперед |
7.0 - Условные переходы
В секции кода, вы также можете использовать метки, как здесь:
.code
mov eax, edx
sub eax, ecx
cmp eax, 2
jz loc1
xor eax, eax
jmp loc2
loc1:
xor eax, eax
inc eax
loc2:
(xor eax, eax означает: eax = 0.)
Давайте исследуем код:
mov eax, edx : поместить edx в eax
sub eax, ecx : вычесть ecx из eax
cmp eax, 2
Это новая команда: cmp. Команда Cmp производит сравнение двух операндов. Она сравнивает два значения (регист, память, непосредственное значение) и устанавливает флаг нуля Z (zeroflag) если они равны. Флаг нуля, так же, как флаг переноса, находится во внутреннем флаговом регистре.
jz loc1
Это также новая команда - условный переход. Jz = (jump if zero) переход если ноль. То есть переход, если флаг нуля установлен. Loc1 это метка для смещения в памяти, на команды 'xor eax, eax | inc eax'. Значит jz loc1 = переход на команды после метки loc1 если флаг нуля установлен.
cmp eax, 2 : устанавливает флаг нуля, если eax=2
jz loc1 : переход, если флаг нуля установлен
=
Переход на команды после метки loc1, если eax равно 2
Далее здесь стоит jmp loc2. Это тоже переход, но безусловный: т.е. всегда переходит. Что именно делает вышеприведенный код:
if ((edx-ecx)==2)
{
eax = 1;
}
else
{
eax = 0;
}
или BASIC версия:
IF (edx-ecx)=2 THEN
EAX = 1
ELSE
EAX = 0
END IF
7.1 - Регистр флагов
Регистр флага имеет набор флагов, которые устанавливаются или сбрасываются в зависимости от вычислений или других событий. Я не буду расказывать о всех, а только о некоторых важных:
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)) но так как мы не будем их использовать, я не буду о них рассказывать.
7.2 - Переходы
Существует целая серия условных переходов и все они переходят в зависимости от состояния флагов. Поскольку большинство переходов имеют четкие названия, вам даже не нужно знать какие флаги должны быть установлены. "Jump if greater or equal" "переход если больше или равно" (jge) например тоже самое, что "Флаг знака = Флагу переполнения", а "Jump if zero" "Переход если нуль" (JZ) тоже самое, что "Переход, если флаг нуля = 1 (JZ)".
В таблице, 'Значение' показывает каким должен быть результат вычисления, чтобы произошел переход.
Опкод | Значение(переход,если...) | Условие |
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 |
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 |
Все команды перехода имеют один операнд: смещение для перехода.
[наверх]
Кое-что о числах
Win32Asm Tutorial |
назад | 8- Кое-что о числах | вперед |
8.0 - Кое-что о числах
Использование целых чисел или 'чисел с плавающей запятой' в большинстве языков программирования зависит только от объявления переменных. На ассемблере они совершенно различны. С 'числами с плавающей запятой', вычисления производятся специальными опкодами и сопроцессором FPU (сопроцессор для операций с 'числами плавающей запятой'). Команды для работы с 'числами с плавающей запятой' будут обсуждены позже. Сначала кое-что о целых числах. На Языке C, есть числа со знаком и без знака. Со знаком означает, что числа имеют знак (+ или -), числа без знака всегда положительны. Смотрите таблицу ниже, чтобы увидеть различия (снова, это пример байта, это работает также, как и с другими размерами):
Значение | 00 | 01 | 02 | 03 | ... | 7F | 80 | ... | FC | FD | FE | FF | ||||||||||||
числа без знака | 00 | 01 | 02 | 03 | ... | 7F | 80 | ... | FC | FD | FE | FF | ||||||||||||
числа со знаком | 00 | 01 | 02 | 03 | ... | 7F | -80 | ... | -04 | -03 | -02 | -01 |
Так для чисел со знаком, байт разбит на два диапазона: 0 - 7F для положительных значений, 80 - FF для отрицательных значений. Для значений dword, так же: 0 - 7FFFFFFFh для положительных, 80000000 - FFFFFFFFh для отрицательных. Как вы могли заметить, отрицательные числа имеют старший разрядный набор, потому что они больше 80000000h. Этот бит называется знаковым битом.
8.1 - Со знаком или без знака?
Ни вы, ни процессор не можете увидеть, какое это число, со знаком или без знака. Хорошие новости, то, что для сложения и вычитания, не имеет значения, число со знаком или без знака:
Вычисляем: -4 + 9
FFFFFFFC + 00000009 = 00000005. (это верно)
Вычислите: 5 - (-9)
00000005 - FFFFFFF7 = 0000000E (это тоже верно ( 5 - -9 = 14)
А теперь плохие новости, то, что результат от умножения, деления или сравнения будет не верным. Следовательно есть специальные mul(умножение) и div(деление) опкоды для чисел со знаком:
imul и idiv
Imul также имеет преимущества над mul, так как может принимать непосредственные значения:
imul множитель
imul множимое, множитель
imul результат, множимое, множитель
imul результат, множимое
idiv делитель
Они такие же, как mul и div, но только производят вычисления с числами со знаком. Сравнение можно производить так же, как и с чилами без знака, но состояния флагов будут другими. Следовательно есть разные команды переходов, для чисел со знаком и без знака:
cmp ax, bx
ja смещение
Ja это без знаковый переход. Переход если 'больше'. Представте, что ax = FFFFh (без знака это FFFFh, а со знаком это -1), а bx = 0005h (без знака это 5 и со знаком это тоже 5). Так как FFFFh больше, чем значение (без знака) 0005, ja-команда выполнит переход. Но если использовать команду jg (которая является переходом со знаком):
cmp ax, bx
jg смещение
jg-команда не выполнит переход, потому, что -1 меньше, чем 5.
Просто запомните:
Разница в числах со знаком или без знака только в том,
как вы эти числа будете обрабатывать.
[наверх]
Еще о кодах операций
Win32Asm Tutorial |
назад | 9- Еще об опкодах | вперед |
9.0 - Еще об опкодах
Здесь я расскажу еще о нескольких опкодах.
TEST |
Команда Test выполняет операцию AND (логическое И) с двумя операндами и в зависимости от результата устанавливает или сбрасывает соответствующие флаги. Результат не сохраняется. Test используется для проверки бит, например в регистре:
test eax, 100b ; (суффикс 'b' означает двоичное число)
jnz смещение
Команда jnz выполнит переход, если в регистре eax третий бит справа - установлен. Очень часто комманду test используют для проверки, равен ли регистр нулю:
test ecx, ecx
jz смещение
Команда jz выполнит переход, если ecx = 0.
Стековые операции |
Перед тем, как рассказать вам о стековых операциях, я сначала попытаюсь объяснить, что такое стек. Стек это область в памяти, на которую указывает регистр стека esp. Стек это место для хранения временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: push и pop. Команда push размещает значение в стеке, а pop извлекает. Значение помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении - увеличивается. Рассмотрим пример:
(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. Конечно вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.
[наверх]
Преимущества MASM
Win32Asm Tutorial |
назад | 10- Преимущества MASM | вперед |
10.0 - Преимущества masm
Если вы не используете masm, то можете пропустить этот раздел и попробовать преобразовать все примеры, или прочитать это и пробовать убедить себя, использовать masm. Конечно это ваш собственный выбор. Но masm делает действительно ассемблер намного проще.
10.1 - Конструкции сравнения и повтора
Masm имеет некоторый синтаксис псевдовысокого уровня, чтобы легко создавать конструкции сравнения и повтора:
.IF, .ELSE, .ELSEIF, .ENDIF
.REPEAT, .UNTIL
.WHILE, .ENDW, .BREAK
.CONTINUE
If
Если у вас есть опыт в языках программирования, возможно вы видели что-то вроде if/else конструкций:
.IF eax==1
;eax равен 1
.ELSEIF eax=3
; eax равен 3
.ELSE
; eax не равен 1 и 3
.ENDIF
Эта конструкция очень полезна. Вам не нужно вставлять сравнения и переходы, а только вставте директиву .IF (не забудьте точку перед .IF и .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) установлен? |
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.
10.2 - Invoke
Это самое большое преимущество над tasm и nasm. Invoke упрощает использование процедур и вызовов.
Обычный стиль:
push параметр_3
push параметр_2
push параметр_1
call procedure
Invoke стиль:
invoke procedure, параметр_1, параметр_2, параметр_3
Собранный код будет одинаковым, но invoke стиль проще и более надежнее. Чтобы использовать 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 PROTO STDCALL :DWORD, :DWORD, :DWORD
.code
testproc proc param1:DWORD, param2:DWORD, param3:DWORD
mov ecx, param1
mov edx, param2
mov eax, param3
add edx, eax
mul eax, ecx
ret
testproc endp
Теперь процедура делает следующие вычисления. testproc(param1, param2, param3) = param1 * (param2 + param3). Значение результата возвращается в регистре eax.
Локальные переменные определяются как здесь:
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
Вы не можете использовать эти переменные вне процедуры. Они сохранены в стеке и удаляются при возврате из процедуры.
10.3 - Макрокоманды
Я не буду сейчас рассказывать о Macro. Возможно в более поздних версиях этого туториала, но сейчас они для нас не важны.
[наверх]
Основы ассемблера в windows
Win32Asm Tutorial |
назад | 11- Основы ассемблера в windows | вперед |
11.0 - Основы ассемблера в windows
Теперь Вы имеете некоторые элементарные знания об ассемблере, здесь вы изучите, как писать ассемблере под windows.
11.1 - API
Основа программирования под windows лежит в Win API (Application Programming Interface). Это набор функций обеспеченных операционной системой. Каждая windows программа использует эти функции. Эти функции находятся в системных dll, таких как kernel, user, gdi, shell, advapi, и т.д. Есть два типа функций: ANSI и Unicode. Это имеет отношение к способу сохранения строки. С ansi, каждый байт представляет символ (ascii-код) и использует 0-байт, для указания конца строки (с нулевым символом в конце). Unicode использует формат widechar, который использует 2 байта на символ. Это позволяет использовать языки, которые нуждаются в большом количестве символов, подобно китайскому. Widechar строки оканчиваются двумя нулевыми байтами. Windows поддерживает оба типа, используя различные имена функций для ansi и unicode. Например:
MessageBoxA (суффикс 'A' означает для ansi)
MessageBoxW (суффикс 'W' означает для widechar (unicode))
Мы будем использовать только ansi.
11.1 - Импорт dll
Чтобы использовать функции из Win API, вы должны импортировать соответствующюю dll. In order to use the functions from the windows API, you need to import the dll's. Это делается библиотеками импорта (.lib). Эти библиотеки необходимы, потому что они позволяют системе (windows) загружать dll динамически. Пакет win32asm (win32asm.cjb.net) снабжен библиотеками для большинства стандартных dll. Вы можете подключить библиотеку директивой includelib.
includelib C:\masm32\lib\kernel32.lib
Этот код подключает библиотеку импорта kernel32.lib.
В примерах, используется вот такая форма:
includelib \masm32\lib\kernel32.lib
Теперь вы можете увидеть, почему ваши исходные файлы должны находится на том же диске, что и masm. Теперь Вы можете компилировать вашу программу на любом другом компьютере без изменения всех путей на правильный диск.
Но подключение библиотека импорта это еще не все, что вы должны сделать. Файлы include (.inc) также необходимы. Они могут быть автоматически сгенерированы из библиотек, используя утилиту l2inc. Файлы include подключаются так:
include \masm32\include\kernel32.inc
Внутри include файлы, содержат определение прототипов функций dll, так что вы можете использовать invoke.
kernel32.inc:
...
MessageBoxA proto stdcall :DWORD, :DWORD, :DWORD, :DWORD
MessageBox textequ <MessageBoxA>
...
Вы можете заметить, что include-файл содержит ansi функции, а также определяет имена функций без суффикса 'A', чтобы они были такими-же, как их реальные имена: вы можете использовать MessageBox вместо MessageBoxA. После того, как вы подключили библиотеку и include-файл, вы можете использовать функцию:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, NULL
11.2 - include-файл 'windows'
Здесь есть еще специальный include-файл, названный windows.inc, который содержит все константы и структуры для windows API. Например, окно сообщений может иметь различные стили. Четвертый параметр функции это стиль. NULL (пустой указатель) замещает MB_OK, которая обозначает кнопку OK. Include-файл windows содержит определения для этих стилей:
MB_OK equ 0
MB_OKCANCEL equ ...
MB_YESNO equ ...
Так что вы можете использовать эти имена, как константы:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_YESNO
Пример использует include-файл из пакета masm:
include \masm32\include\windows.inc
11.3 - "Каркас" программы
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
include \masm32\include\windows.inc
.data
<Здесь ваши инициализируемые данные>
.data?
<Здесь ваши не инициализируемые данные>
.const
<Здесь ваши константы>
.code
start:
<Здесь ваш код>
end start
Это основной "каркас" программы исходного файла ассемблера под windows (.asm).
.486 | Сообщает ассемблеру, чтобы он генерировал опкоды для 486 процессора. Вы также можете использовать .386, но .486 работает в большинстве случаев. |
.model flat, stdcall | Используйте плоскую модель памяти (о которой рассказано в предыдущих уроках), и используйте вид вызовов stdcall. Это означает, что параметры для функции помещаются в стек справа налево (последний параметр, помещается в стек первым) и что функция сама должна исправить стек при выходе из нее. Это стандарт почти для всех windows API функций и dll. |
option casemap:none | говорит ассемблеру сделать метки чувствительными к регистрам, то есть MessageBox и messagebox - это различные имена. Для правильной работы файла windows.inc, это значение должно быть 'none'. |
includelib | рассмотрены выше |
include | также рассмотрены выше |
.data | начало секции инициализированных данных (см. предыдущие уроки) |
.data? | начало секции не инициализированных данных (см. предыдущие уроки) |
.const | начало секции констант (см. предыдущие уроки) |
.code | начало секции кода (см. предыдущие уроки) |
start: end start |
Метка указывающая начало программы. Обратите внимание, что не требуется вызов метки 'start'. Вы можете использовать любое название для нее, т.к. используя директиву 'end' вы указываете, что это метка начала: startofprog: end startofprog |
[наверх]
Первая программа
Win32Asm Tutorial |
назад | 12- Первая программа | вперед |
12.0 - Первая программа
Пришло время создать вашу первую программу. Указания в этом уроке выделенны вот так, следуйте указаниям.
12.1 - Шаг 1
Если все хорошо, то у вас должна быть папка 'win32' (или 'win32asm') на вашем жестком диске, на том-же, что и 'masm'. Для каждого проекта вы должны создавать папку.
Создайте подпапку 'Firstprogram' в папке win32. Создайте новый текстовый файл и переименуйте его в 'first.asm'.
Важно: если вы используете ultraedit, удостоверьтесь , что установили мой 'wordfile' и переключитесь на окно 'functions' (view, views/lists, function list). Наконец, удостоверьтесь, что установили значение tab stop на 4 пробела (Advanced, configuration, Edit tab)
12.2 - Шаг 2
Поместите следующий код в first.asm:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\windows.inc
Пока нам нужны только два dll: kernel32 и user32.
12.3 - Шаг 3
Мы собираемся сделать известную всем программу 'Hello world'. Чтобы отобразить строку 'hello world', мы будем использовать окно сообщений (messagebox). Messagebox создан, используя функцию MessageBox. Вы можете найти эту функцию в win32 справочнике программиста (см. урок 2). Вот, что там говорится:
Функция MessageBox создает, отображает, и использует окно сообщений. Окно сообщений содержит определенное приложением сообщение и заголовок, плюс любая комбинация предопределенных значков и кнопок.
int MessageBox(
HWND hWnd, // хэндл (дескриптор) окна владельца
LPCTSTR lpText, // адрес текста в окне сообщений
LPCTSTR lpCaption, // адрес заголовка окна сообщений
UINT uType // стиль окна сообщений
);
Параметры
hWnd | устанавливает владельца окна сообщений, которое будет создано. Если этот параметр NULL (ПУСТОЙ), окно сообщений не имеет никакого владельца. |
lpText | Указывает на строку с нулевым символом в конце, содержащую сообщение, которое будет отображено. |
lpCaption | Указывает на строку с нулевым символом в конце, используемую для заголовка диалогового окна. Если этот параметр NULL, используется заданный по умолчанию заголовок - ошибка. |
uType | Определяет флаги, которые определяют содержание и поведение диалогового окна. Этот параметр может быть комбинацией флагов из следующих групп флагов. |
После этого текста следует целый список констант и флагов (которые определены в windows.inc). Я не отобразил его здесь, потому что он слишком большой. Смотрите в справочнике, вы можете увидеть, что функция MessageBox берет 4 параметра: родительское окно, указатель на строку сообщения, указатель на строку заголовка и тип messagebox.
hWnd может быть NULL, потому, что наша программа не имеет окна.
lpText Должен быть указателем на наш текст. Это значит, что этот параметр - смещение в памяти, где находится наш текст.
lpCaption смещение строки заголовка.
uType комбинация значений, описанных в справочнике, таких как MB_OK, MB_OKCANCEL, MB_ICONERROR, и т.д.
Давайте сначала определим две строки для messagebox:
Добавте это в first.asm:
.data
MsgText db "Hello world!",0
MsgTitle db "This is a messagebox",0
.data указывает на начало секции данных. Директива db непосредственно вставляет байты, а строка это набор байт, секция данных будет содержать строки выше, а чтобы они заканчивались 0, вставляем дополнительно ,0. MsgText содержит смещение первой строки, а MsgTitle смещение второй. Теперь мы можем использовать функцию:
invoke MessageBox, NULL, offset MsgText, offset MsgTitle, NULL
А так, как вы используете invoke, вы можете использовать (более безопасную) ADDR вместо offset:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, NULL
Мы еще не смотрели на последний параметр, но это прекрасно будет работать, потому что MB_OK (стиль для messagebox с кнопкой OK) равняется 0 (NULL). Но Вы можете использовать любой другой стиль. UType (4-ый параметр) определяет:
Определяет флаги, которые определяют содержание и поведение диалогового окна. Этот параметр может быть комбинацией флагов из следующих групп флагов.
Теперь возьмем для напримера, что мы хотим простой messagebox с кнопкой OK с значком 'информация'. MB_OK это стиль для кнопки OK, MB_ICONINFORMATION это стиль для информационного значка. Стили объединены с помощью оператора 'or'. Это не опкод. Masm выполнит операцию or перед ассемблированием. Вместо or, вы можете использовать знак + (добавление), но так иногда возникают проблемы с накладывающимися стилями (один стиль содержит некоторые другие). В нашем случае, вы можете использовать +.
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK + MB_ICONINFORMATION
end start
Добавьте вышеприведенный код к вашему файлу first.asm
Мы также добавили метку начала. Если бы вы ассемблировали сейчас нашу программу и запустили ее, она бы отобразила messagebox и вероятно повисла (или выдала ошибку) после того, как вы нажали OK. Это произойдет потому, что программа еще не закончена, и процессор начнет выполнять код, который находится после кода messagebox (а там, в данном случае, может быть всякий мусор). Программы в windows завершаются функцией ExitProcess:
Функция ExitProcess завершает процесс и все его нити.
VOID ExitProcess(
UINT uExitCode // код выхода для всех нитей
);
Мы можем использовать 0, как код выхода:
Замените ваш код на этот:
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK + MB_ICONINFORMATION
invoke ExitProcess, NULL
end start
12.4 - Шаг 4
Вот, та программа, которая у нас получилась:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\windows.inc
.data
MsgText db "Hello world!",0
MsgTitle db "This is a messagebox",0
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK or MB_ICONINFORMATION
invoke ExitProcess, NULL
end start
12.5 - Шаг 5
Теперь создадим исполняемую программу из этого исходного текста.
Создайте новый текстовый файл и назовите его make.bat со следующим содержанием:
@echo off
ml /c /coff first.asm
link /subsystem:windows first.obj
pause>nul
Анализ:
ml /c /coff first.asm |
ml это макроассемблер (masm). Masm создаст необработанный код программы. Опции означают: /c = Ассемблирование без линковки (компоновки). (потому, что мы используем link.exe для этого) /coff = генерировать объектный файл COFF-формата. Это стандарт для программ windows. first.asm = ассемблируемый файл first.asm |
link /subsystem:windows first.obj | Линкер (компоновщик) берет объектный файл и связывает его со всеми импортированными dll и библиотеками. Опции: /subsystem:windows = создать исполняемый файл для windows. first.obj = Линковать first.obj |
Теперь, если вы сделали все правильно и запустите файл make.bat, то здесь появится файл first.exe. Запустите его и увидите результат.
[наверх]
Окна в windows
Win32Asm Tutorial |
назад | 13- Окна в windows | вперед |
12.0 - Окна в windows
В этом уроке мы создадим программу с окном.
12.1 - Окна
Вы наверняка догадываетесь, почему windows так назван. В windows, есть два типа программ: GUI-приложения и консольные приложения. Консольные программы похожи на DOS-программы, они выполняются в поле подобном DOS. Большинство программ, которые вы используете это GUI (graphical user interface) приложения. У них есть графический интерфейс, для взаимодействия с пользователем. Это сделано, созданием окон. Почти все, что вы видите в windows, это окна. Сначала вы создаете родительское окно, а затем его дочерние окна (контролы) такие, как окна редактирования, статические контролы, кнопки и т.д.
12.2 - Классы окон
Каждое окно имеет имя класса. Для вашего родительского окна, вы определяете ваш собственный класс. Для контролов, вы можете использовать стандартные классы окон (такие как 'EDIT', 'STATIC', 'BUTTON').
12.3 - Структуры
Класс окна в вашей программе регистрируется с помощью функции 'RegisterClassEx' (RegisterClassEx это расширенная версия RegisterClass. RegisterClass используется редко). Объявление этой функции:
ATOM RegisterClassEx(
CONST WNDCLASSEX *lpwcx // адрес структуры с данными класса
);
lpwcx: Указатель на структуру WNDCLASSEX. Вы должны заполнить структуру соответствующими классу аттрибутами перед передачей ее функции.
Единственный параметр это указатель на структуру. Сначала некоторые основы относительно структур:
Структура это набор переменных (данных). Структура определяется директивой STRUCT:
SOMESTRUCTURE STRUCT
dword1 dd ?
dword2 dd ?
some_word dw ?
abyte db ?
anotherbyte db ?
SOMESTRUCTURE ENDS
(имя структуры не должно содержать прописных букв)
Вы также можете объявить ваши переменные в секции неинициализированных данных, со знаком вопроса. Теперь вы можете создать структуру из объявления:
Инициализированная
Initializedstructure SOMESTRUCTURE <100,200,10,'A',90h>
Неинициализированная
UnInitializedstructure SOMESTRUCTURE <>
В первом примере, новая структура создана (Initializedstructure содержит ее смещение), и каждый элемент данных структуры заполнен начальным значением. Второй пример просто говорит masm'у зарезервировать память для структуры, и каждый элемент данных структуры установить в 0. После создания структуры вы можете обращаться к любому ее элементу:
mov eax, Initializedstructure.some_word
; теперь eax будет содержать 10
inc UnInitializedstructure.dword1
; переменная dword1 структуры увеличена на 1
Таблица показывает, как эта структура была бы сохранена в памяти.
Расположение в памяти | Содержимое |
offset of Initializedstructure | 100 (dword, 4 байта) |
offset of Initializedstructure + 4 | 200 (dword, 4 байта) |
offset of Initializedstructure + 8 | 10 (word, 2 байта) |
offset of Initializedstructure + 10 | 65 or 'A' (1 байт) |
offset of Initializedstructure + 11 | 90h (1 байт) |
12.3 - WNDCLASSEX
Достаточно о структурах, давайте продолжать RegisterClassEx. В win32 справочнике программиста вы можете найти описание структуры WNDCLASSEX.
typedef struct _WNDCLASSEX { // wc
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;
Анализ:
cbSize | Размер структуры WNDCLASSEX в байтах. Вы можете получить этот размер с помощью оператора SIZEOF: mov ws.cbSize, SIZEOF WNDCLASSEX |
style | Стиль окон, создаваемых из это класса. |
lpfnWndProc | Указатель на процедуру окна (об этом я расскажу ниже) |
cbClsExtra | Количество дополнительных байтов, которые нужно зарезервировать (они будут следовать за самой структурой). |
cbWndExtra | Количество дополнительных батов, которые нужно зарезервировать (они будут следовать за window instance). |
hInstance | Хэндл вашей программы. Вы можете получить это хэндл функцией GetModuleHandle. |
hIcon | Хэндл иконки. Получите его функцией LoadIcon. |
hCursor | Хэндл курсора. Получите его функцией LoadCursor. |
hbrBackground | Хэндл кисти для закрашивания фона, или один из стандартных, таких как COLOR_WINDOW, COLOR_BTNFACE , COLOR_BACKGROUND. |
lpszMenuName | Указатель на строку с нулевым символом в конце, которая определяет имя ресурса меню класса. Это также может быть ID ресурса. |
lpszClassName | Указатель на строку с нулевым символом в конце, которая определяет имя класса для окон. |
hIconSm | Хэнд маленькой иконки. |
Создайте новую подпапку firstwindow в папке win32 и создайте новый файл window.asm в этой папке со следующим содержанием:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
Затем создайте .bat-файл с именем make.bat. Скопируйте в него этот текст:
@echo off
ml /c /coff window.asm
link /subsystem:windows window.obj
pause>nul
<смотреть код>
Далее я буду приводить код не полностью, а только его части (для экономии места), вы можете нажать на <смотреть код>, чтобы отобразить код полностью (в новом окне).
12.4 - Регистрация класса
Теперь зарегистрируем класс в процедуре WinMain. В этой процедуре находится инициализация окна.
Добавьте это к своему файлу:
WinMain PROTO STDCALL :DWORD, :DWORD, :DWORD
.data?
hInstance dd ?
.code
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL
end start
Этот код получит хэндл модуля функцией getmodulehandle, поместите хэндл в переменную hInstance. Этот хэнд модуля очень часто используется в windows API. Затем вызывается процедура WinMain. Это не API функция, а процедура, которую мы теперь определим. Ее прототип: WinMain PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD, функция с 4 параметрами.:
<смотреть код>
Теперь поместите этот код перед end start:
WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD
ret
WinMain endp
Вы не обязаны объявлять функцию winmain. На самом деле, вы совершенно свободны в этом отношении.
hInst это описатель экземпляра (= хэндл модуля),
hPrevInst это хэндл предыдущего экземпляра программы. Под win32 нет такого понятия, как предыдущий экземпляр программы. Каждая программа одна единственная в своем адресном пространстве, поэтому значение этой переменной всегда 0. Это пережиток времен Win16, когда все экземпляры программы запускались в одном и том-же адресном пространстве, и экземпляр мог узнать, были ли запущены еще копии этой программы. Под Win16, если это значение равно NULL, тогда этот экземпляр является первым.
CmdLine указатель на коммандную строку.
CmdShow это флаг, который определяет, как должно быть показанно окно. (Подробнее об этом, вы можете найти в справочнике API функция ShowWindow).
Теперь мы можем написать наш код инициализации в WinMain:
WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
ret
WinMain endp
Здесь мы резервируем две локальные переменные, они нам понадобятся в этой процедуре.
.data
ClassName db "FirstWindowClass",0
.code
WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
; now set all the structure members of the WNDCLASSEX structure wc:
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, ADDR wc
ret
WinMain endp
Давайте посмотрим, что получается:
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
Размер структуры инициализирован (это требуется RegisterClassEx). Стиль класса установлен в "CS_HREDRAW or CS_VREDRAW", затем установлен указатель оконной процедуры. Позже вы узнаете, что такое оконная процедура, а сейчас запомните, что вам нужен адрес процедуры WndProc, который вы сможете получить 'offset WndProc'. CbClsExtra и cbWndExtra не используются нами, так что установите их в NULL.
push hInst
pop wc.hInstance
В wc.hInstance устанавливается хэндл модуля, параметр процедуры WinMain. А почему мы не используем: mov wc.hInstance, hInst? Потому, что команда mov не позволяет перемещать данные из одной ячейки памяти в другую. Здесь мы копируем методом push/pop, значение помещается в стек, а затем извлекается туда, куда нам надо.
mov wc.hbrBackground, COLOR_WINDOW
mov wc.lpszMenuName, NULL
mov wc.lpszClassName, OFFSET ClassName
Цвет фона класса установлен в COLOR_WINDOW, никакое меню не определено (NULL), и lpszClassName установлен адрес строки имени класса, закачивающейся нулем: "FirstWindowClass". Это должно быть уникальное имя, определенное для вашего собственного приложения.
invoke LoadIcon, NULL, IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
Окну нужен зачек (иконка), а для этого нам нужен хэндл иконки, мы используем LoadIcon, чтобы загрузить значок (иконку) и получать ее хэндл. LoadIcon имеет два параметра: hInstance, и lpIconName. hInstance - хэндл модуля, чей исполняемый файл содержит значок. LpIconName - указатель на строку, которая является названием ресурса значка или ID ресурса. Если вы используете NULL как hInstance, то вы можете выбрать из некоторых стандартных значков. (что мы здесь и делаем, потому что у нас нет ресурса значка). HIconSm - маленький значок, вы можете использовать тот же хэндл и для него.
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
Тоже самое и для курсора, только NULL в hInstance, и стандартный тип курсора: IDC_ARROW, стандартная стрелка windows.
invoke RegisterClassEx, ADDR wc
И теперь наконец зарегистрируем класс, используя RegisterClassEx с указателем на wc структуру WNDCLASSEX как параметр.
<смотреть код>
12.5 - Создание окна
Теперь, после того, как вы зарегистрировали класс, вы можете создать из него окно:
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 // указатель на данные создания окна.
);
dwExStyle и dwStyle это два параметра, которые определяют стиль окна.
lpClassName это указатель на ваше зарегистрированное имя класса.
lpWindowName это имя вашего окна (оно будет в заголовке вашего окна)
x, y, nWidth, nHeight определяют позицию и размер вашего окна.
hWndParent это хэндл окна, которому принадлежит новое окно. Может быть нулевым, если нет родительского окна.
hMenu это хэндл меню окна.
hInstance это хэндл программного модуля, создающего окно.
lpParam дополнительное значение, которое вы можете использовать в своей программе.
.data
AppName "FirstWindow",0
.code
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,400,300,NULL,NULL,\
hInst,NULL
mov hwnd, eax
invoke ShowWindow, hwnd, SW_SHOWNORMAL
invoke UpdateWindow, hwnd
(Обратите внимание, что символ '\' говорит ассемблеру, что следующая строка, это продолжение текущей строки.)
Наш код создаст новое окно, с нашим именем класса, которое мы только что зарегистрировали. Заголовок будет "FirstWindow" (AppName), стиль - WS_OVERLAPPEDWINDOW, который создает перекрытое окно с заголовком, системным меню, с изменяемым размером и кнопками свернуть/развернуть. CW_USEDEFAULT как x и y позиция, установит окну заданные по умолчанию позиции для новых окон. Размер окна - 400x300 пикселей.
Возвращаемое значение функции это хэндл окна, HWND, который сохраняется в локальной переменной hwnd. Затем окно выводится на экран функцией ShowWindow. Функция UpdateWindow гарантирует, что окно будет выведено.
12.6 - Цикл сообщений
Windows может связываться с вашей программой и другими онами, используя сообщения. Оконная процедура (см. ниже в этом уроке) вызывается всякий раз, когда для определенного окна ожидается сообщение. Каждое окно имеет цикл сообщений. Это бесконечный цикл, который проверяет есть ли сообщение для вашего окна, и если есть, то передает сообщение функции dispatchmessage. Эта функция вызовет вашу оконную процедуру. Цикл сообщений и оконная процедура это две разные вещи!!!
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
LOCAL msg:MSG ;<<<новая переменная
........
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
Вот, как может выглядеть цикл сообщений. .WHILE TRUE, .ENDW повторяют цикл до тех пор, пока eax не стпнет равным 0. Функция GetMessage возвращает 0, если получает сообщение WM_QUIT, которое должно закрыть окно, так что программа должна выйти из цикла сообщений всякий раз, когда GetMessage возвращает 0. Если нет, то сообщение передается функции TranslateMessage (эта функция транслирует нажатия клавиш в сообщения) и затем сообщение посылается окной процедуре, с помощью функции DispatchMessage. Сообщение непосредственно в цикле сообщений состоит из структуры MSG (для этого мы в процедуру добавили LOCAL msg:MSG). Вы можете использовать этот цикл сообщений во всех своих программах.
12.7 - Оконная процедура
Сообщения будут посланы оконной процедуре. Оконная процедура должна всегда выглядеть следующим образом:
WndProc PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD
.code
WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
mov eax, uMsg
.IF eax==XXXX
.ELSEIF eax==XXXX
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp
Оконная процедура должна всегда иметь 4 параметра:
hWnd содержит хэндл окна.
uMsg это сообщение
wParam это первый параметр для сообщения
lParam второй параметр для сообщения
Сообщения, которые не обрабатывает окно, должны быть переданы функции DefWindowProc, которая занимается их обработкой. Пример оконной прцедуры:
WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
mov eax, uMsg
.IF eax==WM_CREATE
invoke MessageBox, NULL, ADDR AppName, ADDR AppName, NULL
.ELSEIF eax==WM_DESTROY
invoke PostQuitMessage, NULL
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp
Этот код отображает название приложения, при содании окна. Также, обратите внимание, что я добавил обработку сообщения WM_DESTROY. Это сообщение будет послано, если окно закрывается (т.е. вы нажали закрыть). Приложение должно реагировать на это сообщение функцией PostQuitMessage.
А теперь посмотрите, что у нас получилось:
<смотреть код>
[наверх]
В стадии разработки
Win32Asm Tutorial |
назад | 14- в стадии разработки |
14.0 - В стадии разработки
Извините, но этот урок пока еще не доступен. Этот учебник все еще находится в стадии разработки. Пожалуйста проверьте позже!
!
От переводчика: я также хочу сразу сказать, что я не профессиональный переводчик, поэтому здесь могут быть ошибки, так что I'm sorry (кто не знает, что это такое переведу: прошу прощения) и сильно не пинайте, а в прочем и автор этого учебника, то ли плохо знает английский, то ли сильно торопился в написании, что также допустил много ошибок.
Перевод выполнен:
UniSoft
если есть какие-то вопросы, пожелания, угрозы и т.д и т.п. то пожалуйста пишите.
[наверх]