Связь Turbo Assembler с Turbo Basic


Практическая совместимость Turbo Assembler с Microsoft Macro
Assembler облегчает жизнь программистов, работающих на Turbo
Basic. В данной главе мы расширенно рассмотрим некоторые примеры
из Руководства по Turbo Basic и приведем ряд других примеров, в
которых будет показано, каким образом Turbo Assembler может
расширить возможности Turbo Basic. Примечание: под Turbo Basic
подразумеваются версии 1.0 и старше данного продукта.

Turbo Basic обеспечивает три основных способа вызова
подпрограммы на ассемблере:

— Можно при помощи оператора CALL вызвать процедуру, содержа-
щую встроенные ассемблерные коды.

— Можно использовать оператор CALL ABSOLUTE для обращения к
конкретному адресу памяти.

— Можно использовать оператор CALL INTERRUPT и средства Turbo
Basic поддержки обработки прерываний процессора для передачи
управления подпрограмме.

Какой бы метод вызова вы ни выбрали, вы должны позаботиться о
сохранении значений определенных регистров. В этом аспекте
наименее требовательным является метод вызова при помощи CALL
INTERRUPT: в данном случае вы должны сохранить только регистры SS
(стекового сегмента) и SP (указателя стека). В случае двух
остальных методов вы должны обеспечить сохранение, помимо SS и SP,
регистры DS (сегмента данных) и BP (указателя базы).

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

Слово «может» использовано потому, что в целом лучше избегать
делать какие-либо предположения, особенно что касается
программирования на ассемблере. Несмотря на то, что ваше
руководство по MS-DOS может специально отмечать, что то или иное
прерывание не изменяет содержимого указателя стека или базы (или
какого-либо другого регистра), это может не во всех случаях
соответствовать действительности. MS-DOS изменяеы значения
регистров, а некоторые комбинации прерываний могут в
действительности противоречить информации в руководстве по MS-DOS.
При таких обстоятельствах лучше принять лишние меры
предосторожности, нежели искать потом ошибку. Если вы сами
позаботитесь о сохранении нужных в вашей программе регистров, это
не слишком скажется на характеристиках вашей программы, особенно
если принять во внимание ее большую надежность и возможности
безопасного переноса на следующие версии MS-DOS.

Передача параметров
——————————————————————

Turbo BASIC передает подпрограммам на языке ассемблера
параметры через стек. Все такие вызовы являются дальними;
последние 4 байта в стеке представляют собой адрес возврата,
который используется Turbo Basic при выходе из подпрограммы. Адрес
первого переданного в подпрограмму параметра равен [SP+4]; к этому
значению для каждого регистра, помещаемого в стек, следует
добавить два. Помните, что в памяти стек растет «вниз», в сторону
меньшего адреса.

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

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

Такой способ использования стека позволяет увеличить уровень
стандартизации подпрограмм. В следующем примере показано, чем это
выгодно. Представьте себе, что значение целой переменной x% равно
ровно 4, и у вас имеется подпрограмма на ассемблере MYROUTINE,
которая ожидает, что ей будет передано целое значение. Эта
подпрограмма будет работать совершенно одинаково, как при запуске
ее в виде CALL MYROUTINE(x%), так и в виде CALL MYROUTINE(4%).
Если подпрограмма была запущена как CALL MYROUTINE(4%) и в ней
была сделана попытка модифицировать значение переданного
параметра, то изменена будет область памяти, где временно хранится
целое число 4, и никаких тяжелых последствий это не вызовет.

Отметим, что во втором случае явно было задано значение (4%).
Это не является совершенно необходимым, хотя и желательно. Если
Tubo Basic почему-либо предположит, что число 4 является
действительным числом одинатной точности, то ваша подпрограмма
получит неправильное значение (2 байта 4-байтового значения
одинарной точности) и будет работать неправильно. Для того, чтобы
обеспечить передачу правильного типа переменной, лучше всего при
каждом запуске подпрограммы явно указывать ее тип.

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

Например, предположим, что у вас имеется небольшая
подпрограмма, для которой в стек требуется поместить только
базовый указатель, BP. В этом случае значение адреса первого
параметра будет равно [SP+6]. При помещении в стек двух регистров
адрес значения первого параметра будет равен [SP+8].

Представим себе, что первый параметр — это целов число и
передается в ассемблерную подпрограмму по значению. В таком случае
вы можете поместить целое значение в регистр CX< записав это как: push bp ;сохранить базовый указатель mov bp,sp ;сделать базовый указатель равным указателю ;стека les di,[bp+6] ;ES содержит сегмент, а DI смещение значения mov cx,ES:[DI] ;теперь значение помещается в CX Примечание: это значение будет находиться не в том же самом сегменте, что и обычные переменные. Для доступа к данному значению вы должны сами позаботиться о том, чтобы адрес был правильным и полным. Мы рассмотрим переменные, хранимые не в текущем сегменте данных, ниже. С другой стороны, если вы знаете, что данное целое было передано не по значению, а по ссылке, то [BP+6] будет содержать адрес смещения переменной в ее сегменте данных. Для того, чтобы поместить в регистр CX значение такого целого значения, нужно записать: push bp ;сохранить базовый указатель mov bp,sp ;сделать базовый указатель равным указателю ;стека mov bx,[bp+6] ;поместить адрес значения в BX mov cx,[bx] ;теперь значение помещается в CX Данная подпрограмма предполагает, что переменная находится в текущем сегменте данных и что для обновления значения этой переменной требуется задать только смещение ее в этом сегменте. Передача параметров становится более безопасной, если всегда предполагать, что она выполняется по значению. Если в действительности переменная передается по ссылке, то вы ничего не потеряете; полный адрес переменной включит в себя текущий сегмент данных. И напротив, если ваша подпрограмма предполагает, что переменная передается по ссылке, а это не так, то полученный вами адрес не будет являться правильным полным адресом, так как сегмент будет неверным. Следовательно, подпрограмма либо обратится не к тому значению, либо, при попытке изменить значение переданной переменной, изменит не ту область памяти, что требуется, с непредсказуемыми последствиями. Передача переменных по ссылке существенно легче выполняется для таких переменных, как строки символов, массивы и числа с плавающей точкой. Эти переменные слишком длинны и могут вызвать проблемы, если фактически передавать их через стек. Кроме того, по времени считывание длинной переменной из стека выполняется гораздо дольше, нежели получение из стека ее адреса с последующим манипулированием переменной непосредственно в памяти, где она хранится. Для обработки строковых переменных (разумеется, если они не очень короткие) в любом случае вряд ли хватит памяти одних только регистров, без выполнения обращений к памяти. Переменные, не находящиеся в текущем сегменте данных ----------------------------------------------------------------- Если переданная переменная не находится в текущем сегменте данных, то для доступа к значению этой переменной в ассемблерной программе вам понадобится указывать и сегмент, и смещение переменной в сегменте. Turbo Basic всегда передает в стек и сегмент, и смещение каждой переменной; следовательно, программисту всегда доступен полный адрес каждой переменной. Сегментная часть адреса располагается в двух байтах, непосредственно следующих за смещением параметра. Наиболее удобным является использование этой информации в ассемблерных программах через команду LES. Команда LES загружает указанный регистр значением смещения переменной и загружает регистр ES сегментной частью адреса. Это гарантированно дает вам полный адрес любой переменной, независимо от того, в котором сегменте данных она расположена. И опять, предположим, что вашей подпрограмме требуется записать значение целочисленной переменной в регистр CX. Поскольку значение регистра ES сохранять нет необходимости, то можно воспользоваться командой LES следующим образом: push bp ;сохранить базовый указатель mov bp,sp ;сделать базовый указатель равным указателю ;стека les di,[bp+6] ;ES содержит сегмент, а DI смещение mov cx,ES:[DI] ;теперь значение переменной помещается в CX Передавая полный адрес каждой переменной, Turbo Basic позволяет программисту писать на ассемблере подпрограммы, не зависящие от того, где хранятся обрабатываемые в них данные. Если вы перепишете вашу основную программу и поместите переменные или массивы в другие сегменты данных, то в случае использования в ваших подпрограммах на ассемблере полных адресов переменных и команд LES переписывать их не придется. Какой использовать оператор CALL? ----------------------------------------------------------------- Существует два типа операторов вызова CALL: ближний и дальний. Дальние CALL обращаются за пределы текущего кодового сегмента, а ближние - только в пределах этого сегмента. В Turbo Basic какие-либо проблемы может вызвать только оператор CALL ABSOLUTE, поскольку он произвольно адресует память. Следовательно, Turbo Basic требует, чтобы подпрограммы, вызываемые с помощью CALL ABSOLUTE, заканчивались дальним возвратом, и при передаче управления таким подпрограммам генерирует дальний вызов оператором CALL. CALL INTERRUPT может неявно генерировать дальний вызов, однако Turbo Basic обрабатывает его внутренним образом. Если вы написали свои собственные обработчики прерываний, для передачи управления обратно в программу на Turbo Basic вам необходимо использовать команду IRET (возврат из прерывания). Встроенный ассемблер вставляется в программу при ее компиляции. Как правило, эти коды должны находиться внутри текущего сегмента данных, но Turbo Basic не предполагает этого сам; такие подпрограммы также заканчиваются дальним возвратом. Turbo Basic автоматически генерирует CALL и возврат, поэтому использовать в ваших кодах команду RET не требуется. Если вы желаете закончить подпрограмму до окончания кодов, следует просто выполнить переход на метку в конце кодов. Примечание: поскольку Turbo Basic не использует программу DOS LINK, вам не требуется ни объявлять ваши подпрограммы как PUBLIC, ни объявлять их в вызывающей программе как external. Извлечение из стека ----------------------------------------------------------------- Перед выходом из вашей подпрограммы вы должны убедиться, что все регистры, помещенные вами в стек, извлечены оттуда. Здесь легко допустить ошибку, особенно если подпрограмма выполняет условное помещение (PUSH) и извлечение (POP) регистров в стек и из стека. Если вы извлечете из стека меньше регистров, чем нужно, то после вызова подпрограммы выход из нее скорее всего не произойдет, поскольку turbo Basic предполагает, что последним в стек был помещен адрес возврата. Если же извлечь из стека слишком много элементов, то случится практически то же самое. Не загружайте в стек и не извлекайте оттуда в сегментные регистры не имеющие отношения к подпрограмме значения, поскольку это может сделать исходную версию вашей программы несовместимой с последующими версиями DOS (например, с защищенным режимом OS/2). Создание ассемблерной программы для использования с Turbo Basic ----------------------------------------------------------------- Если вы написали ассемблерную программу и хотите преобразовать ее в .COM-файл для использования в программе на Turbo Basic, вы можете для этого воспользоваться примером пакетного файла, приведенным в руководстве по Turbo Basic: TASM 51; Tlink /t %1; Вы не должны включать стековый сегмент, поскольку ассемблерная подпрограмма при выполнении всегда использует стек, распределяемый в Turbo Basic. Если в начале вашей программы вы явно не зададите оператор ORG 100h, то Turbo Assembler по умолчанию использует начальный адрес 100h. Тем не менее, для последующих ссылок лучше явно задать оператор OGR. Если ваша подпрограмма предназначена для последующей работы на процессорах 80186, 80286 или 80386, в начале ассемблерных кодов можно также использовать директивы .186, .286 или .386. Тогда Turbo Assembler позволит вам использовать коды операций, применимые на указанных процессорах. Далее вы увидите, что это большое преимущество. Вызов процедуры на встроенном ассемблере ----------------------------------------------------------------- Предположим, что вы создали ассемблерную подпрограмму и преобразовали ее в .COM-файл в Turbo Assembler. Имеется два способа использования результата в программе на Turbo Basic: при помощи директивы $INLINE COM или $INCLUDE. Наиболее пямой метод - это использование конструкции $INLINE COM имя_файла, когда Turbo Basic вставляет указанный .COM-файл в указанную вами точку программы. Этот метод относительно легко реализуется, но имеет ряд недостатков: - Turbo Basic имеет ограничение - 16 директив $INLINE на про- цедуру. Это может вызвать проблемы при сложных структурах программы (однако такие структуры нежелательны). - Более серьезная проблема возникает из того факта, что файлы .COM не включают в себя документирование программы. Разуме- ется, соответственные комментарии можно включить и в вызы- вающую программу, но было бы лучше, если бы .COM-файл сам содержал документирующие его комментарии. - $INLINE .COM-файлы могут также разрастаться. Полезно помещать несколько таких файлов в один файл, особенно если они часто используются все вместе. (Это одна из причин для использова- ния библиотеки ассемблерных подпрограмм; однако к сожалению, создать библиотеку .COM-файлов непросто.) - И наконец, $INLINE .COM-файлы для изменения исходных текстов должны быть модифицированы и реассемблированы. Если изменения минимальны, то эта работа может показаться излишней. Вследствие способа работы директивы $INCLUDE COM вам может захотеться преобразовать .COM-файлы в последовательность шестнадцатиричных значений, вставляемых в программу директивой $INCLUDE. Такие подпрограммы также могут быть считаны с диска при помощи команды Turbo Basic Read File (Ctrl-K R); тем самым ваш исходный файл будет явно показывать, что, собственно в него включено. Для работающего на Turbo Basic программиста это может явиться большим преимуществом. Поскольку шестнадцатиричные коды представляют собой текст, который может быть отредактирован, вы можете включить или добавить к нему комментарии. Вы также имеете возможность в редакторе turbo Basic внести в них небольшие изменения без необходимости реассемблирования, а также можете поместить несколько подпрограмм в один файл. Комбинируя эти методы, можно эффективно создать библиотеку ассемблерных подпрограмм для использовании в некотором семействе программ на Turbo Basic. Сопровождение такой библиотеки также проще, чем при помощи формальной программы управления библиотеками. Если подпрограмма имеет большую длину, то файл в шестнадцатиричных кодов может быть очень большим, и файл исходного текста программы также станет слишком велик для удобного редактирования. Имеется ограничение в 64Кб для максимального размера файла, редактируемого одновременно. Если в этом возникнет проблема, то вы можете включить в вашу программу шестнадцатиричный файл как включаемый $INCLUDE-файл. (Однако, это ни коим образом не сделает вашу программу более читаемой.) Ниже приводится небольшая программа на Turbo Basic, в которой .COM-файлы преобразовываются в шестнадцатиричные файлы: 'COM2INC.BAS 'Данная программа преобразовывает .COM-файлы в $INCLUDE-файлы 'при помощи матакоманды Turbo Basic $INLINE для простоты их 'вставки в программы на Бейсике. DEFINT A-Z 'Все переменные будут целочисленными FS=COMMAND$ 'Проверка наличия командной строки WHILE F$="" PRINT"Эта программа преобразует .COM-файлы в $INCLUDE-файлы" PRINT"для использования в Turbo Basic. По умолчанию тип ис-" PRINT"ходного файла COM. По умолчанию тип выходного файла INC." PRINT"Любое умолчание может быть изменено явным указанием кон-" PRINT"кретного типа файла." PRINT"Если вы не ввели имя для выходного файла, то оно будет" PRINT"принято равным имени входного файла, но будет иметь спе-" PRINT"цификацию типа INC." LINE INPUT"Введите имя преобразуемого файла: ";F$ WEND IF COMMAND$="" THEN LINE INPUT"Введите имя желаемого выходного файла: ";O$ END IF IF INSTR(F$,".")=0 THEN F$=F$+".COM" 'установка спецификации 'входного файла IF O$="" THEN O$=LEFT$(F$,INSTR(F$,"."))+"INC" 'установка спецификации 'выходного файла ELSE IF INSTR(O$,".")=0 THEN O$=OS+".INC" 'двумя способами END IF OPEN"R",#1,F$,1 'чтение входного файла FIELD #1,1 AS A$ 'в A$ по одному байту за один раз LASTBYTE&=LOF(1) 'позиция конца файла OPEN"O",2,O$ 'открытие выходного файла FOR I&=1 TO LASTBYTE&-1 GET 1,I& X%=ASC(A$) IF ((I&-1) MOD 5=0) THEN PRINT #2,"":PRINT #2,"$INLINE "; PRINT #2,"&H";HEX(X%); IF ((I&-1) MOD 5<>4) THEN PRINT #2,»,»;
NEXT I&
GET 1,LASTBYTE&
PRINT #2,»&H»;HEX(ASC(A$))
PRINT»Преобразование закончено. «;LASTBYTE&;» байтов считано.»
PRINT O$:» содержит «;LOF(2);» байтов.»
CLOSE
END

Данная программа создаст на выходе файл, в каждой строке
которого будет находиться до пяти шестнадцатиричных кодов. Каждая
строка будет начинаться директивой $INLINE, а результирующий файл
будет иметь достаточно свободного места для помещения в него любых
желательных для вас комментариев. Если вы хотите, чтобы в одной
строке было больше или меньше шестнадцатиричных кодов, в программе
нужно просто изменить MOD 5 на MOD N, где N это любое число,
большее или меньшее, чем 5.

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

Размещение подпрограммы Turbo Basic в памяти
——————————————————————

Имеется три основных способа определения размещения
подпрограммы в памяти:

— Вы можете сделать так, чтобы подпрограмма сама возвращала
свой адрес.

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

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

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

xy: mov ax,cs ;послать в AX регистр кодового сегмента
push bp ;сохранить базовый указатель
mov bp,sp ;и скопировать в BP указатель сегмента
les di,[bp+6] ;ES содержит сегмент, а DI смещение
mov ES:[DI],AX ;записать значение CS в первый параметр
mov dx, offset xy
;прием текущего смещения
les di,[bp+0ah]
;адресация второго параметра
mov ES:[DI],DX ;записать значение смещения во второй
;параметр
jmp fin ;передача управления в обход
;»действительного» кода
;действительный код должен быть здесь
fin: pop bp ;восстановление BP и возврат

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

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

Однако, адрес подпрограммы все же можно определить. Если вы
сгруппировали вместе несколько подпрограмм и поместили в программу
на Turbo Basic соответствующие метки, что дает вам возможность
вызвать желаемую подпрограмму, позволит ли это включить в число
прочих и подпрограмму, сообщающую адрес?

Нет, не позволит. Помните, что Turbo Basic сам обрабатывает
команду возврата RET. Поскольку подпрограммам даны различные
имена, Turbo Basic предполагает, что каждая из них представляет
собой переместимый код. Нет гарантии, что в итоговом .EXE-файле
эти отдельные подпрограммы окажутся в одной области памяти. Даже
если они будут расположены в одной области и в той же
последовательности, вы не сможете узнать, сколько байтов кода
Turbo Basic поместил между ними, что не позволит вам узнать, где
именно в каждой из них вносить необходимые вам изменения.

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

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

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

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

Скрытые строки
——————————————————————

Для хранения строк символов Turbo Basic позволяет иметь
максимум до 64Кб. Иногда вам может понадобиться каждый байт этой
области, и кроме того, строковые константы в кавычках (иногда
используемые в меню или в приглашениях программы на ввод данных)
также занимают часть строкового пространства.

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

Рассмотрим следующую подпрограмму:

;Эта подпрограмма принимает два целочисленных параметра и
;возвращает сегмент и смещение текста в теле программы
;
push sp
mov bp,sp
mov dx,offset show ;расположение строки
mov ax,cs ;кодовый сегмент в AX
les di,[bp+6] ;ES:DI указывает на параметр
mov ES:[DI],DX ;сообщение об адресе строки
les di,[bp+0ah] ;следующий параметр
mov ES:[DI],AX ;сообщение о кодовом сегменте
jmp fini ;и возврат из подпрограммы
DB ‘Любой текст любого размера’
DB ‘и сколько угодно, заканчивающийся любым
DB ‘символом. В данном случае нулем.’,0
fini pop bp

Действие этой подпрограммы несколько иное, чем можно ожидать
для изменяемого из программы встроенного кода. Дело в том, что вы
в данном случае храните не код (хотя Turbo Basic будет
обрабатывать результирующий .COM-файл, как если бы это были
программные коды); вы храните данные.

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

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

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

CALL ABSOLUTE
——————————————————————

Вы имеете также возможность определить количество байт,
доступных данной подпрограмме. В частности, можно определить
количество байтов, предшествующих тексту. Просто замените все,
кроме последней команды, своими собственными кодами: поскольку вам
известно, где расположена подпрограмма и каков ее размер, при
помощи команды BLOAD вы можете записать поверх ее что угодно. С
точки зрения файла .EXE Turbo Basic ничего не изменилось — даже
если теперь вся подпрограмма другая.

В Руководстве по Turbo Basic имеется лишь краткое упоминание
о CALL ABSOLUTE, и тому есть несколько причин. Первая из них
состоит в том, что в случае таких подпрограмм Turbo Basic имеет
худшие средства управления ими. Во-вторых, как правило, такие
подпрограммы использовались в режиме интерпретатора Бейсика; Turbo
Basic нстолько далек от интерпретатора, что такие подпрограммы
могут просто не работать. В третьих, использование подпрограмм,
вызываемых при помощи CALL ABSOLUTE, возможно, не будет допустимо
в будущих операционных системах. В частности, операционные
системы, делающие четкое различие между программной областью и
областью данных, модут не позволить процессору обработку команд,
расположенных в области данных. В четвертых, подпрограммам,
вызываемым с помощью CALL ABSOLUTE, могут передаваться только
простые целочисленные переменные. Это не столь существенное
ограничение, поскольку в виде целочисленных переменных могут быть
переданы адреса сегмента и смещения для переменных любого типа.
Тем не менее, это может замедлить процедуру передачи параметров.

При обсуждении будет принято, что вы работаете с MS-DOS
версии 2.0 или старше, и что операционная система позволяет
процессору выполнять команды в любой точке памяти.

CALL ABSOLUTE для фиксированных адресов памяти
——————————————————————

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

С командой MEMSET часто используется команда ENDMEM. ENDMEM
возвращает длинное целое, представляющее собой последний адрес
памяти, который может быть использован компилированной программой
на Turbo Basic. Подпрограммы обычно помещаются в старшей области
памяти по некоторому фиксированному адресу, находящемуся ниже
данного граничного адреса.

Если у вас имеется такого рода набор подпрограмм, вам
понадобится вызывать их командой CALL ABSOLUTE. Для помещения их в
старшую память может служить команда BLOAD. Для установки адреса
сегмента, в который эти подпрограммы будут загружены, использовать
команду DEF SEG не требуется; также не требуется объявлять
конкретный адрес смещения, по которому они должны быть загружены в
сегменте.

При создании таких подпрограмм в Turbo Assembler следует
соблюдать следующие правила:

1. Если подпрограмма не будет всегда выполняться по одному и
только одному адресу, то все команды передачи управления
(JMP и CALL) в программе должны быть полностью перемести-
мыми. (Полное обсуждение переместимых кодов не входит в
число обсуждаемых в данной главе вопросов.)

2. Если программа всегда будет выполняться только по одному
адресу, вы должны задать этот адрес в директиве ORG в ис-
ходном тексте подпрограммы на ассемблере.

CALL ABSOLUTE для прочих адресов памяти
——————————————————————

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

Рассмотрим следующий фрагмент программы:

DEFINT A-Z
$DYNAMIC ‘массив будет динамическим
DIM ROUTINEARRAY(10000) ‘распределено 20,002 байтов
‘здесь могут находиться произвольные коды
WHERESEG%=VARSEG(ROUTINEARRAY(0)) ‘адрес сегмента
WHEREOFFSET%=VARPTR(ROUTINEARRAY(0)) ‘адрес смещения
DEF SEG=WHERESEG% ‘установка сегмента по умолчанию
BLOAD»COMFILE»,WHEREOFFSET% ‘считывание подпрограммы
CALL ABSOLUTE WHEREOFFSET%(PARAMETER%) ‘вызов подпрограммы
DEF SEG ‘возврат к умолчанию

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

Как вы могли заметить, подпрограммы, предназначенные для
использования при помощи CALL ABSOLUTE, легче найти и
модифицировать, чем предназначенные для использования при помощи
$INLINE. Сложность с подпрограммами CALL ABSOLUTE состоит в том,
что если планируется их широкое использование, то они обязательно
должны быть полностью переместимыми в памяти. Для коротких
подпрограмм это может не являться проблемой; однако для сложных
программ обеспечить полную переместимость в памяти тяжело.

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

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

Строки в Turbo Basic хранятся не так, как числовые
переменные. Если вы выполните VARPTR(A%), вы получите адрес целой
переменной A%. Если вы выполните VARPTR(A$), вы получите адрес
дескриптора строки A$. Адрес памяти в последующих двух байтах
будет содержать фактический адрес строки в строковой области
памяти. Для того, чтобы получить такой же результат, что и для
VARPTR(A%), следует выполнить нечто эквивалентное следующему:

A%=VARPTR(A$)
A%=A%+2
STRINGADDRESS%=CVI(CHR$(PEEK(A%)+CHR$(PEEK(A%+1)))

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

Если вы желаете избежать использования BLOAD, можно также
выполнять загрузку .COM-файлов в строки при помощи команд
ввода/вывода двоичных файлов; то есть для этого нужно открыть
.COM-файл с типом binary и затем прочесть в строку нужное
количество байтов. Этот же способ можно использовать и для чтения
данных в целочисленный массив. Однако, команда BLOAD работает
быстрее и проще.

Прочие проблемы, связанные с CALL ABSOLUTE
——————————————————————

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

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

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

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

CALL INTERRUPT
——————————————————————

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

Большинство программистов использует CALL INTERRUPT для
доступа к обычным сервисным средствам MS-DOS. В данном случае
собственно об ассемблере речь не идет. Вместо этого нужно
запомнить следующее:

———————-
Имя Регистр
———————-
REG 0 Флаги
REG 1 AX
REG 2 BX
REG 3 CX
REG 4 DX
REG 5 SI
REG 6 DI
REG 7 BP
REG 8 DS
REG 9 ES
———————-

Для установки значения регистра служит оператор REG:

REG 3,&H0F01

Данный оператор устанавливает значение регистра CX равным
шестнадцатиричному 0F01. Регистр CH будет равен шестнадцатиричному
)F, а CL — 01.

Для считывания значения регистра служит функция REG:

A%=REG(3)

Тем самым переменной A% будет присвоено текущее значение
регистра CX.

В приводимом ниже примере на экране выполняется скроллинг в
обратном направлении, от строки 1 до строки 24:

REG 3,0 ‘ряд ноль, колонка ноль для вершины экрана
REG 4,&H175F ‘ряд 23, колонка 79 для дна экрана
REG 2,&H70 ‘цвет 7,0
REG 1,&H0701 ‘функция BIOS 7, скроллинг одной строки
CALL INTERRUPT &H10 ‘видео-прерывание 10h

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

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

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

В программе на Turbo Basic вы должны вы должны установить
вектор прерывания на подпрограмму на Turbo Assembler. Как отмечено
в Руководстве по Turbo Basic, подпрограмма прерывания обязана
сохранить значения регистров SS и SP; все остальные регистры могут
быть модифицированы. В конце работы подпрограммы управление
возвращается в программу на Turbo Basic через команду IRET.

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

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

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

Пример программы
——————————————————————

FILLIT2$ = CHR$(&HFC)+CHR$(&HF3)+CHR$(&HAB)+CHR$(&HCB)
‘ cld rep stosw ret
DIM a%(100) ‘целочисленный массив с нулевыми элементами
WHERE%=VARPTR(FILLIT2$) ‘по данному адресу хранится длина
WHERE%=WHERE%+2 ‘здесь находится адрес строки
CLS:PRINT PEEK(WHERE%),PEEK(WHERE%+1)
HERE%=PEEK(WHERE%)+256*PEEK(WHERE%+1) ‘а это сам адрес строки
DEF SEG ‘здесь это необязательно, но вообще это хорошая
‘практика программирования
WHERE%=PEEK(0)+256*PEEK(1)
DEF SEG=WHERE% ‘сегмент строки является первым словом в
‘DS по умолчанию
REGES%=VARSEG(a%(0))
REGSI%=VARPTR(a%(0))
REG 1,5% ‘помещение в AX значения заполнителя
REG 3,101% ‘число заполняемых элементов, от 0 до 100,
‘помещается в CX
REG 9,REGES% ‘в ES помещается сегмент заполняемого массива
REG 6,REGSI% ‘в SI помещается смещение первого заполняемого
‘элемента массива
CALL ABSOLUTE HERE% ‘заполнение массива значением 5
DEF SEG
FOR i%=0 TO 100:PRINT a%(i%);:NEXT i%
PRINT
PRINT REG(1),REG(3),REG(9),REG(6):STOP
CALL FILLIT(a%(0),-1%,101%) ‘заполнение массива значением -1
FOR i%=0 TO 100:PRINT a%(i%);:NEXT i%
PRINT
END
SUB FILLIT INLINE
$INLINE &H55,&H8B,&HEC,&HC4,&H7E
$INLINE &HE,&H26,&H8B,&HD,&HC4
$INLINE &H7E,&HA,&H26,&H8B,&H5
$INLINE &HC4,&H7E,&H6,&HFC,&HF3
$INLINE &HAB,&H5D
END SUB

;Подпрограмма передачи произвольного числа элементов с произво-
;льными значениями в целочисленный массив для call absolute.
;Синтаксис вызова:
;REG 1,FILLVALUE% ‘AX содержит значение заполнителя
;REG 3,FILLCOUNT% ‘CX содержит число заполняемых элементов
;REG 9,VARSEG(ARRAY(0)) ‘ES содержит сегмент массива
;REG 6,VARPTR(ARRAY(0)) ‘DI содержит смещение первого элемента
;CALL ABSOLUTE FILLIT2
;FILLIT2 это адрес вызываемой по абсолютному адресу подпро-
;граммы, а DEF SEG перед выполнением CALL ABSOLUTE устанавливает
;кодовый сегмент по умолчанию в значение для FILLIT2.

PROGRAM SEGMENT
START PROC FAR ;этим задается дальний возврат
ASSUME cs:PROGRAM
push bp ;сохранение базового указателя
cld ;очистка флага направления
rep ;следующая команда будет повторяться,
;пока CX не станет равным 0
stosw ;запись AX в ES:DI и инкремент DI на 2
pop bp ;восстановление базового указателя
ret ;межсегментный (дальний) возврат
START ENDP
PROGRAM ENDS ;конец сегмента
END

;Подпрограмма передачи произвольного числа элементов с произво-
;льными значениями в целочисленный массив.
;Синтаксис вызова:
;CALL FILLIT(ARRAY(0),FILLVALUE,NUMTIMES)

ORG 100h
PROGRAM SEGMENT
ASSUME cs:PROGRAM
push bp ;сохранение базового указателя
mov bp,sp ;помещение в BP указателя стека
les di,[bp+0eh] ;прием адреса смещения числа заполняемых
;элементов
mov cx,es:[di] ;число заполняемых элементов помещаем в CX
les di,[bp+0ah] ;прием адреса смещения значения заполнителя
mov ax,es:[di] ;помещение значения заполнителя в AX
les di,[bp+6] ;адрес смещения заполняемого массива
cld ;очистка флага направления
rep ;следующая команда будет повторяться,
;пока CX не станет равным 0
stosw ;запись AX в ES:DI и инкремент DI на 2
pop bp ;восстановление базового указателя
PROGRAM ENDS ;конец сегмента — без команды RET
END