Вызов собственного кода с помощью рукописной сборки

#assembly #x86 #sse

# #сборка #x86 #sse

Вопрос:

Я пытаюсь вызвать собственную функцию из управляемой сборки. Я делал это в предварительно скомпилированных библиотеках, и все прошло хорошо. В данный момент я создаю свою собственную библиотеку, и я не могу получить эту работу.

Собственный источник 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));
        }
    }
}
 

При использовании приведенных выше источников код выполняется без ошибок (указатель на функцию получен, и делегат фактически создан.

Проблема возникает, когда я вызываю делегат: он выполняется (и я также могу его отладить!), Но при выходе из функции управляемое приложение запускает систему.ExecutionEngineException (когда он не завершается без исключений).

Фактическая проблема заключается в реализации функции: она содержит блок asm с инструкциями SSE; если я удалю блок asm, код будет работать отлично.

Я подозреваю, что мне не хватает какой-то сборки для сохранения / восстановления реестра, но я совершенно не осведомлен об этом.

Странно то, что если я изменю соглашение о вызовах на __stdcall, отладочная версия «кажется» работает, в то время как релизная версия ведет себя так, как если бы использовался __cdecl, вызывающий convetion .

(И только потому, что мы здесь, можете ли вы уточнить, имеет ли значение вызывающий convetion?)


Хорошо, благодаря комментарию Дэвида Хеффернана я выяснил, что неправильные инструкции, вызывающие проблему, заключаются в следующем:

  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;
 

Возможно, ошибка вызвана маршалом.AllocHGlobal или черная магия IntPtr?


Это минимальный источник для обнаружения ошибки:

 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);
}
 

Практически после второго перемещения стек изменяет значение результата (сохраненное в стеке) и сохраняет значения xmm0 по измененному (и неправильному) адресу, сохраненному в результате.

После выхода из *Matrix4x4_Multiply_SSE* исходная память не изменяется.

Чего мне не хватает?

Комментарии:

1. Вы не показали код asm, что странно, поскольку именно в этом, по вашему мнению, проблема. Я бы рекомендовал вам проводить тестирование из собственного кода и исключить путаницу управляемого кода из уравнения.

2. @David Heffernan Глоток, вы совершенно правы: проблема заключается не в наличии блока __asm, а в самой инструкции ASM. Теперь я пытаюсь изолировать плохую инструкцию, даже если я перемещаю только значения с плавающей запятой в регистрах xmm .

3. Отладка работает, сбой выпуска. Это звучит как поврежденный стек. Вам не хватает push / pops?. Изменяется ли SP при выполнении операций с ошибками?. Является ли местоположение записи правильным или вы перезаписываете свой стек? Проверьте в окне памяти со значением SP, что изменяет ваши данные стека.

4. @AloisKraus На самом деле теперь также отлаживаются сбои (я думаю, потому что там временный запах кода). Однако, я думаю, вы поняли суть: после первой инструкции movups отладчик оценивает все параметры функции (указатели) до 0x00 (указанная память по результату не изменяется). После этого вторая инструкция вызывает системное исключение. Как мне протестировать память трассировки стека? Регистр ESP не изменяется.

5. Макет стека очень хорошо объяснен здесь: en.wikibooks.org/wiki/X86_Disassembly /… в начале функции сохраните ebp и откройте адрес в окне памяти. Когда вы выполняете следующий шаг в функции, вы можете увидеть свой стек (помните, что новые переменные хранятся по меньшим адресам), выделяя его локальные переменные и изменяя их. В начале ebp помещается в стек. Если это значение перезаписывается или что-то меняется в более высоком адресе при возникновении этого значения, у вас повреждение стека.

Ответ №1:

Коррекция выравнивания неверна. Вам нужно добавить alignment-misalignment , чтобы исправить выравнивание. Таким образом, код должен читать:

 mAlignedBuffer = 
    new IntPtr(mUnmanagedBuffer.ToInt64()   alignment - misalignment);
 

Однако я бы рекомендовал сначала протестировать функцию в собственных настройках. Как только вы узнаете, что это работает, вы можете перейти к управляемым настройкам и узнать, что любые проблемы связаны с управляемым кодом.

Комментарии:

1. 1 звучит разумно. Хотя используемая инструкция по сборке (moveUps) была невыровненной версией, которая не должна вызывать исключение общей защиты.

2. @Alois Я знаю, что это не все объясняет, но у нас нет всей информации, так что это хотя бы начало.

3. Это не решило основную проблему, но, несомненно, вы избавили меня от еще одной головной боли!

4. Код «вылетает» таким же образом и в собственном приложении. слово » сбой » заключено в кавычки, потому что код не работает, но приложение не завершается с исключением… на мой вкус, это слишком сильно пахнет.

Ответ №2:

Ваша сборка была ошибочной. Существует разница между

 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(amp;x);
 

С mov [x],10 вы не устанавливаете x равным 10, а записываете в свой стек.

Ответ №3:

Я нахожу решение. Загрузка значения указателя в регистр процессора и использование регистра для перенаправления в память:

 mov esi, resu<
movups [esi][ 0], xmm0;
 

Использование этих инструкций заставляет код работать так, как ожидалось.


Но вопрос остается полностью нерешенным, поскольку инструкция movups может принимать в качестве первого аргумента адрес памяти; поэтому, если кто-то знает, что происходит, я с удовольствием проверю лучший ответ.