Как получить адрес указателя базового стека

Я переношу приложение с x86 на x64. Я использую Visual Studio 2009; большая часть кода - С++, а некоторые части - это просто C. Ключевое слово __asm ​​не поддерживается при компиляции в направлении x64, и наше приложение содержит несколько частей встроенного ассемблера. Я не писал этот код, поэтому не знаю точно, что должен делать et.

int CallStackSize() {
    DWORD Frame;
    PDWORD pFrame;
    __asm
        {
            mov EAX, EBP
            mov Frame, EAX
        }
    pFrame = (PDWORD)Frame;
    /*... do stuff with pFrame here*/
}

EBP является базовым указателем на стек текущей функции. Есть ли способ получить указатель стека без использования встроенного asm? Я смотрел на внутренности, которые Microsoft предлагает в качестве замены inline asm, но я не мог найти ничего, что дало бы мне что-то полезное. Любые идеи?

Андреас спросил, что делать с pFrame. Вот полная функция:

int CallStackSize(DWORD frameEBP = 0)
{
    DWORD pc;
    int tmpint = 0;
    DWORD Frame;
    PDWORD pFrame, pPrevFrame;

    if(!frameEBP) // No frame supplied. Use current.
    {
        __asm
        {
            mov EAX, EBP
            mov Frame, EAX
        }
    }
    else Frame = frameEBP;

    pFrame = (PDWORD)Frame;
    do
    {
        pc = pFrame[1];
        pPrevFrame = pFrame;
        pFrame = (PDWORD)pFrame[0]; // precede to next higher frame on stack

        if ((DWORD)pFrame & 3) // Frame pointer must be aligned on a DWORD boundary. Bail if not so.
        break;

        if (pFrame <= pPrevFrame)
        break;

        // Can two DWORDs be read from the supposed frame address?
        if(IsBadWritePtr(pFrame, sizeof(PVOID)*2))
        break;

        tmpint++;
    } while (true);
    return tmpint;
}

Переменная pc не используется. Похоже, что эта функция проходит по стеку, пока не сработает. Он предполагает, что он не может читать вне стека приложений, поэтому, когда он терпит неудачу, он измеряет глубину стека вызовов. Этот код не нужно компилировать в компиляторе _EVERY_SINGLE. Просто VS2009. Приложение не нужно запускать на компьютере EVERY_SINGLE. Мы полностью контролируем развертывание, так как мы сами устанавливаем и настраиваем его и доставляем все это нашим клиентам.

Ответы

Ответ 1

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

Но чтобы делать то, что вы ищете, вы должны иметь возможность:

int CallStackSize() {
    __int64 Frame = 0; /* MUST be the very first thing in the function */
    PDWORD pFrame;

    Frame++; /* make sure that Frame doesn't get optimized out */

    pFrame = (PDWORD)(&Frame);
    /*... do stuff with pFrame here*/
}

Причина этого в том, что в C обычно первое, что делает функция, это сберечь местоположение базового указателя (ebp) до выделения локальных переменных. Создавая локальную переменную (Frame), а затем получая адрес if, мы действительно получаем адрес начала этого фрейма стека функций.

Примечание. Некоторые оптимизации могут привести к удалению переменной "Frame". Наверное, нет, но будьте осторожны.

Второе примечание: ваш исходный код, а также этот код управляет данными, на которые указывает "pFrame", когда сам "pFrame" находится в стеке. Здесь можно случайно перезаписать pFrame, а затем у вас будет плохой указатель и может возникнуть какое-то странное поведение. Особо следует помнить об этом при переходе с x86 на x64, поскольку pFrame теперь составляет 8 байт вместо 4, поэтому, если ваш старый код "do stuff with pFrame" учитывает размер Frame и pFrame, прежде чем возиться с памятью, вы будете необходимо учитывать новый больший размер.

Ответ 2

Вы можете использовать встроенный _AddressOfReturnAddress() для определения местоположения в текущем указателе кадра, предполагая, что он не был полностью оптимизирован. Я предполагаю, что компилятор не позволит этой функции оптимизировать указатель кадра, если вы явно ссылаетесь на него. Или, если вы используете только один поток, вы можете использовать IMAGE_NT_HEADER.OptionalHeader.SizeOfStackReserve и IMAGE_NT_HEADER.OptionalHeader.SizeOfStackCommit, чтобы определить размер стека основного потока. См. this для доступа к IMAGE_NT_HEADER для текущего изображения.

Я бы также рекомендовал не использовать IsBadWritePtr для определения конца стека. По крайней мере, вы, вероятно, заставите стек расти, пока не нажмете резерв, поскольку вы выйдете на страницу охраны. Если вы действительно хотите найти текущий размер стека, используйте VirtualQuery с адресом, который вы проверяете.

И если исходное использование состоит в том, чтобы ходить в стек, вы можете использовать StackWalk64 для этого.

Ответ 3

Microsoft предоставляет библиотеку (DbgHelp), которая заботится о стекировании стека, и вы должны использовать вместо того, чтобы полагаться на трюки сборки. Например, если файлы PDB присутствуют, он может также перемещать оптимизированные фреймы стека (те, которые не используют EBP).

В CodeProject есть статья, в которой объясняется, как ее использовать:

http://www.codeproject.com/KB/threads/StackWalker.aspx

Ответ 4

Если вам нужен точный "базовый указатель", тогда встроенная сборка - единственный способ пойти.

Удивительно, что можно написать код, который пугает стек с относительно небольшим кодом, специфичным для платформы, но трудно вообще избежать сборки (в зависимости от того, что вы делаете).

Если все, что вы пытаетесь сделать, это избежать, вы можете просто взять адрес любой локальной переменной.

Ответ 5

.code

PUBLIC getStackFrameADDR _getStackFrameADDR
getStackFrameADDR:
    mov RAX, RBP
    ret 0

END

Что-то подобное может сработать для вас.

Скомпилируйте его с помощью ml64 или jwasm и вызовите его, используя это в своем коде. extern "C" void getstackFrameADDR (void);

Ответ 6

Нет гарантии, что RBP (эквивалент x64 EBP) на самом деле является указателем на текущий кадр в стоп-кадре. Я предполагаю, что Microsoft решила, что, несмотря на несколько новых регистров общего назначения, им нужен еще один освобожденный, поэтому RBP используется только как фреймпоинтер в функциях, которые вызывают alloca() и в некоторых других случаях. Таким образом, даже если встроенная сборка была поддержана, это не было бы возможным.

Если вы просто хотите вернуться, вам нужно использовать StackWalk64 в файле dbghelp.dll. Это в dbghelp.dll, поставляемом с XP, и pre-XP не было 64-битной поддержки, поэтому вам не нужно отправлять DLL с вашим приложением.

Для вашей 32-разрядной версии просто используйте свой текущий метод. Ваши собственные методы, вероятно, будут меньше, чем библиотека импорта для dbghelp, а тем более фактическая dll в памяти, поэтому это определенная оптимизация (личный опыт: я реализовал backtrace в стиле Glibc и backtrace_symbols для x86 менее чем за один раз, в десятом размере библиотеки импорта dbghelp).

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

Возможно, в какой-то момент я решит ориентироваться на x64 всерьез и выясню дешевый способ использования StackWalk64, который я могу предоставить, но поскольку я все еще нацелен на x86 для всех своих проектов, я не беспокоился.