
Лекции / GL15
.doc
Модульное программирование
Термин "модульность" применительно к программированию имеет два смысла:
-
разбиение программы на подпрограммы;
-
размещение текста программы в различных файлах.
До сих пор мы создавали программы, которые умещались в одном файле. Для небольших учебных программ это было вполне достаточно. Но большие программы целесообразно хранить в нескольких файлах. Часто наборы служебных подпрограмм (например для ввода-вывода) хранятся в одном файле, так как там находятся тексты отлаженных, проверенных подпрограмм. Новые программы размещаем в других файлах. Трансляция каждого исходного файла в объектый файл проводится с использованием Ассемблера. Далее эти объектные файлы объединяются в загрузочный файл с помощью компоновщика. Для такого объединения компоновщик должен располагать, в частности, информацией:
-
какие имена являются общими для модулей;
-
как объединять логические сегменты (программные секции).
В следующих разделах мы подробно изучим эти аспекты.
Директивы связи
Рассмотрим пример.
Из заданной строки получить новую строку, в которой строчные латинские буквы исходной строки преобразованы в прописные. Подпрограмму преобразования разместить в отдельном файле.
В главной программе, размещенной в файле main.asm определены две строки: String — исходная строка, завершенная знаком $, NewStr — преобразованная строка. Подпрограмма ToUpper, размещенная в файле sub.asm, преобразует те символы строки, которые являются строчными латинскими буквами, в соответствующие прописные буквы. Адрес строки String передается в регистре SI. Строка NewStr является глобальным объектом для обоих модулей (файлов). Так сделано в учебных целях. (Было бы логичнее адрес NewStr передавать в регистре DI.) При этом строка NewStr определена в файле main.asm. В этом же файле имеется директива PUBLIC NewStr, которая делает это имя доступным для программ в других файлах. В файле sub.asm с помощью директивы EXTRN NewStr:BYTE:50 транслятору сообщается, что имя NewStr определено в другом файле, имеет тип: байты и размер 50. Тип нужен для правильного кодирования команд (в данном случае указание типа излишне, так как в программе используется только адрес строки). Число 50 в нашем примере также можно было не указывать. Далее мы приведем пример, когда указание такого числа может принести пользу. Аналогичные директивы используются для имени подпрограммы ToUpper.
main.asm
INCLUDE macro.inc
.MODEL small
.STACK 100h
PUBLIC NewStr
EXTRN ToUpper:PROC
.DATA
String DB "aB2",CRLFT
NewStr DB 50 DUP(?)
.CODE
start:mov ax,@data
mov ds,ax
message String ; Вывод исходной строки
mov si, OFFSET String
call ToUpper ; Преобразование строки
message NewStr ; Вывод новой строки
exit
END start
Подпрограмма, описанная в sub.asm, может быть реализована более эффективно с использованием так называемых строковых команд, которые мы изучим позднее.
sub.asm
JUMPS
.MODEL small
EXTRN NewStr:BYTE:50
PUBLIC ToUpper
.CODE
; вход si – адрес строки, заканчивающейся $
ToUpper PROC
mov di, OFFSET NewStr
n: mov al,[si] ; Очередной символ строки - в AL
cmp al,'a' ; Если символ входит в диапазон 'a'-'z'
jnae copy ; - преобразовать его
cmp al,'z'
jnbe copy
sub al,'a' - 'A' ; Перевести символ в верхний регистр
copy:mov [di],al ; Поместить символ в новую строку
inc si ; Переместить указатели
inc di
cmp al,'$' ; Конец строки ?
jne n
ret
ToUpper ENDP
END ; Обратите внимание, что метки стартового адреса нет,
; потому что такой метки нет в файле
Вариант с использованием строковых команд:
ToUpper PROC
mov ax, @data
mov es,ax
cld
mov di, OFFSET NewStr
n: lodsb ; Очередной символ строки - в AL
cmp al,'a' ; Если символ входит в диапазон 'a'-'z'
jnae copy ; - преобразовать его
cmp al,'z'
jnbe copy
sub al,'a' - 'A' ; Перевести символ в верхний регистр
copy:stosb ; Поместить символ в новую строку
cmp al,'$' ; Конец строки ?
jne n
ret
ToUpper ENDP
Команды для получения исполняемого файла
c:\tasm\bin\tasm /z/zi/la/m main+sub
c:\tasm\bin\tlink /v/m main sub
Здесь оба файла с исходными текстами обрабатываются одной командой tasm. При компоновки выполняемый файл получает имя первого из объектных файлов, перечисленных в списке.
Итак, в модуле, где имя определено, но должно быть "известно" другим модулям, используется директива PUBLIC имя. В модуле, для которого имя является внешним используется другая директива: EXTRN имя: тип: количество.
В директиве EXTRN для имени указывается его тип:
-
для имен данных BYTE, WORD, DWORD…
-
для имен процедур NEAR, FAR, либо PROC (тогда NEAR или FAR определяется моделью памяти).
Если тип указан для имен данных, то полезно указывать также и количество элементов, если количество опущено, то по умолчанию оно полагается равным одному.
Упражнение. Посмотрите листинги и карту памяти для main.asm и sub.asm. Что нового в них появилось благодаря использованию внешних имен? Что произойдет, если закомментировать директивы PUBLIC и EXTRN.
Если нужно указать, что внешними являются несколько имен, то в директивах PUBLIC и EXTRN они перечисляются через запятую.
В директиве PUBLIC тип имени указывать не нужно, он и так известен транслятору из текста модуля. В директиве EXTRN тип имени нужен для генерации правильных кодов команд. Пусть, например, процедура ToUpper должна принудительно менять первый символ строки на символ '*'. Перед ret добавим команду mov NewStr, '*', а директиву EXTRN NewStr:BYTE:50 заменим на EXTRN NewStr.
Упражнение. Что при этом произойдет?
Ответ. Трансляция пройдет без замечаний, а в отладчике мы увидим команду mov word ptr [0006], 002A. Коды символов в строке NewStr станут такими: 2A 00 32 0D 0A 24. Если бы Ассемблер "знал", что NewStr — массив байтов, то была бы сгенерирована команда mov byte ptr [0006], 2A.
Сегментные директивы
До сих пор мы использовали упрощенные сегментные директивы (.CODE, .DATA). При более изощренном программировании может возникнуть потребность свободнее распоряжаться храрактеристиками логических сегментов. Поэтому нужно наряду с упрощенными сегментными директивами освоить и стандартные сегментные директивы. Также их нужно знать еще по двум причинам: эти директивы используются в литературе по языку Ассемблера (зачастую без особой необходимости); при программировании на языках высокого уровня можно получать листинг на языке Ассемблера, при этом используются стандартные сегментные директивы (мы будем работать с ними).
Прежде всего надо подчеркнуть отличие физического и логического сегмента. Мы уже знаем, что физический сегмент — область памяти размером 64 К, ее начальный адрес лежит на границе параграфа. Логический сегмент (программный сегмент, программная секция) — часть программы. Но связь между этими понятиями есть. Логический сегмент определяет область, адрес которой лежит в сегментном регистре.
Логический сегмент начинается с директивы
имя SEGMENT атрибуты
и завершается директивой
имя ENDS
Пример.
data1 SEGMENT
p DW 2
q DW 3
data1 ENDS
data2 SEGMENT
r DW 0
data2 ENDS
cseg SEGMENT
s: mov ax, data1
mov ds, ax
mov ax, data2
mov es, ax
mov ax, p
add ax, q
mov r,ax
mov ax, 4C00h
int 21h
cseg ENDS
END s
Здесь атрибуты отсутствуют. Они принимаются по умолчанию (чуть позже, мы узнаем какие именно). Для ссылки на содержимое сегмента нужно загрузить сегментный регистр
mov ax, data1
mov ds, ax
(Напомню, что команда mov ds, data1 недопустима, поэтому приходится использовать регистр AX как "промежуточное звено".)
Эту программу tasm отказывается транслировать. Для команды mov ax, p и двух следующих он выдает сообщение об ошибке:
**Error** s1.ASM(13) Can't address with currently ASSUMEd segment registers
Не могу адресовать с текущим предполагаемым сегментным регистром)
Исправление:
mov ax, ds:p
add ax, ds:q
mov es:r,ax
Программа нормально транслируется и работает. В листинге мы видим строку
15 0011 26: A3 0000r mov es:r,ax
В коде команды присутствует префикс переопределения сегмента.
Неудобно перед всеми именами переменных расставлять префиксы. Избежать этого можно применяя директиву ASSUME. Перед строкой с меткой s вставим директиву:
ASSUME ds:data1, es:data2
Теперь префиксы можно снять. А в листинге мы увидим, что префикс добавляется автоматически:
16 0011 26: A3 0000r mov r,ax
Сама по себе директива ASSUME не загружает регистры DS и ES нужными значениями. Она дает Ассемблеру информацию о связи сегментного регистра с соответствующим сегментом.
Внесем в программу еще одно изменение: после команды mov r,ax добавим команды:
cmp r, 4
jg t
inc ax
t:
(Искать в этих командах скрытого смысла не следует.)
**Error** s1.ASM(18) Near jump or call to different CS
В сегменте кода появилась ссылка на символическое имя t. Чтобы избежать сообщения об ошибке нужно добавить директиву
ASSUME cs:cseg
а еще проще добавить к уже имеющейся директиве еще один параметр
ASSUME ds:data1, es:data2, cs:cseg
Сообщение об ошибке исчезнет.