Построение заголовка загрузочного модуля.
Но на этом работа компоновщика не заканчивается, он еще должен построить заголовок ЗМ, включив в него информацию, по которой затем можно будет дотранслировать программу до конца и запустить ее на счет.
В упрощенном виде заголовок ЗМ состоит из следующих разделов:
1) длина программы; 2) точка входа; 3) начало и длина сегмента стека; 4) таблица перемещаемых адресов.
Прежде чем рассмотреть, как компоновщик заполняет эти разделы, отметим следующее. До сих пор адреса каких-то мест в ЗМ были представлены в условной форме - с указанием имен сегментов (типа S2:8). Однако в дальнейшем имена сегментов никому не нужны, а нужны только адреса сегментов, поэтому компоновщик должен заменить имя сегмента (S2) на его начальный адрес. Но этот адрес компоновщик не знает, т.к. он зависит от того, с какого места в памяти будет размещена программа во время счета, а это станет известным только позже. Что делать?
Отметим, что абсолютный, адрес (Aабс) любой точки программы можно представить в виде суммы Aабс=Aнач+Aотн, где Aнач - начальный адрес программы, а Aотн - относительный адрес этой точки, т.е. адрес, отсчитанный от начала программы:
0 ------¬
¦ ¦
Aнач ¦-----¦ ¬ <-- начало программы
¦/////¦ ¦ Aотн
Aабс ¦=====¦ -
¦/////¦
Причем компоновщик знает относительный адрес сегмента Aотн
(он указан в ОТС) и не знает начальный адрес программы Aнач. Учитывая это, он поступает так: он запоминает только относительный адрес сегмента, чтобы позже, когда станет известным начальный адрес программы, к нему можно было прибавить этот адрес и получить уже настоящий, абсолютный адрес сегмента. Таким образом, все условные адреса компоновщик заменяет на пары Аотн:ofs. Позже ко всем этим относительным адресам будет добавлен начальный адрес программы для получения абсолютных адресов сегментов.
Длина программы.
Эта длина, т.е. число байтов, занимаемых машинным кодом программы, уже была определена компоновщиком при построении ОТС. Она и переносится в заголовок ЗМ. По этой длине затем будет определяться, хватит ли программе места в памяти.
Точка входа.
Это адрес команды, с которой надо начинать выполнение программы. Данный адрес берется из заголовка того ОМ, в котором он указан, и переносится в заголовок ЗМ. (Замечание: если точки входа указаны в нескольких модулях, то учитывается первая их них, а если точка входа вообще не указана, то фиксируется ошибка.)
Начало и длина сегмента стека.
В одном из ОМ программы указывается имя сегмента стека. (Замечание: если стеки указаны в нескольких модулях, то учитывается первый их них, а если стек вообще не указан, то выдается предупреждение.) Компоновщик заменяет имя этого сегмента на его относительный адрес, который он узнает из ОТС. Из этой же таблицы он узнает и длину сегмента, которая также записывает в заголовок ЗМ.
Таблица перемещаемых адресов.
Напомним, что программа пока не оттранслирована до конца - в некоторые ее ячейки еще надо будет записать начальные адреса сегментов программы (без последнего 0). Поскольку эти адреса зависят от места размещения программы в памяти во время ее счета, а это место пока неизвестно, то ассемблер и компоновщик так и не смогли заменить имена сегментов на их адреса. Вместо этого они в своих таблицах перемещаемых адресов (ТПА) запомнили те ячейки, куда затем надо будет записать адреса сегментов. Эти таблицы имеются в каждом ОМ (их составил ассемблер), и есть еще одна таблица, которую составил сам компоновщик при редактировании межмодульных связей. Естественно, компоновщик должен сохранить сведения обо всех таких ячейках, для чего он объединяет все эти таблицы в одну, заменив в них условные адреса сегментов на их относительные адреса. В таком виде таблица и заносится в заголовок ЗМ.
На этом составление заголовка ЗМ закончено. Компоновщик записывает весь ЗМ (заголовок и тело) во внешнюю память (например, в файл M.EXE) и на этом завершает свою работу.
Замечание. Как видно, главная задача компоновщика - объединить машинные коды нескольких ОМ в одну машинную программу и оттранслировать ссылки из одних модулей в другие. Ясно, что если программа состоит из одного модуля, то эти действия не нужны. Но чтобы не было двух схем трансляции (одной для многомодульных программ и другой для одномодульных), одномодульные программы также заставляют "проходить" через компоновщик. В этом случае компоновщик фактически делает только одно - преобразует заголовок единственного модуля из одного формата в другой, тело же модуля при этом не меняется.