C++

индекс
236,98

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

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


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

Сейчас мы попробуем вызвать функцию по стандарту 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("\
        movl $0, %%eax;\
        movl %2,%%ebx; \
        movl %3,%%ecx; \
        l1: cmpl %%ecx, %%eax; \
        je l2;\
        pushl (%%ebx,%%eax,4); \
        addl $1,%%eax;\
        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: Подсветил исходники
+45
21 декабря 2009, 18:57
52

комментарии (92)

+8
ALLIGATOR #
Всё это ооочень странно, скажите пожалуйста, где это Вам понадобилось?
–3
ALLIGATOR #
Постановка задачи

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

Что же у нас есть:
1) char* sName — тут находится имя функции
2) int N — количество параметров
Хм…
+2
Antigluk #
а что странного? «становятся известными только во время выполнения программы» — это и значит что в переменной sName значение появляется только в runtime — например, при чтении с клавиатуры.
+2
Antigluk #
Сейчас я занимаюсь разработкой интерпретатора Си. Вызов функций, которые объявлены программистом, проблем не вызывает. Но нужно вызывать и другие функции из разных библиотек. Для этого и делал.
0
ALLIGATOR #
Аааа, ну извиняйте :-)
Мне теперь тоже стало интересно как это можно сделать, но на более высоком уровне языка.
0
Antigluk #
Если найдете решение на высшем уровне, скажите, я занимаюсь этим исследованием уже около месяца. Будет интересно посмотреть.
0
std #
Можно предположить, что все параметры преобразуются в машинному слову [или в два машинных слова, если не влазит] и обычно функции не имеют больше N аргументов [имхо, меньше восьми].

Исходя из этого наклепать N функциональных типов и вызывать функций через приведение типа.
Что-то в духе:
   size_t ExecuteFunc__сdecl(void* func_ptr, int argc, size_t argv[] )
   {
      typedef size_t (__cdecl *func0)(void);
      typedef size_t (__cdecl *func1)(size_t);
      ....
      typedef size_t (__cdecl *funcN)(size_t, .... , size_t);
   
      switch( argc ) {
         case 0:  return ((func0)func_ptr)();
         case 1:  return ((func1)func_ptr)(argv[0]);
         ....
         case N:  return ((func0)func_ptr)(argv[0], argv[1], .... , argv[N]);
      }
      /* как-то разбираемся с нештатной ситуацией */
      return 0;
   }

Возвращаемое значение «использовать по вкусу» — исходя из того должна ли функция вернуть что-либо или нет.
Получается китайский код, но избавляемся от ассемблера ^_^ Не уверен, что такой вариант лучше, но все же ;)
0
ukc #
Соглашения о вызове — штука низкоуровневая. Выше никак.
+3
ALLIGATOR #
минусанул топик не разобравшись, возвращаю в карму — всё по чесноку.
НЛО прилетело и опубликовало эту надпись здесь
0
Antigluk #
А если я скажу, что я не собираюсь контролировать? ;)
Ну во-первых, учитывая интерпретаторность ( :) ) при объявлении импортированой функции мы явно указываем список параметров. Да, я не могу узнать сколько параметров ДОЛЖНА принимать эта функция. Я передаю ровно столько, сколько задано в скрипте.
Да, это огромная дыра в безопасности, но пока что я о ней не сильно заботился.
Пока что я не вижу решений этой проблемы. Могу ответить одно — пускай проактивная защита этим занимается ;)
+1
destman #
Возможное решение проблемы — изменить стиль вызова функции. Я точно не помню как он зовется (вроде pascal), но его суть такая:
1) Мы в стек заносим параметры
2) Функция делает сове черное дело
3) Перед выходом — сама очищает стек

Т.е. логично сделать проеверку по вершине стека, если после выхода из функции она сместилась — вот тут и кидаем кесепшен или еще чего :)
0
Damaskus #
До чистки стека можно не добраться вообще.
Что если количество заявленных параметров не совпадает с фактически помещенными в стек, если фактических меньше?
Функция при выполнении начнет считывать параметры [esp+4],[esp+8] и т.д.
Начнем читать мусор в параметры, а если стек не наполнен, а параметров много, то можем и за границу сегмента вылезти, словим Access Violation или другого зверя.
0
ionicman #
Базовый контроль можно проевести через исключения например :)
НЛО прилетело и опубликовало эту надпись здесь
0
ionicman #
дак это всегда можно сделать если речь идет об асме :-D
НЛО прилетело и опубликовало эту надпись здесь
0
arilou_camper #
Вы бы лучше прикрутили к своей программе Lua или какой-то другой интерпретируемый язык, и всё бы было хорошо.

А сейчас вы решаете метапроблему («создание С интерпретатора, чтобы можно было вызывать любую функцию с неизвестными параметрами») вместо решения проблемы «обеспечить вынос части логики программы в скрипт и обеспечить двустороннее взаимодействие между С кодом и скриптом».

Такие дела.
0
Antigluk #
«создание С интерпретатора, чтобы можно было вызывать любую функцию с неизвестными параметрами»… вообще-то я решаю другую проблему:
«Добавление в интерпретатор С возможность вызова любой функции с неизвестными параметрами».
0
arilou_camper #
тогда ок =)
0
arilou_camper #
тогда ок =)
0
emendz #
Особенно если вспомнить, что есть такой зверь как IDispatch
0
akalend #
подцеплять плагины,
имя которых нам неизвестно,
его можно взять из имени файла, который лежит в определенной директории
или типа того!!!

спасибо за статью, оч полезно!
0
gribozavr #
www.sco.com/developers/devspecs/abi386-4.pdf — вот тут нужно читать про соглашение вызова, а не в Википедии.
+1
Antigluk #
Согласен, просто в википедии наиболее кратко для цитаты.
0
ALLIGATOR #
По второй проблеме — почему бы не написать программу с использованием FP и потом посмотреть в дизассемблере как забрать результат? Или я снова ничего не понял?
0
ALLIGATOR #
Может так:
MOVQ [EAX], MM0
EMMS

?
0
Antigluk #
по поводу Q сомневаюсь… double — вроде ж как 2 слова.
да и смотрел я листинги…
там сразу после вызова функции:
call _Z4funcd
fstpl 16(%esp)

и каждый раз 16(%esp) разное.
и непонятно как в конкретную переменную положить.
я ассемблер знаю плохо, так что не взыщите)
0
ALLIGATOR #
я тоже не ас.
MM0 — 64 разрядный регистр, так что Q в самый раз.
0
ALLIGATOR #
Ааа, всё, понял что Вы имели ввиду :-)
Да, тут нужно подумать.
0
ALLIGATOR #
Думаю Вам нужно ещё узнать тип результата. Ведь результат в интерпретаторе должен куда-то присваиваться…
0
Antigluk #
тип данных результата я знаю:
CParamType RetType
+2
farcaller #
sourceware.org/libffi/ — это не оно?
0
Antigluk #
Не встречал, посмотрю.
0
gribozavr #
По идее здесь нужно изъять из ST(0) ответ. Пока что у меня не получилось этого сделать.


fstpl
0
Antigluk #
подскажите пожалуйста, как его использовать?
тут я описал проблему...
0
youROCK #
А что, неужели нельзя просто использовать function pointer и делать приведение типов? Зачем с ассемблером связываться? Неужели аргументы вызываемых функций тоже будут неизвестны?
0
Antigluk #
почитайте комментарии, аргументы неизвестны тоже.
0
youROCK #
Извините, не разглядел, что Вы делаете свой интерпретатор Си… Тогда здесь действительно сложно что-либо возразить :). Ваша статья будет безусловно очень полезна людям, которые будут заниматься тем же самым… А также будет наверняка использоваться не по назначению, поскольку Вы описали всё достаточно подробно :)).
0
Antigluk #
«Если бы все было так просто...»
–26
jjlol #
Чё за параша, где STL ???
0
Antigluk #
при чем тут STL?
0
jjlol #
А при чём тут C++?
0
alexandrov #
STL тут явно не при чём.
+1
ulysses #
А почему нет кода для раскрутки стека? В конвенциях cdecl раскручивать стек должен вызывающий код.
–3
IoGA #
Переизобретение лямбда-функций, а потом и остальных столпов функционального программирования на С++?
0
Antigluk #
Ну подскажите более красивое решение. Я буду вам только благодарен
–4
IoGA #
Писать на функциональных языках. :-)
А по проблеме, если я не путаю, нечто подобное есть (ну или будет) в стандарте C++0x и уже более-менее реализовано в GCC 4.4.x
0
ganqqwerty #
вы говорите о лямбда-выражениях в 0x?
0
IoGA #
Да.
+2
OS2 #
Спасибо за вашу статью, получил удовольствие от чтения!
+2
ivlis #
Что-то мне подсказывает, что портированием будет ужас :)
+1
asfd #
0
Antigluk #
извиняюсь за задержку, подсветил.
0
legrus #
По делу сказать нечего, посоветую только посмотреть на существующий cint

0
highw #
Иногда читая такие топики мне становится ужасно представить себе, что я провожу часы в долгой отладке в поисках потерянного байта! )
–1
gorinich #
Я знал! Я знал, что Си это высшее шоманство…
0
Gorthauer87 #
>The cdecl calling convention is used by many C systems for the x86 architecture

Значит уже даже на amd64 способ не годится, а что насчет arm, mips?
Хм… вижу юзание ассемблера. Значит код не портируемый. На вид это эдакий маленький красивенький велосипедик, но с одним колесом, поэтому удерживать равновесие тяжело и далеко на нём не уедешь.
Одним словом таких вещей, как вызов неизвестной функции с неизвестными параметрами в рамках Си/Си++ хотеться не должно. А если очень хочется, то выбирайте более подходящие языки.
0
Antigluk #
хм, ну почему-же. не писать же весь проект на другом языке, лишь из-за этого.
0
Gorthauer87 #
Ну мне лично тяжело представить ситуации, когда без такого не обойтись. А наличие таких вот шаманств в продакшн коде может когда-нибудь и подвести, ибо это грабли. Хотя для общего развития и понимания работы процессора такие штуки оч полезны. И всё же прошу, при, написании подобных статей, указывать, что это решение представляет из себя грабли, которые могут в один прекрасный день по лбу дать.
0
Antigluk #
может и грабли, но иногда без этого не обойтись.
0
Gorthauer87 #
>А значит это то, что имя функции, её параметры и, в конце концов, соглашение вызова, становятся известными только во время выполнения программы

Ну хорошо, задачку я понял. Это вполне хорошая тренировка для ума. Но где в реальном то коде такое может понадобится?
0
Antigluk #
прочитайте-же комментарий, на который я дал вам ссылку. в нем я написал, что пишу интерпретатор, в котором нужно вызывать функции из разных библиотек
0
Gorthauer87 #
а чем вам TCC тогда не угодил?
ru.wikipedia.org/wiki/Tiny_C_Compiler
0
Antigluk #
тем что он компилятор :) исполняет он только скомпилированный им-же код.
0
Gorthauer87 #
>От других распространённых компиляторов TCC отличается прежде всего тем, что может исполнять скомпилированную им программу, то есть выполнять функцию интерпретатора. Данное свойство позволяет использовать язык Си в качестве скриптового языка

Разве это не то?
0
Antigluk #
нет. он перед выполнением кода компилирует его.
0
Antigluk #
пробовал и под windows, оно работает:)
так что код портируемый. другое дело под 64bit…
0
corristo #
портируемость между разными аппаратными платформами, а не ОСями :)
Вообще, учитывая что Вы это делаете в рамках разработки интерпретатора непортируемость решения не есть недостаток. Сдается мне что процент портируемого кода в том же gcc весьма мал, все равно для каждой аппаратной платформы надо писать свой бэкенд.

Поправьте если где ошибся.
0
ivanrt #
На x86-64 код для win и linux отличается
0
corristo #
о, а можно ссылочку?
0
ivanrt #
svn.apache.org/viewvc/harmony/enhanced/drlvm/trunk/vm/interpreter/src/
Смотреть на файлы invoke*
Есть тонкости с выравниванием фрэйма. Может еще что, уже не помню, написал это 3+ года назад…
0
corristo #
спасибо.
–1
Gorthauer87 #
>пробовал и под windows, оно работает:)
Я бы крайне удивился, если бы не работало. Ибо вроде как у Виндовса еще не появилось своего набора команд. В Линуксе и Виндовсе разное ABI, но вот код и там и там в итоге обычный x86
>другое дело под 64bit…
И не только. Вполне вероятно, что потребуется код портировать на arm архитектуру, которая вполне может соперничать с ia-32 и amd64 по распространённости. Вот тут и получите граблями по лбу :)
0
corristo #
блин, человек какие-то элементы интерпретатора реализует, какой граблями по лбу? Что-то я не видел 100% переносимых компиляторов/интерпретаторов, наверное потому что пока их существование невозможно :)
–1
Gorthauer87 #
А зачем простому человеку, кроме как в целях обучения и just for fun, этим заниматься? :) Поэтому и предупреждаю, что такие штуки в реальном коде нежелательны.
0
Antigluk #
в моем случае это так, но вы считаете, что интерпретаторы пишут только just for fun?
+1
corristo #
А в реальном коде реального интерпретатора как Вы без этого обойдетесь? О_о
0
Gorthauer87 #
Хорошо, беру свои слова обратно, в интерпретаторах без такого тяжеловато.
0
ivanrt #
Писал такой код для i386, x86-64, ia64 (linux, win), если интересно могу кинуть ссылку. Ключевое слово apache harmony interpreter mode. Сейчас пишу с телефона, локаничность повышена.
0
torkve #
А запостите, мне тоже очень интересно :)
0
Antigluk #
интересно, очень!
0
ivanrt #
см чуть ниже, никак не могу попасть ;)
0
ivanrt #
0
AxisPod #
Пишете свой rundll.exe? :-)

Хотя на самом деле у меня компилятор моего скриптового языка определял по началу описания функций, позже стал использовать .h файлы для проверок.

А гадать на кофейной гуще не дело на самом деле, данный подход не является безопасным и дает дорогу куче различных эксплоитов.
0
Antigluk #
дописал в конец статьи, так как много вопросов очень по поводу зачем все это :)
ну в какой-то мере да. интерпретатор я пишу, а это его часть.
а небезопасно — да. нужно обдумать это…
0
mt_ #
Совсем недавно реализовал примерно похожий функционал. Нужно было вызывать между DLL функции с переменным числом аргументов. Причём подключение тоже было динамическое, run-time.

Причём в моём случае, также заранее не был известен набор аргументов вызываемых функций. То есть сама вызываемая функция не знала, с какими аргументами её вызовут. Поэтому, все аргументы я передавал так: число аргументов, после чего список пар тип аргумента / значение.

Также хочу напомнить, что псевдостек x87 используется GCC. В случае использования компилятора M$, а также использования DLL, созданных на нём, числа с плавающей точкой надо передавать вместе со всеми остальными аргументами.
–3
angel_hearted #
У меня одного cdecl ассоциируется с «Сишный Децл»? :)
+1
ivanzoid #
у одного :D я читаю как «сидекл»
0
rule #
спасибо, забавная статья, но достаточно нежизнеспособный подход, я бы лично избегал использование таких конструкций " во избежании ".
0
Antigluk #
к сожалению, избежать я этого не могу в своем проекте
0
loginsin #
Описанное соглашение — соглашение __stdcall. В __cdecl функция стек не очищает, его очищает _вызывающая_ функция. Отталкиваясь от той же статьи в англоязычной википедии, можно увидеть следующий код:
push c
push b
push a
call function_name
add esp, 12 ;Stack clearing
mov x, eax

В статье нигде об этом не сказано. Исходя из вышеизложенного, автор все-таки пользуется соглашением __stdcall :-)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.