Процедуры в программах ассемблера

Если бы строители строили здания так же, как
программисты пишут программы, первый же
залетевший дятел разрушил бы цивилизацию.
Второй закон Вейнберга (Прикладная Мерфология)

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

Реализация рекурсивных процедур

Принимаясь за дело, соберись с духом.
Козьма Прутков

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

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

Подробно эти вопросы обсуждались в уроке 14 «Модульное программирование» учебника. Но при организации рекурсии они приобретают особый смысл, так как требуется не просто вызвать процедуру, а вызвать ее из себя несколько раз, обрабатывая при каждом вызове свои локальные данные, и в конечном итоге возвратить управление в точку программы, расположенную следом за первым вызовом данной процедуры.
Как правило, для работы с локальными данными процедуры и параметрами, передаваемыми в процедуру, используется стек. При этом необязательно использовать для этого только системный стек, иногда удобнее создать его модель, работу с которой осуществлять средствами самой программы. Пример организации такого программного стека мы использовали при написании программы обхода дерева с рекурсивной процедурой LRBeat.
Рассмотрим характерные моменты рекурсивного вызова процедур на примере классической рекурсивной задачи — вычисления факториала. Вспомним алгоритм вычисления факториала: F(0)=l: F(i)=ixF(i-1)
Как было отмечено выше, с точки зрения скорости работы кода рекурсивный вариант вычисления факториала неэффективен, его лучше вычислять итеративно в цикле, но в учебных целях этот пример оправдан.

.data
r_fact dw 0
.code
fact proc
push bp nrav bp.sp mov cx.[bp+4] mov ax.ex mul r_fact mov r_fact.ax dec ex jcxz end_p
push ex call fact
end_p: mov sp.bp

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

fact ргос
push bp mov bp.sp mov cx.[bp+4]

Смысл этого фрагмента легче понять, наблюдая поведение программы вычисления факториала в отладчике. Как сказано выше, перед вызовом процедуры в стек помещаются данные (или указатель на них), информация о местонахождении которых должна быть сохранена в интересах как вызывающей, так и вызываемой процедуры. В нашем случае в процедуру fact передается переменная факториала. После этого производится вызов процедуры, в результате чего в стек помещается адрес возврата. В вызванной процедуре к данным переменным необходимо получить доступ. Для этого предназначен регистр ВР. Перед использованием его содержимое должно быть также сохранено в стеке. Для первого вызова его значение несущественно. В этот момент весь предыдущий контекст работы программы сохранен. Команда mov bp.sp загружает в регистр ВР указатель на текущую вершину стека, после чего можно обращаться к данным, переданным в процедуру. По сути, сейчас мы с вами сформировали кадр стека. Следующий рекурсивный вызов этой функции придает действию сохранения регистра ВР особый смысл. Команда push bp сохраняет в стеке адрес кадра стека для предыдущего вызова рекурсивной процедуры. Теперь для выхода из процедуры достаточно выполнить приведенные ниже команды эпилога, позволяющие корректно отработать обратную цепочку возврата в основную программу: будет рассмотрена в этой главе ниже. Далее сравним работу функций DrawPattern i и DrawPattern 1.
Вызов функции DrawPattern_1 из основной программы осуществляется следующим фрагментом кода (полный текст приведен в ПРИМЕРе в каталоге программ для данной главы).

:prg3_1.asm - фрагмент оконного приложения, вызывающего рекурсивную процедуру :DrawPattern_l
объявление пользовательских процедур (из maket_dll.DLL) extrn DrawPattern_l:PROC extrn DrawPattem_2:PR0C
.data
определение констант для фигуры "Узор из окружностей"
р dd 5 ;порядок узора
г dd 60 :радиус окружности
y_Pdd 140 начальная у-координата центра окружности
х_Р dd 200 начальная х-координата центра окружности
.code
обработка сообщений от меню
MenuProc proc
arg (a@hwnd: DWORD. №wparam: DWORD, @(ahdc: DWORD.@@hbrush: DWORD
uses eax.ebx
mov ebx.@@wparam :в Ьх идентификатор меню
onpbx.IDMJ)LL_LACESJ je @@idmdlllaces_l cmpbx.IDM_DLLJ_ACES_2 je @@idmdlllaces_2 jmp@@exit
e@1 chndl 11 aces_l:
;рисуем узор из окружностей, рекурсивная функция для рисования находится
;в DLL-библиотеке:
;DrawPattern_l(hwnd.hdc,x.y.r.p) - функция не работает с локальными переменными:
push p :порядок узора
push г :радиус окружности
push y_P :у-координата центра окружности
push x_P ;х-координата центра окружности
push memdc :контекст устройства
push @@hwnd
call DrawPattern_l
jmp@@exit :.........
Фрагмент файла maket_dll.DLL, содержащий процедуру DrawPattern_l, приведен ниже:
iinaket_dn.DLL - фрагмент DLL-библиотеки, содержащей рекурсивную процедуру DrawPatternJ
объявление процедур DLL-библиотеки общедоступными publicdll WriteCon publicdll DrawPatternJ publicdll DrawPattern_2
.code DrawPatternJ proc
:DrawPattern_l - рекурсивная процедура рисования узора :(без использования локальных переменных)
arg @@hwnd:dword.@@hdc:dword.@@x:dword.@@y:dword.@@r:dword.@@p:dword
:рисуем окружность
:рекурсивно вызываем DrawPattern_l(hwnd.hdc,x.y.r,p)
:BOOL Ellipse(HDC hdc. int nLeftRect. int nTopRect. int nRightRect.int nBottomRect):
:готовим параметры в стеке для вызова Ellipse
call Ellipse:рисуем окружность :и еще четыре меньшего порядка
dec @@p
стр @@р, 0
je @@End_Draw
shr@@r,l -.делим на 2
:готовим параметры в стеке для вызова DrawPatternJ
call DrawPattern_l :готовим параметры в стеке для вызова DrawPattern_l
call DrawPatternJ :готовим параметры в стеке для вызова DrawPatternJ
call DrawPattern_l :готовим параметры в стеке для вызова DrawPatternJ.
call DrawPatternJ
@@End_Draw:
генерация сообщения WM_PAINT для вывода изображения на экран
call InvalidateRect endp DrawPatternJ

Такой вариант процедуры не требует внимания к параметрам, которые пере-, даются в стеке при вызове рекурсивной процедуры, так как после возврата из [ нее они попросту не нужны и удаляются из стека. Но стоит нам в процедуре DrawPattern изменить порядок обращения к процедуре Ellipse, как ситуация резко меняется. Рассмотрим второй вариант организации процедуры DrawPattern.
VOID DrawPattern (HWND hwnd.HDC hdc.INT_DWORD x.INT_DWORD y.INTJMRD r,INT_DWORD p)

//DrawPattern - рекурсивная процедура DrawPatten (вариант 2) вывода на экран узора
//из окружностей на псевдоязыке (фрагмент)
//Вход: х и у - координаты центра окружности; г - радиус окружности:
//р - порядок узора, hwnd - дескриптор окна. HDC - контекст устройства.
ПЕРЕМЕННЫЕ
HWND hwnd: HDC hdc;
INT_DWORD hdc. x. y. r.p
НАЧ_ПРОГ
ЕСЛИ (р) ТО //пока р*0
НАЧ_БЛОК_1
//рисуем еще четыре окружности по с центрами по краям этой DrawPattern (hwnd. hdc. х-г. у. г, р-1) DrawPattern (hwnd. hdc. х. у-г. г. р-1) DrawPattern (hwnd. hdc. х+г. у, г, р-1) DrawPattern (hwnd. hdc. х. у+г. г. р-1)
//Ellipse - функция Win32 API для вывода эллипса (окружности), вписанного //в прямоугольник (квадрат) с координатами правого верхнего угла (x_up. y_up) //и левого нижнего угла (x_low. y_low):
//Ellipse(HDC hdc. INT_DW0RD x_up. INT_DWORD y_up. INTJMJRD x_low. INT_DWORD yjow) //так как для рисования нужны координаты прямоугольника, а не центра окружности, //то преобразуем их при вызове Ellipse: .
Ellipsethdc. x_up-r. y_up-r. x_low+r, y_low+r)
КОН_БЛОК_1 КОН_ПРОГ

Если в первом варианте процедуры DrawPattern — DrawPatternl окружности рисовались перед очередной рекурсивной передачей управления в процедуру DrawPattern, то во втором варианте это делается в последнюю очередь — во время обратного хода по цепочке вызовов процедуры DrawPattern. Это уже требует наличия локальных переменных в процедуре и их сохранения на период пока осуществляются рекурсивные вызовы процедуры DrawPattern. Приведем соответствующие фрагменты основной программы и функции DrawPattern_2 из DLL-библиотеки maket dll.DLL
Из этого фрагмента хорошо видно, в чем разница между размещением параметров, передаваемых в рекурсивную процедуру, и локальными переменными этой процедуры. Для доступа к параметрам используются положительные смещения относительно адреса в ВР (это скрыто от нас с помощью директивы ARG), а для доступа к локальным параметрам — отрицательные смещения.
Разница в изображениях возникла из-за разных мест в программе, где вызывается функция InvalidateRect. Попробуйте самостоятельно исправить этот «дефект».

Реализация вложенных процедур

Понятие вложенной процедуры включает в себя возможность описания процедур внутри друг друга, при этом каждая из процедур может иметь локальные данные, видимые для вложенных в нее процедур, но не видимые для процедур, находящихся на одном уровне вложенности с данной процедурой. Для организации такой вложенности существуют две возможности — организация средствами ассемблера и определенная самим пользователем. Рассмотрим первую из них. Для нее требуются команды ассемблера ENTER и LEAVE. Их формат приведен ниже.
Команда enter loc_size.lexjev — ENTER (setup parameter block for ENTERing procedure) — реализует установку кадра стека для параметров процедуры. Работа команды заключается в следующем.

  1. Размещение текущего значения регистра ЕВР/ВР в стеке.
  2. Сохранение текущего значения ESP/SP в промежуточной переменной FP (имя переменной выбрано случайно).
  3. Если лексический уровень вложенности (операнд lexlev) не равен нулю, то (1ex_1ev-l) сделать следующее:
    1. в зависимости от установленного режима адресации usel6 или use32
    2. выполнить вычитание (ВР-2) или (ЕВР-4) и записать результат обратно в ЕВР/ВР;
    3. сохранить значение ЕВР/ВР в стеке;
    4. сохранить в стеке значение промежуточной переменной fp.
  4. Запись значения промежуточной переменной fp в регистр ЕВР/ВР.
  5. Уменьшение значения регистра ESP/SP на величину, заданную первым операндом, минус размер области локальных переменных locsize: ESP/SP= (ESP/SP)-loc size.

Команда LEAVE (LEAVE from procedure — выход из процедуры) не имеет операндов и выполняет удаление из стека области локальных (динамических) переменных, выделенной командой ENTER. Команда выполняет обратные команде ENTER действия.

  1. Содержимое ebp/bp копируется в ESP/SP, тем самым восстанавливается значение ESP/SP, которое было до вызова данной процедуры. С другой стороны, восстановление старого значения ESP/SP означает освобождение пространства в стеке, отведенного для завершающейся процедуры (локальные переменные процедуры уничтожаются).
  2. Из стека восстанавливается содержимое ЕВР/ВР, которое было до входа в процедуру. После этого действия значение ESP/SP также становится таким, каким оно было до входа в процедуру.

В результате этих двух действий также восстанавливается кадр стека, если он был, вызывающей программы.
Команды ENTER и LEAVE специально введены в систему команд микропроцессора для поддержки вложенных процедур, как это делают для блочно-структури-рованных языков высокого уровня типа Паскаль или С. В этих языках программа разбивается на блоки. В блоках можно описать свои собственные (локальные) идентификаторы, которые не могут быть использованы вне этого блока. К примеру, на рис. 3.1 в виде блоков изображена структура некоторой программы.

Рис. 3.1. Изображение структуры некоторой программы в виде блоков

В правом верхнем углу каждого блока (процедуры) стоит номер лексического уровня вложенности этого блока относительно других блоков программы. Большинство блочно-структурированных языков в качестве основного метода распределения памяти для переменных в блоках используют автоматическое распределение памяти. Это означает, что при входе в блок (вызове процедуры и т. п.) в некотором месте оперативной памяти (или в стеке) выделяется область памяти для переменных этого блока (ее можно назвать областью инициализации). После выхода из этого блока связь программы с этой областью теряется, то есть эти переменные становятся недоступными. Но если, как в нашем примере, в этой Процедуре есть вложенные блоки (процедуры), то для некоторого внутреннего блока (например, С) могут быть доступны области инициализации (переменные) блоков, объемлющих данный блок. В нашем примере для блока С доступны также переменные блоков В и А, но не D. Возникает вопрос: как же программа, находясь в конкретной точке своего выполнения, может отслеживать то, какие области инициализации ей доступны? Это делается с помощью структуры данных называемой дисплеем. Дисплей содержит указатели на самую последнюю область текущего блока и на области инициализации всех блоков, объемлющих данный блок в программе. Например, если в программе А была вызвана сначала процедура В, а затем С, то дисплей содержит указатели на области инициализации А В и С (рис. 3.2).

Рис. 3.2. Соответствие содержимого дисплея области инициализации после вызова процедур В и С

Если после этого вызвать процедуру D (в то время как В и С еще не завершены), то картина изменится (рис. 3.3).

Рис. 3.3. Соответствие содержимого дисплея области инициализации после вызова процедуры D

После того как некоторый блок (процедура) завершает свою работу, его область инициализации удаляется из памяти (стека) и одновременно соответствующим образом корректируется дисплей.
Большинство языков высокого уровня хранит локальные данные блоков в стеке. Эти переменные называют еще автоматическими, или динамическими. Память для них резервируется путем уменьшения значения регистра-указателя стека ESP/SP на величину, равную длине области, занимаемой этими динамическими переменными. Доступ к этим переменным осуществляется посредством регистра ЕВР/ВР. Если один блок вложен в другой, то для его динамических (локальных) переменных также выделяется место (кадр) в стеке, но в этот кадр помещается указатель на кадр стека для включающего его блока. Команды ENTER И LEAVE как раз и позволяют поддержать в языке ассемблера принципы работы с переменными блоков, как в блочно-структурированных языках. Дисплей организуется с помощью второго операнда команды ENTER и стека.
Например, в начале работы главной процедуры А и после вызова процедуры В кадр стека будет выглядеть так, как показано на рис. 3.4.

Рис. 3.4. Кадр стека после вызова процедур А и В

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

Разработка динамических (DLL) библиотек

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

Динамические подключаемые библиотеки (Dynamic Link Libraries, DLLs) являются хранилищем общедоступных процедур. Механизм DLL-библиотек появился вместе с операционной системой Windows и является ее неотъемлемой частью. Суть этого механизма в том, что в процессе компоновки исполняемого модуля с использованием внешних процедур в него помещаются не сами процедуры, а только их названия (номера) вместе с названиями DLL-библиотек, в которых они содержится. В уроке 14 «Модульное программирование» учебника для связи модулей на разных языках рассматривались стандартные соглашения по передаче параметров, которые специфическим образом реализовывались на уровне конкретных компиляторов языков программирования. Этот механизм был, пожалуй, единственным средством связи разноязыковых модулей при программировании для MS DOS. В среде Windows более естественным является механизм DLL-библиотек. Он позволяет, в частности, разработать набор процедур на ассемблере и затем использовать их в программах на языках высокого уровня, поддерживающих механизм динамического связывания.
Как правило, если язык программирования поддерживает разработку Windows-приложений, то он имеет средства для разработки и использования DLL-библиотек. Ассемблер не является исключением. Общие принципы разработки DLL-библиотек для всех языков одинаковы, так как эти библиотеки являются универсальным механизмом, не зависящим от конкретного языка. Поэтому, разрабатывая DLL-библиотеку, необходимо учитывать общие требования к таким библиотекам. Структурно DLL-библиотека представляет собой обычную программу, включающую некоторые специфические элементы. Рассмотрим процесс создания и использования DLL-библиотеки на языке ассемблера. Для этого разработаем консольное приложение, которое выводит некоторую строку на экран 10 раз. На каждой итерации вывода меняются атрибуты этой строки. За основу взята программа prg05_ll.asm из главы 5. Только теперь строка с выводимым сообщением находится в приложении, а сама процедура вывода — в DLL-библиотеке. Для демонстрации передачи и возврата параметров в процедуру передаются длина и адрес строки, а возвращаются значения Offffffffh в четыре регистрах ЕАХ, ЕВХ, ЕСХ, EDX. Обсудим процесс по шагам.

Шаг 1. Разработка текста DLL-библиотеки

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

;maket_dll.asm - текст DLL-библиотеки. :Содержит одну функцию - WriteCon
locals
.model flat.STDCALL ;модель памяти flat.
Объявление внешними используемых в данной программе функций Win32 (ASCII):
:обьявление процедуры WriteCon общедоступной publicdll WriteCon
.data
.code
DllMainproc
arg №h I nst: dword. @@event: dword. @<ano_use: dword
@@m: moveax.l
ret
DllMainendp
WriteCon ргос :см. дискету и prg05_ll.asm из главы 5 arg@@adr_str:dword.@@len_str:dword
ret
endp WriteCon endDllMain

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

  • Во-первых, этот код должен быть рассчитан на то, что он получает управление в одном из четырех случаев. О наступлении каждого из этих случаевоперационная система извещает DLL-библиотеку путем передачи ей одного из четырех предопределенных значений — флагов. Значения этих флагов перечислены в файле winnt.h. Рассмотрим эти флаги и возможные действия при их поступлении в DLL-библиотеку.
  • DLLPR0CESSATTACH-1 — передается операционной системой DLL-библиотеке при проецировании последней в адресное пространство процесса. Передача этого флага DLL-библиотеке производится всего один раз, обычно при загрузке приложения, использующего данную DLL-библиотеку. Если позже другой поток процесса попытается загрузить эту же библиотеку, то система попросту увеличит ее счетчик использования без посылки флага DLLPROCESSATTACH. Получив данный флаг, DLL-библиотека должна выполнить действия по созданию необходимой среды функционирования для своих функций. Например, обеспечить их кучей.
  • DLL_THREAD_ATTACH=2 — передается операционной системой DLL-библиотеке при создании нового потока в процессе. Этим DLL-библиотеке предоставляется возможность нужным образом обработать факт создания нового потока. Следует иметь в виду, что этот процесс не является обратимым, то есть если DLL-библиотека загружается в процесс, когда в нем уже функционируют потоки, то ни одному из них не посылается флаг DLL_THREAD_ATTACH.
    # DLL_THREAD_DETACH=3 — передается операционной системой DLL-библиотеке при выгрузке потоком DLL-библиотеки.
    # DLL_PROCESS_DETACH=0 — передается операционной системой DLL-библиотеке при выгрузке DLL-библиотеки из адресного пространства процесса. Логично, что при этом требуется провести завершающие действия по освобождению всех ресурсов, которыми владеет DLL-библиотека. Обычно эти действия являются обратными по отношению к предпринятым при инициализации библиотеки (см. флаг DLLPROCESSATTACH).
    Во-вторых, имя точки входа DLL-библиотеки может быть любым. Главное, чтобы при наличии кода инициализации это имя было указано в директиве END.
    В-третьих, оформление кода инициализации в виде отдельной процедуры необязательно. Главное, выполнить два основных действия кода инициализации DLL-библиотеки (при его наличии):
  • # вернуть единицу в регистре ЕАХ;
    # удалить из стека три параметра, которые передаются DLL-библиотеке при передаче описанных выше флагов: hlnstDLL — дескриптор DLL-библиотеки, назначенный ей системой при ее загрузке в адресное пространство процесса;
  • vent — значение флага, передаваемого в DLL-библиотеку; f ImpLoad — параметр не равен 0, если библиотека загружена неявно (см. ниже), и равен 0 в обратном случае.

Структура полного варианта инициализациониого кода выглядит так:

includeWindowConA.inc;проверьте присутствие значений флагов в этом файле"
DllMain ргос
arg hlnstDLL:dword. event:dword,fImpLoad:dword
cmp [event].DLL_PROCESS_ATTACH
jne m выполняем действия для DLL_PROCESS_ATTACH
cmp [event].DLL_THREAD_ATTACH
jnem :выполняем действия для DLL_THREAD_ATTACH
cmp [event]. DLL_THREAD_DETACH
jnem выполняем действия для DLL_THREAD_DETACH
cmp [event].DLL_PROCESS_DETACH
jnem
выполняем действия для DLL_PROCESS_DETACH m: moveax.l
ret DllMainendp

Минимальный вариант может выглядеть так, как это сделано в нашем примере:

DllMain ргос
arg hlnstDLL:dword. event:dword,fImpLoad:dword
m: mov eax.l
ret DllMainendp

Или так:

DllMain: m: moveax.l ret 12

He забывайте, что директива arg приводит к тому, что в код, генерируемый транслятором, вставляются команды ENTERD и LEAVED (см. выше разделы «Реализация рекурсивных процедур» и «Реализация вложенных процедур»). Кроме этого, команда RET процедуры дополняется значением, равным сумме длин параметров, указанных в директиве ARG . Исполнение такой команды приводит к удалению из стека количества байт, равного этому сформированному значению.
Что касается кода функций (процедур), составляющих DLL-библиотеку, то для их написания используются обычные правила разработки программ. Описание данных также ничем не отличается от обычной программы ассемблера. Ведь в конечном итоге код и данные процедур DLL-библиотеки оказываются в адресном пространстве процесса наравне с его кодом и данными.
Последнее, что необходимо отметить, — все экземпляры данных и имена процедур, которые должны быть видны вне пределов DLL-библиотеки, объявляются общими с использованием одной из директив PUBLIC или PUBLICDLL.

Шаг 2. Трансляция и компоновка исходного текста DLL-библиотеки

После того как подготовлен исходный текст библиотеки, его транслируют обычным для программ ассемблера образом. Что же касается компоновки, то необходимо помнить, что ее целью является получение файла с расширением .dll, а не обычного файла с расширением .ехе. Весь этот процесс удобно обсуждать на примере реального файла makefile, текст которого приведен ниже:

TASM0PT=/m3 /mx /z /q /DWINVER=0400 /D_WIN32_WINNT=0400
!1f $d(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
lelse
TASMDEBUG=/1
LINKDEBUG=
lendif
!if Sd(MAKEDIR)
IMP0RT=import32
lelse
IMP0RT=import32
lendif
${NAME).EXE: $(OBJS) $(DEF)
t1ink32 /Tpd /aa /c $(LINKDEBUG) $(OBJS).$(NAME).. S(IMPORT). $(DEF) .asm.obj:
tasm32 KTASMDEBUG) S(TASMOPT) $&.asm

Запуск данного файла производится командной строкой:

make -DOEBUG -fmakefile_dll.mak >p.txt

В результате формируется несколько файлов, перечень которых определяется тем, насколько успешно отработали программы транслятора tasm32 и компоновщика nk.32. Для быстрой оценки этого результата мы перенаправили весь вывод в файл p.txt Просмотрев этот файл, можно оценить успешность создания DLL-библиотеки, не анализируя другие файлы (например, файл листинга). При наличии синтаксических ошибок необходимо исправить их и повторить запуск make-файла на исполнение.
Для успешной компоновки необходим еще один файл — с расширением .def. Необходимое и достаточное содержимое файла maket_dll.def приведено ниже:

LIBRARY maketjll DESCRIPTION 'Win32 DLL' EXPORTS WriteCon @1

В этом файле следует обратить внимание на директиву EXPORTS, которая содержит имена экспортируемых функций DLL-библиотеки и их ординалы, то есть порядковые номера этих функций в DLL-библиотеке. Последние использовались в 16-разрядных версиях Windows, однако в современных версиях этой операционной системы их использование необязательно, и Microsoft настоятельно рекомендует этого не делать.
О том, что компоновщик должен создать именно DLL-библиотеку, указывают с помощью ключа /Tpd.

Шаг 3. Создание lib-файла

Как указать приложению местонахождение внешних функций, расположенных в DLL-библиотеках? Если бы приложение использовало только одну DLL-библиотеку, то проблем бы не было — указывай нужную и продолжай процесс сборки приложения. Если количество необходимых приложению DLL-библиотек больше одной, а тем более если их десятки, то ситуация требует иного решения, нежели простое перечисление нужных приложению DLL-библиотек. Для централизованного хранения информации о размещении используемых приложением функций в DLL-библиотеках применяют LIB-файлы. Эти файлы представляют собой своеобразный справочник о размещении функций в DLL-библиотеках. При этом не указывается никаких путей, так как при обращении к DLL-библиотеке операционная система ищет ее по следующему алгоритму.

  1. В каталоге, содержащем ехе-файл приложения.
  2. В текущем каталоге процесса.
  3. В системном каталоге Windows.
  4. В основном каталоге Windows.
  5. В каталогах, указанных в переменной окружения PATH.

В пакете TASM для создания LIB-файла предназначена утилита Implib.exe. Для создания LIB-файла в нашем примере необходимо выполнить следующую командную строку:

IMPLIB.EXE maketjll .lib maket_dll.DLL >p.txt

Как видите, мы опять используем перенаправление вывода в файл p.txt для быстрой оценки результата работы программы IMPLIB.EXE. Если выполнение этой утилиты было успешным, то формируется файл maket_dll.lib, который в дальнейшем используется для сборки целевого приложения.

Шаг 4. Сборка приложения с использованием DLL-библиотеки

Приведем содержимое make-файла для сборки целевого приложения:

NAME = maket
OBJS = $(NAME).obj
DEF = $(NAME).def
lif Sd(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
'.else
TASMDEBUG=
LINKDEBUG=
lendif
TASMOPT=/m3 /z /q # /DWINVER=0400 /D_WIN32_WINNT-0400
# /mx
lif Sd(MAKEDIR)
IMPORT=$(MAKEDIR)\import32+maket_dll
lelse
IMPORT=import32+maket_dl1
lendif
$(NAME).EXE: $(OBJS) $(DEF)
tlink32 /Tpe /aa /x /c $(LINKDEBUG) $(OBJS).$(NAME).. $(IMPORT). $(DEF) .asm.obj:
del $(NAME).EXE
tasm32 $(TASMDEBUG) /ml $(TASMOPT) $&.asm...

Теперь, имея два make-файла (для сборки файлов .dll и .ехе ), можно провести сравнительный анализ их содержимого. Отметим два момента:
Ш в макропеременной IMPORT указываются имена (без расширений) LIB-фай-лов, содержащих сведения о нужных приложению функциях в DLL-библиотеках (если LIB-файлов несколько, то они перечисляются с использованием знака +);
ш для сборки ехе-приложения используется ключ компоновщика. Содержимое DEF-файла maket.def приложения:

NAME maket
DESCRIPTION 'Assembly Console Windows Program'
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE
EXPORTS

И наконец, содержимое самого файла maket.asm, использующего функцию из разработанной нами DLL-библиотеки maket_dll.dll.

: maket.asm - программа, вызывающая функцию WriteCon из файла maket_dll.dll
includelibmaket_dll .lib необязательно
.data
TitleText db "Строка выводится процедурой из DLL"
Lenjitl eText-$ - Ti tl eText
.code
start proc near ;точка входа в программу:
:работаем .........
push Len_TitleText
push offset TitleText
call WriteCon exit: ;выход из приложения

Импортируемую из DLL-библиотеки функцию необходимо объявить внешней директивой extrn WriteCon:PROC.

Шаг 5. Проверка работоспособности приложения с использованием DLL-библиотеки

Для проверки работоспособности полученного на предыдущем шаге приложения можно использовать отладчик TD32.EXE. Кстати, когда вы будете в нем работать, обратите внимание на то, как происходит переход из DLL-библиотеки на код в процедуре. Вы увидите, что помощь в этом оказывает неизвестно откуда появившаяся команда JMP. Причину этого вы можете выяснить, прочитав раздел «Секция описания импортируемых функций РЕ-файла» главы «Форматы исполняемых файлов» книги.
При разработке DLL-библиотек естественным образом возникает вопрос о совместимости с приложениями, разработанными на других языках. Это тем более актуально, если речь идет о продуктах разных фирм-производителей программного обеспечения. Проверить, насколько совместима разработанная нами с помощью средств TASM DLL-библиотека, можно с помощью утилиты DumpBin.exe из пакета Microsoft Visual Studio. Запустите ее командной строкой вида:

DUMPBIN.EXE -exports maketjJll.DLL>p.txt

Тогда в файле p.txt вы получите отчет о содержимом раздела экспорта DLL-библиотеки maket_dll.dll. Проанализировав полученные результаты, вы убедитесь, что проблем с распознаванием нашей DLL-библиотеки у этого программного средства фирмы Microsoft не возникло. Это дает основание полагать, что данную библиотеку при соответствующем наполнении полезными функциями можно использовать при программировании на VisualC/C++, VisualBasic и т. п. При этом необходимо иметь в виду, что может иметь место искажение имен функций при использовании компиляторов различных фирм. Подробнее об этом можно узнать в соответствующей литературе.
Не следует забывать, что на практике возможны три формы загрузки DLL-библиотеки в адресное пространство процесса: неявная, явная и отложенная. Описанный выше способ сборки приложения на самом деле был неявным и предполагал, что загрузка DLL-библиотеки производится при запуске самого приложения. Явный способ загрузки DLL-библиотеки предполагает ее загрузку во время работы приложения. Для этого в Win32 API существуют специальные функции:

HINSTANCE LoadLibraryC LPCTSTR lpLibFileName ):
HMODULE LoadLibraryExtLPCTSTR lpLibFileName,HANDLE hF1le, DWORD dwFlags):

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