ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 24.12.2021

Просмотров: 12219

Скачиваний: 10

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
background image

5 3 8 Глава 7. Уровень языка ассемблера

При применении любого из этих способов мы пытаемся смоделировать

 ассоциа-

тивную память,

 которая представляет собой набор пар (символьное имя, значе-

ние). По имени ассоциативная память должна выдавать его значение.

Проще всего реализовать таблицу символьных имен в виде массива пар, где пер-

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

Другой способ организации — отсортировать таблицу по именам и для поиска

имен использовать алгоритм

 двоичного поиска.

 В соответствии с этим алгорит-

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

Предположим, что средний элемент таблицы не равен символу, который мы

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

чим совпадение, либо определим нужную четверть таблицы. Таким образом, в таб-
лице из п элементов нужный символ можно найти примерно за lo&n попыток. Оче-
видно, что такой алгоритм работает быстрее, чем просто последовательный просмотр
таблицы, но при этом элементы таблицы нужно сохранять в алфавитном порядке.

Совершенно другой подход —

 хэш-кодирование.

 Для этого подхода требуется

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

 к

 или разделить полу-

ченное значение на простое число. Фактически подойдет любая входная функция,

которая дает равномерное распределение значений.

Символьные имена можно хранить в таблице, состоящей из

 к

 участков, от 0 до

к-1. Все пары (символьное имя, значение), в которых имя соответствует i, сохра-
няются в связном списке, на который указывает слот i в хэш-таблице. Если в хэш-
таблице содержится п символьных имен и к слотов, то в среднем длина списка
будет n/k. Если мы выберем к, приблизительно равное п, то на нахождение нужно-
го символьного имени в среднем потребуется всего один поиск. Путем корректи-
ровки

 к

 мы можем сократить размер таблицы, но при этом скорость поиска сни-

зится. Хэш-код показан на рис. 7.1.

Связывание и загрузка

Большинство программ содержат более одной процедуры. Компиляторы и ассем-

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


background image

Связывание и загрузка

539

слированные процедуры. Если виртуальной памяти нет, связанная программа долж-
на загружаться в основную память. Программы, которые выполняют эти функ-
ции, называются по-разному:

 компоновщиками, связывающими загрузчиками

и

 редакторами связей.

 Для полной трансляции исходной программы требуется

два шага, как показано на рис. 7.2:

1. Компиляция или ассемблирование исходных процедур.

2. Связывание объектных модулей.

Andy

Anton
Cathy

Dick

Erik

Frances

Frank
Gerrit

Hans
Henri

Jan

Jaco

Maarten

Reind

Roel

Willem

Wiebern

14025

31253
65254
54185
47357
56445

14332

32334
44546
75544

17097

64533
23267
63453
76764
34544
34344

0
4
5
0
6
3
3
4
4
2

5

6
0

1

7
6

1

Хэш-

таблица

0

1

2

3

4

5

6

7

Связная таблица

Andy | 14025 | -Ы Maarten | 23267 | 4+-| Dick | 54185~

Reind | 63453

Henri

Hans

Jan

Jaco

Roel

75544

44546

17097

64533

76764

Wiebern | 34344

Frances | 56445 |

 Ц->\

 Frank | 14332

Gerrit

Willem

32334

* ^ H Cathy | 65254

34544

Anton

31253

Erjk

"r7357

Рис.  7 . 1 . Хэш-кодирование: символьные имена, значения и хэш-коды, образованные

от символьных имен (а); хэш-таблица из 8 элементов со связным списком символьных

имен и значений

 (б)

Первый шаг выполняется ассемблером или компилятором, а второй — компо-

новщиком.


background image

5 4 0 Глава 7. Уровень языка ассемблера

Трансляция исходной процедуры в объектном модуле — это переход на другой

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

исполняемый двоичный код.

 В системах MS-DOS, Windows 95/98 и NT объект-

ные модули имеют расширение .obj, а исполняемые двоичные программы — рас-
ширение .ехе. В системе UNIX объектные модули имеют расширение .о, а испол-
няемые двоичные программы не имеют расширения.

Исходная

процедура 1

Исходная

процедура 2

Исходная

процедура 3

Транслятор

Объектный

модуль 1

Объектный

модуль 2

Объектный

модуль 3

Компоновщик

Исполняемый

двоичный

код

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

оттранслированных независимо друг от друга процедур используется компоновщик

Компиляторы и ассемблеры транслируют каждую исходную процедуру как от-

дельную единицу. На это есть веская причина. Если компилятор или ассемблер

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

Если каждая процедура транслируется по отдельности, как показано на рис. 7.2,

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

Задачи компоновщика

В начале первого прохода ассемблирования счетчик адреса команды устанавлива-

ется на 0. Этот шаг эквивалентен предположению, что объектный модуль во время
выполнения будет находиться в ячейке с адресом 0. На рис. 7.3 показаны 4 объек-
тных модуля для типичной машины. В этом примере каждый модуль начинается
с команды перехода BRANCH к команде

 MOVE

 в том же модуле.

Чтобы запустить программу, компоновщик помещает объектные модули в ос-

новную память, формируя отображение исполняемого двоичного кода (рис. 7.4,

 а).

Цель — создать точное отображение виртуального адресного пространства ис-


background image

Связывание и загрузка

541

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

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

Объектный модуль В

Объектный модуль А

400

300

200

100

0

CALL В

MOVE P ТО X

BRANCH TO 200

еии

500

400

300

200

100

0

CALL С

MOVE Q ТО X

BRANCH TO 300

500

400

300

200

100

Объектный модуль С

CALLD

MOVE R ТО X

BRANCH TO 200

300

200

100

Объектный модуль D

MOVE S ТО X

BRANCH TO 200

Рис. 7.3. Каждый модуль имеет свое собственное адресное

пространство, начинающееся с нуля

Посмотрите на рис. 7.4,

 а.

 Хотя программа уже загружена в отображение ис-

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

Программа не совершит перехода к команде MOVE, поскольку эта команда находит-

ся в ячейке с адресом 300. Фактически все команды обращения к памяти не будут

выполнены по той же причине.


background image

5 4 2 Глава 7. Уровень языка ассемблера

1900

1800

1700

1600

1500

1400

1300

1200

1100

1000

900

800

700

600

500

400

300

200

100

п

MOVE S ТО X

BRANCH TO 200

CALLD

MOVE R ТО X

BRANCH TO 200

CALL С

MOVE Q ТО X

BRANCH TO 300

CALL В

MOVE P ТО X

BRANCH TO 200

Объектный

У

 модуль D

Объектный

i Объектный

/ модуль В

Объектный

модуль А

1900

1800

1700

1600

1500

1400

модуль С

  1 3 0 0

1200

1100

1000

900

800

700

600

500

400

300

200

100

0

MOVE S ТО X

BRANCH ТО 1800

CALL 1600

MOVE R TO X

BRANCH TO 1300

CALL 1100

MOVE Q TO X

BRANCH TO 800

CALL 500

MOVE P TO X

BRANCH TO 300

Объектный

/" модуль D

V Объектный

/ модуль С

v Объектный

}

 модуль В

Объектный

модуль А

Рис. 7.4. Объектные модули после размещения в двоичном отображении,

но до перераспределения памяти и связывания (а); те же объектные модули

после связывания и перераспределения памяти (б). В результате получается

исполняемая двоичная программа, которую можно запускать