Соглашение вызова fastcall не может использоваться на современных процессорах

Обновлено: 03.07.2024

В общем, параметры передаются через стек в обратном порядке, результирующее значение будет в EAX кроме чисел с плавающей точкой — они будут в псевдо-стеке x87.

Составим план работы:
1) Сгенерировать некий буфер в памяти, который можно будет без изменений, пословно(4 байта) поместить в стек.
2) Узнать адрес функции, которую будем вызывать
3) Поместить в стек буфер по словам
4) Вызвать функцию
5) Выдернуть результат

Что же у нас есть:
1) char* sName — тут находится имя функции
2) int N — количество параметров
3) enum CParamType — возможные типы данных — ограничимся пока что этими
4) CParamType Params[] — список типов параметров
5) void* ParamList[] — собственно, указатели на переменные с параметрами
6) CParamType RetType — тип данных результата
7) void* Ret — указатель на память, куда нужно скинуть результат
8) enum CCallConvention — типы соглашений вызова
9) CCallConvention conv — соглашение вызова. Для начала будем вызывать только cdecl функции

Это необходимый и достаточный список объявлений, которые нам нужны для вызова.
На C/C++ нету средств для осуществления этой операции, поэтому придется обратиться к ассемблеру.

1. Создаем буфер


Во-первых, посчитаем количество слов. Все просто — void*, int — 4 байта — 1 слово, double — 8 байт — 2 слова.
  1. int WordCount= 0 ;
  2. for ( int i= 0 ,i<N,i++)
  3. switch (Params[i])
  4. case cptPointer:
  5. case cptInt:
  6. WordCount++;
  7. break ;
  8. case cptDouble:
  9. WordCount+= 2 ;
  10. break ;
  11. >
  12. >

Посчитали. Выделяем память:
void* Buffer = new char[4*WordCount];

Заполняем буфер: void*, int — помещаем без изменений, а в double меняем слова местами.

  1. int offset= 0 ;
  2. double x;
  3. for ( int i= 0 ,i<N,i++)
  4. switch (Params[i])
  5. case cptPointer:
  6. case cptInt:
  7. *( int *)(buf+offset)=*(( int *)(ParamList[i]));
  8. offset+= 4 ;
  9. break ;
  10. case cptDouble:
  11. x=*(( double *)(((DTMain*)(v->T))->pData));
  12. memcpy(buf+offset+ 4 ,&x, 4 );
  13. memcpy(buf+offset,( char *)&x+ 4 , 4 );
  14. offset+= 8 ;
  15. break ;
  16. >
  17. >

Думаю, тут комментировать нечего. offset — смещение по буферу.

2. Узнаем адрес функции

Тут все достаточно просто.
void* addr = dlsym(NULL,sName);
Где первый параметр — дескриптор библиотеки. NULL для поиска в текущем контексте.
Подключаем dlfcn.h и не забываем в параметры линковки дописать -ldl.

3. Помещаем в стек буфер по словам


Фуух. Самое интересное.
Для работы со стеком нам, естественно, понадобится ассемблер. Я пользуюсь gnu компилятором, поэтому ассемблер с синтаксисом AT&T — ногами не пинать, мне самому не очень нравится, но выбирать не приходится.
  1. asm( "\ <br/> movl $0, %%eax;\ <br/> movl %2,%%ebx; \ <br/> movl %3,%%ecx; \ <br/> l1: cmpl %%ecx, %%eax; \ <br/> je l2;\ <br/> pushl (%%ebx,%%eax,4); \ <br/> addl $1,%%eax;\ <br/> jmp l1;"
  2. : "=r" (b)
  3. : "r" (addr), "r" ( Buffer ), "g" (WordCount)
  4. : "%eax"
  5. );

Мы делаем цикл: пока ecx (WordCount) не станет 0, кладем в стек слово и уменьшаем ecx.

4. Вызываем функцию

Делаем
l2: call *%1;
после заполнения стека. %1 — указатель на функцию (addr).

5. Возвратить результат

Тут 2 варианта: целый результат или дробный. Согласно соглашению, по умолчанию результат будет в %eax, но если с плавающей точкой — то в всевдо-стеке x87.
1) Целый результат
movl %%eax, %0;
где %0 — переменная результата.

2) Вариант с плавающей точкой
По идее здесь нужно изъять из ST(0) ответ. Пока что у меня не получилось этого сделать. Хотелось бы увидеть в комментариях возможные решения. Заранее спасибо.

Ну вот и все! Задача была действительно не тривиальная. Надеюсь, этот пост кому-то понадобится.

PS Нужно все это для написания интерпретатора.
_________
Текст подготовлен в ХабраРедакторе

. when altering one's mind becomes as easy as programming a computer, what does it mean to be human.

вторник, 9 декабря 2008 г.

История соглашений вызова, часть 1

Хорошо, когда вокруг так много соглашений вызовов: есть из чего выбирать!

В 16-ти битном мире, часть соглашения вызова была зафиксирована набором инструкций: регистр BP был селектором для сегмента SS, а остальные регистры были привязаны к DS. Поэтому регистр BP был необходим для доступа к параметрам в стеке.

Регистры для возвращаемых значений также выбирались исходя из набора инструкций. Регистр AX работал как накопитель (аккумулятор) и поэтому был естественным выбором для возвращения через него результата функции. В наборе инструкций 8086 были также специальные команды, которые оперировали с парой регистров DX:AX, интерпретируя их как одно 32-х битное число, поэтому эта пара также была естественным выбором для возврата 32-х битных значений.

Оставались свободными регистры SI, DI, BX и CX.

(Терминологическое примечание: регистры, которые не нужно сохранять при вызове функции часто называются свободно модифицируемыми ("scratch"-регистрами)).

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

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

Одним из факторов также стала и неравномерность набора инструкций x86. Регистр CX не мог использоваться для доступа к памяти, поэтому вам, вероятно, хотелось бы, чтобы какой-нибудь другой регистр был бы свободным, таким образом листовые функции могли бы обращаться к памяти, без необходимости сохранения каких-либо регистров. Поэтому свободным был выбран регистр BX, а SI и DI стали сохраняемыми.

Итак, вот краткая выдержка 16-ти битных соглашений вызова:

Все модели
Все модели вызова в 16-ти битном мире сохраняют регистры BP, SI, DI (остальные - свободны) и размещают результат функции в DX:AX или AX - в зависимости от размера.

Суммируя: вызывающий чистит стек, параметры передаются справа-налево.

Имена функций декорировались знаком подчёркивания в начале. Я подозреваю, что ведущее подчёркивание не позволяло имени функции совпасть с зарезервированным словом ассемблера (представьте, к примеру, если бы у вас была функция с именем "call").

Pascal (pascal)
Соглашение вызова Pascal не поддерживает функции с переменным числом параметров, так что оно может использовать очистку стека вызываемым. Параметры передаются слева-направо, потому что, ну, это же так естественно выглядит. Декорирование имён функций заключалось в приведении их имён в верхний регистр. Это было необходимо, потому что Паскаль не был языком, чувствительным к регистру.

Почти все функции Win16 экспортировались с моделью вызова Pascal. Очистка стека вызываемым позволяла экономить три байта на каждый вызов, с разовым перерасходом двух байт на саму функцию. Поэтому, если функцию вызывали десять раз, вы экономили 3*10 = 30 байт на вызовах и платили за это только 2 байта в самой функции, получая суммарный выигрыш в 28 байт. И это также работало немного быстрее. В мире Win16 экономия нескольких сотен байт и нескольких циклов процессора значила много.

Fortran (fortran)
Модель вызова Fortran - такая же как и соглашение Pascal. У этого соглашения было отдельное имя, вероятно, потому что в языке Fortran было странное поведение для передачи данных по ссылке.

Fastcall (fastcall)
Соглашение вызова Fastcall передаёт первый параметр в регистре DX, а второй - в регистре CX (мне так кажется). Будет ли это работать действительно быстрее - зависит от того, как вы используете вызовы. В общем случае это работает быстрее, т.к. параметры передаётся в регистрах и поэтому их не нужно сохранять/загружать в медленный стек. С другой стороны, если между использованием первого и второго параметра будут идти какие-то интенсивные вычисления, то вызываемому и так придётся сохранять параметры в стек. Но что ещё хуже, вызываемая функция часто копировала регистр в память, потому что ей нужен был свободный регистр для другой работы, что в случае "интенсивные вычисления между использованием параметров" превращалось в двойное копирование. Ой!

Соответственно, fastcall обычно работал более быстро для коротких листовых функций и то не всегда.

Окей, это все 16-ти битные соглашения по вызову, что я смог вспомнить. Часть 2 расскажет о 32-х битных соглашениях.

Ой, не надо ерничать.
fastcall Borland придумал и ответ топикстартер в первом посте дал.
Кто же виноват, что он сам его не понял .

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

Хотите все функции __fastcall - установите это в проекте.

Некоторые не могут быть __fastcall, например функции с переменным числом аргументов, к которым надо применять va_arg, должны быть __cdecl.

Соглашение о формате передаче параметров для main и WinMain стоит особняком, они всегда фиксированы, чтобы их корректно могла вызвать crt.

__fastcall передаёт некоторые (по-моему, так только один) параметры через
регистры, а не через стек. На практике параметров у функций больше чем 1,
и не все функции влезут со своими параметрами в регистры и смогут
выиграть от __fastcall. Потому что если хоть один параметр остаётся,
то использование стека остаётся.

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

На этом давайте эту нещастную дискуссию закроем.

Posted via ActualForum NNTP Server 1.4

Хотел по привычке ответить про словесный понос и детскую наивность, но подумал, что наверное за все эти годы в IT я просто стал законченным циником и у меня самого этой непосредственности совсем не осталось . Вопрос про соглашения о вызовах очень больной, корни его глубоки, задевает он практически всех и потому неизбежно вызывает раздражение. Экономия на спичках и корпоративные мордобойчики привели к тому, что если раньше все меж собой прекрасно стыковалось, то теперь все со всем не совместимо. Это бесит, а fastcall вообще выкидыш Borland-а.
Создать с чистого листа - это конечно зачетно, но . вавилонская башня уже слишком высока

Соглашение вызова или модель вызова (англ. Calling convention ) — часть двоичного интерфейса приложений, которая регламентирует технические особенности вызова подпрограммы, передачи параметров, возврата из подпрограммы и передачи результата вычислений в точку вызова.

Содержание

Состав соглашения вызова

Соглашение вызова определяет следующие особенности процесса использования подпрограмм:

  • Расположение входных параметров подпрограммы и возвращаемых ею значений. Наиболее распространённые варианты:
    • В регистрах.
    • В стеке.
    • В динамически распределяемой памяти
    • прямой порядок — параметры размещаются в том же порядке, в котором они перечислены в описании подпрограммы. Преимущество — единообразие кода и записи на языке высокого уровня;
    • обратный порядок — параметры передаются в порядке от конца к началу. Преимущество — при любом количестве параметров на вершине стека после адреса возврата оказывается сначала первый параметр, за ним второй и так далее. Это упрощает реализацию подпрограмм с неопределённым числом параметров произвольных типов.
    • вызываемая подпрограмма — это сокращает объём команд, необходимых для вызова подпрограммы, поскольку команды восстановления указателя стека записываются только один раз, в конце подпрограммы;
    • вызывающая программа — в этом случае вызов становится сложнее, но облегчается использование подпрограмм с переменным количеством и типом параметров.

    Соглашения вызова зависят от архитектуры целевой машины и компилятора.

    Когда это важно

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

    Наиболее распространённые соглашения вызова на 32-битном x86

    Список неполный, написаны основные из применяемых по сей день.

    Во всех нижеперечисленных соглашениях подпрограмма обязана обеспечить восстановление перед возвратом значений сегментных регистров процессора, а также регистров ESP и EBP . Значения остальных могут не восстанавливаться. Возвращаемое значение функции хранится в регистре eax . Если его размер слишком велик для размещения в регистре, то оно размещается на верхушке стека, а значение в регистре eax будет указывать на него.

    cdecl

    Основной способ вызова для Си (отсюда название, сокращение от «c-declaration»). Аргументы передаются через стек, справа налево. Очистку стека производит вызывающая подпрограмма. Это основной способ вызова функций с переменным числом аргументов (напр. printf(. ))

    pascal

    Основной способ вызова для Паскаля, также применялся в Windows 3.x. Аргументы передаются через стек, слева направо. Указатель стека на исходную позицию возвращает подпрограмма.

    stdcall/winapi

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

    fastcall

    Передача параметров через регистры, обычно самая быстрая; если все параметры и промежуточные результаты умещаются в регистрах, манипуляции со стеком вообще не нужны. Fastcall не стандартизирован, поэтому используется только в функциях, которые программа не экспортирует наружу. Например, у Borland параметры передаются слева направо в eax , edx , ecx и, если параметров больше трёх, в стеке. Указатель стека на исходное значение возвращает подпрограмма.

    Fastcall Borland применяется по умолчанию в Delphi.

    safecall

    Обеспечивает более удобный для использования в распространённых языках высокого уровня способ вызова методов интерфейсов при использовании модели COM.

    Все методы интерфейсов COM представляют собой функции, возвращающие код завершения типа HRESULT, который должен анализироваться в месте вызова. Как правило, это не вполне удобно: большинство используемых с этой технологией языков имеют механизмы обработки исключений, и в них удобнее использовать обычные вызовы функций и процедур, возвращающие прикладные значения, а для обработки ошибок применять исключения. Именно такую возможность предоставляет safecall.

    Можно считать, что вызов

    в действительности представляет собой вызов

    причём организованный таким образом, что если возвращённое значение типа HRESULT будет содержать флаг ошибки, то в результате вызова будет сгенерировано исключение с кодом и текстом, соответствующим обнаруженной ошибке.

    thiscall

    Используется в компиляторах C++. Обеспечивает передачу аргументов при вызовах методов класса в объектно ориентированной среде. Аргументы передаются через стек, справа налево. Очистку стека производит вызываемая функция, т.е. тот же самый stdcall. Указатель (this) на объект, для которого вызывается метод, записывается в регистр ECX.

    Читайте также: