- •Спекулятивная загрузка
- •Краткое содержание главы
- •Вопросы и задания
- •Глава 6
- •Виртуальная память
- •Страничная организация памяти
- •Реализация страничной организации памяти
- •Вызов страниц по требованию и рабочее множество
- •Политика замещения страниц
- •Размер страниц и фрагментация
- •Сегментация
- •Реализация сегментации
- •Виртуальная память Pentium 4
- •Виртуальная память UltraSparc III
- •Виртуальная память и кэширование
- •Виртуальные команды ввода-вывода
- •Реализация виртуальных команд ввода-вывода
- •Команды управления каталогами
- •Виртуальные команды для параллельной работы
- •Формирование процесса
- •Состояние гонок
- •Синхронизация процесса с использованием семафоров
- •Краткое содержание главы
- •Вопросы и задания
- •Уровень ассемблера
- •Знакомство с ассемблером
- •Макросы
- •Процесс ассемблирования
- •Компоновка и загрузка
- •Глава 8
- •Параллельные компьютерные архитектуры
Компоновка и загрузка
Большинство программ содержат более одной процедуры. Компиляторы и ассемблеры транслируют одну процедуру и записывают полученный результат на диск. Перед запуском программы должны быть найдены и скомпонованы все оттранслированные процедуры. Если виртуальная память недоступна, скомпонованная программа должна загружаться непосредственно в основную память. Программы, которые выполняют эти функции, называются по-разному: компоновщиками, компонующими загрузчиками, редакторами связей. Для полной трансляции исходной программы требуется два шага (рис. 7.2):
Компиляция или ассемблирование исходных процедур.
Компоновка объектных модулей.
Первый шаг выполняется ассемблером или компилятором, второй — компоновщиком.
Трансляция исходной процедуры в объектный модуль — это переход на другой уровень, поскольку исходный и выходной языки имеют разные команды и синтаксис. Однако при компоновке перехода на другой уровень не происходит, поскольку программы на входе и выходе компоновщика предназначены для одной и той же виртуальной машины. Цель компоновщика — скомпоновать вместе все процедуры, которые транслировались отдельно, чтобы в результате получился исполняемый двоичный код. В системах MS-DOS, Windows 95/98 и Windows NT объектные модули имеют расширение .obj, а исполняемые программы — .ехе. В UNIX объектные модули имеют расширение .о, а исполняемые программы расширения не имеют.
Компиляторы и ассемблеры транслируют каждую исходную процедуру как отдельную единицу. На это есть веская причина. Если компилятор или ассемблер считывал бы целый ряд исходных процедур и сразу переводил бы их в готовую программу на машинном языке, то при изменении одного оператора в исходной процедуре потребовалось бы заново транслировать все исходные процедуры.
Если каждая процедура транслируется отдельно, как показано на рис. 7.2, то транслировать заново придется только одну измененную процедуру, хотя понадобится заново скомпоновать все объектные модули. Однако компоновка происходит гораздо быстрее, чем трансляция, поэтому при доработке программы оба шага (трансляция и компоновка) выполняются быстро. Это особенно важно для программ, содержащих сотни или тысячи модулей.
Задачи компоновщика
В начале первого прохода ассемблирования счетчик адресов команд устанавливается на 0. Этот шаг эквивалентен предположению, что объектный модуль во время выполнения будет находиться в ячейке с адресом 0. На рис. 7.3 показаны 4 объектных модуля, подготовленных для типичной машины. В этом примере каждый модуль начинается с команды перехода BRANCH к команде MOVE в том же модуле.
Чтобы можно было запускать программу, компоновщик помещает объектные модули в основную память, формируя образ исполняемого двоичного кода (рис. 7.4, а). Цель — создать точный образ виртуального адресного пространства
исполняемой программы внутри компоновщика и разместить все объектные модули по соответствующим адресам. Если физической или виртуальной памяти для формирования образа недостаточно, можно использовать дисковый файл. Обычно небольшой раздел памяти, начинающийся с нулевого адреса, используется для векторов прерываний, взаимодействия с операционной системой, обнаружения неинициализированных указателей и других целей, поэтому, как правило, программы начинаются не с нулевого адреса, а выше. В нашем примере программы начинаются с адреса 100.
Посмотрите на рис. 7.4, а. Хотя программа уже загружена в образ исполняемого двоичного файла, она еще не готова для выполнения. Посмотрим, что произойдет, если выполнение программы начнется с команды в начале модуля А. Программа не совершит перехода к команде MOVE, поскольку эта команда находится в ячейке с адресом 300. Фактически все команды обращения к памяти по той же причине выполнены не будут.
Здесь возникает проблема перераспределения памяти, поскольку каждый объектный модуль на рис. 7.3 занимает собственное адресное пространство. В машине с сегментированным адресным пространством (например, в Pentium 4) каждый объектный модуль теоретически может иметь собственное адресное пространство, если его поместить в отдельный сегмент. Однако для Pentium 4 только система OS/2 поддерживает такую структуру6.
Все версии Windows и UNIX поддерживают одно линейное адресное пространство, поэтому все объектные модули должны быть объединены в одном адресном пространстве.
Более того, команды вызова процедур на рис. 7.4, а вообще не будут работать. В ячейке с адресом 400 программист намеревается вызвать объектный модуль В, но поскольку каждая процедура транслируется отдельно, ассемблер не может определить, какой адрес вставлять в команду CALL В, поскольку адрес объектного модуля В до компоновки неизвестен. Эта проблема называется проблемой внешней ссылки. Обе проблемы решаются с помощью компоновщика.
Компоновщик объединяет отдельные адресные пространства объектных модулей в единое линейное адресное пространство. Для этого совершаются следующие шаги:
Компоновщик строит таблицу объектных модулей и их размеров.
На основе этой таблицы он приписывает начальные адреса каждому объектному модулю.
Компоновщик находит все команды, которые обращаются к памяти, и прибавляет к каждой из них константу перераспределения, равную начальному адресу этого модуля
Компоновщик находит все команды, которые обращаются к процедурам, и вставляет в них адреса этих процедур.
Таблица 7.6 соответствует построенной на первом шаге таблице объектных модулей, представленных на рис. 7.4. В ней даются имя, длина и начальный адрес каждого модуля.
На рис. 7.4, б показано, как выглядит адресное пространство после завершения работы компоновщика.
Таблица 7.6. Имя, длина и начальный адрес каждого модуля на рис. 7.4
Модуль |
Длина |
Начальный адрес |
А |
400 |
100 |
В |
600 |
500 |
С |
500 |
1100 |
D |
300 |
1600 |
Структура объектного модуля
Объектные модули обычно состоят из шести частей:
Идентификация.
Таблица точек входа.
Таблица внешних ссылок.
Машинные команды и константы.
Словарь перераспределения.
Конец модуля.
В первой части содержатся имя модуля, некоторая информация, необходимая компоновщику (например, данные о длине различных частей модуля), а иногда дата ассемблирования.
Вторая часть объектного модуля — это список символов, определенных в модуле, вместе с их значениями. К этим символам могут обращаться другие модули. Например, если модуль состоит из процедуры bigbug, то элемент таблицы будет содержать символьную строку "bigbug" с соответствующим адресом. Программист, пишущий на языке ассемблера, с помощью директивы PUBLIC указывает, какие символические имена считаются точками входа.
Третья часть объектного модуля состоит из списка символических имен, которые применяются в данном модуле, а определены в других модулях. Здесь также имеется еще один список, который показывает, какие именно символические имена используются теми или иными машинными командами. Второй список нужен для того, чтобы компоновщик мог вставлять правильные адреса в команды, которые используют внешние имена. Процедура может вызывать другие независимо транслируемые процедуры, объявив имена вызываемых процедур внешними. Программист, пишущий на языке ассемблера, с помощью директивы EXTERN указывает, какие символы нужно объявить внешними. В некоторых компьютерах точки входа и внешние ссылки объединены в одной таблице.
Четвертая часть объектного модуля — машинные команды и константы. Это единственная часть объектного модуля, которая загружается в память для выполнения. Остальные пять частей используются компоновщиком, а затем отбрасываются еще до начала выполнения программы.
Пятая часть объектного модуля — это словарь перераспределения памяти. К командам, которые содержат адреса памяти, должна прибавляться константа перераспределения (см. рис. 7.4). Компоновщик сам не может определить, какие слова в части 4 содержат машинные команды, а какие — константы. Поэтому в этой таблице содержится информация о том, какие адреса нужно перераспределять. Это может быть битовая таблица, где на каждый бит приходится потенциально перераспределяемый адрес, либо явный список перераспределяемых адресов.
Шестая часть содержит указание на конец модуля, а иногда — контрольную сумму для определения ошибок, сделанных во время чтения модуля, и адрес, с которого должно начинаться исполнение.
Большинству компоновщиков требуется два прохода. На первом проходе компоновщик считывает все объектные модули и строит таблицу имен и размеров модулей, а также глобальную таблицу символов, которая состоит из всех точек входа и внешних ссылок. На втором проходе модули поочередно считываются, перераспределяются в памяти и компонуются.
Время компоновки и динамическое перераспределение памяти
В мультипрограммной системе программу можно считать в основную память, запустить на некоторое время, записать на диск, а затем снова считать в основную память для выполнения. В сложной системе с большим количеством программ трудно быть уверенным, что программа считывается каждый раз в одно и то же место в памяти.
На рис. 7.5 показано, что произойдет, если уже распределенная в памяти программа (см. рис. 7.4, б) будет загружена по адресу 400, а не по адресу 100, куда ее изначально поместил компоновщик. Все адреса памяти оказываются неправильными, и это при отсутствии какой-либо информации о распределении памяти. Даже если эта информация была бы доступна, перераспределять все адреса при каждой загрузке программы было бы расточительно.
Проблема перемещения программ, уже скомпонованных и размещенных в памяти, имеет непосредственное отношение к моменту окончательного связывания символических имен с абсолютными адресами физической памяти. В программе содержатся символические имена для адресов памяти (например, BR L). Время, в которое определяется адрес в основной памяти, соответствующий имени L, называется временем связывания. Существует по крайней мере шесть вариантов времени связывания:
Когда программа пишется.
Когда программа транслируется.
Когда программа компонуется, но еще до загрузки.
Когда программа загружается.
Когда загружается базовый регистр, который используется для адресации.
Когда выполняется команда, содержащая требуемый адрес.
Если команда, содержащая адрес, после связывания перераспределяется в памяти, этот адрес оказывается неправильным (предполагается, что объект, на который происходит ссылка, тоже перемещается в памяти). Если транслятор генерирует исполняемый двоичный код, то связывание происходит во время трансляции, и программа должна быть запущена с адреса, указанного транслятором. При применении метода, описанного в предыдущем подразделе, символические имена в процессе компоновки связываются с абсолютными адресами, и именно по этой причине перемещать программы после компоновки нельзя (см. рис. 7.5).
Здесь возникает два вопроса. Во-первых, когда символические имена связываются с виртуальными адресами? Во-вторых, когда виртуальные адреса связываются с физическими адресами? Только после двух этих операций процесс связывания можно считать завершенным. Когда компоновщик объединяет отдельные адресные пространства объектных модулей в единое линейное адресное пространство, он фактически создает виртуальное адресное пространство. Перераспределение в памяти и компоновка нужны для связывания символических имен с определенными виртуальными адресами. Это верно независимо от того, используется виртуальная память или нет.
Предположим, что адресное пространство, изображенное на рис. 7.4, б, было разбито на страницы. Ясно, что виртуальные адреса, соответствующие символическим именам А, В, С и D, уже определены, хотя их физические адреса будут зависеть от содержания таблицы страниц. Реально связывание символических имен с виртуальными адресами происходит в исполняемом двоичном коде.
Любой механизм, позволяющий легко изменять отображение виртуальных адресов на адреса основной физической памяти, упростит перемещение программы в основной памяти, даже если они уже связаны с виртуальным адресным пространством. Одним из таких механизмов является разбиение на страницы. Если программа перемещается в основной памяти, потребуется изменить только ее таблицу страниц, а не саму программу.
Второй механизм — применение регистра перераспределения времени исполнения. Компьютер CDC 6600 и его последователи содержали такой регистр. В машинах, в которых используется эта технология перераспределения памяти, регистр всегда указывает на физический адрес начала текущей программы. Аппаратно этот регистр добавляется ко всем адресам перед их распределением в памяти. Весь процесс перераспределения памяти является прозрачным для пользовательских программ. Пользовательские программы даже не подозревают, что происходит перераспределение памяти. Если программа перемещается, операционная система должна обновить регистр перераспределения. Такой механизм менее распространен, чем разбиение на страницы, поскольку перемещаться должна вся программа целиком (если есть отдельные регистры перераспределения для кода и данных, как, например, в процессоре Intel 8088, то объектами перемещения будут обе части программы).
Третий механизм можно использовать в машинах, в которых реализована возможность обращения к памяти относительно счетчика команд. В этих машинах всякий раз, когда программа перемещается в основной памяти, достаточно обновить только счетчик команд. Программа, все обращения к памяти которой либо связаны со счетчиком команд, либо абсолютны (например, обращения по абсолютным адресам к регистрам устройств ввода-вывода), называется позиционно-независимой. Позиционно-независимую процедуру можно поместить в любом месте виртуального адресного пространства без необходимости перераспределять адреса памяти.
Динамическая компоновка
Стратегия компоновки, которую мы обсуждали в подразделе «Задачи компоновщика», имеет одну особенность: все процедуры, требуемые программе, компонуются до начала работы программы. Однако если мы будем устанавливать все связи до начала работы программы в компьютере с виртуальной памятью, то мы не используем всех возможностей виртуальной памяти. Многие программы содержат процедуры, которые вызываются только при определенных обстоятельствах. Например, компиляторы содержат процедуры для компиляции редко используемых операторов или исправления редко встречающихся ошибок.
Более гибкий способ компоновки раздельно скомпилированных процедур — компоновка каждой процедуры в тот момент, когда она впервые вызывается. Этот процесс называется динамической компоновкой. Впервые он был применен в системе MULTICS. Давайте рассмотрим примеры динамической компоновки в нескольких системах.
Динамическая компоновка в MULTICS
В системе MULTICS с каждой программой соотносится сегмент, так называемый сегмент компоновки (linkage segment). Он содержит один информационный блок для каждой процедуры, которая может быть вызвана. Этот блок начинается со слова, зарезервированного для виртуального адреса процедуры, за ним следует имя процедуры, которое сохраняется в виде символьной строки.
При динамической компоновке вызовы процедур во входном языке транслируются в команды, которые путем косвенной адресации обращаются к первому слову соответствующего блока, как показано на рис. 7.6, а. Компилятор заполняет это слово либо недействительным адресом либо специальным набором битов, который вызывает исключение.
Когда вызывается процедура в другом сегменте, попытка косвенно обратиться к недействительному слову вызывает исключение компоновщика. Затем компоновщик находит символьную строку в слове, которое следует за неправильным адресом, и начинает искать пользовательский каталог для скомпилированной процедуры с найденным именем. Далее этой процедуре выделяется виртуальный адрес (обычно в ее собственном сегменте), и этот виртуальный адрес записывается поверх неправильного адреса, как показано на рис. 7.6, б. После этого команда, которая вызвала ошибку компоновки, выполняется заново, что позволяет программе продолжать работу с того места, где она находилась до исключения.
Все последующие обращения к этой процедуре будут выполняться без ошибок, поскольку недействительное ранее слово теперь содержит правильный виртуальный адрес. Следовательно, при динамической компоновке компоновщик вызывается только тогда, когда процедура вызывается впервые. Вызывать его повторно уже не нужно.
Динамическая компоновка в Windows
Все версии Windows, в том числе Windows NT, поддерживают динамическую компоновку. При динамической компоновке используется специальный файловый формат, который называется DLL (Dynamic Link Library — библиотека динамической компоновки). Библиотеки динамической компоновки могут содержать процедуры, данные или то и другое вместе. Обычно они применяются для того, чтобы два и более процессов могли разделять процедуры и данные библиотеки. Большинство DDL-файлов имеют расширение .dll, но встречаются и другие расширения, например .drv (для библиотек драйверов — driver libraries) и .fon (для библиотек шрифтов — font libraries).
Самая распространенная форма DLL — библиотека, состоящая из набора процедур, которые могут загружаться в память и к которым имеют доступ несколько процессов одновременно. На рисунке 7.7 показаны два процесса, которые совместно используют DLL-файл, содержащий 4 процедуры, Л, В, С и D. Программа 1 использует процедуру А; программа 2 — процедуру С, хотя они вполне могли бы использовать одну и ту же процедуру.
DLL-файл строится компоновщиком из группы входных файлов. Построение DDL напоминает создание исполняемого двоичного кода, только при создании DLL компоновщику передается специальный флаг, сообщающий о том, что требуется именно библиотека DLL. DLL-файлы обычно собираются из набора библиотечных процедур, которые могут понадобиться нескольким процессам. Типичными примерами DLL-файлов являются процедуры сопряжения с библиотекой системных вызовов Windows и большими графическими библиотеками. Применяя DDL-файлы, мы экономим пространство в памяти и на диске. Если бы та или иная библиотека была связана статически с каждой использующей ее программой, эту библиотеку пришлось бы включать во все исполняемые двоичные программы в памяти и на диске, что не экономно. А при наличии DLL-файла на диске и в памяти будет находиться всего одна библиотека.
Кроме того, такой подход упрощает обновление библиотечных процедур, причем даже после того, как использующие их программы скомпилированы и скомпонованы. Для коммерческих программных пакетов, где входная программа пользователям обычно недоступна, наличие DLL-файлов означает, что поставщик программного обеспечения сможет исправлять обнаруженные программные ошибки, просто распространяя новые DLL-файлы через Интернету, причем при этом не потребуется никаких изменений в двоичных кодах основных программ.
Основное различие между DLL и исполняемой двоичной программой состоит в том, что DLL-файл не может запускаться и работать сам по себе (поскольку у него нет главной программы). Он также содержит совершенно другую информацию в заголовке. Кроме того, DLL-файл имеет несколько дополнительных процедур, не связанных с процедурами в библиотеке. Например, существует одна процедура, которая вызывается автоматически всякий раз, когда новый процесс связывается с DLL-файлом, и еще одна процедура, которая вызывается автоматически всякий раз, когда связь процесса с DLL-файлом разрывается. Эти процедуры могут выделять и освобождать память или управлять другими ресурсами, необходимыми DLL-файлу.
Программа может установить связь с DLL-файлом двумя способами: путем неявной или явной компоновки. При неявной компоновке пользовательская программа статически компонуется со специальным файлом, так называемой библиотекой импорта. Библиотека импорта создается специальной утилитой, предназначенной для извлечения определенной информации из DLL-файла. Библиотека импорта предоставляет связующее звено, через которое пользовательская программа получает доступ к DLL-файлу. Пользовательская программа может быть связана с несколькими библиотеками импорта. Когда программа, в которой имеет место неявная компоновка, загружается в память для исполнения, Windows проверяет, какие DLL-файлы ей требуются и все ли они находятся в памяти. Те файлы, которых еще нет в памяти, немедленно туда загружаются (но необязательно целиком, поскольку они разбиты на страницы). Затем производятся определенные изменения структур данных в библиотеках импорта так, чтобы можно было определить местоположение вызываемых процедур (это похоже на изменения, которые иллюстрирует рис. 7.6). Их тоже нужно отобразить на виртуальное адресное пространство программы. С этого момента пользовательскую программу можно запускать, поскольку она может вызывать процедуры из DLL-файлов, как будто они статически с ней связаны.
Альтернативой неявной является явная компоновка. Явная компоновка не требует ни библиотек импорта, ни одновременной загрузки DLL-файлов с пользовательской программой. Вместо этого пользовательская программа делает явный вызов прямо во время работы, чтобы установить связь с DLL-фай- лом, а затем совершает дополнительные вызовы, чтобы получить адреса процедур, которые ей требуются. Когда все это сделано, программа совершает финальный вызов, чтобы разорвать связь с DLL-файлом. Когда последний процесс разрывает связь с DLL-файлом, этот файл может быть выгружен из памяти.
Важно понимать, что процедура в DLL-файле не имеет отличительных признаков (как процессы или программные потоки). Она работает в потоке вызывающей программы и для своих локальных переменных использует стек вызывающей программы. Она может содержать статические данные, специфичные для процесса (а также общие), а в остальном работает, как статически скомпонованная процедура. Единственным существенным различием является способ установления связи.
Динамическая компоновка в UNIX
В UNIX используются библиотеки коллективного доступа, по сути напоминающие библиотеки DLL Windows. Как и DLL-файл, библиотека коллективного доступа представляет собой архивный файл, содержащий несколько процедур или модулей данных, которые присутствуют в памяти во время работы программы и одновременно могут быть связаны с несколькими процессами. Стандартная библиотека С и большинство сетевых программ являются библиотеками коллективного доступа.
UNIX поддерживает только неявную компоновку, поэтому библиотека коллективного доступа состоит из двух частей: главной библиотеки (host library), которая статически скомпонована с исполняемым файлом, и целевой библиотеки (target library), которая вызывается во время работы программы. Несмотря на некоторые различия в деталях, по существу эта концепция соответствует концепции DLL.
Краткое содержание главы
Хотя большинство программ можно и нужно писать на языках высокого уровня, существуют такие ситуации, в которых необходимо применять язык ассемблера, по крайней мере, отчасти. Сюда можно отнести программы для компьютеров с недостаточным объемом ресурсов (например, процессоры для смарт-карт, различных приборов, портативных цифровых устройств). Ассемблерная программа — это символьное представление программы на том или ином машинном языке. Она транслируется на машинный язык специальной программой, которая называется ассемблером.
Если для какого-либо приложения требуется высокое быстродействие, лучше всего сначала написать программу на языке высокого уровня, затем путем тестов установить, исполнение какой части программы занимает большую часть времени, и переписать на ассемблере только эти части программы. Практика показывает, что часто небольшая часть всей программы занимает большую часть всего времени выполнения этой программы.
Во многих ассемблерах предусмотрены макросы, которые позволяют программистам давать символические имена целым последовательностям команд. Обычно эти макросы могут быть параметризированы. Макросы реализуются с помощью алгоритма обработки строковых литералов.
Большинство ассемблеров двухпроходные. Во время первого прохода строится таблица символов для меток, литералов и объявляемых идентификаторов. Символические имена можно либо не сортировать и искать путем последовательного просмотра таблицы, либо сначала сортировать, а потом применять двоичный поиск, либо хэшировать. Если символические имена не нужно удалять во время первого прохода, хэширование — это лучший метод. Исполняемая программа создается во время второго прохода. Что касается директив (псевдокоманд), то одни из них выполняются при первом проходе, другие — при втором.
Программы, которые ассемблируются независимо друг от друга, можно скомпоновать вместе и получить исполняемую двоичную программу. Эту работу выполняет компоновщик. Его цель — это перераспределение в памяти и связывание имен. Динамическая компоновка — это технология, при которой определенные процедуры не компонуются до тех пор, пока они не вызваны. Библиотеки коллективного доступа в UNIX и библиотеки динамической компоновки (DLL) в Windows используют технологию динамической компоновки.
Вопросы и задания
Всего 2 % кода программы отвечают за 50 % времени выполнения этой программы. Сравните следующие три стратегии с точки зрения времени программирования и выполнения. Предположим, что для написания программы
на языке С требуется 100 человеко-месяцев, а программу на ассемблере писать в 10 раз труднее, но зато она работает в 4 раза эффективнее.
Вся программа написана на С.
Вся программа написана на ассемблере.
Программа сначала написана на С, а затем нужные 2 % программы переписаны на ассемблере.
Для двухпроходных ассемблеров существуют определенные соглашения. Могли бы они подойти и для следующих компиляторов:
Компилятор вместо ассемблерного кода генерирует объектные модули?
Компилятор генерирует символический язык ассемблера?
Все ассемблеры для процессоров Intel в качестве первого операнда получают целевой адрес, а в качестве второго — исходный адрес. Какие проблемы могут возникнуть при другом подходе?
Можно ли следующую программу ассемблировать в два прохода? EQU — это директива, которая приравнивает метку к выражению в поле операнда.
A EQU В В EQU С С EQU D D EQU 4
Некая компания планирует разработать ассемблер для компьютера с 40-раз- рядным словом. Чтобы снизить стоимость, менеджер проекта решил ограничить длину символических имен, чтобы каждое имя можно было хранить в одном слове. Менеджер объявил, что символические имена могут состоять только из букв, причем буква Q запрещена. Какова максимальная длина символического имени? Опишите вашу схему кодирования.
Чем отличается команда от директивы?
Чем отличается счетчик адресов команд от счетчика команд? А существует ли вообще между ними различие? Ведь тот и другой выполняют трассировку следующей команды в программе.
Какой будет таблица символов (имен) после обработки следующих операторов ассемблера Pentium 4 (первому оператору приписан адрес 1000)?
EVEREST: |
POP BX |
(1 байт) |
К2: |
PUSH BP |
(1 байт) |
WHITNEY: |
MOV BP.SP |
(2 байта) |
MCKINLEY: |
PUSH X |
(3 байта) |
FUJI: |
PUSH SI |
(1 байт) |
KIBO: |
SUB SI,300 |
(3 байта) |
Можете ли вы представить себе обстоятельства, при которых метка совпадет с кодом операции (например, может ли команда MOV использоваться в качестве метки)? Аргументируйте.
Какие шаги нужно совершить, чтобы путем двоичного поиска найти элемент «Berkeley» в следующем списке: Ann Arbor, Berkeley, Cambridge, Eugene, Madison, New Haven, Palo Alto, Pasadena, Santa Cruz, Stony Brook, Westwood, Yellow Springs. Когда будете вычислять средний элемент в списке из четного числа элементов, возьмите элемент, который идет сразу после среднего.
И. Можно ли использовать двоичный поиск в таблице, в которой содержится простое число элементов?
Вычислите хэш-код для каждого из следующих символических имен. Для этого сложите буквы (а = 1, Ъ = 2 и т. д.) и возьмите результат по модулю размера хэш-таблицы. Хэш-таблица содержит 19 слотов (от 0 до 18).
els, jan, jelle, maaike
Образует ли каждое символическое имя уникальное значение хэш-функции? Если нет, то как разрешить эту коллизию?
Метод хэширования, описанный в тексте главы, позволяет скомпоновать все элементы, имеющие один хэш-код, в связном списке. Альтернативный метод — иметь только одну таблицу из п слотов, в которой в каждом слоте имеется пространство для одного ключа и его значения (или для указателей на них). Если алгоритм хэширования порождает слот, который уже заполнен, производится вторая попытка с использованием того же алгоритма хэширования. Если и на этот раз слот заполнен, алгоритм применяется снова и т. д. Так продолжается до тех пор, пока не будет найден пустой слот. Если доля слотов, которые уже заполнены, составляет R, сколько попыток в среднем понадобится для того, чтобы ввести в таблицу новый символ?
Вероятно, когда-нибудь в будущем на одну микросхему можно будет помещать тысячи идентичных процессоров, каждый из которых содержит несколько слов локальной памяти. Если все процессоры могут считывать и записывать три общих регистра, то как реализовать ассоциативную память?
Pentium 4 имеет сегментированную архитектуру. Сегменты независимы. Ассемблер для этой машины может содержать директиву SEG N, которая помещает последующий код и данные в сегмент N. Повлияет ли такая схема на счетчик адресов команд?
Программы часто компонуются с многочисленными библиотеками DLL. А не будет ли более эффективным просто поместить все процедуры в один большой DLL-файл, а затем скомпоновать его?
Можно ли отобразить DLL-файл на виртуальные адресные пространства двух процессов с разными виртуальными адресами? Если да, то какие проблемы при этом возникают? Можно ли их разрешить? Если нет, то что можно сделать, чтобы устранить их?
Опишем один из способов компоновки (статическая компоновка). Перед сканированием библиотеки компоновщик составляет список необходимых процедур, то есть имен, которые в компонуемых модулях определены как внешние (EXTERN). Затем компоновщик последовательно просматривает всю библиотеку, извлекая каждую процедуру, которая находится в списке нужных имен. Будет ли работать такая схема? Если нет, то почему и как это можно исправить?
Может ли регистр использоваться в качестве фактического параметра в макровызове? А константа? Если да, то почему? Если нет, то почему?
Вам нужно реализовать макроассемблер. Из эстетических соображений ваш начальник решил, что макроопределения не должны предшествовать вызовам макросов. Как повлияет это решение на реализацию?
Подумайте, как можно поместить макроассемблер в бесконечный цикл.
Компоновщик считывает 5 модулей размером 200, 800, 600, 500 и 700 слов соответственно. Если они загружаются в этом порядке, то каковы константы перераспределения памяти?
Напишите модуль таблицы символов, состоящий из двух процедур: enter(symbol, value) и lookup (symbol, value). Первая вводит новые символические имена в таблицу, а вторая ищет их в таблице. Используйте какой-либо вариант хэширования.
Напишите простой ассемблер для компьютера Mic-1, о котором мы говорили в главе 4. Помимо оперирования машинными командами, обеспечьте возможность приписывать константы символическим именам во время ассемблирования, а также возможность ассемблировать константу в машинное слово.
Добавьте макросы к ассемблеру, который вы должны были написать, выполняя предыдущее задание.
