суббота, 3 сентября 2011 г.

Вызов импортируемой функции, наивный способ

Это перевод Calling an imported function, the naive way. Автор: Реймонд Чен.

Библиотека импорта (import library) разрешает (resolve) символы импортируемых функций, но к ней не обращаются до этапа компоновки. Давайте посмотрим на наивную реализацию, когда компилятор слепо не осведомлён о существовании импортируемых функций.

В 16-битном мире это не вызывало никаких проблем. Компилятор генерировал дальний (far) вызов функции и оставлял внешнюю запись в объектном файле, указывающую, что вот этот адрес функции требует заполнения компоновщиком. На стадии компоновки линкер осознаёт, что этот внешний символ соответствует импортируемой функции, так что он собирает вместе все места её вызова в цепочку и создаёт запись импорта функции в таблице импорта модуля. В run-time эти записи исправляются загрузчиком ОС и все счастливы.

Давайте посмотрим, как справится с этой же ситуацией наивный 32-битный компилятор. Компилятор сгенерирует обычную инструкцию вызова функции, перекладывая разрешение адреса на компоновщик. Линкер увидит, что этот внешний символ, на самом деле, является импортируемой функций, и, ой, прямой вызов нужно переделать в косвенный. Но компоновщик не может переписать код, сгенерированный компилятором. Что же делать компоновщику?

Решение заключается во вставке ещё одно уровня косвенности (предупреждение: информация ниже не верна буквально, но она "достаточно правдоподобна". Мы копнём детали в следующих постах).

Для каждой экспортируемой функции создаётся два символа. Первый из них - запись в таблице импортируемых функций. Назовём его __imp__FunctionName. Конечно же, наивный компилятор не знает ни про какой префикс __imp__. Он просто генерирует код для инструкции call FunctionName, ожидая что компоновщик подставит нужный адрес.

Тут на сцену выходит второй символ. Он имеет имя FunctionName и является однострочной функцией, состоящей из одной-единственной инструкции: jmp [__imp__FunctionName] (генерируется компоновщиком). Эта крохотная заглушка предназначена для удовлетворения внешних ссылок на функцию FunctionName, и в свою очередь она генерирует ссылку на __imp__FunctionName, которая разрешается записью в таблице импортируемых функций.

Когда модуль загружается - его зависимости импорта будут разрешены, и реальный адрес функции будет записан в __imp__FunctionName. Затем, когда код вызовет импортируемую функцию, то сгенерированный компилятором код вызовет функцию FunctionName, которая является заглушкой, которая и вызовет целевую функцию через косвенный вызов.


Заметьте, что с наивным компилятором, если ваш код попытается взять адрес импортируемой функции, то он получит адрес заглушки, поскольку наивный компилятор оперирует с адресом функции FunctionName, не зная о том, что она импортируется из другого модуля и для неё, на самом деле, создаётся заглушка.

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

5 комментариев:

  1. Хех, какой знакомый код. Я на него насмотрелся, помнится, когда враппер к kernel32 и user32 писал. :)

    P.S. Ещё бы понять зачем там вставляется этот mov eax, eax... Была мысль, что для выравнивания кода, но почему именно mov, а не серия nop'ов?..

    ОтветитьУдалить
  2. Выравнивание кода однозначно. С ним - 8 байт ровно. А вот почему именно mov eax, eax - я без понятия.

    ОтветитьУдалить
  3. Единственная сколько-нибудь разумная мысль - mov использован с целью сократить ассемблерный листинг. Так в два раза больше функций на экран влезает.

    ОтветитьУдалить
  4. По поводу mov eax, eax
    http://blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

    ОтветитьУдалить
  5. Не, это не оно.

    По ссылке идёт речь о командах внутри функций. Что сама функция в DLL начинается с этой команды. А здесь на этот mov управление даже никогда не передаётся.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.