Work ends
Рабочее поле размещается в оперативной памяти одновременно с сегментами головного модуля, ТВИ и ТВА. После загрузки Beta поле Offset в строке TBA_proc принимает значение адреса начала процедуры на рабочем поле. В нашем случае процедура Beta загружается с начала рабочего поля, так как оно пока не содержит других внешних процедур, так что поле Offset принимает значение 00000h.
3. Анализируется адрес дальнего возврата, расположенный на вершине стека (по этому адресу процедура Beta волжна возвратиться после окончания своей работы). Целью такого анализа является определение того, производится ли вызов процедуры Beta из головного модуля программы (как в нашем примере), или же из некоторой внешней процедуры, уже расположенной на рабочем поле (что, конечно, тоже возможно). Ясно, что такой анализ легко провести по значению сегмента в адресе возврата (обязательно поймите, как это сделать).
· Если вызов внешней процедуры производится из головного модуля, то наша служебная процедура LoadGo производит дальний абсолютный переход на начало требуемой внешней процедуры, расположенной на рабочем поле, по команде вида jmp Work:Offset . Ясно, что в этом случае возврат из внешней процедуры будет производиться в головной модуль нашей программы (адрес возврата, как обычно, на вершине стека).
· Если вызов производится из процедуры, расположенной на рабочем поле, то процедура LoadGo производит следующие действия, при выполнении которых используется ещё один служебный сегмент динамического загрузчика, описанный, например, так:
My_Stack segment
Free dw 0
dw 15000 dup (?)
My_Stack ends
Этот сегмент используется как вспомогательный программый (не аппаратный) стек динамического загрузчика. Наш стек My_Stack в отличие от машинного стека, будет "расти" сверху-вниз, при этом переменная Free выполняет роль указателя вершины программного стека – регистра SP. Сначала LoadGo извлекает из аппаратного стека (на него, как обычно, указывает регистровая пара <SS,SP>) адрес возврата (два слова) и записывает этот адрес в программный стек My_Stack. Затем туда же записывается значение TBA_proc, таким образом запоминается, из какой процедуры произошёл вызов. И, наконец, LoadGo производит вызов необходимой внешней процедуры, расположенной на рабочем поле, командой вида callWork:Offset . Очевидно, что возврат из внешней процедуры в этом случае будет производиться в нашу служебную процедуру LoadGo.
4. После возврата из внешней процедуры в LoadGo, она производит следующие действия (напомним, что уже известно о том, что вызов внешней прохедуры был осуществлён из некоторой процедуры, расположенной на рабочем поле).
· Сначала из вспомогательного стека My_Stack извлекается значение TBA_proc той процедуры, в которую необходимо вернуться (это значение на вершине нашего программного стека по адресу My_Stack[Free]).
· Затем анализируется значение поля Offset в строке TBA_proc. Если величина Offset=-1 , то это означает, что наша процедура была удалена с рабочего поля. В этом случае производится повторная загрузка процедуры на рабочее поле (вообще говоря, начиная с другого свободного места этого поля). Адрес нового положения процедуры на рабочем поле записывается в поле Offset в строке TBA_proc.
· Из вспомогательного стека My_Stack извлекается адрес дальнего возврата, в котором слово, содержащее значение сегмента возврата, заменяется величиной Offset из строки TBA_proc. И, наконец, производится дальний безусловный переход по так скорректированному адресу возврата.
На этом LoadGo завершает обработку вызова внешней процедуры. Как видим, эта процедура играет роль своеобразного буфера, располагаясь между вызывающей программой и вызываемой внешней процедурой. Как следует из описания алгоритма работы LoadGo, она может контролировать наличие требуемой процедуры на рабочем поле и, при необходимости, загружать вызываемую процедуру на свободной место рабочего поля. Обратите внимание, что контроль наличия процедуры на рабочем поле производится как при вызове этой процедуры, так и при возврате в неё.
Упражнение. Объясните, каким образом служебная процедура LoadGo, получив управление по команде jmp LoadGo , вычислит величину TBA_proc, то есть определит, что надо загружать именно процедуру с именем Beta, а не какую-нибудь другую внешнюю процедуру.
Продолжим анализ работы динамического загрузчика на нашем примере. Пусть загруженная процедура Beta имеет длину, например, 30000 байт и, в свою очередь, содержит вызов некоторой внешней процедуры с именем Delta:
Beta proc far
. . .
extrn Delta:far
call Delta
. . .
Ret
Beta endp
Теперь, после динамической загрузки процедуры Beta на рабочее поле и связывание внешних адресов с помощью ТВА, вызов процедуры Beta будет производиться с помощью служебной процедуры LoadGo. Правда, необходимо заметить, что вызов стал длиннее, чем при статическом связывании, за счёт дополнительных команд, выполняемых процедурой LoadGo. Кроме того, как мы вскоре выясним, внешние процедуры могут неоднократно загружаться на рабочее поле и удаляться с него, что, конечно, может вызвать существенное замедление выполнения программы пользователя. Это, однако, неизбежная плата за преимущества динамической загрузки модулей. По существу, здесь опять работает уже упоминавшееся нами правило рычага: выигрывая в объёме памяти, необходимом для счёта модульной программы, мы неизбежно сколько-то проигрываем в скорости работы нашей программы. Важно чтобы выигрыш, с точки зрения конкретного пользователя, был больше проигрыша.
На рис. 10.5 показан вид ТВИ, ТВА и рабочего поля после загрузки процедуры Beta.
ТВИ segment | ||||||||||||||||
0 | Free=19 | |||||||||||||||
'A' | \0 | 'B' | 'e' | |||||||||||||
't' | 'a' | \0 | 'C' | |||||||||||||
'1' | '2' | \0 | 'D' | |||||||||||||
'e' | 'l' | 't' | 'a' | |||||||||||||
18 | \0 | |||||||||||||||
ТВА segment | ||||||||||||||||
Free=42 | ||||||||||||||||
jmp LoadGo | 0FFFFh | 2 ('A') | flags | |||||||||||||
jmp LoadGo | 00000h | 4 ('Beta') | flags | |||||||||||||
jmp LoadGo | 0FFFFh | 9 ('C12') | flags | |||||||||||||
jmp LoadGo | 0FFFFh | 13 ('Delta') | flags | |||||||||||||
42 | ||||||||||||||||
Work segment | ||||||||||||||||
Процедура Beta | ||||||||||||||||
Рис. 10.5. Вид ТВИ, ТВА и рабочего поля после загрузки процедуры Beta. | ||||||||||||||||
Продолжим изучение выполнения нашей модульной программы. Предположим далее, что, проработав некоторое время, процедура Beta вызовет внешнюю процедуру с именем Delta, которая имеет длину 15000 байт. Так как команда call Delta в процедуре Beta при загрузке этой процедуры на рабочее поле заменена динамическим загрузчиком на команду call ТВА:32 , то управление опять получает служебная процедура LoadGo. Она находит процедуру Delta [59] и размещает её на свободном месте рабочего поля (в нашем примере с адреса 30000), затем настраивает внешние адреса в этой процедуре (если они есть) и соответствующие строки в ТВА.
На рис. 10.6 показан вид ТВА и рабочего поля после загрузки и связывания процедуры Delta.
Продолжим наше исследование работы динамического загрузчика. Предположим теперь, что произошёл возврат из процедур Delta и Beta в основную программы, которая после этого вызвала процедуру A длиной в 25000 байт. Процедуры A нет на рабочем поле, поэтому её надо загрузить, однако вызванная процедура LoadGo определяет, что на рабочем поле нет достаточного места для размещения процедуры A. Выход здесь только один – удалить с рабочего поля одну или несколько процедур, чтобы освободить достаточно место для загрузки процедуры A. В нашем случае достаточно, например, удалить с рабочего поля процедуру Beta.
ТВА segment | |||||||||||
Free=42 | |||||||||||
jmp LoadGo | 0FFFFh | 2 ('A') | flags | ||||||||
jmp LoadGo | 4 ('Beta') | flags | |||||||||
jmp LoadGo | 0FFFFh | 9 ('C12') | flags | ||||||||
jmp LoadGo | 13 ('Delta') | flags | |||||||||
42 | |||||||||||
Work segment | |||||||||||
Процедура Beta | |||||||||||
Процедура Delta | |||||||||||
Рис. 10.6. Вид ТВА и рабочего поля после загрузки процедуры Delta. | |||||||||||
Итак, служебная процедура LoadGo удаляет с рабочего поля процедуру Beta, загружает на освободившееся место процедуру A и корректирует соответствующим образом строки ТВА. На рис. 10.7 показан вид ТВА и рабочего поля после загрузки процедуры A.
ТВА segment | |||||||||||
Free | |||||||||||
jmp LoadGo | 2 ('A') | flags | |||||||||
jmp LoadGo | 0FFFFh | 4 ('Beta') | flags | ||||||||
jmp LoadGo | 0FFFFh | 9 ('C12') | flags | ||||||||
jmp LoadGo | 13 ('Delta') | flags | |||||||||
42 | |||||||||||
Work segment | |||||||||||
Процедура A | |||||||||||
Процедура Delta | |||||||||||
Рис. 10.7. Вид ТВА и рабочего поля после загрузки процедуры A. | |||||||||||
Как следует из описания работы динамического загрузчика, на рабочем поле всегда находятся последние из выполняемых процедур, а программа пользователя не должна ни о чём заботиться и работает, как и при статической загрузке модулей, просто обычным образом вызывая необходимые ей внешние процедуры. Часто говорят, что действия динамического загрузчика прозрачны (т.е. невидимы) для программы пользователя.
Иногда, однако, программа пользователя нуждается в некотором управлении динамической загрузкой модулей на рабочее поле. Например, пусть программист знает, что некоторая процедура X будет вызываться часто (в цикле). В этом случае программист может потребовать у динамического загрузчика, чтобы эта процедура X по возможности не удалялась с рабочего поля. Другими словами, динамический загрузчик при нехватке памяти на рабочем поле должен сначала стараться удалить с него другие процедуры, а лишь в последнюю очередь процедуру X. Говорят, что процедура X фиксируется на рабочем поле.
Для фиксации процедуры на рабочем поле в состав динамического загрузчика входит служебная процедура с именем Lock. Программа пользователя должна вызвать эту процедуру с параметром – именем фиксируемой процедуры. На Ассемблере необходимо определить способ передачи этого строкового параметра в служебную процедуру, а на языке Паскаль это можно записать, например, так
Lock('X');
Процедура Lock находит в ТВА строку, соответствующую указанной процедуре, и ставит в этой строке признак о том, что она зафиксирована на рабочем поле. Когда необходимость в фиксации процедуры X на рабочем поле отпадёт, программист может расфиксировать эту процедуру, вызвав служебную процедуру динамического загрузчика с именем UnLock. На Паскале это, например, можно сделать так:
UnLock('X');
Разумеется, в строке ТВА в поле флагов теперь надо предусмотреть битовый признак Lock/UnLock. Обратите также внимание, что служебные процедуры LoadGo,Lock и UnLock статически связаны с программой пользователя, т.е. расположены в её сегменте кода. Об этом должен позаботиться динамический загрузчик при размещении в оперативной памяти головного модуля программы.
Рассмотрим теперь главные недостатки схемы счёта с динамической загрузкой и связыванием модулей. Во-первых, следует отметить дополнительные вычислительные затраты на выполнение служебных процедур (LoadGo,Lock,UnLock и других) во время счёта программы пользователя. Во-вторых, может достаточно существенно замедлиться выполнения всей программы, так как теперь во время счёта может понадобиться периодически загружать модули на рабочее поле, т.е. использовать относительно медленную внешнюю память. В том случае, если такие затраты допустимы, то схеме счёта с динамической загрузкой следует отдать предпочтение.[60]
В современных ЭВМ наборы динамически загружаемых модулей одной тематики обычно объединяют в один файл – библиотеку динамически загружаемых модулей (по-английски Dynamic Link Library – DLL).
На этом мы завершим наше краткое знакомство со схемами выполнения модульных программ.
Дата добавления: 2015-10-05; просмотров: 599;