В.Г. Баула - Введение в архитектуру ЭВМ и системы программирования (1110549), страница 24
Текст из файла (страница 24)
L: add ax,A[bx]
jno L1
; Обнаружена ошибка
pop bx
pop cx
pop ax
extrn Error:near
jmp Error
L1: add bx,type A
loop L
mov Summa,ax
pop bx
pop cx
pop ax; восстановление регистров
ret
Code ends
end
Наш второй модуль не является головным, поэтому в его директиве end нет метки первой команды программы. Модуль p2.asm имеет три внешних имени A,N и Error и две входные точки с именами Sum и Summa. Так как второй модуль не производит никаких операций ввода/вывода, то он не подключает к себе файл io.asm. Оба наших модуля используют общий стек объёмом 64 слова, что, наверное, достаточно, так как стековый кадр процедуры Sum невелик.
Разберём работу нашей программы. После ввода массива A головной модуль вызывает внешнюю процедуру Sum. Это статическая связь модулей по управлению, дальний адрес процедуры Sum будет известен головному модулю до начала счёта. Этот адрес будет расположен в формате i32 на месте операнда Sum команды call Sum .
Между основной программой и процедурой установлены следующие (нестандартные) соглашения о связях. Суммируемый массив знаковых чисел расположен в головном модуле и имеет общедоступное имя A. Длина массива является общедоступной константой с именем N, описанной в головном модуле. Вычисленная сумма массива помещается в общедоступную переменную с именем Summa, описанную во втором модуле. Всё это примеры статических связей по данным. Наша программа не содержит динамических связей по данным, в качестве примера такой связи можно привести передачу параметра по ссылке в процедуру другого модуля. Действительно, адрес переменной становится известным процедуре только во время счёта программы, когда он передан ей вызывающей программой (обычно в стеке).
В том случае, если при суммировании массива обнаружена ошибка (переполнение), второй модуль передаёт управление на общедоступную метку с именем Error, описанную в головном модуле. Остальные имена являются локальными в модулях, например, обратите внимание, что в обоих модулях используется одинаковая метка c именем L.
Здесь необходимо отметить важную особенность использования внешних адресов. Рассмотрим, например, команду
L: add ax,A[bx]
во втором модуле. При получении из этого предложения языка Ассемблера машинной команды необходимо знать, по какому сегментному регистру базируется наш внешний адрес A. На это во втором модуле (а только его и видит во время перевода программа Ассемблера, первый модуль недоступен!) указывает местоположение директивы
extrn A:word; Внешний адрес
внутри второго модуля. Эта директива располагается в сегменте с именем Data, а директива
assume cs:Code,ds:Data
определяет, что во время счёта на этот сегмент будет установлен регистр ds. Следовательно, адрес A соответствует области памяти в том сегменте, на который указывает регистр ds.1 Как видим, директива assume нам снова пригодилась.
Продолжим рассмотрение работы нашей программы. Получив управление, процедура Sum сохраняет в стеке используемые регистры (эта часть соглашения о связях выполняется), и накапливает сумму всех элементов массива A в регистре ax. При ошибке переполнения процедура восстанавливает значения регистров и передаёт управление на метку Error в головном модуле. В нашем примере второй вызов процедуры Sum специально сделан так, чтобы вызвать ошибку переполнения. Заметим, что переход на внешнюю метку Error – это тоже статическая связь по управлению, так как адрес метки известен до начала счёта. В то же время возврат из внешней процедуры по команде ret является динамической связью по управлению, так как конкретный адрес возврата в другой модуль будет помещён в стек только во время счёта программы.
Программа Ассемблера не в состоянии перевести каждый исходный модуль в готовый к счёту фрагмент программы на машинном языке, так как, во-первых, не может определить внешние адреса модуля, а, во-вторых, не знает будущего расположения сегментов модуля в памяти. Говорят, что Ассемблер переводит исходный модуль на специальный промежуточный язык, который называется объектным языком. Следовательно, программа Ассемблер преобразует входной модуль в объектный модуль. Полученный объектный модуль оформляется в виде файла, имя этого файла обычно совпадает с именем исходного файла на языке Ассемблер, но имеет другое расширение. Так, наши исходные файлы p1.asm и p2.asm будут переводиться (или, как чаще говорят, компилироваться или транслироваться) в объектные файлы с именами p1.obj и p2.obj.
Рассмотрим теперь, чего не хватает в объектном модуле, чтобы быть готовым к счёту фрагментом программы на машинном языке. Например, самая первая команда всех наших программ
mov ax,Data
должна переводится в машинную команду пересылки формата mov ax,i16 , однако значение константы i16, которая равна физическому адресу начала сегмента Data в памяти, делённому на 16, неизвестна программе Ассемблера и поле операнда i16 в команде пересылки остаётся незаполненным. Таким образом, в объектном модуле некоторые адреса остаются неизвестными (неопределёнными). До начала счёта программы, однако, все такие адреса обязательно должны получить конкретные значения.
Объектный модуль, получаемый программой Ассемблера, состоит из двух частей: тела модуля и паспорта (или заголовка) модуля. Тело модуля состоит из сегментов, в которых находятся команды и переменные нашего модуля, а паспорт содержит описание структуры объектного модуля. В этом описании содержатся следующие данные об объектном модуле.
-
Сведения обо всех сегментах модуля (длина сегмента, его спецификация).
-
Сведения обо всех общедоступных (экспортируемых) именах модуля, с каждым таким именем связан его тип (abs,byte,word,near и т.д.) и адрес (входная точка) внутри какого-либо сегмента модуля (для константы типа abs это просто целое число).
-
Сведения о местоположении и типе всех внешних адресов модуля.
-
Сведения о местоположении всех остальных незаполненных адресов в модуле, для каждого такого адреса содержится информация о способе его заполнения перед началом счёта.
-
Другая информация, необходимая для сборки программы из модулей.
На рис. 10.1 показано схематическое изображение объектных модулей p1.obj и p2.obj, полученных программой Ассемблера, для каждого модуля изображены его сегменты, входные точки и внешние адреса. Вся эта информация содержится в паспортах объектных модулей.2
p1.obj | p2.obj | ||
St segment stack | Data segment public extrn A:word extrn N:abs public Summa:word | ||
public A:word public N:abs extrn Summa:word | A N Summa | A N Summa | |
Extrn Sum:far public Error:near | Sum Error | Sum Error | Code segment public public Sum:far extrn Error:near |
Рис. 10.1. Схематический вид объектных модулей с внешними адресами и входными точками. |
Обратимся теперь к проблеме сборки программы из модулей. Как мы уже упоминали, эту работу выполняет специальная системная программа, которая называется редактором внешних связей. Из этого названия хорошо видно одно из назначений этой программы – устанавливать связи между внешними адресами и входными точками модулей. Рассмотрим схему работы редактора внешних связей на нашем примере.
10.2. Схема работы редактора внешних связей.
Целью работы редактора внешних связей является построение из объектных модулей почти готового к счёту программного модуля, который называется загрузочным модулем. Загрузочный модуль всё ещё не является полностью готовой к счёту программой на машинном языке, в этом модуле остаются незаполненными некоторые поля. Например, наша команда
mov ax,Data
всё ещё будет иметь незаполненное поле Data формата i16 на месте второго операнда, так как конкретное значение этого поля будет известно только перед самым началом счёта программы, когда все её сегменты будут размещены в памяти компьютера.
При вызове редактора внешних связей ему в качестве параметров передаются имена всех объектных модулей, а также имя загрузочного модуля, который необходимо построить. Для нашего примера вызов редактора внешних связей (его имя link) будет выглядеть, например, так
link p1+p2+ioproc,p
Здесь p1,p2 и ioproc – имена объектных модулей (не забывайте о третьем объектном модуле с именем ioproc), а p – имя загрузочного модуля, который надо построить.1 Первый из перечисленных объектных модулей считается головным модулем, с него начинается процесс сборки загрузочного модуля. Работа редактора внешних связей включает в себя два этапа. На первом этапе происходит обработка сегментов, а на втором – собственно редактирование внешних связей и построение загрузочного модуля (загрузочные модули для нашего компьютера имеют расширение .exe). Разберёмся сначала с первым этапом.
В нашем примере (если не принимать во внимание объектный модуль ioproc.obj) имеется пять сегментов: три сегмента с именами St,Data и Code в модуле p1.obj и два сегмента с именами Data и Code в модуле p2.obj. Спрашивается, сколько сегментов будет в загрузочном модуле p.exe ? Здесь логически возможны три случая.
-
Все сегменты переходят в загрузочный модуль. В этом случае в нашем модуле p.exe должно было бы быть 5 сегментов: один стековый, два кодовых и два сегмента данных.
-
Некоторые из сегментов склеиваются, то есть один сегмент присоединяется в конец другого сегмента.
-
Некоторые из сегментов накладываются друг на друга (если сегменты имеют разную длину, то, конечно, более длинный сегмент будет "торчать" из-под более короткого сегмента). Разумеется, почти всегда накладывать друг на друга имеет смысл только сегменты данных, в этом случае у нескольких модулей будут общие сегменты данных (или, как иногда говорят, общие области данных).
Как именно будут обрабатываться сегменты при сборке загрузочного модуля из объектных модулей, определяет программист, задавая определённые параметры в директивах segment. Существуют следующие параметры, управляющие обработкой сегментов.
Параметр public у одноимённых сегментов означает их склеивание.1 Так как сборка начинается с головного модуля, то из двух одноимённых сегментов с параметром public сегмент из головного модуля будет первым, в его конец будут добавляться соответствующие сегменты из других объектных модулей. В том случае, если одноимённые сегменты с параметром public встречаются не в головном модуле, то их порядок при склейке определяется конкретным редактором внешних связей (надо читать документацию к нему).2
Для нашего примера сегмент данных с именем Data объектного модуля p2.obj будет добавлен в конец одноимённого сегмента данных головного модуля p1.obj. Такая же операция будет проведена и для сегментов кода этих двух модулей. Таким образом, в загрузочном модуле останутся только три сегмента: сегмент стека St, сегмент данных Data и кодовый сегмент Code. При склейке кодовых сегментов редактору внешних связей придётся изменить некоторые адреса в командах перехода внутри добавляемого модуля. Правда, как легко понять, меняются адреса только в командах абсолютного перехода и не меняются относительные переходы (это ещё одно достоинство команд перехода, которые реализуют относительный переход).