使用手写汇编调用本地代码

3

我试图从托管程序集中调用本地函数。在预编译库上我做到了这一点,一切都很顺利。但目前我正在构建自己的库,却无法让它正常工作。

本地DLL源代码如下:

#define DERM_SIMD_EXPORT        __declspec(dllexport)

#define DERM_SIMD_API           __cdecl

extern "C" {

    DERM_SIMD_EXPORT void DERM_SIMD_API Matrix4x4_Multiply_SSE(float *result, float *left, float *right);

}

void DERM_SIMD_API Matrix4x4_Multiply_SSE(float *result, float *left, float *right) {
    __asm {
       ....
    }
}

下面是代码示例,在托管代码中加载库,并从函数指针创建委托。

public unsafe class Simd
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void MatrixMultiplyDelegate(float* result, float* left, float* right);

    public static MatrixMultiplyDelegate MatrixMultiply;

    public static void LoadSimdExtensions()
    {
        string assemblyPath = "Derm.Simd.dll";

        IntPtr address = GetProcAddress.GetAddress(assemblyPath, "Matrix4x4_Multiply_SSE");

        if (address != IntPtr.Zero) {
            MatrixMultiply = (MatrixMultiplyDelegate)Marshal.GetDelegateForFunctionPointer(address, typeof(MatrixMultiplyDelegate));
        }
    }
}

使用以上的资源,代码运行没有错误(函数指针被获取并且委托实际上已被创建)。

问题出现在我调用委托时:它被执行了(我也可以调试它!),但在函数退出时,托管应用程序会引发System.ExecutionEngineException异常(当没有异常退出时)。

实际问题是函数的实现:它包含有SSE指令的asm块;如果我删除这个asm块,代码就能完美地工作。

我怀疑自己遗漏了一些寄存器保存/恢复程序集,但我对这方面完全不了解。

奇怪的是,如果我将调用约定更改为__stdcall,调试版本“似乎”可以工作,而发布版本则表现得好像使用了__cdecl调用约定。

(正因为我们在这里,你能澄清一下调用约定是否重要吗?)


好的,感谢David Heffernan的评论,我发现导致问题的坏指令如下:

 movups result[ 0], xmm4;
 movups result[16], xmm5;

movups指令将16字节数据移动到(非对齐)内存。

该函数由以下代码调用:

 unsafe {
    float* prodFix = (float*)prod.MatrixBuffer.AlignedBuffer.ToPointer();
    float* m1Fix = (float*)m2.MatrixBuffer.AlignedBuffer.ToPointer();
    float* m2Fix = (float*)m1.MatrixBuffer.AlignedBuffer.ToPointer();

    if (Simd.Simd.MatrixMultiply == null) {
                    // ... unsafe C# code
    } else {
        Simd.Simd.MatrixMultiply(prodFix, m1Fix, m2Fix);
    }
}

这里的MatrixBuffer是我的一个类;它的成员AlignedBuffer是以下方式分配的:

// Allocate unmanaged buffer
mUnmanagedBuffer = Marshal.AllocHGlobal(new IntPtr((long)(size + alignment - 1)));

// Align buffer pointer
long misalignment = mUnmanagedBuffer.ToInt64() % alignment;
if (misalignment != 0)
    mAlignedBuffer = new IntPtr(mUnmanagedBuffer.ToInt64() + misalignment);
else
    mAlignedBuffer = mUnmanagedBuffer;

也许错误是由Marshal.AllocHGlobalIntPtr黑魔法引起的?
这是发现错误的最简源代码:
void Matrix4x4_Multiply_SSE(float *result, float *left, float *right)
{
    __asm {
        movups xmm0,    right[ 0];

        movups result, xmm0;
    }
}


int main(int argc, char *argv[])
{
    float r0[16];
    float m1[16], m2[16];

    m1[ 0] = 1.0f; m1[ 4] = 0.0f; m1[ 8] = 0.0f; m1[12] = 0.0f;
    m1[ 1] = 0.0f; m1[ 5] = 1.0f; m1[ 9] = 0.0f; m1[13] = 0.0f;
    m1[ 2] = 0.0f; m1[ 6] = 0.0f; m1[10] = 1.0f; m1[14] = 0.0f;
    m1[ 3] = 0.0f; m1[ 7] = 0.0f; m1[11] = 0.0f; m1[15] = 1.0f;

    m2[ 0] = 1.0f; m2[ 4] = 0.0f; m2[ 8] = 0.0f; m2[12] = 0.0f;
    m2[ 1] = 0.0f; m2[ 5] = 1.0f; m2[ 9] = 0.0f; m2[13] = 0.0f;
    m2[ 2] = 0.0f; m2[ 6] = 0.0f; m2[10] = 1.0f; m2[14] = 0.0f;
    m2[ 3] = 0.0f; m2[ 7] = 0.0f; m2[11] = 0.0f; m2[15] = 1.0f;

    r0[ 0] = 0.0f; r0[ 4] = 0.0f; r0[ 8] = 0.0f; r0[12] = 0.0f;
    r0[ 1] = 0.0f; r0[ 5] = 0.0f; r0[ 9] = 0.0f; r0[13] = 0.0f;
    r0[ 2] = 0.0f; r0[ 6] = 0.0f; r0[10] = 0.0f; r0[14] = 0.0f;
    r0[ 3] = 0.0f; r0[ 7] = 0.0f; r0[11] = 0.0f; r0[15] = 0.0f;

    Matrix4x4_Multiply_SSE(r0, m1, m2);
    Matrix4x4_Multiply_SSE(r0, m1, m2);

    return (0);
}

实际上,在第二个movups之后,堆栈会更改result值(存储在堆栈上),并将xmm0的值存储在result中修改的(错误的)地址上。

从 *Matrix4x4_Multiply_SSE* 中退出后,原始内存不会被修改。

我错过了什么?


1
你没有展示汇编代码,这很奇怪,因为你认为问题出在那里。我建议你从本地代码进行测试,并将托管代码的混淆排除在外。 - David Heffernan
@David Heffernan Gulp,你说得完全正确:问题不在于__asm块的存在,而在于汇编指令本身。现在我正在尝试隔离有问题的指令,即使我只是在xmm寄存器上移动浮点数。 - Luca
1
调试版可以运行,但发布版会崩溃。这听起来像是堆栈损坏了。你是否缺少push/pops操作?当执行出错的操作时,SP是否发生变化?写入位置是否正确,或者你是否在覆盖堆栈?在内存窗口中检查SP的值,看看是什么导致了堆栈数据的变化。 - Alois Kraus
@AloisKraus 实际上现在调试器也会崩溃(我认为是因为那里有一个临时的代码问题)。然而,我认为你已经明白了:在第一个movups指令之后,调试器会将所有函数参数(指针)评估为0x00(由result指向的内存未被修改)。之后,第二个指令会导致系统异常。我如何测试堆栈跟踪内存?ESP寄存器不会改变。 - Luca
栈布局在这里得到了很好的解释:http://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames,在函数开头存储ebp并打开内存窗口中的地址。当您进一步执行函数时,可以看到堆栈(请记住,新变量存储在较小的地址中)分配其本地变量并更改它们。在开始时,将ebp放入堆栈中。如果此值被覆盖或在高于此值发生更改,则会发生堆栈损坏。 - Alois Kraus
的确,我的堆栈已经损坏。 - Luca
3个回答

2
对齐校正错误。你需要添加 alignment-misalignment 来纠正对齐。因此,代码应该为:
mAlignedBuffer = 
    new IntPtr(mUnmanagedBuffer.ToInt64() + alignment - misalignment);

然而,我建议您首先在本地环境中测试该函数。一旦确定它在本地环境中可行,您可以转到托管环境,并知道任何问题都是由托管代码引起的。


2
+1 听起来很合理。虽然使用的汇编指令(moveUps)是未对齐版本,不应该导致通用保护异常。 - Alois Kraus
@Alois 我知道这并不能解释一切,但我们还没有得到所有的信息,所以这至少是一个开始。 - David Heffernan
这并没有解决根本问题,但肯定让我免去了另一个头疼的问题! - Luca
代码在本地应用程序中也以相同的方式“崩溃”。崩溃一词被引用,因为代码不起作用,但应用程序没有以异常终止... 这对我来说有点过分了。 - Luca

1

你的汇编有缺陷。这里存在差异。

void DoSomething(int *x)
{
    __asm
    {
        mov x[0], 10   // wrong
            mov [x], 10    // also wrong
        mov esi,x      // first get address
        mov [esi],500  // then assign - correct
    }
}

前两个示例没有写入指针所指向的内存位置,而是写入了指针本身的存储位置。由于参数来自堆栈,您使用movups指令覆盖了堆栈。当您调用例如时,可以在调试器窗口中看到这一点。

int x=0;
DoSomething(&x);

使用 mov [x],10 指令时,你并没有将 x 设为 10,而是向堆栈中写入数据。

0

我找到了一个解决方案。将指针值加载到CPU寄存器中,然后使用该寄存器重定向到内存:

mov esi, result;
movups [esi][ 0], xmm0;

使用这些指令可以使代码按预期工作。


但问题仍未完全解决,因为movups指令可以将内存地址作为第一个参数;如果有人知道发生了什么,我很高兴查看最佳答案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接