Вступление


Доброго времени суток. Знакомо ли вам исключение 0xc00007b? С момента перевода движка X-Ray под x64 приходило очень много репортов о проблеме 0cx00007b. В 90% случаев, это была проблема с отсутствием 64 битного драйвера OpenAL.


Идеи по решению проблемы


Первое время мы постоянно отвечали, что нужно установить драйвер, спустя пару месяцев написали FAQ по запуску и вероятным проблемам. Но подобные репорты не уходили, народ у нас в СНГ читать не особо любит, поэтому мы решили решить проблему радикально: устанавливать драйвер из-под движка, если таковой отсутствует.


Шаг 1: запуск программы, когда не хватает dll


Самый простой способ подключения библиотек друг к другу — компоновка(pragma comment), но в нашем случае так делать нельзя.


Итак, шаг 1: явная линковка или привет extern "C".


Что нам требуется: отвязать exe от библиотек движка. Делается это следующим способом:


1) Выносим функцию запуска движка в динамическую библиотеку:


extern "C" --// LoadLibrary, если мне не изменяет память, Сишное API
{
   void ENGINE_API RunApplication(LPCSTR commandLine)
   {
         ... // Your code
   }
}

2) Вызываем функцию из нашего exe:


using RunFunc = void(__cdecl*)(const char*); // Объявляем удобный для себя тип функции 
bool OpenALFound = false; // Оно нам потом пригодится

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    if(OpenALFound)
    {
        HMODULE hLib = LoadLibrary("xrEngine.dll"); // Получаем модуль с нашей функцией
        IsRunFunc RunFunc = (IsRunFunc)GetProcAddress(hLib, "RunApplication"); // Получаем функцию из модуля
        RunFunc(params); // Запускаем движок
    }

    return 0;
}

Шаг 2: Проверка драйвера


Ну, тут всё просто, получаем системный(может кто-то удивится, но OS не всегда на C:) раздел и проверяем dll в папке с драйверами:


/// R_ASSERT - внутренний макросс, проверяющий значение первого аргумента на истину, если лож - программа выведет сообщение об ошибке
... // WinMain code
TCHAR szOpenALDir[MAX_PATH] = { 0 }; // Получаем системный патч
R_ASSERT(GetSystemDirectory(szOpenALDir, MAX_PATH * sizeof(TCHAR)));

#ifndef UNICODE 
    _snprintf_s(szOpenALDir, MAX_PATH * sizeof(CHAR), "%s%s", szOpenALDir, "\\OpenAL32.dll");
#else
    _snwprintf_s(szOpenALDir, MAX_PATH * sizeof(WCHAR), L"%s%s", szOpenALDir, L"\\OpenAL32.dll");
#endif
DWORD dwOpenALInstalled = GetFileAttributes(szOpenALDir);

// Атрибуты валидные, значит драйвер на месте. Всё в порядке, запускаем.
if (dwOpenALInstalled != INVALID_FILE_ATTRIBUTES)
{
    OpenALFound = true;
}

Шаг 3: Файл не найден


Первым делом нам нужно попросить права администратора у пользователя, т.к. работать придётся с системным каталогом:


// Проверяем, запущенны ли мы от администратора
bool IsProcessWithAdminPrivilege()
{
    SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
    LPVOID pAdministratorsGroup = nullptr;
    BOOL bRet = FALSE;

    // init SID to control privileges
    AllocateAndInitializeSid(&NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pAdministratorsGroup);

    // ckeck membership
    CheckTokenMembership(nullptr, pAdministratorsGroup, &bRet);

    // clean pointer
    if (pAdministratorsGroup) 
    { 
        FreeSid(pAdministratorsGroup); 
        pAdministratorsGroup = nullptr; 
    }

    return !!bRet;
}

{
...// WinMain code
    // Если нет прав администратора, попросим их и перезапустимся 
    if (!IsProcessWithAdminPrivilege())
    {
        TCHAR szPathToLib[MAX_PATH] = { 0 };
        GetModuleFileName(nullptr, szPathToLib, ARRAYSIZE(szPathToLib));

        SHELLEXECUTEINFO shellInfo = { sizeof(SHELLEXECUTEINFO) };
        shellInfo.lpVerb = TEXT("runas");
        shellInfo.lpFile = szPathToLib;
        shellInfo.hwnd = nullptr;
        shellInfo.nShow = SW_NORMAL;

        if (ShellExecuteEx(&shellInfo)) 
            ExitProcess(GetCurrentProcessId());
    }
}

Этап второй: копируем библиотеку в систему


{
...
// WinMain code
    TCHAR szPath[MAX_PATH] = { 0 };
    // Для примера беру путь от исполняемого файла
    GetModuleFileName(GetModuleHandle(nullptr), szPath, MAX_PATH);
    PathRemoveFileSpec(szPath);

#ifndef UNICODE 
    _snprintf_s(szPath, MAX_PATH * sizeof(CHAR), "%s%s", szPath, "\\OpenAL32.dll");
#else
    _snwprintf_s(szPath, MAX_PATH * sizeof(WCHAR), L"%s%s", szPath, L"\\OpenAL32.dll");
#endif

    dwOpenALInstalled = GetFileAttributes(szPath);
    if (dwOpenALInstalled != INVALID_FILE_ATTRIBUTES) // Файл найдет, идём дальше
    {
        DWORD LibrarySize = 0;
        HANDLE hFile = CreateFile(szPath, GENERIC_READ, NULL, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
        R_ASSERT(hFile != INVALID_HANDLE_VALUE);

        FILE_STANDARD_INFO fileInfo = { 0 };
        GetFileInformationByHandleEx(hFile, FileStandardInfo, &fileInfo, sizeof(fileInfo));

        LPVOID pImage = HeapAlloc(GetProcessHeap(), 0, fileInfo.EndOfFile.QuadPart);
        ReadFile(hFile, pImage, fileInfo.EndOfFile.QuadPart, &LibrarySize, nullptr);

        CloseHandle(hFile);

        hFile = CreateFile(szOpenALDir, GENERIC_WRITE, NULL, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
        R_ASSERT(hFile != INVALID_HANDLE_VALUE);

        WriteFile(hFile, pImage, fileInfo.EndOfFile.QuadPart, &LibrarySize, nullptr);

        HeapFree(GetProcessHeap(), 0, pImage);
        CloseHandle(hFile);

        OpenALFound = true; // Говорим, что всё в порядке, можно запускать движок
    }
}

Вывод


Конечно, способ весьма забавный, но для подобных проектов подойдёт. Всем удачи!

Комментарии (19)


  1. dmxvlx
    03.12.2018 03:43
    +1

    Зачем мешать NULL и nullptr? Используйте что-нибудь одно.

    PS: nullptr был ведён в с++11 ))


    1. mapron
      03.12.2018 04:46

      HANDLE CreateFileA(
        LPCSTR                lpFileName,
        DWORD                 dwDesiredAccess,
        DWORD                 dwShareMode,
        LPSECURITY_ATTRIBUTES lpSecurityAttributes,
        DWORD                 dwCreationDisposition,
        DWORD                 dwFlagsAndAttributes,
        HANDLE                hTemplateFile
      );
      


       HANDLE hFile = CreateFile(szPath, GENERIC_READ, NULL, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
      

      Хотите, поведаю, почему так?
      NULL, как известно это дефайн для 0. Автор где-то нашел кусок [г]кода, где 3 и 4 параметры передавались как NULL. Ну с винапи древним кодом такое встречается повсеместно.
      Потом как порядочный человек, думает «ну блин, на дворе 17 год, адекватные люди давно nullptr используют». И заменяет NULL везде на nullptr. Но к его сожалению, код не компилится (алилуйя, ради этого nullptr и придумали). Автор возвращает NULL в тех местах, где не собралось, в итоге имеем, что имеем)


      1. dmxvlx
        03.12.2018 05:03

        Я не спец по WinAPI, но на месте автора, я бы не пользовал NULL для типа DWORD ))


        1. aleaksah
          03.12.2018 12:51
          -1

          А почему нет? В документации WinAPI везде предлагают использовать NULL, потому что он взаимозаменяем с нулем, но добавляет наглядность в духе «тут ничего нет».


          1. SmallSnowball
            03.12.2018 17:12

            Ну тут спорный вопрос, обычно нужно смотреть документацию к конкретной функции, бывает, что для семантики «тут ничего нет» нужно указывать не NULL, а вообще какой-нибудь INVALID_HANDLE_VALUE, который даже нулю не равен.
            Вот, например, ярчайший пример:
            docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createfilemappinga
            Функция на вход принимает handle, указатели и dwordы, возвращает тоже handle. При этом на входе просят передавать не NULL, а INVALID_HANDLE_VALUE в семантике, когда хэндла нет вообще. А на выходе при ошибке для того же типа возвращается NULL. Где здесь логика, я так и не понял. При этом в качестве дефолтного значения для флагов указывается конкретно целочисленный ноль, а не NULL. Легаси во всей красе, как по мне


            1. ForserX Автор
              03.12.2018 17:12

              Твоя правда, мой косяк. Прошу прощения.


            1. dmxvlx
              03.12.2018 17:55

              Мне так всё это видится:
              NULL всё таки больше подходит для указателей, так же как 0 для целочисленных типов. DWORD — это двойное слово, слово — 16бит(2байта) — имеем 32битное целое число, для которого лучше пользовать 0, нежели NULL.


              Опять же, если упорно доказывать самому себе и окружающим, что NULL можно использовать везде в том числе и для целочисленных типов, то вас (не имею ввиду ИМЕННО ВАС) никто не будет закидывать гнилыми помидорами, но взгляните на этот код, и скажите, радует ли он ваш глаз/привычно ли оно выглядит:


              int count = NULL;

              Случай же с INVALID_HANDLE_VALUE на входе в CreateFileMappingA, скорее означает инициализацию, то есть первоначальное состояние; при возврате NULL — возврат есть ничто ( зачастую в Си такая практика: если указатель нулевой при возврате, — проверяй errno)


  1. mapron
    03.12.2018 04:55

    Можете объяснить одну вещь — разве нахождения OpenAL*.dll в директории с исполняемыми файлами недостаточно? Зачем ее вообще необходимо в системную директорию-то копировать?
    Т.е. почему нельзя сделать что-то вроде как в Qt сделано с opengl реализацией:
    1) проверяем, есть ли реализация в системе
    2) если ее нет или она нас не устраивает (LoadLibrary зафейлилась) — переименовываем OpenAL_dist.dll в OpenAL.dll в директории с программой. (ну это если мы в Roaming/пользовательской директории. Если нет, то да, надо права запросить — но один черт мы в систему не срем)

    Что не так с предложенной идеей?


    1. mayorovp
      03.12.2018 10:01
      +1

      Вместо переименования не лучше ли положить в отдельную папку, а потом оттуда загружать через LoadLibrary с указанием полного пути?


    1. ForserX Автор
      03.12.2018 11:05

      Вообще, у OpenAL там не только 1 dll. Тут код для примера местами был.


    1. Ezhyg
      03.12.2018 14:46

      И именно так очень часто делают с библиотеками MSVCP (штук по 5 бывает сразу лежит), правда, похоже, что многие и не проверяют, есть ли они в системе, но это уже другой вопрос :).
      А с OpenAL, если не путаю, так игры поступают, через одну.


  1. Evengard
    03.12.2018 05:29

    О, способ определить запущены ли мы с админ правами. Спасибо.


  1. monah_tuk
    03.12.2018 09:30

    Уже хотел было спросить: как в драйвера теперь подписываете… А тут...


  1. gecube
    03.12.2018 09:51

    Вообще я очень удивлен
    По драйвером я понимаю sys-файлы, которые имплементируют работу с каким-либо устройством
    А здесь в статье речь про OpenAL.
    Налицо — смешение понятий и их неконсистентное использование.
    Ещё присоединюсь к оратору выше: в моих разработках хватало наличия разделяемых библиотек в каталоге приложения и их было необязательно закидывать в системный каталог (более того: это является плохим паттернов)


  1. Endeavour
    03.12.2018 11:32
    +2

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

    А получил «как сделать dll hell» и отказ от использования стандартного порядка поиска библиотек.


  1. qw1
    03.12.2018 11:41

    Зачем так сложно (CreateFile/GetFileInformationByHandleEx/HeapAlloc/ReadFile/WriteFile/HeapFree/CloseHandle), да ещё без обработки всех возможных ошибок, когда есть CopyFile?


    1. ForserX Автор
      03.12.2018 12:08

      Код выдран из контекста. Это не есть туториал, а, скорее, краткая выжимка.


  1. IGrunelf
    03.12.2018 12:08
    +1

    В заголовке опечатка, д.б. "0xc00007b", HEX-код ошибки же.


    2gecube
    Есть ещё "User Mode" драйвера, выглядят обычными dll.
    Например, драйвер на звуковую карту стоит от производителя, а звук хочется выводить по-своему, используя, например, системный API или сторонний движок.


    1. gecube
      03.12.2018 17:07

      Да, я понимаю, реальность опять отличается от моих ожиданий… Но автор мог хотя бы сгладить это впечатление сразу разъяснив про что идёт речь.