Pages

Saturday, September 12, 2009

Win32: Формат вызова функций

Что происходит при вызове функций из програм на C/C++? Я о том, что в функцию нужно передать параметры, получить возвращаемое значение.
В общем-то, большинство программистов не хотять знать такие подробности. Мне в нескольких критических ситуациях пришлось разбираться с этим вопросом. Например, имея только DLL экспортирующий класс, я умудрился создать объект этого класса и вызвать из него функцию.
Я не академик, но имею академический склероз и каждый раз забываю технические детали, которыми пользуюсь не часто. Если опишу то, что помню сейчас о форматах вызова функций на Win32 платформе, то, если понадобиться снова, буду знать, где искать.

Процесс вызова функций для Win32 можно описать так:
1. Все параметры функций выравниваются до 4 байт и размещаются либо на стеке, либо в регистрах.
2. Процесс выполнения программы переходит в вызываемую функцию.
3. В начале функции созраняются в стеке регистры ESI, EDI, EBX, EBP. Это стандартное начало и его называют проголом функции.
4. Выполняется сама функция и ее возвращаемое значение заносится в регистр EAX.
5. Восстанавливаются регистры ESI, EDI, EBX, EBP из стека. Эта часть называется эпилогом функции.
6. Параметры функции удаляются из стека. Эту операцию называют очисткой стека (stack cleanup).

Для программиста на С, C++ или Basic, код для всех этих 6 названных пунктов «пишет» компилятор.
Только иногда, если мы, например, разрабатываем DLL или пишем математический код на C, или пытаемся оптимизировать нашу программу, мы задаем иногда что-то типа __cdecl, __stdcall или __fastcall в определении функций. Просто потому, что так написано в книге или статье, даже не задумываясь и не запоминая.
Если посмотреть на самую главную функцию WinMain, то и перед ее именем стоит WINAPI. Но мало кому интересно зачем это нужно.

Сделаем маленький шажок в глубь – рассмотрим форматы вызова функций на Win32 платформе.
Формат вызова – это мой свободный перевод ангийского Calling Convention. Буквальный перевод, наверное, соглашение вызова. Просто моему уху это не приятно. Хоть я не удивлюсь, если в университетах используют именно такой перевод – абсолютно точно и абсолютно непонятно.
Итак, разберемся с __cdecl, __stdcall, WINAPI, __fastcall и Thiscall. Первые 4 термина уже упоминались, а Thiscall... и по названию понятно, что речь о классах на C++.

__cdecl
Этот формат (соглашение) принято по умолчанию для C/C++ програм – если явно не указан формат вызова функции, то подразумевается именно этот __cdecl:
1. Параметры передаются в функцию справа налево – заталкиваются в стек в порядке справа налево.
2. Вызываемая программа очищает стек после выполнения кода функции.
Формат вызова определяет и так называемое декорирование имени функции (name decorating), в случае __cdecl впереди к имени функции добавляется символ подчеркивания ‘_’.Например, для функции объявленной как:
int Sum(int x, int y);

компилятор создаст новое имя: _Sum.
Вот простая програмка, которая все покажет:
int __cdecl Sum(int x, int y);

int main()
{
int a = Sum(1, 2);
return 0;
}

int Sum(int x, int y)
{
return x + y;
}
Если поставить точку останова в строке с вызовом функции и перейти в окно дизассеблирования, то все видно:

А вот и сама функция:

Здесь видно и пролог и эпилог и очистку стека. Правда у функции оказалось имя Sum – это «обман зрения» для тех, кто дебагирует? MSDN утверждает, что символ подчеркивания должен быть в начале имени функции.
Наверное, так и есть. Проверить можно используя утилиту dumpbin:
>dumpbin /disasm main.obj

А тут уже все, как утверждает MSDN:

Ключ компилятора /Gd указывает использовать __cdecl формат вызова функций.
MSDN в разделе Visual C++ дает информацию об этом формате на этой странице:
http://msdn.microsoft.com/en-us/library/zkwh89ks.aspx

__stdcall
Этот формат (соглашение) используется для вызова Win32 API. Формат WINAPI обозначает то же самое – в файле windef.h можно найти:
#define WINAPI      __stdcall
Компилятору можно задать ключ /Gz и все функции, у которых явно не определен формат вызова будут сгенерированы как __stdcall:
1. Параметры заносятся в стек в порядке справа налево.
2. Стек очищается самой вызываемой функцией.
3. К имени функции вначале добавляется символ подчеркивания ‘_‘, и в конце символ @ и число байт в стеке (то есть _Sum@8 для нашего примера).
Стек очищается самой вызываемой функцией, то есть размер выполняемого файла (executable) меньше, чем в случае __cdecl, где стек очищается вызывающей программой и поэтому код очистки генерируется для каждого вызова функции.
Этот формат жестко указывает размер стека и поэтому он неприменим для функций с переменным числом параметров таких как printf.
Вот описание этого формата в MSDN:
http://msdn.microsoft.com/en-us/library/zxk0tw93.aspx

__fastcall
Этот формат указывает компилятору расположить (когда это возможно) параметры функции в регистрах, а не в стеке. Характеристики этого формата вызова следующие:
1. Первые два параметра должны быть 32-битными или короче. Они размещаются в регистрах ECX и EDX. Остальные параметры заталкиваются в стек справа налево.
2. Стек очищается вызываемой функцией.
3. К имени функции вначале добавляется символ @ и в конце символ @ и число байт в стеке (для нашего примера @Sum@8).
Ключ /Gr включает __fastcall формат для вызова функций.
Вот, что говорит об этом формате вызова MSDN:
http://msdn.microsoft.com/en-us/library/6xa169sk.aspx

__thiscall
И по названию понятно, что этот формат применяется по умолчанию для функций-членов С++ класса (кроме тех, что имеют переменное число параметров). Основные характеристики этого формата следующие:
1. Параметры передаются справа налево и размещаются в стеке.
2. This размешается в регистре ECX.
3. Очистка стека выполняется вызываемой функцией.
Если функция-член класса имеет переменное число параметров, то используется формат __cdecl и this заносится в стек последним.
MSDN о __thiscall:
http://msdn.microsoft.com/en-us/library/ek8tkfbw.aspx

Вот так все просто. Сложно не забыть. Поэтому хорошо, если есть возможность "подсмотреть" в те моменты, когда эта информация нужна.

В MSND есть еще описания для naked («голых») функций – тех, для которых не нужно писать пролога и эпилога. Например если функция на встроенном ассемблере. Есть и формат вызова для managed code (управляемый код) - __clrcall. Есть описания уже устаревших форматов - __pascal, __fortran, __syscall.

No comments:

Post a Comment