Содержание страницы

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

Регистры

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

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

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

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

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

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

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

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

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

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

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

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

Флаги состояния (биты 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.

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

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

Системные флаги и поле IOP

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

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

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

Процессор имеет 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 были добавлены позже. Существовало несколько моделей памяти, каждая из которых подразумевала выделение программе одного или нескольких сегментов кода и одного или нескольких сегментов данных: tinysmallmediumcompactlarge и huge. Для команд языка ассемблера существовали определённые соглашения: адреса перехода сегментировались по регистру CS, обращения к данным сегментировались по регистру DS, а обращения к стеку – по регистру SS. Если программе выделялось несколько сегментов для кода или данных, то приходилось менять значения в регистрах CS и DS для обращения к другому сегменту. Существовали так называемые «ближние» и «дальние» переходы. Если команда, на которую надо совершить переход, находилась в том же сегменте, то для перехода достаточно было изменить только значение регистра IP. Такой переход назывался ближним. Если же команда, на которую надо совершить переход, находилась в другом сегменте, то для перехода необходимо было изменить как значение регистра CS, так и значение регистра IP. Такой переход назывался дальним и осуществлялся дольше.

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

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

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

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

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

Пусть у нас есть функция 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. Поэтому каждая процедура обязательно должна заботиться об инициализации своих локальных данных.

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

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

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

Целые числа

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

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

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

Комментарии

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

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

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

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

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

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

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

Язык С, который создавался как высокоуровневая замена языку ассемблера, имеет гораздо менее жёсткую структуру типов. Все целочисленные типы совместимы, тип 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.

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

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

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

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

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

Команды

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Мнемокод Название Условие перехода после команды 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.

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

Мнемокод Условие перехода Мнемокод Условие перехода
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.

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

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

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

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

Команда LOOP

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

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

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

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

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

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

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

До начала цикла в регистр 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 в противном случае.

Массивы

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

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

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

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

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

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

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

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

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

Команда LEA

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Умножение

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

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

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

Деление

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

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

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

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

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

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

Для отрицательного делимого xи положительного делителя n

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Параметры процедуры можно записать в глобальные переменные, к которым затем будет обращаться процедура. Однако этот метод является неэффективным, и его использование может привести к тому, что рекурсия и повторная входимость3[[http:natalia.appmat.ru/c&c++/assembler.html#nota3|]] станут невозможными.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Примеры

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

Ввод и вывод в консольном приложении. В программе используются следующие функции Win32 API.

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

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

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

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

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

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

Примечания

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

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

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

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

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

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

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *