【问题标题】:How to call a function through a function pointer passed as an argument?如何通过作为参数传递的函数指针调用函数?
【发布时间】:2016-12-06 11:07:49
【问题描述】:

如何在汇编的 f1 函数的第三个参数中调用传递函数 (*f2)? 声明如下:

extern float f1(int v1, float v2, float (*f2)(int v3, float v4));

我想将 v1 传给 v3,v2 传给 v4,调用函数 f2,并返回值

f1:
    push rbp           
    mov rbp, rsp

    mov rdx, rdi ; v1 to v3
    mov xmm1, xmm0 ; v2 to v4
    call ??? 
    mov xmm0, xmm1

    mov rsp, rbp       
    pop rbp    
ret

问号用什么代替?

【问题讨论】:

  • 你所在平台的应用程序二进制接口是什么?这将定义参数在哪里以及调用约定是什么。
  • @DavidHoelzer Abi64
  • 那是什么?没有这样的 ABI。

标签: assembly function-pointers x86-64 masm


【解决方案1】:

没有“Abi64”这样的东西。由于您标记了问题 MASM,我们可以猜测您正在使用 Windows 平台,并且显然“64”意味着这是 64 位代码,因此这确实极大地缩小了可能性。但是,对于 Windows 上的 64 位代码,仍有两种常见的调用约定。其中一个是__vectorcall,另一个是 Microsoft x64 调用约定(最初是为了使所有其他调用约定过时而发明的,但……没有)。

由于 Microsoft x64 调用约定是最常见的,并且在这种特殊情况下,使用 __vectorcall 不会改变任何内容,我假设它就是您正在使用的那个。然后所需的代码变得非常简单。您需要做的就是从f1 跳转到f2,因为堆栈的设置方式相同。 f1的前两个参数是应该传递给f2的两个参数,f2的返回值就是f1的返回值。因此:

f1:
    rex_jmp  r8    ; third parameter (pointer to f2) is passed in r8

这不仅写起来很简单,而且在大小和速度上都是最优化的实现。
您甚至可以根据需要预先修改v1v2 参数,例如:

f1:
    inc      ecx        ; increment v1 (passed in ecx)

    ; multiply v2 (xmm1) by v1 (ecx)
    movd     xmm0, ecx
    cvtdq2ps xmm0, xmm0
    mulss    xmm1, xmm0

    rex_jmp  r8    ; third parameter (pointer to f2) is passed in r8

如果你想做一些更复杂的事情,它的工作原理如下:

f1:
    sub   rsp, 40     ; allocate the required space on the stack
    call  r8          ; call f2 through the pointer, passed in r8
    add   rsp, 40     ; clean up the stack
    ret

请注意,您不需要问题中显示的序言/尾声代码,但如果您选择包含它不会有任何影响。

但是,您在问题中显示的示例代码中所做的参数改组是错误!在 Microsoft x64 调用约定中,前最多四个整数参数在寄存器中传递,从左到右,在 RCX、RDX、R8 和 R9 中。所有其他整数参数都在堆栈上传递。前最多四个浮点值也在寄存器中传递,从左到右,在 XMM0、XMM1、XMM2 和 XMM3 中。其余的都在堆栈上传递,以及对于寄存器来说太大的结构。

不过,奇怪的是插槽是“固定的”,所以总共只能使用 4 个寄存器参数,即使你有整数和 fp 参数的混合。因此:

╔═══════════╦══════════════════════════╗
║           ║           TYPE           ║
║ PARAMETER ╠═════════╦════════════════╣
║           ║ Integer ║ Floating-Point ║
╠═══════════╬═════════╬════════════════╣
║ First     ║   RCX   ║      XMM0      ║
╠═══════════╬═════════╬════════════════╣
║ Second    ║   RDX   ║      XMM1      ║
╠═══════════╬═════════╬════════════════╣
║ Third     ║   R8    ║      XMM2      ║
╠═══════════╬═════════╬════════════════╣
║ Fourth    ║   R9    ║      XMM3      ║
╠═══════════╬═════════╩════════════════╣
║ (rest)    ║         on stack         ║
╚═══════════╩══════════════════════════╝

第二个参数是第一个被传递的浮点值并不重要。它不进入 XMM0 因为它是第一个浮点值,它进入 XMM1 因为它是第二个参数,因此在第二个“插槽”中。 (这与 the x86-64 System V ABI 不同,其中前 6 个整数 args 进入寄存器,无论是否有 FP args)。

here 提供有关 Windows 参数传递的更详细文档,包括示例。

【讨论】:

  • 尺寸和速度的最佳实现:对于尺寸,您可以使用 CVTSI2SS 并忽略对 XMM0 旧值的错误依赖(感谢简短-PIII,英特尔的前瞻性设计)。我期待 gcc 所做的(pxor xmm0, xmm0 / cvtsi2ss xmm0, ecx)对于速度而言是最佳的,但它实际上看起来像 MOVD,然后打包转换可能具有相同的更低延迟和更少的总 uops。从 MOVD 到 CVTDQ2PS 可能存在 1c 的旁路延迟。 (Agner 在 Nehalem 上将其列为 3+2c,因此 CVTDQ2PS 需要浮点输入。)
  • 糟糕。我的意思是该注释适用于它上面的代码,不一定是我对浮点值的转换。我不是为了速度而写的!不过,感谢您的信息和研究!我展示的代码本质上是 MSVC 将生成的。 Agner 没有显示后几代处理器具有相同的惩罚,因此它可能更愿意针对那些最有可能成为 64 位构建目标的处理器进行优化。这将是我的选择,至少。虽然可以说 GCC 的代码更具可读性。
  • 您为什么说__fastcall 与“Microsoft x64 调用约定”?您似乎在说 vectorcall 和 fastcall (几乎?)是同一件事,并且与其他一些调用约定不同。但我认为 x64 __fastcall 只是原始 Microsoft x64 调用约定的一个很少使用的术语,它不会在 XMM / YMM regs 中传递向量。 (顺便说一句,这个Intel tutorial guide for asm on Windows 对调用约定有详细的描述,而且很全面。)
  • @peter 我不确定这里是否有必要,但是 x64 上的尾部调用函数通常需要在 JMP 指令上加上 REX 前缀,所以这是我一直遵循的规则。见这里:blogs.msdn.microsoft.com/freik/2009/02/09/…。哦,我看到罗斯里奇已经为这个问题提供了一个很好的答案:stackoverflow.com/questions/36788685/…
  • @Peter rex_jmp 中的 REX 前缀被编码为 0100 1000 (48h),其中设置了 W 字段的位(指示 64 位操作数大小)。相比之下,当您将 jmp 与 R8-R15 一起使用时,REX 前缀被编码为 0100 0001 (41h),其中设置了 B 字段的位(表示 MODRM.rm 字段的扩展)。所以我不认为将R8-R15用于内部跳转表有问题,因为堆栈展开代码看起来特别48 FF作为编码。不过,很难确定。这个我没考虑过,官方文档也比较模糊。
【解决方案2】:

汇编代码因使用的微控制器而异。

不完全是您要查找的内容,而是遵循在具有英特尔核心 I7 的 Windows 平台上生成的汇编代码:-

C代码:-

extern float f1(int v1, float v2, float (*f2)(int v3, float v4))
{
    float a = 10.0;
    int b = 12;

    f2(b, a);
    return a+ 12.5;
}

汇编代码:-

_f1:
 pushl  %ebp
 movl   %esp, %ebp
 subl   $40, %esp
 movl   LC0, %eax

 movl   %eax, -12(%ebp)
 movl   $12, -16(%ebp)
 movl   -12(%ebp), %eax
 movl   %eax, 4(%esp)
 movl   -16(%ebp), %eax
 movl   %eax, (%esp)
 movl   16(%ebp), %eax
 call   *%eax
 fstp   %st(0)
 flds   -12(%ebp)
 flds   LC1
 faddp  %st, %st(1)
 leave
ret

希望这会有所帮助。

【讨论】:

  • 显然,您在没有启用优化的情况下编译了这段代码,这是一个坏主意,因为它会给您带来很多不相关的噪音。您显然还在一个使用Microsoft的x64调用约定的编译器上编译了它,这会使提出问题的人感到困惑和无益。诚然,这个问题没有详细说明,但很明显他正在使用 MASM 并针对 x64。这不是 MASM 语法,并且 Windows 上的任何 x64 调用约定都不会传递堆栈上的前四个整数或浮点参数。
猜你喜欢
  • 2021-07-18
  • 1970-01-01
  • 2015-01-29
  • 2012-11-28
  • 2020-11-08
  • 2022-01-20
  • 1970-01-01
  • 2019-08-17
  • 2012-08-25
相关资源
最近更新 更多