Chapter_10 (1110562), страница 2
Текст из файла (страница 2)
1 Действительно, чтобы выполнить команду из другого модуля, а также считать или записать значение в переменную, нужно знать месторасположение (адрес) этой команды или переменной. Заметим, что численные значения связей между модулями (значения адресов) невозможно установить на этапе компиляции модуля. Так происходит потому, что, во-первых,компилятор "видит" только один этот модуль, и, во-вторых, будущее расположение модулей (ихсегментов) в памяти во время счёта на этапе компиляции, как правило, неизвестно.Связи между модулями будем называть статическими, если численные значения этих связей(т.е.
адреса) известны сразу после размещения программы в памяти компьютера, но до начала счётапрограммы (до выполнения её первой команды). В отличие от статических, значения динамическихсвязей между модулями становятся известны только во время счёта программы. Вскоре мы приведём примеры статических и динамических связей, как по данным, так и по управлению.На языке Ассемблера (явные) статические связи между модулями задаются с помощью специальных директив. Директиваpublic <список имён модуля>объявляет перечисленные в этой директиве имена общедоступными (public), т.е. разрешает использование этих имён в других модулях.
Общедоступными можно сделать только имена переменных, меток (в частности, имён процедур) и целочисленных констант. В некоторых модульных системах программирования про такие имена говорится, что они экспортируются в другие модули. 2 Впринципе, вместе с каждым именем может экспортироваться и его тип. Как мы уже знаем, в Ассемблере для имён, использованных в директивах резервирования памяти, тип имени определяетдлину области памяти, а для меток тип равен –1 , если метка описана в текущем модуле и –2 длявнешней метки, описанной в другом модуле (как описать внешние метки мы узнаем чуть ниже). Остальные имена (имена сегментов, имена констант в директивах числовой эквивалентности и другие)имеют тип ноль.
Тип имени в принципе позволяет проводить контроль использования этого имени вдругом модуле. Все остальные имена модуля, кроме имён, перечисленных в директивах public, являются локальными и не видны извне (из других модулей). 3Экспортируемые имена одного модуля, перечисленные в директиве public, не становятся автоматически доступными в других модулях на Ассемблере. Для получения доступа к таким именам, этот другой модуль должен, с помощью специальной директивы, явно объявить о своём желании использовать общедоступные имена других модулей. Это делается с помощью директивыextrn <имя:тип>,...,<имя:тип>1Это верно и для случая, когда один модуль использует константу, определённую в другом модуле, таккак константа – это чаще всего тоже целое число (непосредственное значение формата i8 или i16).
Кроме того, мы не будем рассматривать связи между модулями с помощью внешних файлов, в этом случае связи задаются уже не целыми числами, а чаще всего текстовыми строками – именами таких файлов.2В некоторых языках имена одного модуля по умолчанию считаются доступными из других модулей, если они описаны или объявлены в определённой части первого модуля. Например, в модуле на языке С общедоступны все имена, описанные вне функций, если про них явно не сказано, что это локальные имена модуля.3Как мы узнаем позже, имена сегментов можно сделать видимыми из других модулей, но другим, болеесложным, способом.4В этой директиве перечисляются внешние имена, которые используются в этом модуле, но неописаны в нём.
Внешние имена должны быть описаны и объявлены общедоступными в каких-тодругих модулях. Вместе с каждым внешним именем объявляется и тип, который должно иметь этоимя в другом модуле. Проверка того, что это имя в другом модуле на самом деле имеет такой тип,может проводиться только на этапе сборки из модулей готовой программы, о чём мы будем говоритьдалее.Таким образом, для установления связи между двумя модулями на Ассемблере первый модульдолжен разрешить использовать некоторые из своих имён в других модулях, а второй модуль – явно объявить, что он хочет использовать внутри себя такие имена. В языке Ассемблера общедоступные имена называются входными точками модуля, что хорошо отражает суть дела, так как тольков эти точки возможен доступ к модулю извне (из других модулей).
Внешние имена модуля называются внешними адресами, так как это адреса областей памяти и команд, а также целочисленныезначения констант в других модулях.Все программы, которые мы писали до сих пор, на самом деле состояли из двух модулей, ноодин из них с именем ioproc.asm мы не писали сами, он поставлялся нам в готовом виде авторомучебника [5].
Этот второй модуль содержит процедуры ввода/вывода, к которым мы обращаемся спомощью макрокоманд (inint, outint и других). Теперь настало время написать программу, которая будет содержать два наших собственных модуля, а третьим, как и раньше, будет модуль с именем ioproc.asm (так как без ввода/вывода в настоящих программах нам, скорее всего, не обойтись).В качестве примера напишем программу, которая вводит массив A знаковых целых чисел и выводит сумму всех элементов этого массива.
Сделаем следующее разбиение нашей задачи на подзадачи-модули. Ввод массива и вывод результатов, а также выдачу диагностики об ошибках будет выполнять головной модуль нашей программы, а подсчёт суммы элементов массива будет выполнятьпроцедура, расположенная во втором модуле программы. Для иллюстрации использования (статических) связей между модулями мы не будем делать процедуру суммирования полностью со стандартными соглашениями о связях, она будет использовать внешние имена для получения своих параметров, выдачи результата работы и диагностики об ошибке.Текстовый файл, содержащий первый (головной) модуль нашей программы на Ассемблере, мы,не долго думая, назовём p1.asm, а файл с текстом второго модуля, содержащим процедуру суммирования массива, назовём p2.asm.
Ниже приведён текст первого модуля.; p1.asm; Ввод массива, вызов внешней процедурыinclude io.asmStksegment stackdw64 dup (?)StkendsNequ1000Data segment publicAdwN dup (?)public A,N; Входные точкиextrn Summa:word; Внешняя переменнаяDiagn db'Переполнение!',13,10,'$'Data endsCode segment publicassume cs:Code,ds:Data,ss:StkStart:movax,Datamovds,axmovcx,Nsubbx,bx; Индекс массиваL:inint A[bx]; Ввод массива Aaddbx,type Aloop Lextrn Sum:far; Внешнее имяcall Sum; Процедура суммированияoutint Summanewline5; А теперь вызов с заведомой ошибкойmovA,7FFFh; MaxintmovA+2,1; Чтобы было переполнениеcall Sumoutint Summa; Сюда возврата не будет!newlinefinish ; Вообще-то не нуженpublic Error; Входная точкаError:leadx,T; ДиагностикаoutstrfinishCode endsendStart; Это головной модульВ нашем головном модуле три входные точки с именами A,N и Error и два внешних имени:Sum, которое имеет тип дальней метки, и Summa, которое имеет тип слова.
Работу программы подробно рассмотрим после написания текста второго модуля с именем p2.asm.Comment * модуль p2.asmСуммирование массива, контроль ошибок, директиваinclude io.asm не нужна, т.к. нет ввода/вывода.Используется стек головного модуля.В конечном end не нужна метка Start*Data segment publicSumma dw?public Summa; Входная точкаextrn N:abs; Внешняя константаextrn A:word; Внешний массивData endsCode segment publicassume cs:Code,ds:Datapublic Sum; Входная точкаSumproc farpush axpush cxpush bx; Сохранение регистровxorax,ax; ax:=0movcx,Nxorbx,bx; Индекс 1-го элементаL:addax,A[bx]joVoz; возвратaddbx,type Aloop LmovSumma,axVoz: popbxpopcxpopax; Восст. регистровjoErr; Обнаружена ошибкаretErr: addSP,4; Удаление возвратаextrn Error:nearjmpError; В первый модульCode endsendНаш второй модуль не является головным, поэтому в его конечной директиве end нет меткипервой команды программы. Модуль p2.asm имеет три внешних имени A,N и Error и две входные точки с именами Sum и Summa.
Так как второй модуль не производит никаких операций ввода/вывода, то он не подключает к себе файл io.asm по директиве include. Оба наших модуля6используют общий стек объёмом 64 слова, что, наверное, достаточно, так как стековый кадр процедуры Sum невелик (всего 5 слов).Разберём работу нашей программы. После ввода массива A головной модуль вызывает внешнююпроцедуру Sum. Это статическая связь модулей по управлению, дальний адрес процедуры Sumбудет известен головному модулю до начала счёта. Этот адрес будет расположен в форматеi32=seg:off на месте операнда Sum команды call Sum = call seg:off .Между основной программой и процедурой установлены следующие (нестандартные) соглашения о связях. Суммируемый массив знаковых чисел расположен в сегменте данных головного модуля и имеет общедоступное имя A.
Длина массива является общедоступной константой с именем N,также описанной в головном модуле. Вычисленная сумма массива помещается в общедоступную переменную с именем Summa, описанную во втором модуле. Всё это примеры статических связеймежду модулями по данным. Наша программа не содержит динамических связей по данным, в качестве примера такой связи можно привести передачу параметра по ссылке в процедуру другого модуля.
Действительно, адрес переданной по ссылке переменной становится известным вызваннойпроцедуре только во время счёта программы, когда он передан ей основной программой (обычно встеке).В том случае, если при суммировании массива обнаружена ошибка (переполнение), второй модуль передаёт управление на общедоступную метку с именем Error, описанную в головном модуле. Остальные имена являются локальными в модулях, например, обратите внимание, что в обоихмодулях используются две метки с одинаковым именем L.Здесь необходимо отметить важную особенность использования внешних адресов. Рассмотрим,например, командуL:add ax,A[bx]во втором модуле. При получении из этого предложения языка Ассемблера машинной команды необходимо знать, по какому сегментному регистру осуществляет доступ наш внешний адрес A.
На этово втором модуле (а только его и видит во время перевода программа Ассемблера, первый модульнедоступен!) указывает местоположение директивыextrn A:word; Внешняя переменнаяЭта директива располагается в сегменте с именем Data, а директиваassume cs:Code,ds:Dataопределяет, что во время счёта на этот сегмент будет установлен регистр ds. Следовательно, адрес Aсоответствует области памяти в том сегменте, на который указывает регистр ds.
1 Как видим, директива assume нам здесь снова пригодилась.Продолжим рассмотрение работы нашей модульной программы. Получив управление, процедура Sum сохраняет в стеке используемые регистры (эта часть соглашения о связях у нас выполняется),и накапливает сумму всех элементов массива A в регистре ax. При ошибке переполнения процедуравосстанавливает запомненные значения регистров, удаляет из стека дальний адрес возврата (4 байта)и выполняет команду безусловного перехода на метку Error в головном модуле. В нашем примеревторой вызов процедуры Sum специально сделан так, чтобы вызвать ошибку переполнения.