윈도우 운영체제에는 잘 알다시피 DLL이라는 게 있어서 한 프로세스를 구성하는 코드들을 여러 모듈로 분할할 수 있고, 반대로 동일한 코드를 여러 프로세스가 동시에 효율적으로 공유할 수 있다. 유닉스 계열의 운영체제에는 이와 비슷한 개념으로 shared library (*.so)라는 게 있다.
윈도우 DLL은 주소 공간이 해당 DLL을 로딩한 프로세스(EXE)에 완전히 종속된다. 그렇기 때문에 DLL이 사용하는 코드는 공유될지언정, 그 DLL이 선언하는 전역변수 같은 건 EXE마다 제각각이며, 심지어 동일 EXE를 여러 번 실행하더라도 제각각이다. 이것은 유닉스의 shared library와 다른 점이다. 윈도우에서 진짜 여러 프로세스들이 공유할 수 있는 메모리를 만들려면 내부적으로 공유되는 메모리 섹션을 별도로 생성해 놔야 한다.
이런 차이로 인해 특별히 조심해야 할 점이 있다. 한 EXE 안에서 3개의 DLL이 돌아가고 있고, 이들은 모두 동일한 버전의 비주얼 C++로 개발되어 동일한 CRT 라이브러리를 사용한다고 치자. 만약 이들이 모두 CRT를 DLL로 링크하여 동일한 CRT 라이브러리의 코드와 데이터를 공유한다면, 어느 모듈에서 malloc/new한 메모리를 다른 모듈에서 곧바로 free/delete해도 된다.
그러나 이들이 제각각 CRT를 static link했다면 그럴 수 없다. 한 모듈에서 할당한 메모리는 반드시 동일한 모듈에서만 해제해야 한다. 비록 메모리를 할당하고 해제하는 코드는 실질적으로 동일하지만 해당 코드가 동작하는 context는 모듈들이 모두 서로 다르기 때문이다. 모듈간에 메모리 할당과 해제를 자유롭게 하려면 각 모듈들은 자신만의 메모리 할당/해제 함수를 외부에다 별도로 제공해 줘야 한다.
DLL을 만드는 일은 64비트 운영체제가 등장하면서 다소 까다로워졌다. 32비트와 64비트 사이에는 DLL의 교차 로딩이 되지 않기 때문이다. 즉, 32비트 모듈(EXE/DLL)은 64비트 DLL을 읽을 수 없으며, 반대로 64비트 모듈은 32비트 DLL을 읽을 수 없다. 다시 말해, x64 운영체제에서도 x86 EXE를 아무 차이 없이 실행은 할 수 있지만, 그래도 64비트 EXE가 32비트 DLL을 내부적으로 읽을 수는 없으므로 착오 없기 바란다.
한 주소 공간은 전부 32비트 아니면 전부 64비트뿐이지, 두 종류의 코드가 섞여 있을 수가 없어졌다. 옛날과는 달리 지저분한 썽킹 계층 같은 게 없다. 동일 코드를 32비트와 64비트용으로 제각기 빌드해서 다같이 내놓아야 한다.
그러니 태생적으로 반드시 네이티브 코드 DLL을 만들어야 하는 컴포넌트/미들웨어/드라이버 분야라든가, 훅킹 내지 운영체제의 shell extension을 만드는 쪽은 다른 어떤 분야보다도 진작부터 64비트 프로그래밍이 필요했다. 사실은 IME 쪽도 예외가 아니다.
그런데 문제가 있다. 지금까지 DLL은 코드를 담는 용도로만 쓰여 온 게 아니라는 점이다. 잘 알다시피 윈도우 운영체제는 실행 파일의 포맷 차원에서 리소스라 하여 나름 계층을 갖춘 custom 데이터를 간편하게 불러오는 기능이 있다. 일일이 메모리 할당을 할 필요가 없이, 운영체제가 알아서 메모리 주소를 잡아서 로딩해 주고, API 함수 호출 한 번에 데이터를 곧장 찾아 주기까지 하니, 상당히 편리하다. 그래서 데이터 리소스만 갖춘 DLL도 엄청 많이 쓰였다.
특히 다국어 지원을 위한 외국어 리소스를 집어넣는 데 DLL보다 더 좋은 수단은 지금까지 별로 없었다. 운영체제의 대화상자 내지 string table 같은 주요 리소스 파일의 포맷은 32비트나 64비트나 바뀐 게 없다. 호환성 차원에서 일부러 동일하게 유지시킨 듯하다. 비주얼 스튜디오 200x의 MSDN(Document Explorer)이 사용하는 *.hxs 파일도 리소스 전용 DLL이다.
그런데 DLL은 태생적으로 컴퓨터 기계 종류에 지극히 종속적인 코드를 담는 게 주 목적인 파일 포맷이다. 그렇다면 기계 종류에 관계없이 동일한 데이터를 담는 DLL도 기계별로 일일이 다 따로 만들어야 할까?
그렇지는 않다. 데이터밖에 들어있지 않은 리소스 전용 DLL을 위해서 운영체제는 다음과 같은 배려를 하고 있다.
먼저, DLL을 읽을 때 흔히 사용하는 LoadLibrary 대신, LoadLibraryEx 함수는 LOAD_LIBRARY_AS_IMAGE_RESOURCE 같은 플래그를 제공하여 DLL의 코드 부분을 전혀 감안하지 않고 데이터 부분만 읽게 할 수 있다. 이렇게 하면 해당 DLL은 DllMain 함수 자체가 실행되지 않으며, 자신과 호환되지 않는 기계를 타겟으로 만들어진 DLL도 읽을 수 있다. 물론, FindResource 같은 함수로 리소스 추출만 가능할 뿐, GetProcAddress 같은 기능을 사용할 수는 없다.
다음으로, DLL 자신이 리소스 전용 DLL일 뿐이라고 명시해 줄 수 있다. 비주얼 C++의 DLL 프로젝트에서 링커 옵션을 보면 /NOENTRY가 있다. 이 옵션이 지정되면 그 DLL에는 아무 코드도 삽입되지 않으며, 진입점인 DllMain 함수 자체가 없는 리소스 전용 DLL임이 인증된다. 이런 DLL은 외부에서 그냥 대놓고 LoadLibrary로만 호출해도 기계 종류에 관계 없이 읽을 수 있다.
윈도우는 공유 라이브러리를 다루는 개념이 유닉스 계열과 차이가 있을 뿐만 아니라 실행 파일을 로딩하는 방식에도 개념적인 차이가 있다. 윈도우는 position-dependent, 즉, 주소 종속적인 방식이다. 윈도우의 EXE와 DLL에는 preferred address라는 게 있어서, 이 메모리 주소에 딱 로딩이 되면 아주 성능이 좋지만, 그 주소에 로딩이 못 되면 재배치 페널티가 따르는 방식을 사용한다. 재배치 작업을 어떻게 하면 되는지에 대한 재배치 정보가 reloc이라는 섹션에 있다.
EXE야 Win32s 같은 과거의 열악한 환경이 아닌 이상, 자신만의 고유한 주소 공간이 있기 때문에 언제나 preferred address에 로딩된다는 보장이 있다. 그에 반해 남의 주소 공간에 달라붙는 형태인 DLL은 그렇지 못하기 때문에 일반적으로 반드시 reloc 섹션이 필요하다.
다만, 이런 재배치 작업도 코드가 없는 리소스 전용 DLL에는 전혀 해당사항이 없다. 리소스 내부에 DLL의 메모리 주소에 종속적인 숫자 데이터 같은 게 있을 리가 없기 때문이다.
그러니 이론적으로 리소스 전용 DLL은 reloc 정보를 생략해 버려도 괜찮지만, 그냥 대놓고 생까-_- 버리는 것도 곤란하다. 윈도우 2000 미만의 옛날 운영체제의 경우 DLL을 preferred base에다 로딩을 못 하는데 재배치 정보마저 없다면, 원래 재배치를 할 게 없는 리소스 전용 DLL임에도 불구하고 그냥 일방적으로 로딩을 거부해 버리게 된다.
재배치를 안 해도 괜찮은 DLL이라는 별도의 플래그는 윈도우 2000 이상에서부터 도입되었다. 물론, 애당초 기계어 코드가 들어있지 않은 DLL의 reloc 섹션에 의미 있는 재배치 정보 자체가 있을 리가 없다. 그냥 껍데기뿐인 잉여인 것이다.
이렇듯, DLL은 원래 코드를 공유하려고 만들어졌지만 기계와는 무관한 리소스나 데이터를 공유하는 container 역할도 결코 무시할 수 없는 위치에 있다. 심지어 트루타입(TTF) 글꼴이 도입되기 전에 윈도우에서 쓰이던 비트맵/벡터 글꼴인 FON 파일은 16비트 형태의 리소스 전용 DLL로, 오늘날의 64비트 윈도우에서도 그 잔재가 남아 있다!
그래서 운영체제가 리소스 전용 DLL에 대해서는 예외적으로 기계를 가리지 않고 읽을 수 있고, 또 그런 DLL에는 불필요한 정보를 합법적으로 생략도 할 수 있게 별도의 배려를 해 왔음을 우리는 윈도우 API의 변천사를 통해 알 수 있다.
그러고 보니, 32/64비트 응용 프로그램이 각각 32/64비트에 해당하는 윈도우 시스템 디렉터리나 각종 프로그램 디렉터리를 얻어 오고 접근하는 법도 꽤 복잡해져 있는데, 이건 다음 기회에 다루도록 하겠다.
Posted by 사무엘