Воробьёвы

(-:

№27. Чем дальше в лес... макросы

Самая главная новость состоит в том, что таки произошло объединение сайтов hi-tech.nsys.by и wasm.zite.ru. Новый адрес - WWW.WASM.RU, аминь ;). Как видите (см. ниже), содержание и оформление рассылки также несколько изменились :). Все что вы думаете на счет этого безобразия - надлежит писать в гостевую гнигу

СТЕК И ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ

Скрючились пальцы черной судорогой
черной судорогой скрючились пальцы...
(C) Егор и Опи..невшие

#1. Представьте себе картину: накачанный колесами и протеиновыми коктейлями шварценеггер бьет со всей дури по боксерской груше. Та отлетает в сторону, а потом по каким-то абсолютно нефизическим законам кинемотографа возвращается назад и бьет этому боксеру по морде лица, в результате чего шварценеггер отлетает на несколько метров, и, обязательно задев что-нибудь из мебели, размазывается соплями по стене - к неописуемому удовольствию зрителя. Посмотрев на такую картину, Станиславский бы сказал: "не верю"! А вот дZенствующей программер, увидя это безобразие, подумал бы: "ба! Да совсем как стек. Помницца, в одной из своих кулхацкерных прог я на похожие грабли как раз и напоролся".

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

Ранее мы уже разобрались с очередностью записи в стек и чтения из а него. Напомню, что доступ к стеку осуществляется в соответствии с принципом LIFO (Last In First Out – Последним Пришел, Первым Ушел). Однако это отнюдь не единственное, что нам нужно знать о стеке - конечно же, если мы не собираемся время от времени получать "отдачу" от "боксерской груши" ;).

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

Также известно, что в инструкциях процессоров от Интел переменные (которые в памяти) не могут выступать в качестве приемника и источника одновременно, то есть инструкция:

     mov  [dwVar1],[dwVar2]

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

     mov eax,[dwVar2]
     mov [dwVar1],eax

либо задействовав стек:

     push [dwVar2]
     pop  [dwVar1]

Кстати, недавно в почтовой рассылке RTFM_helpers прошло обсуждение того, как можно копировать из памяти в память - там было упомянуто, например, использование movs. А если подумать, можно найти и другие нетривиальные способы.

Для тех, кто ещё не понял. Вот этот кусок кода:

     push 1
     push 2
     push 3
     pop  eax
     pop  ebx
     pop  ecx

делает то же самое, что и следующий код (если только не считать разницу в скорости, размере кода и побочных эффектах):

     mov  eax,3
     mov  ebx,2
     mov  ecx,1

Сомневающиеся могут проверить под отладчиком:). Также попробуйте сравнить:

     push 1
     pop  eax

и психоделическое:

     sub  esp,4
     mov  dword ptr [esp],1
     mov  eax,[esp]
     add  esp,4

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

- Что за esp такой? – спросите вы.

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

#2. Помните мою аналогию с блинами от штанги и вделанным в пол штырём, на который они надевались для хранения. Там еще был учитель физкультуры, который приставал к нашим неполовозрелым одноклассницам, в результате чего ему перебили нос. Так вот, адрес самого верхнего блина – это вершина стека и хранится он (адрес) в регистре esp/sp. Другими словами, вершина стека – это адрес последнего занесенного в стек элемента.

Давайте посмотрим на поведение esp при трассировке следующего кода:

     Main proc
      push 1
      push 2
      push 3
      pop  eax
      pop  ebx
      pop  ecx
      invoke ExitProcess,0
     Main endp

При загрузке мы видим, что регистр esp инициализирован значением 12FFC4 (в других условиях стартовое значение может быть другим, но суть от этого не меняется). Давайте выполним один шаг (команда "t" - trace). В результате этого в стек ляжет 1, а значение регистра esp поменяется на 12FFC0. Трассируем дальше и наблюдаем, как изменяется esp:

      push 1    ;esp=12FFC0
      push 2    ;esp=12FFBC
      push 3    ;esp=12FFB8
      pop  eax  ;esp=12FFBC
      pop  ebx  ;esp=12FFC0
      pop  ecx  ;esp=12FFC4

Протрассировав первые три строчки, мы можем сделать вывод, что стек "растет" в сторону младших адресов (12FFC0 > 12FFBC > 12FFB8, логично?), а шагом изменения регистра esp является 4. И это правильно, так как при 32-битном режиме адресации в стеке сохраняются двойные слова, они же - 4 байта (хотя допускается класть в стек также и 2-байтные слова - при этом шаг равен 2). К слову сказать: если бы мы писали под DOS (точнее, в реальном или виртуальном режиме), то стек у нас адресовался бы регистром sp и изменялся бы он на плюс-минус 2.

Обобщаем. Алгоритм работы команды push <источник> следующий:

  1. Уменьшение значения указателя стека esp/sp на 4/2.
  2. Запись значения источника по адресу ss:esp/sp (вершина стека).

Об алгоритме работы команды pop догадайтесь сами…

Для последователей дZена предлагаем тему для исследования: какое значение будет лежать в стеке после инструкции push esp. :)

ПРЕДУПРЕЖДЕНИЕ: ни в коем случае не принимайте результаты отдельных экспериментов в конкретных условиях за абсолютную истину, верную всюду и всегда!

Теперь, после вышесказанного, вы легко сообразите, какое значение примет регистр eax в следующем извращенном случае:

     push 1    ;esp=12FFC0
     push 2    ;esp=12FFBC
     push 3    ;esp=12FFB8
     add  esp,4
     pop  eax  ;esp=12FFC0
     pop  ebx  ;esp=12FFC4

[Правильный ответ – 2. :)]

#3. Есть ещё один регистр, ассоциируемый со стеком - ebp/bp, и описывается его функция так, что выговорить страшно – указатель базы кадра стека. Такое название этого регистра я нашел в книжке Юрова & Хорошенко "ASSEMBLER, учебный курс". Нет, конечно же, можно назвать калоши "мокроступами", а bitmap "двоично-точечной картинкой", но... "У меня нет слов, у меня есть только выражения в адрес того, кто заворачивает такие коленца" (C) Аркадий Белоусов. А по сему давайте заменим словосочетание "кадр стека" простым народным :) словом "фрейм", а "указатель базы" заменим на просто "база" (или "указатель" - по вкусу).

В результате подобных терминологических "подстановок" получается следующая картина: есть у нас в компьютере некие "фреймы", располагаются они в стеке и адреса этих пока что непонятных нам штук завязаны с регистром ebp/bp.

"Дело в следующем". Любая процедура/функция в терминах любого процедурного языка имеет (может иметь) нуль и больше параметров и локальных переменных. Область памяти, создаваемая (выделяемая) при вызове процедур для аргументов и локальных переменных,, и называется фреймом. А чтобы процедура могла быть рекусивной (т.е. могла вызывать саму себя или вызываться из других процедур, которые она вызывает) или реентерабельной (т.е. чтобы код процедуры мог использоваться в параллельных процессах), фреймы для неё должны размещаться в стекоподобной структуре данных - "стеке". А поскольку процедурные языки ближе к "естественному" мышлению, то в наборы инструкций процессоров и ассемблеры вводят поддержку подобных высокоуровневых конструкций.

Существуют нюансы и различия в реализациях, например:

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

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

Теперь, собсна, про ebp/ep. В случае стекового фрейма его адрес не фиксирован, поэтому адресация параметров и переменных в нём должна быть "базисно-индексной", относительно начала фрейма. На писюке под это идеально подошёл (или изначально проектировался) BP/EBP - в отличие от SP, он может служить базой, и также адресуется относительно SS.

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

#4. Мы привыкли к тому, что извлекать данные из стека можно только повинуясь очередности LIFO. А что, если нам понадобилось обратиться к произвольному элементу стека? Один из возможных способов мы уже рассмотрели: это изменение значения регистра esp/sp плюс команда pop. Это далеко не совсем хорошая идея, и вам не стоит издеваться над стеком таким изощренным способом. Если, конечно же, вы не хотите уподобляться Штирлицу и Мюллеру, которые стреляли по очереди...

Напомню: регистр ebp/bp ведет себя приблизительно таким же образом, как и хорошо нам знакомый ebx/bx. То есть он может выполнять роль базы.

Напомню, что, например, следующий код:

     mov ebx,12FFC0h
     mov al,[ebx]

присвоит регистру AL значение байта по адресу 12FFC0 из сегмента, задаваемого регистром DS.

Точно таким же образом можно использовать и регистр ebp/bp. Говоря другими словами, это один из немногих регистров, которые можно "брать в квадратные скобки" не увеличивая при этом размер инструкции :). То есть (и это будут уже третьи слова) он позволяет работать с ячейкой памяти, адрес которой находится в регистре.

Проиллюстрирую это на простом примере. Допустим, занесли вы в стек разных параметров кучу:

     адрес     значение
     0012FFC4  77E7EB69
     0012FFC8  0047E4AC
     0012FFCC  0012DAB4
     0012FFD0  7FFDF080

И возжелалось вам в силу какой-нибудь нездоровой производственной необходимости прочитать "здесь и сейчаc", например, предпоследний элемент. В этом случае делаем вот что:

     mov ebp,esp
     mov eax,[ebp+4]

Расшифровываю. Мы принимаем адрес самого последнего из записанных в стек элемента за точку отсчета, и путем прибавления к этой "точке отсчета" четвёрок можно легко получить доступ к тому элементу стека, который нашей программерской душеньке возжелался. В принципе, в данном примере можно было бы использовать ESP вместо EBP, но, во-первых, должны же мы были показать использование EBP, :) а, во-вторых, в больших фрагментах кода использование ESP непосредственно имеет свои недостатки (больший размер инструкций и необходимость отслеживать изменение вершины стека).

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

#5. Перед тем, как мы пойдем дальше и разберемся-таки с локальными переменными – пара слов для особо продвинутых:

В защищённом режиме (или в реальном режиме с префиксами смены разрядности) базой может служить любой регистр, поэтому, если мы точно знаем состояние регистра ESP (т.е. мы точно знаем, сколько раз мы делали push, а сколько pop), то для доступа к фрейму можно использовать ESP (при этом индексы одной и той же перменной в разных местах процедуры могут отличаться из-за промежуточных push/pop). Собсно, подобного рода оптимизацией занимаются, насколько я знаю, последние версии BC и VC. А в их "асмах" появилась директива "фраме-поин-оммисинс" как раз для таких извратов и предназначенная.

Однако, здесь есть недостатки по сравнению с использованием более-менее статичного EBP, как об этом было упомянуто выше: во-первых, с [ESP] инструкции длиннее, во-вторых, нужно быть очень аккуратным в подсчёте промежуточных push/pop, чтобы верно подставлять смещение до аргумента или переменной в [ESP+xx] (а ведь есть относительно непредсказуемые инструкции вида push [esp+xxx]). Наконец, поскольку индекс xx может постоянно меняться, поэтому использовать встроенные директивы типа ARGS или даже вручную раставленные EQU становится малореальным. Поэтому возможность использования ESPв качестве базы (при ручной кодогенерации – глюкалово полное) отнюдь не умаляет полезности EBP.

#6. Ну вот, мы и подошли к самой интересной части марлезонского балета. Сейчас мы готовы проанализировать нашу программу на предмет того, чего она там вытворяет с локальными переменными.

     :00401000 NumberOfCharsWritten= dword ptr -90h
     :00401000 nNumberOfCharsToWrite= dword ptr -8Ch
     :00401000 hConsoleInput   = dword ptr -88h
     :00401000 hConsoleOutput  = dword ptr -84h
     :00401000 Buffer          = byte ptr -80h
     :00401000
     :00401000 push    ebp
     :00401001 mov     ebp, esp
     :00401003 add     esp, 0FFFFFF70h
     :00401009 push    offset aInputOutput ; lpConsoleTitle
     :0040100E call    SetConsoleTitleA
     :00401013 push    0FFFFFFF5h      ; nStdHandle
     :00401015 call    GetStdHandle
     :0040101A mov     [ebp+hConsoleOutput], eax
     :00401020 push    0               ; lpReserved
     :00401022 lea     eax, [ebp+NumberOfCharsWritten]
     :00401028 push    eax             ; lpNumberOfCharsWritten
     :00401029 push    11h             ; nNumberOfCharsToWrite
     :0040102B push    offset aTypeSomething ; lpBuffer
     :00401030 push    [ebp+hConsoleOutput] ; hConsoleOutput
     :00401036 call    WriteConsoleA
     :0040103B push    0FFFFFFF6h      ; nStdHandle
     :0040103D call    GetStdHandle
     :00401042 mov     [ebp+hConsoleInput], eax
     :00401048 push    0               ; lpReserved
     :0040104A lea     eax, [ebp+nNumberOfCharsToWrite]
     :00401050 push    eax             ; lpNumberOfCharsRead
     :00401051 push    80h             ; nNumberOfCharsToRead
     :00401056 lea     eax, [ebp+Buffer]
     :00401059 push    eax             ; lpBuffer
     :0040105A push    [ebp+hConsoleInput] ; hConsoleInput
     :00401060 call    ReadConsoleA
     :00401065 push    0               ; lpReserved
     :00401067 lea     eax, [ebp+NumberOfCharsWritten]
     :0040106D push    eax             ; lpNumberOfCharsWritten
     :0040106E push    0Ch             ; nNumberOfCharsToWrite
     :00401070 push    offset aYouTyped ; lpBuffer
     :00401075 push    [ebp+hConsoleOutput] ; hConsoleOutput
     :0040107B call    WriteConsoleA
     :00401080 push    0               ; lpReserved
     :00401082 lea     eax, [ebp+NumberOfCharsWritten]
     :00401088 push    eax             ; lpNumberOfCharsWritten
     :00401089 push    [ebp+nNumberOfCharsToWrite] ; nNumberOfCharsToWrite
     :0040108F lea     eax, [ebp+Buffer]
     :00401092 push    eax             ; lpBuffer
     :00401093 push    [ebp+hConsoleOutput] ; hConsoleOutput
     :00401099 call    WriteConsoleA
     :0040109E push    7D0h            ; dwMilliseconds
     :004010A3 call    Sleep
     :004010A8 push    0               ; uExitCode
     :004010AA call    ExitProcess

Начнем с того, что полегче ;). Нетрудно заметить, что команда addr в применении к локальным переменным (в контексте invoke) , во всех случаях генерит следующий код:

     lea eax,[ebp-X]
     push eax

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

     push [ebp-X]

Вы не поверите, но почему-то многие новички здесь путаются... Как, вы тоже? ;))

Первое – заносит в стек указатель на. А второе – значение. Всем медитировать!

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

     a = -84
     b = x+a

имеет более чем тривиальное решение:

     b = x-84

Хе-хе... слышала бы меня сейчас моя школьная учительница математики ;)).

#7. Очевидно, что ebp – это некая "точка отсчета", относительно которой адресуются локальные переменные. А что ж у нас в ebp? Смотрим на начало процедуры, и ищем там строчки

     :00401001 mov     ebp, esp
     :00401003 add     esp, 0FFFFFF70h

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

     LOCAL InputBuffer[128] :BYTE  ;буффер для ввода
     LOCAL hOutPut          :DWORD ;хэндл для вывода
     LOCAL hInput           :DWORD ;хэндл для ввода
     LOCAL nRead            :DWORD ;прочитано байт
     LOCAL nWriten          :DWORD ;напечатано байт

То есть 128 штук байт плюс 4 двойных слова по 4 байта в каждом. Итого для этого всего богатства нужно выделить 144 байт памяти.

Ну и замечательно! Сохраняем адрес вершины стека (память под переменные еще не отведена!) в регистре ebp. Теперь это у нас вовсе не вершина, а "точка отсчёта" для локальных переменных. А саму вершину стека передвигаем на 144 байта "вверх", в сторону младших адресов (там у нас будет область для хранения локальных переменных). Как видите, все очень просто. :)

Задание для медитации: чем будут отличаться генеримые инструкции для директивы addr в случае, если addr будет применяться к аргументам процедуры, а не к локальным переменным.

Если вас смущает то, что для "поднятия планки" используется инструкция add и столь большое число, то вернитесь к выпуску о кодировании отрицательных чисел и особенностях дополнительного кода. Либо поставьте в Иде указатель на смущающее вас число 0FFFFFF70h и нажмите на Ctrl+-, после чего долго и упорно медитируйте. :)

Если подключить фантазию и пару раз протрассировать это безобразие под отладчиком, то получится такая картинка:

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

Распечатайте её и повесьте над своей кроватью. И пусть она время от времени напоминает вам о смерти...

ФИЗИЧЕСКИЕ АДРЕСА В WIN9X

Зубчатые колеса завертелись в башке...
(C) Егор и Опи..невшие

О ЧЕМ ПОЙДЕТ РЕЧЬ

Вы никогда не задумывались над тем, в каком именно мегабайте вашего компа выполняется ваша программа? А в каком уютно разместился кернел? Нет? А мне вот стало интересно, и я решил узнать...

СТРАНИЧНАЯ АДРЕСАЦИЯ

Для начала немного теории.

Несколько слов о том, как процессоры 386+ осуществляют страничную адресацию. Информация взята из книги Е.Бердышева "Технология MMX. Возможности процессоров P5 и P6".

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

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

     mov byte ptr ds:[eax],2h

Как же процессор может вычислять физический адрес требуемой ячейки памяти ?

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

     Физический адрес = база из дескриптора + eax.

Если включено страничное преобразование, то все происходит иначе. Рассмотрим случай размера страницы в 4 килобайта. Процессор выделяет из значения смещения в eax три части:

Номера битов смещенияDirectory
22..31Index in Page
12..21Index in Page Table
0..11Index in Page

Взяв "Index in Page Directory", процессор обращается к так называемой "Page Directory" - каталогу страниц. Это область памяти с физическими адресами таблиц страниц. Физический адрес этого каталога находится в регистре CR3 (только старшие 20 бит CR3!), а число элементов нетрудно получить из количества бит, отводимых под индекс в каталоге - 10 бит дают 1024 элемента. Таким образом, сначала процессор извлекает элемент из каталога страниц:

    Элемент каталога = [CR3 + Index in Page Directory*4],

который является (не совсем весь, только старшие 20 бит) физическим адресом начала одной из таблиц страниц. Таблицы страниц (Page Table) являются, в свою очередь, набором физических адресов (опять только старшие 20 бит) начала самих страниц в памяти. Для выборки конкретного элемента из Page Table процессор использует адрес ее начала (Элемент каталога) и "Index in Page Table" из смещения команды:

    Адрес начала страницы = [Элемент каталога + Index in Page Table*4]

Окончательный физический адрес элемента памяти вычисляется по адресу начала страницы и индексу элемента страницы из смещения в команде:

    Физический адрес = Адрес начала страницы + Index in Page

Схематично это можно представить таким образом:

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

УПРАВЛЕНИЕ СТРАНИЧНОЙ АДРЕСАЦИЕЙ

Теперь подробнее о том, как задается страничная адресация.

За включение страничной адресации ответственен бит PG (Paging Flag) (31 бит) регистра CR0 процессора. Если он 1, то страничное преобразование разрешено.

Тип страничной адресации задается битами PAE (Physical Address Extention) (5 бит), PSE (Page Size Extention) (4 бит) регистра CR4 процессора, а также битом PS (Page Size) (7 бит) в выбранном элементе Page Directory.

Следует отметить, что регистр CR4 доступен только в процессорах Pentium, и в общем случае необходимо проверять тип процессора командой CPUID и только затем - биты в регистре CR4.

Если бит PAE=1, то разрешен 36-ти разрядный физический адрес, иначе - "обычный" 32-х разрядный. Если бит PSE=0, то размер страницы 4 Килобайта, иначе - может быть 2 или 4 Мегабайта.

Экспериментально тип страничного преобразования можно проверить, например, так:

     .586p ; Pentium Processor
     PG equ 1 shl 31
     PAE equ 1 shl 5
     PSE equ 1 shl 4
     ; ...
       mov  eax,CR0
       test eax,PG
       jz   @@NoPageRegim
       mov  eax,CR4
       test eax,PAE
       jnz  @@PhysAddr_36bit
     ; ...
     @@PhysAddr_36bit:
     ; ...
     @@NoPageRegim:

Итак, если бит PG=1, а биты PSE=PAE=0, то у нас "обычное" страничное преобразование с 4-х килобайтным размером страницы и 32-х битным адресом.

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

КАК ПОЛУЧИТЬ ФИЗИЧЕСКИЙ АДРЕС ПО ЛИНЕЙНОМУ АДРЕСУ

Решим следующую небольшую задачку: напишем процедуру, которая будет возвращать win32-приложению по заданному линейному адресу физический адрес.

Код процедуры разместим в динамическом VxD, поскольку для его реализации необходимо использование привилегированных инструкций типа "mov eax,CR0", недопустимых в win32-коде. Подробнее о написании динамических VxD можно прочесть, например, в туториалах Iczelion’а, размещенных на сайте WASM.RU.

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

Итак, начнем. Сначала - стандартное начало динамического VxD:

     ; Программа чтения физического адреса по линейному.
     ; Этот динамический VxD можно загружать через DeviceIOControl
     ; и получать по указателю физический адрес по линейному адресу.
     ; Coded by Chingachguk. 2002.
     .386p
     include vmm.inc
     include vwin32.inc
     DECLARE_VIRTUAL_DEVICE PHY,1,0, PHY_Control,\
          UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
     Begin_control_dispatch PHY
       Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
     End_control_dispatch PHY

Определимся с передачей параметров и получением результатов нашего кода. Будем передавать VxD адрес следующей структуры:

     ; Структура параметров при вызове
     CallParams struc
     LinearAddr dd ? ; Линейный адрес
     PhysAddr   dd ? ; Сюда вернуть физический адрес
     CallParams ends

Определим в сегменте данных одну переменную - флаг ошибки:

     ; Сегмент данных нашего VxD
     VxD_PAGEABLE_DATA_SEG
     Result dd ? ; Если будет ошибка, мы будем хранит тут 0FFFFFFFFh (-1).
     VxD_PAGEABLE_DATA_ENDS

Алгоритм вычисления физического адреса понятен. Остается решить один важный вопрос: как же обращаться к памяти, если у нас есть физический адрес, а мы находимся в режиме страничного преобразования? Например, мы получили физический адрес элемента в Page Directory(например, сейчас он в eax), но команда вида:

     mov eax,[eax]

совсем не приведет к чтению элемента каталога страниц, поскольку процессор будет трактовать значение в eax согласно страничному преобразованию!

Разумным решением было бы вызвать сервис другого VxD, например VMM, с целью получить физический адрес по линейному (в этом случае нам вообще делать будет нечего, даже не надо читать никаких Page Directory, Page Table...). Однако такого сервиса не существует (DDK) ! С другой стороны, существует сервис получения линейного адреса по физическому у VMM. Необходимость его существования следует из необходимости некоторым драйверам адресоваться к конкретной физической памяти, например при работе с BIOS и т.д.:

     Get linear address by physical address(DDK)
     VMMCall _MapPhysToLinear,

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

А вот наш сегмент кода:

     VxD_PAGEABLE_CODE_SEG
     BeginProc OnDeviceIoControl
       assume esi:ptr DIOCParams
     .if [esi].dwIoControlCode==DIOC_Open ; Контрольное сообщение ?!
       xor  eax,eax ; Надо отвечать: eax=0.

А вот это уже серьезно. Это вызов из вин-приложения с конкретным заданием. Что это за задание - знаем только мы и тот, кто нас вызвал.

     .elseif [esi].dwIoControlCode==1
       mov dword ptr Result,0FFFFFFFFh ; Установим флаг ошибки
       pushad ; На всякий случай сохраним все регистры, кроме сегментных
       pushfd ; Сохраним флаг направления. Видимо, это перестраховка.
       mov  edi,[esi].lpvInBuffer
       mov  edi,[edi] ; указатель на буфер, который нам передал win32-код

Теперь получаем начальный физический адрес Page Directory:

      mov  eax,CR3
      and  eax,1111111111111111111100000000000b ; Выделить биты 31..12

Получаем индекс в Page Directory (биты 22..31):

      mov  ecx,[edi].LinearAddr
      shr  ecx,22
      shl  ecx,2

Получаем физический адрес элемента в Page Directory:

     add  eax,ecx

Получаем линейный адрес по физическому адресу от VMM.vxd. Физический адрес элемента сейчас в eax:

       call GetLinearAddr_Memory
       jz   @@PageDirectoryErr

Прочтем по линейному адресу, выданному нам системой, содержимое ячейки Page Directory:

     mov  eax,[eax]
     and  eax,1111111111111111111100000000000b

И теперь у нас eax=физический адрес Page Table. Получаем индекс в Page Table (биты 21..12):

     mov  ecx,[edi].LinearAddr
     shr  ecx,12
     and  ecx,1111111111b
     shl  ecx,2

Получаем физический адрес элемента в Page Table:

     add  eax,ecx

Получаем линейный адрес по физическому адресу от VMM.vxd (аналогично Page Directory):

       call GetLinearAddr_Memory
       jz   @@PageDirectoryErr

Читаем элемент Page Table:

       mov  eax,[eax]
       and  eax,1111111111111111111100000000000b

Теперь eax=физический адрес Page. Получаем индекс в Page (биты 11..0):

     mov  ecx,[edi].LinearAddr
     and  ecx,111111111111b

И, наконец, получить физический адрес:

     add  eax,ecx

Вернем его вызвавшей программе:

     mov  [edi].PhysAddr,eax
     mov  dword ptr Result,0h ; Сбросим флаг ошибки - все прошло нормально.

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

     @@PageDirectoryErr:
       popfd ; Восстановим флаги направления и т.д. - перестраховка ?!
       popad
       mov  eax,dword ptr Result ; Вернем в eax флаг ошибки
     .endif
       ret
     EndProc OnDeviceIoControl

Это служебная подпрограмма – оформление вызова сервиса VMM. Получает физический адрес в eax и в нем же возвращает линейный:

     GetLinearAddr_Memory proc
     ; Input: eax=phys addr
     ; Result: eax=linear addr or ZF is set
     ; Get linear address by physical address(DDK)
     ; VMMCall _MapPhysToLinear,
       push 0h ; flags
       push 4h ; 4 bytes
       push eax ; PhysAddr
       int  20h   ; Call VxD
       dw   006Ch  ; 006Ch map physical address to linear address
       dw   0001h   ; ID VMM
       add  esp,3*4   ; C-call function
       cmp  eax,0FFFFFFFFh ; 0FFFFFFFFh if not addressable
     ; eax = address of first byte
     ; Returns the linear address of the first byte in the specified range of
     ; physical addresses. Uses EAX, ECX, EDX and Flags.
       ret
     GetLinearAddr_Memory endp
     VxD_PAGEABLE_CODE_ENDS
     end

Для примера приведен фрагмент вызова такого VxD из win32-кода. Та же структура для передачи параметров:

     .data
     CallParams struc
     LinearAddr dd ?
     PhysAddr dd ?
     CallParams ends

Имя загружаемого VxD, которое передается CreateFile-у:

     VxDName     db "\\.\PHY.VXD",0

Структура, которой передаются параметры коду VxD:

     InBuffer    dd offset MyMem ; Указатель на буфер для чтения параметров

Тут будет храниться хэндл открытого VxD:

     .data?
     hVxD        dd ?
     MyMem CallParams <>

Начало выполнимого кода:

     .code
     start:

Загрузим динамический VxD через CreateFile:

       invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
       .if eax!=INVALID_HANDLE_VALUE ; VxD Успешно загружена ?
         mov hVxD,eax

Получить физический адрес какого-нибудь линейного, например, текущего указателя EIP:

         call @@GetOfs
     @@GetOfs:
         pop eax

Передать наш линейный адрес VxD-коду:

     mov dword ptr MyMem.LinearAddr,eax
     invoke DeviceIoControl,hVxD,1,addr InBuffer,
            sizeof InBuffer,NULL,NULL,NULL,NULL

Проверка на ошибку. Если ошибка, то eax = 0:

     test eax,eax
     jz @@ErrorReadPhys

Ошибки нет. Покажем физический адрес:

     call Print_PhysAddr
     ; ...
     @@ErrorReadPhys:

ПРИМЕР ТРАНСЛЯЦИИ ЛИНЕЙНЫХ АДРЕСОВ

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

Линейный адресФизический адрес
Сегмент кода win32-приложения(EIP)0040103Bh0072103Bh (~7 Мегабайт)
Kernel32.dll, ф-ция CreateFileBFF77ADFh00345ADFh (~3 Мегабайт)
Стек win32-приложения(ESP)0063FE3Ch00635E3Ch (~6 Мегабайт)
Сегмент кода загруженного VxDC188A4E6h00E3C4E6h (~14 Мегабайт)

ИТОГО

Таким образом, единственный необходимый сервис ОС для получения физического адреса по линейному - это сервис "Получить линейный адрес по физическому". Очевидно, аналогичные сервисы существуют не только windows98(95), а и windows NT и ее наследниках - windows2000 и т.д, что позволит переносить приведенный выше код без особых изменений на эти платформы, используя соответствующие модели драйверов (*.wdm).

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

БЕСПЛАТНОЕ ПРИЛОЖЕНИЕ, #1

БЕСПЛАТНОЕ ПРИЛОЖЕНИЕ, #2

ТАКИЕ РАЗНЫЕ МАКРОСЫ

Пауки в банке - хотели выжить,
через отрезок пустоты увидеть солнце...
(C) Янка

Makros (макрос) – греческое слово, обозначающее "большой" или "длинный". В Кратком оксфордском словаре английского языка сказано, что от него происходит префикс macro. В литературе по программированию macro обычно употребляется как синоним слова "макрокоманда".

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

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

Цель данной статьи обратить внимание программистов, работающих с ассемблером, на существование специальных средств, предназначенных для составления макросов и показать примеры их применения. Для проверки приведенных примеров версия MASM (Microsoft Macro Assembler) должна быть не ниже чем 6.0.

1. ОБЩИЕ ПОЛОЖЕНИЯ

Макросы делятся на макроопределения (macro definition или просто macro) и макровызовы (macro instruction или macrocode).

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

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

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

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

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

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

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

В дистрибутивном пакете MASM 6.0 имеется каталог INCLUDE, в котором собраны файлы с макроопределениями различного назначения. Например, файлы bios.inc и dos.inc содержат макроопределения запросов функций BIOS и DOS, соответственно, а файл macros.inc содержит макросы, выполняющие различные вспомогательные действия, в том числе и обработку строк текста. В том же каталоге находятся файлы с другими макроопределениями.

В дистрибутивном пакете MASM32 имеется каталог MACROS, в котором находятся файлы usemacro.inc и macros.asm, содержащие макроопределениями запросов функций, выполняемых 32-х разрядными операционными системами семейства WINDOWS. Если запросы функций BIOS и DOS программируются сравнительно просто, то программировать запросы системных функций WIDOWS, без использования готовых макроопределений просто нецелесообразно.

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

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

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

2. МАКРОСЫ ДЛЯ РАБОТЫ СО СТЕКОМ

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

Исходный вариант макроопределений, выполняющих сохранение в стеке и восстановление из стека параметров команд общего назначения, приведен в примере 1. Тексты макросов различаются только одной строкой. У PushArg в ней записана заготовка команды push par, а у PopArg – заготовка команды pop par.

Пример 1. Макроопределения PushArg и PopArg

    ; Сохранение аргументов в стеке
    PushArg macro arg ; заголовок макроопределения
     local par ; описание локальной переменной par
    ; цикл формирования последовательности команд push arg[i]
     irp  par,  ; par = arg[i] – имя очередного аргумента
     push par ; push arg[i] – заготовка формируемой команды push
     endm  ; конец цикла (действия директивы irp)
    endm   ; конец макроопределения
    ; Выборка аргументов из стека
    PopArg macro arg ; заголовок макроопределения
     local par ; описание локальной переменной par
    ; цикл формирования последовательности команд pop arg[i]
     irp par,  ; par = arg[i] – имя очередного аргумента
     pop par ; pop arg[i] – заготовка формируемой команды pop
     endm  ; конец цикла (действия директивы irp)
    endm   ; конец макроопределения

Описание макроопределения открывает директива MACRO, в данном случае после нее указан один параметр arg, но он является списком имен операндов команд push или pop. Завершает макроопределение директива ENDM, кроме того, она используется для указания конца директив IRP.

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

Директива IRP является заголовком тела цикла, а завершает его директива ENDM. При каждом повторе цикла символьной переменной par присваивается имя очередного операнда из списка arg. При отсутствии ошибок цикл повторяется до окончания списка. Запись означает, что arg является параметром макроопределения.

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

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

Пример 2. Варианты макровызовов определений примера 1

     PushArg   ; сохранение содержимого регистров
     PopArg    ; восстановление содержимого регистров

Несколько замечаний относительно списка параметров:

  • работа со стеком организована по принципу "первым пришел, последним ушел", поэтому для восстановления сохраненных значений параметров их имена должны располагаться в списках PushArg и PopArg в противоположном порядке;
  • это не мешает вам при выталкивании из стека сознательно изменить последовательность параметров или указать другие имена;
  • в списках можно указывать имена любых операндов, использование которых допускают команды push и pop, а не только регистров общего назначения, чтобы подчеркнуть это обстоятельство, макросы имеют имена PushArg и PopArg;
  • для сохранения содержимого сразу всех регистров общего назначения, лучше использовать не макросы, а команды pusha и popa, это сократит не только исходный текст, но и объектный модуль и несколько ускорит выполнение задачи.

Изменения макроопределений. Текст примера 1 достаточно прост и вполне корректен, тем не менее, в него можно внести некоторые полезные изменения.

Изменение 1. Директива irp морально устарела, начиная с MASM 6.0, появилась ее более привычная запись FOR <тело цикла> ENDM. Директива for обладает некоторыми преимуществами перед irp, но в данном случае их действия одинаковы.

Для использования новой директивы в обоих вариантах примера 1 замените строку irp par, <arg> на строку for par, . Логика выполнения макроопределений при такой замене не изменяется.

Примечание. Существуют директивы irpc и forc. Они отличаются от irp и for тем, что при каждом повторе цикла из строки выбирается _только один символ_.

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

Параметры макроопределений могут иметь атрибуты. В данном случае нас интересует только один из них, а именно VARARG. Он указывает на то, что параметр является списком имен, отделенных друг от друга запятыми. Для использования этого атрибута первые строки обоих вариантов примера 1 надо записать так:

    PushArg macro arg:vararg ; заголовок макроопределения PushArg
    PopArg macro arg:vararg ; заголовок макроопределения PopArg

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

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

Изменение 3. Можно так изменить текст макроопределения PushArg, что при вызове PopArg не потребуется указание списка операндов. Для этого надо запомнить имена сохраненных в стеке операндов, а затем использовать их при восстановлении из стека.

Полный текст соответствующего примера находится в файле macros.inc в каталоге Include дистрибутивного пакета MASM версии 6.0. Он состоит из трех макроопределений @SaveRegs, @RestoreRegs, @ArgRev и описания глобальной текстовой переменной pregs (имена взяты из файла macros.inc).

Макроопределение @SaveRegs отличается от PushArg только тем, что перед формированием команд push список фактических параметров, макровызова копируется в переменную pregs с помощью строковой директивы CATSTR (см ниже).

Макроопределение @RestoreRegs вызывается без параметров. При его выполнении используется вспомогательное макроопределение @ArgRev. Оно инвертирует список параметров, сохраненный в переменной pregs, и передает его макросу @RestoreRegs, для формирования группы команд pop. Способ инверсии списка параметров показан в примере 6.

Работа с новыми регистрами. У микропроцессоров семейства Pentium появилось сначала восемь 64-х разрядных регистров, имеющих имена от MM0 до MM7, а затем еще восемь 128-ми разрядных регистров с именами от XMM0 до XMM7. Они доступны только новым инструкциям групп MMX, SSE1 и SSE2. Инструкциям общего назначения, к которым относятся push и pop, новые регистры недоступны. Поэтому при работе со стеком пересылка данных выполняется с помощью новых инструкций.

В примере 3 приведены макроопределения для сохранения и восстановления содержимого регистров mmx. Вместо push и pop в текст программы вставляются две команды. Одна корректирует указатель стека на размер операнда (8 байтов), а другая выполняет пересылку из регистра в стек или в обратном направлении. Для пересылки 64-х разрядных операндов, вместо команды mov используется MOVQ из группы mmx.

Пример 3. Макроопределения PushMmx и PopMmx

    ; Сохранение в стеке содержимого регистров mmx
    PushMmx macro regs64:vararg ; заголовок макроопределения
    ; цикл формирования последовательности команд
     for par,  ; par=regs64[i] очередное имя регистра
     sub esp, 8 ; команда коррекции указателя стека
     movq [esp], par ; заготовка команды пересылки в стек
     endm  ; конец действия директивы for
    endm   ; конец макроопределения
    : Восстановление из стека содержимого регистров mmx
    PopMmx macro regs64:vararg ; заголовок макроопределения
    ; цикл формирования последовательности команд
     for par,  ; par=regs64[i] очередное имя регистра
     movq par, [esp] ; заготовка команды пересылки из стека
     add esp, 8 ; команда коррекции указателя стека
     endm  ; конец действия директивы for
    endm   ; конец макроопределения

В примере 3 циклы организованы с помощью директивы for, а список параметров специфицирован как vararg, для исключения угловых скобок, ограничивающих список операндов макровызова. Вариант макровызовов показан в примере 4.

Пример 4. Вызовы макроопределений примера 3

     PushMmx  mm0, mm1, mm2 ; сохранение содержимого трех регистров
     PopMmx   mm2, mm1, mm0 ; восстановление содержимого трех регистров

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

128-ми разрядные регистры. При сохранении или восстановлении содержимого 128-ми разрядных регистров xmm указатель стека должен изменяться на 16 байтов, а для пересылки данных используется команда MOVUPS. Она входит в группу инструкций SSE1 и пересылает невыровненные данные, расположенные в памяти начиная с любого адреса. Для получения новых макроопределений надо переименовать макросы примера 3, а тела циклов для сохранения и выборки записать так:

    ; при сохранении в стеке содержимого регистров xmm
     sub esp, 16 ; команда коррекции указателя стека
      movups [esp], par ; заготовка команды пересылки в стек
    ;  при восстановлении из стека содержимого регистров xmm
      movups par, [esp]  ; заготовка команды пересылки из стека
      add esp, 16 ; команда коррекции указателя стека

MASM 6.0 не компилирует новые инструкции микропроцессоров Pentium. Поэтому при работе с макросами, приведенными в примерах 3 и 4, надо либо расширять возможности MASM 6.0 с помощью макроопределений, выполняющих компиляцию новых инструкций, либо использовать версию MASM32 6.14.

Стек числовых регистров. В состав всех микропроцессоров семейства Pentium входит специализированный процессор, выполняющий вычисления с вещественными числами (FPU). Исполняемые им инструкции работают со специальными регистрами, сгруппированными в стек и имеющими имена от st(0) до st(7). Регистр st(0) или просто st находится в верхушке стека и обязательно участвует во всех операциях. Числовые регистры недоступны командам push и pop, поэтому для обмена данными со стеком общего назначения используются инструкции FPU.

Копирование в память содержимого верхушки числового стека, и освобождение числового регистра (перемещение верхушки) выполняет инструкция fstp. Обратное действие, т. е. выделение нового регистра (перемещение верхушки числового стека) и запись в него содержимого памяти выполняет инструкция fld. В зависимости от размера операнда обе инструкции пересылают 32-х или 64-х разрядные вещественные числа (обычная или двойная точность).

В примере 5 приведены макроопределения для сохранения и восстановления заданного количества числовых регистров (от 1 до 8).

Пример 5. Макроопределения PushFlt и PopFtt

    ;   Сохранение в стеке содержимого числовых регистров
    PushFltr macro num ; заголовок макроопределения
     repeat num ; начало цикла формирования команд
     sub  esp, 8 ; команда коррекции указателя стека
     fstp  qword ptr [esp] ; команда копирования содержимого st в стек
     endm ; конец директивы repeat
    endm  ; конец макроопределения
    ;   Восстановление из стека содержимого числовых регистров
    PopFltr macro num ; заголовок макроопределения
     repeat num ; начало цикла формирования команд
     fld  qword ptr [esp] ; команда копирования из стека в регистр st
     add  esp, 8 ; команда коррекции указателя стека
     endm ; конец директивы repeat
    endm  ; конец макроопределения

В данном случае невозможно указать список имен числовых регистров, поэтому в примере 5 для организации циклов формирования команд использована директива REPEAT. При каждом выполнении цикла исходное значение num уменьшается на 1, если результат отличен от 0, то цикл повторяется.

Тело цикла состоит из двух команд и заканчивается директивой ENDM. В командах fstp и fld явно указывается размер операнда (qword ptr). В отличие от предыдущих примеров, в данном случае используются готовые команды, не требующие настройки.

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

Инверсия списка параметров. В некоторых случаях может возникнуть необходимость перед записью в стек изменить порядок параметров на противоположный тому, в котором они перечислены в списке макровызова. Например, при компиляции директивы INVOKE, MASM анализирует язык, на котором составлена основная программа (Си, Бейсик, Фортран или Паскаль) и, в зависимости от этого, выбирает прямой или обратный способ записи в стек параметров подпрограммы.

Список параметров является строкой текста, а для манипуляций со строками в MASM 6.0 были введены следующие четыре директивы:

  • CatStr – объединение строк;
  • InStr – определение позиции символа в строке;
  • SizeStr – определение количества символов в строке;
  • SubStr – выделение подстроки.

Их можно использовать и как операторы в символьных выражениях, в таком случае перед именем указывается символ @ – @CatStr, @InStr, @SizeStr, @SubStr.

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

Пример 6. Инверсия списка параметров

    InvList MACRO arglist ; заголовок макроопределения
     LOCAL txt, arg ; описание локальных переменных
     txt TEXTEQU <> ; очистка текстовой переменной
    % FOR arg, arglist ; начало цикла перестановки параметров
     txt CATSTR , , txt; запись в txt очередного параметра и запятой
     ENDM ; конец цикла перестановки параметров
     txt SUBSTR  txt, 1, @SizeStr( %txt ) - 1; удаление последней запятой в txt
     txt CATSTR  , txt, > ; окружение содержимого txt угловыми скобками
     EXITM txt ; завершение выполнения с передачей имени txt
    ENDM  ; конец макроопределения

Обратите внимание на то, что в тексте примера 6 нет ни одной ассемблерной команды, следовательно, при его вызове текст программы не изменяется. На основе указанного при вызове списка параметров, макроопределение примера 6 формирует в переменной txt инвертированный список. Предположим, что в качестве параметра arglist передается следующий текст: <eax,ebx,ecx,edx>, после выполнения макроса переменная txt будет содержать инвертированный список, а именно: <edx,ecx,ebx,eax>.

В исходе строка txt пустая. При каждом повторе цикла перестановки строковая директива CATSTR добавляет в ее начало имя очередного элемента списка arglist и запятую после него. После окончания цикла перестановок в конце строки txt окажется лишняя запятая, которую надо исключить.

После выхода из цикла директива SUBSTR укорачивает размер строки txt на один символ и таким способом исключает последнюю запятую. В списке этой директивы использован оператор @SizeStr, который возвращает исходный размер строки (количество символов в строке).

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

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

Пример 7. Запись параметров в стек в обратном порядке

    PushArg MACRO argum:VARARG ; заголовок макроопределения
     local prm ; описание локальной переменной
    % FOR prm, InvList (<>); заголовок цикла с вызовом InvList
     push prm ; заготовка формируемой команды
     ENDM ; конец действия директивы for
    ENDM  ; конец макроопределения

Пусть вас не смущает обилие скобок при вызове InvList. Круглые скобки ограничивают список параметров, в данном случае он состоит из одного элемента. Две угловые скобки нужны потому, что argum является параметром внешнего макроопределения (PushArg), который специфицирован как vararg.

Примечание. В примерах 6 и 7 перед директивой for указан знак процента (%). Обычно он означает, что текст интерпретируется как выражение.

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

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

В следующем выпуске - очередная порция бреда от Serrgio и продолжение статьи про макросы ;)