Uploaded by Егор Котов

Лекции Системное программирование МДК 01.01

advertisement
Лекция 1
Программы и программное обеспечение
Определение (ГОСТ)
Программа - это данные, предназначенные для управления конкретными
компонентами системы обработки информации (СОИ) в целях реализации
определенного алгоритма.
Определения даются по: ГОСТ 19781-90. Обеспечение систем обработки
информации программное. Термины и определения. - М.:Изд-во стандартов,
1990.
Обратить внимание: программа - это данные. Один из основных принципов
машины фон Неймана - то, что и программы, и данные хранятся в одной и той
же памяти. Сохраняемая в памяти программа представляет собой некоторые
коды, которые могут рассматриваться как данные. Возможно, с точки зрения
программиста программа - активный компонент, она выполняет
некоторые действия. Но с точки зрения процессора команды программы это данные, которые процессор читает и интерпретирует. С другой стороны
программа - это данные с точки зрения обслуживающих программ, например, с
точки зрения компилятора, который на входе получает одни данные программу на языке высокого уровня (ЯВУ), а на выходе выдает другие данные
- программу в машинных кодах.
Определение (ГОСТ)
Программное обеспечение (ПО) - совокупность программ
программных документов, необходимых для их эксплуатации
СОИ
и
Существенно, что ПО - это программы, предназначенные для многократного
использования и применения разными пользователями. В связи с этим следует
обратить внимание на ряд необходимых свойств ПО.
1. Необходимость документирования. По определению программы
становятся ПО только при наличии документации. Конечный
пользователь не может работать, не имея документации. Документация
делает возможным тиражирование ПО и продажу его без его
разработчика. По Бруксу ошибкой в ПО является ситуация, когда
программное изделие функционирует не в соответствии со своим
описанием, следовательно, ошибка в документации также является
ошибкой в программном изделии.
2. Эффективность. ПО, рассчитанное на многократное использование
(например, ОС, текстовый редактор и т.п.) пишется и отлаживается один
раз, а выполняется многократно. Таким образом, выгодно переносить
затраты на этап производства ПО и освобождать от затрат этап
выполнения, чтобы избежать тиражирования затрат.
3. Надежность. В том числе:
4. Тестирование программы при всех допустимых спецификациях входных
данных
5. Защита от неправильных действий пользователя
6. Защита от взлома - пользователи должны иметь возможность
взаимодействия с ПО только через легальные интерфейсы. Готье:
"Ошибки в системе возможны из-за сбоев аппаратуры, ошибок ПО,
неправильных действий пользователя. Первые - неизбежны, вторые вероятны,
третьи
гарантированы".
Появление ошибок любого уровня не должно приводить к краху системы.
Ошибки должны вылавливаться диагностироваться и (если их
невозможно исправить) превращаться в корректные отказы.
Системные структуры данных должны сохраняться безусловно.
Сохранение целостности пользовательских данных желательно.
7. Возможность сопровождения. Возможные цели сопровождения адаптация ПО к конкретным условиям применения, устранение ошибок,
модификация.
Во всех случаях требуется тщательное структурирование ПО и носителем
информации о структуре ПО должна быть программная документация.
Адаптация во многих случаях м.б. передоверена пользователю - при
тщательной отработке и описании сценариев инсталляции и настройки.
Исправление ошибок требует развитой сервисной службы, собирающей
информацию об ошибках и формирующей исправляющие пакеты.
Модификация предполагает изменение спецификаций на ПО. При этом,
как правило, должны поддерживаться и старые спецификации.
Эволюционное развитие ПО экономит вложения пользователей.
Системное программирование
Определение (ГОСТ)
Системная программа - программа, предназначенная для поддержания
работоспособности СОИ или повышения эффективности ее использования.
Определение (ГОСТ)
Прикладная программа - программа, предназначенная для решения задачи
или класса задач в определенной области применения СОИ.
В соответствии с терминологией, системное программирование - это процесс
разработки системных программ (в т.ч., управляющих и обслуживающих).
С другой стороны, по определению Гегеля система - единое целое, состоящее из
множества компонентов и множества связей между ними. Тогда системное
программирование - это разработка программ сложной структуры.
Эти два определения не противоречат друг другу, так как разработка программ
сложной структуры ведется именно для обеспечения работоспособности или
повышения эффективности СОИ.
Зафиксированное в ГОСТ подразделение ПО на системное и прикладное
является до некоторой степени устаревшим. Сегодняшнее деление
предусматривает по меньшей мере три градации ПО:
Системное
 Промежуточное
 Прикладное
Промежуточное ПО (middleware) мы определяем как совокупность программ,
осуществляющих управление вторичными (конструируемыми самим ПО)
ресурсами, ориентированными на решение определенного (широкого) класса
задач. К такому ПО относятся менеджеры транзакций, серверы БД, серверы
коммуникаций и другие программные серверы. С точки зрения
инструментальных средств разработки промежуточное ПО ближе к
прикладному, так как не работает на прямую с первичными ресурсами, а
использует для этого сервисы, предоставляемые системным ПО. С точки зрения
алгоритмов и технологий разработки промежуточное ПО ближе к системному,
так как всегда является сложным программным изделием многократного и

многоцелевого использования и в нем применяются те же или сходные
алгоритмы, что и в системном ПО.
Современные тенденции развития ПО состоит в снижении объема как
системного, так и прикладного программирования. Основная часть работы
программистов выполняется в промежуточном ПО. Снижение объема
системного программирования определено современными концепциями ОС,
объектно-ориентированной архитектурой и архитектурой микроядра, в
соответствии с которыми большая часть функций системы выносится в
утилиты, которые можно отнести и к промежуточному ПО. Снижение объема
прикладного программирования обусловлено тем, что современные продукты
промежуточного ПО предлагают все больший набор инструментальных средств
и шаблонов для решения задач своего класса.
Значительная часть системного и практически все прикладное ПО пишется на
языках высокого уровня, что обеспечивает сокращение расходов на их
разработку/модификацию и переносимость.
Системное ПО подразделяется на системные управляющие программы и
системные обслуживающие программы.
Определение (ГОСТ)
Управляющая программа - системная программа, реализующая набор
функций управления, который включает в себя управление ресурсами и
взаимодействие с внешней средой СОИ, восстановление работы системы после
проявления неисправностей в технических средствах.
Определение (ГОСТ)
Программа обслуживания (утилита) - программа, предназначенная для
оказания услуг общего характера пользователям и обслуживающему
персоналу СОИ.
Управляющая программа совместно с набором необходимых для эксплуатации
системы утилит составляют операционную систему (ОС).
Кроме входящих в состав ОС утилит могут существовать и другие утилиты
(того же или стороннего производителя), выполняющие дополнительное
(опционное) обслуживание. Как правило, это утилиты, обеспечивающие
разработку программного обеспечения для операционной системы.
Определение (ГОСТ)
Система программирования - система, образуемая языком программирования,
компилятором или интерпретатором программ, представленных на этом языке,
соответствующей документацией, а также вспомогательными средствами для
подготовки программ к форме, пригодной для выполнения.
Лекция 2
Этапы подготовки программы
При разработке программ, а тем более - сложных, используется принцип
модульности, разбиения сложной программы на составные части, каждая из
которых может подготавливаться отдельно. Модульность является основным
инструментом структурирования программного изделия, облегчающим его
разработку, отладку и сопровождение.
Определение (ГОСТ)
Программный модуль - программа или функционально завершенный
фрагмент программы, предназначенный для хранения, трансляции,
объединения с другими программными модулями и загрузки в
оперативную память.
При выборе модульной структуры должны учитываться следующие основные
соображения:

Функциональность - модуль должен выполнять законченную функцию

Несвязность - модуль должен иметь минимум связей с другими
модулями, связь через глобальные переменные и области памяти
нежелательна

Специфицируемость - входные и выходные параметры модуля должны
четко формулироваться
На рисунке показаны этапы, которые проходит программа от своего написания
до выполнения
Программа пишется в виде исходного модуля, на рисунке - файл ИМ.
Определение (ГОСТ)
Исходный модуль - программный модуль на исходном языке, обрабатываемый
транслятором и представляемый для него как целое, достаточное для
проведения трансляции.
Первым (не для всех языков программирования обязательным) этапом
подготовки программы является обработка ее Макропроцессором (или
Препроцессором). Макропроцессор обрабатывает текст программы и на выходе
его получается новая редакция текста (на рис. - ИМ'). В большинстве систем
программирования Макропроцессор совмещен с транслятором, и для
программиста его работа и промежуточный ИМ' "не видны". Следует иметь в
виду, что Макропроцессор выполняет обработку текста, это означает, с одной
стороны, что он "не понимает" операторов языка программирования и "не
знает" переменных программы, с другой, что все операторы и переменные
Макроязыка (тех выражений в программе, которые адресованы
Макропроцессору) в промежуточном ИМ' уже отсутствуют и для дальнейших
этапов обработки "не видны". Так, если Макропроцессор заменил в программе
некоторый текст A на текст B, то транслятор уже видит только текст B, и не
знает, был этот текст написан программистом "своей рукой" или подставлен
Макропроцессором.
Следующим этапом является трансляция.
Определение (ГОСТ)
Трансляция - преобразование программы, представленной на одном языке
программирования, в программу на другом языке программирования, в
определенном смысле равносильную первой.
Как правило, выходным языком транслятора является машинный язык целевой
вычислительной системы. (Целевая ВС - та ВС, на которой программа будет
выполняться.)
Определение (ГОСТ)
Машинный язык - язык программирования, предназначенный для
представления программы в форме, позволяющей выполнять ее
непосредственно техническими средствами обработки информации.
Трансляторы - общее название для программ, осуществляющих трансляцию.
Они подразделяются на Ассемблеры и Компиляторы - в зависимости от
исходного языка программы, которую они обрабатывают. Ассемблеры
работают с Автокодами или языками Ассемблера, Компиляторы - с языками
высокого уровня.
Определение (ГОСТ)
Автокод - символьный язык программирования, предложения которого по
своей структуре в основном подобны командам и обрабатываемым данным
конкретного машинного языка.
Определение (ГОСТ)
Язык Ассемблера - язык программирования, который представляет собой
символьную форму машинного языка с рядом возможностей, характерных для
языка высокого уровня (обычно включает в себя макросредства).
Определение (ГОСТ)
Язык высокого уровня - язык программирования, понятия и структура которого
удобны для восприятия человеком.
Определение (ГОСТ)
Объектный модуль - программный модуль, получаемый в результате
трансляции исходного модуля.
Поскольку результатом трансляции является модуль на языке, близком к
машинному, в нем уже не остается признаков того, на каком исходном языке
был написан программный модуль. Это создает принципиальную возможность
создавать программы из модулей, написанных на разных языках. Специфика
исходного языка, однако, может сказываться на физическом представлении
базовых типов данных, способах обращения к процедурам/функциям и т.п. Для
совместимости разноязыковых модулей должны выдерживаться общие
соглашения.
Большая часть объектного модуля - команды и данные машинного языка
именно в той форме, в какой они будут существовать во время выполнения
программы. Однако, программа в общем случае состоит из многих модулей.
Поскольку транслятор обрабатывает только один конкретный модуль, он
не может должным образом обработать те части этого модуля, в которых
запрограммированы обращения к данным или процедурам, определенным
в другом модуле. Такие обращения называются внешними ссылками. Те
места в объектном модуле, где содержатся внешние ссылки, транслируются в
некоторую промежуточную форму, подлежащую дальнейшей обработке.
Говорят, что объектный модуль представляет собой программу на
машинном языке с неразрешенными внешними ссылками.
Разрешение внешних ссылок выполняется на следующем этапе подготовки,
который обеспечивается Редактором Связей (Компоновщиком). Редактор
Связей соединяет вместе все объектные модули, входящие в программу.
Поскольку Редактор Связей "видит" уже все компоненты программы, он имеет
возможность обработать те места в объектных модулях, которые содержат
внешние ссылки. Результатом работы Редактора Связей является
загрузочный модуль.
Определение (ГОСТ)
Загрузочный модуль - программный модуль, представленный в форме,
пригодной для загрузки в оперативную память для выполнения.
Загрузочный модуль сохраняется в виде файла на внешней памяти. Для
выполнения программа должна быть перенесена (загружена) в оперативную
память. Иногда при этом требуется некоторая дополнительная обработка
(например, настройка адресов в программе на ту область оперативной памяти, в
которую программа загрузилась). Эта функция выполняется Загрузчиком,
который обычно входит в состав операционной системы.
Возможен также вариант, в котором редактирование связей выполняется при
каждом запуске программы на выполнение и совмещается с загрузкой. Это
делает Связывающий Загрузчик. Вариант связывания при запуске более
расходный, т.к. затраты на связывание тиражируются при каждом запуске. Но
он обеспечивает:

большую гибкость в сопровождении, так как позволяет менять отдельные
объектные модули программы, не меняя остальных модулей;

экономию внешней памяти, т.к. объектные модули, используемые во
многих программах не копируются в каждый загрузочный модуль, а
хранятся в одном экземпляре.
Вариант интерпретации подразумевает прямое исполнение исходного модуля.
Определение (ГОСТ)
Интерпретация - реализация смысла некоторого синтаксически законченного
текста, представленного на конкретном языке.
Интерпретатор читает из исходного модуля очередное предложение
программы, переводит его в машинный язык и выполняет. Все затраты на
подготовку тиражируются при каждом выполнении, следовательно,
интепретируемая программа принципиально менее эффективна, чем
транслируемая. Однако, интерпретация обеспечивает удобство разработки,
гибкость в сопровождении и переносимость.
Примеры интерпретаторов: языки процедур (sell, REXX), JVM.
Не обязательно подготовка программы должна вестись на той же
вычислительной системе и в той же операционной среде, в которых программа
будет выполняться. Системы, обеспечивающие подготовку программ в
среде, отличной от целевой называются кросс-системами. В кросс-системе
может выполняться вся подготовка или ее отдельные этапы:

Макрообработка и трансляция

Редактирование связей

Отладка
Типовое применение кросс-систем - для тех случаев, когда целевая
вычислительная среда просто не имеет ресурсов, необходимых для подготовки
программ, например, встроенные системы.
Программные средства, обеспечивающие отладку программы на целевой
системе можно также рассматривать как частный случай кросс-системы.
Лекция 3 Ассемблеры
Определение (не по ГОСТ)
Язык Ассемблера - система записи программы с детализацией до отдельной
машинной команды, позволяющая использовать мнемоническое обозначение
команд и символическое задание адресов.
Поскольку в разных аппаратных архитектурах разные программно-доступные
компоненты (система команд, регистры, способы адресации), язык Ассемблера
аппаратно-зависимый. Программы, написанные на языке Ассемблера м.б.
перенесены только на вычислительную систему той же архитектуры.
Программирование на языке Ассемблера позволяет в максимальной степени
использовать особенности архитектуры вычислительной системы. До недавнего
времени воспринималась как аксиома, что Ассемблерная программа всегда
является более эффективной и в смысле быстродействия, и в смысле требований
к памяти. Для Intel-архитектуры это и сейчас так. Но это уже не так для RISKархитектур. Для того, чтобы программа могла эффективно выполняться в
вычислительной среде с распараллеливанием на уровне команд, она д.б.
определенным образом оптимизирована, т.е., команды д.б. расположены в
определенном порядке, допускающим их параллельное выполнение.
Программист просто не сможет покомандно оптимизировать всю свою
программу. С задачей такой оптимизации более эффективно справляются
компиляторы.
Доля программ, которые пишутся на языках Ассемблеров в мире, неуклонно
уменьшается, прикладное программирование на языках Ассемблеров
применяется только по недомыслию. Язык Ассемблера "в чистом виде"
применяется только для написания отдельных небольших частей системного
ПО: микроядра ОС, самых нижних уровней драйверов - тех частей, которые
непосредственно взаимодействуют с реальными аппаратными компонентами.
Этим занимается узкий круг программистов, работающих в фирмах,
производящих аппаратуру и ОС. Зачем же нам тогда изучать построение
Ассемблера?
Хотя разработка программ, взаимодействующих с реальными аппаратными
компонентами, - редкая задача, в современном программировании при
разработке прикладного, а еще более - промежуточного ПО довольно часто
применяется технологии виртуальных машин. Для выполнения того или иного
класса задач программно моделируется некоторое виртуальное вычислительное
устройство, функции которого соответствуют нуждам этого класса задач. Для
управления таким устройством для него м.б. создан соответствующий язык
команд. (Широко известные примеры: MI AS/400, JVM.) Говоря шире, любую
программу можно представить себе как виртуальное "железо", решающее
конкретную задачу. (Конечный пользователь обычно не видит разницы между
программой и аппаратурой и часто говорит не "мне программа выдала то-то", а
"мне компьютер выдал то-то"). В некоторых случаях интерфейс программы м.б.
удобно представить в виде системы команд, а следовательно, нужен
соответствующий Ассемблер. (Это, конечно, относится не к программам "для
чайников", а к инструментальным средствам программистов, системам
моделирования и т.п.).
Предложения языка Ассемблера
Предложения языка Ассемблера описывают команды или псевдокоманды
(директивы).
Предложения-команды
задают
машинные
команды
вычислительной системы; обработка Ассемблером команды приводит к
генерации машинного кода. Обработка псевдокоманды не приводит к
непосредственной генерации кода, псевдокоманда управляет работой самого
Ассемблера. Для одной и той же аппаратной архитектуры м.б. построены
разные Ассемблеры, в которых команды будут обязательно одинаковые, но
псевдокоманды м.б. разные.
Во всех языках Ассемблеров каждое новое предложение языка начинается с
новой строки. Каждое предложение, как правило, занимает одну строку, хотя
обычно допускается продолжение на следующей строке/строках. Формат записи
предложений языка м.б. жесткий или свободный. При записи в жестком
формате составляющие предложения должны располагаться в фиксированных
позициях строки. (Например: метка должна располагаться в позициях 1-8,
позиция 9 - пустая, позиции 10-12 - мнемоника команды, позиция 13 - пустая,
начиная с позиции 14 - операнды, позиция 72 - признак продолжения). Обычно
для записи программ при жестком формате создаются бланки. Жесткий формат
удобен для обработки Ассемблером (удобен и для чтения). Свободный формат
допускает любое количество пробелов между составляющими предложения.
В общих случаях предложения языка Ассемблера состоят из следующих
компонент:

метка или имя;

мнемоника;

операнды;

комментарии.
Метка или имя является необязательным компонентом. Не во всех языках
Ассемблеров эти понятия различаются. Если они различаются (например,
MASM), то метка - точка программы, на которую передается управление,
следовательно, метка стоит в предложении, содержащем команду; имя - имя
переменной программы, ячейки памяти, следовательно, имя стоит в
предложении, содержащем псевдокоманду резервирования памяти или
определения константы. В некоторых случаях метка и имя могут отличаться
даже синтаксически, так, в MASM/TASM после метки ставится двоеточие, а
после имени - нет.
Однако, физический смысл и метки, и имени - одинаков, это - адрес памяти.
Во всех случаях, когда Ассемблер встречает в программе имя или метку, он
заменяет ее на адрес той ячейки памяти, к которую имя/метка именует. Правила
формирования
имен/меток
совпадают
с
таковыми
для
языков
программирования. В некоторых Ассемблерах (HLAM S/390) не делается
различия между меткой и именем.
В языке должны предусматриваться некоторые специальные правила,
позволяющие Ассемблеру распознать и выделить метку/имя, например:

метка/имя должна начинаться в 1-й позиции строки, если метки/имени
нет, то в 1-й позиции д.б. пробел, или

за меткой/именем должно следовать двоеточие, и т.п.
Мнемоника - символическое обозначение команды/псевдокоманды.
Операнды - один или несколько операндов, обычно разделяемые запятыми.
Операндами команд являются имена регистров, непосредственные операнды,
адреса памяти (задаваемые в виде констант, литералов, символических имен
или сложных выражений, включающих специальный синтаксис). Операнды
псевдокоманд м.б. сложнее и разнообразнее.
Комментарии - любой текст, который игнорируется Ассемблером.
Комментарии располагаются в конце предложения и отделяются от текста
предложения, обрабатываемого Ассемблером, каким-либо специальным
символом (в некоторых языках - пробелом). Всегда предусматривается
возможность строк, содержащих только комментарий, обычно такие строки
содержат специальный символ в 1-й позиции.
Операнды команд
Константы - могут представлять непосредственные операнды или абсолютные
адреса памяти. Применяются 10-ные, 8-ные, 16-ные, 2-ные, символьные
константы.
Непосредственные операнды - записываются в сам код команды.
Имена - адреса ячеек памяти. При трансляции Ассемблер преобразует имена в
адреса. Способ преобразования имени в значение зависит от принятых способов
адресации. Как правило, в основным способом адресации в машинных языках
является адресация относительная: адрес в команде задается в виде смещения
относительно какого-то базового адреса, значение которого содержится в
некотором базовом регистре. В качестве базового могут применяться либо
специальные регистры (DS, CS в Intel) или регистры общего назначения (S/390).
Литералы - записанные в особой форме константы. Концептуально литералы те же имена. При появлении в программе литерала Ассемблер выделяет ячейку
памяти и записывает в нее заданную в литерале константу. Далее все появления
этого литерала Ассемблер заменяет на обращения по адресу этой ячейки. Таким
образом, литеральные константы, хранятся в памяти в одном экземпляре,
независимо от числа обращений к ним.
Специальный синтаксис - явное описание способа адресации (например,
указание базового регистра и смещения и т.п.).
Директивы
Директивы являются указаниями Ассемблеру о том, как проводить
ассемблирование. Директив м.б. великое множество. В 1-ом приближении мы
рассмотрим лишь несколько практически обязательных директивы (мнемоники
директив везде - условные, в конкретных Ассемблерах те же по смыслу
директивы могут иметь другие мнемоники).
EQU
Определение имени. Перед этой директивой обязательно стоит имя.
Операнд этой директивы определяет значение имени. Операндом может
быть и выражение, вычисляемое при ассемблировании. Имя может
определяться и через другое имя, определенное выше. Как правило, не
допускается определение имени со ссылкой вперед.
DD
Определение данных. Выделяются ячейки памяти и в них
записываются значения, определяемые операндом директивы. Перед
директивой может стоять метка/имя. Как правило, одной директивой
могут определяться несколько объектов данных. В конкретных
Ассемблерах может существовать либо одна общая директива DD,
тогда тип данных, размещаемых в памяти определяется формой записи
операндов, либо несколько подобных директив - для разных типов
данных. В отличие от других,, эта директива приводит
непосредственной к генерации некоторого выходного кода - значений
данных.
BSS
Резервирование памяти. Выделяются ячейки памяти, но значения в
них не записываются. Объем выделяемой памяти определяется
операндом директивы. Перед директивой может стоять метка/имя.
END
Конец программного модуля. Указание Ассемблеру на прекращение
трансляции. Обычно в модуле, являющемся главным (main) операндом
этой директивы является имя точки, на которую передается управление
при начале выполнения программы. Во всех других модулях эта
директива употребляется без операндов.
Структуры (базы) данных Ассемблера
Алгоритмы работы Ассемблеров
Ассемблер просматривает исходный программный модуль один или несколько
раз. Наиболее распространенными являются двухпроходные Ассемблеры,
выполняющие два просмотра исходного модуля. На первом проходе Ассемблер
формирует таблицу символов модуля, а на втором - генерирует код программы
Двухпроходный Ассемблер 1й проход
Алгоритм работы 1-го прохода двухпроходного Ассемблера показан на рисунке.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
Начало 1-го прохода ассемблирования.
Начальные установки:
установка в 0 счетчика адреса PC;
создание пустой таблицы символов;
создание пустой таблицы литералов;
открытие файла исходного модуля;
установка в FASLE признака окончания.
Признак окончания TRUE?
Считывание следующей строки исходного модуля. Добавка к
счетчику адреса устанавливается равной 0.
При считывании был обнаружен конец файла?
Если конец файла обнаружен до того, как обработана директива
END, - ошибка (преждевременный конец файла), при этом также
устанавливается признак окончания обработки..
Лексический разбор оператора программы. При этом:
выделяется метка/имя, если она есть;
выделяется мнемоника операции;
выделяется поле операндов;
удаляются комментарии в операторе;
распознается строка, содержащая только комментарий.
Строка содержит только комментарий? В этом случае обработка
оператора не производится.
Мнемоника операции ищется в таблице директив.
Завершился ли поиск в таблице директив успешно?
Если мнемоника была найдена в таблице директив, происходит
ветвление, в зависимости от того, какая директива была опознана.
Обработка директив типа DD (определения данных) включает в
себя:
выделение элементов списка операндов (одной директивой DD
может определяться несколько объектов данных);
определение типа и, следовательно, размера объекта данных,
заданного операндом;
обработка для каждого операнда возможного коэффициента
повторения.
Добавка к счетчику адреса устанавливается равной суммарному
размеру объектов данных, определяемых директивой.
Обработка директив типа BSS подобна обработке директив типа DD.
Добавка к счетчику адреса устанавливается равной суммарному
объему памяти, резервируемому директивой.
Обработка директивы END состоит в установке в TRUE признака
окончания обработки.
Обработка директивы включает в себя вычисление значения имени и
занесение его в таблицу символов.
Обработка прочих директив ведется по индивидуальным для каждой
директивы алгоритмам. Существенно, что никакие директивы, кроме
DD и BSS, не изменяют нулевого значения добавки к счетчику
адреса.
Если мнемоника операции не найдена в таблице директив, она
ищется в таблице команд.
Завершился ли поиск в таблице команд успешно?
Если мнемоника не была найдена в таблице команд, - ошибка
(неправильная мнемоника).
Если мнемоника найдена в таблице команд - определение длины
команды, она же будет добавкой к счетчику адреса.
Есть ли в операторе литерал?
Занесение литерала в таблицу литералов (если его еще нет в
таблице).
Была ли в операторе метка?
Поиск имени в таблице символов.
Имя в таблице символов найдено?
Если имя найдено в таблице символов - ошибка (повторяющееся
имя).Если имя не найдено в таблице символов - занесение имени в
таблицу символов.
Формирование и печать строки листинга.
Модификация счетчика адреса вычисленной добавкой к счетчику
Печать строки листинга и переход к чтению следующего оператора.
При окончании обработки - закрытие файла исходного модуля.
Были ли ошибки на 1-ом проходе ассемблирования?
Формирование литерального пула
Выполнение 2-го прохода ассемблирования.
Конец работы Ассемблера.
Примечания
1.
Определение длины команды (п.21). Эта задача может решаться
существенно разным образом для разных языков. В языках некоторых
Ассемблеров мнемоника команды однозначно определяет ее формат и длину
(S/390, все RISC-процессоры). В этом случае длина команды просто выбирается
из таблицы команд. В других языках длина и формат зависит от того, с какими
операндами употреблена команда (Intel). В этом случае длина вычисляется по
некоторому специфическому алгоритму, в который входит выделение
отдельных операндов и определение их типов. В последнем случае должна
производиться также необходимая проверка правильности кодирования
операндов (количество операндов, допустимость типов).
2.
Обнаружение литералов (п.22). Требует, как минимум, выделения
операндов команды. (Подробнее об обработке литералов см. ниже).
3.
Листинг. Строка листинга печатается в конце каждой итерации
обработки команды. Строка листинга 1-го прохода содержит: номер оператора,
значение счетчика адреса (только для команд и директив, приводящих к
выделению памяти), текст оператора. Листинг 1-го прохода не является
окончательным, фактически он используется только для поиска ошибок,
поэтому печатать его необязательно. Режим печати листинга 1-го прохода
может определяться параметром Ассемблера или специальной директивой.
После листинга программы может (опционно) печататься таблица символов.
4.
/i>Ошибки. На первом проходе выявляются не все ошибки, а только те,
которые связаны с выполнением задачи 1-го прохода. Сообщение об ошибке
включает в себя: код ошибки, диагностический текст, номер и текст оператора
программы, в котором обнаружена ошибка.
Некоторые структуры данных 1го прохода
Таблица команд содержит одну строку для каждой мнемоники машинной
команды. Ниже приведен пример структуры такой таблицы для простого
случая, когда мнемоника однозначно определяет формат и длину команды.
Обработка команд происходит по всего нескольким алгоритмам, зависящим от
формата команды, поэтому в данном случае все параметры обработки могут
быть представлены в виде данных, содержащихся в таблице.
Таблица директив содержит одну строку для каждой директивы Обработка
каждой директивы происходит по индивидуальному алгоритму, поэтому
параметры обработки нельзя представить в виде данных единого для всех
директив формата. Для каждой директивы в таблице хранится только
идентификация (имя или адрес, или номер) процедуры Ассемблера,
выполняющей эту обработку. Некоторые директивы обрабатываются только на
1-ом проходе, некоторые - только на 2-ом, для некоторых обработка
распределяется между двумя проходами.
Таблица символов является основным результатом 1-го прохода Ассемблера.
Каждое имя, определенное в программе, должно быть записано в таблице
символов. Для каждого имени в таблице хранится его значение. , размер
объекта,
связанного
с
этим
именем
и
признак
перемещаемости/неперемещаемости. Значением имени является число, в
большинстве случаев интерпретируемое как адрес, поэтому разрядность
значения равна разрядности адреса. Перемещаемость рассматривается в
разделе, посвященном Загрузчикам, здесь укажем только, что значение
перемещаемого имени должно изменяться при загрузке программы в память.
Имена, относящиеся к командам или к памяти, выделяемой директивами DD,
BSS, как правило, являются перемещаемыми (относительными), имена,
значения которых определяются директивой EQU (см. ниже), являются
неперемещаемыми (абсолютными).
Таблица литералов содержит запись для каждого употребленного в модуле
литерала. Для каждого литерала в таблице содержится его символьное
обозначение, длина и ссылка на значение. Литерал представляет собой
константу, записанную в памяти. Обращение к литералам производится так же,
как и к именам ячеек программы. По мере обнаружения в программе литералов
Ассемблер заносит их данные в т.наз. литеральный пул. Значение,
записываемое в таблицу литералов является смещением литерала в литеральном
пуле. После окончания 1-го прохода Ассемблер размещает литеральный пул в
конце программы (т.е., назначает ему адрес, соответствующий последнему
значению счетчик адреса) и корректирует значения в таблице литералов,
заменяя их смещениями относительно начала программы. После выполнения
этой корректировки таблица литералов может быть совмещена с таблицей
символов.
О структуре таблиц Ассемблера
Структура таблиц Ассемблера выбирается таким образом, чтобы обеспечить
максимальную скорость поиска в них.
Таблицы команд и директив являются постоянной базой данных. Они
заполняются один раз - при разработке Ассемблера, а затем остаются
неизменными. Эти таблицы целесоообразно строить как таблицы прямого
доступа с функцией хеширования, осуществляющей преобразование мнемоники
в адрес записи в таблице. Имеет смысл постараться и подобрать функцию
хеширования такой, чтобы в таблице не было коллизий. Поскольку заполнение
таблицы происходит один раз, а доступ к ней производится многократно, эти
затраты окупаются.
Таблица символов формируется динамически - в процессе работы 1-го прохода.
Поиск же в этой таблице осуществляется как в 1-ом проходе (перед занесением
в таблицу нового имени, проверяется, нет ли его уже в таблице). Построение
этой таблицы как таблицы прямого доступа не очень целесообразно, так как
неизбежно возникновение коллизий. Поэтому поиск в таблице символов может
быть дихотомическим, но для этого таблица должна быть упорядочена.
Поскольку новые имена добавляются в таблицу "по одному", и после каждого
добавления
упорядоченность
таблицы
должна
восстанавливаться,
целесообразно применять алгоритму сортировки, чувствительные к исходной
упорядоченности данных.
Эти же соображения относятся и к другим таблицам, формируемым
Ассемблером в процессе работы. При больших размерах таблиц и размещении
их на внешней памяти могут применяться и более сложные (но и более
эффективные) методы их организации, например - B+-деревья.
Двухпроходный Ассемблер 2й проход
Обычно 2-й проход Ассемблера читает исходный модуль с самого начала и
отчасти повторяет действия 1-го прохода (лексический разбор, распознавание
команд и директив, подсчет адресов). Это, однако, скорее дань традиции - с тех
времен, когда в вычислительных системах ощущалась нехватка (или даже
полное отсутствие) внешней памяти. В те далекие времена колода перфокарт
или рулон перфоленты, содержащие текст модуля, вставлялись в устройство
ввода повторно. В системах с эволюционным развитием сохраняются
перфокарты и перфоленты (виртуальные), так что схема работы Ассемблера - та
же. В новых системах Ассемблер может создавать промежуточный файл результат 1-го прохода, который является входом для 2-го прохода. Каждому
оператору исходного модуля соответствует одна запись промежуточного файла,
и формат этой записи приблизительно такой:
Текст исходного оператора нужен только для печати листинга, Ассемблер на 2ом проходе использует только первые 4 поля записи. Первое поле позволяет
исключить строки, целиком состоящие из комментария. Второе поле позволяет
избежать подсчета адресов, третье - поиска мнемоники в таблицах. Основная
работа 2-го прохода состоит в разборе поля операндов и генерации объектного
кода.
Некоторые общие соображения, касающиеся этой работы. Объектный код
команды состоит из поля кода операции и одного или нескольких полей
операндов. Код операции, как правило, имеет размер 1 байт, количество,
формат и семантика полей операндом определяется для каждого типа команд
данной аппаратной платформы. В общем случае операндом команды может
быть:
 регистр;
 непосредственный операнд;
 адресное выражение
Виды адресных выражений зависят от способов адресации вычислительной
системы, некоторые (возможно, наиболее типовые) способы адресации:
•
абсолютный адрес;
•
[базовый регистр]+смещение (здесь и далее квадратные скобки означают)
"содержимое того, что взято в скобки);
•
[базовый регистр]+[индексный регистр]+смещение;
•
имя+смещение;
•
литерал.
Адресное выражение может содержать арифметические операции, соображения,
касающиеся арифметики в этом случае - те же, что и в адресной арифметике
языка C.
Имена в адресных выражениях должны заменяться на значения. Замена
абсолютных имен (определенных в директиве EQU) очень проста - значение
имени из таблицы символов просто подставляется вместо имени.
Перемещаемые имена (метки и имена переменных) превращаются Ассемблером
в адресное выражение вида [базовый регистр]+смещение. В таблице символов
значения этих имен определены как смещение соответствующих ячеек памяти
относительно начала программы. При трансляции имен необходимо, чтобы:
•
Ассемблер "знал", какой регистр он должен использовать в качестве
базового;
•
Ассемблер "знал", какое значение содержится в базовом регистре;
•
в базовом регистре действительно содержалось это значение.
Первые два требования обеспечиваются директивами, третье - машинными
командами. Ответственность за то, чтобы при обеспечении этих требований
директивы согласовывались с командами, лежит на программисте. Эти
требования по-разному реализуются в разных вычислительных системах.
Приведем два примера.
В Intel Ассемблер использует в качестве базовых сегментные регистры (DS при
трансляции имен переменных, CS при трансляции меток). Для простой
программы, состоящей из одной секции, Загрузчик перед выполнением заносит
во все сегментные регистры сегментный адрес начала программы и Ассемблер
считает все смещения относительно него. Сложная программа может состоять
из нескольких секций, и в сегментном регистре может содержаться адрес той
или иной секции, причем содержимое сегментного регистра может меняться в
ходе выполнения программы. Загрузка в сегментный регистр адреса секции
выполняется
машинными
командами:
MOV
AX,секция
MOVсегментный_регистр,AX
Для того, чтобы Ассемблер знал, что адрес секции находится в
сегментном_регистре,
применяется
директива:
ASSUME
сегментный_регистр:секция
Далее при трансляции имен Ассемблер превращает имена в адресные
выражения вида [сегментный_регистр]+смещение в секции
Отмена использования сегментного регистра задается директивой: ASSUME
сегментный_регистр:NOTHING
Обратим внимание на то, что при трансляции команды MOV AX,секция в поле
операнда заносится относительный адрес секции, при выполнении же здесь
должен быть ее абсолютный адрес. Поэтому поля операндов такого типа
должны быть модифицированы Загрузчиком после размещения программы в
оперативной памяти.
Более гибкая система базовой адресации применяется в S/360, S/370, S/390. В
качестве базового может быть использован любой регистр общего назначения.
Директива: USING относительный_адрес,регистр сообщает Ассемблеру, что он
может использовать регистр в качестве базового, и в регистре содержится адрес
- 1-й операнд. Чаще всего относительный_адрес кодируется как * (обозначение
текущего значения счетчика адреса), это означает, что в регистре содержится
адрес первой команды, следующей за директивой USING. Занесение адреса в
базовый регистр выполняется машинной командой BALR. Обычный контекст
определения базового регистра: BALR регистр,0 USING *,регистр С такими
операндами команда BALR заносит в регистр адрес следующей за собой
команды.
Зная смещение именованной ячейки относительно начала программы и
смещение относительно начала же программы содержимого базового регистра,
Ассемблер легко может вычислить смещение именованной ячейки
относительно содержимого базового регистра.
В отличие от предыдущего примера, в этом случае не требуется модификации
при загрузке, так как команда BALR заносит в регистр абсолютный адрес.
Директива DROP регистр отменяет использование регистра в качестве базового.
В качестве базовых могут быть назначены несколько регистров, Ассемблер сам
выбирает, какой из них использовать в каждом случае.
Выше мы говорили, что Ассемблер "знает" базовый регистр и его содержимое.
Это "знание хранится в таблице базовых регистров. Обычно таблица содержит
строки для всех регистров, которые могут быть базовыми и признак,
используется ли регистр в таком качестве. Формат строки таблицы:
Алгоритм выполнения 2-го прохода представлен на рисунке. Мы исходили из
того, что 2-й проход использует промежуточный файл, сформированный 1-ым
проходом. Если же 2-й проход использует исходный модуль, то алгоритм
должен быть расширен лексическим разбором оператора, распознаванием
мнемоники и подсчетом адресов - так же, как и в 1-ом проходе.
1. Начало 2-го прохода ассемблирования.
2. Начальные установки:
3. создание пустой таблицы базовых регистров;
4. открытие промежуточного файла исходного модуля;
5. установка в FASLE признака окончания
6. Признак окончания TRUE?
7. Считывание следующей записи промежуточного файла.
8. Если запись промежуточного файла описывает комментарий, переход на печать
строки листинга
9. Выясняется, содержит оператор команду или директиву
10. Если оператор содержит команду, формируется байт кода операции (код
выбирается из таблицы команд) в объектном коде.
11. Выделение следующего элемента из списка операндов с удалением его из
списка и с проверкой, не обнаружен ли при выделении конец списка операндов?
12. Если конец не обнаружен, обрабатывается выделенный операнд. Проверяется,
не превысило ли число операндов требуемого для данного типа команды
(выбирается из таблицы команд)
13. Если число операндов превышает требуемое - формирование сообщения об
ошибке
14. Если число операндов правильное, распознается и проверяется тип операнда.
15. Если тип операнда не распознан или недопустим для данной команды формирование сообщения об ошибке.
16. Есть ли в команде имя?
17. Если в команде есть имя, оно ищется в таблице символов.
18. Если имя в таблице символов не найдено - формирование сообщения об
ошибке.
19. Если найдено имя в таблице символов, оно переводится в "база-смещение"
20. Если имени в команде нет, выполняется разбор и интерпретация операнда с
проверкой правильности его кодирования.
21. Если обнаружены ошибки в кодировании операнда - формирование сообщения
об ошибке.
22. Формируется код поля операнда и заносится в объектный код команды и
обрабатывается следующий элемент списка операндов.
23. Если обнаружен конец списка операндов, проверяется, не меньше ли число
операндов требуемого для данного типа команды. Если число операндов
соответствует требуемого, управление переходит на вывод объектного кода.
24. Если число операндов меньше требуемого - формирование сообщения об
ошибке
25. Если обрабатываемый оператор является директивой, алгоритм разветвляется, в
зависимости от того, какая это директива. При обработке любой директивы
производится разбор и анализ ее операндов и (не показано на схеме алгоритма)
возможно формирование сообщения об ошибке.
26. Обработка директивы типа DD включает в себя:
27. выделение элементов списка операндов;
28. для каждого элемента - распознавание типа и значения константы;
29. генерация объектного кода константы;
30. обработка возможных коэффициентов повторения.
31. Обработка директивы типа BSS может вестись точно так же, как и DD за
исключением того, что вместо кода константы генерируются некоторые
"пустые" коды. Однако, эти коды не нужны в объектном модуле, они могут не
генерироваться, в этом случае должны предприниматься некоторые действия,
формирующие "разрыв" в объектных кодах.
32. Обработка директивы типа USING (ASSUME) включает в себя занесение в
соответствующую строку таблицы базовых регистров значения операнда-адреса
и установку для данного регистра признака использования.
33. Обработка директивы типа USING (ASSUME) включает в себя занесение в
соответствующую строку таблицы базовых регистров значения операнда-адреса
и установку для данного регистра признака использования.
34. Обработка директивы END устанавливает признак окончания в TRUE. При
обработке этой директивы в объектный модуль также может заносится
стартовый адрес программы - параметр директивы.
35. Обработка прочих директив ведется по своим алгоритмам.
36. После окончания обработки команды или директивы сформированный
объектный код выводится в файл объектного модуля.
37. Печать строки листинга. На эту точку также управление передается при
выявлении ошибок. При наличии ошибки сообщение об ошибке печатается
после строки листинга. Управление затем передается на считывание следующей
записи промежуточного файла.
38. После того, как установлен признак окончания работы формируются и
выводятся в объектный модуль коды литерального пула, таблицы связываний и
перемещений.
39. Закрываются файлы, освобождается выделенная память.
40. Работа Ассемблера завершается
При рассмотрении алгоритма мы принимали во внимание только генерацию
объектных кодов, соответствующих командам и константам. Мы не
рассматривали то, какова общая структура объектного модуля.
Лекция 4
В данной части курса рассматриваются основы программирования на
языке ассемблера для архитектуры Win32.
Все процессы в машине на самом низком, аппаратном уровне приводятся в
действие только командами (инструкциями) машинного языка. Язык ассемблера
– это символическое представление машинного языка. Ассемблер позволяет
писать короткие и быстрые программы. Однако этот процесс чрезвычайно
трудоёмкий. Для написания максимально эффективной программы необходимо
хорошее знание особенностей команд языка ассемблера, внимание и
аккуратность. Поэтому реально на языке ассемблера пишутся в основном
программы, которые должны обеспечить эффективную работу с аппаратной
частью. Также на языке ассемблера пишутся критичные по времени выполнения
или расходованию памяти участки программы. Впоследствии они оформляются
в виде подпрограмм и совмещаются с кодом на языке высокого уровня.
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
3
0
4
AF
5
0
6
ZF
Zero Flag
Флаг нуля
Состояние
7
SF
Sign Flag
Флаг знака
Состояние
8
TF
Trap Flag
Флаг трассировки
Системный
9
IF
Interrupt Enable
Flag
Флаг разрешения
прерываний
Системный
Зарезервирован
Parity Flag
Флаг чётности
Состояние
Зарезервирован
Auxiliary Carry
Flag
Вспомогательный
флаг переноса
Состояние
Зарезервирован
10
DF
Direction Flag
Флаг направления
Управляющий
11
OF
Overflow Flag
Флаг переполнения
Состояние
IOPL
I/O Privilege
Level
Уровень приоритета
ввода-вывода
Системный
14
NT
Nested Task
Флаг вложенности
задач
Системный
15
0
12
13
Зарезервирован
EFLAGS
16
RF
Resume Flag
Флаг возобновления
Системный
17
VM
Virtual-8086
Mode
Режим виртуального
процессора 8086
Системный
18
AC
Alignment Check
Проверка
выравнивания
Системный
VIF
Virtual Interrupt
Flag
Виртуальный флаг
разрешения
прерывания
Системный
20
VIP
Virtual Interrupt
Pending
Ожидающее
виртуальное
прерывание
Системный
21
ID
ID Flag
Проверка на
доступность
инструкции CPUID
Системный
19
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. Директива эквивалентности
Директива эквивалентности позволяет описывать константы:
<имя> EQU <операнд>
Все вхождения имени заменяются операндом. Операндом может быть
константное выражение, строка, другое имя.
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 <операнд1>, <операнд2>
По команде MOV значение второго операнда записывается в первый
операнд. Операнды должны иметь одинаковый размер. Команда не меняет
флаги.
mov eax, ebx
; Пересылаем значение регистра EBX в регистр EAX
mov eax, 0ffffh
; Записываем в регистр EAX шестнадцатеричное значение ffff
mov x, 0
; Записываем в переменную x значение 0
mov eax, x
; Переслать значение из одной ячейки памяти в другую нельзя.
mov y, eax
; Но можно использовать две команды MOV.
На самом деле процессор имеет много команд пересылки – код команды
зависит от того, куда и откуда пересылаются данные. Но компилятор языка
ассемблера сам выбирает нужный код в зависимости от операндов, так что, с
точки зрения программиста, команда пересылки только одна.
Для перестановки двух величин используется команда обмена:
XCHG <операнд1>, <операнд2>
Каждый из операндов может быть регистром или ячейкой памяти. Однако
переставить содержимое двух регистров можно, а двух ячеек памяти – нет.
Операнды должны иметь одинаковый размер. Команда не меняет флаги.
3.2. Оператор указания типа
Как было сказано, операнды команды MOV должны иметь одинаковый
размер. В некоторых случаях компилятор может определить размер операнда.
Например, регистр EAX имеет размер 32 бита, а регистр DX – 16 бит. Размер
переменной определяется по директиве, указанной в её объявлении. Если
можно определить размер только одного операнда, то размер второго операнда
подгоняется под размер первого, если это возможно. Если же можно определить
размеры обоих операндов, то они должны совпадать.
x db ?
mov x, 0
; 0 может иметь любой размер, в данном случае берётся 1 байт
mov eax, 0
; 0 может иметь любой размер, в данном случае берётся 4 байта
; Ошибка – попытка записать 2-байтное число в 1-байтный
mov al, 1000h
регистр
mov eax, cx
; Ошибка – размеры операндов не совпадают
Однако не всегда бывает возможно определить размер пересылаемой
величины по операндам команды MOV. Например, если один из операндов
является ячейкой памяти, адрес которой записан в регистре, то по этому адресу
можно записать и 1 байт, и 2 байта, и 4 байта. Если второй операнд является
регистром, то размер пересылаемых данных определяется по размеру регистра.
Если же второй операнд является константой, то размер пересылаемых данных
определить нельзя, и компилятор фиксирует ошибку. Для того чтобы избежать
этой ошибки, надо явно указать размер пересылаемых данных. Для этого
используется оператор PTR:
<тип> PTR <выражение>
В качестве типа используется BYTE, WORD или DWORD.
mov [ebx], 0
; Ошибка, т.к. 0 может иметь любой размер
mov byte ptr [ebx], 0
; Пересылаем 1 байт
mov dword ptr [ebx], 0
; Пересылаем 4 байта
3.3. Команды сложения и вычитания
Команды сложения и вычитания реализуют хорошо всем известные
арифметические операции. Единственное, что нужно учитывать при
использовании этих команд – особенности сложения и вычитания, связанные с
представлением чисел в памяти компьютера.
ADD <операнд1>, <операнд2>
SUB <операнд1>, <операнд2>
Команда 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 свой
операнд.
INC <операнд>
DEC <операнд>
Операндом может быть регистр или ячейка памяти любого размера.
Команды меняют флаги AF, OF, PF, SF и ZF. Команды инкремента и декремента
выгодны тем, что они занимают меньше места, чем соответствующие команды
сложения и вычитания.
inc eax
К арифметическим операциям можно также отнести команду изменения
знака:
NEG <операнд>
Операндом может быть регистр или ячейка памяти любого размера.
Команда 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 <операнд>
Операнд, указываемый в команде, – это один из сомножителей. Он может
быть регистром или ячейкой памяти, но не может быть непосредственным
операндом.
Местонахождение второго сомножителя и результата фиксировано, и в
команде явно не указывается. Если операнд команды 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 <операнд1>, <операнд2>, <непосредственный операнд>
IMUL <операнд1>, <операнд2>
Команда знакового умножения имеет несколько вариантов. Первый
соответствует команде 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
idiv bl
; AL = e7h = -25, AH = 02h = 2
mov ax, -127
mov bl, 5
idiv bl
; AL = e7h = -25, AH = feh = -2
mov ax, -127
mov bl, -5
idiv bl
; AL = 19h = 25, AH = feh = -2
;x=a*b+c
mov eax, a
imul b
add eax, c
; Операнды команды сложения вычисляются слева направо
mov x, eax
;x=a+b*c
mov eax, b
imul c
add eax, a
; Операнды команды сложения вычисляются справа налево
mov x, eax
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 <операнд1>, <операнд2>
заполняются знаковым битом
; Знаковое расширение – старшие биты
MOVZX <операнд1>, <операнд2>
заполняются нулём
; Беззнаковое расширение – старшие биты
Операнд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
DX:AX
; Умножаем AX на BX. Но! – результат размещается в
; Второй вариант
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
imul ebx
; 429496730 = 4294967296 / 10
; EDX = x / 10. Выполняется в ≈5 раз быстрее, чем деление
Чем обусловлено получение такого результата? Всегда ли будет работать
этот механизм?
4. Переходы и циклы
Для изменения порядка выполнения команд в языке ассемблера
используются команды условного и безусловного перехода, а также команды
управления циклом. Все эти команды не меняют флаги.
4.1. Безусловный переход
Команда безусловного перехода имеет следующий синтаксис:
JMP <операнд>
Операнд указывает адрес перехода. Существует два способа указания этого
адреса, соответственно различают прямой и косвенный переходы.
4.1.1. Прямой переход
Если в команде перехода указывается метка команды, на которую надо
перейти, то переход называется прямым.
jmp L
...
L: mov eax, x
Вообще, любой переход заключается в изменении адреса следующей
исполняемой команды, т.е. в изменении значения регистра EIP. Казалось бы, в
команде перехода должен задаваться именно адрес перехода. Однако в команде
прямого перехода задаётся не абсолютный адрес, а разность между адресом
перехода и адресом команды перехода. Действие команды перехода
заключается в прибавлении этой величины к текущему значению регистра EIP2.
Операнд команды перехода рассматривается как поле со знаком, поэтому при
сложении его со значением регистра EIP значение в этом регистре может как
увеличиться, так и уменьшиться, т.е. возможен переход и вперёд, и назад.
Запись в команде перехода не абсолютного, а относительного адреса
перехода позволяет уменьшить размер команды перехода. Абсолютный адрес
должен быть 32-битным, а относительный может быть и 8-битным, и 16битным.
4.1.2. Косвенный переход
При косвенном переходе в команде перехода указывается не адрес
перехода, а регистр или ячейка памяти, где этот адрес находится. Содержимое
указанного регистра или ячейки памяти рассматривается как абсолютный адрес
перехода. Косвенные переходы используются в тех случаях, когда адрес
перехода становится известен только во время работы программы.
jmp ebx
4.2. Команды сравнения и условного перехода
Команды условного перехода осуществляют переход, который
выполняется только в случае истинности некоторого условия. Истинность
условия проверяется по значениям флагов. Поэтому обычно непосредственно
перед командой условного перехода ставится команда сравнения, которая
формирует значения флагов:
CMP <операнд1>, <операнд2>
Команда сравнения эквивалентна команде SUB за исключением того, что
вычисленная разность никуда не заносится. Назначение команды CMP–
установка и сброс флагов.
Что касается команд условного перехода, то их достаточно много, но все
они записываются единообразно:
Jxx <метка>
Все команды условного перехода можно разделить на три группы.
В первую группу входят команды, которые обычно ставятся после
команды сравнения. В их мнемокодах указывается тот результат сравнения, при
котором надо делать переход.
Мнемокод
Название
Условие перехода
после команды CMP
op1, op2
Значения флагов
JE
Переход если
равно
op1 = op2
ZF = 1
JNE
Переход если не
равно
op1 ≠ op2
ZF = 0
JL/JNGE
Переход если
меньше
op1 < op2
SF ≠ OF
JLE/JNG
Переход если
меньше или
равно
op1 ≤ op2
SF ≠ OF или ZF = 1
JG/JNLE
Переход если
op1 > op2
SF = OF и ZF = 0
Примечание
Для всех
чисел
Для чисел
со знаком
больше
JGE/JNL
Переход если
больше или
равно
op1 ≥ op2
SF = OF
JB/JNAE
Переход если
ниже
op1 < op2
CF = 1
JBE/JNA
Переход если
ниже или равно
op1 ≤ op2
CF = 1 или ZF = 1
JA/JNBE
Переход если
выше
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 – для
jge/jae L
беззнаковых
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: ...
L2:
;S
; 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 окажется ненулевое значение, и цикл будет
выполняться 232 раз.
Команда 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[ebx]
Однако принципиальное отличие состоит в том, в программе на языке
высокого уровня мы указываем индекс элемента массива, а компилятор
умножает его на размер элемента массива, получая смещение элемента
массива. В программе на языке ассемблера указывается именно смещение, т.е.
программист должен сам учитывать размер элемента массива. Компилятор же
языка ассемблера просто прибавляет смещение к указанному адресу.
Приведённые выше команды можно записать по-другому:
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 <регистр>, <ячейка памяти>
Команда не меняет флаги. В простейшем случае с помощью
команды 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
2. В регистре можно хранить номер элемента массива и умножать его на
размер элемента.
mov eax, 0
mov ecx, n
L: dec ecx
add eax, x[ecx * type x]
cmp ecx, 0
jne L
3. В регистре можно хранить адрес элемента массива. Адрес начала массива
можно записать в регистр с помощью команды 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
4. При необходимости можно в один регистр записать адрес начала массива,
а в другой – номер или смещение элемента массива.
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
удовлетворяющих условию
mov ecx, m
L1: mov edi, 0
; Начальное смещение строки
;
EBX
будет
содержать
количество
строк,
; Загружаем в ECX количество строк
; Начальное смещение элемента в строке
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
увеличение EBX
; Если сумма меньше или равна 0, то обходим
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. Логические команды
Операция отрицания меняет значение всех битов переменной на
противоположное. Операция имеет один операнд, который может быть
регистром или ячейкой памяти. Операция не меняет флаги.
NOT <операнд>
Операция поразрядное «и» выполняет логическое умножение всех пар бит
операндов.
AND <операнд1>, <операнд2>
Операция поразрядное «или» выполняет логическое сложение всех пар бит
операндов.
OR <операнд1>, <операнд2>
Операция поразрядное исключающее
модулю 2 всех пар бит операндов.
«или» выполняет
сложение
по
XOR <операнд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 (и только 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 <операнд1>, <операнд2>, <количество>
; Расширенный сдвиг влево
SHRD <операнд1>, <операнд2>, <количество>
; Расширенный сдвиг вправо
Команда 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
sal ax, 1
; AX = 68bch = 26812
; AX = d178h = -11912
; Знаковое положительное число перешло в отрицательное
mov ax, 32943
sal ax, 2
; AX = 80afh = 32943
; 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
shr ax, 1
; AX = a805h = 43013
; 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
; Деление на 2n (в данном примере n = 3)
mov eax, x
cdq
and edx, 111b
уменьшенный на 1
; Расширяем двойное слово до учетверённого
; Если EAX отрицателен, то EDX содержит делитель,
add eax, edx
значение
; Если EAX отрицателен, прибавляем полученное
sar eax, 3
; Если EAX был положителен, то EDX = 0, и предыдущие
две операции ничего не меняют
Если число беззнаковое или если мы знаем, что число положительное,
можно просто использовать сдвиг вправо, который выполняется примерно в 10
раз быстрее, чем деление. Если же для знакового числа не известно,
положительное оно или отрицательное, то придётся использовать
вышеприведённую последовательность команд, которая, однако, также
выполняется примерно в 5-7 раз быстрее, чем деление.
6.3.3. Получение остатка от деления
Для беззнаковых и положительных чисел остаток от деления на 2 n – это
последние n бит числа. Поэтому для получения остатка от деления на 2nнужно
выделить эти последние n бит с помощью операции AND.
mov eax, x
and eax, 111b
; EAX = EAX % 23
Для отрицательного делимого x и положительного делителя n (x % n) = -(-x
% n).
mov eax, x
neg eax
and eax, 1111b
; EAX = EAX % 24
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:
PUSH <операнд>
Операнд может быть регистром, ячейкой памяти или непосредственным
операндом. Размер операнда должен быть 2 или 4 байта. Операнд кладётся на
вершину стека, а значение регистра ESP уменьшается на размер операнда.
Для того чтобы взять данные из стека используется команда POP:
POP <операнд>
Операнд может быть регистром или ячейкой памяти. Размер операнда
должен быть 2 или 4 байта. В соответствии с размером операнда из вершины
стека берутся 2 или 4 байта и помещаются в указанный регистр или ячейку
памяти. Значение регистра ESP увеличивается на размер операнда.
Кроме этих основных команд существуют ещё команды, которые
позволяют сохранять в стеке и восстанавливать из стека содержимое всех
регистров общего назначения, и команды, которые позволяют сохранять в стеке
и восстанавливать из стека содержимое регистра флагов.
PUSHA
PUSHAD
Команда PUSHA сохраняет в стеке содержимое регистров AX, CX, DX,
BX, SP, BP, SI, DI. Команда PUSHAD сохраняет в стеке содержимое регистров
EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI. Для регистра (E)SP сохраняется
значение, которое было до того, как мы положили регистры в стек. После этого
значение регистра (E)SP изменяется как обычно.
POPA
POPAD
Эти команды противоположны предыдущим – они восстанавливают из
стека значения регистров (E)DI, (E)SI, (E)BP, (E)SP, (E)BX, (E)DX, (E)CX,
(E)AX. Содержимое регистра (E)SP не восстанавливается из стека, а изменяется
как обычно.
PUSHF
PUSHFD
Команда PUSHF сохраняет в стеке младшие 16 бит регистра флагов.
Команда PUSHFD сохраняет в стеке все 32 бита регистра флагов.
POPF
POPFD
Команда POPF восстанавливает из стека младшие 16 бит регистра флагов.
Команда POPFD восстанавливает из стека все 32 бита регистра флагов.
7.3. Синтаксис процедуры
Описание процедуры на языке ассемблера выглядит следующим образом:
<имя процедуры> PROC
<тело процедуры>
<имя процедуры> ENDP
Несмотря на то, что после имени процедуры не ставится двоеточие, это
имя является меткой, обозначающей первую команду процедуры.
В языке ассемблера имена и метки, описанные в процедуре, не
локализуются внутри неё, поэтому они должны быть уникальны.
Размещать процедуру в программе на языке ассемблера следует таким
образом, чтобы команды процедуры выполнялись не сами по себе, а только
тогда, когда происходит обращение к процедуре. Обычно процедуры
размещают либо в конце секции кода после вызова функции 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. Этот метод
самый быстрый, но он удобен только для процедур с небольшим количеством
параметров.
2. Параметры можно передавать в глобальных переменных.
Параметры процедуры можно записать в глобальные переменные, к
которым затем будет обращаться процедура. Однако этот метод является
неэффективным, и его использование может привести к тому, что рекурсия и
повторная входимость3 станут невозможными.
3. Параметры можно передавать в блоке параметров.
Блок параметров – это участок памяти, содержащий параметры и
располагающийся обычно в сегменте данных. Процедура получает адрес начала
этого блока при помощи любого метода передачи параметров (в регистре, в
переменной, в стеке, в коде или даже в другом блоке параметров).
4. Параметры можно передавать через стек.
Передача параметров через стек – наиболее распространённых способ.
Именно его используют языки высокого уровня, такие как С++ и Паскаль.
Параметры помещаются в стек непосредственно перед вызовом процедуры.
При внимательном анализе этого метода передачи параметров возникает
сразу два вопроса: кто должен удалять параметры из стека, процедура или
вызывающая её программа, и в каком порядке помещать параметры в стек. В
обоих случаях оказывается, что оба варианта имеют свои «за» и «против». Если
стек освобождает процедура, то код программы получается меньшим, а если за
освобождение стека от параметров отвечает вызывающая программа, то
становится возможным вызвать несколько функций с одними и теми же
параметрами просто последовательными командами CALL. Первый способ,
более строгий, используется при реализации процедур в языке Паскаль, а
второй, дающий больше возможностей для оптимизации, – в языке С++.
Основное соглашение о вызовах языка Паскаль предполагает, что
параметры кладутся в стек в прямом порядке. Соглашения о вызовах языка С++,
в том числе одно из основных соглашений о вызовах ОС Windows stdcall,
предполагают, что параметры помещаются в стек в обратном порядке. Это
делает возможной реализацию функций с переменным числом параметров (как,
например, printf). При этом первый параметр определяет число остальных
параметров.
push <параметрn>
...
push <параметр1>
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
5. Параметры можно передавать в потоке кода.
В этом необычном методе передаваемые процедуре данные размещаются
прямо в коде программы, сразу после команды 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
mov d, eax
; Освобождаем 8 байт стека
;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 L1
; Если EAX = 0, то обходим рекурсивную ветвь
dec eax
; Уменьшаем значение в регистре EAX на 1
push eax
рекурсивного вызова
call factorial
;
Кладём
в
стек
; Вызываем процедуру
параметр
для
следующего
add esp, 4
параметров
; Очищаем стек, т.к. процедура использует RET без
mul dword ptr [esp + 4]
; Умножаем EAX, хранящий
предыдущего вызова, на параметр текущего вызова процедуры
результат
; Возврат из процедуры (без параметров)
ret
L1: inc eax
; Если EAX был равен 0, записываем в EAX единицу
L2: ret
; Возврат из процедуры (без параметров)
factorial endp
8. Оптимизация программ, написанных на языке ассемблера
Наиболее популярным применением ассемблера обычно считается именно
оптимизация программ, то есть уменьшение времени выполнения программ по
сравнению с языками высокого уровня. Но если просто переписать текст,
например с языка С на ассемблер, переводя каждую команду наиболее
очевидным способом, часто оказывается, что процедура на языке С
выполняется быстрее. Вообще говоря, ассемблер, как и любой другой язык, сам
по себе не является панацеей от неэффективного программирования – чтобы
действительно оптимизировать программу, требуется не только знание команд
процессора, но и знание алгоритмов, навык оптимальных способов их
реализации и подробная информация об архитектуре процессора.
Проблему оптимизации принято делить на три основных уровня:
1. выбор наиболее оптимального алгоритма – высокоуровневая
оптимизация;
2. наиболее оптимальная реализация алгоритма – оптимизация среднего
уровня;
3. подсчёт тактов, тратящихся на выполнение каждой команды, и
оптимизация их порядка для конкретного процессора – низкоуровневая
оптимизация.
8.1. Высокоуровневая оптимизация
Выбор оптимального алгоритма для решения задачи всегда приводит к
лучшим результатам, чем любой другой вид оптимизации. Действительно, при
замене
пузырьковой
сортировки,
время
выполнения
которой
2
пропорционально n , на быструю сортировку, время выполнения которой
пропорционально n * log(n), вторая программа будет выполняться быстрее в
подавляющем большинстве случаев, как бы она ни была реализована. Поиск
лучшего алгоритма – универсальная стадия, и она относится не только к
ассемблеру, но и к любому языку программирования, поэтому будем считать,
что оптимальный алгоритм уже выбран.
8.2. Оптимизация среднего уровня
Реализация алгоритма на данном конкретном языке программирования –
самая ответственная стадия оптимизации. Именно здесь можно получить
выигрыш в скорости в десятки раз или сделать программу в десятки раз
медленнее, при серьёзных ошибках в реализации. Методы оптимизации сильно
зависят от конкретного реализуемого алгоритма, поэтому невозможно описать
правила на все случаи жизни, хотя, конечно, есть ряд общих приёмов, например,
хранение переменных, с которыми выполняется активная работа, в регистрах,
использование таблиц переходов вместо длинных последовательностей
проверок и условных переходов и т.п. Тем не менее, даже плохо реализованные
операции не вносят заметных замедлений в программу, если они не
повторяются в цикле. Практически можно говорить, что все проблемы
оптимизации на среднем уровне так или иначе связаны с циклами, и именно
поэтому мы рассмотрим основные правила, которые стоит иметь в виду при
реализации любого алгоритма, содержащего циклы.
8.2.1. Вычисление констант вне цикла
Самым очевидным и самым важным правилом при создании цикла на
любом языке программирования является вынос всех переменных, которые не
изменяются на протяжении цикла, за его пределы. В случае ассемблера имеет
смысл также по возможности разместить все переменные, которые будут
использоваться внутри цикла, в регистры, а старые значения нужных после
цикла регистров сохранить в стеке.
8.2.2. Перенос проверки условия в конец цикла
Циклы типа while или for, которые так часто применяются в языках
высокого уровня, оказываются менее эффективными по сравнению с циклами
типа until из-за того, что в них требуется лишняя команда перехода.
; for (i = start_i; i < n; i++) <тело цикла>
mov edi, start_i
; Начальное значение счётчика
mov esi, n
; Конечное значение счётчика
loop_start:
cmp edi, esi
je loop_end
; Пока EDI < ESI – выполнять
<тело цикла>
inc edi
jmp loop_start
loop_end:
; i = start_i; do { <тело цикла> } while (i < n);
mov edi, start_i
mov esi, n
loop_start:
<тело цикла>
inc edi
cmp edi, esi
jb loop_start
; Пока EDI < ESI – выполнять
Предположим, в цикле должен быть один шаг. Тогда в цикле с
предусловием будет выполнено сравнение, тело цикла, безусловный переход к
началу цикла, сравнение и переход за цикл. В цикле с постусловием будет
выполнено тело цикла, сравнение и нереализованный переход. Таким образом, в
цикле с предусловием выполняется одно лишнее сравнение и два
реализованных перехода (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
jnz loop_start
выходим из цикла
; Уменьшаем EDX на 1. Если EDX = 0, то ZF = 1
; Переход если ZF = 0. Когда EDX = 0, ZF = 1, поэтому
; Цикл от 10 до 0
mov edx, 10
loop_start:
<тело цикла>
dec edx
jns loop_start
выходим из цикла
; Уменьшаем EDX на 1. Если EDX = -1, то SF = 1
; Переход если 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 <число>
Число должно быть степенью двойки. Данные и команда, расположенные
после директивы 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
2. Ввод и вывод в консольном приложении. В программе используются
следующие функции 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
и 10
; Заголовок окна консоли. Заканчивается
; Приглашение для ввода. Символы с кодами 13
; обеспечивают перевод курсора на следующую
строку
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
push 0
количества выведенных символов,
; Зарезервированный параметр, в стек кладём 0
;
Указатель
на
переменную
для
записи
; в данном случае не нужен, поэтому в стек
кладём 0
push 10h
; Количество выводимых символов
push offset prompt
; Адрес выводимой строки
push hStdOut
; Идентификатор устройства вывода
call WriteConsole
; Вызываем функцию
; Вводим строку
push 0
push offset slength
количество введённых символов
; Зарезервированный параметр, в стек кладём 0
; Адрес переменной, куда будет записано
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
3. Процедура
ввода
целого
числа
в
16-ричной
системе
счисления. Процедура предназначена для использования в консольном
приложении и предполагает, что идентификатор устройства ввода был
получен основной программой и сохранён в переменной hStdIn.
InputNumber proc
push ebp
; Сохраняем в стеке значение регистра EBP
mov ebp, esp
вершины стека
; Заносим в регистр EBP текущее значение
sub esp, 16
содержать до 8 цифр.
; Резервируем 16 байт. Вводимая строка может
; 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
нулём
jz L2
L1: mov bl, [esi]
старших байта EBX
; Используем команду TEST для сравнения с
; Если ECX = 0, то завершаем работу процедуры
; Заносим в регистр BL текущий символ (три
; содержат 0, т.к. ранее была команда XOR EBX,
EBX)
lea edx, [ebx - '0']
; Заносим в регистр EDX разность между кодом
текущего символа и кодом символа '0'
cmp edx, 9
ja M1
сравнению
sub bl, '0'
jmp M3
текущую цифру
; Сравниваем значение в регистре EDX с 9
; Если выше, то переходим к следующему
; Иначе получаем число из кода символа
;
Переходим
к
действиям,
учитывающим
M1: lea edx, [ebx - 'a']
; Заносим в регистр EDX разность между кодом
текущего символа и кодом символа 'a'
cmp edx, 'f' - 'a'
ja M2
сравнению
sub bl, 'a' - 10d
jmp M3
текущую цифру
; Сравниваем значение в регистре EDX с 5
; Если выше, то переходим к следующему
; Иначе получаем число из кода символа
;
Переходим
к
действиям,
учитывающим
M2: lea edx, [ebx - 'A']
; Заносим в регистр EDX разность между кодом
текущего символа и кодом символа 'A'
cmp edx, 'F' - 'A'
ja L2
не определён,
; Сравниваем значение в регистре EDX с 5
; Если выше, то завершаем процедуру. Результат
; т.к. был введён некорректный символ
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
4. Процедура вывода числа в 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
EDX
; Копируем значение регистра EAX в регистр
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
push 0
push 0
push 10
push esi
push hStdOut
call WriteConsole
; Регистр ESI указывает на начало строки
pop esi
mov esp, ebp
; Освобождаем стек
pop ebp
; Восстанавливаем значение регистра EBP
ret 4
возвращаемся
; Удаляем из стека переданный параметр и
OutputNumber endp
5. Функция, находящая в одномерном массиве 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
L: push [esi]
call edi
в регистре EDI
; Обнуляем регистр EDX
; Кладём в стек элемент массива
; Вызываем функцию, адрес которой находится
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
6. Процедура,
проверяющая
сбалансированность
круглых
и
квадратных скобок в строке. Строка должна заканчиваться нулём. Для
проверки сбалансированности открывающие скобки будем класть в стек, а
при нахождении в строке закрывающей скобки будем извлекать из стека
последнюю положенную туда открывающую скобку и проверять, что она
соответствует закрывающей скобке. Будем считать, что скобок в тексте
меньше, чем других символов, поэтому после сравнения делаем переход
«если равно», считая, что это событие менее вероятно. При любом выходе
из процедуры нужно очистить стек. Поскольку мы не можем заранее
знать, сколько скобок будет туда положено и сколько извлечено,
восстановление значения регистра 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
результата
xor edx, edx
L1: mov dl, [ebx]
; Заносим в регистр EAX предварительное значение
; Обнуляем регистр EDX
; Заносим в регистр 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
В защищённом режиме программе выделяется один сегмент размером 4 Гб для кода и
один сегмент размером 4 Гб для данных (физически они обычно совпадают). Виртуальный
адрес состоит из 16-битного значения, хранящегося в сегментном регистре, и 32-битного
смещения. Однако преобразование виртуального адреса в физический осуществляется не
путём сложения, а по более сложной схеме. Сначала процессор преобразует виртуальный
адрес в линейный. При этом он обращается к таблицам дескрипторов, которые заранее
строятся операционной системой. На втором этапе по линейному адресу определяется
1
физический. В этом преобразовании участвует другой набор системных таблиц – таблицы
страничной трансляции, которые также составляются операционной системой. Оба набора
таблиц могут динамически меняться, обеспечивая максимальное использование оперативной
памяти.
В сегментные регистры записываются не адреса сегментов, а селекторы, которые
представляют собой номера ячеек специальной таблицы, содержащей дескрипторы сегментов
программ. Каждый дескриптор хранит все характеристики, необходимые для обслуживания
сегмента: базовый линейный адрес сегмента, границу сегмента (номер последнего байта), а
также атрибуты сегмента, определяющие его свойства. Процессор с помощью селектора
определяет индекс дескриптора адресуемого сегмента, извлекает из него базовый линейный
32-битный адрес сегмента и, сложив его с 32-битным смещением, получает линейный адрес
адресуемой ячейки памяти. Получив линейный адрес адресуемого байта, процессор с
помощью таблиц трансляции преобразует его в 32-битный физический адрес. Этот адрес
зависит от объёма оперативной памяти, установленной на компьютере.
В 32-битной модели Windows предоставляет всем запущенным приложениям один и тот
же селектор для сегмента кода и один и тот же селектор для сегмента данных. Базы обоих
сегментов равны 0, а границы – FFFFFFFF. Другими слова, каждому приложению как бы
предоставляется всё линейное пространство. Поскольку базовые линейные адреса сегментов
программы равны 0, виртуальные смещения, с которыми работают приложения, совпадают с
линейными адресами. Другими словами, плоское виртуальное адресное пространство
программы совпадает с плоским линейным адресным пространством. При этом все
приложения используют один и тот же диапазон линейных адресов. Для того чтобы при
одинаковых линейных адресах приложения занимали различные участки физической памяти
и не затирали друг друга, Windows при смене приложения изменяет таблицы страничной
трансляции, с помощью которых как раз и происходит преобразование линейных адресов в
физические.
Если говорить точнее, то относительный адрес перехода отсчитывается не от самой
команды перехода, а от следующей за ней команды. Дело в том, что выполнение любой
команды начинается с засылки в регистр EIP адреса следующей по порядку команды и только
затем выполняется собственно команда. Поэтому в команде перехода относительный адрес
будет прибавляться к значению регистра EIP, которое уже указывает на следующую команду,
а потому от этой следующей команды и приходится отсчитывать относительный адрес
перехода. Однако, в любом случае, программисту нет необходимости самому высчитывать
относительный адрес перехода, это делает компилятор языка ассемблера.
2
Компьютерная программа в целом или её отдельная процедура называется
реентерабельной (от англ. reentrant – повторно входимый), если она разработана таким
образом, что одна и та же копия инструкций программы в памяти может быть совместно
использована несколькими пользователями или процессами. При этом второй пользователь
может вызвать реентерабельный код до того, как с ним завершит работу первый пользователь
и это как минимум не должно привести к ошибке, а в лучшем случае не должно вызвать
потери вычислений (то есть не должно появиться необходимости выполнять уже
выполненные фрагменты кода).
3
Для обеспечения реентерабельности необходимо выполнение нескольких условий:



никакая часть вызываемого кода не должна модифицироваться;
вызываемая процедура не должна сохранять информацию между вызовами;
если процедура изменяет какие-либо данные, то они должны быть уникальными для
каждого пользователя;

процедура не должна возвращать указатели на объекты, общие для разных
пользователей.
В общем случае, для обеспечения реентерабельности необходимо, чтобы вызывающий
процесс или функция каждый раз передавал вызываемому процессу все необходимые
данные. Таким образом, функция, которая зависит только от своих параметров, не использует
глобальные и статические переменные и вызывает только реентерабельные функции, будет
реентерабельной. Если функция использует глобальные или статические переменные,
необходимо обеспечить, чтобы каждый пользователь хранил свою локальную копию этих
переменных.
Download