Как DLL функции экспортировались в 16-битной Windows?


Данная публикация является  переводом блога Раймонда Чена The Old New Thing.

Ключевой момент динамически связываемых библиотек (DLL) заключается в том, что связывание является динамическим. Если статические библиотеки просто включаются в окончательный продукт, то модуль, использующий динамическую библиотеку, просто говорит: "Мне нужна функция Х из Y.DLL". Этот подход имеет достоинства и недостатки. Одно из достоинств заключается в более эффективном использовании памяти, так как в памяти присутствует только одна копия библиотеки Y.DLL, вместо нескольких копий, встроенных в каждый модуль. Другое достоинство заключается в том, что обновление Y.DLL можно произвести без необходимости перекомпилировать все программы, которые ее используют. С другой стороны, возможность изменять функциональность является одним из главных недостатков динамических библиотек, потому что одна программа может изменить DLL таким образом, что вызовет каскадный эффект в других программах, использующих эту библиотеку.

В любом случае, давайте начнем с того, как 16-битная Windows управляла импортом и экспортом. После этого мы рассмотрим, что изменилось с переходом на 32-битную Windows, а затем  коснемся опции компилятора dllimport. (dllexport обсуждалась ранее)

16-битная DLL библиотека имеет не одну, а три таблицы экспорта. (На самом деле, все обстоит еще сложнее, чем описано здесь, но для того, чтобы уберечь головы читателей от опухания, мы опустим детали.) Наиболее важная таблица является разреженным массивом функций, индексируемая целыми  числам, начиная с 1 ("порядковый номер"). Эта таблица функций является основным списком для всех экспортируемых функций. Если вы запрашиваете функцию по ее порядковому номеру, этот номер ищется в этой таблице. Физически таблица является более сложной структурой, ввиду ее разреженности, но логически она выглядит так:

Номер

Адрес

Прочие данные

1

02:0014

2

04:0000

5

02:02С8

Первая колонка в таблице — это порядковый номер функции, а вторая — указывает, где эта функция находится. (Обратите внимание, что здесь нет функций с номерами 3 или 4) Ситуация становится интереснее, когда вы хотите экспортировать функцию по ее имени. Таблица имен экспортируемых функций представляет собой список имен и их порядковые номера. Например, секция таблицы экспортируемых имен для 16-битного оконного менеджера (USER) выглядела так:

ClipCursor           16

GetCursorPos      17

SetCapture          18

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

Постойте, разве я сказал "таблица экспортируемых имен"? Прошу прощения, это было бы сверхупрощением. На самом деле существует две таблицы экспортируемых имен, резидентная и нерезидентная. Как следует из названия, имена в резидентной таблице остаются в памяти до тех пор, пока DLL загружена, тогда как имена из нерезидентной таблицы загружаются в память только когда кто-то вызывает GetProcAddress (или ее эквивалент). Такое разделение является отражением чрезвычайно жестких ограничений используемой памяти, которые существовали в то время. Например, оконный менеджер (USER) имеет более шестисот экспортируемых функций. Если все имена будут загружены в память, это займет более 10 килобайт данных. В итоге более 4% памяти на машине с 256 Кб ОЗУ было бы потрачено на хранение данных, в которых большую часть времени не было необходимости.

Большой размер таблицы для имен экспортируемых функций означал, что только те функции, для которых GetProcAddress вызывался чаще всего должны быть размещены в резидентной таблице. Для большинства DLL ни одна из функций не попадает в данную категорию и резидентная таблица остается пустой. (Опять же, подробности опущены, дабы сохранить психику в целости.)

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

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

Это был беглый обзор того, как функции экспортировались в 16-битной Windows. В следующий раз мы рассмотрим, как они импортируются.

Реклама
Запись опубликована в рубрике DLL. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s