Редактор связей

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

Программа редактирования связей LINK (LINK.EXE), записанная на той же дискете, что и другие распространенные в DOS программы, фактически реализует две различные функции. Во-первых, она может связать много различных объектных модулей в одну программу. Во-вторых, на основе ассемблерного объектного модуля редактор связи формирует выполняемый загружаемый модуль. Рассмотрим по отдельности обе эти функции программы LINK.

Многомодульность

Как указывает само имя программы LINK, ее основное назначение "связать", или объединить, несколько объектных модулей в один выполняемый модуль. Все рассмотренные до сих пор примеры относились к одномодульным программам, т.е. все, что они должны были выполнять, реализовывалось в одном исходном модуле. Однако этот путь не всегда возможен или желателен.

Имеется ряд причин, в силу которых программа программа сожет быть разбита на несколько модулей. Во-первых, размер программы. Редактирование очень большой программы становится весьма громоздким и трудным делом, а ее ассемблирование занимает много времени. Предположим, что вы допустили ошибку в единственной строке ассемблерной программы, состоящей из 5000 строк. Чтобы изменить одну эту строку, необходимо выполнить редактирование всей программы. После этого нужно оттранслировать всю программу из 5000 строк, что занимает довольно много времени. После относительно быстрого этапа редактирования связей программа готова к выполнению.

Предположим, теперь, что вместо того, чтобы иметь дело с программой, состоящей из 5000 строк, вы разбиваете ее на десять программых модулей, по 500 строк в каждом. Для внесения изменений в единственную строку вам необходимо выполнить редактирование только исходного файла из 500 строк. Ассемблирование программы из 500 строк занимает значительно меньше времени, чем программы из 5000 строк. Этап редактирования связей все так же будет занимать относительно немного времени, особенно по сравнению с ассемблированием большой программы. Уменьшение размеров отдельных модулей позволяет более быстро осуществить процесс редактирования - ассемблирования.

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

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

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

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

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

Операторы EXTRN и PUBLIC

Редактор связей не может, однако, выполнить все, о чем говорилось выше, самостоятельно. Ассемблер должен получить от программиста информацию о подпрограммах, относящихся к другому программному модулю. Это выполняется с помощью оператора PUBLIC, извещающего ассемблер о том, что данное символическое имя доступно другим программам. Кроме того, программист указывает ассмеблеру, какие из символических имен является внешними для данного программного модуля. В языке ассемблера это реализуется оператором EXTRN, который объявляет соответствующее имя внешним для текущего ассемблирования, чтобы оно могло быть правильно обработано. Ассемблер помечает данную команду таким образом, чтобы редактор связей мог впоследствии найти ее и вставить туда правильное значение адреса.

Оператор EXTRN выполняет две функции. Во-первых, он сообщает ассемблеру, что указанное символическое имя является внешним для текущего ассемблирования. Конечно, на этом этапе ассемблер мог бы считать, что любое имя, не идентифицированное им в процессе ассемблирования, является внешним. Однако, если когда-нибудь вы ошиблись в указании имен, то ассемблер решит, что имеется в виду внешнее имя, и не выдает сообщения об ошибке. Это отложит индикацию ошибки до этапа редактирования связей. Для большинства программистов это слишком поздно, особенно, если речь идет о чем-то простом, вроде описки. Таким образом, ассемблер индицирует ошибку в случае любого не определенного им символического имени.

Вторая функция оператора EXTRN состоит в том, что он указывает ассемблеру тип соответствующего символического имени. Так как ассемблирование является очень формальной процедурой, то ассемблер должен знать, что представляет из себя каждый символ. Это позволяет ему генерировать правильные команды. В случае данных оператор EXTRN может указывать на байт, двойное слово или другой типовой элемент. Тип имени подпрограммы или другой программной метки может быть либо NEAR, либо FAR, в зависимости от того, в каком сегменте она находится. От программиста требуется указать в операторе EXTRN тип символического имени. Так как кроме того ассемблером осуществляется посегментная адресация программы, то оператор EXTERN указывает на сегмент, в котором появляется данный идентификатор. Это не входит в синтаксис оператора EXTRN, а определяется местоположением этого оператора в программе. Ассемблер считает, что внешнее имя относится к тому же сегменту, в котором появляется оператор EXTERN для этого символического имени.

На рис.П5.13 приведен пример ассемблерной программы, иллюстрирующей использование оператора EXTRN. Здесь имеются два имени, являющиеся внешними для данной программы. OUTPUT_CHARACTER обозначает однобайтовую переменную. Соответствующий этой переменной атрибут ":BYTE" указывается после имени переменной. Указатель NEAR программной метки OUTPUT_ROUTINE говорит о том, что она находится в том же сегменте. Хотя приведенная на рис.П5.13 прогграмма содержит ссылки на эти символические имена, при трансляции ассемблер знает, как ему сегментировать правильные команды. Если бы оператор EXTRN отсутствовал в программе, то в этом случае ассемблер инициализировал бы ошибки. Из ассемблерного листинга видно, что после поля адреса в командах, ссылающихся на внешние имена, стоит символ E.

Рассмотрим эту же задачу с другой стороны. Каким образом редактор связей узнает о местоположении внешних имен? На рис.П5.14 приведена подпрограмма, на которую ссылается другая программа, относящаяся к рис.П5.13. Переменные и программные метки, на которые имеются ссылки в программе, на рис.П5.13, объявлены в подпрограмме с помощью оператора PUBLIC. Это означает, что их имена доступны для другого программного модуля. Ни на какие другие переменные или программные метки в этой программе, не указанные в операторе PUBLIC, ссылки в других программах невозможны. Хотя это может показаться неудобным, однако, если все имена имели бы атрибут PUBLIC, то возникла бы другая трудность. Это означало бы, что каждое имя в любом из модулей, которые вы могли бы связать между собой, должны быть уникальными, т.е. вы никогда бы не смогли использовать одно и то же символическое имя дважды в разных модулях. Это может быть серьезным препятствием для повторного использования некоторых подпрограмм, так как такое использование возможно и через несколько лет, а помнить все символические имена и следить за тем, чтобы ни одно из них не повторялось дважды довольно сложно. Заметьте, что в операторе PUBLIC не требуется указывать атрибуты имен: об этом заботятся обычные операторы языка ассемблера.

Программа LINK устанавливает соответствие между всеми внешними именами и соответствующими операторами PUBLIC, которые их объявляют. После этого редактор связей записывает правильные значения адресов в команды, где есть ссылки на внешние имена. Обрабатываются те поля в командах, рядом с которыми в ассемблерном листинге стоял символ "E".

Кроме того, ассемблер осуществляет объединение любых сегментов с одними тем же именем. В случае программ на рис.П5.13 и П5.14 основная программа и подпрограмма принадлежат одному и тому же сегменту с именем CODE. Так как в операторе EXTRN основной программы для программы OUTPUT_ROUTINE указан атрибут NEAR, то желательно, чтобы эта программа была в том же сегменте. Атрибут PUBLIC в операторе SEGMENT указывает редактору связей объединить оба программных модуля в один выполняемый сегмент.

В программе на рис.П5.13 есть еще один сегмент, который следует рассмотреть. Данная программа выполняется как программа типа .EXE. При передаче управления программе типа .EXE система DOS организует для этой программы стек. Информация для стека поступает от редактора связей, который записывает ее в головную метку файла типа .EXE. Подготовить все для стека обязан программист. Если он этого не сделает, то редактор связей выдает соответствующее сообщение. В обычной ситуации это не может служить препятствием для выполнения программы. Однако в таком случае параметры стека для программы выбираются по умолчанию, т.е. местоположение и размер стека могут оказаться неподходящими. За подготовку стека отвечает сегмент STACK, входящий в программу на рис.5.13. Его имя STACK и задание соответствующего атрибута равным STACK говорят о том, что это область памяти предназначена для стека. Редактор связей, кроме того, проверяет, правильно ли установлен указатель стека в момент, когда управление передается программе.

Операция связывания

Рассмотрим теперь операции, с помощью которых описанные выше программные модули были объединены в один исполняемый модуль. Ассемблирование программ выполняется с помощью команд, описанных в предыдущем разделе:

B:>A:MASM FIG5_13,,,

B:>A:MASM FIG5=14,,,

При этом получается два объектных модуля FIG5_13.OBJ и FIG5_14.OBJ. Для объединения этих модулей вызывается программа LINK. На рис.П5.15 приведены операции, с которых начинается работа программы LINK. В данном примере предполагается, что дискета с DOS установлена в дисководе A:, рабочая дискета - в дисководе B:, и дисковод B: выбирается по умолчанию. После запуска программа LINK запрашивает пользователя, для каких объектных файлов следует выполнить редактирование связей. Имена файлов вводятся без указания типа .OBJ. Если связываемых модулей больше одного, то их имена вводятся через разделитель "+". В данном примере выполняется редактирование связей для модулей FIG5_13 и FIG5_14.

Модули связываются в том же порядке, в каком их имена передаются программе LINK. В данном случае программа FIG5_13 предшествует программе FIG5_14. Перечисление модулей в обратном порядке привело бы к такому же обратному порядку их расположения в итоговом модуле. Как правило, порядок формирования программы безразличен. Единственное исключение делается для входной точки программы.

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

Далее запрашивается имя файла для хранения карты связей. Допускается любое имя, однако в режиме по умолчанию формирование карты не производится. В нашем примере ввод символа B: является указанием редактору связей записать карту связей на дисководе B:. Редактор связей выбрал для этого файла имя FIG5_13.MAP. Полученный в результате операции связывания файл FIG5_13.MAP приводится на рис.П5.16 и будет рассмотрен в следующем параграфе.

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

Карта связей

На рис.П5.16 приведена полученная в результате редактирования карта связей. Так как рассматриваемый пример прост, то и карта не очень содержательна. Каждому сегменту в исполняемом файле соответствует отдельная строка карты. В строке последовательно указаны значения начального и конечного адресов каждого сегмента в том виде, в каком они будут загружены в память. Заметьте, что размер сегмента CODE равен 3CH. Это является суммарным размером сегментов CODE в двух программных модулях. Другой сегмент в нашем примере - это сегмент STACK размером 80H байт.

По поводу адресов, приведенных в карте связи, нужно сделать следующие замечания. Во-первых, все они являются 20-битовыми адресами и отсчитываются от ячейки 0. Так как загрузка программы будет осуществляться DOS, то загрузчик изменит значения этих адресов. Однако относительно друг друга их значения останутся такими же. В отношении сегментов следует еще отметить, что в памяти они не располагаются последовательно. Хотя длина сегмента CODE равняется только 3CH, сегмент STACK начинается с адреса 40H. Сегменты должны располагаться на границах параграфов для того, чтобы адреса смещения сохраняли правильные значения. Благодаря привязке к границам параграфов, сегментные регистры указывают точно на первую ячейку сегмента. Следовательно, в нашем случае редактор связей расположил сегмент STACK на первой границе параграфа сразу же после конечного адреса сегмента CODE. С учетом того, что конец сегмента CODE равен 3BH, следующий адрес ячейки, который делится на 16, будет равен 40H.

Возможно вы заметили, что общая длина сегментов CODE в действительности не равна 3CH. В отсутствие других спецификаций редактор связей поместил начало каждой части сегмента CODE на границах параграфов. Длина первого модуля FIG5_13 равняется 2BH. Следующий модуль, FIG5_14, редактор связей поместил на следующей границе параграфа, в данном случае по адресу 30H. Так как длина второго модуля равна 0CH, общий размер сегмента CODE будет равен 03CH. Выравнивание по границам параграфов выполняется по умолчанию совместно с ассемблированием и редактированием связей.

Оператор языка ассемблера SEGMENT может, если нужно, изменить это выравнивание на побайтовое (BYTE), либо пословно (WORD). При выравнивании типа BYTE программы упаковываются в один сегмент. В смысле экономии памяти это наиболее эффективный способ объединения модулей. Однако выравнивание по границе параграфа гарантирует, что во время выполнения программы не возникает никаких трудностей с адресацией сегментов. Если в программе производятся вычисления адресов и выравнивание производилось не по границам параграфов, то возможно возникновение ошибок.

В конце карты связей указывается входная точка исполняемого файла. Этот адрес, как и другие, вычисляется по отношению к началу исполняемого модуля и перераспределяется загрузчиком. Имеется несколько способов указания стартового адреса для программы типа .EXE. При одном из способов программа выполняется, начиная с первого байта программного модуля. При этом вы должны следить за тем, чтобы в первом байте первого сегмента рабочего файла содержалась команда, с которой вы хотите начать выполнение. Более предпочтительным является способ задания входной точки в операторе END головной программы. На рис.П5.13 последним в программе является оператор

END START

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

При любой операции связывания должен быть только один оператор END с указанием стартового адреса. Обратите внимание, что в подпрограмме FIG5_14 в операторе END входная точка не указана. Если имеется более одной входной точки, то редактор связей обычно выбирает ту, которая указана последней. Лучше непосредственно задавать входную точку, чем допускать вероятность того, что редактор связей выберет не ту входную точку. Имейте в виду, что этот способ задания входной точки программы пригоден только в случае файла типа .EXE. Программа типа .COM всегда выполняется, начиная со смещения 100H сегмента команд.

Хостинг от uCoz