Язык ассемблера – функции компоновщика, динамическое связывание. Часть 2.

Пример HTML-страницы

Язык ассемблераПервая часть данной статьи.

Организация таблицы символьных имен в ассемблере.

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

Последовательное ассемблирование.

Таблица символьных имен представляется как результат первого прохода в виде массива пар имя — значе­ние. Поиск требуемого символа осуществляется последовательным просмотром таблицы до тех пор, пока не будет определено соответствие. Такой способ до­вольно легко программируется, но медленно работает.

Сортировка по именам.

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

Алго­ритм двоичного отсечения работает быстрее, чем последовательный просмотр таблицы, однако элементы таблицы необходимо располагать в алфавитном по­рядке.

рис. 5.2.1

Кэш–кодирование.

При этом способе на основании исходной таблицы строится кэш–функция, которая отображает имена в целые числа в промежутке от О до k–1 (рис. 5.2.1, а). Кэш–функцией может быть, например, функция перемно­жения всех разрядов имени, представленного кодом ASCII, или любая другая функция, которая дает равномерное распределение значений. После этого со­здается кэш–таблица, которая содержит к строк (слотов). В каждой строке распо­лагаются (например, в алфавитном порядке) имена, имеющие одинаковые значе­ния кэш–функции (рис. 5.2.1, б), или номер слота. Если в кэш–таблице содержится п символьных имен, то среднее количество имен в каждом слоте составляет n/k. При n = k для нахождения нужного символь­ного имени в среднем потребуется всего один поиск. Путем изменения к можно варьировать размер таблицы (число слотов) и скорость поиска. Связывание и загрузка. Программу можно представить как совокупность процедур (подпрограмм). Ассемблер поочередно транслируют одну процедуру за другой, создавая объектные модули и размещая их в памяти. Для получения исполняемого двоичного кода должны быть найдены и связаны все оттранслиро­ванные процедуры.

Функции связывания и загрузки выполняют специальные про­граммы, называемые компоновщиками, связывающими загрузчиками, редакто­рами связей или линкерами.

рис. 5.2.2 Таким образом, для полной готовности к исполнению исходной программы требуется два шага (рис. 5.2.2):

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

● связывание объектных модулей, выполняемое компоновщиком для получения исполняемого двоичного кода. Отдельная трансляция процедур вызвана возможными ошибками или необхо­димостью изменения процедур. В этих случаях понадобится заново связать все объектные модули. Так как связывание происходит гораздо быстрее, чем транс­ляция, то выполнение этих двух шагов (трансляции и связывания) сэкономит вре­мя при доработке программы. Это особенно важно для программ, которые со­держат сотни или тысячи модулей. В операционных системах MS–DOS, Windows и NT объектные моду­ли имеют расширение «.obj», а исполняемые двоичные программы — расширение «.ехе». В системе UNIX объектные модули имеют расширение «.о», а исполняемые двоичные программы не имеют расширения.

Функции компоновщика.

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

Цель компоновки — создать точное отображение виртуального адресного про­странства исполняемой программы внутри компоновщика и разместить все объектные модули по соответствующим адресам.

рис. 5.2.3 Рассмотрим особенности компоновки четырех объектных модулей (рис. 5.2.3, а), полагая при этом, что каждый из них находится в ячейке с адресом 0 и начинает­ся с команды перехода BRANCH к команде MOVE в том же модуле. Перед запуском программы компоновщик помещает объектные модули в ос­новную память, формируя отображение исполняемого двоичного кода. Обычно небольшой раздел памяти, начинающийся с нулевого адреса, используется для векторов прерывания, взаимодействия с операционной системой и других целей.

Поэтому, как показано на рис. 5.2.3, б, программы начинаются не с нулевого ад­реса, а с адреса 100. Поскольку каждый объектный модуль на рис. 5.2.3, а занимает отдельное ад­ресное пространство, возникает проблема перераспределения памяти. Все ко­манды обращения к памяти не будут выполнены по причине некорректной адре­сации. Например, команда вызова объектного модуля B (рис. 5.2.3, б), указанная в ячейке с адресом 300 объектного модуля А (рис. 5.2.3, а), не выполнится по двум причинам:

● команда CALL B находится в ячейке с другим адресом (300, а не 200); ● поскольку каждая процедура транслируется отдельно, ассемблер не может определить, какой адрес вставлять в команду CALL В. Адрес объектного мо­дуля В не известен до связывания. Такая проблема называется проблемой внешней ссылки. Обе причины устраняются с помощью компоновщика, который сливает отдель­ные адресные пространства объектных модулей в единое линейное адресное пространство, для чего:

● строит таблицу объектных модулей и их длин;

● на основе этой таблицы приписывает начальные адреса каждому объектному модулю;

● находит все команды, которые обращаются к памяти, и прибавляет к каждой из них константу перемещения, которая равна начальному адресу этого мо­дуля (в рассматриваемом случае 100);

● находит все команды, которые обращаются к процедурам, и вставляет в них адрес этих процедур. табл. 5.2.6 Ниже приведена таблица объектных модулей (табл. 5.2.6), построенная на первом шаге. В ней дается имя, длина и начальный адрес каждого модуля. Адресное пространство после выполнения компоновщиком всех шагов пока­зано в табл. 5.2.6 и на рис. 5.2.3, в. Структура объектного модуля. Объектные модули состоят из следующих частей:

имя модуля, некоторая дополнительная информация (например, длины раз­личных частей модуля, дата ассемблирования);

список определенных в модуле символов (символьных имен) вместе с их зна­чениями. К этим символам могут обращаться другие модули. Программист на языке ассемблера с помощью директивы PUBLIC указывает, какие символь­ные имена считаются точками входа;

список используемых символьных имен, которые определены в других моду­лях. В списке также указываются символьные имена, используемые теми или иными машинными командами. Это позволяет компоновщику вставить пра­вильные адреса в команды, которые используют внешние имена. Благодаря этому процедура может вызывать другие независимо транслируемые проце­дуры, объявив (с помощью директивы EXTERN) имена вызываемых процедур внешними. В некоторых случаях точки входа и внешние ссылки объединены в одной таблице;

машинные команды и константы;

словарь перемещений. К командам, которые содержат адреса памяти, долж­на прибавляться константа перемещения (см. рис. 5.2.3). Компоновщик сам не может определить, какие слова содержат машинные команды, а какие — константы. Поэтому в этой таблице содержится информация о том, какие ад­реса нужно переместить. Это может быть битовая таблица, где на каждый бит приходится потенциально перемещаемый адрес, либо явный список адресов, которые нужно переместить;

конец модуля, адрес начала выполнения, а также контрольная сумма для оп­ределения ошибок, сделанных во время чтения модуля. Отметим, что машинные команды и константы единственная часть объектного модуля, которая будет загружаться в память для выполнения. Остальные части используются и отбрасываются компоновщиком до начала выполнения программы. Большинство компоновщиков используют два прохода:

● сначала считываются все объектные модули и строится таблица имен и длин модулей, а также таблица символов, которая состоит из всех точек входа и внешних ссылок;

● затем модули еще раз считываются, перемещаются в памяти и связываются. О перемещении программ. Проблема перемещения связанных и находя­щихся в памяти программ обусловлена тем, что после их перемещения хранящи­еся в таблицах адреса становятся ошибочными. Для принятия решения о перемещении программ необходимо знать момент времени финального связывания символических имен с абсолютными адресами физической памяти.

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

При связывании можно выделить два этапа:

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

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

● разбиение на страницы. Адресное пространство, изображенное на рис. 5.2.3, в, содержит виртуальные адреса, которые уже определены и соответствуют символическим именам А, В, С и D. Их физические адреса будут зависеть от содержания таблицы страниц. Поэтому для перемещения программы в ос­новной памяти достаточно изменить только ее таблицу страниц, но не саму программу;

● использование регистра перемещения. Этот регистр указывает на физичес­кий адрес начала текущей программы, загружаемый операционной системой перед перемещением программы. С помощью аппаратных средств содержи­мое регистра перемещения прибавляется ко всем адресам памяти, прежде чем они загружаются в память. Процесс перемещения является «прозрач­ным» для каждой пользовательской программы. Особенность механизма: в отличие от разбиения на страницы должна перемещаться вся программа целиком. Если имеются отдельные регистры (или сегменты памяти как, на­пример, в процессорах Intel) для перемещения кода и перемещения данных, то в этом случае программу нужно перемещать как два компонента;

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

Рассмотренный выше способ связывания имеет одну особенность: связь со всеми процедурами, нужными программе, устанавли­вается до начала работы программы. Более рациональный способ связывания от­дельно скомпилированных процедур, называемый динамическим связыванием, состоит в установлении связи с каждой процедурой во время первого вызова. Впервые он был применен в системе MULTICS. рис. 5.2.4

Динамическое связывание в системе MULTICS. За каждой программой закреплен сегмент связывания, содержащий блок информации для каж­дой процедуры (рис. 5.2.4).

 

Информация включает:

● слово «Косвенный адрес», зарезервированное для виртуального адреса про­цедуры;

● имя процедуры (EARTH, FIRE и др.), которое сохраняется в виде цепочки сим­волов. При динамическом связывании вызовы процедур во входном языке трансли­руются в команды, которые с помощью косвенной адресации обращаются к слову «Косвенный адрес» соответствующего блока (рис. 5.2.4). Компилятор заполняет это слово либо недействительным адресом, либо специальным набором бит, ко­торый вызывает системное прерывание (типа ловушки). После этого:

● компоновщик находит имя процедуры (например, EARTH) и приступает к по­иску пользовательской директории для скомпилированной процедуры с таким именем;

● найденной процедуре приписывается виртуальный адрес «Адрес EARTH» (обычно в ее собственном сегменте), который записывается поверх недей­ствительного адреса, как показано на рис. 5.2.4;

● затем команда, которая вызвала ошибку, выполняется заново. Это позволяет программе продолжать работу с того места, где она находилась до систем­ного прерывания. Все последующие обращения к процедуре EARTH будут выполняться без оши­бок, поскольку в сегменте связывания вместо слова «Косвенный адрес» теперь указан действительный виртуальный адрес «Адрес EARTH». Таким образом, ком­поновщик задействован только тогда, когда некоторая процедура вызывается впервые. После этого вызывать компоновщик не требуется.

Динамическое связывание в системе Windows.

Для связывания ис­пользуются динамически подключаемые библиотеки (Dynamic Link Library — DLL), которые содержат процедуры и (или) данные. Библиотеки оформляются в виде файлов с расширениями «.dll», «.drv» (для библиотек драйверов — driver libraries) и «.fon» (для библиотек шрифтов — font libraries). Они позволяют свои процедуры и данные разделять между несколькими программами (процессами). Поэтому са­мой распространенной формой DLL является библиотека, состоящая из набора загружаемых в память процедур, к которым имеют доступ несколько программ одновременно. В качестве примера на рис. 5.2.5 показаны четыре процесса, ко­торые разделяют файл DLL, содержащий процедуры А, В, С и D. Программы 1 и 2 использует процедуру А; программа 3 — процедуру D, программа 4 — процедуру В. рис. 5.2.5 Файл DLL строится компоновщиком из набора входных файлов. Принцип пост­роения подобен построению исполняемого двоичного кода. Отличие проявляется в том, что при построении файла DLL компоновщику передается специальный флаг для сообщения о создании DLL. Файлы DLL обычно конструируются из набо­ра библиотечных процедур, которые могут понадобиться нескольким процессо­рам. Типичными примерами файлов DLL являются процедуры сопряжения с биб­лиотекой системных вызовов Windows и большими графическими библиотеками. Использование файлов DDL позволяет:

● сэкономить пространство в памяти и на диске. Например, если какая–то биб­лиотека была связана с каждой использующей ее программой, то эта библио­тека будет появляться во многих исполняемых двоичных программах в памя­ти и на диске. Если же использовать файлы DLL, то каждая библиотека будет появляться один раз на диске и один раз в памяти;

●упростить обновление библиотечных процедур и, кроме того, осуществить обновление, даже после того как программы, использующие их, были ском­пилированы и связаны;

● исправлять обнаруженные ошибки путем распространения новых файлов DLL (например, по Интернету). При этом не требуется производить никаких изме­нений в основных бинарных программах. Основное различие между файлом DLL и исполняемой двоичной програм­мой состоит в том, что файл DLL:

● не может запускаться и работать сам по себе, поскольку у него нет ведущей программы;

● содержит другую информацию в заголовке;

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

Эта библиотека создается обслуживающей программой, или утилитой, путем извлечения определенной информации из файла DLL. Библиотека импорта через связующий элемент позволяет пользовательской программе получать доступ к файлу DLL, при этом она может быть связана с несколькими библиотеками импорта. Система Windows при неявном связывании контролирует загружаемую для выполнения программу. Система выявляет, какие файлы DLL будет использовать программа, и все ли тре­буемые файлы уже находятся в памяти. Отсутствующие файлы немедленно загру­жаются в память.

Затем производятся соответствующие изменения в структурах данных библиотек импорта для того, чтобы можно было определить местополо­жение вызываемых процедур. Эти изменения отображаются в виртуальное адрес­ное пространство программы, после чего пользовательская программа может вызывать процедуры в файлах DLL, как будто они статически связаны с ней, и ее запускают.

При явном связывании не требуются библиотеки импорта и не нужно загру­жать файлы DLL одновременно с пользовательской программой. Вместо этого пользовательская программа:

● делает явный вызов прямо во время работы, чтобы установить связь с файлом DLL;

● затем совершает дополнительные вызовы, чтобы получить адреса процедур, которые ей требуются;

● после этого программа совершает финальный вызов, чтобы разорвать связь с файлом DLL;

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


Понравилась статья? Поделиться с друзьями:
Все об энергетике, электротехнике и электронике
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: