Надстройка Excel: назначение в цикле for вызывает ошибку сегментации, но построчные назначения работают. Почему?

Я только что протестировал игрушечный проект надстройки Excel, кросс-сборку XLL с цепочками инструментов mingw32.

Вот мой код:

//testXLL.c
#include "windows.h"
#include "xlcall.h"

#define MEMORYSIZE 65535000
char vMemBlock[MEMORYSIZE];
int vOffsetMemBlock =0;

LPSTR GetTempMemory(int cBytes){
    LPSTR lpMemory;
    if(vOffsetMemBlock + cBytes > MEMORYSIZE)
        return 0;
    else{
        lpMemory = (LPSTR) &vMemBlock + vOffsetMemBlock;
        vOffsetMemBlock += cBytes;
        if(vOffsetMemBlock & 1) vOffsetMemBlock++;
        return lpMemory;
    }
}

LPXLOPER TempStr(LPSTR lpstr){
    LPXLOPER lpx;
    int chars;
    lpx = (LPXLOPER)GetTempMemory(sizeof(XLOPER));
    if(!lpx) return 0;
    chars = lstrlen(lpstr); 
    if(chars>255) chars=255;
    lpx->val.str=(char*)GetTempMemory((sizeof(char)*chars+1));
    if(!lpx->val.str) return 0;
    strncpy(lpx->val.str, lpstr,chars);
    lpx->val.str[0]=(BYTE) chars;
    //lpx->val.str[chars]='\0';
    lpx->xltype = xltypeStr;
    return lpx;
}
   
#ifdef __cplusplus
extern "C" {
#endif

    __declspec(dllexport) double __stdcall myadd2(double a1,double a2){
        return a1+a2;
    }

    static char functionTable[11][255] =
    {" myadd2",                    // procedure
        " BBB",                        // type_text
        " add",                     // function_text
        " add1,add2",                     // argument_text
        " 1",                          // macro_type
        " category",              // category
        " ",                           // shortcut_text
        " some help topic",                           // help_topic
        " Adds toy",    // function_help
        " 1st.",   // argument_help1
        " 2nd"   // argument_help2
    };

    __declspec(dllexport) int __stdcall xlAutoOpen(){
        LPXLOPER pxDLL;
        Excel4(xlGetName,pxDLL,0);

        XLOPER xlRegArgs[11];
        for(int i = 0; i < 11; i++){
            xlRegArgs[i] = *TempStr(functionTable[i]);
        }


        Excel4(xlfRegister, 0, 12,
                pxDLL,
                &xlRegArgs[0], &xlRegArgs[1], &xlRegArgs[2], 
                &xlRegArgs[3], &xlRegArgs[4], &xlRegArgs[5],
                &xlRegArgs[6], &xlRegArgs[7], &xlRegArgs[8],
                &xlRegArgs[9], &xlRegArgs[10]);

        return 1;
    }

    __declspec(dllexport) LPXLOPER __stdcall xlAddInManagerInfo(LPXLOPER xlAction) {
        static XLOPER xlReturn, xlLongName, xlTemp;

        xlTemp.xltype = xltypeInt;
        xlTemp.val.w = xltypeInt;
        Excel4(xlCoerce, &xlReturn, 2, xlAction, &xlTemp);

        if(1 == xlReturn.val.w) {
            xlLongName = *TempStr(" xll-name"); 
        } else {
            xlLongName.xltype = xltypeErr;
            xlLongName.val.err = xlerrValue;
        }

        return &xlLongName;
    }

#ifdef __cplusplus
}
#endif

Я создал этот файл testXLL.c в Ubuntu:

>i686-w64-mingw32-gcc -shared -Wl,--kill-at testXLL.c -o win.xll -L. -lxlcall32

Это успешно создает файл win.xll, но при загрузке этого файла win.xll происходит сбой Excel.

В Windows 10 я пытался использовать gdb для его отладки, но я не могу поймать точку останова в файле xll — он отключается автоматически при загрузке. Но я вижу в выводе gdb, что это ошибка сегментации при сбое Excel.

 XLOPER xlRegArgs[11];
for(int i = 0; i < 11; i++){
    xlRegArgs[i] = *TempStr(functionTable[i]);
}

Странно то, что если я заменю приведенный выше цикл for следующими построчными назначениями в функции xlAutoOpen, скомпилированный файл XLL отлично работает в Excel:

XLOPER xlRegArgs[11];
xlRegArgs[0] = *TempStr(functionTable[0]);
xlRegArgs[1] = *TempStr(functionTable[1]);
xlRegArgs[2] = *TempStr(functionTable[2]);
xlRegArgs[3] = *TempStr(functionTable[3]);
xlRegArgs[4] = *TempStr(functionTable[4]);
xlRegArgs[5] = *TempStr(functionTable[5]);
xlRegArgs[6] = *TempStr(functionTable[6]);
xlRegArgs[7] = *TempStr(functionTable[7]);
xlRegArgs[8] = *TempStr(functionTable[8]);
xlRegArgs[9] = *TempStr(functionTable[9]);
xlRegArgs[10] = *TempStr(functionTable[10]);

Пожалуйста, просветите меня. В чем разница между этими двумя подходами к назначению?


person user3148104    schedule 01.04.2021    source источник
comment
Второстепенный: if(chars>255) chars=255; должен быть 254, чтобы включать завершающий нуль. После strncpy вы должны завершить строку по индексу 254.   -  person Paul Ogilvie    schedule 01.04.2021
comment
Excel4(xlfRegister, 0, 12, может потребоваться 11.   -  person Paul Ogilvie    schedule 01.04.2021
comment
@PaulOgilvie должно быть 12, первый аргумент xlfRegister — это полный путь к xll, pxDLL. остальные 11 аргументов связаны с экспортируемой функцией. Так как версия с построчным назначением работает, то и другая часть кодов должна быть в порядке.   -  person user3148104    schedule 01.04.2021
comment
У меня были похожие проблемы при создании DLL для x86. Попробуйте сделать индекс цикла (i) переменной static вне функции; если это работает, то это похоже на какую-то проблему с повреждением стека. Я копну немного глубже, чтобы увидеть, решил ли я когда-либо проблему.   -  person Adrian Mole    schedule 01.04.2021
comment
@AdrianMole Вот это да, спасибо! Я просто объявляю i как глобальную переменную, она никогда не падает! Это ошибка EXCEL?   -  person user3148104    schedule 01.04.2021
comment
Не думайте, что это проблема Excel. Скорее всего, сборка DLL совсем неверна. Проверьте все настройки вашего проекта - проблемы с выравниванием, соглашения о вызовах и т. д. У меня то же самое с использованием расширений DLL, вызываемых из моей основной программы ... но только в сборках x86 (не x64).   -  person Adrian Mole    schedule 01.04.2021
comment
@AdrianMole Этот игрушечный проект имеет только один исходный файл, как и указанный testXLL.c, и явно определяет соглашение о вызовах __stdcall. Это проблема кроссплатформенного компилятора?   -  person user3148104    schedule 01.04.2021
comment
В MSVC я бы добавил __debugbreak(); перед циклом, указал Excel в качестве хоста для отладки DLL и начал отладку. Когда отладчик достигает точки останова, установите ловушку данных на i и несколько раз осторожно перешагните через (F10), соблюдая значение i. Этот https://stackoverflow.com/a/49079078/8666197 может помочь вам добавить жесткую точку останова в gcc. В качестве альтернативы используйте отладку для бедняков с помощью MessageBox или OutputDebugString, если gdb поддерживает это.   -  person Daniel Sęk    schedule 07.04.2021
comment
Надстройки @PaulOgilvie Excel используют строки с подсчетом байтов, а не заканчивающиеся нулем, поэтому пробел в начале каждого строкового литерала в functionTable. Длина строки задается в первом байте.   -  person DS_London    schedule 07.04.2021
comment
Это интересно. В прошлом я пытался закодировать более общий механизм регистрации функций и столкнулся с такой проблемой (64-битная VS) ... которую, честно говоря, я так и не решил, просто работал. Я также обнаружил, что код, который работал как отладочная сборка, давал сбой в релизных версиях: возможно, это связано с тем, что отладка добавляет защитные байты вокруг памяти, возможно?   -  person DS_London    schedule 07.04.2021


Ответы (1)


Хотя у меня (пока) нет полного объяснения такого поведения, я публикую его как возможный «обходной путь», который я использовал в очень похожем случае, с которым я столкнулся в одном из моих проекты.

Проблема похоже в некоторой форме "повреждения стека", вызванного использованием локальной переменной функции (i), используемой в качестве индекса цикла; преобразование этого в глобальную/статическую переменную, скорее всего, решит проблему. Следующий фрагмент кода является предлагаемым исправлением (я изменил имя индексной переменной, чтобы избежать возможных конфликтов имен в других частях кода):

///...
    static int regloop; // Used as the loop index, below...

    __declspec(dllexport) int __stdcall xlAutoOpen(){
        LPXLOPER pxDLL;
        Excel4(xlGetName,pxDLL,0);

        XLOPER xlRegArgs[11];
        for(regloop = 0; regloop < 11; regloop++){
            xlRegArgs[regloop] = *TempStr(functionTable[regloop]);
        }

Вот фрагмент кода из моего вышеупомянутого проекта (но обратите внимание, что это C++/MFC), который демонстрирует такое же поведение, но только в сборках x86 (сборки x64 работают без проблем):

static int plin;    // NOTA BENE:-  We use this in the two functions below, as the use of
                    // a local 'plin' loop index is prone to induce stack corruption (?), 
                    // especially in MSVC 2017 (MFC 14) builds for x86.

void BasicApp::OnUpdatePICmd(uint32_t nID, void *pUI)
{
//! for (int plin = 0; plin < Plugin_Number; ++plin) { // Can cause problems - vide supra
    for (plin = 0;  plin < Plugin_Number;  ++plin) {
        BOOL mEbl = FALSE;  int mChk = -1;
        if ((Plugin_UDCfnc[plin] != nullptr) && Plugin_UDCfnc[plin](nID, &mEbl, &mChk)) {
            CommandEnable(pUI, mEbl ? true : false);
            if (mChk >= 0) CmdUISetCheck(pUI, mChk);
            return;
        }
    }
    CommandEnable(pUI, false);
    return;
}

(Plugin_UDCfnc является членом массива static класса BasicApp.)

За годы, прошедшие после написания приведенного выше кода, у меня были случайные «мимолетные озарения» о том, почему это происходит, но на данный момент я не могу предложить более надежное решение. Я вернусь к проблеме и обновлю этот пост, если наткнусь на решение. Тем временем другие могут принять это как «подсказку» и опубликовать свои собственные объяснения/решения.

person Adrian Mole    schedule 01.04.2021
comment
Я установил x64 версию Excel на виртуальную машину, скопировал соответствующую x64 XLCALL32.dll в исходный каталог и скомпилировал оригинальный исходный файл локальной версии итератора i: x86_64-w64-mingw32-gcc -shared -Wl,--kill-at testXLL.c -o win64.xll -L. -lXLCALL32. На этот раз x64' xll работает нормально, без сбоев, как x86. Действительно, возможно, это проблема исключительно для x86. - person user3148104; 01.04.2021