Tutorial 17: Dynamic Link Libraries
이 번장에서는, DLL이 무엇이며, 어떻게 생성할 수 있을는지를 설명한다.
DLL 소스 | 정의 파일 |
DLL 실행 소스 1 | 실행 결과 1 |
DLL 실행 소스 2 | 실행 결과 2 |
|
프로그래밍경험이 많을수록 느끼는 점이지만, 대부분 같은 처리를 하는 코드가 필요하게 된다. 같은처리를 그때마다 생성해서 사용한다면 효율성이 떯어지게 된다. DOS 시절의 프로그래머들은 이런 처리를 정적 링크 라이브러리(LIB)로 해결하고 있었다. 공통 부분의 함수를 사용하고 싶다면, 링크시에 LIB로부터 해당함수 부분을 링커로 부터 추출받아서, 생성하는 실행 파일에 포함 시킨다. 이런 방법을 「정적 링크」라고 부른다. C의 런타임 라이브러리가 좋은 예이다.
다만, 이 방법의 단점은, 동일한 코드가 각각의 실행 파일에 존재하므로, 디스크공간의 낭비가 발생한다는 것이다. 그러나 DOS 프로그램에서는, (싱글 태스크 OS이므로) 메모리에서 동작되는 프로그램은 하나만 존재하기 때문에, 메모리를 쓸데없이 사용하는 경우는 거의 없기때문에, 이 방법도 사용이 가능했었다.
그런데 Windows에서는 멀티태스킹 OS이므로 상황이 바뀌어서, 많은 프로그램을 실행시킨다면 메모리를 소모해 버리게 된다. 그래서,동적 링크 라이브러리(DLL)를 사용하는 방법으로 이 문제를 해결한다. DLL이 함수의 모임이라는 것은 LIB와 같지만, DLL함수를 사용하는 프로그램이 몇개가 있다해도, 실행코드는 실제로는 한개만 메모리에 로드된다는 것이 차이점이다. 이는 효율적으로 메모리를 사용하게 되는 장점을 가지고 있다.
좀더 자세히 설명하면, 같은 DLL을 사용하는 프로세스는 모두, DLL의 복사본을 자신의 프로세스의 메모리 공간 으로 인식해서 사용하게 된다. 그렇기 때문에, 메모리에 DLL을 여러개 복사해서 사용하고 있는것 같지만 실제로는, Windows가 페이징이라는 기술을 사용해서, 모든 프로그램은 같은 DLL을 공유하고 있는 것이다. 그렇기 때문에, 물리적인 메모리에서 실제 DLL의 복사본은 한개뿐인 것이다. 각각의 프로세스마다 DLL을 사용할 때에는 필요한 데이터 섹션이 있다.
이전방식의 LIB와는 달라서, DLL은 실행시에 링크를 수행하므로, 동적링크 프로그램 라이브러리라고 불린다. 필요할때 DLL을 읽어서 사용하고, 사용하지 않으면 해제해서 메모리를 효율적으로 운영할 수있는 것이다. DLL을 사용하는 프로그램이 없어지면, 곧바로 메모리로부터 언로드되지만, DLL를 사용하고 있는 프로그램이 있다면, 메모리에 남아 있게 된다.(내부적으로는 카운터를 유지)
단순히 생각하면 되고, 실제적인 처리는 링커가 처리해준다. 링커가 실행파일을 생성할 경우 DLL의 주소공간을 결정해야만 한다. 왜 이런 주소공간의 결정이 어려운가 하면 , DLL로부터 함수부분을 추출해서 실행파일을 생성할 수 없기 때문에, 실행시에 DLL로부터 함수 부분을 추출해서, 실행파일의 주소공간에 배치해야 하므로, DLL의정보를 실행파일에 포함시켜야 한다.
정보라는 것이 임포트 라이브러리에 들어있다. 링커는 임포트 라이브러리로부터 필요한 정보를 추출해서, 실행파일에 「정보」를 포함시킨다. 이런 구조로 인해, Winodws 로더가 프로그램을 메모리에 로드했을 경우, 프로그램이 DLL을 링크한다는 것을 알 수 있으므로, DLL을 찾아내고 , DLL의 함수를 사용할 수 있도록 주소공간을 매핑 하게 된다.
또한, Windows 로더의 도움을 받지 않고도 DLL을 로드하는 방법도 있지만, 이 방법은 다음과 같은 것을 감안해 두고 사용하기 바란다.
- 이 경우 임포트 라이브러리는 필요없기 때문에, 임포트 라이브러리의 정보없이 DLL을 사용할 수 있다. 다만, DLL의 함수에 대해서, 어떤 인수를 취하는지와, 어떤 함수명인지를 미리 알고 있어야 사용이 가능하다.
- 로더가 프로그램을 로드할 때, 만약 프로그램이 사용하는 DLL이 발견되지 않았으면, 「지정된 xxxxx.dll을 찾을 수 없습니다」라는 에러 메세지가 표시되고, 비록 해당 DLL이 없어도 프로그램은 동작하는 것일지라도 강제로 프로그램이 종료하게 된다.
- 함수에 대한 정보를 충분히 가지고 있는 경우, 임포트 라이브러리에 포함되지 않은 함수를 호출할 수 있다.
- 만약에 LoadLibrary 함수를 사용한다면, 함수를 호출할 때 마다 GetProcAdress 함수를 호출 해야만 한다. GetProcAddress 함수는 DLL파일 속에서 함수의 엔트리 포인트를 얻는다. 그렇기 때문에, 프로그램이 약간커지게 되지만,지장을 줄 정도는 아니다. (가장 많이 사용하는 방식이다)
LoadLibrary 함수를 호출 하는 방법에 대한 장단점을 알았다면, 이번에는 어떻게 DLL을 생성하는지를 설명한다. DLL를 생성하는 표준코드는 다음과 같다.
;-------------------------------------------------------------------------------------- ; DLLSkeleton.asm ;-------------------------------------------------------------------------------------- .386 .model flat, stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib .data .code DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax, TRUE ret DllEntry Endp ;--------------------------------------------------------------------------------------------------- ; 이것은 더미함수로서 , 아무런 일도하지 않는다 ; 단지, DLL에 함수를 저장하려면 어디를 수정하면 되는지를 설명하기 위해서 사용하고 있다 ;---------------------------------------------------------------------------------------------------- TestFunction proc ret TestFunction endp End DllEntry ;------------------------------------------------------------------------------------- ; DLLSkeleton.def file ;------------------------------------------------------------------------------------- LIBRARY DLLSkeleton EXPORTS TestFunction모든 DLL은 엔트리 포인트 함수를 가져야 한다. Windows는 엔트리 포인트 함수를,
- DLL이 로드 될경우
- DLL이 언로드될 경우
- thread와 같은 프로세스로 생성될 경우
- 그리고 thread가 파괴 될 경우
에 호출하도록 되어 있다(대부분은 초기화와 같은 역할).
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax, TRUE ret DllEntry Endp엔트리 포인트 함수명은 ENDP<Entrypoint function name> 와 짝을 이루며, 어떤 이름이라도 사용할 수 있다. 이 함수는 3개의 인수를 취하고, 처음 2개의 인수가 중요하다.
- hInstDLL은 DLL모듈의 핸들이다. 프로세스의 인스턴스핸들과는 유사하다, 이후에 다시 사용할려면 이 값을 어디엔가 저장해 두어야만 한다.
- reason은 다음의 4개중 하나로 설정한다.
- DLL_PROCESS_ATTACH
가장 먼저 프로세스의 주소공간에 저장되었을 때, DLL은 이 값을 받게 되어 있다. 이 상태에서 초기화를 수행할 수 있다.- DLL_PROCESS_DETACH
프로세스의 주소공간으로 부터 언로드되었을 때에, DLL은 이 값을 받는다. 이 때에 메모리를 해제하는 등의 클린업 코드를 수행한다.- DLL_THREAD_ATTACH
프로세스가 새로운 thread를 생성했을 때에 받는다.- DLL_THREAD_DETACH
프로세스의 thread가 파괴 되었을 때에 받는다.DLL에 대한 처리를 한후에, eax 레지스터에 TRUE를 리턴 하면 좋다. FALSE를 리턴 하면, DLL은 로드 되지 않은 것이다. 예를 들면, 초기화 코드는 메모리에 확보되지만 만약에 제대로 실행되지 않았다면, 엔트리 포인트 함수는 DLL을 실행하지 못했다는 것을 나타내기 위해 FALSE를 반환 할 것이다.
DLL에 저장하고 싶은 함수는 엔트리 포인트 함수의 뒤에서도 정의 할 수 있지만, 다른 프로그램에서 호출 할 수 있도록 하고 싶다면, 모듈 정의 파일(. def 파일)의 export 리스트(외부 프로그램을 사용할 수 있는 함수 리스트)에 함수명을 기입해야 한다.
DLL의 개발 과정에서, 모듈 파일이 필요하게 된다면 다음과 같이 기술 할 수 있다.
LIBRARY DLLSkeleton EXPORTS TestFunction처음의 LIBRARY는 DLL의 내부 모듈명을 정의하고 , DLL 파일명과 일치해야 한다.
두번째행의 EXPORTS는 링커에게 DLL의 어떤 함수가 export 되고 있는지를 지정한다. 즉 다른 프로그램에서 이 이름으로 호출할 수 있는 것이다. 이 예에서는, TestFunction이라는 함수를 다른 프로그램에서 호출하기 때문에, EXPORTS에 그 함수명을 입력했다.
link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def/LIBPATH:c:\masm32\lib DLLSkeleton.obj어셈블러의 옵션은 위와같이, /c /coff /Cp 지만, 오브젝트 파일을 링크 하게되면 .dll 파일과 .lib 파일이 생긴다. .lib 파일은, DLL의 함수를 사용하고 싶은 다른 프로그램과 링크 하기 위해 사용하는 임포트 라이브러리이다.
;--------------------------------------------------------------------------------------------- ; UseDLL.asm ;---------------------------------------------------------------------------------------------- .386 .model flat, stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib .data LibName db "DLLSkeleton.dll", 0 FunctionName db "TestHello", 0 DllNotFound db "Cannot load library", 0 AppName db "Load Library", 0 FunctionNotFound db "TestHello function not found", 0 .data? hLib dd ? ; the handle of the library (DLL) TestHelloAddr dd ? ; the address of the TestHello function .code start: invoke LoadLibrary, addr LibName ;--------------------------------------------------------------------------------------------------------- ; 로드하고 싶은 DLL 파일명을 지정해서 LoadLibrary 함수를 호출한다. 만약 성공하면 ; DLL의 핸들이 반환되지만, 실패한다면NULL이 반환된다. ; DLL의 핸들을 GetProcAddress 함수나 다른 핸들을 필요로 하는 함수의 인수로 넘겨서 ; DLL에 대한 다양한 처리를 할 수 있다 ;------------------------------------------------------------------------------------------------------------ .if eax==NULL invoke MessageBox, NULL, addr DllNotFound, addr AppName, MB_OK .else mov hLib, eax invoke GetProcAddress, hLib, addr FunctionName ;------------------------------------------------------------------------------------------------------------- ; DLL의 핸들을 얻었으므로, DLL에서 호출하고 싶은 함수의 포인터를 얻기위해, ; 현재 얻은 DLL의 핸들과 그 함수명을 인수로 해서 GetProcAddress 함수를 호출한다. ; 성공적으로 수행되면 함수의 포인터가 반환되고, 실패한다면 NULL이 반환된다. ; DLL를 언로드하거나 리로드 하지 않는다면 주소는 변경되지 않기 때문에, ; 미래를 대비해서 주소를 글로벌 변수에 저장해 둔다 ;------------------------------------------------------------------------------------------------------------- .if eax==NULL invoke MessageBox, NULL, addr FunctionNotFound, addr AppName, MB_OK .else mov TestHelloAddr, eax call [TestHelloAddr] ;------------------------------------------------------------------------------------------------------------- ; 다음으로,인수에 함수의 포인터를 지정해서, 함수를 호출한다(함수의 포인터라 해서 특별한 것은없다) ;------------------------------------------------------------------------------------------------------------- .endif invoke FreeLibrary, hLib ;------------------------------------------------------------------------------------------------------------- ; DLL이 필요없다면, FreeLibrary 함수로 언로드할 수 있다 ;------------------------------------------------------------------------------------------------------------- .endif invoke ExitProcess, NULL end start이상으로 알수 있듯이, LoadLibrary 함수를 사용하는 것은 조금 복잡하기는 하지만, 보다 효율적으로 메모리를 사용할 수 있다.