x64 asm如何将函数指针设置为_cdecl C函数并调用它?

Posted

技术标签:

【中文标题】x64 asm如何将函数指针设置为_cdecl C函数并调用它?【英文标题】:x64 asm how to set a function pointer to a _cdecl C function and call it? 【发布时间】:2014-05-28 11:02:20 【问题描述】:

我正在尝试在 x64 asm 中做一些非常基本的事情:

    有一个 asm 函数,它接受一个函数指针并将其设置在一个变量中。这个函数是从 C 代码中调用的。

    有另一个 asm 函数调用函数指针,如果不为空,这个函数指针也是一个 C 函数(由 1 中的函数设置)。

到目前为止,我对 C 方面的看法如下:

extern "C" void _asm_set_func_ptr(void* ptr);

void _cdecl c_call_back()




void init()

    _asm_set_func_ptr(c_call_back);

还有 asm 方面:

.DATA

g_pFuncPtr QWORD 0

.CODE             ;Indicates the start of a code segment.

_asm_set_func_ptr PROC fPtr:QWORD
    mov     [rsp+qword ptr 8], rcx
    mov     rax, [rsp+qword ptr 8]
    mov     g_pFuncPtr, rax
    ret
_asm_set_func_ptr ENDP 

_asm_func PROC

push RBX
push RBP
push RDI
push RSI
push RSP
push R12
push R13
push R14
push R15

CMP g_pFuncPtr, 0
JE SkipCall
    MOV RAX, [ g_pFuncPtr ];
    CALL RAX;
SkipCall:

pop RBX
pop RBP
pop RDI
pop RSI
pop RSP
pop R12
pop R13
pop R14
pop R15
ret

_asm_func ENDP 

但似乎我在调用_asm_set_func_ptr() 后损坏了堆栈,我也不确定在_asm_func 中如何调用g_pFuncPtr 是否正确?我的代码有什么问题?我正在使用 VS2013 MASM64 构建它。

【问题讨论】:

【参考方案1】:

首先,您通常需要以与推送它们相反的顺序弹出寄存器,即:push RBX, push RBP ... push R15 --> pop R15 ... @ 987654327@,pop RBXret。这肯定会破坏_asm_func的调用者。


接下来您应该查看Windows x64 calling convention 进行正确的函数调用所需的一切。正确满足所有需求非常重要,否则可能会出现问题,甚至在其他代码中很晚,这不是最适合调试的事情。

例如,您不需要保存所有寄存器。如果回调函数销毁它们,它会自己保存和恢复它们。所以那里不需要推送和弹出,RAX 无论如何都可以无效,没有参数被传递。

但是请注意这部分:

在 Microsoft x64 调用约定中,调用者有责任在调用函数之前在堆栈上分配 32 字节的“影子空间”(无论使用的实际参数数量如何),并在调用后弹出堆栈.

所以你应该在你的代码之前写SUB ESP, 32,然后在RET之前写ADD ESP, 32

还有“栈在 16 字节上对齐”的要求,但你目前不需要解决这个问题,因为“8 字节的返回地址 + 32 字节的影子空间 + 8下一个返回地址的字节”按 16 个字节对齐。

此外,Windows x64 ABI 对异常处理和正确展开也有严格要求。正如 Raymond 在评论中指出的那样,由于您的函数不是叶函数(调用其他函数),因此您需要提供适当的序言和结尾——请参阅 here。


不需要在_asm_set_func_ptr开头临时保存RCX

不过,否则我看不出有任何问题。


最后,在汇编文件的行尾不需要分号;

【讨论】:

由于您的函数不是叶函数,因此您还需要声明序言和尾声代码,以便在发生异常时发生正确的事情。【参考方案2】:

您在检查 g_pFuncPtr 之前推送了很多寄存器,但如果没有设置,您不会将它们从堆栈中弹出。如果您将某些东西压入堆栈,然后不进行调用也不将它们弹回,您的堆栈将很快填满。

您必须以与推送相反的顺序弹出寄存器,否则您将取回错误的寄存器。

最后,不要浪费时间和 CPU 周期来推动它们,除非你与它们有关:

    CMP g_pFuncPtr, 0
    JE SkipCall

    PUSH RBX
    PUSH RBP
    PUSH RDI
    PUSH RSI
    PUSH RSP
    PUSH R12
    PUSH R13
    PUSH R14
    PUSH R15
    MOV RAX, [ g_pFuncPtr ];
    CALL RAX;
    POP R15
    POP R14
    POP R13
    POP R12
    POP RSP
    POP RSI
    POP RDI
    POP RBP
    POP RBX
SkipCall:
    ret

...请-请...阅读有关设置堆栈帧和管理调用内的堆栈帧的内容。 C 调用和 ASM 调用处理堆栈帧的方式非常不同。

【讨论】:

Mmmmmmm... 在阁楼上,从我第一次学习使用 DOS 的 C 和 Pascal 调用约定编写汇编语言 (MASM) TSR 时开始... 回到 1990 年代... 这个看起来(乍一看)是一个很好的起点:en.wikibooks.org/wiki/X86_Disassembly/… 这也是:csee.umbc.edu/~chang/cs313.s02/stack.shtml 但这似乎只与32位有关? 64bit 的 ABI 似乎完全不同 不同的寄存器名称,更大的寄存器,但堆栈帧处理技术完全相同。每个 CPU 上的堆栈向下增长,而堆向上增长,C/C++ 调用期望参数在堆栈上的顺序相同(但它们的位置会有所不同,因为寄存器更大),返回处理技术是相同的,所有 PUSHED(而不是被调用函数弹出)仍然需要以相反的顺序获得 POPPED,C 和 PASCAL 调用约定仍然彼此完全不同; ASM 通常使用 PASCAL 样式,因为它更快、更便宜。 但是在 x64 中只有一个调用约定?

以上是关于x64 asm如何将函数指针设置为_cdecl C函数并调用它?的主要内容,如果未能解决你的问题,请参考以下文章

x86 x64下调用约定浅析

如何获取基本堆栈指针的地址

如何获取基堆栈指针的地址

函数调用方法之__cdecl与_stdcall

CDECL 调用函数内。获得参数个数。

谜团:将 GNU C 标签指针转换为函数指针,并使用内联 asm 在该块中放置一个 ret。块被优化掉?