Assembler & win32


Содержание

Win32 API и ассемблер

Вступление

Надеюсь, что мой вклад в понимание Application Programming Interface (API), имеющего широчайшее применение при создании любых приложений, расчитанных для работы в среде Windows, не останется незамеченным в кругах, близких к программированию.

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

Наиболее полным сборником документации по API несомненно является Microsoft Developer Network Library (MSDN). Причем, чем свежее, тем лучше. Описание функций в MSDN приведено в нотации Microsoft Visual С++. В последних релизах MSDN также приведены способы вызова функций на различных высокоуровневых «новомодных» языках (C#, VB .NET, J# и т.д.). Я же буду использовать пакет masm32 Стивена Хатчессона (для тех, кто еще не скачал вот ссылка. Версия пакета большого значения не имеет, но по опыту могу рекомендовать версию не ниже 8-й. Поэтому примеры и описание параметров функций я также буду приводить в нотации masm32, чтобы не создавать дополнительной путаницы. Пакет masm32 включает довольно объемный набор включаемых файлов с объявлением API-функций. Удобством я считаю и то, что в случае отсутствия объявления необходимой мне функции или значения флага, я свободно могу добавить его в существующий включаемый файл, либо создать свой, либо поместить данное описание непосредственно в .asm файл проекта.

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

Assembler & win32

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

В отличие от программирования под DOS, где программы написанные на языках высокого уровня (ЯВУ) были мало похожи на свои аналоги, написанные на ассемблере, приложения под Win32 имеют гораздо больше общего. В первую очередь, это связано с тем, что обращение к сервису операционной системы в Windows осуществляется посредством вызова функций, а не прерываний, что было характерно для DOS. Здесь нет передачи параметров в регистрах при обращении к сервисным функциям и, соответственно, нет и множества результирующих значений возвращаемых в регистрах общего назначения и регистре флагов. Следовательно проще запомнить и использовать протоколы вызова функций системного сервиса. С другой стороны, в Win32 нельзя непосредственно работать с аппаратным уровнем, чем “грешили” программы для DOS. Вообще написание программ под Win32 стало значительно проще и это обусловлено следующими факторами:

— отсутствие startup кода, характерного для приложений и динамических библиотек написанных под Windows 3.x;

— гибкая система адресации к памяти: возможность обращаться к памяти через любой регистр общего назначения; “отсутствие” сегментных регистров;

— доступность больших объёмов виртуальной памяти;

— развитый сервис операционной системы, обилие функций, облегчающих разработку приложений;

— многообразие и доступность средств создания интерфейса с пользователем (диалоги, меню и т. п.).

Современный ассемблер, к которому относится и TASM 5.0 фирмы Borland International Inc., в свою очередь, развивал средства, которые ранее были характерны только для ЯВУ. К таким средствам можно отнести макроопределение вызова процедур, возможность введения шаблонов процедур (описание прототипов) и даже объектно-ориентированные расширения. Однако, ассемблер сохранил и такой прекрасный инструмент, как макроопределения вводимые пользователем, полноценного аналога которому нет ни в одном ЯВУ.

Все эти факторы позволяют рассматривать ассемблер, как самостоятельный инструмент для написания приложений под платформы Win32 (Windows NT и Windows 95). Как иллюстрацию данного положения, рассмотрим простой пример приложения, работающего с диалоговым окном.

Пример 1. Программа работы с диалогом

Файл, содержащий текст приложения, dlg.asm

include «winconst.inc» ; API Win32 consts

include «winptype.inc» ; API Win32 functions prototype

include «winprocs.inc» ; API Win32 function

include «resource.inc» ; resource consts

szAppName db ‘Demo 1’, 0

szHello db ‘Hello, ‘

szUser db MAX_USER_NAME dup (0)

Start: call GetModuleHandleA, 0

call DialogBoxParamA, eax, IDD_DIALOG, 0, offset DlgProc, 0

call MessageBoxA, 0, offset szHello, \

MB_OK or MB_ICONINFORMATION

bye: call ExitProcess, 0

public stdcall DlgProc

proc DlgProc stdcall

arg @@hDlg :dword, @@iMsg :dword, @@wPar :dword, @@lPar :dword

call GetDlgItemTextA, @@hDlg, IDR_NAME, \

offset szUser, MAX_USER_NAME

@@cancel: call EndDialog, @@hDlg, eax

@@ret_false: xor eax,eax

@@init: call GetDlgItem, @@hDlg, IDR_NAME

call SetFocus, eax

Файл ресурсов dlg.rc

IDD_DIALOG DIALOGEX 0, 0, 187, 95

STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_CAPTION | WS_SYSMENU

FONT 8, «MS Sans Serif»

LTEXT «Type your name»,IDC_STATIC,4,36,52,8

Остальные файлы из данного примера, приведены в приложении 1.

Краткие комментарии к программе

Сразу после метки Start, программа обращается к функции API Win32 GetModuleHandle для получения handle данного модуля (данный параметр чаще именуют как handle of instance). Получив handle, мы вызываем диалог, созданный либо вручную, либо с помощью какой-либо программы построителя ресурсов. Далее программа проверяет результат работы диалогового окна. Если пользователь вышел из диалога посредством нажатия клавиши OK, то приложение запускает MessageBox с текстом приветствия.

Диалоговая процедура обрабатывает следующие сообщения. При инициализации диалога (WM_INITDIALOG) она просит Windows установить фокус на поле ввода имени пользователя. Сообщение WM_COMMAND обрабатывается в таком порядке: делается проверка на код нажатия клавиши. Если была нажата клавиша OK, то пользовательский ввод копируется в переменную szValue, если же была нажата клавиша Cancel, то копирования не производится. Но и в том и другом случае вызывается функция окончания диалога: EndDialog. Остальные сообщения в группе WM_COMMAND просто игнорируются, предоставляя Windows действовать по умолчанию.

Вы можете сравнить приведённую программу с аналогичной программой, написанной на ЯВУ, разница в написании будет незначительна. Очевидно те, кто писал приложения на ассемблере под Windows 3.x, отметят тот факт, что исчезла необходимость в сложном и громоздком startup коде. Теперь приложение выглядит более просто и естественно.

Пример 2. Динамическая библиотека

Написание динамических библиотек под Win32 также значительно упростилось, по сравнению с тем, как это делалось под Windows 3.x. Исчезла необходимость вставлять startup код, а использование четырёх событий инициализации/деинициализации на уровне процессов и потоков, кажется логичным.

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

Assembler & win32

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

Недостатки зачастую обусловлены лишь склонностью современного рынка к предпочтению количества качеству. Современные компьютеры способны легко справиться с нагромождением команд высокоуровневых функций, а если нелегко — будьте добры обновите аппаратную часть вашей машины! Таков закон коммерческого программирования. Если же речь идет о программировании для души, то компактная и шустрая программа, написанная на ассемблере, оставит намного более приятное впечатление, нежели высокоуровневая громадина, обремененная кучей лишних операций. Бытует мнение, что программировать на ассемблере могут только избранные. Это неправда. Конечно, талантливых программистов-ассемблерщиков можно пересчитать по пальцам, но ведь так обстоит дело практически в любой сфере человеческой деятельности. Не так уж много найдется водителей-асов, но научиться управлять автомобилем сумеет каждый — было бы желание. Ознакомившись с данным циклом статей, вы не станете крутым хакером. Однако вы получите общие сведения и научитесь простым способам программирования на ассемблере для Windows, используя ее встроенные функции и макроинструкции компилятора. Естественно, для того, чтобы освоить программирование для Windows, вам необходимо иметь навыки и опыт работы в Windows. Сначала вам будет многое непонятно, но не расстраивайтесь из- за этого и читайте дальше: со временем все встанет на свои места.

Итак, для того, чтобы начать программировать, нам как минимум понадобится компилятор. Компилятор — это программа, которая переводит исходный текст, написанный программистом, в исполняемый процессором машинный код. Основная масса учебников по ассемблеру делает упор на использование пакета MASM32 (Microsoft Macro Assembler). Но я в виде разнообразия и по ряду других причин буду знакомить вас с молодым стремительно набирающим популярность компилятором FASM (Flat Assembler). Этот компилятор достаточно прост в установке и использовании, отличается компактностью и быстротой работы, имеет богатый и емкий макросинтаксис, позволяющий автоматизировать множество рутинных задач. Его последнюю версию вы можете скачать по адресу: сайт выбрав flat assembler for Windows. Чтобы установить FASM, создайте папку, например, «D:\FASM» и в нее распакуйте содержимое скачанного zip-архива. Запустите FASMW.EXE и закройте, ничего не изменяя. Кстати, если вы пользуетесь стандартным проводником, и у вас не отображается расширение файла (например, .EXE), рекомендую выполнить Сервис -> Свойства папки -> Вид и снять птичку с пункта Скрывать расширения для зарегистрированных типов файлов. После первого запуска компилятора в нашей папке должен появиться файл конфигурации — FASMW.INI. Откройте его при помощи стандартного блокнота и допишите в самом низу 3 строчки:
[Environment]
Fasminc=D:\FASM\INCLUDE
Include=D:\FASM\INCLUDE

Если вы распаковали FASM в другое место — замените «D:\FASM\» на свой путь. Сохраните и закройте FASMW.INI. Забегая вперед, вкратце объясню, как мы будем пользоваться компилятором:
1. Пишем текст программы, или открываем ранее написанный текст, сохраненный в файле .asm, или вставляем текст программы из буфера обмена комбинацией.
2. Жмем F9, чтобы скомпилировать и запустить программу, или Ctrl+F9, чтобы только скомпилировать. Если текст программы еще не сохранен — компилятор попросит сохранить его перед компиляцией.
3. Если программа запустилась, тестируем ее на правильность работы, если нет — ищем ошибки, на самые грубые из которых компилятор нам укажет или тонко намекнет.
Ну, а теперь мы можем приступить к долгожданной практике. Запускаем наш FASMW.EXE и набираем в нем код нашей первой программы:

.data
Caption db ‘Моя первая программа.’,0
Text db ‘Всем привет!’,0

.code
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0

Жмем Run -> Run, или F9 на клавиатуре. В окне сохранения указываем имя файла и папку для сохранения. Желательно привыкнуть сохранять каждую программу в отдельную папку, чтобы не путаться в будущем, когда при каждой программе может оказаться куча файлов: картинки, иконки, музыка и прочее. Если компилятор выдал ошибку, внимательно перепроверьте указанную им строку — может, запятую пропустили или пробел. Также необходимо знать, что компилятор чувствителен к регистру, поэтому .data и .Data воспринимаются как две разные инструкции. Если же вы все правильно сделали, то результатом будет простейший MessageBox (рис. 1). Теперь давайте разбираться, что же мы написали в тексте программы. В первой строке директивой include мы включили в нашу программу большой текст из нескольких файлов. Помните, при установке мы прописывали в фасмовский ини-файл 3 строчки? Теперь %fasminc% в тексте программы означает D:\FASM\INCLUDE или тот путь, который указали вы. Директива include как бы вставляет в указанное место текст из другого файла. Откройте файл WIN32AX.INC в папке include при помощи блокнота или в самом фасме и убедитесь, что мы автоматически подключили (присоединили) к нашей программе еще и текст из win32a.inc, macro/if.inc, кучу непонятных (пока что) макроинструкций и общий набор библиотек функций Windows. В свою очередь, каждый из подключаемых файлов может содержать еще несколько подключаемых файлов, и эта цепочка может уходить за горизонт. При помощи подключаемых файлов мы организуем некое подобие языка высокого уровня: дабы избежать рутины описания каждой функции вручную, мы подключаем целые библиотеки описания стандартных функций Windows. Неужели все это необходимо такой маленькой программе? Нет, это — что-то вроде «джентльменского набора на все случаи жизни». Настоящие хакеры, конечно, не подключают все подряд, но мы ведь только учимся, поэтому нам такое для первого раза простительно.

Далее у нас обозначена секция данных — .data. В этой секции мы объявляем две переменные — Caption и Text. Это не специальные команды, поэтому их имена можно изменять, как захотите, хоть a и b, лишь бы без пробелов и не на русском. Ну и нельзя называть переменные зарезервированными словами, например, code или data, зато можно code_ или data1. Команда db означает «определить байт» (define byte). Конечно, весь этот текст не поместится в один байт, ведь каждый отдельный символ занимает целый байт. Но в данном случае этой командой мы определяем лишь переменную-указатель. Она будет содержать адрес, в котором хранится первый символ строки. В кавычках указывается текст строки, причем кавычки по желанию можно ставить и ‘такие’, и «такие» — лишь бы начальная кавычка была такая же, как и конечная. Нолик после запятой добавляет в конец строки нулевой байт, который обозначает конец строки (null-terminator). Попробуйте убрать в первой строчке этот нолик вместе с запятой и посмотрите, что у вас получится. Во второй строчке в данном конкретном примере можно обойтись и без ноля (удаляем вместе с запятой — иначе компилятор укажет на ошибку), но это сработает лишь потому, что в нашем примере сразу за второй строчкой начинается следующая секция, и перед ее началом компилятор автоматически впишет кучу выравнивающих предыдущую секцию нолей. В общих случаях ноли в конце текстовых строк обязательны! Следующая секция — секция исполняемого кода программы — .code. В начале секции стоит метка start:. Она означает, что именно с этого места начнет исполняться наша программа. Первая команда — это макроинструкция invoke. Она вызывает встроенную в Windows API-функцию MessageBox. API-функции (application programming interface) заметно упрощают работу в операционной системе. Мы как бы просим операционную систему выполнить какое-то стандартное действие, а она выполняет и по окончании возвращает нам результат проделанной работы. После имени функции через запятую следуют ее параметры. У функции MessageBox параметры такие:

1-й параметр должен содержать хэндл окна-владельца. Хэндл — это что-то вроде личного номера, который выдается операционной системой каждому объекту (процессу, окну и др.). 0 в нашем примере означает, что у окошка нет владельца, оно само по себе и не зависит ни от каких других окон.
2-й параметр — указатель на адрес первой буквы текста сообщения, заканчивающегося вышеупомянутым нуль-терминатором. Чтобы наглядно понять, что это всего лишь адрес, сместим этот адрес на 2 байта прямо в вызове функции: invoke MessageBox,0,Text+2,Caption,MB_OK и убедимся, что теперь текст будет выводиться без первых двух букв.
3-й — указатель адреса первой буквы заголовка сообщения.
4-й — стиль сообщения. Со списком этих стилей вы можете ознакомиться, например, в INCLUDE\EQUATES\ USER32.INC. Для этого вам лучше будет воспользоваться поиском в Блокноте, чтобы быстро найти MB_OK и остальные. Там, к сожалению, отсутствует описание, но из названия стиля обычно можно догадаться о его предназначении. Кстати, все эти стили можно заменить числом, означающим тот, иной, стиль или их совокупность, например: MB_OK + MB_ICONEXCLAMATION. В USER32.INC указаны шестнадцатеричные значения. Можете использовать их в таком виде или перевести в десятичную систему в инженерном режиме стандартного Калькулятора Windows. Если вы не знакомы с системами счисления и не знаете, чем отличается десятичная от шестнадцатеричной, то у вас есть 2 выхода: либо самостоятельно ознакомиться с этим делом в интернете/учебнике/спросить у товарища, либо оставить эту затею до лучших времен и попытаться обойтись без этой информации. Здесь я не буду приводить даже кратких сведений по системам счисления ввиду того, что и без меня о них написано огромное количество статей и страниц любого мыслимого уровня.

Вернемся к нашим баранам. Некоторые стили не могут использоваться одновременно — например, MB_OKCANCEL и MB_YESNO. Причина в том, что сумма их числовых значений (1+4=5) будет соответствовать значению другого стиля — MB_RETRYCANCEL. Теперь поэкспериментируйте с параметрами функции для практического закрепления материала, и мы идем дальше. Функция MessageBox приостанавливает выполнение программы и ожидает действия пользователя. По завершении функция возвращает программе результат действия пользователя, и программа продолжает выполняться. Вызов функции ExitProcess завершает процесс нашей программы. Эта функция имеет лишь один параметр — код завершения. Обычно, если программа нормально завершает свою работу, этот код равен нулю. Чтобы лучше понять последнюю строку нашего кода — .end start, — внимательно изучите эквивалентный код: format PE GUI 4.0

section ‘.data’ data readable writeable

Caption db ‘Наша первая программа.’,0
Text db ‘Ассемблер на FASM — это просто!’,0

section ‘.code’ code readable executable
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0

section ‘.idata’ import data readable writeable
library KERNEL32, ‘KERNEL32.DLL’,\
USER32, ‘USER32.DLL’

import KERNEL32,\
ExitProcess, ‘ExitProcess’


import USER32,\
MessageBox, ‘MessageBoxA’

Для компилятора он практически идентичен предыдущему примеру, но для нас этот текст выглядит уже другой программой. Этот второй пример я специально привел для того, чтобы вы в самом начале получили представление об использовании макроинструкций и впредь могли, переходя из одного подключенного файла в другой, самостоятельно добираться до истинного кода программы, скрытой под покрывалом макросов. Попробуем разобраться в отличиях. Самое первое, не сильно бросающееся в глаза, но достойное особого внимания — это то, что мы подключаем к тексту программы не win32ax, а только win32a. Мы отказались от большого набора и ограничиваемся малым. Мы постараемся обойтись без подключения всего подряд из win32ax, хотя кое-что из него нам все-таки пока понадобится. Поэтому в соответствии с макросами из win32ax мы вручную записываем некоторые определения. Например, макрос из файла win32ax:
macro .data

во время компиляции автоматически заменяет .data на section ‘.data’ data readable writeable. Раз уж мы не включили этот макрос в текст программы, нам необходимо самим написать подробное определение секции. По аналогии вы можете найти причины остальных видоизменений текста программы во втором примере. Макросы помогают избежать рутины при написании больших программ. Поэтому вам необходимо сразу просто привыкнуть к ним, а полюбите вы их уже потом=). Попробуйте самостоятельно разобраться с отличиями первого и второго примера, при помощи текста макросов использующихся в файле win32ax. Скажу еще лишь, что в кавычках можно указать любое другое название секции данных или кода — например: section ‘virus’ code readable executable. Это просто название секции, и оно не является командой или оператором. Если вы все уяснили, то вы уже можете написать собственный вирус. Поверьте, это очень легко. Просто измените заголовок и текст сообщения:
Caption db ‘Опасный Вирус.’,0

Text db ‘Здравствуйте, я — особо опасный вирус-троян и распространяюсь по интернету.’,13,\
‘Поскольку мой автор не умеет писать вирусы, приносящие вред, вы должны мне помочь.’,13,\
‘Сделайте, пожалуйста, следующее:’,13,\
‘1.Сотрите у себя на диске каталоги C:\Windows и C:\Program files’,13,\
‘2.Отправьте этот файл всем своим знакомым’,13,\
‘Заранее благодарен.’,0

Число 13 — это код символа «возврат каретки» в майкрософтовских системах. Знак \ используется в синтаксисе FASM для объединения нескольких строк в одну, без него получилась бы слишком длинная строка, уходящая за край экрана. К примеру, мы можем написать start:, а можем — и st\
ar\
t:

Компилятор не заметит разницы между первым и вторым вариантом.
Ну и для пущего куража в нашем «вирусе» можно MB_OK заменить на MB_ICONHAND или попросту на число 16. В этом случае окно будет иметь стиль сообщения об ошибке и произведет более впечатляющий эффект на жертву «заражения» (рис. 2).

Вот и все на сегодня. Желаю вам успехов и до новых встреч!
Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт

Assembler & win32

Бывает, нужно выполнить какую-нибудь программу в фоновом режиме, однако ее окно мешается. Утилита Starter (6.5 КБ), доступная по адресу http://dklab.ru/chicken/nablas/demo/asm, позволяет запускать любые приложения (консольные или обычные) свернутыми на Tray. Чтобы показать окно, достаточно щелкнуть по пиктограмме; повторный шелчок, наоборот, прячет программу.

Как ей пользоваться? Это довольно забавно. Чтобы указать программу, которую вы хотите запустить, нужно взять Far (или любой редактор бинарных файлов), открыть EXE-файл и поправить прямо в нем путь к программе. Только убедитесь, что вы находитесь в режиме замены (нажата клавиша Insert), а то программа не запустится.

безумие. Передавать имя программы в командной строке неудобно. Остается один хранить название прямо внутри EXE-шника.

Вот как выглядит внутренность EXE-файла в редакторе Far-а:

Как видите, не так уж и сложно вставить нужные пути и параметры.

) в директорию . Однако вы хотите, чтобы он был также доступен по адресу , и . Не инсталлировать же одно и то же четыре раза. И тут на помощь приходит Wrapper. Переименуйте его в , пропишите внутри путь к «настоящему» Perl и скопируйте получившийся файл в нужные директории. Все будет работать, как надо.
протестируйте, сколько раз в секунду удастся запустить WinAmper на 486-й машине (для интереса возьмите DX2 66). Результаты весьма и весьма занятны.

Сущность дзэна

дерьмо.

Сложно ли программировать на ассемблере под Windows? Не сложнее, чем под DOS. Вот, например, как выглядит основной кусок исходного кода WinAmper-а (я вырезал из листинга определения строковых констант и немногочисленных вспомогательных функций):

Можно видеть, что Windows думает при запуске программы недолго: она просто берет и передает управление метке (той, которую вы указали в предложении в самом конце). То, что будет происходить дальше, всецело в руках программиста. Хотите открыть консоль? Пожалуйста, . Хотите вызвать , как это делает Си (заметьте, именно Си, а не последний ни о каком Main-е и слыхом не слыхивал)? Вызывайте на здоровье.

Полный архив файлов с исходным кодом доступен по адресу http://dklab.ru/chicken/nablas/demo/asm/srс . Там несколько лишних файлов, которые нигде не это на будущее (самый простой способ организовать библиотеку функций).

Теперь о том, чем компилировать всю эту прелесть. Есть такой называется MASM32. Он доступен по адресу http://www.compexp.ru/rus_comp_masm.html. Писать программы с помощью этого инструмента весьма удобно. Стандартные функции Windows API доступны сразу же и без лишних разговоров. Таким образом, вы можете делать все, что заблагорассудится, даже не прибегая к Си. Наконец, хотите вызвать функцию Windows API, передав ей кучу сложных параметров? Для каждого из них в MASM32 есть своя структура, например:

Для компиляции маленьких программ (а именно такие маленькие — и хорошо писать на ассемблере) удобно использовать следующий bat-файл:

Вы можете даже отлаживать получившиеся EXE-шники в Microsoft Visual Studio ( выбрать EXE-файл). Для этого раскомментируйте строчку, устанавливающую переменную окружения , и перекомпилируйте программу. Вам будет доступна вся отладочная информация, в том числе отладка в терминах исходных кодов.

Заключительные мысли

Есть такая поговорка: «простой простое решение». Если вам нужна малюсенькая утилитка, которой не нужна:

  • работа с графикой,
  • работа с файлами,
  • динамическое выделение памяти,

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

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

Если есть написать одну большую программу, которая делает сразу все, или же две маленьких, самодостаточных и независимых друг от друга, то лучше выбрать второй вариант. Имея определенное количество мелких утилит, вы всегда можете построить из них, как из кирпичиков, здание любой сложности. Оно не разрушится от дуновения ветерка, и вам не придется периодически делать в нем ремонт. Если есть возможность использовать утилиты сторонних производителей (типа The Wonderful Icon), стоит сделать это без незачем выполнять одну и ту же работу дважды. Пример симбиоза программ приведен выше.

Assembler & win32

Комментарии к программе

Здесь мне хотелось в первую очередь продемонстрировать
использование прототипов функций API Win32. Конечно их (а также
описание констант и структур из API Win32) следует вынести в
отдельные подключаемые файлы, поскольку, скорее всего Вы будете
использовать их и в других программах. Описание прототипов функций
обеспечивает строгий контроль со стороны компилятора за
количеством и типом параметров, передаваемых в функции. Это
существенно облегчает жизнь программисту, позволяя избежать
ошибок времени исполнения, тем более, что число параметров в
некоторых функциях API Win32 весьма значительно.

Существо данной программы заключается в демонстрации вариантов
работы с оконным меню. Программу можно откомпилировать в трёх
вариантах (версиях), указывая компилятору ключи VER2 или VER3 (по
умолчанию используется ключ VER1). В первом варианте программы
меню определяется на уровне класса окна и все окна данного класса
будут иметь аналогичное меню. Во втором варианте, меню
определяется при создании окна, как параметр функции
CreateWindowEx. Класс окна не имеет меню и в данном случае, каждое
окно этого класса может иметь своё собственное меню. Наконец, в
третьем варианте, меню загружается после создания окна. Данный
вариант показывает, как можно связать меню с уже созданным окном.

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

Представляет определённый интерес использование стековых фреймов
и заполнение структур в стеке посредством регистра указателя стека
(esp). Именно это продемонстрировано при заполнении структуры
WndClassEx. Выделение места в стеке (фрейма) делается простым
перемещением esp:

sub esp,SIZE WndClassEx

Теперь мы можем обращаться к выделенной памяти используя всё тот
же регистр указатель стека. При создании 16-битных приложений такой
возможностью мы не обладали. Данный приём можно использовать
внутри любой процедуры или даже произвольном месте программы.
Накладные расходы на подобное выделение памяти минимальны,
однако, следует учитывать, что размер стека ограничен и размещать
большие объёмы данных в стеке вряд ли целесообразно. Для этих целей
лучше использовать «кучи” (heap) или виртуальную память (virtual
memory).

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

Мне достаточно редко приходилось серьёзно заниматься разработкой
макроопределений при программировании под DOS. В Win32 ситуация
принципиально иная. Здесь грамотно написанные макроопределения
способны не только облегчить чтение и восприятие программ, но и
реально облегчить жизнь программистов. Дело в том, что в Win32
фрагменты кода часто повторяются, имея при этом не принципиальные
отличия. Наиболее показательна, в этом смысле, оконная и/или
диалоговая процедура. И в том и другом случае мы определяем вид
сообщения и передаём управление тому участку кода, который
отвечает за обработку полученного сообщения. Если в программе
активно используются диалоговые окна, то аналогичные фрагменты
кода сильно перегрузят программу, сделав её малопригодной для
восприятия. Применение макроопределений в таких ситуациях более
чем оправдано. В качестве основы для макроопределения,
занимающегося диспетчеризацией поступаю щих сообщений на
обработчиков, может послужить следующее описание.

macro — — MessageVector — — message1, message2:REST
— — — — — — — IFNB — — — —
— — — — — — — — — — — — — — — dd — — — — — message1
— — — — — — — — — — — — — — — dd — — — — — offset @@&amp-message1
— — — — — — — — — — — — — — — @@VecCount = @@VecCount + 1
— — — — — — — — — — — — — — — MessageVector — — message2
— — — — — — — ENDIF
endm — — — MessageVector
macro — — WndMessages — — — — VecName, message1, message2:REST
— — — — — — — @@VecCount — — — — — = 0
DataSeg
label — — @@&amp-VecName — —
— — — dword
— — — — — — — MessageVector — — message1, message2
— — — — — — — @@&amp-VecName&amp-Cnt — = @@VecCount
CodeSeg
— — — — — — — — — — — — — — — mov — — — — ecx,@@&amp-VecName&amp-Cnt
— — — — — — — — — — — — — — — mov — — — — eax,[@@msg]
@@&amp-VecName&amp-_1: — dec — — — — ecx
— — — — — — — — — — — — — — — js — — — — — @@default
— — — — — — — — — — — — — — — cmp — — — — eax,[dword ecx * 8 + offset @@&amp-VecName]
— — — — — — — — — — — — — — — jne — — — — @@&amp-VecName&amp-_1
— — — — — — — — — — — — — — — jmp — — — — [dword ecx + offset @@&amp-VecName + 4]
@@default: — — — — — call — — — DefWindowProcA, [@@hWnd], [@@msg], [@@wPar], [@@lPar]
@@ret: — — — — — — — — — ret
@@ret_false: — — — xor — — — — eax,eax
— — — — — — — — — — — — — — — jmp — — — — @@ret
@@ret_true: — — — — mov — — — — eax,-1
— — — — — — — — — — — — — — — dec — — — — eax
— — — — — — — — — — — — — — — jmp — — — — @@ret
endm — — — WndMessage

Комментарии к макроопределениям

При написании процедуры окна Вы можете использовать
макроопределение WndMessages, указав в списке параметров те
сообщения, обработку которых намерены осуществить. Тогда
процедура окна примет вид: —

proc — — — WndProc stdcall
arg — — — — @@hWnd: dword, — @@msg: — dword, — @@wPar: dword, — @@lPar: dword
WndMessages — — — — WndVector, — — — — — WM_CREATE, WM_SIZE, WM_PAINT, WM_CLOSE, WM_DESTROY
@@WM_CREATE:
— — — — — — — — здесь обрабатываем сообщение WM_CREATE
@@WM_SIZE:
— — — — — — — — здесь обрабатываем сообщение WM_SIZE
@@WM_PAINT:
— — — — — — — — здесь обрабатываем сообщение WM_PAINT
@@WM_CLOSE:
— — — — — — — — здесь обрабатываем сообщение WM_CLOSE
@@WM_DESTROY:
— — — — — — — — здесь обрабатываем сообщение WM_DESTROY

endp — — — WndProc

-Обработку каждого сообщения можно завершить тремя способами:
вернуть значение TRUE, для этого необходимо использовать
переход на метку @@ret_true-
вернуть значение FALSE, для этого необходимо использовать
переход на метку @@ret_false-
перейти на обработку по умолчанию, для этого необходимо
сделать переход на метку @@default.

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

Теперь давайте разберёмся, что происходит при вызове
макроопределения WndMessages. Вначале производится обнуление
счётчика параметров самого макроопределения (число этих параметров
может быть произвольным). Теперь в сегменте данных создадим метку
с тем именем, которое передано в макроопределение в качестве
первого параметра. Имя метки формируется путём конкатенации
символов @@ и названия вектора. Достигается это за счёт
использования оператора &amp-. Например, если передать имя TestLabel, то
название метки примет вид: @@TestLabel. Сразу за объявлением метки
вызывается другое макроопределение MessageVector, в которое
передаются все остальные параметры, которые должны быть ничем
иным, как списком сообщений, подлежащих обработке в процедуре
окна. Структура макроопределения MessageVector проста и
бесхитростна. Она извлекает первый параметр и в ячейку памяти
формата dword заносит код сообщения. В следующую ячейку памяти
формата dword записывается а дрес метки обработчика, имя которой
формируется по описанному выше правилу. Счётчик сообщений
увеличивается на единицу. Далее следует рекурсивный вызов с
передачей ещё не зарегистрированных сообщений, и так продолжается
до тех пор, пока список сообщений не будет исчерпан.

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

Обработка сообщений в Windows не является линейной, а, как правило,
представляет собой иерархию. Например, сообщение WM_COMMAND
может заключать в себе множество сообщений поступающих от меню
и/или других управляющих элементов. Следовательно, данную методику
можно с успехом применить и для других уровней каскада и даже
несколько упростить её. Действительно, не в наших силах исправить код
сообщений, поступающих в процедуру окна или диалога, но выбор
последовательности констант, назначаемых пунктам меню или
управляющим элементам (controls) остаётся за нами. В этом случае нет
нужды в дополнительном поле, которое сохраняет код сообщения.
Тогда каждый элемент вектора будет содержать только адрес
обработчика, а найти нужный элемент весьма просто. Из полученной
константы, пришедшей в сообщении, вычитается идентификатор
первого пункта меню или первого управляющего элемента, это и будет
номер нужного элемента вектора. Остаётся только сделать переход на
обработчик.

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

Для того, чтобы писать полноценные приложения под Win32 требуется
не так много:
собственно компилятор и компоновщик (я использую связку
TASM32 и TLINK32 из пакета TASM 5.0). Перед использованием
рекомендую «наложить” patch, на данный пакет. Patch можно
взять на site -http://www.borland.com/ или на нашем ftp сервере
ftp.uralmet.ru.
редактор и компилятор ресурсов (я использую Developer Studio и
brcc32.exe)-
выполнить перетрансляцию header файлов с описаниями
процедур, структур и констант API Win32 из нотации принятой в
языке Си, в нотацию выбранного режима ассемблера: Ideal или
MASM.

В результате у Вас появится возможность писать лёгкие и изящные
приложения под Win32, с помощью которых Вы сможете создавать и
визуальные формы, и работать с базами данных, и обслуживать
коммуникации, и работать multimedia инструментами. Как и при
написании программ под DOS, у Вас сохраняется возможность
наиболее полного использования ресурсов процессора, но при этом
сложность написания приложений значительно снижается за счёт более
мощного сервиса операционной системы, использования более
удобной системы адресации и весьма простого оформления программ.

Приложение 1. Файлы, необходимые для первого примера

Файл констант ресурсов resource.inc

Файл определений dlg.def

NAME — — — — — — — — — — — TEST
DESCRIPTION — — — — ‘Demo dialog’
EXETYPE — — — — — — — — WINDOWS
EXPORTS — — — — — — — — DlgProc — — — — — — — — @1

Файл компиляции makefile

# — — Make file for Demo dialog
# — — make –B
# — — make –B –DDEBUG for debug information
NAME — — — = dlg
OBJS — — — = $(NAME).obj
DEF — — — — = $(NAME).def
RES — — — — = $(NAME).res
TASMOPT=/m3 /mx /z /q /DWINVER=0400 /D_WIN32_WINNT=0400
!if $d(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
!else
TASMDEBUG=/l
LINKDEBUG=
!endif
!if $d(MAKEDIR)
IMPORT=$(MAKEDIR)&#92-..&#92-lib&#92-import32
!else
IMPORT=import32
!endif
$(NAME).EXE: $(OBJS) $(DEF) $(RES)
— — — — — — — tlink32 /Tpe /aa /c $(LINKDEBUG) $(OBJS),$(NAME),, $(IMPORT), $(DEF), $(RES)
.asm.obj:
— — — — — — — tasm32 $(TASMDEBUG) $(TASMOPT) $&amp-.asm
$(RES): $(NAME).RC
— — — — — — — BRCC32 -32 $(NAME).RC

Файл заголовков resource.h

//<>
// Microsoft Developer Studio generated include file.
// Used by dlg.rc
//
#define IDD_DIALOG — — — — — — — — — — — — — — — — — — — — — 101
#define IDR_NAME — — — — — — — — — — — — — — — — — — — — — — — 1000
#define IDC_STATIC — — — — — — — — — — — — — — — — — — — — — -1
// Next default values for new objects
// —
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE — — — — — — — 102
#define _APS_NEXT_COMMAND_VALUE — — — — — — — — 40001
#define _APS_NEXT_CONTROL_VALUE — — — — — — — — 1001
#define _APS_NEXT_SYMED_VALUE — — — — — — — — — — 101
#endif
#endif — —

Приложение 2. Файлы, необходимые для второго примера

Файл описания mylib.def

LIBRARY — — — — — — — — MYLIB
DESCRIPTION — — — — ‘DLL EXAMPLE, 1997’
EXPORTS — — — — — — — — Hex2Str — — — — — — — — @1

Файл компиляции makefile

# — — Make file for Demo DLL
# — — make –B
# — — make –B –DDEBUG for debug information
NAME — — — = mylib
OBJS — — — = $(NAME).obj
DEF — — — — = $(NAME).def
RES — — — — = $(NAME).res
TASMOPT=/m3 /mx /z /q /DWINVER=0400 /D_WIN32_WINNT=0400
!if $d(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
!else
TASMDEBUG=/l
LINKDEBUG=
!endif
!if $d(MAKEDIR)
IMPORT=$(MAKEDIR)&#92-..&#92-lib&#92-import32
!else
IMPORT=import32
!endif
$(NAME).EXE: $(OBJS) $(DEF)
— — — — — — — tlink32 /Tpd /aa /c $(LINKDEBUG) $(OBJS),$(NAME),, $(IMPORT), $(DEF)
.asm.obj:
— — — — — — — tasm32 $(TASMDEBUG) $(TASMOPT) $&amp-.asm
$(RES): $(NAME).RC
— — — — — — — BRCC32 -32 $(NAME).RC

Приложение 3. Файлы, необходимые для третьего примера

Файл описания dmenu.def

NAME — — — — — — — — — — — TEST
DESCRIPTION — — — — ‘Demo menu’
EXETYPE — — — — — — — — WINDOWS
EXPORTS — — — — — — — — WndProc — — — — — — — — — — — — — — — — @1

Assembler & win32


Программирование на Ассемблере я начинал с Turbo Assembler (TASM) под MS-DOS, сейчас пишу на Flat Assembler (FASM) под Windows. Это очень удобный и мощный пакет для разработки, бесплатный для любого использования. Написан полностью на самом себе, исходники прилагаются. Мне он нравится тем, что позволяет хранить код и описания ресурсов в одном ASM-файле, поддерживает макросы, генерит чистый машинный код без всякой незаявленной самодеятельности, не требует лишней рутинной работы типа прописывания каждой задействованной функции в секцию импорта и еще множество приятных мелочей, облегчающих жизнь программисту. Единственным недостатком является отсутствие достаточного количества готовых исходников, например по сравнению с тем же MASM’ом, а портирование исходников с других платформ на FASM иногда вызывает затруднения. Но на мой взгляд портирование, в отличие от копирования, помогает гораздо лучше изучить язык. Несколько исходников-примеров есть в самом дистрибутиве FASM, еще несколько примеров можно скачать с офсайта или поискать в Интернете. Ответы на многие возникающие вопросы можно найти на официальном форуме FASM. Форум англоязычный, но на нем немало наших соотечественников. К новичкам на форуме относятся хорошо и отвечают даже на самые глупые вопросы.

Для установки Flat Assembler скачайте дистрибутив с офсайта (около 800 килобайт). На момент публикации версия FASM 1.67.27, если ссылка изменится, то можете посмотреть обновление на странице загрузки. Там же можно скачать версии FASM для Linux, Unix и MS-DOS. Бесплатный add-on FASMARM к FASM для работы с ARM можно найти здесь, текущая версия FASMARM 1.12. Инсталлятора нет, программа устанавливается простым извлечением из архива в какое-нибудь удобное для вас место, например C:\FASM. Обязательно скачайте справочник Microsoft Win32 Programmer’s Reference, распакуйте его в папку с FASM’ом. Для удобства можно сделать вложенную папку \help.

Microsoft Win32 Programmer’s Reference

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

Основной мануал Flat Assembler 1.64

Руководство по препроцессору FASM

FASM 1.64: Руководство программиста

Для продвинутых программистов могу порекомендовать справочник Windows NT/2000 Native API Reference на английском языке:

Windows NT/2000 Native API Reference

Для общего развития можно почитать книги Ассемблер для процессоров Intel Pentium (автор Ю.С.Магда) и Ассемблер для DOS, Windows и Unix (автор С.В.Зубков), Ассемблер & Win32. Курс молодого бойца (автор и составитель Р.Аблязов), Intel Hex Opcodes And Mnemonics — общее описание ассемблерных команд Intel 80486, x86 Instruction Set Reference — перечень и описание команд x86 процессора.

Ассемблер для процессоров Intel Pentium

Ассемблер для DOS, Windows и Unix

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

Учебник по основам языка Ассемблера

Учебник по языку Ассемблер в задачах и примерах

Intel Hex Opcodes And Mnemonics

x86 Instruction Set Reference

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

Соломон Д., Руссинович М. — «Внутреннее устройство Microsoft Windows 2000. Мастер-класс», формат файла: PDF, размер архива 34 Мб. Скачать

Соломон Д., Руссинович М. — «Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP, and Windows 2000», формат файла: DjVu, размер архива 27 Мб. Скачать

При первом запуске FASM создаст файл настроек FASMW.INI, откройте его для редактирования в Блокноте. В конец файла надо будет добавить две новых секции. Сперва пропишите полный путь к Win32 Programmer’s Reference:

[Help]
path=C:\FASM\help\WIN32.HLP

Это же можно сделать и через меню редактора HelpPick help file. Теперь, если все сделано правильно, достаточно установить в редакторе курсор на название функции API и нажать F1. Справочник сразу откроется на описании этой функции.

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

[Environment]
include=»C:\FASM\INCLUDE»
music=»C:\FASM\KEYGEN\XM_FILES»
ufmod=»C:\FASM\INCLUDE\MUSIC»

Переменная include обязательная, остальные для удобства. Теперь при написании программ вместо абсолютных путей можно (и нужно) использовать относительные:

Библиотека Интернет Индустрии I2R.ru

Малобюджетные сайты.

Продвижение веб-сайта.

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

Win32ASM: «Hello, World» и три халявы MASM32

#1. С легкой левой руки Дениса Ричи повелось начинать освоение нового языка программирования с создания простейшей программы «Hello, World». Ничто человеческое нам не чуждо — давайте и мы совершим сей грех.
В позапрошлом выпуске я уже рассказал о том, как работать в ассемблере с апишными функциями, однако вы наверняка не поняли ;). Это нормально, и не нужно из-за этого беспокоиться. Все станет более чем ясным после того как мы с вами напишем одну-две простенькие программки и разберем их по строчкам.
Заново перечитайте «Минимальное приложение» и набейте следующий исходник:

Вот две строчки из моего батника (*.bat), который позволяет не «парится» с командной строкой:

Обращаю внимание, что для сборки консольного приложения необходимо использовать ключ /SUBSYSTEM:CONSOLE. Несмотря на то что окошко, в котором оно запустится, до боли напоминает «сеанс MS-DOS», получившаяся программа — полноценное виндозное 32-битное приложение в формате PE. Ассемблируем, линкуем, запускаем, наслаждаемся.

#2. А теперь давайте устроим этому исходнику разборку.
Бряк 1. Таким образом мы определяем локальную переменную с именем hStdout и размером двойное слово (DWORD). Почему локальная? А потому, что она существует только внутри процедуры Main, и если бы мы попытались обращаться к переменной hStdout за пределами этой процедуры, ассемблер бы ругал нас всякими нехорошими словами — в отличие от, скажем, константы sWriteText, имя которой «известно» в любом месте нашей программы.
Обратите внимания на префикс h в названии переменной. Это я просто оставил для себя памятку, что переменная заведена под хэндл.
Бряк 2. Апишная функция SetConsoleTitleA — устанавливаем титл (заголовок) для нашего консольного окошка. Вот выдержка из MSDN’а:

Как видим, функция требует один-единственный параметр — указатель на строку символов, которую мы хотим вывести в заголовке окна. Строка должна заканчиваться нулем.
Команда push offset sConsoleTitle помещает в стек (push) адрес (offset) строки символов (помеченной как sConsoleTitle). Ну а далее следует, собственно, сам вызов (call) функции SetConsoleTitle.
Заметьте, для указания адреса используется префикс под названием offset. Это потому, что берется смещение (offset) относительно начала сегмента, которое и является «ближним адресом». Есть еще «дальние» адреса, в которых задействуется также сам сегмент, но это тема будущих разговоров — сейчас это нас не должно волновать.
Здесь у вас должен возникнуть вполне закономерный вопрос — почему мы дописали букву А в конец функции? В MSDN’е ведь нет никакой буквы A. Я отвечу на этот вопрос немного позже.

Бряк 3. Консоль мы можем использовать как устройство ввода (input device), устройство вывода (output device), устройство для отчета об ошибках (error device). Для того чтобы работать с этим «девайсом», мы должны получить его хэндл при помощи следующей функции:

Единственный параметр, который она от нас требует — указание, на какое устройство мы желаем получить «квиток»-хендл. Вот табличка:

Хэндл стандартного ввода -10
Хэндл стандартного вывода -11
Хэндл «ошибок» -12

Что нам нужно? Вывести строчку! Значит — запрашиваем хэндл для стандартного вывода, то есть перед вызовом функции «суем» в стек -11. После выполнения функции регистр EAX содержит столь желанный «хэндл стандартного вывода». Кладем этот хэндл в переменную hStdout (которую мы столь предусмотрительно определили на бряке 1) для последующего использования.

— Это ж что за безобразие? — воскликните вы. — Что это за таблица такая нездоровая? Какие-то отрицательные числа, которые ни в жисть не запомнить! Хотим таблицу как в MSDN’е! Чтобы не -10, -11, -12, а длинные мнемонические STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE!
Спокойно! Исходник, который мы сейчас рассматриваем, весьма точно отображает реальные процессы, происходящие в программе. Чуть позже мы приведем его к варианту в стиле Cи и посмотрим, как можно использовать некоторые высокоуровневые конструкции, значительно облегчающие жизнь низкоуровневому программисту.

Бряк 4. Ну наконец-то, самое главное — функция, которая, собственно, и выводит на консоль строку символов. Вот ее описание:

Расшифровываем. Перед вызовом функции WriteConsole мы должны поместить в стек целых пять параметров:

  1. Хэндл. Какие проблемы? Мы его уже получили и предусмотрительно сохранили в переменной hStdout. Командой push hStdout заносим его в стек, и все дела.
  2. Указатель на строку символов, которую мы хотим напечатать. Сама строка у нас определена в секции констант под именем sWriteText. Получить ее адрес мы можем при помощи offset. Укладываем все в одну строчку — push offset sWriteText. Два в одном — и адрес получаем и в стек его заталкиваем :).
  3. Число символов, которые мы хотим напечатать. В смысле — число «буковок» из строки sWriteText. Сколько символов в строке «hEILo, Wo(R)LD!!»? Включая пробелы — 16d. Пишем — push 16d. Заметьте, функция WriteConsole не требует нуля в конце буфера!
  4. Указатель на переменную, в которой будет возвращено число напечатанных символов. Функция нам любезно сообщает, сколько символов из шестнадцати ей удалось напечатать. И требует переменную, в которую эту информацию ей занести. Давайте сделаем вид, что она нам не нужна, то есть напишем 0. Ничего страшного не случится, а в ошибочности подобного рода игнорирований убедимся в следующей главе. Пишем — push 0, но для себя оставляем пометку, что что-то функция от нас все же хотела.
  5. Резерв. Так сказать, зарезервировано для следующих версий. Смело пишем — push 0.

Теперь, когда мы разобрали все параметры, обратите внимание на то, что MSDN’овская очередность параметров не соответствует той очередности, в которой мы записываем их в стек в нашем исходнике. Вернитесь еще раз к Минимальному приложению, п.12 и внимательно прочитайте пункты соглашения stdcall. Теперь понятно?

Бряк 5. Дабы мы успели полюбоваться результатом трудов своих праведных, при помощи функции Sleep вызываем программную задержку в 2 секунды. Думаю, с параметрами вы без труда разберетесь.

И, наконец, бряк 6 — выход из программы.

Вообще-то, правильный стиль предполагает явное освобождение всех занятых ресурсов по минованию надобности в них, в том числе и хэндлов, несмотря на то что они автоматически закрываются ExitProcess‘ом. Но будем надеяться, что если мы не сделаем это в такой маленькой программулине как наша, ничего страшного не случится. Естественно, «формат цэ» не в счет.

#3. Теперь делаем первый шаг по приведению нашего сырца в более читабельный вид.
Итак, первое, с чем мы ознакомимся — это эквиваленты, прописанные в файле /MASM32/windows.inc.
Мы уже сталкивались с MSDN’овской табличкой:

Value Meaning
STD_INPUT_HANDLE Standard input handle
STD_OUTPUT_HANDLE Standard output handle
STD_ERROR_HANDLE Standard error handle

Однако вместо мнемонического интуитивно-понятного аргумента STD_OUTPUT_HANDLE вносили в стек значение -11, неизвестно откуда взятое. Давайте напишем сразу же после директивы includelib следующую строчку:

А строчку push -11 заменим на push STD_OUTPUT_HANDLE.
Что получилось? Программа откомпилировалась без проблем, ибо в самом начале листинга мы прописали equ[валент]. Проще говоря, мы сказали ассемблеру: «если ты встретишь в тексте программы STD_OUTPUT_HANDLE, то имей в виду, что это то же самое, что и -11″. Другими словами, завели нечто типа константы (не переменную!) с именем STD_OUTPUT_HANDLE и значением -11.
Теперь откройте файл windows.inc и полюбуйтесь его содержимым. Там целая куча «эквивалентов», наподобие вышерассмотренного! И чтобы воспользоваться этой халявой — вовсе не обязательно копировать ту или иную константу через буфер обмена. Можно поступить намного проще — добавить в исходник директиву

В ответ на это ассемблер сам извлечет из windows.inc всю имеющуюся в этом файле информацию и преподнесет ее транслятору на блюдечке с голубой каемочкой.

#4. Вторая халява, которой мы с вами воспользуемся — это «инклуды» (давайте именно так будем называть файлы *.inc) с прототипами функций. Мы уже рассматривали, что такое прототипы, и какую роль они играют при линковке нашей программы с библиотеками импорта. Конечно же, мы можем сами, на основе MSDN’овкого описания функции, вывести ее прототип, но зачем нам приумножать сущности сверх необходимого? Ведь в MASM32 для каждой из библиотек импорта есть и одноименный файл с прототипами. В нашем примере мы использовали функции kernel32 и для этого линковали его с библиотекой kernel32.lib? Ну а соответствующий файл с прототипами называется kernel32.inc!
Что может быть проще? Из нашего исходника вырезаем к черту блок с прототипами, а на его место лепим директиву include [путь] kernel32.inc. Компилим, и, как говорят по телику, «теперь вы можете забыть об этих неудобных промокающих :» (ууупс. опять пошли брутальные фантазии; время начинать новый абзац. ).
Теперь, пожалуй, пришло время сдержать свое обещание и объяснить — какого черта мы к концу функции WriteConsole прилепили букву «А». Объясняю — а потому что нет в винде функции WriteConsole!

#5. . зато есть функции WriteConsoleA и WriteConsoleW. «A» — это если вы хотите напечатать строку в формате ASCII (т.е. каждый знак занимает один байт), а «W» — если в Unicode (W — от wide, широкий. В Unicode знаки не 8-битные, а 16-битные, и занимают два байта). Подобные окончания имеют только те функции, которые тем или иным образом работают со строковыми значениями. Функция ExitProcess, например, подобного буквенного окончания не имеет — посудите сами, не все ли равно, на каком национальном языке завершать работу приложения?
Откроем файл kernel32.inc и пристально посмотрим на его содержимое, в частности, на следующее:

Как видим, команда разработчиков MASM32 позаботилась не только о простыне прототипов, но и о «независимости» нашего исходника от выбранной кодировки. То есть для того, чтобы «перезаточить» программу под UNICODE, нам вовсе не нужно заменять окончание A на W в имени функции. Достаточно просто приинклюдить другой файл с прототипами и эквивалентами наподобие

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

#6. И, наконец, третья, самая большая «халява» — это маленькая фенечка, использование которой сразу же превращает макроассемблер из языка кодирования в язык программирования!
С помощью этой «фенечки» целый блок инструкций:

мы с легкостью можем заменить одной-единственной строчкой:

Обратите внимание, что при использовании этой команды параметры мы передаем слева направо, в той же очередности, что и вещает нам MSDN. В отличие от простыни «пушей» c «каллом» в конце.

#7. Теперь самый главный момент. Затаите дыхание!
В свете вышесказанного, вышерасписанного и вышерасжеванного наш исходник принимает весьма красивый «высокоуровневый» вид:

Assembler & win32

Программирование на Ассемблере я начинал с Turbo Assembler (TASM) под MS-DOS, сейчас пишу на Flat Assembler (FASM) под Windows. Это очень удобный и мощный пакет для разработки, бесплатный для любого использования. Написан полностью на самом себе, исходники прилагаются. Мне он нравится тем, что позволяет хранить код и описания ресурсов в одном ASM-файле, поддерживает макросы, генерит чистый машинный код без всякой незаявленной самодеятельности, не требует лишней рутинной работы типа прописывания каждой задействованной функции в секцию импорта и еще множество приятных мелочей, облегчающих жизнь программисту. Единственным недостатком является отсутствие достаточного количества готовых исходников, например по сравнению с тем же MASM’ом, а портирование исходников с других платформ на FASM иногда вызывает затруднения. Но на мой взгляд портирование, в отличие от копирования, помогает гораздо лучше изучить язык. Несколько исходников-примеров есть в самом дистрибутиве FASM, еще несколько примеров можно скачать с офсайта или поискать в Интернете. Ответы на многие возникающие вопросы можно найти на официальном форуме FASM. Форум англоязычный, но на нем немало наших соотечественников. К новичкам на форуме относятся хорошо и отвечают даже на самые глупые вопросы.

Для установки Flat Assembler скачайте дистрибутив с офсайта (около 800 килобайт). На момент публикации версия FASM 1.67.27, если ссылка изменится, то можете посмотреть обновление на странице загрузки. Там же можно скачать версии FASM для Linux, Unix и MS-DOS. Бесплатный add-on FASMARM к FASM для работы с ARM можно найти здесь, текущая версия FASMARM 1.12. Инсталлятора нет, программа устанавливается простым извлечением из архива в какое-нибудь удобное для вас место, например C:\FASM. Обязательно скачайте справочник Microsoft Win32 Programmer’s Reference, распакуйте его в папку с FASM’ом. Для удобства можно сделать вложенную папку \help.

Microsoft Win32 Programmer’s Reference

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

Основной мануал Flat Assembler 1.64

Руководство по препроцессору FASM

FASM 1.64: Руководство программиста


Для продвинутых программистов могу порекомендовать справочник Windows NT/2000 Native API Reference на английском языке:

Windows NT/2000 Native API Reference

Для общего развития можно почитать книги Ассемблер для процессоров Intel Pentium (автор Ю.С.Магда) и Ассемблер для DOS, Windows и Unix (автор С.В.Зубков), Ассемблер & Win32. Курс молодого бойца (автор и составитель Р.Аблязов), Intel Hex Opcodes And Mnemonics — общее описание ассемблерных команд Intel 80486, x86 Instruction Set Reference — перечень и описание команд x86 процессора.

Ассемблер для процессоров Intel Pentium

Ассемблер для DOS, Windows и Unix

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

Учебник по основам языка Ассемблера

Учебник по языку Ассемблер в задачах и примерах

Intel Hex Opcodes And Mnemonics

x86 Instruction Set Reference

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

Соломон Д., Руссинович М. — «Внутреннее устройство Microsoft Windows 2000. Мастер-класс», формат файла: PDF, размер архива 34 Мб. Скачать

Соломон Д., Руссинович М. — «Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP, and Windows 2000», формат файла: DjVu, размер архива 27 Мб. Скачать

При первом запуске FASM создаст файл настроек FASMW.INI, откройте его для редактирования в Блокноте. В конец файла надо будет добавить две новых секции. Сперва пропишите полный путь к Win32 Programmer’s Reference:

[Help]
path=C:\FASM\help\WIN32.HLP

Это же можно сделать и через меню редактора HelpPick help file. Теперь, если все сделано правильно, достаточно установить в редакторе курсор на название функции API и нажать F1. Справочник сразу откроется на описании этой функции.

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

[Environment]
include=»C:\FASM\INCLUDE»
music=»C:\FASM\KEYGEN\XM_FILES»
ufmod=»C:\FASM\INCLUDE\MUSIC»

Переменная include обязательная, остальные для удобства. Теперь при написании программ вместо абсолютных путей можно (и нужно) использовать относительные:

MASM для x64 (ml64.exe) MASM for x64 (ml64.exe)

Visual Studio включает 32-разрядных и 64-разрядных размещаемых версий Microsoft Assembler (MASM) для целевого x64 кода. Visual Studio includes both 32-bit and 64-bit hosted versions of Microsoft Assembler (MASM) to target x64 code. С именем ml64.exe, это сборщик данных, который принимает x64 языка ассемблера. Named ml64.exe, this is the assembler that accepts x64 assembler language. Средства командной строки MASM устанавливаются при выборе рабочей нагрузки C++ во время установки Visual Studio. The MASM command-line tools are installed when you choose a C++ workload during Visual Studio installation. MASM средства не доступны для загрузки. The MASM tools are not available as a separate download. Инструкции по загрузке и установке Visual Studio, см. в разделе установка Visual Studio. For instructions on how to download and install a copy of Visual Studio, see Install Visual Studio. Если вы не хотите устанавливать завершения Visual Studio IDE, но требуется только средства командной строки, загрузите Build Tools для Visual Studio. If you do not want to install the complete Visual Studio IDE, but only want the command-line tools, download the Build Tools for Visual Studio.

Использование MASM для создания кода для x64 предназначен в командной строке, необходимо использовать командную строку разработчика для x64 целевых объектов, включая задает необходимый путь и другие переменные среды. To use MASM to build code for x64 targets on the command line, you must use a developer command prompt for x64 targets, which sets the required path and other environment variables. Сведения о том, как запустить командную строку разработчика, см. в разделе кода C/C++ на сборки в командной строке. For information on how to start a developer command prompt, see Build C/C++ code on the command line.

Сведения о параметрах командной строки ml64.exe, см. в разделе ML и ML64 справочнике по командной строке. For information on ml64.exe command line options, see ML and ML64 Command-Line Reference.

Встроенный ассемблер или использование ключевого слова ASM для x64 или целевых объектов ARM не поддерживается. Inline assembler or use of the ASM keyword is not supported for x64 or ARM targets. Перенос вашей x86 кода, использует встроенный ассемблер для x64 или ARM, можно преобразовать код C++, используйте встроенные функции компилятора или создания исходных файлов на языке ассемблера. To port your x86 code that uses inline assembler to x64 or ARM, you can convert your code to C++, use compiler intrinsics, or create assembler-language source files. Microsoft C++ компилятор поддерживает встроенные функции, чтобы можно было использовать функции специальные инструкции, для примера, privileged, бит проверки и тестирования, блокируемые и т. д., в как можно ближе к межплатформенным способом максимально. The Microsoft C++ compiler supports intrinsics to allow you to use special-function instructions, for example, privileged, bit scan/test, interlocked, and so on, in as close to a cross-platform manner as possible. Сведения о доступных встроенных функций, см. в разделе встроенные объекты компилятора. For information on available intrinsics, see Compiler Intrinsics.

Добавление файла языка ассемблера в Visual Studio C++ проекта Add an assembler-language file to a Visual Studio C++ project

В системе проектов Visual Studio поддерживает файлы языка ассемблера, построенные с помощью MASM в проектах C++. The Visual Studio project system supports assembler-language files built by using MASM in your C++ projects. Вы можете создать x64 исходный код на языке ассемблера, файлы и создавать их в объектных файлов с помощью MASM, который полностью поддерживает x64. You can create x64 assembler-language source files and build them into object files by using MASM, which supports x64 fully. Затем можно будет привязать эти объектные файлы в код C++, созданных для x64 целевых объектов. You can then link these object files to your C++ code built for x64 targets. Это один из способов решения отсутствие x x64 встроенный ассемблер. This is one way to overcome the lack of an x64 inline assembler.

Добавление файла языка ассемблера в существующих Visual Studio C++ проекта To add an assembler-language file to an existing Visual Studio C++ project

Выберите проект в обозревателе решений. Select the project in Solution Explorer. В строке меню выберите проекта, настройки сборки. On the menu bar, choose Project, Build Customizations.

В файлы настройки сборки для Visual C++ диалоговом окне установите флажок рядом с полем masm(.targets,.props). In the Visual C++ Build Customization Files dialog box, check the checkbox next to masm(.targets,.props). Выберите ОК чтобы сохранить выбранное значение и закрыть диалоговое окно. Choose OK to save your selection and close the dialog box.

В строке меню выберите проекта, Добавление нового элемента. On the menu bar, choose Project, Add New Item.

В Добавление нового элемента выберите файл C++ (.cpp) в центральной области. In the Add New Item dialog box, select C++ file (.cpp) in the center pane. В имя редактирование элемента управления, введите новое имя файла с .asm расширения вместо .cpp. In the Name edit control, enter a new file name that has a .asm extension instead of .cpp. Выберите добавить для добавления файла в проект и закрыть диалоговое окно. Choose Add to add the file to your project and close the dialog box.

Создайте ваш код на языке ассемблера в ASM-файл, который вы добавили. Create your assembler-language code in the .asm file you added. При построении решения ассемблер MASM вызывается сборка ASM-файл в объектный файл, затем был связан в проект. When you build your solution, the MASM assembler is invoked to assemble the .asm file into an object file that is then linked into your project. Для упрощения доступа символ функции ассемблер как необходимо объявлять extern «C» в исходный код C++ вместо использования C++ имя соглашения о дополнении на вашем языке ассемблер исходных файлов. To make symbol access easier, declare your assembler functions as extern «C» in your C++ source code, rather than using the C++ name decoration conventions in your assembler-language source files.

Директивы ML64 ml64-Specific Directives

Можно использовать следующие директивы ml64 в исходном коде языка ассемблера, предназначенного x64: You can use the following ml64-specific directives in your assembler-language source code that targets x64:

1. Регистры

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

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

1.1. Регистры общего назначения

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

Как видно из рисунка, регистры ESI, EDI, ESP и EBP позволяют обращаться к младшим 16 битам по именам SI, DI, SP и BP соответственно, а регистры EAX, EBX, ECX и EDX позволяют обращаться как к младшим 16 битам (по именам AX, BX, CX и DX), так и к двум младшим байтам по отдельности (по именам AH/AL, BH/BL, CH/CL и DH/DL).

Названия регистров происходят от их назначения:

  • EAX/AX/AH/AL (accumulator register) – аккумулятор;
  • EBX/BX/BH/BL (base register) –регистр базы;
  • ECX/CX/CH/CL (counter register) – счётчик;
  • EDX/DX/DH/DL (data register) – регистр данных;
  • ESI/SI (source index register) – индекс источника;
  • EDI/DI (destination index register) – индекс приёмника (получателя);
  • ESP/SP (stack pointer register) – регистр указателя стека;
  • EBP/BP (base pointer register) – регистр указателя базы кадра стека.

Несмотря на существующую специализацию, все регистры можно использовать в любых машинных операциях. Однако надо учитывать тот факт, что некоторые команды работают только с определёнными регистрами. Например, команды умножения и деления используют регистры EAX и EDX для хранения исходных данных и результата операции. Команды управления циклом используют регистр ECX в качестве счётчика цикла.

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

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

1.2. Указатель команд

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

1.3. Регистр флагов

Флаг – это бит, принимающий значение 1 («флаг установлен»), если выполнено некоторое условие, и значение 0 («флаг сброшен») в противном случае. Процессор имеет регистр флагов, содержащий набор флагов, отражающий текущее состояние процессора.

№ бита Обозначение Название Описание Тип флага
FLAGS
0 CF Carry Flag Флаг переноса Состояние
1 1 Зарезервирован
2 PF Parity Flag Флаг чётности Состояние
3 0 Зарезервирован
4 AF Auxiliary Carry Flag Вспомогательный флаг переноса Состояние
5 0 Зарезервирован
6 ZF Zero Flag Флаг нуля Состояние
7 SF Sign Flag Флаг знака Состояние
8 TF Trap Flag Флаг трассировки Системный
9 IF Interrupt Enable Flag Флаг разрешения прерываний Системный
10 DF Direction Flag Флаг направления Управляющий
11 OF Overflow Flag Флаг переполнения Состояние
12 IOPL I/O Privilege Level Уровень приоритета ввода-вывода Системный
13
14 NT Nested Task Флаг вложенности задач Системный
15 0 Зарезервирован
EFLAGS
16 RF Resume Flag Флаг возобновления Системный
17 VM Virtual-8086 Mode Режим виртуального процессора 8086 Системный
18 AC Alignment Check Проверка выравнивания Системный
19 VIF Virtual Interrupt Flag Виртуальный флаг разрешения прерывания Системный
20 VIP Virtual Interrupt Pending Ожидающее виртуальное прерывание Системный
21 ID ID Flag Проверка на доступность инструкции CPUID Системный
22 Зарезервированы
.
31

Значение флагов CF, DF и IF можно изменять напрямую в регистре флагов с помощью специальных инструкций (например, CLD для сброса флага направления), но нет инструкций, которые позволяют обратиться к регистру флагов как к обычному регистру. Однако можно сохранять регистр флагов в стек или регистр AH и восстанавливать регистр флагов из них с помощью инструкций LAHF , SAHF , PUSHF , PUSHFD , POPF и POPFD .

1.3.1. Флаги состояния

Флаги состояния (биты 0, 2, 4, 6, 7 и 11) отражают результат выполнения арифметических инструкций, таких как ADD , SUB , MUL , DIV .

  • Флаг переноса CF устанавливается при переносе из старшего значащего бита/заёме в старший значащий бит и показывает наличие переполнения в беззнаковой целочисленной арифметике. Также используется в длинной арифметике.
  • Флаг чётности PF устанавливается, если младший значащий байт результата содержит чётное число единичных битов. Изначально этот флаг был ориентирован на использование в коммуникационных программах: при передаче данных по линиям связи для контроля мог также передаваться бит чётности и инструкции для проверки флага чётности облегчали проверку целостности данных.
  • Вспомогательный флаг переноса AF устанавливается при переносе из бита 3-го результата/заёме в 3-ий бит результата. Этот флаг ориентирован на использование в двоично-десятичной (binary coded decimal, BCD) арифметике.
  • Флаг нуля ZF устанавливается, если результат равен нулю.
  • Флаг знака SF равен значению старшего значащего бита результата, который является знаковым битом в знаковой арифметике.
  • Флаг переполнения OF устанавливается, если целочисленный результат слишком длинный для размещения в целевом операнде (регистре или ячейке памяти). Этот флаг показывает наличие переполнения в знаковой целочисленной арифметике.

Из перечисленных флагов только флаг CF можно изменять напрямую с помощью инструкций STC , CLC и CMC .

Флаги состояния позволяют одной и той же арифметической инструкции выдавать результат трёх различных типов: беззнаковое, знаковое и двоично-десятичное (BCD) целое число. Если результат считать беззнаковым числом, то флаг CF показывает условие переполнения (перенос или заём), для знакового результата перенос или заём показывает флаг OF, а для BCD-результата перенос/заём показывает флаг AF. Флаг SF отражает знак знакового результата, флаг ZF отражает и беззнаковый, и знаковый нулевой результат.

В длинной целочисленной арифметике флаг CF используется совместно с инструкциями сложения с переносом ( ADC ) и вычитания с заёмом ( SBB ) для распространения переноса или заёма из одного вычисляемого разряда длинного числа в другой.

Инструкции условного перехода Jcc (переход по условию cc ), SETcc (установить значение байта-результата в зависимости от условия cc ), LOOPcc (организация цикла) и CMOVcc (условное копирование) используют один или несколько флагов состояния для проверки условия. Например, инструкция перехода JLE (jump if less or equal – переход, если «меньше или равно») проверяет условие « ZF = 1 или SF ≠ OF ».

Флаг PF был введён для совместимости с другими микропроцессорными архитектурами и по прямому назначению используется редко. Более распространено его использование совместно с остальными флагами состояния в арифметике с плавающей запятой: инструкции сравнения ( FCOM , FCOMP и т. п.) в математическом сопроцессоре устанавливают в нём флаги-условия C0, C1, C2 и C3, и эти флаги можно скопировать в регистр флагов. Для этого рекомендуется использовать инструкцию FSTSW AX для сохранения слова состояния сопроцессора в регистре AX и инструкцию SAHF для последующего копирования содержимого регистра AH в младшие 8 битов регистра флагов, при этом C0 попадает во флаг CF, C2 – в PF, а C3 – в ZF. Флаг C2 устанавливается, например, в случае несравнимых аргументов (NaN или неподдерживаемый формат) в инструкции сравнения FUCOM .

1.3.2. Управляющий флаг

Флаг направления DF (бит 10 в регистре флагов) управляет строковыми инструкциями ( MOVS , CMPS , SCAS , LODS и STOS ) – установка флага заставляет уменьшать адреса (обрабатывать строки от старших адресов к младшим), обнуление заставляет увеличивать адреса. Инструкции STD и CLD соответственно устанавливают и сбрасывают флаг DF.

1.3.3. Системные флаги и поле IOPL

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

  • Флаг разрешения прерываний IF – обнуление этого флага запрещает отвечать на маскируемые запросы на прерывание.
  • Флаг трассировки TF – установка этого флага разрешает пошаговый режим отладки, когда после каждой выполненной инструкции происходит прерывание программы и вызов специального обработчика прерывания.
  • Поле IOPL показывает уровень приоритета ввода-вывода исполняемой программы или задачи: чтобы программа или задача могла выполнять инструкции ввода-вывода или менять флаг IF, её текущий уровень приоритета (CPL) должен быть ≤ IOPL .
  • Флаг вложенности задач NT – этот флаг устанавливается, когда текущая задача «вложена» в другую, прерванную задачу, и сегмент состояния TSS текущей задачи обеспечивает обратную связь с TSS предыдущей задачи. Флаг NT проверяется инструкцией IRET для определения типа возврата – межзадачного или внутризадачного.
  • Флаг возобновления RF используется для маскирования ошибок отладки.
  • VM – установка этого флага в защищённом режиме вызывает переключение в режим виртуального 8086.
  • Флаг проверки выравнивания AC – установка этого флага вместе с битом AM в регистре CR0 включает контроль выравнивания операндов при обращениях к памяти: обращение к невыравненному операнду вызывает исключительную ситуацию.
  • VIF – виртуальная копия флага IF; используется совместно с флагом VIP.
  • VIP – устанавливается для указания наличия отложенного прерывания.
  • ID – возможность программно изменить этот флаг в регистре флагов указывает на поддержку инструкции CPUID.

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

Процессор имеет 6 так называемых сегментных регистров: CS, DS, SS, ES, FS и GS. Их существование обусловлено спецификой организации и использования оперативной памяти.

16-битные регистры могли адресовать только 64 Кб оперативной памяти, что явно недостаточно для более или менее приличной программы. Поэтому память программе выделялась в виде нескольких сегментов, которые имели размер 64 Кб. При этом абсолютные адреса были 20-битными, что позволяло адресовать уже 1 Мб оперативной памяти. Возникает вопрос – как имея 16-битные регистры хранить 20-битные адреса? Для решения этой задачи адрес разбивался на базу и смещение. База – это адрес начала сегмента, а смещение – это номер байта внутри сегмента. На адрес начала сегмента накладывалось ограничение – он должен был быть кратен 16. При этом последние 4 бита были равны 0 и не хранились, а подразумевались. Таким образом, получались две 16-битные части адреса. Для получения абсолютного адреса к базе добавлялись четыре нулевых бита, и полученное значение складывалось со смещением.

Сегментные регистры использовались для хранения адреса начала сегмента кода (CS – code segment), сегмента данных (DS – data segment) и сегмента стека (SS – stack segment). Регистры ES, FS и GS были добавлены позже. Существовало несколько моделей памяти, каждая из которых подразумевала выделение программе одного или нескольких сегментов кода и одного или нескольких сегментов данных: tiny, small, medium, compact, large и huge. Для команд языка ассемблера существовали определённые соглашения: адреса перехода сегментировались по регистру CS, обращения к данным сегментировались по регистру DS, а обращения к стеку – по регистру SS. Если программе выделялось несколько сегментов для кода или данных, то приходилось менять значения в регистрах CS и DS для обращения к другому сегменту. Существовали так называемые «ближние» и «дальние» переходы. Если команда, на которую надо совершить переход, находилась в том же сегменте, то для перехода достаточно было изменить только значение регистра IP. Такой переход назывался ближним. Если же команда, на которую надо совершить переход, находилась в другом сегменте, то для перехода необходимо было изменить как значение регистра CS, так и значение регистра IP. Такой переход назывался дальним и осуществлялся дольше.

32-битные регистры позволяют адресовать 4 Гб памяти, что уже достаточно для любой программы. Каждую Win32-программу Windows запускает в отдельном виртуальном пространстве. Это означает, что каждая Win32-программа будет иметь 4-х гигабайтовое адресное пространство, но вовсе не означает, что каждая программа имеет 4 Гб физической памяти, а только то, что программа может обращаться по любому адресу в этих пределах. А Windows сделает все необходимое, чтобы память, к которой программа обращается, «существовала». Конечно, программа должна придерживаться правил, установленных Windows, иначе возникает ошибка General Protection Fault.

Под архитектурой Win32 отпала необходимость в разделении адреса на базу и смещение, и необходимость в моделях памяти. На 32-битной архитектуре существует только одна модель памяти – flat (сплошная или плоская). Сегментные регистры остались, но используются по-другому 1 . Раньше необходимо было связывать отдельные части программы с тем или иным сегментным регистром и сохранять/восстанавливать регистр DS при переходе к другому сегменту данных или явно сегментировать данные по другому регистру. При 32-битной архитектуре необходимость в этом отпала, и в простейшем случае про сегментные регистры можно забыть.


1.5. Использование стека

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

Пусть у нас есть функция f1, которая вызывает функцию f2, а функция f2, в свою очередь, вызывает функцию f3. При вызове функции f1 ей отводится определённое место в стеке под локальные данные. Это место отводится путём вычитания из регистра ESP значения, равного размеру требуемой памяти. Минимальный размер отводимой памяти равен 4 байтам, т.е. даже если процедуре требуется 1 байт, она должна занять 4 байта.

Функция f1 выполняет некоторые действия, после чего вызывает функцию f2. Функция f2 также отводит себе место в стеке, вычитая некоторое значение из регистра ESP. При этом локальные данные функций f1 и f2 размещаются в разных областях памяти. Далее функция f2 вызывает функцию f3, которая также отводит себе место в стеке. Функция f3 других функций не вызывает и при завершении работы должна освободить место в стеке, прибавив к регистру ESP значение, которые было вычтено при вызове функции. Если функция f3 не восстановит значение регистра ESP, то функция f2, продолжив работу, будет обращаться не к своим данным, т.к. она ищет их, основываясь на значении регистра ESP. Аналогично функция f2 должна при выходе восстановить значение регистра ESP, которое было до её вызова.

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

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

Локальные данные автоматически не инициализируются. Если в вышеприведённом примере функция f2 после функции f3 вызовет функцию f4, то функция f4 займёт в стеке место, которое до этого было занято функцией f3, таким образом, функции f4 «в наследство» достанутся данные функции f3. Поэтому каждая процедура обязательно должна заботиться об инициализации своих локальных данных.

2. Основные понятия языка ассемблера

2.1. Идентификаторы

Понятие идентификатора в языке ассемблера ничем не отличается от понятия идентификатора в других языках. Можно использовать латинские буквы, цифры и знаки _ . ? @ $ , причём точка может быть только первым символом идентификатора. Большие и маленькие буквы считаются эквивалентными.

2.2. Целые числа

В программе на языке ассемблера целые числа могут быть записаны в двоичной, восьмеричной, десятичной и шестнадцатеричной системах счисления. Для задания системы счисления в конце числа ставится буква b , o/q , d или h соответственно. Шестнадцатеричные числа, которые начинаются с «буквенной» цифры, должны предваряться нулём, иначе компилятор не сможет отличить число от идентификатора. Примеры чисел см. в разделе 2.6.

2.3. Символьные данные

Символы и строки в языке ассемблера могут заключаться в апострофы или двойные кавычки. Если в качестве символа или внутри строки надо указать апостроф или кавычку, то делается это следующим образом: если символ или строка заключены в апострофы, то апостроф надо удваивать, а кавычку удваивать не надо, и наоборот, если символ или строка заключены в двойные кавычки, то надо удваивать кавычку и не надо удваивать апостроф. Все следующие примеры корректны и эквивалентны: ‘don»t’ , ‘don»t’ , «don’t» , «don»»t» .

2.4. Комментарии

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

2.5. Директива эквивалентности

Директива эквивалентности позволяет описывать константы:

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

2.6. Директивы определения данных

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

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

Язык С, который создавался как высокоуровневая замена языку ассемблера, имеет гораздо менее жёсткую структуру типов. Все целочисленные типы совместимы, тип char, конечно, хранит символы, но также сопоставим с целыми типами, логический тип отсутствует в принципе (для языка С это именно так!), над указателями определены операции сложения и вычитания. Сложные типы, такие как массивы, строки и множества, не поддерживаются.

Что касается языка ассемблера, то тут вообще вряд ли можно говорить о какой-либо структуре типов. Команды языка ассемблера оперируют объектами, существующими в оперативной памяти, т.е. байтом и его производными (слово, двойное слово и т.д.). Символьный, логический тип? Какая глупость! Указатели? Вот тебе 4 байта и делай с ними, что хочешь. В итоге, конечно, и можно сделать, что хочешь, только предварительно стоит хорошо подумать, что из этого получится.

Соответственно, в языке ассемблера существует 5 (!) директив для определения данных:

  • DB (define byte) – определяет переменную размером в 1 байт;
  • DW (define word) – определяет переменную размеров в 2 байта (слово);
  • DD (define double word) – определяет переменную размером в 4 байта (двойное слово);
  • DQ (define quad word) – определяет переменную размером в 8 байт (учетверённое слово);
  • DT (define ten bytes) – определяет переменную размером в 10 байт.

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

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

DB [, ] DW [, ] DD [, ] DQ [, ] DT [, ]

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

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

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

a db 10011001b ; Определяем переменную размером 1 байт с начальным значением, заданным в двоичной системе счисления b db ‘!’ ; Определяем переменную в 1 байт, инициализируемую символом ‘!’ d db ‘string’,13,10 ; Определяем массив из 8 байт e db ‘string’,0 ; Определяем строку из 7 байт, заканчивающую нулём f dw 1235o ; Определяем переменную размером 2 байта с начальным значением, заданным в восьмеричной системе счисления g dd -345d ; Определяем переменную размером 4 байта с начальным значением, заданным в десятичной системе счисления h dd 0f1ah ; Определяем переменную размером 4 байта с начальным значением, заданным в шестнадцатеричной системе счисления i dd ? ; Определяем неинициализированную переменную размером 4 байта j dd 100 dup (0) ; Определяем массив из 100 двойных слов, инициализированных 0 k dq 10 dup (0, 1, 2) ; Определяем массив из 30 учетверённых слов, инициализированный повторяющимися значениями 0, 1 и 2 l dd 100 dup (?) ; Определяем массив из 100 неинициализированных двойных слов

К переменным можно применить две операции – offset и type . Первая определяет адрес переменной, а вторая – размер переменной. Однако размер переменной определяется по директиве, и даже если с директивой, например, DD определён массив из нескольких элементов, размер всё равно будет равен 4.

2.7. Команды

Команды языка ассемблера – это символьная форма записи машинных команд. Команды имеют следующий синтаксис:

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

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

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

Операнды команды, если они есть, отделяются друг от друга запятыми.

2.8. Операнды команд

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

  • регистры, обращение к которым осуществляется по именам;
  • непосредственные операнды – константы, записываемые непосредственно в команде;
  • ячейки памяти – в команде записывается адрес нужной ячейки.

Для задания адреса существуют следующие возможности.

  • Имя переменной, по сути, является адресом этой переменной. Встретив имя переменной в операндах команды, компилятор понимает, что нужно обратиться к оперативной памяти по определённому адресу. Обычно адрес в команде указывается в квадратных скобках, но имя переменной является исключением и может быть указано как в квадратных скобках, так и без них. Например, для обращения к переменной x в команде можно указать x или [x] .
  • Если переменная была объявлена как массив, то к элементу массива можно обратиться, указав имя и смещение. Для этого существует ряд синтаксических форм, например: [ ] и [ + ] (см. раздел 5). Однако следует понимать, что смещение – это вовсе не индекс элемента массива. Индекс элемента массива – это его номер, и этот номер не зависит от размера самого элемента. Смещение же задаётся в байтах, и при задании смещения программист сам должен учитывать размер элемента массива.
  • Адрес ячейки памяти может храниться в регистре. Для обращения к памяти по адресу, хранящемуся в регистре, в команде указывается имя регистра в квадратных скобках, например: [ebx] . Как уже говорилось, в качестве регистров базы рекомендуется использовать регистры EBX, ESI, EDI и EBP.
  • Адрес может быть вычислен по определённой формуле. Для этого в квадратных скобках можно указывать достаточно сложные выражения, например, [ebx + ecx] или [ebx + 4 * ecx] .

В описаниях команд языка ассемблера для обозначения возможных операндов используют сокращения, состоящие из буквы r (для регистров), m (для памяти) или i (для непосредственного операнда) и числа 8, 16 или 32, указывающего размер операнда. Например:

add r8/r16/r32, r8/r16/r32 ; Сложение регистра с регистром add r8/r16/r32, m8/m16/m32 ; Сложение регистра с ячейкой памяти add r8/r16/r32, i8/i16/i32 ; Сложение регистра с непосредственным операндом add m8/m16/m32, r8/r16/r32 ; Сложение ячейки памяти с регистром add m8/m16/m32, i8/i16/i32 ; Сложение ячейки памяти с непосредственным операндом

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

3. Пересылка и арифметические команды

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

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

По команде MOV значение второго операнда записывается в первый операнд. Операнды должны иметь одинаковый размер. Команда не меняет флаги.

mov eax, ebx ; Пересылаем значение регистра EBX в регистр EAX mov eax, 0ffffh ; Записываем в регистр EAX шестнадцатеричное значение ffff mov x, 0 ; Записываем в переменную x значение 0 mov eax, x ; Переслать значение из одной ячейки памяти в другую нельзя. mov y, eax ; Но можно использовать две команды MOV .

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

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

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

3.2. Оператор указания типа

Как было сказано, операнды команды MOV должны иметь одинаковый размер. В некоторых случаях компилятор может определить размер операнда. Например, регистр EAX имеет размер 32 бита, а регистр DX – 16 бит. Размер переменной определяется по директиве, указанной в её объявлении. Если можно определить размер только одного операнда, то размер второго операнда подгоняется под размер первого, если это возможно. Если же можно определить размеры обоих операндов, то они должны совпадать.

x db ? mov x, 0 ; 0 может иметь любой размер, в данном случае берётся 1 байт mov eax, 0 ; 0 может иметь любой размер, в данном случае берётся 4 байта mov al, 1000h ; Ошибка – попытка записать 2-байтное число в 1-байтный регистр mov eax, cx ; Ошибка – размеры операндов не совпадают

Однако не всегда бывает возможно определить размер пересылаемой величины по операндам команды MOV . Например, если один из операндов является ячейкой памяти, адрес которой записан в регистре, то по этому адресу можно записать и 1 байт, и 2 байта, и 4 байта. Если второй операнд является регистром, то размер пересылаемых данных определяется по размеру регистра. Если же второй операнд является константой, то размер пересылаемых данных определить нельзя, и компилятор фиксирует ошибку. Для того чтобы избежать этой ошибки, надо явно указать размер пересылаемых данных. Для этого используется оператор PTR :

В качестве типа используется BYTE , WORD или DWORD .

mov [ebx], 0 ; Ошибка, т.к. 0 может иметь любой размер mov byte ptr [ebx], 0 ; Пересылаем 1 байт mov dword ptr [ebx], 0 ; Пересылаем 4 байта

3.3. Команды сложения и вычитания

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

Команда ADD складывает операнды и записывает их сумму на место первого операнда. Команда SUB вычитает из первого операнда второй и записывает полученную разность на место первого операнда. Операнды должны иметь одинаковый размер. Если первый операнд – регистр, то второй может быть также регистром, ячейкой памяти и непосредственным операндом. Если первый операнд – ячейка памяти, то второй операнд может быть регистром или непосредственным операндом. Возможно сложение и вычитание как знаковых, так и беззнаковых чисел любого размера. Команды меняют флаги AF, CF, OF, PF, SF и ZF.

a dd 45d b dd -32d c dd ? mov eax, a add eax, b mov c, eax ; c = a + b

Команды инкремента и декремента увеличивают и уменьшают на 1 свой операнд.


Операндом может быть регистр или ячейка памяти любого размера. Команды меняют флаги AF, OF, PF, SF и ZF. Команды инкремента и декремента выгодны тем, что они занимают меньше места, чем соответствующие команды сложения и вычитания.

К арифметическим операциям можно также отнести команду изменения знака:

Операндом может быть регистр или ячейка памяти любого размера. Команда NEG рассматривает свой операнд как число со знаком и меняет знак операнда на противоположный. Команда меняет флаги AF, CF, OF, PF, SF и ZF.

mov ax, 1 neg ax ; AX = -1 = ffffh mov bl, -128 neg bl ; BL = -128, OF = 1

3.4. Команды умножения и деления

3.4.1. Команды умножения

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

Для беззнакового умножения используется команда MUL :

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

Местонахождение второго сомножителя и результата фиксировано, и в команде явно не указывается. Если операнд команды MUL имеет размер 1 байт, то второй сомножитель берётся из регистра AL, а результат помещается в регистр AX. Если операнд команды MUL имеет размер 2 байта, то второй сомножитель берётся из регистра AX, а результат помещается в регистровую пару DX:AX. Если операнд команды MUL имеет размер 4 байта, то второй сомножитель берётся из регистра EAX, а результат помещается в регистровую пару EDX:EAX.

Команда меняет флаги CF и OF. Если произведение имеет такой же размер, что и сомножители, то оба флага сбрасываются в 0. Если же размер произведения удваивается относительно размера сомножителей, то оба флага устанавливаются в 1.

x dw 256 mov ax, 105 mul x ; AX = AX * x, AX = 26880, CF = OF = 0 mov eax, 500000 mov ebx, 100000 mul ebx ; EDX:EAX = EAX * EBX, EDX:EAX = 50000000000, CF = OF = 1

Для знакового умножения используется команда IMUL :

IMUL IMUL , IMUL , , IMUL ,

Команда знакового умножения имеет несколько вариантов. Первый соответствует команде MUL – один из сомножителей указывается в команде, второй должен находиться в регистре EAX/AX/AL, а результат помещается в регистры EDX:EAX/DX:AX/AX.

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

Третий вариант команды IMUL позволяет указать и результат, и оба сомножителя. Однако результат может быть помещён только в регистр, а второй сомножитель может быть только непосредственным операндом. Первый сомножитель может быть регистром или ячейкой памяти.

Четвёртый вариант команды IMUL позволяет указать оба сомножителя. Первый должен быть регистром, а второй – регистром или ячейкой памяти. Результат помещается в регистр, являющийся первым операндом.

Команда IMUL устанавливает флаги так же, как и команда MUL . Однако расширение результата в регистр EDX/DX происходит только при использовании первого варианта команды IMUL . В остальных случаях часть произведения, не помещающаяся в регистр-результат, теряется, даже если в качестве результата указан регистр EAX/AX. При умножении двух 1-байтовых чисел, произведение которых больше байта, но меньше слова, в регистре-результате получается корректное произведение.

mov eax, 5 mov ebx, -7 imul ebx ; EAX = ffffffdd, EDX = ffffffff, CF = 0 mov ebx, 3 imul ebx, 6 ; EBX = EBX * 6 mov ebx, 500000 imul eax, ebx, 100000 ; EAX = EBX * 100000, старшая часть результата теряется x dd 40 mov eax, 55 imul eax, x ; EAX = EAX * x

3.4.2. Команды деления

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

DIV ; Беззнаковое деление IDIV ; Знаковое деление

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

Если делитель имеет размер 1 байт, то делимое берётся из регистра AX. Если делитель имеет размер 2 байта, то делимое берётся из регистровой пары DX:AX. Если же делитель имеет размер 4 байта, то делимое берётся из регистровой пары EDX:EAX.

Поскольку процессор работает с целыми числами, то в результате деления получается сразу два числа – частное и остаток. Эти два числа также помещаются в определённые регистры. Если делитель имеет размер 1 байт, то частное помещается в регистр AL, а остаток – в регистр AH. Если делитель имеет размер 2 байта, то частное помещается в регистр AX, а остаток – в регистр DX. Если же делитель имеет размер 4 байта, то частное помещается в регистр EAX, а остаток – в регистр EDX.

mov ax, 127 mov bl, 5 div bl ; AL = 19h = 25, AH = 02h = 2 mov ax, 127 mov bl, -5 >

3.5. Изменение размера числа

В операциях деления размер делимого в два раза больше, чем размер делителя. Поэтому нельзя просто загрузить данные в регистр EAX и поделить его на какое-либо значение, т.к. в операции деления будет задействован также и регистр EDX. Поэтому прежде чем выполнять деление, надо установить корректное значение в регистр EDX, иначе результат будет неправильным. Значение регистра EDX должно зависеть от значения регистра EAX. Тут возможны два варианта – для знаковых и беззнаковых чисел.

Если мы используем беззнаковые числа, то в любом случае в регистр EDX необходимо записать значение 0: aaaaaaaah → 00000000aaaaaaaah .

Если же мы используем знаковые числа, то значение регистра EDX будет зависеть от знака числа: 55555555h → 0000000055555555h , aaaaaaaah → ffffffffaaaaaaaah .

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

cbw ; Знаковое расширение AL до AX cwd ; Знаковое расширение AX до DX:AX cwde ; Знаковое расширение AX до EAX cdq ; Знаковое расширение EAX до EDX:EAX

Таким образом, если делитель имеет размер 2 или 4 байта, то нужно устанавливать значение не только регистра AX/EAX, но и регистра DX/EDX. Если же делитель имеет размер 1 байт, то можно просто записать делимое в регистр AX.

x dd ? mov eax, x ; Заносим в регистр EAX значение переменной x , которое заранее неизвестно cdq ; Знаковое расширение EAX в EDX:EAX mov ebx, 7 idiv ebx

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

MOVSX , ; Знаковое расширение – старшие биты заполняются знаковым битом MOVZX , ; Беззнаковое расширение – старшие биты заполняются нулём

Операнд1 и операнд2 могут иметь любой размер. Понятно, что операнд1 должен быть больше, чем операнд2. В случае равенства размера операндов следует использовать обычную команду пересылки MOV , которая выполняется быстрее.

Рассмотрим пример: необходимо вычислить x * x * x , где x – 1-байтовая переменная.

; Первый вариант mov al, x ; Пересылаем x в регистр AL imul al ; Умножаем регистр AL на себя, AX = x * x movsx bx, x ; Пересылаем x в регистр BX со знаковым расширением imul bx ; Умножаем AX на BX. Но! – результат размещается в DX:AX ; Второй вариант mov al, x ; Пересылаем x в регистр AL imul al ; Умножаем регистр AL на себя, AX = x * x cwde ; Расширяем AX до EAX movsx ebx, x ; Пересылаем x в регистр EBX со знаковым расширением imul ebx ; Умножаем EAX на EBX. Поскольку x – 1-байтовая переменная, результат благополучно помещается в EAX

Рассмотрим ещё один пример.

mov eax, x mov ebx, 429496730 ; 429496730 = 4294967296 / 10 imul ebx ; EDX = x / 10. Выполняется в ≈5 раз быстрее, чем деление

Чем обусловлено получение такого результата? Всегда ли будет работать этот механизм?

4. Переходы и циклы

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

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

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

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

4.1.1. Прямой переход

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

jmp L . L: mov eax, x

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

Запись в команде перехода не абсолютного, а относительного адреса перехода позволяет уменьшить размер команды перехода. Абсолютный адрес должен быть 32-битным, а относительный может быть и 8-битным, и 16-битным.

4.1.2. Косвенный переход

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

4.2. Команды сравнения и условного перехода

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

Команда сравнения эквивалентна команде SUB за исключением того, что вычисленная разность никуда не заносится. Назначение команды CMP – установка и сброс флагов.

Что касается команд условного перехода, то их достаточно много, но все они записываются единообразно:

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

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

Мнемокод Название Условие перехода после команды CMP op1, op2 Значения флагов Примечание
JE Переход если равно op1 = op2 ZF = 1 Для всех чисел
JNE Переход если не равно op1 ≠ op2 ZF = 0
JL/JNGE Переход если меньше op1 op2 SF = OF и ZF = 0
JGE/JNL Переход если больше или равно op1 ≥ op2 SF = OF
JB/JNAE Переход если ниже op1 op2 CF = 0 и ZF = 0
JAE/JNB Переход если выше или равно op1 ≥ op2 CF = 0

Рассмотрим пример: даны две переменные x и y , в переменную z нужно записать максимальное из чисел x и y .

mov eax, x cmp eax, y jge/jae L ; Используем JGE для знаковых чисел и JAE – для беззнаковых mov eax, y L: mov z, eax

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

Мнемокод Условие перехода Мнемокод Условие перехода
JZ ZF = 1 JNZ ZF = 0
JS SF = 1 JNS SF = 0
JC CF = 1 JNC CF = 0
JO OF = 1 JNO OF = 0
JP PF = 1 JNP PF = 0

Рассмотрим пример: пусть a , b и c – беззнаковые переменные размером 1 байт, требуется вычислить c = a * a + b , но если результат превосходит размер байта, передать управление на метку ERROR.

mov al, a mul al jc ERROR add al, b jc ERROR mov c, al

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


JCXZ ; Переход, если значение регистра CX равно 0 JECXZ ; Переход, если значение регистра ECX равно 0

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

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

; if (x > 0) S cmp x, 0 jle L . ; S L: ; if (x) S1 else S2 cmp x, 0 je L1 . ; S1 jmp L2 L1: . ; S2 L2: ; if (a > 0 && b > 0) S cmp a, 0 jle L cmp b, 0 jle L . ; S L: ; if (a > 0 || b > 0) S cmp a, 0 jg L1 cmp b, 0 jle L2 L1: . ; S L2: ; if (a > 0 || b > 0 && c > 0) S cmp a, 0 jg L1 cmp b, 0 jle L2 cmp c, 0 jle L2 L1: . ; S L2: ; while (x > 0) do S L1: cmp x, 0 jle L2 . ; S jmp L1 L2: ; do S while (x > 0) L: . ; S cmp x, 0 jg L

4.3. Команды управления циклом

4.3.1. Команда LOOP

Команда LOOP позволяет организовать цикл с известным числом повторений:

mov ecx, n L: . . loop L

Команда LOOP требует, чтобы в качестве счётчика цикла использовался регистр ECX. Собственно, команда LOOP вычитает единицу именно из этого регистра, сравнивает полученное значение с нулём и осуществляет переход на указанную метку, если значение в регистре ECX больше 0. Метка определяет смещение перехода, которое не может превышать 128 байт.

При использовании команды LOOP следует также учитывать, что с её помощью реализуется цикл с постусловием, следовательно, тело цикла выполняется хотя бы один раз. Хуже того, если до начала цикла записать в регистр ECX значение 0, то при вычитании единицы, которое выполняется до сравнения с нулём, в регистре ECX окажется ненулевое значение, и цикл будет выполняться 2 32 раз.

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

4.3.2. Команды LOOPE / LOOPZ и LOOPNE / LOOPNZ

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

LOOPE ; Команды являются синонимами LOOPZ

Действие этой команды можно описать следующим образом: ECX = ECX — 1; if (ECX != 0 && ZF == 1) goto ;

До начала цикла в регистр ECX необходимо записать число повторений цикла. Команда LOOPE / LOOPZ , как и команда LOOP ставится в конце цикла, а перед ней помещается команда, которая меняет флаг ZF (обычно это команда сравнения CMP ). Команда LOOPE / LOOPZ заставляет цикл повторяться ECX раз, но только если предыдущая команда фиксирует равенство сравниваемых величин (вырабатывает нулевой результат, т.е. ZF = 1 ).

По какой именно причине произошёл выход из цикла надо проверять после цикла. Причём надо проверять флаг ZF, а не регистр ECX, т.к. условие ZF = 0 может появиться как раз на последнем шаге цикла, когда и регистр ECX стал нулевым.

Команда LOOPNE / LOOPNZ аналогична команде LOOPE / LOOPZ , но досрочный выход из цикла осуществляется, если ZF = 1 .

Рассмотрим пример: пусть в регистре ESI находится адрес начала некоторого массива двойных слов, а в переменной n – количество элементов массива, требуется проверить наличие в массиве элементов, кратных заданному числу x , и занести в переменную f значение 1, если такие элементы есть, и 0 в противном случае.

mov ebx, x mov ecx, n mov f, 1 L1: mov eax, [esi] add esi, 4 cdq idiv ebx cmp edx, 0 loopne L1 je L2 mov f, 0 L2:

5. Массивы

5.1. Модификация адресов

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

Пусть X – некий массив. Тогда адрес элемента массива можно вычислить по следующей формуле:

адрес(X[i]) = X + (type X) * i, где i – номер элемента массива, начинающийся с 0

Напомним, что имя переменной эквивалентно её адресу (для массива – адресу начала массива), а операция type определяет размер переменной (для массива определяется размер элемента массива в соответствии с использованной директивой).

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

Однако принципиальное отличие состоит в том, в программе на языке высокого уровня мы указываем индекс элемента массива, а компилятор умножает его на размер элемента массива, получая смещение элемента массива. В программе на языке ассемблера указывается именно смещение, т.е. программист должен сам учитывать размер элемента массива. Компилятор же языка ассемблера просто прибавляет смещение к указанному адресу. Приведённые выше команды можно записать по-другому:

x + 4 [x + 4] [x] + [4] [x][4] [x + ebx] [x] + [ebx] [x][ebx]

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

Адрес может вычисляться и по более сложной схеме:

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

mov eax, [ebx + 4 * ecx — 32] mov eax, [x + 2 * ecx]

5.2. Команда LEA

Команда LEA осуществляет загрузку в регистр так называемого эффективного адреса:

Команда не меняет флаги. В простейшем случае с помощью команды LEA можно загрузить в регистр адрес переменной или начала массива:

x dd 100 dup (0) lea ebx, x

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

5.3. Обработка массивов

Пусть есть массив x и переменная n , хранящая количество элементов этого массива.

x dd 100 dup(?) n dd ?

Для обработки массива можно использовать несколько способов.

  1. В регистре можно хранить смещение элемента массива.

mov eax, 0 mov ecx, n mov ebx, 0 L: add eax, x[ebx] add ebx, type x dec ecx cmp ecx, 0 jne L

  1. В регистре можно хранить номер элемента массива и умножать его на размер элемента.

mov eax, 0 mov ecx, n L: dec ecx add eax, x[ecx * type x] cmp ecx, 0 jne L

  1. В регистре можно хранить адрес элемента массива. Адрес начала массива можно записать в регистр с помощью команды LEA .

mov eax, 0 mov ecx, n lea ebx, x L: add eax, [ebx] add ebx, type x dec ecx cmp ecx, 0 jne L

  1. При необходимости можно в один регистр записать адрес начала массива, а в другой – номер или смещение элемента массива.

mov eax, 0 mov ecx, n lea ebx, x L: dec ecx add eax, [ebx + ecx * type x] cmp ecx, 0 jne L

Модификацию адреса можно производить также по двум регистрам: x[ebx][esi] . Это может быть удобно при работе со структурами данных, которые рассматриваются как матрицы. Рассмотрим для примера подсчёт количества строк матриц с положительной суммой элементов.

mov esi, 0 ; Начальное смещение строки mov ebx, 0 ; EBX будет содержать количество строк, удовлетворяющих условию mov ecx, m ; Загружаем в ECX количество строк L1: mov edi, 0 ; Начальное смещение элемента в строке mov eax, 0 ; EAX будет содержать сумму элементов строки mov edx, n ; Загружаем в EDX количество элементов в строке L2: add eax, y[esi][edi] ; Прибавляем к EAX элемент массива add edi, type y ; Прибавляем к смещению элемента в строке размер элемента dec edx ; Уменьшаем на 1 счётчик внутреннего цикла cmp edx, 0 ; Сравниваем EDX с нулём jne L2 ; Если EDX не равно 0, то переходим к началу цикла cmp eax, 0 ; После цикла сравниваем сумму элементов строки с нулём jle L3 ; Если сумма меньше или равна 0, то обходим увеличение EBX inc ebx ; Если же сумму больше 0, то увеличиваем EBX L3: mov eax, n ; Загружаем в EAX количество элементов в строке imul eax, type y ; Умножаем количество элементов в строке на размер элемента add esi, eax ; Прибавляем к смещению полученный размер строки dec ecx ; Уменьшаем на 1 счётчик внешнего цикла cmp ecx, 0 ; Сравниваем ECX с нулём jne L1 ; Если ECX не равно 0, то переходим к началу цикла

6. Поразрядные операции

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

6.1. Логические команды

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

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

Операция поразрядное «или» выполняет логическое сложение всех пар бит операндов.

Операция поразрядное исключающее «или» выполняет сложение по модулю 2 всех пар бит операндов.

Операции AND , OR и XOR имеют по два операнда. Первый может быть регистром или ячейкой памяти, а второй – регистром, ячейкой памяти или непосредственным операндом. Операнды должны иметь одинаковый размер. Результат помещается на место первого операнда. Операции меняют флаги CF, OF, PF, SF и ZF.

Операция XOR имеет интересную особенность – если значения операндов совпадают, то результатом будет значение 0. Поэтому операцию XOR используют для обнуления регистров – она выполняется быстрее, чем запись нуля с помощью команды MOV .

xor eax, eax ; При любом значении EAX результат будет равен 0

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

xor eax, ebx ; EAX = EAX xor EBX xor ebx, eax ; Теперь EBX содержит исходное значение EAX xor eax, ebx ; А теперь EAX содержит исходное значение EBX

6.2. Команды сдвига

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

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


Команды сдвига меняют флаги CF, OF, PF, SF и ZF.

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

6.2.1. Логические сдвиги

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

SHL , ; Логический сдвиг влево SHR , ; Логический сдвиг вправо

6.2.2. Арифметические сдвиги

Арифметический сдвиг влево эквивалентен логическому сдвигу влево (это одна и та же команда) – «освобождающие» биты заполняются нулями. При арифметическом сдвиге вправо «освобождающиеся» биты заполняются знаковым битом. Последний ушедший бит сохраняется во флаге CF.

SAL , ; Арифметический сдвиг влево SAR , ; Арифметический сдвиг вправо

6.2.3. Циклические сдвиги

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

ROL , ; Циклический сдвиг влево ROR , ; Циклический сдвиг вправо

6.2.4. Расширенные сдвиги

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

SHLD , , ; Расширенный сдвиг влево SHRD , , ; Расширенный сдвиг вправо

Команда SHLD сдвигает влево биты операнда1 на указанное количество позиций. Младшие («освободившиеся») биты операнда1 заполняются старшими битами операнда2. Сам операнд2 не меняется.

Команда SHRD сдвигает вправо биты операнда1 на указанное количество позиций. Старшие («освободившиеся») биты операнда1 заполняются младшими битами операнда2. Сам операнд2 не меняется.

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

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

6.3. Умножение и деление с помощью поразрядных операций

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

6.3.1. Умножение

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

mov ax, 250 ; AX = 00fah = 250 sal ax, 4 ; Умножение на 24 = 16, AX = 0fa0h = 4000 mov ax, 1 ; AX = 1 sal ax, 10 ; Умножение на 210, AX = 0400h = 1024 mov ax, -48 ; AX = ffd0h = -48 (в дополнительном коде) sal ax, 2 ; AX = ff40h = -192 (в дополнительном коде) mov ax, 26812 ; AX = 68bch = 26812 sal ax, 1 ; AX = d178h = -11912 ; Знаковое положительное число перешло в отрицательное mov ax, 32943 ; AX = 80afh = 32943 sal ax, 2 ; AX = 02bch = 700 ; Большое беззнаковое число стало гораздо меньше

Сочетая сдвиги со сложением и вычитанием можно выполнить умножение на любое положительное число. Для умножения на отрицательное число следует добавить команду NEG .

mov ebx, x mov eax, ebx sal eax, 2 add eax, ebx ; EAX = x * 5 mov ebx, x mov eax, ebx sal eax, 3 sub eax, ebx ; EAX = x * 7 mov ebx, x mov eax, ebx sal eax, 2 add eax, ebx sal eax, 1 ; EAX = x * 10

Такой набор операций выполняется в 1.5-2 раза быстрее, чем обычное умножение. Но если оба сомножителя заранее неизвестны, то лучше использовать умножение.

6.3.2. Деление

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

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

mov ax, 43013 ; AX = a805h = 43013 shr ax, 1 ; AX = 5402h = 21506

Со знаковыми числами дело обстоит несколько сложнее. В принципе, для деления знаковых чисел следует использовать арифметический сдвиг вправо. Однако для отрицательных чисел получается не совсем корректный результат: 1 / 2 = 0, 3 / 2 = 1, но -1 / 2 = -1, -3 / 2 = -2,, т.е. результат отличается от правильного на единицу. Для того чтобы получить правильный результат, необходимо прибавить к делимому делитель, уменьшенный на 1. Однако это необходимо только для отрицательных чисел, поэтому для того, чтобы не делать проверок, используют следующий алгоритм.

; Деление на 2 mov eax, x cdq ; Расширяем двойное слово до учетверённого. Если в регистре EAX находится положительное число, ; то регистр EDX будет содержать 0, а если в регистре EAX находится отрицательное число, ; то регистр EDX будет содержать -1 (ffffffffh) sub eax, edx ; Если регистр EDX содержит 0, то регистр EAX не меняется. Если же регистр EDX содержит -1 ; (при отрицательном EAX), то к EAX будет прибавлена требуемая единица sar eax, 1 ; Деление на 2 n (в данном примере n = 3) mov eax, x cdq ; Расширяем двойное слово до учетверённого and edx, 111b ; Если EAX отрицателен, то EDX содержит делитель, уменьшенный на 1 add eax, edx ; Если EAX отрицателен, прибавляем полученное значение sar eax, 3 ; Если EAX был положителен, то EDX = 0, и предыдущие две операции ничего не меняют

Если число беззнаковое или если мы знаем, что число положительное, можно просто использовать сдвиг вправо, который выполняется примерно в 10 раз быстрее, чем деление. Если же для знакового числа не известно, положительное оно или отрицательное, то придётся использовать вышеприведённую последовательность команд, которая, однако, также выполняется примерно в 5-7 раз быстрее, чем деление.

6.3.3. Получение остатка от деления

Для беззнаковых и положительных чисел остаток от деления на 2 n – это последние n бит числа. Поэтому для получения остатка от деления на 2 n нужно выделить эти последние n бит с помощью операции AND .

mov eax, x and eax, 111b ; EAX = EAX % 2 3

Для отрицательного делимого x и положительного делителя n (x % n) = -(-x % n) .

mov eax, x neg eax and eax, 1111b ; EAX = EAX % 2 4 neg eax

7. Программа. Процедуры

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

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

.686 .model flat, stdcall option casemap: none .data .data? .const .code end

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

Директива .model позволяет указывать используемую модель памяти и соглашение о вызовах. Как уже было сказано, на архитектуре Win32 используется только одна модель памяти – flat, что и указано в приведённом примере. Соглашения о вызовах определяют порядок передачи параметров и порядок очистки стека.

Директива option casemap: none заставляет компилятор языка ассемблера различать большие и маленькие буквы в метках и именах процедур.

Директивы .data , .data? , .const и .code определяют то, что называется секциями. В Win32 нет сегментов, но адресное пространство можно поделить на логические секции. Начало одной секции отмечает конец предыдущей. Есть две группы секций: данных и кода.

Секция .data содержит инициализированные данные программы.

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

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

Задействовать все три секции не обязательно.

Есть только одна секция для кода: .code . В ней содержится весь код.

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

Любая программа под Windows должна, как минимум, корректно завершится. Для этого необходимо вызвать функцию Win32 API ExitProcess.

.686 .model flat, stdcall option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .code program: push 0 call ExitProcess end program

Выше приведён пример минимальной программы на языке ассемблера, которая делает только одно – корректно завершается. В ней появились две новые директивы: include и includelib . Первая позволяет включать в программу файлы, содержащие прототипы процедур, а также определения констант и структур, которые могут понадобиться для программирования под Win32. Вторая директива указывает, какие библиотеки использует программа. Компоновщик должен будет прилинковать их. Без указания включаемого файла kernel2.inc и библиотеки импорта kernel32.lib невозможно будет вызвать процедуру ExitProcess. Файл windows.inc в данном случае включать не обязательно, но он требуется достаточно часто, а включаемые файлы не увеличивают размер получаемой программы.

Команда PUSH кладёт в стек параметр для процедуры ExitProcess. Этот параметр определяет код завершения. Значение 0 – это код нормального завершения программы.

Команда CALL вызывает процедуру ExitProcess.

Если вы используете компилятор MASM32, то пункт меню Project содержит команды Assemble & Link и Console Assemble & Link, которые позволяют скомпилировать обычное и консольное приложение под Windows. Приведённую программу можно откомпилировать обоими способами.

7.2. Команды работы со стеком

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

Для того чтобы положить данные в стек используется команда PUSH :

Операнд может быть регистром, ячейкой памяти или непосредственным операндом. Размер операнда должен быть 2 или 4 байта. Операнд кладётся на вершину стека, а значение регистра ESP уменьшается на размер операнда.

Для того чтобы взять данные из стека используется команда POP :

Операнд может быть регистром или ячейкой памяти. Размер операнда должен быть 2 или 4 байта. В соответствии с размером операнда из вершины стека берутся 2 или 4 байта и помещаются в указанный регистр или ячейку памяти. Значение регистра ESP увеличивается на размер операнда.

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

Команда PUSHA сохраняет в стеке содержимое регистров AX, CX, DX, BX, SP, BP, SI, DI. Команда PUSHAD сохраняет в стеке содержимое регистров EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI. Для регистра (E)SP сохраняется значение, которое было до того, как мы положили регистры в стек. После этого значение регистра (E)SP изменяется как обычно.

Эти команды противоположны предыдущим – они восстанавливают из стека значения регистров (E)DI, (E)SI, (E)BP, (E)SP, (E)BX, (E)DX, (E)CX, (E)AX. Содержимое регистра (E)SP не восстанавливается из стека, а изменяется как обычно.


Команда PUSHF сохраняет в стеке младшие 16 бит регистра флагов. Команда PUSHFD сохраняет в стеке все 32 бита регистра флагов.

Команда POPF восстанавливает из стека младшие 16 бит регистра флагов. Команда POPFD восстанавливает из стека все 32 бита регистра флагов.

7.3. Синтаксис процедуры

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

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

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

Размещать процедуру в программе на языке ассемблера следует таким образом, чтобы команды процедуры выполнялись не сами по себе, а только тогда, когда происходит обращение к процедуре. Обычно процедуры размещают либо в конце секции кода после вызова функции ExitProcess, либо в самом начале секции кода, сразу после директивы .code .

7.4. Вызов процедуры и возврат из процедуры

Вызов процедуры – это, по сути, передача управления на первую команду процедуры. Для передачи управления можно использовать команду безусловного перехода на метку, являющуюся именем процедуры. Можно даже не использовать директивы proc и endp , а написать обычную метку с двоеточием после вызова функции ExitProcess.

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

.686 .model flat, stdcall option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .code program: push L jmp Procedure L: nop push 0 call ExitProcess Procedure: pop eax jmp eax end program

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

CALL ; Вызов процедуры RET ; Возврат из процедуры

Команда CALL записывает адрес следующей за ней команды в стек и осуществляет переход на первую команду указанной процедуры. Команда RET считывает из вершины стека адрес и выполняет переход по нему.

.686 .model flat, stdcall option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .code program: call Procedure push 0 call ExitProcess Procedure proc ret Procedure endp end program

7.5. Передача параметров процедуры

Существуют несколько способов передачи параметров в процедуру.

  1. Параметры можно передавать через регистры.

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

  1. Параметры можно передавать в глобальных переменных.

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

  1. Параметры можно передавать в блоке параметров.

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

  1. Параметры можно передавать через стек.

Передача параметров через стек – наиболее распространённых способ. Именно его используют языки высокого уровня, такие как С++ и Паскаль. Параметры помещаются в стек непосредственно перед вызовом процедуры.

При внимательном анализе этого метода передачи параметров возникает сразу два вопроса: кто должен удалять параметры из стека, процедура или вызывающая её программа, и в каком порядке помещать параметры в стек. В обоих случаях оказывается, что оба варианта имеют свои «за» и «против». Если стек освобождает процедура, то код программы получается меньшим, а если за освобождение стека от параметров отвечает вызывающая программа, то становится возможным вызвать несколько функций с одними и теми же параметрами просто последовательными командами CALL . Первый способ, более строгий, используется при реализации процедур в языке Паскаль, а второй, дающий больше возможностей для оптимизации, – в языке С++.

Основное соглашение о вызовах языка Паскаль предполагает, что параметры кладутся в стек в прямом порядке. Соглашения о вызовах языка С++, в том числе одно из основных соглашений о вызовах ОС Windows stdcall, предполагают, что параметры помещаются в стек в обратном порядке. Это делает возможной реализацию функций с переменным числом параметров (как, например, printf). При этом первый параметр определяет число остальных параметров.

push . push call Procedure

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

Адрес возврата оказывается в стеке поверх параметров. Однако поскольку в рамках своего участка стека процедура может обращаться без ограничений к любой ячейки памяти, нет необходимости перекладывать куда-то адрес возврата, а потом возвращать его обратно в стек. Для обращения к первому параметру используют адрес [ESP + 4] (прибавляем 4, т.к. на архитектуре Win32 адрес имеет размер 32 бита), для обращения ко второму параметру – адрес [ESP + 8] и т.д.

После завершения работы процедуры необходимо освободить стек. Если используется соглашение о вызовах stdcall (или любое другое, предполагающее, что стек освобождается процедурой), то в команде RET следует указать суммарный размер в байтах всех параметров процедуры. Тогда команда RET после извлечения адреса возврата прибавит к регистру ESP указанное значение, освободив таким образом стек. Если же используется соглашение о вызовах cdecl (или любое другое, предполагающее, что стек освобождается вызывающей программой), то после команды CALL следует поместить команду, которая прибавит к регистру ESP нужное значение.

; Передача параметров и возврат из процедуры с использованием соглашения о вызовах stdcall .686 .model flat, stdcall option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data x dd 0 y dd 4 .code program: push y ; Кладём в стек два параметра размером по 4 байта push x call Procedure push 0 call ExitProcess Procedure proc ret 8 ; В команде возврата указываем, что надо освободить 8 байт стека Procedure endp end program ; Передача параметров и возврат из процедуры с использованием соглашения о вызовах cdecl .686 .model flat, c option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data x dd 0 y dd 4 .code program: push y ; Кладём в стек два параметра размером по 4 байта push x call Procedure add esp, 8 ; Освобождаем 8 байт стека push 0 call ExitProcess Procedure proc ret ; Используем команду возврата без параметров Procedure endp end program

  1. Параметры можно передавать в потоке кода.

В этом необычном методе передаваемые процедуре данные размещаются прямо в коде программы, сразу после команды CALL . Чтобы прочитать параметр, процедура должна использовать его адрес, который автоматически передаётся в стеке как адрес возврата из процедуры. Разумеется, процедура должна будет изменить адрес возврата на первый байт после конца переданных параметров перед выполнением команды RET .

.686 .model flat, stdcall option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .code program: call Procedure ; Команда CALL кладёт в стек адрес следующей команды db ‘string’,0 ; В нашем случае – адрес начала строки push 0 call ExitProcess Procedure proc pop esi ; Извлекаем из стека адрес начала строки xor eax, eax ; Обнуляем EAX, в нём будет храниться количество символов L1: mov bl, [esi] ; Заносим в регистр BL байт, хранящийся по адресу ESI inc esi ; Увеличиваем значение в регистре ESI на 1 inc eax ; Увеличиваем значение в регистре EAX на 1 cmp bl, 0 ; Сравниваем прочитанный символ с нулём jne L1 ; Если не 0, переходим к началу цикла push esi ; Кладём в стек адрес байта, следующего сразу за строкой ret ; Возврат из процедуры Procedure endp end program

7.6. Передача результата процедуры

Для передачи результата процедуры обычно используется регистр EAX. Этот способ используется не только в программах на языке ассемблера, но и в программах на языке С++. Объекты, имеющие размер не более 8 байт, могут передаваться через регистровую пару EDX:EAX. Вещественные числа передаются через вершину стека вещественных регистров. Если эти способы не подходят, то следует передать в качестве параметра адрес ячейки памяти, куда будет записан результат.

; Передача параметров через стек, возврат результата через регистр EAX .686 .model flat, c option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data a dd 76 b dd -8 d dd ? .code program: push b ; Кладём параметры в стек push a call Procedure add esp, 8 ; Освобождаем 8 байт стека mov d, eax ; d = a – b push 0 call ExitProcess Procedure proc mov eax, [esp + 4] ; Заносим в регистр EAX первый параметр mov edx, [esp + 8] ; Заносим в регистр EDX второй параметр sub eax, edx ; В регистре EAX получилась разность параметров ret Procedure endp end program ; Передача параметров через стек, возврат результата по адресу .686 .model flat, c option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data a dd 76 b dd -8 d dd ? .code program: push offset d ; Кладём в стек адрес переменной, куда будет записан результат push b push a call Procedure add esp, 12 ; Освобождаем 12 байт стека push 0 call ExitProcess Procedure proc mov eax, [esp + 4] ; Заносим в регистр EAX первый параметр mov edx, [esp + 8] ; Заносим в регистр EDX второй параметр sub eax, edx ; В регистре EAX получилась разность параметров mov edx, [esp + 12] ; Заносим в регистр EDX третий параметр – адрес результата mov [edx], eax ; Записываем результат по адресу в регистре EDX ret Procedure endp end program

7.7. Сохранение регистров в процедуре

Практически любые действия в языке ассемблера требуют использования регистров. Однако регистров очень мало и даже в небольшой программе невозможно будет разделить регистры между частями программы, т.е. договориться, что основная программа использует, например, регистры EAX, ECX, EBP, ESP, а процедура – регистры EBX, EDX, ESI, EDI. В принципе, сделать так можно, но смысла в этом нет, т.к. программировать будет крайне неудобно, придётся перемещать данные из регистров в оперативную память и обратно, что замедлит выполнение программы. Кроме того, существуют правила, которые изменить нельзя – в регистре ESP хранится адрес вершины стека, а команды умножения и деления всегда используют регистры EAX и EDX. Поэтому получается, что основная программа и процедура вынуждены использовать одни и те же регистры, причём, вычисления в основной программе прерываются для того, чтобы выполнить вычисления процедуры. Таким образом, чтобы основная программа могла продолжить вычисления, процедура должна при выходе восстановить те значения регистров, которые были до начала выполнения процедуры. Естественно, для этого процедуре придётся предварительно сохранить значения регистров. Всё вышесказанное относится также к случаю, когда одна процедура вызывает другую процедуру.

Особенно внимательно следует относиться к регистрам ESI, EDI, EBP и EBX. ОС Windows использует эти регистры для своих целей и не ожидает, что вы измените их значение.

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

Где можно сохранить значения регистров? Конечно же, в стеке. Можно сохранить используемые регистры по одному с помощью команды PUSH , или все сразу с помощью команды PUSHAD . В первом случае в конце процедуры нужно будет восстановить значения сохранённых регистров с помощью команды POP в обратном порядке. Во втором случае для восстановления значений регистров используется команду POPAD .

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

; Процедура получает два параметра по 4 байта Procedure proc push esi ; Сохраняем используемые регистры push edi mov esi, [esp + 12] ; Извлекаем параметры из стека. Адрес вычисляется mov edi, [esp + 16] ; с учётом 8 байт, использованных при сохранении регистров . pop edi ; Извлекаем сохранённые регистры из стека pop esi ; в обратном порядке ret Procedure endp ; Процедура получает два параметра по 4 байта Procedure proc pushad ; Сохраняем все регистры mov eax, [esp + 4 + 32] ; Извлекаем параметры из стека. Адрес вычисляется mov ebx, [esp + 8 + 32] ; с учётом 32 байт, использованных при сохранении регистров . popad ; Извлекаем сохранённые регистры из стека ret Procedure endp

7.8. Локальные данные процедур

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

При вызове других процедур, а также в ходе выполнения текущей процедуры в стек могут быть положены другие данные. При этом значение регистра ESP изменится. Поэтому регистр ESP не является надёжной точкой отсчёта для адресов локальных переменных. Для того чтобы получить такую точку отсчёта, значение регистра ESP переписывают в регистр EBP, предварительно сохранив значение регистра EBP в стеке. В этом случае регистр EBP отмечает часть стека, занятую на момент начала работы процедуры (отсюда происходит название регистра EBP – указатель базы кадра стека). При таком подходе первый параметр процедуры всегда находится по адресу [EBP + 8]. Адреса локальных переменных отсчитываются от регистра EBP с отрицательным смещением. По окончании работы процедуры значение регистра ESP восстанавливается по регистру EBP, а значение регистра EBP – из стека.

Procedure proc var_104 = byte ptr -104h var_4 = dword ptr -4 arg_0 = dword ptr 8 arg_4 = dword ptr 0ch push ebp mov ebp, esp sub esp, 104h mov edx, [ebp + arg_0] mov eax, [ebp + arg_4] push ebx push esi push edi . pop edi pop esi pop ebx mov esp, ebp pop ebp ret Procedure endp

Такой способ позволяет также отводить различное количество места под локальные данные, и при необходимости не заботится о парности команд PUSH и POP .

7.9. Рекурсивные процедуры

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

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

Для примера рассмотрим рекурсивную процедуру вычисления факториала целого беззнакового числа. Процедура получает параметр через стек и возвращает результат через регистр EAX.

factorial proc mov eax, [esp + 4] ; Заносим в регистр EAX параметр процедуры test eax, eax ; Проверяем значение в регистре EAX jz END ; Если EAX = 0, то обходим рекурсивную ветвь dec eax ; Уменьшаем значение в регистре EAX на 1 push eax ; Кладём в стек параметр для следующего рекурсивного вызова call factorial ; Вызываем процедуру add esp, 4 ; Очищаем стек, т.к. процедура использует RET без параметров mul dword ptr [esp + 4] ; Умножаем EAX, хранящий результат предыдущего вызова, на параметр текущего вызова процедуры ret ; Возврат из процедуры (без параметров) END: inc eax ; Если EAX был равен 0, записываем в EAX единицу ret ; Возврат из процедуры (без параметров) factorial endp

8. Оптимизация программ, написанных на языке ассемблера

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

Проблему оптимизации принято делить на три основных уровня:

  1. выбор наиболее оптимального алгоритма – высокоуровневая оптимизация;
  2. наиболее оптимальная реализация алгоритма – оптимизация среднего уровня;
  3. подсчёт тактов, тратящихся на выполнение каждой команды, и оптимизация их порядка для конкретного процессора – низкоуровневая оптимизация.

8.1. Высокоуровневая оптимизация

Выбор оптимального алгоритма для решения задачи всегда приводит к лучшим результатам, чем любой другой вид оптимизации. Действительно, при замене пузырьковой сортировки, время выполнения которой пропорционально n 2 , на быструю сортировку, время выполнения которой пропорционально n * log(n) , вторая программа будет выполняться быстрее в подавляющем большинстве случаев, как бы она ни была реализована. Поиск лучшего алгоритма – универсальная стадия, и она относится не только к ассемблеру, но и к любому языку программирования, поэтому будем считать, что оптимальный алгоритм уже выбран.

8.2. Оптимизация среднего уровня

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

8.2.1. Вычисление констант вне цикла

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

8.2.2. Перенос проверки условия в конец цикла

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


; for (i = start_i; i mov edi, start_i ; Начальное значение счётчика mov esi, n ; Конечное значение счётчика loop_start: cmp edi, esi ; Пока EDI inc edi jmp loop_start loop_end: ; i = start_i; do < >while (i inc edi cmp edi, esi jb loop_start ; Пока EDI

Предположим, в цикле должен быть один шаг. Тогда в цикле с предусловием будет выполнено сравнение, тело цикла, безусловный переход к началу цикла, сравнение и переход за цикл. В цикле с постусловием будет выполнено тело цикла, сравнение и нереализованный переход. Таким образом, в цикле с предусловием выполняется одно лишнее сравнение и два реализованных перехода (2 * 3 такта = 6 тактов) вместо одного нереализованного (1 такт). Вроде бы и немного, но если цикл окажется внутри другого цикла, то все эти лишние такты будут повторяться многократно. Кроме того, цикл с постусловием содержит на одну команду меньше.

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

8.2.3. Выполнение цикла задом наперёд

Циклы, в которых значение счётчика растёт от единицы или нуля до некоторой величины, можно реализовать вообще без операции сравнения, выполняя цикл в обратном направлении. Флаги меняются не только командой сравнения, но и многими другими. В частности, команда DEC меняет флаги AF, OF, PF, SF и ZF. Команда сравнения кроме этих флагов меняет также флаг CF, но для сравнения с нулём можно обойтись флагами SF и ZF.

; Цикл от 10 до 1 mov edx, 10 loop_start: dec edx ; Уменьшаем EDX на 1. Если EDX = 0, то ZF = 1 jnz loop_start ; Переход если ZF = 0. Когда EDX = 0, ZF = 1, поэтому выходим из цикла ; Цикл от 10 до 0 mov edx, 10 loop_start: dec edx ; Уменьшаем EDX на 1. Если EDX = -1, то SF = 1 jns loop_start ; Переход если SF = 0. Когда EDX = -1, SF = 1, поэтому выходим из цикла

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

8.2.4. Разворачивание циклов

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

; Цикл от 10 до -1 mov edx, 10 loop_start: dec edx jns loop_start ; Выходим из цикла, когда EDX станет равны -1 ; Но повторяем тело цикла ещё раз

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

8.3. Низкоуровневая оптимизация

8.3.1. Основные принципы

Так как современные процессоры используют весьма сложный набор команд, большинство операций можно выполнить на низком уровне очень многими способами. При этом иногда оказывается, что наиболее очевидный способ – не самый быстрый. Часто простыми перестановками команд, зная механизм выполнения команд на современных процессорах, можно заставить ту же процедуру выполняться на 50–200% быстрее. Разумеется, переходить к этому уровню оптимизации можно только после того, как текст программы окончательно написан и максимально оптимизирован на среднем уровне.

Перечислим основные рекомендации.

  • Используйте регистр ЕАХ всюду, где возможно. Команды с непосредственным операндом, с операндом – абсолютным адресом переменной и команды XCHG с регистрами занимают на один байт меньше, если другой операнд – регистр ЕАХ.
  • Если к переменной в памяти, адресуемой со смещением, выполняется несколько обращений – загрузите её в регистр.
  • Не используйте сложные команды – ENTER , LEAVE , LOOP , строковые команды, если аналогичное действие можно выполнить небольшой последовательностью простых команд.
  • Не используйте умножение или деление на константу – его можно заменить другими командами (см. раздел 6.3).
  • Старайтесь программировать условия и переходы так, чтобы переход выполнялся по менее вероятному событию.
  • Следующее эмпирическое правило, относящееся к переходам и вызовам, очень простое: избавляться от них везде, где только можно. Для этого организуйте программу так, чтобы она исполнялась прямым, последовательным образом, с минимальным числом точек принятия решения. В результате очередь команд будет почти всегда заполнена, а вашу программу будет легче читать, сопровождать и отлаживать. Процедуры, особенно небольшие, нужно не вызывать, а встраивать. Это, конечно, увеличивает размер программы, но даёт существенный выигрыш во времени её исполнения.
  • Используйте короткую форму команды JMP , где возможно ( jmp short ).
  • Команда LEA быстро выполняется и имеет много неожиданных применений (см. раздел 8.3.2).
  • Многие одиночные команды, как это ни странно, выполняются дольше, чем две или три команды, приводящие к тому же результату. Это может быть связано с различными особенностями выполнения команд, в том числе, с возможностью/невозможность попарного выполнения команд в разных конвейерах (см. раздел 8.3.3).
  • Старайтесь выравнивать данные и метки по адресам, кратным 2/4/8/16 (см. раздел 8.3.4).
  • Если команда обращается к 32-битному регистру, например ЕАХ, сразу после команды, выполнявшей запись в соответствующий частичный регистр (АХ, AL, АН), может происходить пауза в один или несколько тактов.

8.3.2. Использование команды LEA

  • Команда LEA может использоваться для трёхоперандного сложения (но только сложения, а не вычитания).

lea eax, [ebx + edx]

  • Команда LEA может использоваться для сложения значения регистра с константой или вычитания константы из значения регистра. В данном случае вычитание возможно, т.к. оно рассматривается как сложение с отрицательной константой. Результат может быть помещён в тот же или другой регистр (кроме регистра ESP). Такой способ используется для сохранения флагов, т.к. команда LEA , в отличие от команд ADD , SUB , INC и DEC , не меняет флаги.

lea eax, [eax + 1] ; Сохраняем флаги lea eax, [ebx – 4]

  • Команда LEA может использоваться для быстрого умножения на константы 2, 3, 4, 5, 7(?), 8, 9. Адрес, загружаемый командой LEA , может быть суммой двух регистров, один из которых может быть умножен на константу 2, 4 или 8. Поэтому комбинируя умножение и сложение можно получить вышеперечисленные константы. Третье слагаемое может быть константой.

lea eax, [eax * 4 + eax] ; EAX = EAX * 5 lea eax, [ebx * 8 + ecx – 32]

8.3.3. Замена команд

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

test eax, eax jz ; Переход, если EAX = 0

  • Если за командой CALL сразу же следует команда RET , замените эти команды командой JMP . Вызываемая процедура осуществит возврат по адресу возврата, переданному вызывающей процедуре.

call dest jmp dest ret

  • Команду CBW можно заменить засылкой нуля, если расширяемое число положительное. Команду CDQ можно заменить засылкой нуля, если расширяемое число положительное, или парой команд MOV + SAR , если знак расширяемого числа не известен. Недостаток – команды XOR и SAR меняют флаги.

cdq xor edx, edx cdq mov edx, eax sar edx, 31

  • Вместо команд инкремента и декремента можно использовать команду LEA .
  • Сложение и вычитание с константой можно заменить командой LEA .
  • Вместо умножения и деления на степень числа 2 используйте сдвиги.
  • Умножение и деление на константу можно заменить командой LEA или сочетанием команд сдвига и команд сложения и вычитания.
  • Деление на константу можно заменить умножением на константу.
  • Обнуление регистров производится с помощью команды XOR .

xor eax, eax ; EAX = 0 при любом значении EAX, которое было до этой команды

  • Не используйте команду MOVZX для чтения байта – это требует 3 тактов для выполнения. Заменой может служить такая пара команд, выполняющаяся за 2 такта:

xor еах, еах mov al,

  • Засылку непосредственного операнда в ячейку памяти можно производить через регистр – такие команды лучше спариваются.

mov x, 1 mov eax, 1 mov x, eax mov [ebx], 1 mov eax, 1 mov [ebx], eax

  • Аналогично команды PUSH и POP , работающие с ячейкой памяти, можно заменить парой команд MOV + PUSH или POP + MOV .

push x mov eax, x push eax pop x pop eax mov x, eax

8.3.4. Выравнивание

  • 80-битные данные должны быть выравнены по 16-байтным границам (то есть четыре младших бита адреса должны быть равны нулю).
  • Восьмибайтные данные должны быть выравнены по восьмибайтным границам (то есть три младших бита адреса должны быть равны нулю).
  • Четырёхбайтные данные должны быть выравнены по границе двойного слова (то есть два младших бита адреса должны быть равны нулю).
  • Двухбайтные данные должны быть выравнены по границе слова.
  • Метки для переходов, особенно метки, отмечающие начало цикла, должны быть выравнены по 16-байтным границам.

Каждое невыравненное обращение к данным означает потерю тактов процессора.

Для выравнивания данных и кода используется директива ALIGN :

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

9. Примеры

  1. Процедура вычисления наибольшего общего делителя двух беззнаковых чисел. Для нахождения НОД используется алгоритм Евклида: пока числа не равны, надо вычитать из большего числа меньшее. Процедура получает параметры через регистры EAX и EDX и возвращает результат через регистр EAX.

NOD proc N1: cmp eax, edx ; Сравниваем числа je N3 ; Если числа равны, завершаем работу процедуры ja N2 ; Если первое число больше, обходим обмен ; Поскольку команды перехода не меняют флаги, оба перехода ; выполняются или не выполняются по результатам одного сравнения xchg eax, edx ; Если первое число было меньше, выполняем обмен N2: sub eax, edx ; Вычитаем из большего числа меньшее jmp N1 ; Переход к началу цикла N3: ret NOD endp

  1. Ввод и вывод в консольном приложении. В программе используются следующие функции Win32 API.
  • SetConsoleTitle – меняет заголовок окна консоли. Получает один параметр – указатель на строку, которая будет выведена в заголовке. Строка должна заканчиваться нулём.
  • GetStrHandle – возвращает идентификатор устройства ввода, устройства вывода или устройства отчёта об ошибках. Для консольного приложения всё три устройства являются консолью, но идентификаторы будут разными. Функция получает один параметр – указание, идентификатор какого устройства нужно вернуть. Чтобы получить идентификатор устройства ввода, надо передать функции число -10, чтобы получить идентификатор устройства вывода – число -11, а чтобы получить идентификатор устройства отчёта об ошибках – число -12. Функция возвращает требуемый идентификатор через регистр EAX.
  • WriteConsole – выводит строку в консоль. Получает следующие параметры – идентификатор устройства вывода, адрес выводимой строки, количество символов для вывода, адрес переменной, куда будет записано количество выведенных символов, зарезервированный указатель.
  • ReadConsole – вводит строку из консоли. Получает следующие параметры – идентификатор устройства ввода, адрес памяти, куда будет записана введённая строка, максимальное количество читаемых символов, адрес переменной, куда будет записано реальное количество введённых символов, зарезервированный указатель.

Не забывайте, что параметры кладутся в стек, начиная с последнего, и что введённая строка всегда будет содержать в конце символы с кодами 13 и 10, которые появляются при нажатии на клавишу ВВОД (без чего, однако, ввод не завершится).

.686 .model flat, c option casemap: none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data str db 256 dup(0) hStdIn dd 0 hStdOut dd 0 slength dd 0 .const sConsoleTitle db ‘Input and Output’,0 ; Заголовок окна консоли. Заканчивается нулём prompt db ‘Input a string’, 13,10 ; Приглашение для ввода. Символы с кодами 13 и 10 ; обеспечивают перевод курсора на следующую строку STD_INPUT_HANDLE equ -10d ; Определяем символические имена для констант, STD_OUTPUT_HANDLE equ -11d ; указывающих требуемое устройство .code program: ; Вывод заголовка консоли push offset sConsoleTitle ; Кладём в стек адрес начала строки заголовка консоли call SetConsoleTitle ; Вызываем функцию ; Получаем идентификатор устройства ввода push STD_INPUT_HANDLE ; Кладём в стек параметр функции GetStdHandle call GetStdHandle ; Вызываем функцию mov hStdIn, eax ; Сохраняем полученный идентификатор ; Получаем идентификатор устройства вывода push STD_OUTPUT_HANDLE call GetStdHandle mov hStdOut, eax ; Выводим приглашение push 0 ; Зарезервированный параметр, в стек кладём 0 push 0 ; Указатель на переменную для записи количества выведенных символов, ; в данном случае не нужен, поэтому в стек кладём 0 push 10h ; Количество выводимых символов push offset prompt ; Адрес выводимой строки push hStdOut ; Идентификатор устройства вывода call WriteConsole ; Вызываем функцию ; Вводим строку push 0 ; Зарезервированный параметр, в стек кладём 0 push offset slength ; Адрес переменной, куда будет записано количество введённых символов push 256 ; Максимальное количество вводимых символов push offset str ; Адрес для записи введённой строки push hStdIn ; Идентификатор устройства ввода call ReadConsole ; Вызываем функцию ; Выводим строку push 0 push 0 push slength push offset str push hStdOut call WriteConsole ; Задержка push 1800h call Sleep push 0 call ExitProcess end program

  1. Процедура ввода целого числа в 16-ричной системе счисления. Процедура предназначена для использования в консольном приложении и предполагает, что идентификатор устройства ввода был получен основной программой и сохранён в переменной hStdIn.

InputNumber proc push ebp ; Сохраняем в стеке значение регистра EBP mov ebp, esp ; Заносим в регистр EBP текущее значение вершины стека sub esp, 16 ; Резервируем 16 байт. Вводимая строка может содержать до 8 цифр. ; 2 байта требуются для символов с кодами 13 и 10. Итого 10 байт. ; 4 байта нужно для целочисленной переменной, куда будет записываться количество ; введённых символов. Итого 14 байт. Но выделим 16 байт, т.е. 4 двойных слова push ebx ; Сохраняем значения важных регистров push esi ; Вводим строку push 0 lea eax, [ebp — 16] ; 4 байта по адресу [EBP – 16] предназначены для хранения количества введённых символов push eax push 10d lea eax, [ebp — 12] ; По адресу [EBP – 12] начинается память для вводимой строки push eax push hStdIn call ReadConsole ; Преобразуем строку в число xor eax, eax ; Обнуляем регистр EAX . xor ebx, ebx ; . и регистр EBX mov ecx, [ebp — 16] ; Заносим в регистр ECX количество введённых символов sub ecx, 2 ; Символы с кодами 13 и 10 обрабатывать не надо lea esi, [ebp — 12] ; Заносим в регистр ESI адрес начала строки test ecx, ecx ; Используем команду TEST для сравнения с нулём jz L2 ; Если ECX = 0, то завершаем работу процедуры L1: mov bl, [esi] ; Заносим в регистр BL текущий символ (три старших байта EBX ; содержат 0, т.к. ранее была команда XOR EBX, EBX) lea edx, [ebx — ‘0’] ; Заносим в регистр EDX разность между кодом текущего символа и кодом символа ‘0’ cmp edx, 9 ; Сравниваем значение в регистре EDX с 9 ja M1 ; Если выше, то переходим к следующему сравнению sub bl, ‘0’ ; Иначе получаем число из кода символа jmp M3 ; Переходим к действиям, учитывающим текущую цифру M1: lea edx, [ebx — ‘a’] ; Заносим в регистр EDX разность между кодом текущего символа и кодом символа ‘a’ cmp edx, ‘f’ — ‘a’ ; Сравниваем значение в регистре EDX с 5 ja M2 ; Если выше, то переходим к следующему сравнению sub bl, ‘a’ — 10d ; Иначе получаем число из кода символа jmp M3 ; Переходим к действиям, учитывающим текущую цифру M2: lea edx, [ebx — ‘A’] ; Заносим в регистр EDX разность между кодом текущего символа и кодом символа ‘A’ cmp edx, ‘F’ — ‘A’ ; Сравниваем значение в регистре EDX с 5 ja L2 ; Если выше, то завершаем процедуру. Результат не определён, ; т.к. был введён некорректный символ sub bl, ‘A’ — 10d ; Иначе получаем число из кода символа M3: sal eax, 4 ; Умножаем EAX на 16 add eax, ebx ; Прибавляем текущую цифру inc esi ; Переходим к следующему символу dec ecx ; Уменьшаем ECX на 1 jnz L1 ; Если ECX не равно 0, продолжаем цикл L2: pop esi ; Восстанавливаем значения использовавшихся регистров pop ebx mov esp, ebp ; Освобождаем стек pop ebp ; Восстанавливаем значение регистра EBP ret InputNumber endp

  1. Процедура вывода числа в 16-ричной системе счисления. Процедура получает один параметр – выводимое число. Для вывода всегда формируется строка из 8-ми шестнадцатеричных цифр с лидирующими нулями. Поскольку количество символов заранее известно, они будут сразу же записываться в строку с конца, и инвертировать строку не придётся. Процедура предназначена для использования в консольном приложении и предполагает, что идентификатор устройства ввода был получен основной программой и сохранён в переменной hStdOut.

digits db ‘0123456789abcdef’ ; Массив шестнадцатеричных цифр OutputNumber proc push ebp ; Сохраняем в стеке значение регистра EBP mov ebp, esp ; Заносим в регистр EBP текущее значение вершины стека sub esp, 12 ; Выделяем в стеке место под формируемую строку push esi ; Преобразуем число в строку mov eax, [ebp + 8] ; Заносим в регистр EAX переданный параметр mov ecx, 8 ; Заносим в регистр ECX количество символов строки mov byte ptr [ebp — 1], 10 ; Добавляем в конец строки символы с кодами 13 и 10 для перевода курсора mov byte ptr [ebp — 2], 13 lea esi, [ebp — 3] ; Начиная с адреса [EBP — 3] будут заносится цифры L3: mov edx, eax ; Копируем значение регистра EAX в регистр EDX and edx, 1111b ; Получаем остаток от деления на 16 shr eax, 4 ; Делим исходное число на 16 mov dl, digits[edx] ; По полученному остатку от деления берём цифру . mov [esi], dl ; . и записываем её в строку dec esi ; Уменьшаем адрес, т.к. строка формируется с конца dec ecx ; Уменьшаем ECX на 1 jnz L3 ; Если ECX не равно 0, продолжаем цикл ; Выводим строку inc esi ; Регистр ESI указывает на начало строки push 0 push 0 push 10 push esi push hStdOut call WriteConsole pop esi mov esp, ebp ; Освобождаем стек pop ebp ; Восстанавливаем значение регистра EBP ret 4 ; Удаляем из стека переданный параметр и возвращаемся OutputNumber endp

  1. Функция, находящая в одномерном массиве x сумму значений f(x[i]), где f – некоторая функция одного целочисленного аргумента, адрес которой передаётся через параметры. Функции используют соглашение о вызовах cdecl.

Sum proc push ebp mov ebp, esp push esi push edi mov ecx, [ebp + 8] ; Заносим в ECX первый параметр – количество элементов массива mov esi, [ebp + 12] ; Заносим в ESI второй параметр – адрес начала массива mov edi, [ebp + 16] ; Заносим в EDI третий параметр – адрес функции xor edx, edx ; Обнуляем регистр EDX L: push [esi] ; Кладём в стек элемент массива call edi ; Вызываем функцию, адрес которой находится в регистре EDI add esp, 4 ; Освобождаем стек add edx, eax ; Прибавляем результат функции к общей сумме add esi, 4 ; Переходим к следующему элементу массива dec ecx ; Уменьшаем значение регистра ECX на 1 jnz L ; Если ECX не равно 0, продолжаем цикл mov eax, edx ; Записываем полученную сумму в регистр EAX, ; через который должен возвращаться результат функции pop edi pop esi mov esp, ebp pop ebp ret Sum endp Sqr proc mov eax, [esp + 4] imul eax ret Sqr endp Negation proc mov eax, [esp + 4] neg eax ret Negation endp

Для вызова функции Sum будет использовать следующая последовательность команд.

push Sqr push offset a push na call Sum add esp, 12 mov sa, eax push Negation push offset a push na call Sum add esp, 12 mov sa, eax

  1. Процедура, проверяющая сбалансированность круглых и квадратных скобок в строке. Строка должна заканчиваться нулём. Для проверки сбалансированности открывающие скобки будем класть в стек, а при нахождении в строке закрывающей скобки будем извлекать из стека последнюю положенную туда открывающую скобку и проверять, что она соответствует закрывающей скобке. Будем считать, что скобок в тексте меньше, чем других символов, поэтому после сравнения делаем переход «если равно», считая, что это событие менее вероятно. При любом выходе из процедуры нужно очистить стек. Поскольку мы не можем заранее знать, сколько скобок будет туда положено и сколько извлечено, восстановление значения регистра ESP можно сделать только с помощью регистра EBP. Процедура возвращает значение через регистр EAX: если скобки сбалансированы, регистр EAX будет содержать значение истина (-1), в противном случае регистр EAX будет содержать значение ложь (0).

Brackets proc push ebx ; Сохраняем регистры push ebp mov ebp, esp ; Сохраняем начальное значение регистра ESP mov ebx, [ebp + 12] ; Заносим в регистр EBX адрес начала строки mov eax, -1 ; Заносим в регистр EAX предварительное значение результата xor edx, edx ; Обнуляем регистр EDX L1: mov dl, [ebx] ; Заносим в регистр DL очередной символ test edx, edx ; Проверяем значение в регистре EDX jz E1 ; Если EDX = 0, выходим из цикла inc ebx ; Меняем адрес символа cmp dl, ‘(‘ ; Сравниваем символ с открывающей круглой скобкой je L2 ; Если равно, . cmp dl, ‘[‘ ; Сравниваем символ с открывающей квадратной скобкой je L2 ; Если равно, . cmp dl, ‘)’ ; Сравниваем символ с закрывающей круглой скобкой je L3 ; Если равно, переходим к сравнению со скобкой из стека cmp dl, ‘]’ ; Сравниваем символ с закрывающей квадратной скобкой je L4 ; Если равно, переходим к сравнению с другой скобкой из стека jmp L1 ; Если символ – не скобка, возвращаемся к началу цикла L2: push dx ; . заносим открывающую скобку в стек (один байт записать в стек нельзя) jmp L1 ; Возвращаемся к началу цикла L3: cmp ebp, esp ; Если была закрывающая скобка, прежде всего проверяем, есть ли скобки в стеке – ; если мы положили что-то в стек, значение регистра ESP будет отличаться от регистра EBP je E2 ; Если значения регистров равны, выходим из процедуры pop cx ; Извлекаем из стека последнюю открывающую скобку cmp cl, ‘(‘ ; Сравниваем jne E2 ; Если скобки не равны, выходим из процедуры jmp L1 ; Иначе возвращаемся к началу цикла L4: cmp ebp, esp ; При нахождении закрывающей квадратной скобки, je E2 ; выполняем те же действия, что и при нахождении закрывающей круглой скобки, pop cx ; только скобку из стека сравниваем с другим значением cmp cl, ‘[‘ ; Дублирование сделано для того, чтобы уменьшить jne E2 ; количество переходов jmp L1 E1: cmp ebp, esp ; При достижении конца строки, сравниваем регистры ESP и EBP je E3 ; Если значения равны, обходим обнуление регистра EAX E2: xor eax, eax ; Если была несбалансированность, обнуляем регистр EAX E3: mov esp, ebp ; Восстанавливаем значение регистра ESP pop ebp pop ebx ret Brackets endp

1 В защищённом режиме программе выделяется один сегмент размером 4 Гб для кода и один сегмент размером 4 Гб для данных (физически они обычно совпадают). Виртуальный адрес состоит из 16-битного значения, хранящегося в сегментном регистре, и 32-битного смещения. Однако преобразование виртуального адреса в физический осуществляется не путём сложения, а по более сложной схеме. Сначала процессор преобразует виртуальный адрес в линейный. При этом он обращается к таблицам дескрипторов, которые заранее строятся операционной системой. На втором этапе по линейному адресу определяется физический. В этом преобразовании участвует другой набор системных таблиц – таблицы страничной трансляции, которые также составляются операционной системой. Оба набора таблиц могут динамически меняться, обеспечивая максимальное использование оперативной памяти.

В сегментные регистры записываются не адреса сегментов, а селекторы, которые представляют собой номера ячеек специальной таблицы, содержащей дескрипторы сегментов программ. Каждый дескриптор хранит все характеристики, необходимые для обслуживания сегмента: базовый линейный адрес сегмента, границу сегмента (номер последнего байта), а также атрибуты сегмента, определяющие его свойства. Процессор с помощью селектора определяет индекс дескриптора адресуемого сегмента, извлекает из него базовый линейный 32-битный адрес сегмента и, сложив его с 32-битным смещением, получает линейный адрес адресуемой ячейки памяти. Получив линейный адрес адресуемого байта, процессор с помощью таблиц трансляции преобразует его в 32-битный физический адрес. Этот адрес зависит от объёма оперативной памяти, установленной на компьютере.

В 32-битной модели Windows предоставляет всем запущенным приложениям один и тот же селектор для сегмента кода и один и тот же селектор для сегмента данных. Базы обоих сегментов равны 0, а границы – FFFFFFFF. Другими слова, каждому приложению как бы предоставляется всё линейное пространство. Поскольку базовые линейные адреса сегментов программы равны 0, виртуальные смещения, с которыми работают приложения, совпадают с линейными адресами. Другими словами, плоское виртуальное адресное пространство программы совпадает с плоским линейным адресным пространством. При этом все приложения используют один и тот же диапазон линейных адресов. Для того чтобы при одинаковых линейных адресах приложения занимали различные участки физической памяти и не затирали друг друга, Windows при смене приложения изменяет таблицы страничной трансляции, с помощью которых как раз и происходит преобразование линейных адресов в физические.

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

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

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

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

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

Илон Маск рекомендует:  Что такое код pg_connection_status
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL