C вручную вызвать функцию со стеком и регистром

Я знаю, что это большая задача для манипулирования стеком, но я думаю, что это будет хорошим уроком для меня. Я искал в Интернете, и я нашел соглашение о вызовах. Я знаю, как это работает и почему. Я хочу имитировать некоторые из "стека очистки Callee", может быть, stdcall, fastcall это не имеет значения, важно думать, что кто будет чистить стек, тогда у меня будет меньше работы:)

например. у меня есть функция в C

double __fastcall Add(int a, int b) {
    return a + b;
}

это будет Кейли

и у меня есть указатель на эту функцию с типом void*,

void* p = reinterpreted_cast<void*>(Add);

И у меня есть функция Caller

void Call(void* p, int a, int b) {

    //some code about push and pop arg
    //some code about save p into register
    //some code about move 'p' into CPU to call this function manually
    //some code about take of from stack/maybe register the output from function
}

Вот и все, это полезно, когда я использую соглашение о вызовах "Очистка Калле", потому что мне не нужно

//some code about cleans-up this mess

Я не знаю, как это сделать, я знаю, что это можно сделать с помощью ассемблера. но я боюсь об этом, и я никогда не "прикасаюсь" к этому языку. Я был бы рад смоделировать этот вызов с C, но когда кто-нибудь может сделать это с ASM, я буду счастлив:)

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

Я использую ОС Windows x64 и MinGw

1 ответ

Решение

Прежде всего: C предназначен для сокрытия соглашений о вызовах и всего, что связано с тем, как ваш код выполняется от программиста, и обеспечивает абстрактный слой над ним.

Единственное условие, когда вам нужно (как вы говорите) "вручную" вызывать функцию, это когда вы делаете это из asm,

C как язык не имеет прямого контроля над стеком или счетчиком программ.

Цитировать из руководства GCC для fastcall для x86:

 On the Intel 386, the `fastcall' attribute causes the compiler to
 pass the first two arguments in the registers ECX and EDX.
 Subsequent arguments are passed on the stack. The called function
 will pop the arguments off the stack. If the number of arguments
 is variable all arguments are pushed on the stack.

Также, насколько я помню, возвращаемые значения передаются в EAX,

Таким образом, чтобы вызвать функцию таким образом, вам нужно предоставить аргументы в ECX, EDX а затем вызвать call инструкция по адресу функции

int __fastcall Add(int a, int b) {
    return a + b;
}

Обратите внимание, что я изменил тип возврата на int, потому что я не помню, как двойники передаются обратно.

int a, b;
// set a,b to something

void* p = reinterpreted_cast<void*>(Add);
int return_val;
asm (
    "call %3"
    : "=a" (return_val) // return value is passed in eax
    : "c" (a) // pass c in ecx
    , "d" (b) // pass b in edx
    , "r" (p) // pass p in a random free register
);

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

Код выше на самом деле является взломом таким образом, что я использую расширенный GCC asm синтаксис для автоматического помещения наших переменных в соответствующие регистры. Это будет генерировать достаточный код вокруг этого asm позвоните, чтобы убедиться, что данные согласованы.

Если вы хотите использовать стековое соглашение о вызовах, тогда cdecl является стандартным

int __cdecl Add(int a, int b) {
    return a + b;
}

Затем нам нужно поместить аргументы в стек до вызова

asm (
    "push %1\n" // push a to the stack
    "push %2\n" // push b to the stack
    "call %3"   // the callee will pop them from the stack and clean up
    : "=a" (return_val) // return value is passed in eax
    : "r" (a) // pass c in any register
    , "r" (b) // pass b in any register
    , "r" (p) // pass p in any register
);

Одна вещь, которую я не упомянул, это то, что это asm Вызов call не сохраняет ни одного из наших используемых регистров, поэтому я не рекомендую помещать это в функцию, которая делает что-либо еще. В 32 битной х86 есть инструкция pushad это подтолкнет все регистры общего назначения в стек и эквивалент (popad) восстановить их. Эквивалент для x86_64 недоступен. Обычно, когда вы компилируете код C, компилятор узнает, какие регистры используются, и сохранит их, чтобы вызываемый объект не перезаписывал их. Здесь это не так. Если ваш вызывающий абонент использует регистры, которые используются вызывающим абонентом - они будут перезаписаны!

Другие вопросы по тегам