如何在Windows上的64位应用程序运行时在程序集中设置函数参数?

我试图使用generics函数中使用的汇编代码设置参数。 这个generics函数的参数 – 驻留在dll中 – 在编译期间是未知的。 在运行期间,使用GetProcAddress函数确定指向此函数的指针。 然而,其论点尚不清楚。 在运行时,我可以使用数据文件(不是头文件或任何可以包含或编译的内容)来确定参数 – 值和类型。 我找到了一个很好的例子,说明如何解决32位的这个问题( C Pass参数作为从LoadLibrary()导入函数的void-pointer-list ),但对于64位这个例子不起作用,因为你无法填充堆栈但你必须填写登记册。 所以我尝试使用汇编代码填充寄存器,但直到现在都没有成功。 我使用C代码来调用汇编代码。 我使用VS2015和MASM(64位)。 下面的C代码工作正常,但汇编代码没有。 那么汇编代码出了什么问题呢? 提前致谢。

C代码:

... void fill_register_xmm0(double); // proto of assembly function ... // code determining the pointer to a func returned by the GetProcAddress() ... double dVal = 12.0; int v; fill_register_xmm0(dVal); v = func->func_i(); // integer function that will use the dVal ... 

不同的.asm文件中的汇编代码(MASM语法):

 TITLE fill_register_xmm0 .code option prologue:none ; turn off default prologue creation option epilogue:none ; turn off default epilogue creation fill_register_xmm0 PROC variable: REAL8 ; REAL8=equivalent to double or float64 movsd xmm0, variable ; fill value of variable into xmm0 ret fill_register_xmm0 ENDP option prologue:PrologueDef ; turn on default prologue creation option epilogue:EpilogueDef ; turn on default epilogue creation END 

所以你需要调用一个函数(在DLL中),但只有在运行时你才能找出参数的数量和类型。 然后,您需要在堆栈或寄存器中查看参数,具体取决于应用程序二进制接口/调用约定。

我会使用以下方法:程序的某些组件计算出参数的数量和类型。 我们假设它创建了一个{type, value}, {type, value}, ...

然后,您将此列表传递给函数以准备ABI调用。 这将是一个汇编程序函数。 对于基于堆栈的ABI(32位),它只是将参数推送到堆栈。 对于基于寄存器的ABI,它可以准备寄存器值并将它们保存为局部变量( add sp,nnn ),并且一旦准备好所有参数(可能使用调用所需的寄存器,因此首先保存它们),加载寄存器(一系列mov指令)并执行call指令。

x86-64 Windows调用约定相当简单,并且可以编写一个不知道任何类型的包装函数。 只需将args的前32个字节加载到寄存器中,然后将其余部分复制到堆栈中。


你肯定需要从asm进行函数调用 ; 它不可能可靠地工作来完成一堆函数调用,如fill_register_xmm0并希望编译器不会破坏任何这些寄存器。 C编译器发出使用寄存器的指令 ,作为其正常工作的一部分,包括将args传递给fill_register_xmm0fill_register_xmm0

唯一的选择是编写带有函数调用的C语句,其中所有args具有正确的类型,以使编译器发出代码以正常进行函数调用。 如果args只有几种可能的不同组合,那么将它们放在if()块中可能会很好。

BTW, movsd xmm0, variable可能会组装成movsd xmm0, xmm0 ,因为第一个函数arg是在XMM0中传递的,如果它是FP的话。


在C中,使用args准备一个缓冲区 (就像在32位的情况下一样)。

如果它更窄,每个都需要填充到8个字节。 请参阅MS的x86-64 __fastcall文档 。 (注意x86-64 __vectorcall在寄存器中按值传递__m128 args,但是对于__fastcall ,在寄存器args之后,args形成一个8字节值的数组是完全正确的。并且将它们存储到阴影空间中会创建一个完整的数组所有的args。)

任何不适合8个字节或不是1,2,4或8个字节的参数必须通过引用传递。 没有尝试在多个寄存器中传播单个参数。

但是在Windows调用约定中使variadic函数变得容易的关键因素也在这里工作: 用于第二个arg的寄存器不依赖于第一个arg的类型 。 即如果FP arg是第一个arg,则使用整数寄存器arg传递槽。 所以你最多只能有4个寄存器args,而不是4个整数和4个FP。

如果第4个arg是整数,则它进入R9 ,即使它是第一个整数arg 。 与x86-64 System V调用约定不同,第一个整数 arg在rdi ,不管寄存器和/或堆栈中有多少早期的FP args。

因此调用该函数的asm包装器可以将前8个字节加载到整数和FP寄存器中 ! (Variadic函数已经要求这样,所以被调用者不必知道是否存储整数或FP寄存器来形成该arg数组.MS优化了调用约定,以简化可变参数函数的function,但牺牲了函数的效率。整数和FP args的混合。)

将所有args放入缓冲区的C端看起来像这样:

 #include  int asmwrapper(const char *argbuf, size_t argp-argbuf, void (*funcpointer)(...)); void somefunc() { alignas(16) uint64_t argbuf[256/8]; // or char argbuf[256]. But if you choose not to use alignas, then uint64_t will still give 8-byte alignment char *argp = (char*)argbuf; for( ; argp < &argbuf[256] ; argp += 8) { if (figure_out_an_arg()) { int foo = get_int_arg(); memcpy(argp, &foo, sizeof(foo)); } else if(bar) { double foo = get_double_arg(); memcpy(argp, &foo, sizeof(foo)); } else ... memcpy whatever size // or allocate space to pass by ref and memcpy a pointer } if (argp == &argbuf[256]) { // error, ran out of space for args } asmwrapper(argbuf, argp-argbuf, funcpointer); } 

不幸的是,我认为我们不能直接在堆栈上使用argbuf 作为函数调用的args +阴影空间。 我们无法阻止编译器在argbuf下面放置有价值的东西,这样我们就可以将argbuf设置到它的底部(并在某处保存返回地址,也可以在argbuf的顶部保留一些空间供asm使用) 。

无论如何,只需复制整个缓冲区即可。 或者实际上,将前32个字节加载到寄存器(整数和FP)中,并仅复制其余的字节。 阴影空间不需要初始化。

如果你提前知道它需要多大, argbuf可能是一个VLA,但是256字节非常小。 它不像读取它的结尾可能是一个问题,它不能在一个页面的末尾与未映射的内存以后,因为我们的父函数的堆栈帧肯定需要一些空间。

 ;; NASM syntax. For MASM just rename the local labels and add whatever PROC / ENDPROC is needed. ;; UNTESTED ;; rcx: argbuf ;; rdx: length in bytes of the args. 0..256, zero-extended to 64 bits ;; r8 : function pointer ;; reserve rdx bytes of space for arg passing ;; load first 32 bytes of argbuf into integer and FP arg-passing registers ;; copy the rest as stack-args above the shadow space global asmwrapper asmwrapper: push rbp mov rbp, rsp ; so we can efficiently restore the stack later mov r10, r8 ; move function pointer to a volatile but non-arg-passing register ; load *both* xmm0-3 and rcx,rdx,r8,r9 from the first 32 bytes of argbuf ; regardless of types or whether there were that many arg bytes ; All bytes are loaded into registers early, some reg->reg transfers are done later ; when we're done with more registers. ; movsd xmm0, [rcx] ; movsd xmm1, [rcx+8] movaps xmm0, [rcx] ; 16-byte alignment required for argbuf. Use movups to allow misalignment if you want movhlps xmm1, xmm0 ; use some ALU instructions instead of just loads ; rcx,rdx can't be set yet, still in use for wrapper args movaps xmm2, [rcx+16] ; it's ok to leave garbage in the high 64-bits of an XMM passing a float or double. ;movhlps xmm3, xmm2 ; the copyloop uses xmm3: do this later movq r8, xmm2 mov r9, [rcx+24] mov eax, 32 cmp edx, eax jbe .small_args ; no copying needed, just shadow space sub rsp, rdx and rsp, -16 ; reserve extra space, realigning the stack by 16 ; rax=32 on entry, start copying just above shadow space (which doesn't need to be copied) .copyloop: ; do { movaps xmm3, [rcx+rax] movaps [rsp+rax], xmm3 ; indexed addressing modes aren't always optimal, but this loop only runs a couple times. add eax, 16 cmp eax, edx jb .copyloop ; } while(bytes_copied < arg_bytes); .done_arg_copying: ; xmm0,xmm1 have the first 2 qwords of args movq rcx, xmm0 ; RCX NO LONGER POINTS AT argbuf movq rdx, xmm1 ; xmm2 still has the 2nd 16 bytes of args ;movhlps xmm3, xmm2 ; don't use: false dependency on old value and we just used it. pshufd xmm3, xmm2, 0xee ; xmm3 = high 64 bits of xmm2. (0xee = _MM_SHUFFLE(3,2,3,2)) ; movq xmm3, r9 ; nah, can be multiple uops on AMD ; r8,r9 set earlier call r10 leave ; restore RSP to its value on entry ret ; could handle this branchlessly, but copy loop still needs to run zero times ; unless we bump up the min arg_bytes to 48 and sometimes copy an unnecessary 16 bytes ; As much work as possible is before the first branch, so it can happen while a mispredict recovers .small_args: sub rsp, rax ; reserve shadow space ;rsp still aligned by 16 after push rbp jmp .done_arg_copying ;byte count. This wrapper is 82 bytes; would be nice to fit it in 80 so we don't waste 14 bytes before the next function. ;eg maybe mov rcx, [rcx] instead of movq rcx, xmm0 ;mov eax, $-asmwrapper align 16 

这确实组装了( 在带有NASM的Godbolt上 ),但我还没有测试过它。

它应该运行得很好,但如果你在<= 32字节到> 32字节的截止值周围出现误预测,改变分支,使它总是复制额外的16字节。 (取消注释cmovb版本中的cmp / cmovb ,但复制循环仍然需要从32个字节开始进入每个缓冲区。)

如果你经常传递很少的args, 16字节的负载可能会从两个窄存储到一个宽的重新加载存储转发停顿 ,导致额外的8个周期的延迟。 这通常不是吞吐量问题,但它可以在被调用函数访问其args之前增加延迟。 如果无序执行无法隐藏,那么值得使用更多的负载uops分别加载每个8字节的arg。 (特别是进入整数寄存器,然后从那里到XMM,如果args大多是整数。那将具有比mem更低的延迟 - > xmm - >整数。)

但是,如果你有多个args,希望前几个已经提交到L1d,并且在asm包装器运行时不再需要存储转发。 或者有足够的复制后来的args,前2个args完成其加载+ ALU链的早期足以不延迟被调用函数内的关键路径。

当然,如果性能是一个很大的问题,你可以编写代码来计算asm中的args所以你不需要这些副本,或者使用带有C编译器可以直接调用的固定函数签名的库接口。 我确实试图在现代的英特尔/ AMD主流CPU( http://agner.org/optimize/ )上尽可能少地使用它,但是我没有对它进行基准测试或调整它,所以可能会改进一些分析它的时间,特别是对于一些真实的用例。

如果你知道FP args不是第一个4的可能性,你可以通过加载整数regs来简化。