Воробьёвы

(-:

№28. FPU посвящается, а также наши "любимые" макросы

Уж не прошло и полгода со дня выхода (28)-го номера рассылки. Как обычно на наш адрес пришло немерянное количество матюгальников на предмет "почему умерла рассылка" и мы в очередной раз со всей ответственностью заявляем: "Хрен вам, не дождетесь! Только после вас!" ;).

Самое время вам почистить мышку, чтобы не растрачивать попусту нервные клетки, тягая вертикальный скролл ;)

ПРОЦЕДУРЫ И ПРОСТОЙ ХЭШ

#1. Для начала давайте вспомним, как мы обращались к функциям DOS'а. Брали толстый талмуд и смотрели на описание того или иного прерывания, в котором было четко и однозначно документировано, в каких именно регистрах мы должны расположить аргументы перед вызовом функции. Там же был документирован и регистр (регистры), через который функция возвращала результат. Значения же остальных регистров, кроме регистра-результата, после выполнения функции оставались неизменными. По такому же принципу мы старались конструировать и собственные процедуры, используя связку push/pop для восстановления первоначальных (до выполнения процедуры) значений регистров.

В win32 мы обращаемся к ресурсам операционной системы путем вызова функций API, которые гарантированно сохраняют регистры общего назначения ebp, esi, edi, ebx и все сегментные (на которые при разработке большинства applications, как уже говорилось ранее, можно смело не обращать внимания). Регистр eax, как правило, содержит возвращаемое значение, а состояние остальных регистров после возврата из функции API следует считать неопределенным . Это первое правило, которое мы должны знать, чтобы не наступать на грабли при разработке собственных процедур. (Если мы хакеры, то значения, остающиеся после вызова системных функций в "неопределённых" регистрах может иногда служить нам лазейкой, через которую можно получить доступ к недоступной иным образом информации.)

Правило второе - регистры esi, edi, ebp, ebx Windows использует для своих "внутренних" нужд и не ожидает, что мы будем изменять их значения в "главной" (которая вызывается windows) процедуре. Однако, не спешите пугаться. Это вовсе не означает, что нам запрещено их использовать - просто будьте бдительны и не забывайте восстанавливать их первоначальные состояния при помощи связки push/pop.

Правило третье - будьте осторожны, осторожны и еще раз осторожны с использованием ebp, если не хотите внести полный бардак в локальные переменные вашей программы. Кто внимательно изучил прошлую главу, тому не надо объяснять, почему это так ;).

Гут ;). Я ознакомил вас с правилами, теперь о главном. Параметры собственной процедуре мы будем передавать точно так, как и апишной функции - через стек, то есть invoke'om ;).

#2. Программа, которую мы сегодня напишем, будет вычислять простой хэш строки символов (в нашем случае функцией хеша будет являться сумма кодов символов). Зачем это нужно, я расскажу позже, а пока что давайте разберем исходник этой программы. Кстати говоря, я полагаю, что дзенствующий читатель уже достиг определенной степени просветления, поэтому буду приводить только рабочие части программы. Различные же ".386" и includelib'ы вы, уж будьте добры, дописывайте самостоятельно - уже не маленькие.

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

    ;=|SimpleHash|===========================================
    ;Функция   : вычисление простой хэш-суммы
    ;Параметры : указатель на строку и ее длина
    ;Результат : хэш-сумма
    ;Использует: -
    ;========================================================
    SimpleHash proc uses EBX ECX EDX pFrom:DWORD,
                                            nIterations:DWORD
      xor EAX,EAX            ;результат
      xor EDX,EDX            ;временная переменная
      mov ECX,nIterations
      mov EBX,pFrom
     @@Labl:
      mov DL, BYTE ptr [EBX]
      add EAX, EDX
      inc EBX
      loop @@Labl
      ret
    SimpleHash endp

Прежде всего обратите внимание на директиву proc, отмечающую начало процедуры. Справа от нее есть еще какие-то непонятные слова. Что же, давайте скомпилируем эту процедуру и посмотрим (используя дизассемблер IDA), в каком виде эта конструкция предстает пред светлые очи процессора.

    :004010B5 sub_0_4010B5     proc near
    :004010B5
    :004010B5 arg_0  = dword ptr  8
    :004010B5 arg_4  = dword ptr  0Ch
    :004010B5
    :004010B5   push    ebp
    :004010B6   mov     ebp, esp
    :004010B8   push    ebx
    :004010B9   push    ecx
    :004010BA   push    edx
    :004010BB   xor     eax, eax
    :004010BD   xor     edx, edx
    :004010BF   mov     ecx, [ebp+arg_4]
    :004010C2   mov     ebx, [ebp+arg_0]
    :004010C5
    :004010C5 loc_0_4010C5:
    :004010C5   mov     dl, [ebx]
    :004010C7   add     eax, edx
    :004010C9   inc     ebx
    :004010CA   loop    loc_0_4010C5
    :004010CC   pop     edx
    :004010CD   pop     ecx
    :004010CE   pop     ebx
    :004010CF   leave
    :004010D0   retn    8
    :004010D0 sub_0_4010B5      endp

Как нетрудно догадаться, "uses EBX ECX EDX" ассемблируется в хорошо знакомую нам простыню push'ей и pop'ов, а "pFrom:DWORD, nIterations:DWORD" - параметры, которые передаются процедуре - в тот же код, что и директива LOCAL (с разницей только в знаке индекса). Любители "нюансов" могут также отметить, что сначала идет создание фрейма, и только потом - сохранение используемых регистров.

Завершает процедуру команда leave, которая убирает фрейм, созданный парочкой push ebp / mov ebp,esp. В результате ее выполнения содержимое ebp копируется в esp (восстанавливается значение esp, которое было до вызова процедуры), и выталкивает из стека содержимое ebp.

Есть еще какой-то навороченный вариант инструкции ret - retn 8. Что делает ret, мы знаем: восстанавливает из стека содержимое eip, загнанное туда call'ом при вызове процедуры (другими словами, выход из подпрограммы). А что означает ее операнд, цифра 8?

Открываем толстый талмуд, и выясняем, что если команда ret имеет операнд, то при этом еще и увеличивается значение регистра esp на число 2*операнд. Зачем, спрашивается?

А все очень просто. При передаче параметров апишной функции мы, следуя соглашению stdcall, не занимались уборкой аргументов из стека после выполнения функции - это делала "где-то там" сама функция. А вот при передаче параметров собственной процедуре... Хм..., пожалуй, при ручном кодировании нам действительно пришлось бы высчитывать значение этого операнда, чтобы убрать параметры из стека. Но, к счастью, мы используем компилятор, и лишены сомнительного удовольствия проделывать эту работу самостоятельно (вот оно, великое достоинство языков высокого уровня перед менее высокоуровневыми языками). Достаточно написать ret, а что эта команда должна делать с esp, компилятор решит самостоятельно ;). Да-да, ret здесь - это не инструкция процессора, а директива компилятору "завершение процедуры", по которой он выполняет кучу работы - и регистры восстановит, и фрейм уберёт, и инструкцию возврата вставит ;) И всё-таки не совсем корректно! Дело в том, что действие компилятора зависит от USES или PROTO, тогда об этом стоит написать ЯВНО!

Для тех, кто не понял. Значение esp увеличивается на такую величину (кратную 4 для 32-битного режима - см. прошлую главу), чтобы уничтожить результат выполнения энного числа push'ей. Например, если мы передаем параметр размером в двойное слово, то стек выравниваем командой retn 4. Если размерность байт либо слово - то все равно retn 4, так как в стек помещаются только двойные слова, а аргументом инструкции (инструкции, а не директивы!) ret выступает размер области аргументов в байтах. Если два двойных слова, то retn 8, если три - retn 12, четыре - retn 16 и так далее.

Что касается самого алгоритма работы процедуры SimpleHash, то... если он не понятен вам с первого взляда, вряд ли стоит читать дальше - проштудируйте для начала предыдущие части моего низкоуровневого эпоса ;).

#3. Вторая процедура называется DWordTo8Char, и все, что она делает - это конвертирует 32-битное число в строку из восьми 16-ричных (в смысле hex) знаков.

    ;=|DWordTo8Char|=========================================
    ;Функция   : конвертация 32-битного числа в строку из 8
    ;            16-ричных знаков
    ;Параметры : 32-битное значение и указатель на буфер для
    ;            результата
    ;Результат : строка в буфере
    ;Использует: -
    ;========================================================
    DWordTo8Char proc uses ECX EBX nValue:DWORD,
                                                pBuffer:DWORD
      mov EBX,pBuffer
      mov ECX,32-4
    @@Loop:
      mov EAX,nValue
      shr EAX,CL
      and AL,0Fh
      cmp AL,10
      sbb AL,69h
      das
      mov [EBX],AL
      inc EBX
      sub ECX,4
      jae @@Loop
      mov EAX,pBuffer
      ret
    DWordTo8Char endp

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

#4. Хорошо, продолжим писать нашу программу. Интерфейс ее будет следующим: печатается приглашение что-нибудь ввести, например, "Type something >", а вычисленная 32-разрядная хэш-сумма выводится после мессаги "Hash > " строкой ниже.

Вот секция данных:

    SConsoleTitle      DB 'Simple Hash Calculator',0
    SMsgOnInput        DB 'Type something > '
    NSizeOfMsgOnInput  = $-SMsgOnInput
    SMsgOnOutput       DB 'Hash > '
    NSizeOfMsgOnOutput = $-SMsgOnOutput
    SHashSummaStr      DB 8 dup (0)
    NHashSummaStr      = $-SHashSummaStr
    NInputBufferSize   = 128

А вот код "главной" процедуры, решающий поставленную задачу:

    LOCAL sInputBuffer[NInputBufferSize] :BYTE     ;(1)
    LOCAL hOutPut                        :DWORD
    LOCAL hInput                         :DWORD
    LOCAL nRead                          :DWORD
    LOCAL nWritten                        :DWORD

    invoke SetConsoleTitle, ADDR SConsoleTitle     ;(2)
    invoke GetStdHandle,STD_OUTPUT_HANDLE
    mov hOutPut, EAX
    invoke GetStdHandle,STD_INPUT_HANDLE
    mov hInput, EAX

    invoke WriteConsole,hOutPut,ADDR SMsgOnInput,  ;(3)
                        NSizeOfMsgOnInput,ADDR nWritten,NULL
    invoke ReadConsole,hInput,ADDR sInputBuffer,
                            NInputBufferSize,ADDR nRead,NULL
    mov EDX, nRead
    sub EDX,2
    invoke SimpleHash, ADDR sInputBuffer, EDX
    invoke DWordTo8Char, EAX, ADDR SHashSummaStr
    invoke WriteConsole,hOutPut,addr SMsgOnOutput,
      NSizeOfMsgOnOutput + NHashSummaStr, addr nWritten,NULL

    invoke Sleep, 2000                              ;(4)
    invoke ExitProcess,0

Для начала проанализируем секцию данных.

SConsoleTitle - это 0-терминированная строка, которую мы выведем в заголовок нашего консольного окошка при помощи функции SetConsoleTitle.

SMsgOnInput и SMsgOnOutput - это мессаги, которые будут предшествовать вводу символов и выводу результата соответственно. Печатать их мы будем при помощи функции WriteConsole, которая, помимо прочих параметров, требует также указания количества символов, которые необходимо напечатать. В прошлой нашей программе мы считали их "ручками", и писали соответствующее число при вызове функции. Сознаюсь, это не только неправильный, но и небезопасный стиль программирования. Представьте, что вы печатаете эту строку раз десять в разных частях программы и вдруг захотели ее изменить - посчитайте сами, какой путь проделает ваша мышка, тягая вертикальный скролл, и сколько досадных ляпов вы при этом можете допустить :(. Выход - завести константу, в которой "держать" число символов в строчке, а в каждой функции, эту строчку печатающей, вместо числа указывать константу. Это во-первых.

А во-вторых - зачем считать число символов "ручками", если эту муторную задачу можно переложить на плечи компилятора? Посмотрите внимательно - сразу же за строчкой, декларирующей глобальную переменную SMsgOnInput, я написал загадочную конструкцию NSizeOfMsgOnInput = $-SMsgOnInput. Значок "$" переводится как "адрес здесь и сейчас", $-SMsgOnInput - "адрес здесь и сейчас" минус адрес SMsgOnInput. В результате мы получаем длину строки, и этим значением инициализируем константу NSizeOfMsgOnInput. Естественно, в машинный код эта инструкция не ассемблируется.

Константа NInputBufferSize заведена из подобных же соображений - в ней мы прописываем размер буфера для ввода символов для последующего декларирования локальной переменной строчкой LOCAL sInputBuffer[NInputBufferSize] :BYTE, т.е. выносим за пределы кода своего рода "настройку".

SHashSummaStr и NHashSummaStr - это восьмибайтный буфер для вывода результата и его размер.

#5. Теперь проанализируем код.

Бряк 1 - заводим локальные переменные sInputBuffer - буфер для введенных символов, hOutPut - хэндл вывода, hInput - хэндл ввода, nRead - число прочитанных символов, nWritten - число напечатанных символов.

Бряк 2 - некоторые подготовительные операции. Установка титла окна, получение хэндлов ввода и вывода и сохранение их в локальных переменных.

Бряк 3 - собственно, рабочая часть "главной" процедуры. Печатаем приглашение "Type something > " при помощи функции WriteConsole. Обращаем внимание на то, что длина строки находится в константе NSizeOfMsgOnInput, и вспоминаем то, чем я вас грузил в #4 ;). Ждем ввода с клавиатуры функцией ReadConsolе. Снова обращаем внимание на константу - размер буфера NSizeOfMsgOnInput, она же - максимальное число символов, которое мы можем ввести. Реально же введенное сохраняется в переменной nRead...

Насчет "реально введенного" - тут есть один нюанс. Обратите внимание на строки

    mov EDX, nRead
    sub EDX,2

Дело в том, что, когда мы, желая закончить ввод строки, жмем на Enter, то в буфер ввода добавляются два непечатных символа - 13 и 10 (CR/LF - Carriage Return/Line Feed). В нашем случае они не нужны, поэтому мы подсовываем функции SimpleHash число введенных символов, обрезанное на два последних байта, и получаем результат - хэш-сумму - в регистре EAX.

Функцией DWordTo8Char мы конвертируем полученную 32-битную сумму из двоичного числа в 8 шестнадцатеричных ASCII-знаков (результат - в переменной SHashSummaStr).

Далее мы делаем "совместный" вывод строки "Hash > " и, собственно, самого результата. Переменные SMsgOnOutput и SHashSummaStr в памяти находятся по соседству. Мы запросто можем передать функции WriteConsole указатель на первую, а в качестве числа печатаемых символов - сумму их размеров, т.е. NSizeOfMsgOnOutput + NHashSummaStr. И вывести их одной функцией WriteConsole. В самом деле, зачем "дергать" за апишные функции два раза, если вполне достаточно одного ;)?

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

(продолжение следует)...

FPU ПОСВЯЩАЕТСЯ (ЧАСТЬ 1)

Тебя Мы помним, как спасенье.
Ты появился, что Абак.
На то был чисел свыше знак -
Дать запятой освобожденье.

1. От автора

Это руководство навеяно вопросом форума WASM.RU. Сейчас вы можете обнаружить множество статей связанных с FPU, все они наперебой долго рассказывают о его архитектуре, командах - обо всем, что можно найти в руководстве Интел. Однако программист чаще сталкивается с другой проблемой. Её сложно назвать. Но вопрос поднимается обычно не об особенностях формата и не о выполнении команд, а "как писать". Собственно, этому и посвящена эта подборка статей.

Начнём с теории: немного почитав, отложим её в сторону, приступив к....

2. Введение

В то время (уже далёкое) числа типа REAL, как это было заведено математиками на Фортране, отображались при помощи "целых". И сколько тут было хитроумных алгоритмов, и сколько было придумано ухищрений. Для простых задач, требующих невысокой точности вполне хватало 32-bits-16-bits представления числа, где одна половина отображала целую часть, а другая - дробную. Например:

    0000000 01110001B = 113D

Если представить, что двоичная точка находится после третьего разряда справа, то:

    0000000 0111.0001B = 14.125D

Правило перевода дробной части просто и подобно переводу целой части:

    S10 = 2n + 2n + 2n + 2n + 2n + .. .. ..

Где n - положение ненулевого бита от точки, считая слева направо.

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

Такое соглашение представления вещественного числа называется "представлением с фиксированной точкой". Его основной недостаток - малый диапазон чисел.

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

Пусть у нас есть число: 12345. Это число в соответствии с правилами научной нотации можно записать: 1.2345*104. При этом выделим несколько характерных частей:

  1. "1.2345" - мантисса
  2. "4" - порядок

Можно придумать несколько вариантов представления числа в данном виде xx*10xx . Например:

     1.2345*104
     12.345*103
     0.12345*105
     0.012345*106

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

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

    1.xxxxx*2n

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

Что мы видим? Старший бит мантиссы всегда будет 1. Да, это так, поскольку двоичное число состоит только из 0 и 1, а когда мы отбросим все "левые" нули, то обязательно получим старший бит равным 1. Недолго думая, инженеры Интел решили оставить этот бит "в уме", повысив тем самым точность представления числа. Но теперь мы вплотную подошли к формату хранения чисел с плавающей точкой.

3. Формат хранения чисел с "плавающей точкой"

x87 (FPU) поддерживает три формата:

  • Одинарной точности
  • Двойной точности
  • Расширенной точности

Или, вы можете услышать следующее определение (любимое Фортраном)

  • Короткие вещественные
  • Длинные вещественные
  • Временные вещественные

Вот их формат:

<- В сторону возрастания адресов

Короткое (одинарной точности)

    S Порядок (8bit) Мантисса (23bits)

Длинное (двойной точности)

    S Порядок (11bit) Мантисса (52bits)

Временное вещественное (расширенной точности)

    S Порядок (15bit) Мантисса (64bits)

Где S - знаковый бит

Как и было сказано, форматы одинарной и двойной точности хранят мантиссу без старшего бита. Однако в последнем случае - формат расширенной точности - мантисса хранится полностью. В действительности формат расширенной точности не используется для хранения переменных, и предназначен для внутренней реализации (в Фортране REAL(4), REAL(8)). Можно только догадываться, почему фирма Интел выбрала именно такой формат (80 бит). Рациональней было бы создать 128- битовое число. Однако именно этот формат был выбран для представления промежуточных результатов. Дело в том, что Инженеры Интел попросту пожадничали, в некотором смысле этого слова.

Они рассчитывали так, чтобы при операциях (подобных z = yx) обеспечить достаточную точность или абсолютную точность формату длинного вещественного. Здесь следовало определить число бит мантиссы, которое необходимо для точного представления результата в формате двойной точности. Взяв, например, выражение z = 2x можно доказать, что для представления результата операции как числа в формате длинного вещественного (обеспечивая заданную точность) необходимо иметь 64 бита в мантиссе (то есть 11bit + 52bit - сумма бит порядка и мантиссы длинного вещественного), что мы и видим в формате расширенной точности.

В случае 128 bits фирме Intel пришлось изобретать ещё один формат "расширенной точности", а по сему выбор 10 байтового формата оказался оптимальным (хотя и недальновидным).

Порядок. Порядок определяет степень двойки, на которую следует умножить мантиссу для получения значения числа с плавающей точкой. Для представления положительного и отрицательного значения порядков фирма Интел использовала метод, отличный от метода дополнения. Так, например, если мантисса имеет длину 8 бит - 256 значений, то считается, что значение порядка равно сумме порядка и константы - смещения, делящего весь диапазон пополам. Для нашего случая смещение будет 127. То есть, если порядок равен 3, то в поле порядка будет записано число 127+3. А если порядок равен -3, то в поле порядка будет записано 127-3. При этом заметьте: диапазон порядка равен от -127 (0) и +128 (255), однако, поскольку эти значения зарезервированы для описания особых ситуаций, диапазон порядка составляет: от -126, до +127. Аналогично для форматов двойной и расширенной точности (-1022/+1023, -16382/+16383).

Другой характерной особенностью является знаковый бит S. Как и ожидалось, его единичное значение соответствует знаку "-", а нулевое "+".

Теперь, когда формат определён, читателю предлагается потренироваться в переводе чисел. Предлагаю два простеньких примера: 0.125 и 0.625. Вы можете усложнить их: 23.125 и 456.625. Свои результаты вы сможете проверить, например, записав результат в переменную dword, и посмотрев под отладчиком число в стеке FPU. Автор настаивает на такой практике, даже если вы не новичок.

4. Особые числа
4.1. Ноль

Не все числа можно представить в нормализованном виде. Это достаточно очевидно, хотя бы потому, что есть такое число как 0. Его представление в расширенной точности: нулевая мантисса и порядок, в то время как мы сказали, что старший бит всегда должен быть равен 1. Интересно отметить, что в FPU существует два нуля, и это является математически корректным. Есть +0 и -0. Далее мы узнаем, что FPU не только способно содержать +/-0, но и образовывать +/-0 при арифметических ситуациях.

4.2. Бесконечность

Говоря о мире математики, вспоминая 0, нельзя не вспомнить его противоположность - бесконечность. Как и ноль, бесконечность - ненормализованное число. У этого числа все биты мантиссы нули, а порядка единицы. Как и нулей, бесконечностей - две: положительная и отрицательная.

5. Особые ситуации

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

  • Деление на ноль - ненулевое число делится на ноль.
  • Неточный результат - результат операции не может быть представлен в данном формате.

Как ни странно, но данная ситуация является наиболее частой. Например, дробь 1/3 представляется периодической десятичной вида 0.3333333.... То же самое наблюдается и в двоичном формате. Следует отметить, что количество "неточно" представляемых дробей для двоичной системы больше, нежели для десятичной.

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

  • Антипереполнение - ненулевой результат слишком мал для его представления, близок к нулю. Далее этот случай будет рассмотрен более подробно.
  • Численное переполнение - результат слишком велик, чтобы быть представленным в приёмнике.
  • Недействительная операция - возникает во всех остальных случаях.

    6. Специальные значения

    FPU позволяет программисту отреагировать на исключительную ситуацию при помощи двух методов: либо при помощи ловушек (прерываний или исключений*), либо при помощи контроля на образование специальных значений. Здесь мы остановимся на разборе этих специальных значений.

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

    6.1. Случай неточного результата

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

    FPU содержит четыре режима округления, управляемого при помощи поля RC регистра управления CR:

           Значение RC         Способ округления
                0               К ближайшему числу
                1               К отрицательной бесконечности
                2               К положительной бесконечности
                3               К нулю
    

    На рисунках показаны схематически эти режимы.

    {картинка поскипана}

    * - В данном контексте и то и другое приблизительно равноправно.

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

    6.2. Численное антипереполнение

    На этот раз результатом данной операции есть специальное значение. Автор напомнит, что, в начале статьи рассказывая о формате чисел, мы упомянули, что значения поля порядка 0 и максимальное зарезервированы для особых ситуаций. Ну вот!!! Вот она - особая ситуация. Пусть будет считаться, что:

    1. Если все биты поля порядка равны 0, то
    2. мы будем считать, что порядок равен 1
    3. а старший бит мантиссы равен не 1, а 0.

    Таким образом, мы получаем ненормализованное число, которое расширяет диапазон представления очень малых чисел от -126 до -149 (для Real4), но при этом точность естественно уменьшается до одного бита. Такое представление чисел называется "денормализованным".

    * Нельзя забывать, что в формате расширенной точности старший бит мантиссы всё равно равен 1.

    Итак, если случай численного антипереполнения замаскирован, и результат слишком мал, чтобы его представить в виде нормализованного значения, FPU формирует денормализованное число. Если же это невозможно, образуется ноль. Такая методика называется плавным переполнением. Она реализована на большинстве современных калькуляторах инженерного типа, и приводит к верному результату. Например:

        (a - b) + b
    

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

    6.3. Особый случай денормализованного операнда

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

    Поскольку при проектировании FPU разработчики стремились обеспечить заданную точность, операции с денормализованным операндом (ами) дают следующие результаты:

    1. ноль, если результат слишком мал, чтобы представить его в виде денормализованного операнда.
    2. Денормализованный результат, если он достаточно мал, чтобы быть представленным денормализованным числом.
    3. Нормализованный результат, если потеря точности при использовании денормализованного операнда меньше, чем потеря точности при ошибке округления (например, произведение большого нормализованного числа и денормализованного).
    4. Ненормализованный результат, если результат неточен, но велик для денормализованного числа - при этом приёмник результата имеет расширенную точность.
    5. Особый случай недействительной операции, если результат подобен условию 4, но приёмник не имеет расширенной точности.

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

    Правила 1-5 позволяют гарантировать, что все операции с ненормализованными/денормализрованными числами, дают либо ненормализованный результат, либо результат имеет малую потерю точности.

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

    6.4. Деление на ноль (или "на ноль делить возможно")

    Вспомним сразу, что деление на ноль порождает антизначение - бесконечность. Будет интересно узнать, что FPU действует верно при делении числа на ноль, если только этот случай замаскирован и замаскирован случай численного переполнения. То есть:

        x/-0 = -[8], x/+0 = +[8];
    

    *Здесь символом [8] обозначена бесконечность.

    Интересней то, что при операциях с бесконечностью FPU даёт математически корректные результаты:

        x/+[8] = +0; -x/+[8] = -0;
        x *( +[8] ) = +[8]; -x *( +[8] ) = -[8];
        (+[8])*(+[8]) = +[8];
    

    где x - любое число, кроме нуля и бесконечности.

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

    6.5. Режимы сравнения

    FPU поддерживал два режима сравнения: первый - проективный, второй - аффинный. Речь идёт о бите IC регистра CR управления бесконечностью. Но в любом случае +[8] > -[8]. В 80286 действовала схема деления на режимы. То есть разработчики фирмы Интел несколько скрыли эффект присутствия знаковых 0 и [8].

    Проективный режим "скрывает" факт наличия двух бесконечностей и нулей. То есть сравнение бесконечности с числом вызывает особый случай недействительной операции, а сравнение бесконечностей всегда даёт результат: равны. Или: сложение / вычитание бесконечности и числа даёт бесконечность, а вычитание/сложение бесконечностей вызывает снова особый случай недействительной операции.

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

    6.6. Численное переполнение

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

    6.7. Особый случай недействительной операции

    Это самый серьёзный случай из всех, здесь рассмотренных, так как после данной ситуации не может быть вычислений. Из всех рассмотренных ситуаций результатом в таком случае может стать NAN (не число). NAN имеет: поле порядка все биты 1, а поле мантиссы что угодно кроме нуля. Особым NAN числом является неопределенность, которая имеет мантиссу 11 в формате расширенной точности, и 1 в остальных случаях. FPU не даёт нечислу превратиться в число. А поэтому в случае бесконтрольного появления нечисла следует их размножение, что может привести к трудно обнаружимым ошибкам.

    7. Далее...

    Начнём с теории; немного почитав, отложим её в сторону, приступив к практике. А как лучше попрактиковаться? Нет ничего лучше, чем практиковаться в окне отладчика. Автор надеется, что перед тем как читатель раскроет следующую часть, он ещё почитает об архитектуре FPU. И разберётся в некоторых азах, если этого ещё не сделал.

    МАКРОСЫ ДЛЯ КОМПИЛЯЦИИ КОМАНД

    (продолжение)

    За последние несколько лет сменилось три поколения микропроцессоров семейства Pentium. У процессоров каждого поколения и у разных моделей одного поколения появлялось достаточно много новых инструкций. Например, по сравнению с Pentium III, у Pentium 4 прибавилось сразу 144 инструкции!

    Компания Intel заблаговременно информирует разработчиков программного обеспечения о своих планах, поэтому Microsoft достаточно оперативно реагирует на технические нововведения и выпускает новые версии компиляторов. Если по каким-то причинам последняя версия MASM вам недоступна, то возможности устаревших версий можно расширить с помощью макроопределений новых команд.

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

    Команда без операндов. Во многих руководствах встречается макроопределение для компиляции инструкции CPUID, которую выполняют все модели микропроцессоров семейства Pentium. Ее могут компилировать только версии MASM, исполняющие директиву .586, в противном случае используется макрос, приведенный в примере 8.

    Пример 8. Компиляция инструкции cpuid

        CPUID   MACRO        ; заголовок макроопределения
                db 0Fh, 0A2h ; объектный код инструкции cpuid
                ENDM     ; конец макроопределения
    

    Макровызовом этого определения является запись в исходном тексте программы команды cpuid, вместо нее в текст программы будет включена директива db 0Fh, 0A2h.

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

    У микропроцессора Pentium Pro появились две группы команд условной пересылки CMOVcc и FCMOVcc. Первая группа относится к категории команд общего назначения. Инструкции второй группы исполняет процессор FPU, они применяются при программировании вычислений с плавающей точкой. В записи конкретной команды вместо маленьких букв cc подставляется мнемоническое обозначение условия, например, CMOVNE или FCMOVB.

    Компилировать инструкции этих групп могут только версии MASM, начиная с 6.12 (исполняющие директиву .686), в противном случае надо применять специальные макроопределения. Мы опишем макроопределения для компиляции инструкций группы FCMOVcc. Операнды инструкций этой группы могут находиться только в числовых регистрах, что упрощает анализ и преобразование их имен.

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

    В примере 9 приведены текст мета-макро определения FPIDEFи список его вызовов, содержащий имена восьми инструкций группы FCMOVcc с соответствующими им кодами операций. При вызове FPIDEF формируется и исполняется одно из 8-ми обычных макроопределений, имена которых совпадают с именем компилируемой инструкции, а параметрами являются ее (инструкции) операнды.

    Пример 9. Мета-макро определения инструкций FCMOVcc

        FPIDEF  MACRO OPNAME, OPCODE1, OPCODE2 ; мета-макроопределение
                OPNAME MACRO OP1, OP2 ; заготовка основного определения
        ;       Проверка имени первого операнда
                LOCAL fvsym, reg   ; описание локальных переменных
                IFDIFI  ,  ; сравнение операндов
                        IFDIFI   ,  ; сравнение операндов
                        .ERR1 %OUT 'Error: First operand must be st'
                        ENDIF ; конец тела второй директивы IFDIFI
                ENDIF   ; конец тела первой директивы IFDIFI
        ;       Проверка имени второго операнда
                reg SUBSTR , 4, 1  ; выделение номера регистра
        fvsym   SUBSTR ,1+reg*5,5 ; выбор эталона
                IFDIFI fvsym,  ; сравнение OP2 с эталоном имени
                        .ERR2
                        %OUT 'Error: Second operand must be st(i)'
                ENDIF   ; конец тела директивы IFDIFI
        ;       Запись кода команды в объектный модуль
                db      opcode1, opcode2+reg
                ENDM    ; конец макроопределения opname
        ENDM            ; конец макроопределения fpidef
        ; Список вызовов макроопределения FPIDEF
        FPIDEF FCMOVB,  0DAh, 0C0h      ; пересылка если меньше
        FPIDEF FCMOVE,  0DAh, 0C8h      ; пересылка если равно
        FPIDEF FCMOVBE, 0DAh, 0D0h      ; пересылка если меньше или равно
        FPIDEF FCMOVU,  0DAh, 0D8h      ; пересылка если неупорядочены
        FPIDEF FCMOVNB, 0DBh, 0C0h      ; пересылка если больше или равно
        FPIDEF FCMOVNE, 0DBh, 0C8h      ; пересылка если не равно
        FPIDEF FCMOVNBE,0DBh, 0D0h      ; пересылка если больше
        FPIDEF FCMOVNU, 0DBh, 0D8h      ; пересылка если упорядочены
    

    Предположим, что в тексте программы встретилась команда fcmove st, st(4). MASM ищет любые имена в своих списках зарезервированных символов и в таблицах имен, описанных в компилируемой программе. Обнаружив имя fcmove в списке, приведенном в конце текста примера 9, он формирует макровызов "fpidef fcmove, 0DAh, 0C8h". Макрос fpidef, в свою очередь, вместо OPNAME формирует и исполняет макроопределение "fcmove st, st(4)", которое компилирует команду и вставляет в текст программы директиву db 0DAh, 0CCh.

    Макроопределение OPNAME проверяет имена операндов, формирует код второго операнда в переменной reg и включает в текст программы указанную выше директиву db, описывающую код команды.

    Имя первого операнда проверяется сравнением с его допустимыми образцами st или st(0). Для того чтобы результат не зависел от регистра, на котором набраны буквы имени (st, ST, sT, St), сравнение выполняет директива IFDIFI (если различаются). Буква I в конце ее имени указывает на то, что перед сравнением операндов коды всех букв имен приводятся к одному регистру.

    Для проверки второго операнда в переменную reg с помощью директивы SUBSTR помещается четвертый символ его имени. Если имя указано правильно, то это будет цифра от 0 до 7. Затем вторая директива SUBSTR копирует в переменную fvsym образец правильной записи имени второго операнда из списка допустимых имен. Наконец, директива IFDIFI сравнивает содержимое fvsym с именем второго операнда. При несовпадении выдается сообщение об ошибке и компиляция прекращается. В случае совпадения в текст программы вставляется код команды.

    Дополнение к примеру 9. При выполнении инструкций группы FCMOVcc проверяется состояние разрядов регистра флагов EFLSGS. Обычным инструкциям FPU регистр флагов недоступен. Поэтому разработчики Pentium Pro ввели 4 новые операции, которые помещают результат сравнения операндов в разряды EFLAGS. Эти четыре инструкции имеют тот же формат, что и инструкции группы FCMOVcc. Поэтому для их компиляции можно использовать макроопределение примера 9 - просто добавьте в конец его текста следующие пять строчек:

        ; Дополнение списка вызовов макроопределения FPIDEF
        FPIDEF FCOMI,   0DBh, 0F0h      ; простое сравнение операндов
        FPIDEF FCOMIP,  0DFh, 0F0h      ; тоже, но с освобождением st
        FPIDEF FUCOMI,  0DBh, 0E8h      ; сравнение неупорядоченных операндов
        FPIDEF FUCOMIP, 0DFh, 0E8h      ; тоже, но с освобождением st.
    

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

    Операнд в оперативной памяти. У новых двухадресных инструкций групп MMX, SSE1 и SSE2 один операнд находится в регистре mmx или xmm, а другой либо в регистре, либо в оперативной памяти. В большинстве случаев в оперативной памяти находится второй операнд, исключением являются инструкции пересылки. Они позволяют перемещать данные как из памяти в регистры, так и в обратном направлении.

    Составить макроопределения для анализа и преобразования имен новых регистров mmx и xmm несложно. Намного сложнее выполнять анализ и преобразование адресов операндов, находящихся в оперативной памяти. При работе с 32-х разрядными адресами код операнда содержит переменное количество байтов, а в его мнемонической записи допускается использование арифметических и логических выражений. В качестве примера приведем три вполне корректные формы записи адресов операндов:

        dword ptr [ebx + 8]
        [eax + 4*ecx + 32]
        16[ebx][eax*4];
    

    Очевидно, что для анализа и преобразования подобных выражений нужен разбор всех допустимых случаев, при этом текст макроопределения становится слишком большим и трудно обозримым. Пример макроса, обрабатывающего ограниченный набор способов записи адресов операндов, приведен в Интернет на моем сайте www.macro.aaanet.ru. Но и при разумных ограничениях текст макроса остается достаточно длинным, поэтому некоторые авторы выбирают другой путь. К их числу относится Агнер Фог.Разработанные им макросы для компиляции новых инструкций находятся в Интернет, на сайте www.agner.org.

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

    Как уже говорилось, начиная с Pentium Pro, микропроцессоры семейства Pentium поддерживают группу CMOVcc, состоящую из 16-ти инструкций условной пересылки. При работе с версиями MASM 6.0 - 6.11 для компиляции инструкций этой группы нужно специальное макроопределение. Его текст приведен в примере 10.

    Пример 10. Макроопределение для компиляции инструкций CMOVcc

        CMOVDEF         MACRO OPNAME, CCODE ; мета-макро определение
                OPNAME MACRO DST, SRC:VARARG ; заготовка макроопределения
                LOCAL X, Y      ; описание локальных переменных (меток)
                X:              ; сохранение адреса начала команды
                BSF  DST, SRC   ; компиляция псевдокоманды
                Y:              ; сохранение адреса конца команды
                ORG X+1 ; возврат ко второму байту кода операции
                DB   CCODE      ; изменение кода второго байта команды
                ORG Y   ; восстановление текущего адреса
                ENDM    ; конец макроопределения OPNAME
        ENDM            ; конец макроопределения CMOVDEF
            ; Начало списка инструкций группы вызовов CMPVDEF
        CMOVDEF CMOVO,  40h     ; пересылка если переполнение
        CMOVDEF CMOVNO, 41h     ; пересылка если нет переполнения
        CMOVDEF CMOVB,  42h     ; пересылка если меньше
        CMOVDEF CMOVNB, 43h     ; пересылка если больше или равно
            ; и так далее вплоть до инструкции CMOVG с кодом 4Fh
    

    Текст примера 6.10 состоит из мета-макро определения CMOVDEF и неполного списка его макровызовов с указанием имен 4-х первых инструкций группы CMOVcc и соответствующих им кодов операций.

    Предположим, что в тексте программы встретилась команда "cmovb cx, [bx+2]". MASM находит имя инструкции в списке CMOVDEF и вызывает одноименное мета-макро определение с параметрами CMOVB, 42h. Оно, в свою очередь, формирует и выполняет макроопределение "cmovb cx, [bx+2]", в котором производится подмена имени компилируемой инструкции.

    Для подмены взята инструкция BSF, ее исполняют микропроцессоры семейства Intel, начиная с модели 386, и могут компилировать все версии MASM, начиная с 5.1. Таким образом, реально компилируется команда "bsf cx, [bx+2]". В результате MASM получается объектный код "0F BC 4F 02". Остается заменить в этом коде содержимое второго байта (BC) на 42, т. е. сформировать код "0F 42 4F 02", соответствующий исходной команде "cmovb cx, [bx+2]"

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

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

    Начиная с Intel 386, микропроцессоры могут работать в 16-ти разрядном (реальном) и в 32-х разрядном (защищенном) режимах. Независимо от установленного режима у команд могут быть как 16-ти, так и 32-х разрядные операнды и адреса. Если разрядность операнда или адреса не совпадает с разрядностью установленного режима работы процессора, то у команды появляются один или два префикса. Префикс размера операнда имеет код 66h, а префикс размера адреса имеет код 67h.

    При наличии префиксов текущий адрес, сохраненный в примере 10 в переменной X, не соответствует адресу начала кода операции, а содержимое адреса X+1 не является вторым байтом кода операции и его изменять нельзя. Как поступать в таких случаях?

    MASM определяет разрядность режима работы микропроцессора исходя из модели памяти, указанной в директиве .MODEL. Допустимы следующие имена моделей: TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE, FLAT. Из них только модель FLAT соответствует 32-х разрядному режиму работы микропроцессора.

    Если компилируемые инструкции работают только с 16-ти разрядными операндами и адресами, то вы можете выбрать любую модель, кроме FLAT. И наоборот, если инструкции работают только с 32-х разрядными операндами и адресами, то надо выбрать модель FLAT. Только при выполнении этих условий у команд отсутствуют префиксы, и макроопределение примера 10 является корректным.

    Если по каким-то причинам указанное требование не выполнимо, то надо произвести предварительную компиляцию и получить листинг программы. По листингу уточняется наличие и количество префиксов перед командой BSF и вносится изменение в текст макроопределения примера 10. В зависимости от количества префиксов адрес в директиве ORG X+1 изменяется на X+2 или X+3. Ничего лучшего предложить нельзя.

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

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

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

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

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

    Пример 11. Управление компиляцией макроопределения

                IFDEF @version ; если переменная @version определена, то
                        IF @version GE 612 ; если версия MASM 6.12 или выше, то
                        ; MASM поддерживает инструкции группы P6
                        .686  ; разрешение компиляции инструкций группы P6
                        SUPPORT EQU 1 ; определение переменной SUPPORT
                        ENDIF  ; конец действия директивы IF
                ENDIF  ; конец действия директивы IFDEF
                IFNDEF SUPPORT  ; если переменная SUPPORT не определена, то
                ; здесь располагается текст макроопределения для компиляции новых
                ; инструкций из группы P6, например, полный текст примера 9 или 10.
                ENDIF  ; конец действия директивы IFNDEF SUPPORT
    

    В примере 6.11 проверяется поддержка MASM инструкций общего назначения, входящих в группу P6. Если вы собираетесь компилировать новые инструкции MMX, то, не изменяя версию, замените в тексте примера 6.11 директиву .686 на .mmx. При компиляции инструкций групп SSE надо изменить номер версии на 614, и вместо директивы .686 указать .xmm.

    Замечание. Директивы .686, .mmx и .xmm разрешают компиляцию разных категорий инструкций микропроцессоров, поэтому их можно использовать совместно, но директива .686 должна быть указана первой. По крайней мере, так надо поступать при работе с версией MASM 6.15.

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

    ОСЕНЬ, которая типа наступила (ага, на горло - очередной эпидемией гриппа), между прочим, есмъ самое продуктивное для кодера время года. В ожидании начала отопительного сезона (ну когда ж они, #### зззатопят) не остается ничего другого, кроме как согреваться подручными средствами.

    Как-то: отлаживать ядро виндов (очень много калорий выделяется при кипении возмущенного разума), участвовать в проекте distributed.net по взлому-вычислению чего-нибудь (загрузка проца на все 100, да не процентов, а градусов цельсия) или, в конце-то концов, поставить Cain'а и согреваться потом, удирая от сисадмина, которому arpwatch накидал мылом такую туеву кучу матюгальников...

    Под убаюкивающий дуэт дождя за окном и вентилятора в десктопе самое время заняться самообразованием - дружно делаем три штуки "клик хере" и скачиваем новые рулезные доки (все в формате chm):

    • ВСЕ туториалы Iczelion'a на русском, release 2.
    • "Путеводитель по написанию вирусов" от Billy Belcebu.
    • ВСЕ материалы, ныне пребывающего в дZенском раю некогда флагмана росийской ассемблерной мысли, сайта Assembler.ru (привет тебе, Серж Ветров, когда ты это прочитаешь!).

    И, наконец, клик www.forum.wasm.ru - это наш новый неповоротливый форум, на котором вас просветлят и оптом, и в розницу, и ногами попинают, и зубы попилят напильником, и утюжком погладят против шерстки...

    И да пребудет с вами сила, братья по безумию!