м проходе уже была построена
По этой директиве на 1- м проходе уже была построена ТРСР. Однако в программе директива ASSUME может встречаться многократно, поэтому состояние этой таблицы в конце 1-го прохода может не соответствовать ее состоянию после первой из директив ASSUME. В связи с этим ассемблер на 2-м проходе заново строит ТРСР после первой из директив ASSUME и затем меняет таблицу после каждой новой такой директивы.
Команда: ADD X,K
Обработка команд на 2-м проходе во многом осуществляется так же, как и на 1-м проходе. По ТИ ассемблер узнает, что имя X
описано в сегменте S2, а по ТРСР узнает, что этому сегменту поставлен в соответствие сегментный регистр DS. Следовательно, запись X - это сокращение адресной пары DS:X. Поскольку регистр DS из этой пары совпадает с регистром, подразумеваемого по умолчанию в команде ADD, то перед этой командой ассемблер не вставит префикс сегментного регистра. (Если бы имя X было описано в сегменте, на который, согласно ТРСП, указывает регистр ES, то ассемблер записал бы префикс ES: в очередной свободный байт памяти и затем увеличил бы АДР на 1.)
Далее ассемблер формирует собственно команду. По ТИ он узнает типы операндов (m16 и i16) и затем по таблице мнемокодов узнает, что команда сложения при таких типах операндов имеет КОП 81 06, который записывает в следующие два байта памяти. После этого ассемблер формирует операнды машинной команды: узнав по ТИ адрес имени X и значение константы K, ассемблер записывает этот адрес и это число в очередные байты памяти. На этом формирование машинной команды закончено. АДР увеличивается на число байтов, занятых командой.
Директива END.
Встретив эту директиву, ассемблер завершает 2-й проход. Машинная программа сформирована, ассемблер записывает ее во внешнюю память и на этом заканчивает свою работу.
Как видно, 2-й проход выполняется достаточно просто. Это объясняется тем, что значительная часть работы была проделана на 1-м проходе.
1.5 МНОГОМОДУЛЬНЫЕ ПРОГРАММЫ.
Мы рассмотрели основные действия ассемблера, выполняемые при трансляции программы, написанной на ЯА, в том случае, когда программа состоит только из одного модуля, когда в этом модуле нет внешних и общих имен. Теперь рассмотрим, какие изменения надо внести в работу ассемблера в случае многомодульной программы.
Структура объектного модуля.
Начнем со следующего важного замечания: в общем случае ассемблер не может довести до конца трансляцию программы, данной ему на входе, и основных причин тому две.
Первая - наличие внешних имен. Если ассемблер транслирует один из модулей многомодульной программы и в нем используются внешние имена, т.е. имена из других модулей, то, транслируя этот модуль независимо от других, ассемблер, естественно, не может оттранслировать эти имена, т.е. не может заменить их на соответствующие им адреса. Эти адреса станут известными позже, на этапе объединения модулей в единую программу, только тогда и появится возможность сделать эти замены.
Вторая причина - наличие имен сегментов. Например, в командах
MOV AX,S2
MOV DS,AX
имя сегмента S2 должно быть заменено на начальный адрес (без последнего 0) соответствующего сегмента памяти, но этот адрес зависит от того, с какого места памяти будет располагаться вся программа при счете. Если, скажем, сегмент S2 является самым первым в программе и если программа размещена с адреса 50000h, тогда имя S2 надо заменять на 5000h, но если программа размещена с адреса 70000h, то имя S2
надо заменять на 7000h. Заранее же начальный адрес программы неизвестен, поэтому ассемблер и не знает, на что заменять имена сегментов. Это станет известным позже, непосредственно перед выполнением программы, тогда и появится возможность сделать эти замены.
Отметим, что адреса, зависящие от места расположения программы в памяти, принято называть перемещаемыми адресами. Имена сегментов - пример таких адресов. (Других перемещаемых адресов в языке MASM нет, хотя в иных ЯА имеются и другие примеры перемещаемых адресов.) Так что второй причиной, по которой ассемблер не может довести трансляцию до конца, являются перемещаемые адреса. Отметим также, что проблема с этими адресами возникает в любой программе - как многомодульной, так и одномодульной.
Итак, имеется ряд вещей, которые ассемблер не может оттранслировать, которые можно дотранслировать только позже. Учитывая это, ассемблер поступает так: все, что может, он транслирует, а то, что не может оттранслировать, он пропускает, оставляет как бы пустые места, но при этом запоминает информацию об этих "пустых" местах, по которой затем можно будет их дотранслировать. В связи с этим при трансляции модуля ассемблер выдает на выходе на самом деле не модуль в полностью оттранслированном виде, а некоторую заготовку его, которую принято называть объектным модулем
(ОМ).
Объектный модуль состоит из двух частей - заголовка и тела. Тело модуля - это машинный код модуля, правда, некоторые места в нем, как уже сказано, недотранслированы. В заголовке же собрана информация, по которой затем можно будет дотранслировать эти места и объединить этот модуль с другими модулями программы.
В упрощенном виде структура заголовка ОМ состоит из следующих разделов: 1) таблица сегментов; 2) точка входа; 3) сегмент стека; 4) таблица общих имен; 5) таблица вхождений внешних имен; 6) таблица перемещаемых адресов.
Прежде чем объяснить смысл этой информации, сделаем такое замечание. В заголовке приходится ссылаться на ячейки внутри тела модуля. Эти ссылки задаются в виде пар s:ofs, где s - символьное имя сегмента, которому принадлежит ячейка, а ofs
- смещение ячейки внутри этого сегмента.
Таблица сегментов.
Напомним, что во время своей работы ассемблер строит несколько таблиц, в том числе таблицу сегментов. Эта таблица и переносится в заголовок ОМ.
Точка входа.
Если модуль является головным в программе, т.е. с него должно начинаться выполнение программы, тогда в его директиве END указывается точка входа - метка той команды модуля, с которого надо начинать выполнение программы. Адрес этой метки и записывается в заголовок. Данный адрес определяется просто: эта метка - одно из имен, описанных в модуле, поэтому информация о метке имеется в таблице имен (ТИ), которую строит ассемблер, а в этой таблице для каждого имени указываются среди прочего имя сегмента, в котором оно описано, и смещение имени внутри сегмента. Когда ассемблер доходит до директивы END и встречает в ней метку, то по ТИ он узнает адрес этой метки, который и записывает в заголовок (например, S3:0).
В заголовке остальных, не головных, модулях как-то помечается, что точки входа нет.
Сегмент стека.
Как известно, если при описании сегмента стека в его директиве SEGMENT указан параметр STACK, тогда перед началом программы регистры SS и SP должны быть автоматически установлены на этот сегмент. Естественно, надо знать, какой из сегментов является стеком. Определяется этот сегмент просто: когда ассемблер встречает директиву SEGMENT с параметром STACK, то имя этого сегмента (например, S1) он заносит в заголовок ОМ.
Таблица общих имен (ТОБ).
Общим называется имя, которое указано в директиве PUBLIC (например, PUBLIC B). Содержательно - это имя, описанное в данном модуле, но доступное для всех остальных модулей программы. При объединении модулей в единую программу в тех модулях, где это имя используется, надо будет заменить его на его адрес внутри данного модуля. Ясно, что ассемблер, транслирующий модули по отдельности, не может сделать эту замену. Он ее и не делает, однако для будущего запоминает информацию о всех общих именах модуля и их адресах внутри модуля. Эта информация и образует ТОБ.
Построить такую таблицу просто. Во-первых, все общие имена описаны в модуле, поэтому информация о них имеется в ТИ. Во-вторых, общие - это те имена, которые перечислены в директиве PUBLIC. Поэтому, встречая (на 2-м проходе) директиву PUBLIC, ассемблер для каждого из указанного здесь имени извлекает информацию из ТИ и заносит ее в ТОБ.
Например, для следующего модуля (слева) будет создана такая ТОБ (справа):
PUBLIC B
EXTRN X:WORD, P:FAR
S2 SEGMENT DATA общее
имя его
адрес
A DW X ----------------------
B DW SEG X, ? B S2:2
C DD P ...
...
Таблица вхождений внешних имен (ТВН).
Внешним называется имя, указанное в директиве EXTRN (например, EXTRN X:BYTE, P:FAR). Содержательно - это имя, которое используется в данном модуле, но описано в другом модуле. Ясно, что, транслируя данный модуль независимо от других, ассемблер не знает адреса внешних имен, поэтому не может заменить их на адреса.
Такую замену внешнего имени на адрес можно будет сделать только позже, на этапе объединения модулей в единую программу, когда будет известна информация о всех модулях. Пока же ассемблер в соответствующую ячейку объектного модуля записывает 0, но фиксирует, что позже в эту ячейку надо будет записать адрес внешнего имени. Такая информация о каждом вхождении
в модуль каждого внешнего имени и запоминается в ТВН. Например, для указанного выше модуля будет создана такая ТВН:
внеш.имя адрес вхождения тип вхождения
------------------------------------------
X S2:0 ofs
X S2:2 seg
P S2:6 segofs
...
Здесь "адрес вхождения" - это адрес той ячейки текущего модуля, в которую надо будет затем вставить адрес внешнего имени, указанного в первой колонке. Однако только этой информации мало. Дело в том, что в разных случаях под "адресом внешнего имени" понимаются разные вещи. Например, в директиве DW X (или в команде MOV AL,X) имя X надо заменять на смещение (ofs) этого имени, а в директиве DW SEG X (или в команде MOV AX,SEG X) - на начальный адрес (без последнего 0) того сегмента, где имя описано (на seg). Что касается директивы DD P (или команды CALL P), то имя P должно заменяться на полный адрес (на адресную пару seg:ofs). На какую именно часть своего полного адреса должно заменяться внешнее имя - отмечается (подходящим образом) в колонке "тип вхождения".
Таблица перемещаемых адресов (ТПА).
Внешние имена - это не единственная вещь, которую ассемблер не может оттранслировать до конца. Как уже сказано, ассемблер не может оттранслировать и имена сегментов. Такие имена надо заменить на начальные адреса сегментов (без последнего 0) в памяти, но эти адреса ассемблер не знает. Они станут известны позже, только перед выполнением программы.
Но что делать сейчас ассемблеру с именем сегмента? В соответствующую ячейку модуля он записывает 0 и при этом запоминает адрес данной ячейки и имя сегмента, чтобы позже можно было сделать замену имени на адрес. Эта информация о каждом вхождении каждого имени сегмента фиксируется в ТПА.
Например, для следующего модуля (слева) будет создана такая ТПА (справа):
S2 SEGMENT DATA ТПА:
... имя сегмента адрес вхождения
S2 ENDS ------------------------------
S3 SEGMENT CODE S2 S3:1
ASSUME DS:S2,CS:S3 ...
BEG: MOV AX,S2
MOV DS,AX
...
(Замечание: указанные две символьные команды транслируются в следующие машинные команды:
0: B8 0000
3: 8E D8
поэтому адрес ячейки, куда надо затем занести начальный адрес сегмента S2, равен S3:1.)
Вот такая информация входит в заголовок объектного модуля. Когда ассемблер оттранслирует модуль (получит тело ОМ) и построит его заголовок, он записывает получившийся ОМ во внешнюю память и на этом заканчивает свою работу.