Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asm 2006

.pdf
Скачиваний:
28
Добавлен:
08.02.2015
Размер:
4 Мб
Скачать
☆

12

граммисту надо быть осторожным и обеспечить уникальность входных имён у всех модулей. Можно сказать, что такие редакторы внешних связей предполагают, что многомодульная программа разрабатывается сравнительно небольшим коллективом программистов, которые всегда смогут договориться между собой об уникальности всех внешних имён модулей (это часть спецификации разрабатываемой программы).1

К большому сожалению, некоторые редакторы внешних связей (в том числе и в нашей системе программирования с Ассемблером MASM-4.0) не проверяют соответствие типов у внешнего имени и входной точки. Таким образом, например, внешнее имя-переменная размером в слово может быть связано с входной точкой – переменной размером в байт или вообще с меткой. При невнимательном программировании это может привести к серьёзным ошибкам, которые будет трудно найти при отладке программы. Заметим, что в тех системах, которые уделяют большое внимание вопросам надёжности программирования, такие проверки всегда делаются. Например, редактор внешних связей Турбо-Паскаля считает несоответствие типов внешнего имени и входной точки фатальной ошибкой. Скажем здесь, что в системе программирования с языком С, который Вы будете изучать в следующем семестре, также принимаются специальные меры по контролю соответствия типов внешних имён и входных точек модулей.

Вернёмся к нашему редактору внешних связей. Когда для некоторого внешнего имени найдена соответствующая входная точка, то устанавливается связь: адрес входной точки записывается в соответствующее поле внешнего имени. Например, для команды

call Sum; Формат i32=seg:off = call seg:off

на место поля off запишется смещение начала процедуры суммирования в объединённом после склеивания сегменте кода, а поле seg пока останется незаполненным, его значение (адрес начала сегмента кода, делённый на 16) будет известно только после размещения программы в оперативной памяти перед началом счёта. Аналогично, в поле с именем N второго операнда команды

mov cx,N

запишется значение 1000:

mov cx,1000

Итак, если для каждого внешнего имени найдена входная точка в другом объектном модуле, то редактор внешних связей нормально заканчивает свою работу, выдавая в качестве результата загрузочный модуль. Загрузочный модуль, как и объектный, состоит из тела модуля и паспорта. Тело загрузочного модуля содержит все его сегменты,2 а в паспорте собраны необходимые для дальнейшей работы данные:

•информация обо всех сегментах (длина и класс сегмента), в частности, данные о сегменте стека;

•информация обо всех ещё неопределённых полях в сегментах модуля;

•информация о расположении входной точки программы (в нашем примере – метки

Start);

•другая необходимая информация.

На рис. 10.2 показан схематический вид загрузочного модуля, полученного для первого варианта нашего примера (со склеиваемыми сегментами). Заметьте, что параметры сегментов public и common уже не нужны. Внутри сегмента кода показаны незаполненные поля (они подчёркнуты). Метку Start можно рассматривать как единственную входную точку загрузочного модуля.

1 В некоторых технологиях программирования программные комплексы разрабатываются большими коллективами программистов, которые, вообще говоря, могут быть разбросаны по всему миру, и общаться между собой только по сети. В таких системах предпринимаются специальные меры, обеспечивающие уникальность всех внешних имён (в частности, и имён самих модулей). См. например [19].

2 В загрузочном модуле могут не храниться "пустые" сегменты данных, которые состоят только из директив резервирования памяти без начальных значений. Для сегментов данных, в которых есть области памяти, как с начальными значениями, так и без начальных значений, лучше сначала описывать области памяти с начальными значениями. Это даёт возможность помещать такой сегмент данных в загрузочный модуль в "урезанном" виде, все области данных без начальных значений в файл не записываются, что позволяет уменьшить размер файла загрузочного модуля. Разумеется, в паспорте загрузочного модуля хранится полная информация обо всех его сегментах (в частности, об их длине).

13

p.exe

St segment stack

Data segment

Code segment

Start: mov ax,Data

. . .

call Code:Sum

. . .

Рис. 10.2. Схематический вид загрузочного модуля, незаполненные поля в сегментах подчёркнуты.

Вот теперь всё готово для запуска программы на счёт. Осталось только поместить нашу программу в оперативную память, задать значения незаполненным полям и передать управление на начало программы (в нашем примере – на метку с именем Start). Эту работу делает служебная программа, которая называется статическим загрузчиком (далее мы познакомимся и с другим видом загрузчика – динамическим загрузчиком). Сейчас мы рассмотрим схему работы статического загрузчика.

10.3. Схема работы статического загрузчика

При своём вызове статический загрузчик (в дальнейшем – просто загрузчик) получает в качестве параметра имя файла, в котором хранится загрузочный модуль. Работа начинается с чтения паспорта загрузочного модуля и определения объёма памяти, необходимого для счёта программы. Обычно это сумма длин всех сегментов программы, однако иногда в процессе счёта программа может запрашивать и дополнительные сегменты (или, как говорят, блоки) памяти. Эти блоки чаще всего используются для размещения динамических переменных, так как заранее неизвестно, сколько памяти потребуется для хранения этих динамических переменных. Иногда в паспорте можно указать минимальный объём дополнительной памяти, без наличия которого нельзя запускать программу на счёт, и максимальный объём такой памяти, который может запросить программа.1

Итак, если на компьютере нет необходимого объёма свободной памяти, то загрузчик выдаёт аварийную диагностику о нехватке памяти (unsufficient memory) и не запускает программу на счёт. В противном случае из загрузочного модуля читаются и размещаются в памяти все сегменты этого модуля,2 для каждого сегмента, таким образом, определяется адрес его начала в оперативной памяти.

Затем загрузчик просматривает паспорт загрузочного модуля и заполняет в его сегментах все поля, которые ещё не имели необходимых значений. В нашем предыдущем примере для загрузочного модуля p.exe это поля с именами Data и Code в сегменте команд (см. рис. 10.2). В эти поля загрузчик записывает соответственно адреса начал сегментов данных и кода, делённые на 16.3 На этом настройка программы на конкретное месторасположение в оперативной памяти заканчивается.

Далее загрузчик, анализируя паспорт, определяет тот сегмент, который будет начальным сегментом стека программы (как мы знаем, этот сегмент имеет параметр Stack в директиве начала сегмента). Адрес начала этого сегмента, делённый на 16, записывается в сегментный регистр SS, а длина этого сегмента – в регистр вершины стека SP. Таким образом, стек программы теперь готов к работе.4

1 Максимальный объём дополнительной памяти указывается из соображений безопасности, чтобы из-за ошибок в программе она не стала запрашивать в цикле всё новые и новые блоки памяти, что может сильно помешать счёту других программ.

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

3Наш компилятор с Ассемблера те поля, которые заполняются редактором внешних связей, помечает в

листинге программы как 0000 E , а те поля, которые заполняет загрузчик – как ---- R .

4 Так как эти действия требуют больше одной команды, то их надо проводить в режиме с закрытыми прерываниями, чтобы не использовать не полностью подготовленный к работе стек при возникновении прерывания (т.е. эта часть загрузчика является уже знакомой нам критической секцией).

14

И, наконец, последним действием загрузчик производит дальний абсолютный переход на начало загруженной программы, например, по команде

jmp Code:Start

Здесь Code – адрес начала головного кодового сегмента, а Start – адрес (смещение) входной точки программы, с которой начинается её выполнение (эта информация, как уже говорилось, содержится в паспорте загрузочного модуля). Далее начинается собственно выполнение загруженной программы.

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

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

Итак, мы изучили схему разработки и выполнения модульной программы, эта схема включает в себя следующие этапы:

•Разбиение задачи на подзадачи.

•Реализация каждой такой подзадачи в виде модуля на некотором языке программрования, такой модуль принято называть исходным модулем.

•Синтаксическую отладку каждого модуля путём его компиляции в объектный модуль.

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

•Запуск загрузочного модуля на счёт с помощью служебной программы-загрузчика.

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

Поймём теперь, что эта схема выполнения модульной программы со статической загрузкой и статическим связыванием модулей имеет два очень серьёзных недостатка. Чтобы понять, в чём они заключаются, предположим сначала, что наша достаточно сложная программа состоит из 100 модулей (для простоты будем считать, что в каждом модуле содержится только одна процедура или функция нашей программы). Тогда перед началом работы все эти 100 процедур должны быть размещены в оперативной памяти и связаны между собой и с основной программой (т.е. в нужных местах проставлены адреса этих процедур).

В то же время чаще всего бывает так, что при каждом конкретном запуске программы на счёт, в зависимости от введённых данных, на самом деле понадобится вызвать только относительно небольшое количество этих процедур (скажем, 10 из 100). Тогда получается, что для каждого запуска программы необходимы лишь 10 процедур из 100, а остальные только зря занимают место в памяти, обращений к ним не будет. Конечно, для следующего запуска программы (с другими входными данны-

1 Для младших моделей нашего семейства компьютеров загрузчик входил в состав так называемой управляющей программы, которая выполняла команды (директивы) пользователя. Таким образом, загруженная на счёт программа пользователя после своего успешного или аварийного завершения должна была возвратиться в эту управляющую программу, которая обычно имела имя COMMAND.COM.

15

ми) могут понадобиться другие 10 процедур из 100, но в целом эта безрадостная картина не меняется: каждый раз во время счёта программы около 90% оперативной памяти не используется!

Исходя из вышесказанного понятно, что на первых ЭВМ, когда оперативной памяти было мало, схема счёта со статическим связыванием и статической загрузкой модулей применялась редко. Первые программисты не могли себе позволить так нерационально использовать дорогую оперативную память, поэтому при счёте модульных программ применялась схема с динамическим связыванием и динамической загрузкой модулей в оперативную память (с этой схемой мы будем знакомиться далее). Однако в дальнейшем, при увеличении объёмов оперативной памяти, и особенно после появления так называемой виртуальной памяти,1 стала в основном использоваться схема счёта модульных программ со статической загрузкой и связыванием, как более простая.

Далее отметим, что в настоящее время счёт модульных программ (а подавляющее большинство "больших" программ только такие теперь и есть), снова чаще всего выполняется с динамическим связыванием и динамической загрузкой модулей. Причина здесь состоит в том, что, несмотря на сильно возросший объём памяти в современных ЭВМ, сложность решаемых задач и число реализующих их модулей растёт быстрее, что объём памяти.

Второй серьёзный недостаток схемы счёта модульной программы со статическоим связыванием и статической загрузкой проявляется при так называемом мультипрограммном режиме работы ЭВМ (отметим, что в настоящее время большинство ЭВМ работают именно в этом реэиме). С мультипрограммным режимом работы мы будем детально знакомиться далее, пока лишь отметим, что в этом режиме в памяти ЭВМ могут одновременно находиться несколько независимых друг от друга и готовых к счёту программ разных пользователей. И вот оказывается, что что во многих программах часто приходится использовать одинаковые программные модули. Эти модули выполняют функции, без которых не может обойтись большинство программ: это организация диалогового интерфейта с пользователями (всемозможные окна, меню, формы для ввода данных и т.д.), работа с файлами, работа с динамическими переменными и многое другое.2 При схеме счёта со статическим связыванием и статической загрузкой приходится все эти общие для многих программ модули включать на этапе редактирования связей в состав каждой такой программы и хранить в каждом загрузочном модуле. Схема счёта модульной программы с динамической загрузкой и динамическим связыванием модулей призвана решить эту проблему.

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

дулей, называется динамическим загрузчиком.

10.4. Схема работы динамического загрузчика

Итак, пусть наша программа состоит из головного модуля (основной программы) и какого-то числа процедур и функций, располагающихся в остальных модулях.

Суть работы динамического загрузчика состоит в следующем. Сначала он размещает в памяти не всю программу целиком, как статический загрузчик, а только её основную часть (головной модуль программы). Все остальные модули, содержащие процедуры и функции, загружаются в оперативную память по мере необходимости, когда к этим процедурам и функциям будет реальное обращение из головного модуля или других (уже загруженных) модулей программы. Иногда это называется за-

грузкой по требованию.

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

1 Виртуальная память позволяет, в частности, использовать в программе объём памяти, превышающий физический объём памяти компьютера. Например, при физической памяти 220 байт (как в нашей младшей модели) можно использовать под (одновременное!) размешение сегментов программы, наприер, 224 байт. Виртуальную память студенты факультета вычислительной математики и кибернетики МГУ изучают в курсе третьего семестра "Системное программное обеспечение".

2 Это так называемая проблема повторного использования (reuse) программного обеспечения: разработанные для одной задачи программные модули часто можно использовать и в других задачах.

16

только надеяться изо всех сил ☺).1 Естественно, что, если какой-нибудь модуль не будет найден, когда понадобится его вызвать, то будет зафиксирована ошибка времени выполнения программы. Отметим, что это один из недостатков такой схемы счёта модульной программы. Впрочем, если посмотреть с другой стороны, то возможность выполнять программы, не все модули которых ещё на самом деле написаны, позволяет начать отладку программной системы на самых ранних стадиях её реализации, что, конечно, является одним из достоинств этой схемы выполнения.

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

Сначала разберёмся с редактированием внешних связей при динамической загрузке модулей (это и называется динамическим связыванием модулей). Работу динамического загрузчика будем рассматривать на примере программы, головной модуль которой вызывает три внешних процедуры с именами A,Beta и C12. Ниже приведён фрагмент сегмента кода этого головного модуля на Ассемблере:

; Фрагмент головного модуля с точкой входа

Code

segment

 

assume cs:Code,ds:Data,ss:Stack

Start:mov

ax,Data

 

mov

ds,ax

 

. . .

 

 

extrn

A:far

 

call

A

 

. . .

 

 

extrn

Beta:far

 

call

Beta

 

. . .

 

 

extrn

C12:far

 

call

C12

 

. . .

 

 

finish

 

Code

ends

Start; головной модуль

 

end

Пусть для простоты внешние процедуры с именами A,Beta и C12 расположены каждая в своём отдельном модуле, где эти имена, естественно, объявлены общедоступными (public) и имеют тип дальних меток (far). При своём вызове динамический загрузчик получает в качестве параметра имя головного объектного модуля, по существу это первый параметр редактора внешних связей (загрузочного модуля у нас нет, и не будет). Сначала динамический загрузчик размещает в оперативной памяти все сегменты головного модуля и начинает настройку его внешних адресов. Для этих целей он строит в оперативной памяти две вспомогательные таблицы: таблицу внешних имён (ТВИ) и таблицу внешних адресов (ТВА), пусть каждая из этих таблиц располагается в своём отдельном сегменте памяти.

В таблицу внешних имён заносятся все внешние имена выполняемой программы (в начале работы у нас это имена внешних процедур головного модуля A,Beta и C12). Каждое имя будем представлять в виде текстовой строки, заканчивающейся, как это часто делается, символом с номером ноль в алфавите (будем обозначать этот символ \0, как это принято в языке С). Ссылка на имя – это смещение начала этого имени от начала ТВИ. В заголовке (в первых двух байтах) ТВИ хранится ссылка на начало свободного места в этой таблице (номер первого свободного байта). На рис. 10.3 приве-

1 В современных технологиях программирования проверка того, что существуют все модули, вызов которых возможен при счёте программы, может оказаться весьма трудоёмкой операцией. Дело в том, что эти модули могут, в принципе, находится где угодно, например, в сетевой (удалённой) библиотеке объектных модулей на другом конце Земли. В качестве примера сошлёмся на так называемую COM технологию программирования

[19].

17

дён вид ТВИ после обработки головного модуля, сейчас в этой таблице три имени: A,Beta и C12 (в каждой строке таблицы, кроме заголовка, мы для удобства чтения разместили по четыре символа).

ТВИ segment

0 Free=13

2 'A' \0 'B' 'e'

6 't' 'a' \0 'C'

10 '1' '2' \0

Рис. 10.3. Вид таблицы внешних имён после загрузки головного модуля.

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

торой служебной процедуры динамического загрузчика. Мы назвали эту служебную процедуру именем LoadGo, что будет хорошо отражать её назначение – загрузить внешнюю процедуру в оперативную память и перейти на выполнение этой процедуры. Отметим, что сама процедура LoadGo загружается в память вместе с головным модулем до начала счёта и статически связана с этим модулем.

Во втором поле (назовём его именем Offset) длиной 2 байта находится адрес (смещение) загруженной внешней процедуры на специальном рабочем поле, о котором мы расскажем немного ниже. До первого обращения к внешней процедуре в это поле динамический загрузчик записывает константу 0FFFFh=-1, что является признаком отсутствия данной процедуры на рабочем поле. В третьем поле тоже длиной в два байта расположена ссылка на имя этой внешней процедуры в таблице внешних имён. В четвёртом поле длиной в два байта с именем Length будет храниться длина внешней процедуры (пока там значение ноль). И, наконец, пятое поле, тоже длиной в 2 байта, содержит различную служебную информацию (флаги режимов работы) для динамического загрузчика, о чём мы также немного поговорим далее. Таким образом, каждая строка таблицы внешних имён описывает одну внешнюю процедуру и имеет длину 12 байт. В заголовке (первых двух байтах) ТВА содержится ссылку на начало свободного места в этой таблице (эта таблица будет только расти сверзувниз, строки из неё никогда не будут удаляться). Таким образом, перед началом счёта программы ТВА будет иметь вид, показанный на рис. 10.4.

 

 

 

ТВА segment

 

 

 

Free=38

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2

Jmp LoadGo

0FFFFh

2

('A')

00000

Flags

14

Jmp LoadGo

0FFFFh

4

('Beta')

00000

Flags

26

Jmp LoadGo

0FFFFh

9

('C12')

00000

Flags

38

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 10.4. Вид таблицы внешних адресов после загрузки головного модуля.

Каждая команда дальнего вызова внешней процедуры в головном модуле заменяется динамическим загрузчиком на команду дальнего перехода с возвратом на соответствующую строку ТВА. Например, команда call Beta заменяется на команду call ТВА:14 , а команда в головном модуле call C12 заменяется на команду call ТВА:26 .

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

Beta proc far

. . .

extrn Delta:far call Delta

. . .

ret

Beta endp

Как видим, в процедуре Beta возможен вызов другой внешней процедуры с именем Delta. Естественно, что при первой попытке основной программы вызвать процедуру Beta, управление пере-

18

даётся на 14-ую строку ТВА и вызывается служебная процедура LoadGo динамического загрузчика. Получив управление, процедура LoadGo последовательно выполняет следующие действия.

•Сначала вычисляется величина TBA_proc, равная адресу строки вызываемой процедуры Beta в таблице TBA. Например, для случая, как у нас, вызова процедуры Beta величина

TBA_proc=14.

•Затем анализируется поле Offset в строке таблицы с адресом TBA_proc. Значение Off- set=-1 означает, что нужной внешней процедуры с именем Beta в оперативной памяти ещё нет. В этом случае процедура LoadGo производит поиск объектного модуля, содержащего требуемую процедуру (в паспорте этого модуля должна быть описана входная точка с именем Beta и типом дальней метки far). Если такой объектный модуль не найден, то фик-

сируется фатальная ошибка времени выполнения и наша программа завершается, иначе объектный модуль с требуемой процедурой Beta загружается в оперативную память и динамически связывается с основной программой. Для этого корректируются таблица внешних адресов ТВА и, возможно, таблица внешних имён ТВИ (если, как в нашем примере, в загружаемой процедуре есть и свои внешние имена). Для загрузки процедур в оперативной памяти компьютера выделяется специальная область, она часто называется рабочим полем процедур. Мы отведём под рабочее поле отдельный сегмент с именем Work, занимающий, например, 50000 байт (заметим, что рабочее поле в нашем простом примере должно помещаться в один сегмент):

Work segment

db 50000 dup (?) Work ends

Рабочее поле размещается в оперативной памяти одновременно с сегментами головного модуля, ТВИ и ТВА. После загрузки процедуры Beta поле Offset в строке TBA_proc принимает значение адреса начала этой процедуры на рабочем поле, а поле Length будет равно длине загруженной процедуры. Пусть длина процедуры Beta будет, например, 30000 байт. В нашем случае процедура Beta загружается с начала рабочего поля, так как оно пока не содержит других внешних процедур, так что поле Offset принимает значение 00000.

•Анализируется адрес дальнего возврата, расположенный на вершине стека (по этому адресу процедура Beta должна возвратиться после окончания своей работы). Целью такого анализа является определение того, производится ли вызов процедуры Beta из головного модуля программы (как в нашем примере), или же из некоторой внешней процедуры, уже расположенной на рабочем поле (что, конечно, тоже возможно). Ясно, что такой анализ легко провести по значению поля сегмента в адресе возврата (обязательно поймите, как это сделать).

•Если, как в нашем примере, вызов внешней процедуры Beta производится из головного модуля, то наша служебная процедура LoadGo производит дальний абсолютный переход на начало требуемой внешней процедуры, расположенной на рабочем поле, по команде вида jmp Work:Offset . Ясно, что в этом случае возврат из внешней процедуры по команде

ret будет производиться в головной модуль нашей программы (дальний адрес возврата, как обычно, находится на вершине стека). В нашем примере динамический загрузчик перейдёт на выполнение процедуры Beta с помощью команды дальнего абсолютного перехода

jmp Work:0 .

На рис. 10.5 показан вид ТВИ, ТВА и рабочего поля после загрузки процедуры Beta.

Упражнение. Объясните, каким образом служебная процедура LoadGo, получив управление по команде jmp LoadGo , вычислит величину TBA_proc, то есть определит, что надо загружать, например, именно процедуру с именем Beta, а не какую-нибудь другую внешнюю процедуру из TBA.

Теперь, после динамической загрузки процедуры Beta на рабочее поле и связывания внешних адресов с помощью ТВА, вызов процедуры Beta будет производиться с помощью служебной процедуры LoadGo. Правда, необходимо заметить, что вызов стал длиннее, чем при статическом связывании, за счёт дополнительных команд, выполняемых процедурой LoadGo. Кроме того, как мы вскоре выясним, внешние процедуры могут неоднократно загружаться на рабочее поле и удаляться с него, что, конечно, может вызвать существенное замедление выполнения программы пользователя. Это,

19

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

 

 

 

 

ТВИ segment

 

 

 

 

 

 

0

 

Free=19

 

 

 

 

 

 

 

 

 

2

 

'A'

 

\0

 

 

'B'

'e'

 

 

 

 

 

6

 

't'

 

'a'

 

\0

'C'

 

 

 

 

 

10

 

'1'

 

'2'

 

\0

'D'

 

 

 

 

 

14

 

'e'

 

'l'

 

't'

'a'

 

 

 

 

 

18

 

\0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ТВА segment

 

 

 

 

 

 

Free=50

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2

jmp LoadGo

0FFFFh

 

2

('A')

00000

Flags

14

jmp LoadGo

00000h

 

4

('Beta')

30000

Flags

26

jmp LoadGo

0FFFFh

 

9

('C12')

00000

Flags

38

jmp LoadGo

0FFFFh

 

13

('Delta')

00000

Flags

50

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Work segment

 

 

 

 

00000

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Процедура Beta

30000

Рис. 10.5. Вид ТВИ, ТВА и рабочего поля после загрузки процедуры Beta.

Продолжим анализ работы динамического загрузчика на нашем примере. Пусть загруженная на рабочее поле процедура Beta, проработав некоторое время, в свою очередь вызывает свою внешнюю процедуру с именем Delta, которая имеет, например, длину 15000 байт. Так как команда call Delta в процедуре Beta при загрузке этой процедуры на рабочее поле заменена динамическим загрузчиком на команду call ТВА:38 , то управление опять получает служебная процедура LoadGo. Она находит процедуру Delta 1 и размещает её на свободном месте рабочего поля (в нашем примере с адреса 30000), затем настраивает внешние адреса в этой процедуре (если они есть) и соответствующие строки в ТВА.

На рис. 10.6 показан вид ТВА и рабочего поля после загрузки и связывания процедуры Delta. Далее процедура LoadGo определила, что вызов процедуры TBA_proc(Delta)=38 произво-

дится не из основной программы, а из процедуры TBA_proc(Beta)=14, расположенной на рабочем поле. В этом случае LoadGo производит следующие действия, при выполнении которых используется ещё один служебный сегмент динамического загрузчика, описанный, например, так:

My_Stack

segment

 

Free

dw

0

 

 

dw

4000

dup (?)

My_Stack

ends

 

Этот сегмент используется как вспомогательный программный (не аппаратный) стек динамического загрузчика. Наш стек с именем My_Stack, в отличие от машинного стека, будет "расти" свер-

1 Точнее, как мы уже говорили, ищется объектный модуль, в котором расположена эта общедоступная (public) процедура.

20

ху-вниз, при этом переменная Free играет роль указателя вершины программного стека, то есть регистра SP.

 

 

 

ТВА segment

 

 

 

Free=50

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2

jmp LoadGo

0FFFFh

2

('A')

00000

Flags

14

jmp LoadGo

00000

4

('Beta')

30000

Flags

26

jmp LoadGo

0FFFFh

9

('C12')

00000

Flags

38

jmp LoadGo

30000

13

('Delta')

15000

Flags

50

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Work segment

00000

Процедура Beta

30000

Процедура Delta

 

 

45000

Рис. 10.6. Вид ТВА и рабочего поля после загрузки процедуры Delta.

Сначала LoadGo извлекает из аппаратного стека (на него, как обычно, указывает регистровая пара <SS,SP>) адрес дальнего возврата (два слова). Затем в программный стек My_Stack сначала записывается одно слово (назовём его именем RETURN), которое имеет значение разности IPTBA_proc(Beta).offset, где TBA_proc(Beta).offset – адрес начала процедуры Beta на рабочем поле, т.е. значение поля offset строки TBA для процедуры Beta. Как легко понять, величина RETURN равна смещению точки возврата относительно начала процедуры, из которой производится вызов.

Затем в программный стек записывается значение TBA_proc(Beta) (одно машинное слово), и таким способом запоминается, из какой процедуры с рабочего поля произошёл вызов. Таким образом, во вспомогательном стеке запоминается, из какой процедуры и из какого места этой процедуоы производится вызов. И, наконец, LoadGo производит вызов необходимой внешней процедуры Delta, расположенной на рабочем поле со смещением Offset=TBA_proc(Delta).offset от его начала, командой дальнего вызова процедуры call Work:Offset . Очевидно, что в этом случае и возврат из процедуры Delta по команде ret будет производиться не в вызвавшую её процедуру Beta, а в нашу служебную процедуру LoadGo.

После возврата из внешней процедуры в LoadGo, она производит следующие действия (напомним, что в этот момент ей уже известно о том, что вызов внешней процедуры был осуществлён из некоторой процедуры, расположенной на рабочем поле, иначе возврат из процедуры производится сразу в основную программу, миную LoadGo).

•Сначала из вспомогательного стека My_Stack извлекается значение TBA_proc той процедуры, в которую необходимо вернуться (это значение на вершине нашего программного стека по адресу My_Stack[Free]).

•Затем анализируется значение поля Offset в этой строке TBA_proc. Если величина Offset<>-1 , то это означает, что наша процедура всё ещё присутствует на рабочем поле. В этом случае из вспомогательного стека My_Stack извлекается значение RETURN смещения точки возврата относительно начала процедуры, после чего динамический загрузчик производит возврат в процедуру на рабочем поле по команде дальнего безуслов-

ного перехода jmp Work:Offset+RETURN .

•Разберём теперь случай, когда после возврата величина Offset=-1, это означает, что наша процедура была удалена с рабочего поля. В этом случае производится повторная загрузка процедуры на рабочее поле (вообще говоря, начиная с другого свободного места этого поля). Адрес нового положения процедуры на рабочем поле записывается в поле

21

Offset в строке TBA_proc. Затем, как и в случае с Offset<>-1, из вспомогательного стека My_Stack извлекается значение RETURN смещения точки возврата относительно начала процедуры, после чего динамический загрузчик производит возврат в процедуру, которая теперь уже снова находится на рабочем поле, по команде дальнего безусловного

перехода jmp Work:Offset+RETURN .

На этом LoadGo завершает обработку вызова внешней процедуры. Как видим, эта процедура играет роль своеобразного буфера, располагаясь между вызывающей программой и вызываемой внешней процедурой. На рис. 10.7 показана схема, поясняющая роль процедуры LoadGo на примере, когда одна процедура с именем X (это может быть и основная программа) вызывает внешнюю процеду-

ру с именем Y.

 

 

 

 

 

 

 

 

 

Сall Y

 

 

 

 

 

Процедура

Служебная

jmp Y

Вызываемая

 

или основная

 

 

 

процедура

 

 

внешняя про-

 

программа X

ret

LoadGo

ret

цедура Y

ret из внешней процедуры в основную программу

Рис. 10.7. Вызов внешней процедуры и возврат из неё при схеме счёта с динамической загрузкой и динамическим связыванием.

Как следует из описания алгоритма работы программы LoadGo, она может контролировать наличие требуемой процедуры на рабочем поле и, при необходимости, загружать вызываемую процедуру на свободное место рабочего поля. Обратите внимание, что контроль наличия процедуры на рабочем поле производится как при вызове этой процедуры, так и при возврате в неё. Это позволяет не накладывать никаких ограничений на порядок вызова процедур, в частности, как мы и привыкли, рекурсивный вызов процедуры для динамического загрузчика ничем не отличается от не рекурсивного вызова.

Продолжим исследование работы динамического загрузчика на нашем примере. Предположим теперь, что произошёл возврат из процедур Delta и Beta в основную программы, которая после этого вызвала процедуру A длиной, скажем, в 25000 байт. Процедура A отсутствует на рабочем поле, поэтому её надо загрузить, однако процедура LoadGo определяет, что на рабочем поле нет достаточного места для размещения процедуры A. Выход здесь только один – удалить с рабочего поля одну или несколько процедур, чтобы освободить достаточно место для загрузки процедуры A.1 В нашем случае достаточно, например, удалить с рабочего поля процедуру Beta.

Итак, служебная процедура LoadGo считает память, занимаемую процедурой Beta на рабочем поле свободной, загружает на это место процедуру A и корректирует соответствующим образом строки ТВА. Будем говорить, что процедура Beta удаляется с рабочего поля, а на её место загружается процедура A. На рис. 10.8 показан вид ТВА и рабочего поля после загрузки процедуры A.

Как следует из описания работы динамического загрузчика, на рабочем поле всегда находятся последние из выполняемых процедур, а программа пользователя не должна ни о чём заботиться и работает, как и при статической загрузке модулей, просто обычным образом вызывая необходимые ей внешние процедуры. Часто говорят, что действия динамического загрузчика прозрачны (т.е. невидимы) для программы пользователя.

Иногда, однако, программист нуждается в некотором управлении динамической загрузкой модулей на рабочее поле. Например, пусть на рабочем поле уже находятся процедуры с именами X и Z, а в основной программ программист начинает в цикле попеременно вызывать процедуры с именами X и Y, например, так:

L:Call X Call Y Loop L

Тогда может случиться так, что при вызове и загрузке на рабочее поле процедуры Y она будет удалять с рабочего поля процедуру X, а процедура X, в свою очередь, при загрузки на рабочее поле

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]