Pull to refresh

Вызов функции с «неизвестным» именем на C++. Часть 1 — cdecl

Reading time 4 min
Views 14K

Постановка задачи


Что же я имел ввиду, когда написал «неизвестное» имя функции? А значит это то, что имя функции, её параметры и, в конце концов, соглашение вызова, становятся известными только во время выполнения программы. Займемся её вызовом! =)

Сейчас мы попробуем вызвать функцию по стандарту cdecl.
Выдержка из Википедии:
The cdecl calling convention is used by many C systems for the x86 architecture. In cdecl, function parameters are pushed on the stack in a right-to-left order. Function return values are returned in the EAX register (except for floating point values, which are returned in the x87 register ST0). Registers EAX, ECX, and EDX are available for use in the function.

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

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

Поехали!



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

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

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


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


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

Заполняем буфер: void*, int — помещаем без изменений, а в double меняем слова местами.
Copy Source | Copy HTML
  1. int offset= 0;
  2. double x;
  3. for(int i= 0,i<N,i++)
  4. {
  5.   switch(Params[i])
  6.   {
  7.     case cptPointer:
  8.     case cptInt:
  9.         *(int*)(buf+offset)=*((int*)(ParamList[i]));
  10.         offset+=4;
  11.         break;
  12.     case cptDouble:
  13.         x=*((double*)(((DTMain*)(v->T))->pData));
  14.         memcpy(buf+offset+4,&x,4);
  15.         memcpy(buf+offset,(char*)&x+4,4);
  16.         offset+=8;
  17.         break;
  18.   }
  19. }


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

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


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

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


Фуух. Самое интересное.
Для работы со стеком нам, естественно, понадобится ассемблер. Я пользуюсь gnu компилятором, поэтому ассемблер с синтаксисом AT&T — ногами не пинать, мне самому не очень нравится, но выбирать не приходится.
Copy Source | Copy HTML
  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 Нужно все это для написания интерпретатора.
_________
Текст подготовлен в ХабраРедакторе

UPD: Подсветил исходники
Tags:
Hubs:
+45
Comments 92
Comments Comments 92

Articles