Таинственное поведение с NtWaitForSingleObject в C++ – возвращает ошибку недопустимой ручки без std::cout

Вопрос или проблема

Я столкнулся с странной проблемой в своей программе, которая использует NtWaitForSingleObject и NtDelayExecution в цикле. Проблема в том, что функция NtWaitForSingleObject иногда возвращает ошибку 0xC0000008 (STATUS_INVALID_HANDLE), но только тогда, когда я удаляю операторы std::cout из своего кода. Это поведение сводит меня с ума, и я не могу понять, в чем дело.

Вот что происходит:

Если у меня есть два оператора std::cout после системных вызовов (NtDelayExecution_Syscall и NtWaitForSingleObject_Syscall), все работает как ожидается. Если я удаляю операторы std::cout (или оставляю только один из них), NtWaitForSingleObject_Syscall возвращает 0xC0000008 (неправильный дескриптор). Я проверил значения в регистрах и переменных, и они кажутся правильными перед вызовом NtWaitForSingleObject. Дескриптор, передаваемый функции, является результатом вызова GetCurrentProcess(), который должен быть действительным.

Вот мой код:

Ассемблерный код (.asm):

.code 

; функция NtDelayExecution
NtDelayExecution_Syscall proc
    mov rax, 34h
    syscall
    ret
NtDelayExecution_Syscall endp

; функция NtWaitForSingleObject
NtWaitForSingleObject_Syscall proc
    mov rax, 04h
    syscall
    ret
NtWaitForSingleObject_Syscall endp
end

C++ код (.cpp):

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <iostream>

extern "C" LONG NtDelayExecution_Syscall(
    BOOLEAN Alertable,
    PLARGE_INTEGER DelayInterval
);

extern "C" LONG NtWaitForSingleObject_Syscall(
    HANDLE hProcess,
    BOOLEAN Alertable,
    PLARGE_INTEGER DelayInterval
);

void StartMonitor(HANDLE hProcessToMonitor) {
    // Установка нулевого таймаута
    LARGE_INTEGER integer;
    integer.QuadPart = -10000 * 1000;
    LARGE_INTEGER timeout;
    timeout.QuadPart = 0;
    LONG result;
    while (true) {
        result = NtDelayExecution_Syscall(FALSE, &integer);
        std::cout << result << std::endl;  // ПРОБЛЕМА: Без этой строки возникает ошибка
        result = NtWaitForSingleObject_Syscall(hProcessToMonitor, FALSE, &timeout);
        std::cout << result << std::endl;  // ПРОБЛЕМА: Без этой строки возникает ошибка
    }
    return;
}

int main() {
    HANDLE hProcess = GetCurrentProcess();  // Использование дескриптора GetCurrentProcess()
    StartMonitor(hProcess);
    return 0;
}

Что я пробовал:

Отключение оптимизаций: Я пробовал отключить оптимизации компилятора (/Od в MSVC), но поведение не изменилось.

Буферизация std::cout: Пытался отключить синхронизацию с C stdio с помощью std::ios_base::sync_with_stdio(false), без изменений.

Добавление искусственных переменных и выравнивания стека: Я добавил переменные, такие как volatile int padding[10];, чтобы проверить, нет ли проблем со стеком — без эффекта.

Пытался использовать alignas(16) для переменных: Не помогло.

Проверял регистры в отладчике: Когда возникает ошибка, RCX (который держит дескриптор) равен FFFFFFFFFFFFFFFF.

Мои вопросы:

Почему наличие std::cout влияет на результат NtWaitForSingleObject_Syscall?

Как поток вывода влияет на поведение этих системных вызовов?

Какие еще шаги отладки я могу предпринять, чтобы изолировать причину проблемы?

Может ли это быть связано с выравниванием памяти, управлением стеком или чем-то специфическим для MSVC?

Существуют ли известные проблемы с обработкой дескрипторов процесса из GetCurrentProcess() в низкоуровневых системных вызовах NtWaitForSingleObject?

Я потратил часы на диагностику этой проблемы, и буду признателен за любые идеи!

Ответ или решение

Ваше описание проблемы с функцией NtWaitForSingleObject, которая возвращает ошибку 0xC0000008 (STATUS_INVALID_HANDLE) при удалении std::cout, очень интригует и указывает на несколько возможных направлений для диагностики. Ниже я попытаюсь объяснить, почему такое поведение возможно и предложить пути решения.

Причины возникновения проблемы

  1. Оптимизация компилятора: Несмотря на то, что вы отключили оптимизацию через /Od, в некоторых случаях компилятор может по-прежнему выполнять определенные преобразования, которые могут влиять на порядок выполнения инструкций и состояние стека. Убедитесь, что вся логика программы действительно выполняется так, как вы ожидаете. Использование std::cout может замедлить выполнение программы, позволяя системным вызовам обрабатывать данные корректнее.

  2. Состояние регистра RCX: Как вы упомянули, когда происходит ошибка, регистр RCX, который должен содержать дескриптор процесса, равен FFFFFFFFFFFFFFFF. Это указывает на то, что в каком-то месте ваш дескриптор повреждается. Причиной может быть переполнение стека, неправильное управление памятью или неоправданный доступ к уже освобожденным ресурсам.

  3. Выравнивание и управление стеком: Если вы пробовали использовать alignas и добавляли переменные для выравнивания стека без изменения ситуации, это может указывать на более глубокие проблемы с управлением памятью. Убедитесь, что ваша ассемблерная часть корректно завершает выполнение и не влияет на состояние стека.

  4. Влияние std::cout: std::cout может не только выводить информацию, но и вызывать перезагрузку буфера, что изменяет поведение потока выполнения. Также есть возможность, что в коде присутствуют гонки потоков (если программа много потоковая) или что функции, которые обращаются к системным вызовам, не имеют необходимых для безопасности вызовов.

Рекомендации по диагностике и решению

  1. Добавьте отладочные приложения: Попробуйте заменить std::cout на более легкий вывод, например, используя OutputDebugString для записи отладочной информации. Это может помочь диагностировать проблему без влияния на время исполнения.

  2. Проверка дескрипторов: Добавьте код для проверки валидности дескрипторов перед вызовом NtWaitForSingleObject. Убедитесь, что он не изменяется в процессе выполнения программы.

  3. Изолированное тестирование: Упростите программу максимально и протестируйте код, состоящий только из системных вызовов и минимальных вспомогательных функций, исключив все, что может повлиять на состояние программы.

  4. Использование инструментов отладки: Используйте такие инструменты, как Application Verifier или WinDbg, чтобы получить более подробную информацию о состоянии программы и выявить потенциальные утечки или повреждения памяти.

  5. Проверьте доступные документы: Обратитесь к официальной документации Microsoft по системным вызовам и убедитесь, что все параметры передаются корректно и соответствуют спецификациям вызовов.

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

Оцените материал
Добавить комментарий

Капча загружается...