coreclr 소스코드로 알아보는 .NET 4.0의 모듈 로딩 함수
(그냥 기록 차원에서 남겨둡니다. ^^)
.NET 4.0 이후부터 DLL을 LoadLibrary로 로딩하지 않는다고 했습니다.
CLR 4.0 환경에서 DLL 모듈의 로드 주소(Base address) 알아내는 방법
; https://www.sysnet.pe.kr/2/0/11325
정말 그럴까요? ^^ 이를 알아보기 위해 coreclr 소스 코드를 이용해 추적해 봤습니다.
우선 단서는 GetHINSTANCE를 역어셈블한 코드에서 시작했습니다.
[SecurityCritical]
public static IntPtr GetHINSTANCE(Module m)
{
if (m == null)
{
throw new ArgumentNullException("m");
}
RuntimeModule internalModule = m as RuntimeModule;
// ...[생략]...
return GetHINSTANCE(internalModule.GetNativeHandle());
}
[SecurityCritical, SuppressUnmanagedCodeSecurity, SuppressUnmanagedCodeSecurity, DllImport("QCall", CharSet=CharSet.Unicode)]
private static extern IntPtr GetHINSTANCE(RuntimeModule m);
이제부터 coreclr 소스 코드 안으로 들어갑니다.
// https://github.com/dotnet/coreclr/blob/46ab1d132c9ad471d79afa20c188c2f9c85e5f20/src/vm/commodule.cpp
// coreclr / src / vm / commodule.cpp
HINSTANCE QCALLTYPE COMModule::GetHINSTANCE(QCall::ModuleHandle pModule)
{
// ...[생략]...
PEFile *pPEFile = pModule->GetFile();
if (!pPEFile->IsDynamic() && !pPEFile->IsResource())
{
hMod = (HMODULE) pModule->GetFile()->GetManagedFileContents();
}
// ...[생략]...
return (HINSTANCE)hMod;
}
보는 바와 같이 PEFile 인스턴스의 GetManagedFileContents 함수의 반환 결과로 나옵니다. 그렇다면 DLL은 PEFile에 의해 관리되는 것으로 짐작됩니다.
// https://github.com/dotnet/coreclr/blob/13e7c4368da664a8b50228b1a5ef01a660fbb2dd/src/vm/pefile.inl
// coreclr / src / vm / pefile.inl
#ifndef DACCESS_COMPILE
inline const void *PEFile::GetManagedFileContents(COUNT_T *pSize/*=NULL*/)
{
// ...[생략]...
LoadLibrary(FALSE);
// ...[생략]...
RETURN GetLoadedIL()->GetBase();
}
inline PTR_PEImageLayout PEFile::GetLoadedIL()
{
// ...[생략]...
return GetOpenedILimage()->GetLoadedLayout();
};
LoadLibrary가 눈에 띄는군요. 얼핏 Win32 API의 LoadLibrary가 호출된다고 생각할 수 있지만 유사한 역할을 하는 것에 불과합니다. LoadLibrary의 선언과 정의를 추적해 들어가면,
// https://github.com/dotnet/coreclr/blob/13e7c4368da664a8b50228b1a5ef01a660fbb2dd/src/vm/pefile.h
// coreclr / src / vm / pefile.h
class PEFile
{
// ...[생략]...
static PEFile *Open(PEImage *image);
PTR_PEImage m_identity;
void LoadLibrary(BOOL allowNativeSkip = TRUE);
PTR_PEImage GetILimage()
{
// ...[생략]...
#ifndef DACCESS_COMPILE
if (m_openedILimage == NULL && m_identity != NULL)
{
PEImage* pOpenedILimage;
m_identity->Clone(MDInternalImport_Default,&pOpenedILimage);
if (InterlockedCompareExchangeT(&m_openedILimage,pOpenedILimage,NULL) != NULL)
pOpenedILimage->Release();
}
#endif
return m_openedILimage;
}
PEImage *GetOpenedILimage()
{
// ...[생략]...
return m_openedILimage;
}
// ...[생략]...
};
LoadLibrary의 정의는 아래의 pefile.cpp에서 찾아볼 수 있는 데 결국 pefile.h에 정의된 GetILimage 함수를 다시 호출합니다. 따라서 GetILimage 함수의 내용에 따라 결국 이미 저장되어 있던 PTR_PEImage 타입의 m_identity에 DLL 정보가 관리됩니다. m_identity는 다시 아래의 pefile.cpp에 정의된 PEFile 생성자에서 할당되는데, 다시 PEFile 생성자가 호출되는 곳은 PEFile::Open static 함수입니다.
// https://github.com/dotnet/coreclr/blob/46ab1d132c9ad471d79afa20c188c2f9c85e5f20/src/vm/pefile.cpp
// coreclr/src/vm/pefile.cpp
void PEFile::LoadLibrary(BOOL allowNativeSkip/*=TRUE*/) // if allowNativeSkip==FALSE force IL image load
{
// ...[생략]...
if (GetILimage()->IsFile())
{
#ifdef PLATFORM_UNIX
if (GetILimage()->IsILOnly())
{
GetILimage()->Load();
}
else
#endif // PLATFORM_UNIX
{
GetILimage()->LoadFromMapped();
}
}
else
{
GetILimage()->LoadNoFile();
}
// ...[생략]...
}
PEFile::PEFile(PEImage *identity, BOOL fCheckAuthenticodeSignature/*=TRUE*/) :
// ...[생략]...
{
// ...[생략]...
if (identity)
{
identity->AddRef();
m_identity = identity;
if(identity->IsOpened())
{
//already opened, prepopulate
identity->AddRef();
m_openedILimage = identity;
}
}
// ...[생략]...
}
PEFile *PEFile::Open(PEImage *image)
{
// ...[생략]...
PEFile *pFile = new PEFile(image, FALSE);
// ...[생략]...
RETURN pFile;
}
그렇다면, 다시 DLL에 대한 관리가 내부적으로는 PEImage에서 된다는 것을 짐작게 합니다. 그래서 이번에는 PEImage 인스턴스의 생성을 찾아봤습니다.
// https://github.com/dotnet/coreclr/blob/ef8d1522eb15cb0371f31a9392891c942547a91f/src/vm/coreassemblyspec.cpp
// src / vm / coreassemblyspec.cpp
// 이 함수 외에도 coreclr / src / vm / compile.cpp 경로의 CEECompileInfo::LoadAssemblyByPath 함수에서도 PEImage::OpenImage를 사용
STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath,
PEImage **ppPEImage,
PEImage **ppNativeImage,
BOOL fExplicitBindToNativeImage)
{
// ...[생략]...
EX_TRY
{
PEImageHolder pImage = NULL;
PEImageHolder pNativeImage = NULL;
// ...[생략]...
{
pImage = PEImage::OpenImage(wszAssemblyPath, MDInternalImport_Default);
// Make sure that the IL image can be opened if the native image is not available.
hr=pImage->TryOpenFile();
// ...[생략]...
}
if (pImage)
*ppPEImage = pImage.Extract();
// ...[생략]...
}
// ...[생략]...
}
그런데, PEImageHolder는 뭘까요? 이에 대한 힌트는 peimage.h에서 찾을 수 있습니다.
// https://github.com/dotnet/coreclr/blob/31b5ee0bc7df328fd9187d25dbadebe7c1623d38/src/vm/peimage.h
// coreclr / src / vm / peimage.h
class PEImage
{
PTR_PEImageLayout m_pLayouts[IMAGE_COUNT];
// ...[생략]...
void Init(LPCWSTR pPath);
// ...[생략]...
};
FORCEINLINE void PEImageRelease(PEImage *i)
{
WRAPPER_NO_CONTRACT;
i->Release();
}
typedef Wrapper<PEImage *, DoNothing, PEImageRelease> PEImageHolder;
하지만 본론으로 돌아와서 ^^ PEImage::OpenImage의 코드를 보겠습니다.
// https://github.com/dotnet/coreclr/blob/ef8d1522eb15cb0371f31a9392891c942547a91f/src/vm/peimage.inl
// coreclr / src / vm / peimage.inl
inline PTR_PEImage PEImage::OpenImage(LPCWSTR pPath, MDInternalImportFlags flags /* = MDInternalImport_Default */)
{
// ...[생략]...
PEImageHolder pImage(new PEImage);
pImage->Init(pPath);
return dac_cast<PTR_PEImage>(pImage.Extract());
// ...[생략]...
}
inline void PEImage::Init(LPCWSTR pPath)
{
// ...[생략]...
m_path = pPath;
m_path.Normalize();
SetModuleFileNameHintForDAC();
}
inline PTR_PEImageLayout PEImage::GetLoadedLayout()
{
// ...[생략]...
return m_pLayouts[IMAGE_LOADED]; //no addref
}
new PEImage와 함께 Init 메서드를 호출하지만 PEImage::Init은 별다르게 로딩 코드가 없습니다. 다시 BinderAcquirePEImage 함수로 가면 pImage 인스턴스에 대해 TryOpenFile을 호출하는데 아마도 여기서 로딩할 것으로 짐작됩니다.
// https://github.com/dotnet/coreclr/blob/31b5ee0bc7df328fd9187d25dbadebe7c1623d38/src/vm/peimage.cpp
// coreclr / src / vm / peimage.cpp
HRESULT PEImage::TryOpenFile()
{
// ...[생략]...
if (m_hFile!=INVALID_HANDLE_VALUE)
return S_OK;
{
ErrorModeHolder mode(SEM_NOOPENFILEERRORBOX|SEM_FAILCRITICALERRORS);
m_hFile=WszCreateFile((LPCWSTR) m_path,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
}
// ...[생략]...
}
PTR_PEImage PEImage::LoadImage(HMODULE hMod)
{
// ...[생략]...
StackSString path;
GetPathFromDll(hMod, path);
PEImageHolder pImage(PEImage::OpenImage(path,(MDInternalImportFlags)(MDInternalImport_CheckLongPath|MDInternalImport_CheckShortPath)));
if (pImage->HasLoadedLayout())
RETURN dac_cast<PTR_PEImage>(pImage.Extract());
SimpleWriteLockHolder lock(pImage->m_pLayoutLock);
if(pImage->m_pLayouts[IMAGE_LOADED]==NULL)
pImage->SetLayout(IMAGE_LOADED,PEImageLayout::CreateFromHMODULE(hMod,pImage,WszGetModuleHandle(NULL)!=hMod));
// ...[생략]...
RETURN dac_cast<PTR_PEImage>(pImage.Extract());
}
WszCreateFile은 결국 내부적으로 CreateFileW를 호출하게 됩니다. 확인 끝났군요. ^^
// https://github.com/dotnet/coreclr/blob/c440335be80ee0762856d0be6e91ec3ea2f90504/src/inc/winwrap.h
// coreclr / src / inc / winwrap.h
#define WszCreateFile CreateFileWrapper
// https://github.com/dotnet/coreclr/blob/52a816d3011f4d03b80b5940dc036b40d701f52d/src/utilcode/longfilepathwrappers.cpp
// coreclr / src / utilcode / longfilepathwrappers.cpp
HANDLE
CreateFileWrapper(
_In_ LPCWSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
)
{
// ...[생략]...
ret = CreateFileW(path.GetUnicode(),
dwDesiredAccess,
dwShareMode,
lpSecurityAttributes,
dwCreationDisposition,
dwFlagsAndAttributes,
hTemplateFile);
// ...[생략]...
return ret;
}
중간에 잠시 살펴봤던 PEImageHolder에 대해 살펴보겠습니다.
typedef Wrapper<PEImage *, DoNothing, PEImageRelease> PEImageHolder;
Wrapper는 단순히 PEImageRelease 인자로 객체 해제 방법을 아는 스마트 포인터 관리 클래스의 일종이라고 보시면 됩니다. 나머지 정의는 다음의 파일에서 각각 찾을 수 있습니다.
// https://github.com/dotnet/coreclr/blob/0ad57d9177ac88aabbf9354b985c5ac192a307e7/src/inc/holder.h
template <typename TYPE, typename BASE,
UINT_PTR DEFAULTVALUE = 0, BOOL IS_NULL(TYPE, TYPE) = CompareDefault<TYPE>, HolderStackValidation VALIDATION_TYPE = HSV_ValidateNormalStackReq>
class BaseWrapper : public BaseHolder<TYPE, BASE, DEFAULTVALUE, IS_NULL, VALIDATION_TYPE>
{
// ...[생략]...
};
template
<
typename TYPE,
void (*ACQUIREF)(TYPE),
void (*RELEASEF)(TYPE),
UINT_PTR DEFAULTVALUE = 0,
BOOL IS_NULL(TYPE, TYPE) = CompareDefault<TYPE>,
HolderStackValidation VALIDATION_TYPE = HSV_ValidateNormalStackReq,
// For legacy compat (see EEJitManager::WriterLockHolder), where default ctor
// causes ACQUIREF(DEFAULTVALUE), but ACQUIREF ignores the argument and
// operates on static or global value instead.
bool DEFAULT_CTOR_ACQUIRE = true
>
class Wrapper : public BaseWrapper<TYPE, FunctionBase<TYPE, ACQUIREF, RELEASEF, VALIDATION_TYPE>,
DEFAULTVALUE, IS_NULL, VALIDATION_TYPE>
{
typedef BaseWrapper<TYPE, FunctionBase<TYPE, ACQUIREF, RELEASEF, VALIDATION_TYPE>,
DEFAULTVALUE, IS_NULL, VALIDATION_TYPE> BaseT;
// ...[생략]...
}; // Wrapper<>
template
<
typename TYPE,
typename BASE,
UINT_PTR DEFAULTVALUE = 0,
BOOL IS_NULL(TYPE, TYPE) = CompareDefault<TYPE>,
HolderStackValidation VALIDATION_TYPE = HSV_ValidateNormalStackReq
>
class BaseHolder : protected BASE
{
// ...[생략]...
FORCEINLINE TYPE Extract()
{
STATIC_CONTRACT_WRAPPER;
SuppressRelease();
return GetValue();
}
FORCEINLINE TYPE GetValue()
{
STATIC_CONTRACT_LEAF;
return this->m_value;
}
};
알아본 김에 모듈의 로딩 주소를 반환하는 과정에서 GetLoadedIL()이 반환하는 PEImage의 GetLoadedLayout 함수의 layout 관련 코드도 살펴봤습니다. 우선 상속 구조는 이렇고,
// https://github.com/dotnet/coreclr/blob/0a8bd5554a736de337ca6c24eba3de3ec0decfd1/src/vm/peimagelayout.h
// coreclr / src / vm / peimagelayout.h
class PEImageLayout : public PEDecoder
{
// ...[생략]...
};
class RawImageLayout: public PEImageLayout
{
// ...[생략]...
};
CreateFromHMODULE에 의해 HMODULE hModule 값을 생성자에서 받아둡니다.
// https://github.com/dotnet/coreclr/blob/0a8bd5554a736de337ca6c24eba3de3ec0decfd1/src/vm/peimagelayout.cpp
// coreclr / src / vm / peimagelayout.cpp
PEImageLayout* PEImageLayout::CreateFromHMODULE(HMODULE hModule,PEImage* pOwner, BOOL bTakeOwnership)
{
// ...[생략]...
return new RawImageLayout(hModule,pOwner,bTakeOwnership,TRUE);
}
맨 처음으로 돌아가 load address를 반환하는 GetManagedFileContents 메서드가 결국 PEDecoder의 GetBase를 호출하는데, 구현은 HMODULE hModule 값을 담고 있는 m_base를 반환하는 것에 불과합니다.
// https://github.com/dotnet/coreclr/blob/master/src/inc/pedecoder.h
// coreclr / src / inc / pedecoder.h
// https://github.com/dotnet/coreclr/blob/master/src/inc/pedecoder.inl
// coreclr / src / inc / pedecoder.inl
inline PTR_VOID PEDecoder::GetBase() const
{
// ...[생략]...
return PTR_VOID(m_base);
}
inline PEDecoder::PEDecoder(PTR_VOID mappedBase, bool fixedUp /*= FALSE*/)
: m_base(dac_cast<TADDR>(mappedBase)),
// ...[생략]...
{
// ...[생략]...
}
inline void PEDecoder::Init(void *flatBase, COUNT_T size)
{
// ...[생략]...
m_base = (TADDR)flatBase;
m_size = size;
m_flags = FLAG_CONTENTS;
}
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]