http://sysnet.pe.kr/221117458825
(그냥 기록 차원에서 남겨둡니다. ^^)
.NET 4.0 이후부터 DLL을 LoadLibrary로 로딩하지 않는다고 했습니다.
CLR 4.0 환경에서 DLL 모듈의 로드 주소(Base address) 알아내는 방법 ; http://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; }