在运行时获取DLL路径

80

我希望能够从dll代码中获取其目录(或文件)路径(而不是程序的.exe文件路径)。

我已尝试过几种方法:
GetCurrentDir - 获取当前目录路径。
GetModuleFileName - 获取可执行文件的路径。

那么如何找出代码所在的dll呢?
我正在寻找类似于C#的Assembly.GetExecutingAssembly的东西。

11个回答

134
你可以使用 GetModuleHandleEx 函数获取 DLL 中静态函数的句柄。你可以在这里找到更多信息:here
之后,你可以使用 GetModuleFileName 从刚获得的句柄中取得路径。有关该调用的更多信息,请参见此处
一个完整的示例:
char path[MAX_PATH];
HMODULE hm = NULL;

if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | 
        GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
        (LPCSTR) &functionInThisDll, &hm) == 0)
{
    int ret = GetLastError();
    fprintf(stderr, "GetModuleHandle failed, error = %d\n", ret);
    // Return or however you want to handle an error.
}
if (GetModuleFileName(hm, path, sizeof(path)) == 0)
{
    int ret = GetLastError();
    fprintf(stderr, "GetModuleFileName failed, error = %d\n", ret);
    // Return or however you want to handle an error.
}

// The path variable should now contain the full filepath for this DLL.

2
有趣的是,在存在 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 标志的情况下,lpModuleName 可能不仅仅是本地函数。它也可能是 (本地)静态变量 的地址。 - Janusz Lenar
1
+1;非常好的答案(我刚刚使用了它);比被接受的答案更优秀。 - Bathsheba
3
PathRemoveFileSpec 也是你的朋友。它可以帮助你删除文件路径中的文件名部分。 - zar
我同意 - 使用GetModuleHandleEx和GetModuleFileName似乎是回答这个问题的最佳方法。我刚刚创建了一个gist,https://gist.github.com/pwm1234/05280cf2e462853e183d,将mkaes的代码打包成一个独立的函数get_module_path,并展示了如何从任何地方调用它。 - Phil
1
在 Windows 10 64 位上的 win32 dll 上对我有效, 但我也必须使用 wchar_t 而不是 char, 并且使用 wchar_t full_file_path[MAX_PATH]; 和将其转换为 (LPCWSTR) &functionInThisDll 而不是 (LPCSTR) &functionInThisDll。 - Noctiluque
显示剩余4条评论

45
EXTERN_C IMAGE_DOS_HEADER __ImageBase;

....

TCHAR   DllPath[MAX_PATH] = {0};
GetModuleFileName((HINSTANCE)&__ImageBase, DllPath, _countof(DllPath));

4
__ImageBase 变量的未来兼容性有何评论? - Ajay
1
ImageBase是IMAGE_DOS_HEADER类型的变量,它在PE(可移植可执行文件格式)中首先出现。它是一个Windows结构,在Windows下才能使用。我认为它是安全的,并且将来不会改变。另一种选择是GetModuleHandle,但需要DLL名称。 - cprogrammer
1
它在微软示例代码中使用,因此应该是安全的。例如,请参见http://msdn.microsoft.com/en-us/library/windows/desktop/dd742738(v=vs.85).aspx - Harry Johnston
为什么不使用GetModuleHandle而是使用__ImageBase? - Agnel Kurian
2
为什么不直接使用DllMain中获取的内容,而要使用__ImageBase呢? - Pyjong
显示剩余6条评论

24

GetModuleFileName() 在 DLL 的代码内部运行良好。 只需确保不要将第一个参数设置为 NULL,因为这会获取调用进程的文件名。 您需要指定 DLL 的实际模块实例。 您可以在 DLL 的 DllEntryPoint() 函数中获取该实例作为输入参数,只需将其保存到某个变量中以备将来使用。


2
GetModuleHandle会给你与DllEntryMain的输入参数相同的句柄。 - Agnel Kurian
这似乎是最简单的答案,而且它也有效。顺便说一下,Agnel指的是DllMain(),而不是DllEntryMain()。 - Ruud van Gaal

5

这是一份Unicode修订版的最受欢迎的答案:

CStringW thisDllDirPath()
{
    CStringW thisPath = L"";
    WCHAR path[MAX_PATH];
    HMODULE hm;
    if( GetModuleHandleExW( GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | 
                            GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                            (LPWSTR) &thisDllDirPath, &hm ) )
    {
        GetModuleFileNameW( hm, path, MAX_PATH );
        PathRemoveFileSpecW( path );
        thisPath = CStringW( path );
        if( !thisPath.IsEmpty() && 
            thisPath.GetAt( thisPath.GetLength()-1 ) != '\\' ) 
            thisPath += L"\\";
    }
    else if( _DEBUG ) std::wcout << L"GetModuleHandle Error: " << GetLastError() << std::endl;
    
    if( _DEBUG ) std::wcout << L"thisDllDirPath: [" << CStringW::PCXSTR( thisPath ) << L"]" << std::endl;       
    return thisPath;
}

3
只要您实现了以下dll入口点(通常为dllmain.cpp):
BOOL APIENTRY DllMain( HMODULE hModule,
                   DWORD  ul_reason_for_call,
                   LPVOID lpReserved
                 )

您可以简单地执行以下操作:
switch (ul_reason_for_call)
{ 
case DLL_PROCESS_ATTACH:
{
    TCHAR dllFilePath[512 + 1] = { 0 };
    GetModuleFileNameA(hModule, dllFilePath, 512)
}
    break;
case DLL_THREAD_ATTACH: break;
...

dllFilePath将包含当前dll代码加载的路径。在这种情况下,hModule由加载dll的进程传递。


2
我认为Remy Lebau的回答是最好的,但像其他所有回答一样,它缺少呈现DLL目录的内容。我引用原始问题:“我想从其代码内部获取dll的目录(或文件)路径。(不是程序的.exe文件路径)。”
正如Remy和Jean-Marc Volle指出的那样,通常包含在dllmain.cpp中的DLL入口函数DllMain提供了对DLL的句柄。这个句柄通常是必要的,所以它将保存在全局变量hMod中。我还添加了std::wstring类型的变量来存储DLL的完整限定名和父路径。
HMODULE hMod;
std::wstring PathAndName;
std::wstring OnlyPath;
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
  switch (ul_reason_for_call)
  {
    case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH:
       case DLL_PROCESS_DETACH:
     break;
  }
  hMod = hModule;
  const int BUFSIZE = 4096;
  wchar_t buffer[BUFSIZE];
  if (::GetModuleFileNameW(hMod, buffer, BUFSIZE - 1) <= 0)
  {
    return TRUE;
  }

  PathAndName = buffer;

  size_t found = PathAndName.find_last_of(L"/\\");
  OnlyPath = PathAndName.substr(0, found);

  return TRUE;
}

这些全局变量可在DLL内部使用。最初的回答中提到,在动态链接库(DLL)中可以使用这些全局变量。

1

1
对于Delphi用户:
SysUtils.GetModuleName(hInstance);              //Works; hInstance is a special global variable
SysUtils.GetModuleName(0);                      //Fails; returns the name of the host exe process
SysUtils.GetModuleName(GetModuleFilename(nil)); //Fails; returns the name of the host exe process

如果你的Delphi没有SysUtils.GetModuleName,它可以声明为:

function GetModuleName(Module: HMODULE): string;
var
   modName: array[0..32767] of Char; //MAX_PATH is for a single filename; paths can be up to 32767 in NTFS - or longer.
begin
   {
      Retrieves the fully qualified path for the file that contains the specified module. 
      The module must have been loaded by the current process.
   }
   SetString(Result, modName, GetModuleFileName(Module, modName, Length(modName)));
end;

0

我(就我的情况而言)正在编写那些揭示了易于使用的 DllMain 方法的人。 任务是查找当前运行在 dll 中的线程的机器位数。为了绝对确定,我还想要运行中的 dll 和 exe 的名称。调用者的名称通过 NULL 参数找到:

char exePat[MAX_PATH] = { 0 }; GetModuleFileName(NULL, exePat, MAX_PATH);

使用 DllMain 的 hModule 感觉是找到 dll 名称和机器位数的最完整和可靠的方式。代码被发现,以便仅需要获取“机器”模块即可。 这是带出其名称以供调试目的的部分。

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  reason,LPVOID lpReserved ) {

     char  path[MAX_PATH] = { 0 };
     GetModuleFileNameA(hModule, path, MAX_PATH);
     PIMAGE_DOS_HEADER startPoint = (PIMAGE_DOS_HEADER)hModule;
     PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)startPoint + startPoint->e_lfanew);
     PIMAGE_FILE_HEADER fileHeader = &pNtHeaders->FileHeader;
     short machine = fileHeader->Machine;
     ////0x8664 is 64bit  0x14c is 32bit.
     int stop = 0;
     return TRUE;
 }

0

我想要实现类似的功能,但是希望将相似的函数制作成一个.dll文件 - 但是这样你就不能使用__ImageBase,因为它是特定于包含函数的那个.dll文件的。我甚至尝试过覆盖方法。

GetDllPath( HMODULE hDll = (HMODULE) __ImageBase)

但那也没有起作用。(由于某种原因在此之后返回应用程序路径。)

然后我想到了一个办法——为什么不使用VirtualQuery,使用函数指针并从中获取HMODULE。但是再次出现了问题——如何获取调用者的函数指针?

现在回到调用堆栈的确定上——我不会打扰您处理所有肮脏的细节,只需跟随所引用链接的链接即可。

以下是整个代码快照:

//
//  Originated from: https://sourceforge.net/projects/diagnostic/
//
//  Similar to windows API function, captures N frames of current call stack.
//  Unlike windows API function, works with managed and native functions.
//
int CaptureStackBackTrace2( 
    int FramesToSkip,                   //[in] frames to skip, 0 - capture everything.
    int nFrames,                        //[in] frames to capture.
    PVOID* BackTrace                    //[out] filled callstack with total size nFrames - FramesToSkip
)
{
#ifdef _WIN64
    CONTEXT ContextRecord;
    RtlCaptureContext(&ContextRecord);

    UINT iFrame;
    for (iFrame = 0; iFrame < (UINT)nFrames; iFrame++)
    {
        DWORD64 ImageBase;
        PRUNTIME_FUNCTION pFunctionEntry = RtlLookupFunctionEntry(ContextRecord.Rip, &ImageBase, NULL);

        if (pFunctionEntry == NULL)
        {
            if (iFrame != -1)
                iFrame--;           // Eat last as it's not valid.
            break;
        }

        PVOID HandlerData;
        DWORD64 EstablisherFrame;
        RtlVirtualUnwind(0 /*UNW_FLAG_NHANDLER*/,
            ImageBase,
            ContextRecord.Rip,
            pFunctionEntry,
            &ContextRecord,
            &HandlerData,
            &EstablisherFrame,
            NULL);

        if(FramesToSkip > (int)iFrame)
            continue;

        BackTrace[iFrame - FramesToSkip] = (PVOID)ContextRecord.Rip;
    }
#else
    //
    //  This approach was taken from StackInfoManager.cpp / FillStackInfo
    //  http://www.codeproject.com/Articles/11221/Easy-Detection-of-Memory-Leaks
    //  - slightly simplified the function itself.
    //
    int regEBP;
    __asm mov regEBP, ebp;

    long *pFrame = (long*)regEBP;               // pointer to current function frame
    void* pNextInstruction;
    int iFrame = 0;

    //
    // Using __try/_catch is faster than using ReadProcessMemory or VirtualProtect.
    // We return whatever frames we have collected so far after exception was encountered.
    //
    __try {
        for (; iFrame < nFrames; iFrame++)
        {
            pNextInstruction = (void*)(*(pFrame + 1));

            if (!pNextInstruction)     // Last frame
                break;

            if (FramesToSkip > iFrame)
                continue;

            BackTrace[iFrame - FramesToSkip] = pNextInstruction;
            pFrame = (long*)(*pFrame);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

#endif //_WIN64
    iFrame -= FramesToSkip;
    if(iFrame < 0)
        iFrame = 0;

    return iFrame;
} //CaptureStackBackTrace2



//
//  Gets .dll full path or only directory.
//
CStringW GetDllPath( bool bPathOnly /* = false */ )
{
    void* pfunc = &GetDllPath;
    wchar_t path[MAX_PATH] = { 0 };
    MEMORY_BASIC_INFORMATION info;
    HMODULE hdll;

    CaptureStackBackTrace2(1, 2, &pfunc);

    // Get the base address of the module that holds the current function
    VirtualQuery(pfunc, &info, sizeof(MEMORY_BASIC_INFORMATION));

    // MEMORY_BASIC_INFORMATION::AllocationBase corresponds to HMODULE
    hdll = (HMODULE)info.AllocationBase;

    // Get the dll filename
    if ( !GetModuleFileName( hdll, path, MAX_PATH ) )
        return L"";

    if ( bPathOnly )
    {
        wchar_t* p = wcsrchr( path, '\\' );
        if ( p )
            *p = 0;
    }

    return path;
} //GetDllPath

除了容易出错的stacktrace函数之外,您在GetDllPath()中返回本地数组'path',因此在返回后被销毁。这将导致崩溃。 只需使用传递给DllMain()的DLL句柄调用GetModuleFileName()即可在3行代码中完成。 - Ruud van Gaal
仅在您想将GetDllPath放入共享dll并从其他dll中使用它时,才使用我的代码示例。更简单的方法是为每个dll分别编写GetDllPath,但是这样会有多个相同函数的副本或者从多个dll中包含相同的源代码 - 当然,这也是一种解决方案。 - TarmoPikaro
但是'path'是在堆栈上创建的,一旦GetDllPath()返回,这个变量就会消失。因此使用该字符串肯定会崩溃,对吧? - Ruud van Gaal
1
函数本身会为调用者创建字符串的副本。理论上不应该崩溃。 - TarmoPikaro
我看到了'wchar_t path',所以它在堆栈上。然后是'return path',它返回一个指向堆栈项的指针。但实际上,我看到一个隐式的CStringW作为返回类型,这可能会进行复制。对于造成的困惑,我很抱歉。 - Ruud van Gaal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接