Соглашение вызова 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 слова.
- int WordCount= 0 ;
- for ( int i= 0 ,i<N,i++)
- switch (Params[i])
- case cptPointer:
- case cptInt:
- WordCount++;
- break ;
- case cptDouble:
- WordCount+= 2 ;
- break ;
- >
- >
Посчитали. Выделяем память:
void* Buffer = new char[4*WordCount];
Заполняем буфер: void*, int — помещаем без изменений, а в double меняем слова местами.
- int offset= 0 ;
- double x;
- for ( int i= 0 ,i<N,i++)
- switch (Params[i])
- case cptPointer:
- case cptInt:
- *( int *)(buf+offset)=*(( int *)(ParamList[i]));
- offset+= 4 ;
- break ;
- case cptDouble:
- x=*(( double *)(((DTMain*)(v->T))->pData));
- memcpy(buf+offset+ 4 ,&x, 4 );
- memcpy(buf+offset,( char *)&x+ 4 , 4 );
- offset+= 8 ;
- break ;
- >
- >
Думаю, тут комментировать нечего. offset — смещение по буферу.
2. Узнаем адрес функции
Тут все достаточно просто.
void* addr = dlsym(NULL,sName);
Где первый параметр — дескриптор библиотеки. NULL для поиска в текущем контексте.
Подключаем dlfcn.h и не забываем в параметры линковки дописать -ldl.
3. Помещаем в стек буфер по словам
Фуух. Самое интересное.
Для работы со стеком нам, естественно, понадобится ассемблер. Я пользуюсь gnu компилятором, поэтому ассемблер с синтаксисом AT&T — ногами не пинать, мне самому не очень нравится, но выбирать не приходится.
- 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;"
- : "=r" (b)
- : "r" (addr), "r" ( Buffer ), "g" (WordCount)
- : "%eax"
- );
Мы делаем цикл: пока 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.
Читайте также: