Глава 13. Модульное программирование и структура программ.

Не в совокупности ищи един­ства, но более - в единообразии разделения.

Козьма Прутков.

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

I.

Рассмотрим вначале простой вариант - один основной модуль и один вспомога­тельный. Основным модулем мы будем называть тот, который получает управление после запуска программы. Два модуля представлены на Рис. 13.1 и Рис. 13.2 (основ­ной, или главный, получает управление после запуска программы). Оттранслируем оба модуля с помощью MASM .ехе. В результате на диске образуются два объектных модуля: PR1.OBJhPR2.OBJ, Теперь дело за редактором связей. В командной строке укажем: LINKPR2+PR1. После этого отвечайте на вопросы как обычно. При просьбе указать имя МАР-файла, укажите PR2 (впрочем, имя не имеет никакого значения). В результате компоновки образуется загрузочный модуль PR2.exeh PR2.MAP. Содер­жимое МАР-файла показано на Рис. 13.3. В этом файле содержатся сведения о сегмен­тах (начальный и конечный адрес, длина, имя, класс), скомпонованных в один модуль в том порядке, как они помещены в него (начиная с младших адресов). Обращаю Ваше внимание, что в начале идут сегменты модуля PR2, а затем сегменты модуля PR1. Именно так, как мы указали модули в командной строке. Если бы в командной строке было LLNKPR1+PR2, то порядок сегментов был бы обратный. При этом сначала запу­стился бы модуль PR 1, что привело бы к катастрофе. Итак, главный модуль должен быть первым, порядок остальных модулей уже нетак важен (см., однако, ниже).

Представленные на Рис. 13.1 и 13.2 модули обладают следующей особенностью: из модуля PR2 вызывается процедура, находящаяся в модуле PR1, откуда, в свою оче­редь, вызывается процедура, находящаяся в модуле PR2. В конечном итоге происхо­дит возврат в модуль PR2, и программа заканчивает свою

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

С помощью слова EXTRN отмечаются имена, определяемые в другихмодулях, -внешние имена. Определение внешних имен предполагает указание типа: для проце­дур и меток перехода указывается NEAR или FAR, для данных указывается BYTE, WORD или DWORD. Указаниетипанеобходимопотому, что имя определено в другом модуле.

1, программа DATA1 SEGMENT

TEXT DB 'Привет! Я в модуле 1.   «,13,10,'$';

DATA1 ENDS

PUBLIC WORK

EXTRN PRI:FAR LIB SEGMENT

ASSUME CS: LIB WORK PROC FAR

PUSH AX

PUSH DS

MOV    AX, SEG TEXT MOV DS,AX MOV AH,9 LEA  DX,TEXT

INT 21H POP DS POP AX

. CALL PRI    ;вызов процедуры,   находящейся в модуле 2 RETF WORK ENDP LIB ENDS

END WORK

Рис. 13. Шодуль 1 (программа PRI ASM), компонуемая с PR2. ASM.

программа DATA SEGMENT

TEXT1 DB 'Вызов процедуры PRI произошел из модуля 1.',13,10,'$'

DATA ENDS

SSEG   SEGMENT STACK DB   40 DUP{?)

SSEG ENDS

EXTRN WORK:FAR

PUBLIC PRI

CODE SEGMENT

ASSUME CS:CODE,   DS:DATA, SS:SSEG

6'

begin:

mov AX, DATA '■ mov    DS, AX !

call WORK ; вызов процедуры, находящейся в модуле 1

mov АН,О

INT 16H mov АН,4СН

INT 21H

;процедура будет  вызвана из модуля 1 PRI  proc far

PUSH AX

PUSH DS

PUSH DX

mov AH, 9 lea DX,TEXT1

INT 21H

POP DX

POP DS

POP AX

RETF PRI ENDP CODE ENDS

END BEGIN

Рис. 13.2. Модуль 2 (главный), компонуемый с модулем 1 (Рис. 13.1).

START    STOP      LENGTH NAME CLASS 00000H 0002BH  0002CH DATA ОООЗОН 00057Н 00028H SSEG 00060H  00080H 00021H CODE 00090H   OOOA9H 0001AH DATA1 OOOBOH OOOC6H 00017H LIB

PROGRAM ENTRY  POINT AT 0006:0000

Puc.13.3. Содержимое файла PR2. MAP.

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

Вернемся, однако,кглаве 11. В ней говорилось о способе нахождения конца програм­мы путем включения в программу фиктивного сегмента. Этот сегмент должен распола­гаться в конце (либо он последний в программе, либо имеет имя, которое после ассембли­рования с опцией /Адолжно располагаться по алфавиту в конце ряда имен сегментов). По логике вещей понятно, что такой сегмент следует указать в модуле 1 (PRI .ASM). Пробле­ма, однако, ЭТИМ не решается - ведь модулей может быть несколько. Нуичто, скажете Вы, нужно компоновать так, чтобы модуль PRI .ASM был последним, в нем же последним должен идти фиктивный сегмент. Пусть это сегмент имеет имя ZSEG. Поставьте в этом модуле метку, скажем ZS, которую в начале программы определим как PUBLIC. В тех модулях, где мы собираемся использовать эту метку, точнее сегмент, где она находится -ведь смещение метки в этом сегменте есть просто О34, необходимо отметить ее как вне­шнюю: EXTRN ZS:FAR. Наконец, чтобы узнать сегментный адрес этой метки (т.е. сег­мент), можно выполнить команду: MOVAX,SEGZS, И в АХ будет помещен адрес сегмен­та, начинающегося за концом программы. И еще одна ситуация: Вы пишите лишь главный модуль, все остальные даны В виде объектных модулей (либо библиотек объектных моду­лей) - как быть В этом случае? Проблема решается элементарно - создайте еще один мо­дуль (см. Рис. 13.4), а при компоновке поставьте его последним.

PUBLIC ZS ZSEG SEGMENT

ASSUME   CS: ZSEG

ZS:

ZSEG ENDS

END

Рис. 13.4. Модуль, используемый для определения конца программы.

Для упрощения работы с объектными модулями используется специальная про­грамма - библиотекарь (LIB.EXE). В библиотеку объектных модулей можно помес­тить модули, которые содержат уже отлаженные процедуры. При компоновке следует на вопрос LIBRARIES [.LIB]: указать имя Вашей библиотеки. Если библиотек несколь­ко, то их следует перечислить в строке, ставя между Рассмотрим теперь

основные команды библиотекаря:

-добавить к

теке новый объектный модуль. Если библиотеки с таким именем не существовало, то при выполнении данной команды она появится.

LIB ИМЯ_БИБЛИОТЕКИ-ИМЯ_ОБЪЕКТНОЕО_МОДУЛЯ - удалить данный

объектный модуль из библиотеки.

J4 Ситуация совсем не так проста. При определении сегмента можно указывать тип подгонки (PAGE - страница, PARA - параграф, WORD - слово, BYTE - байт). Согласно этомутипу сегмент будет начинаться на границе страницы (100Н байт), либо границе параграфа, либо на границе слова, либо выравнивания никакого не будет. По умолчанию всегда действует подгонка по началу параграфа. Если же взять тип подгонки сегмента, скажем, BYTE, смеще­ние метки ZS в сегменте будет не нулевым, и это придется учитывать, в противном случае ■Вы рискуете испортить код программы.

Ь1ВИМЯЗИБЛИОТЕКИ*ИМЯ_ОБЪЕет^

дыуказанный объектный модуль образуется на диске с расширением OBJ. При этом он сохранится и в библиотеке.

LIB ИМЯ_БИБЛИОТЕКТ4"-*ИМЯ_ОГЛЕетНО чтои предыдущая команда, но судалением объектного модуля из библиотеки.

ЬЮИМЯ_БИБЛИОТЕЬСИЛМЯ^АЙЛА-вь1юдкаталогабибш сто имени файламожно использовать стандартные именаустройств: CON, PRNhta

LIB ИМЯ_БИБЛИОТЕКИ-ИМЯ_1 +ИМЯ_2 - удаляет из библиотеки объектный файл с именем ИМЯ_1 и заменяет его на объектный файл с именем ИМЯ_2. При этом требуется указать имя новой библиотеки.

П.

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

Когда упоминают, что сегмент может начинаться на границе слова, параграфа или начало его никак не выравнивается, то о чем собственно идет речь? Во многих книгах говорится, что сегмент начинается на границе параграфа. Возникает явное противоре­чие. Противоречие, однако, чисто методологического порядка. Нужно различать сег­мент и то, что в нем находится. Сегмент всегда начинается с границы параграфа, тогда как для его содержимого это не всегда справедливо. Рассмотрим конкретный пример (Рис. 13.5). Вопрос: что будет содержаться в регистрах АХ и ВХ после выполнения двух первых команд? Общий ответ гласит: в АХ - адрес сегмента в параграфах (и это естественно, т.к. сегмент начинается на границе параграфа), в ВХ - смещение L1 в сегменте. как типом подгонки этом случае является PARA, то данные в сегменте тоже должны начинаться на границе параграфа. Поэтому в ВХ должен содержаться 0. Теперь изменим тип подгонки на BYTE: DATA SEGMENT BYTE. Как и раньше, в АХ будет содержаться адрес сегмента в параграфах. Но содержимое ВХ теперь уже не

обязано быть равным 0.

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

DATA SEGMENT LI      DB ?

DATA ENDS

CODE SEGMENT

ASSUME  DS:DATA, CS:CODE

35 В языках высокого уровня это достигается путем установки определенной опции.

BEGIN:

MOV АХ, SEG L1 MOV ВХ, OFFSET Ll

CODE ENDS

END BEGIN

Рис. 13.5. Что будет содержаться врегистрах АХ и ВХ?

ш.

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

ИМЯ_СЕГМЕНТА SEGMENT   [тип выравнивания]    [тип объединения] ["класс"]

ENDS

Здесь в квадратные скобки взяты операнды, которые могут отсутствовать в опре­делении сегмента. Класс представляет собой некоторое имя, взятое в кавычки. Тип объединения позволяет компоновщику объединять сегменты, имеющие одинаковые имена и одинаковые классы (классы могут отсутствовать). Перечислим типы объеди­нения.

РШПС. Прикомпоновкесетентыста™^ так, что длина получившегося сегмента равна сумме длин отдельных сегментов. По­рядок сцепления определяется порядком следования модулей в командной строке.

COMMON. Сегменты с таким типом объединяются в один сегмент с общим нача­лом. Длина получившегося сегмента равна длине наибольшего сегмента.

STACK. Если среди сегментов имеется один сегмент с таким типом объединения, то это дает возможность компоновщику определить значения регистров SS и SP. SS устанавливается на начало сегмента, SP - на конец (старший Если имеется

несколько сегментов с типом STACK, то они объединяются как по типу PUBLIC. Об­разуется один большой стек. SP будет указывать на дно этого стека, SS - на начало.

MEMORY. Данный тип объединения вызывает размещение сегмента в конец мо­дуля (программы, если всего один модуль). Если связываются несколько сегментов с таким типом, то первый из них считается имеющим тип MEMORY, а остальные обье-

диняются так, как тип COMMON.

В главе 14 мы расскажем о структуре ЕХЕ-программ, в заголовке которых, содержится ин­формация для загрузчика. Среди этой информации содержатся значения регистров СЭ, 1Р, SS,SP.

AT. После этого слова должен идти параграф - число или арифметическое выраже­ние. С помощью данного типа можно задать определение данных и меток по фиксиро­ванным адресам памяти. Например, возможен следующий вариант сегмента:

VIDEO BUF SEGMENT AT OB800H LI   DB ? VIDEO_BUF ENDS

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

MOV AX,SEG VIDEO BUF MOV ES,AX    MOV ES:L1, " ! "

IV.

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

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

1. Обмен ведется через регистры. Это самый быстрый способ, хотя и не самый эффективный. Через регистры могут передаваться как сами данные, так и указатели на область памяти, эти данные находятся. Например, указатель на строку, содержа­щую путь к файлу, принято передавать через пару регистров DS:DX.

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

однако, проблема разрешима. Можно поступить следующим образом: передать через

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

"известна" вызываемой процедуре.

2. Второй способ тоже достаточно очевиден. Предположим, в некотором сегменте имеется переменная типа BYTE: LI DB ?. Посредством директив PUBLIC и EXTRN доступ к данной переменной может получить любая процедура, находящаяся вдругих модулях. Сегмент и смещение L1 получается обычным образом:

MOV AX, SEG L1

MOV ES,AX

MOV ВХ,OFFSET Ll

Теперь ES:BX указывает на Ll.

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

3. Этот способ до некоторой степени является частным случаем первого. Однако в силу его специфики следуетразобраться в нем подробно. Для простоты будем предпо­лагать, что вызов процедуры будетдлинным, т.е. в стек будет положено 4 байта. Изме­нения стека после вызова процедуры показано на Рис. 13.6. Команда RETF возвраща­ет стек в исходное состояние.

Предположим, что мы хотим передать два параметра (типа WORD) в процедуру через стек. Поместим их соответственно в АХ и в ВХ и выполним две команды: PUSH АХ и PUSH ВХ. После этого выполним команду вызова процедуры. Изменения, про­исходящие со стеком в этом случае, отображены на Рис. 13.7. Теперь после входа в процедуру образовалась некоторая область, где содержатся входные параметры. Для доступа к ним удобно использовать регистр BP: MOV BP,SP, после этого ВР+6 указы­вает на первый параметр (PUSH АХ), а ВР+4—на второй параметр Для того чтобы удобнее было работать, можно сразу увеличить содержимое BP на 4: ADD BP,4. Те­перь BP будет указывать на последний параметр.

Мы вплотную подошли к одному замечательному заключению. Стек можно ис­пользовать как сегмент для временного хранения данных для процедуры. Действи­тельно , ведь перед вызовом процедуры можно зарезервировать некоторую часть стека для временного хранения данных во время работы процедуры. Для этого достаточно, перед тем как засылать параметры в стек уменьшить значение SP на нужное количе­ство байт (размер области локальных переменных). Доступ к этой области также осу­ществляется через регистр BP. Возможны и другие варианты устройства области ло-кальныхпеременных- например, изменение содержимого SP уже в самой процедуре. Область стека, где хранятся передаваемые параметры и локальные переменные, назы­вается кадром. Пример с локальной переменной будет приведен ниже.

Следует иметь в виду, что часто значение регистра BP должно быть сохранено. Выполнение команды PUSH BP приведет к тому, что значение SP уменьшится на 2, поэтому на область параметров будет указывать BP+6.

При выходе из процедуры состояние стека должно быть восстановлено. Если нет параметровдосостояниестекавосстанавливаетсякомандойРчЕТР.Есликакая-точасть стека былазарезервирована, как это показано на Рис. 13.7,то командой восстановле­ния будет RETFN, rfleN - число байт, на которые нужно сдвинуть SP(b сторону стар­ших адресов) после извлечения адреса возврата.

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

Если Вы хотите, чтобы параметры передавались и из процедуры, то состояние стека восстанавливать не надо. В этом случае SPno выходу из процедуры будет пока­зывать на начало области параметров. Возвратить же стек в исходное состояние мож­но командой SUB SP,N, где N - размер области параметров.

Передача параметров посредством стека взята на вооружение языками высокого уровня. Мы воспользуемся материалом главы, посвященной языкам высокого уровня (см. главу 15и особенно главы 24,25).

37  Напомню читателю, что регистр ВР по умолчанию показывает смещение в сегменте стека, т.е. команды МОУАХ,[ВР+2] и МОУАХ,88:[ВР+2] идентичны (см. главу 4.).

 

 

SS

SP |

(A)

(B)

Рис. 13.6. Стек до (А) ипосле (В) вызова процедуры. Пунктиром отмечено старое положение указателя стека.

*- ss

4- SP

 

i-

IP

CS

4-

параметры

 

 

 

SS

SP

SP+4+4

(А)

(В)

 

 

 

4-

<-

Локальные перем.

 

IP CS

параметры

 

 

SS

SP

SP+4+4

(С)

Рис. 13.7. Стек до (А) ипосле (В) вызова процедуры. (С) локальные переменные. Перед вызовом процедуры выполнены команды PUSHAXu PUSH ВХ.

Если в процедуре необходимо использовать локальные переменные, то место в памяти для них резервируется следующим образом. Содержимое SP увеличивается на общую длину для всех локальных переменных. Память между старым и новым значе-ниями8Рибудетобластьюдлялокальныхпеременных(Рис. 13.7(C)).

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

/программа loc.asm

процедур,   используемых из других модулей EXTRN COPY:FAR EXTRN  PRINT:FAR

стека

STE SEGMENT STACK

DB   100 DUP(O)

STE ENDS

;сегмент кода

CODE SEGMENT

ASSUME CS:CODE,   DS:DATA, SS:STE

BEGIN:

установки MOV AX,DATA MOV DS,AX

;вызов процедуры

процедуры INT ;BUFER,   STRl,   STR2   - указатели на строки

указатель  представляет  собой совокупность адреса  и смещения результате выполнения  строка  STR2  добавляется  к строке и результат помещается в строк   отмечается  кодом О

возвращает  длину  получившейся строки /возврат,   как и положено,   осуществляется через АХ

на строки будем помещать  в  стек слева направо

PUSH DS

LEA АХ,BUFER

PUSH AX PUSH DS

LEA АХ,STROKA1

PUSH DS .

LEA AX,STR0KA2

PUSH AX

CALL COPY ;вывод содержимого буфера /прототип процедуры VOID  PRINT(BUFER)

/процедура выводит содержимое буфера, в конце буфера должен /стоять код О

PUSH DS

LEA АХ,BUFER

PUSH AX

CALL PRINT /конец программы

MOV АХ,4С00Н INT 21H CODE ENDS /сегмент данных DATA SEGMENT

STROKA1   DB   "Первая  строка" ,0 STROKA2   DB   "Вторая  строка",0 BUFER       DB   100 DUP(?)

DATA ENDS

END BEGIN

Puc. 13.8. Основная программа конкатенации двух строк.

/программа loci.asm

/имена   внешних процедур PUBLIC  COPY, PRINT кода

COD SEGMENT

ASSUME CS:C0D

/процедура копирования двух строк в буфер COPY PROC

PUSH ВР

MOV BP,SP

SUB SP,2    /локальная переменная   [BP-2]     (тип WORD)

/параметры [ВР+6]    -   строка STROKA2

/[ВР+10]   - строка  STROKAl,     [ВР+14]   - буфер

MOV WORD PTR   [ВР-2],0 /обнулить локальную переменную

LDS SI, [ВР+10] / STROKAl

LDS DI,[ВР+14] /буфер

L01:

MOV AL,DS:[SI]

CMP AL,0

JZ ZER1

MOV DS:[DI],AL

Глава ІЗ. Модульное программирование и структура программ

2З7

INC DI

INC SI

INC WORD   PTR    [BP-2]         ; Счетчик

JMP SHORT L01

LDS SI, [BP+6] ;STROKA2

MOV AL,DS:[SI]

CMP AL,0

JZ ZER2

MOV DS:[DI],AL

INC DI

INC SI

INC     WORD   PTR    [BP-2] ;СЧвТЧИК

JMP   SHORT LO2

MOV    BYTE  PTR DS:[DI],0   ;0 -  завершение строки MOV    AX, [BP-2] ;в AX  - длина  строки в буфере

/освободить память,   которую занимала локальная переменная ADD    SP,2

POP ВР

/выход с   освобождением стека

RETF 16 COPY ENDP

/ вывод строки,   адрес  строки передается через стек /Строка должна  заканчиваться кодом О

PRINT PROC

PUSH ВР

MOV BP,SP

LDS SI,[ВР+б] /BUFER

L03 :

MOV

DL,DS:

[SI]

CMP

DL, 0

 

JZ

ZER3

 

MOV

АН, 2

 

INT

21H

 

INC

SI

 

JMP

SHORT

L03

POP

BP

 

RETF

4

 

PRINT ENDP COD ENDS

END

ZER1: L02 :

ZER2:

Рис. 13.9. Модуль для программы конкатенации.

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

чтобы процедура PRINT требовала еще и длину выводимой строки, но заканчивала

вывод, если встречается код 0. Если Вам пока еще не все понятно, вернитесь к этой программе после главы