【问题标题】:C manually call function with stack and registerC手动调用带有堆栈和寄存器的函数
【发布时间】:2013-08-09 12:22:46
【问题描述】:

我知道这是操纵堆栈的大事,但我认为这对我来说将是一个很好的教训。 我搜索了互联网,我发现了调用约定。我知道它是如何工作的以及为什么。我想模拟一些“Callee clean-up stack”也许stdcall,fastcall没关系,重要的是谁清理堆栈,那么我将有更少的工作要做:)

例如。 我在 C 中有函数

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

是凯莉

我有指向这个函数的指针,类型为 void*,

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

我有函数调用者

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
}

就是这样,当我使用调用约定“Calle clean-up”时它很有帮助,因为我不需要

//some code about cleans-up this mess

我不知道该怎么做,我知道它可以用汇编程序来完成。但我害怕它,我从不“接触”这种语言。我会很高兴用 C 模拟调用,但是当任何人都可以用 ASM 来做时,我会很高兴 :)

我还告诉了我想用它做什么, 当我知道如何手动调用函数时,我将能够使用多个参数(如果我知道它的数量和大小)和任何类型的函数来调用函数。 因此,当该函数符合正确的调用约定时,我将能够以任何语言调用任何函数。

我使用的是 Windows OS x64 和 MinGw

【问题讨论】:

  • 在你的Call函数的情况下,你有什么理由不能定义一个像typedef double (__fastcall *binaryop)(int, int);这样的新类型并将其用于p
  • Win64 甚至 fastcall 吗?我的印象是只有一个 Windows 64 位调用约定,这是“一种”fastcall,因为它在寄存器中传递 args 1..4,但它绝对不同于传统的 32 位 Windows fastcall。你是说你试图从 64 位代码中调用 32 位代码,即实现你自己的“thunking”?

标签: c assembly fastcall


【解决方案1】:

首先:C 旨在隐藏调用约定和所有特定于程序员如何执行代码的内容,并在其之上提供一个抽象层。

您需要(如您所说)“手动”调用函数的唯一条件是从asm 执行此操作。

C 作为一门语言不能直接控制堆栈或程序计数器。

引用 x86 的 fastcall 的 GCC 手册:

 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 中传递的。

因此,为了以这种方式调用函数,您需要在ECXEDX 中提供参数,然后在函数地址上调用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,使其自动清理堆栈。

上面的代码实际上是一个 hack,我使用 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 调用不会保存我们正在使用的任何寄存器,所以我不建议将它放在执行其他任何操作的函数中。在 32 位 x86 中,有一条指令 pushad 会将所有通用寄存器推入堆栈,并有一条等效指令 (popad) 来恢复它们。但是,x86_64 的等效项不可用。通常,当您编译 C 代码时,编译器会知道哪些寄存器正在使用,并将保存它们以便被调用者不会覆盖它们。在这里它没有。如果您的被调用者使用调用者正在使用的寄存器 - 它们将被覆盖!

【讨论】:

  • 你说的是Intel386,所以这段代码是“不可移动的代码”?而这种病态的“爆炸”在 AMD 上?有没有办法制作多平台代码?我的意思是,我只想使用 Windows,但 AMD、Intel 和架构 x86 和 x64
  • @user2418897 Intel i386 是通常所说的 x86 的技术术语。相同的调用约定被移植到 x86_64。就call 指令而言,从80 年代的80386 开始,Intel、AMD、VIA(天知道还有谁)的所有基于x86/x86_64 的处理器的语法都是相同的。它不适用于 IBM Power PC 或 ARM(手机处理器),但您也不会在它们上找到 Windows 实现;)
  • @user2418897 调用约定是特定于处理器体系结构的。如果您手动使用它们而不是让您的编译器为您执行,那么您需要为不同的处理器架构实现不同的。幸运的是,x86 和 x86_64 共享相同的调用约定和相同的指令,并且所有制造 x86/x86_64 兼容处理器的硬件供应商都实现了它。
  • 感谢它非常有帮助:) 我还有另一个问题(也许不是最后一个),我在线程“__fastcall 约定”中调用,据我所知,这个约定与 arg 混合,2 第一个 int 进入寄存器和休息到堆栈中。我在维基百科上找到了一些调用约定的表格,并且有一些约定,所有(有趣的 arg)都进入堆栈,你能告诉我如何用另一个约定调用函数 ADD(其中约定使用堆栈)
  • @user2418897 添加和cdecl 示例
猜你喜欢
  • 2015-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-11
  • 2019-11-22
  • 2016-06-26
  • 1970-01-01
相关资源
最近更新 更多