1.
프로그래밍을 하다 보면 단독 실행이 가능한 EXE 형태의 프로그램만 만드는 게 아니라, 다른 프로그램에 부속물로 붙거나 여러 프로그램들 사이에서 공유되는 라이브러리, 플러그 인 같은 걸 만들 때가 있다.
플러그 인 정도야 호스트 프로그램이라도 분명하게 존재하니 양반이지만, 임의의 프로토콜을 갖는 공용 라이브러리는 static LIB이든 DLL이든, 그 자체로 단독 실행이 가능하지 않다. 그렇다 보니 그 라이브러리를 사용하는 프로그램을 또 별도로 만들어야 해서 테스트와 디버깅이 여러 모로 불편하다.
그래서 Windows에서 DLL을 만드는 솔루션의 경우, 그 솔루션에다가 DLL을 테스트하는 간단한 EXE도 프로젝트로 따로 만드는 게 보통이다.
Visual C++은 지난 2005부터인가 프로젝트를 새로 생성할 때, 솔루션 디렉터리 아래에 동명의 프로젝트 디렉터리가 한 단계 더 생기고, 한 솔루션에 소속된 프로젝트들의 생성물은 다 동일한 output 디렉터리에 만들어지도록 기본 동작 방식이 바뀌었다. obj 같은 임시 파일들만이 프로젝트별로 자기 고유한 위치에 생성된다. 이것은 나름 바람직한 조치라 여겨진다.
2003 이하 | 2005 이상 |
프로젝트1\Release\프로젝트1.exe 프로젝트1\Release\프로젝트1.obj 프로젝트1\프로젝트2\Release\프로젝트2.dll 프로젝트1\프로젝트2\Release\프로젝트2.obj |
솔루션\Release\프로젝트1.exe 솔루션\Release\프로젝트2.dll 솔루션\프로젝트1\Release\프로젝트1.obj 솔루션\프로젝트2\Release\프로젝트2.obj |
그런데, 발상을 전환하면 DLL을 생성하는 소스를 기반으로 곧바로 EXE를 만들어 DLL의 함수들을 의외로 굉장히 간편하게 테스트를 할 수 있다.
링커의 SUBSYSTEM 옵션 하나만 바꿈으로써 WinMain을 사용하는 GUI 프로그램과 main을 사용하는 콘솔 프로그램을 곧바로 전환할 수 있듯, EXE와 DLL은 똑같이 PE 헤더가 있는 실행 파일이며 본질적인 차이가 거의 없다. 구조체 필드 값이 일부 차이가 나고 entry point에서 같이 전달되는 인자의 타입이 다를 뿐이다.
DLL 프로젝트에서 configuration을 하나 만든다. 테스트와 디버그가 목적이므로 Debug 빌드 것을 초기값으로 가져오면 되겠다. configuration 이름은 Debug EXE 정도로 하자.
그 뒤 프로젝트 속성의 General (일반)으로 가서 Target Extension (대상 확장명)은 .dll이던 것을 당연히 .exe로 바꾼다.
그리고 제일 중요한 Configuration Type (구성 형식)을 Dynamic Library (.dll)이던 것을 Application (.exe)으로 바꾼다.
'확인'을 누른 뒤, DLL 소스의 한구석엔 원래의 DLL엔 없던 WinMain 내지 main 함수를 추가하고, 그 안에다 호출하고 싶은 DLL 클래스/함수들을 마음껏 사용하며 테스트한다.
이것만 해 주면 끝이다. 프로젝트를 이 configuration대로 빌드해서 돌리면 된다.
별도의 EXE를 따로 만들어서 테스트를 하는 거라면 그 EXE에 또 테스트 대상 DLL을 로딩하는 코드가 추가되어야 하지만 DLL 자체의 소스로부터 EXE를 생성하면 그런 번거로운 절차가 필요하지 않으니 더욱 좋다. EXE 자체에 DLL의 코드가 그대로 포함되기 때문이다.
static LIB을 만드는 프로젝트도 이런 식으로 별도의 EXE 생성 configuration을 만들어서 테스트가 가능할 것이다.
다만 DLL/EXE와는 달리 static LIB는 링크 절차가 존재하지 않고 그냥 컴파일만 가능하면 라이브러리 파일이 만들어지기 때문에 이로부터 온전한 EXE를 만들려면 추가적인 링커 설정 같은 게 필요할 것으로 보인다.
2.
여담이다만 DLL뿐만 아니라 EXE도 DLL처럼 export 심벌을 가질 수 있으며 그걸 GetProcAddress를 통해 얻어 올 수 있다.
EXE만 자신이 로딩한 플러그 인 DLL로부터 함수를 얻어 오는 게 아니라, DLL 역시 자신을 로드한 EXE로부터
GetProcAddress( GetModuleHandle(NULL), "GetHostInfo") 이런 식으로 코드를 얻을 수 있다. 이것도 참 기발한 발상이 아닐 수 없다. 어디 활용할 데가 없을까?
내가 개인적으로 굉장히 놀란 것은, 저렇게 한 프로세스 공간의 주인 역할을 하는 EXE가 아니라..
완전히 다른 EXE를 로딩해서 거기에 있는 코드를 실행하는 것도 가능하다는 것이다. EXE는 보통 0x400000 같은 고정된 주소에 로드되며 재배치 정보가 존재하지 않기 때문에 자기 위치에 로드가 못 되면 로딩이 실패한다.
그런데 자신과 로드 주소가 겹치는 EXE도 LoadLibrary를 하면 일단 작업이 성공하며 리소스 추출뿐만이 아니라 GetProcAddress도 실행 가능한 듯하다. 이쯤 되면 EXE와 DLL의 경계가 어찌 되는지가 궁금해진다.
3.
아무 중간 계층 없이 C/C++ 언어만으로 뭔가 라이브러리를 남에게 제공하는 건 애로사항이 적지 않다.
- 디버그 or 릴리스?
- 32 or 64비트?
- 최종 형태는 DLL or LIB?
- VC++ 어느 버전? (보안 기능 링크 에러)
- 사용하는 CRT의 형태는 DLL or static?
이런 식으로 상호 일치해야 하는 변수가 급격히 늘어나기 때문이다. 조건부 컴파일이 괜히 필요했던 게 아니다.
C/C++ 런타임 라이브러리도 비주얼 C++의 버전이 바뀜에 따라 내부적으로 야금야금 더해지고 바뀌는 기능이 있기 때문에--특히 보안 관련-- static 링크하는 경우 빌드 툴의 버전이 안 맞으면 이상한 심벌명에서 링크 에러가 나고 각종 문제가 생기기 쉽다.
그나마 같은 비주얼 C++끼리이니까 망정이지 서로 다른 컴파일러끼리 C++ 클래스 라이브러리를 공유한다면 name decoration까지 문제가 됐을 것이다. 사실상 공유 불가능이다.
옛날에는 문자 집합의 크기(일명 유니코드/ANSI)조차도 변수가 따로 있었을 정도이지만 요즘은 그래도 유니코드, 정확히는 wide string만 고려하면 되니 그건 그나마 나아졌다.
이 문제가 워낙 복잡하니..
일차적으로는 COM 같은 바이너리 표준이 나왔을 것이다.
아니면 그냥 소스 코드를 통째로 넘겨줘서 필요한 사람이 알아서 빌드해서 쓰게 하든가. 그 라이브러리가 애초부터 오픈소스 진영의 작품이라면 다행이지만, 상업용 코드라면 인터페이스 부분을 제외한 나머지에다가는 난독화 처리가 필요할 것이다.
그것도 싫으면 저런 골치아픈 요소들을 싹 잊어버리고 자바/C# 같은 바이트코드 기반으로 가는 수밖에 없는데... 그건 물론 성능은 COM보다도 엄청나게 더 희생시킨 귀결일 것이다.
그래도 아무 클래스에나 public static void Main만 있으면 그게 곧 실행 가능한 물건이고 빌드 속도도 안드로메다 급으로 빠르며 골치 아픈 32/64비트 구분 같은 것도 없는 환경이.. C++ 프로그래머로서 참 부럽게 느껴질 때가 있다.
Posted by 사무엘